Commit Graph

14 Commits

Author SHA1 Message Date
Teknium fe615a0099 fix(desktop): republish the connections registry to renderers after every successful save (#95393)
Live-confirmed on the Phase B build: hermesDesktop.connections.save()
succeeds and the registry on disk gains the row, but the switcher menu
(fed by the renderer $connectionsRegistry snapshot) keeps painting the
stale list until reload. remove() already broadcasts
hermes:connections:changed; save() only did so on the dial-material-edit
branch, so a brand-new connection or a label rename never reached the
switcher's onChanged re-pull (or any other window).

Fix at the publish seam only: saveRegistryConnection now broadcasts a new
'saved' reason for every successful save that isn't a dial-material edit.
'saved' is a pure registry-refresh signal — the use-gateway-boot listener
explicitly ignores it (nothing moved, so no dispose/redial/forget), while
the switcher's existing onChanged listener re-pulls the snapshot.

Tests:
- electron/hardening.test.ts pins both broadcast branches in
  saveRegistryConnection (source-assertion pattern; main.ts has no exports).
- connection-switcher.test.tsx mirrors the live repro scenario
  (/tmp/mg-ab/w2_95393.py): menu before save lacks the row, Electron's
  'saved' push arrives, menu after — without reload — shows it.
2026-08-26 04:49:22 -07:00
Brooklyn Nicholson c5bf3b7ce3 test(desktop): replace apply/liveness source-regex assertions with behavior tests
The salvaged hardening tests matched main.ts source text to assert that
fetchConnectionStatus reaches for a bearer and that Apply preflights before
persisting. A rename breaks them while a real auth regression that keeps the
substrings passes.

Make the preflight a first-class option on applyConnectionConfigAtomically so
its ordering is observable, and assert it through the seam: preflight runs
before either write, and a rejected preflight leaves both stores and the
activation untouched.
2026-08-23 20:26:46 -05:00
Alessandro Levantini 96e5898542 fix(desktop): synchronize applied gateway registry 2026-08-23 20:26:46 -05:00
hermes-seaeye[bot] ecc7a354f0 fmt(js): npm run fix on merge (#88981)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-18 07:11:16 +00:00
Christopher Strnad 4bb7e4913f fix(desktop): preserve cross-machine group routing 2026-08-18 00:03:16 -07:00
张豪杰 7e151bd9d3 fix(desktop): create connection.json owner-only
`connection.json` under the desktop app's Electron `userData` was written with no
file mode, so it landed at the `0644` umask default — while its two
credential-bearing neighbours in the same directory, `desktop-installation.json`
and `native-oauth-tokens.json`, were already `0600`. That file holds the
safeStorage-encrypted gateway token plus the fields that are NOT encrypted: the
gateway URL and the SSH host, user, and key path.

- Route the single write choke point through a helper that creates the file
  owner-only and atomically.
- Tighten an already-existing `0644` file once per launch on the read path, so
  installs that already have one do not stay world-readable until the next save.
- Refuse to act on a path that is a symlink or not owned by the current user,
  matching the guards `desktop-installation.ts` already applies to its sibling.

The symlink guard alone turned out to be insufficient, and that is worth
recording: `writeSecretFileAtomic` tightens its *temp* path, so a symlink planted
at `connection.json.tmp` meant `writeFileSync` followed it, the guard correctly
bailed, and `renameSync` then moved the link onto `connection.json` permanently.
Measured, guard-only vs. as-landed:

    guards only          token leaked: true    config is a symlink: true   755
    guards + temp unlink token leaked: false   config is a symlink: false  600

So the temp path is unlinked before the write.

Issue #77486's headline claim — that a dashboard session token is persisted in
plaintext — does not hold against main. The token has been safeStorage-encrypted
since the desktop app reached mainline in 51c68d4ab, and `encryptDesktopSecret`
aborts with an actionable message rather than degrading to plaintext when
safeStorage is unavailable. The `{ encoding: 'plain', value }` literal does exist
at main.ts:7084, but only on the `persistToken: false` branch, whose sole caller
is the connection-test handler, which never writes. So no mainline path *writes*
a plaintext token. The commits that did contain a plaintext-writing fallback
(d3d177283, d208f2c2c) are not ancestors of main — they live only on
upstream/bb/gui-* and the desktop-pr20059-installers pre-release tag.

At-rest migration of legacy non-safeStorage payloads is deliberately NOT included.
An earlier revision of this branch implemented it and it was removed after review
reproduced two token-loss paths: it force-converts the opt-in plaintext choice
PR #62319 adds (silently reverting the user's decision, then destroying the token
on the next launch without the `--password-store=basic` flag), and it converts a
portable credential into a keychain-bound one with no consent — destroying the
only recoverable copy while not remediating the real exposure, since every
existing backup still holds the plaintext and the true remedy is rotation. It also
persisted raw `parsed`, bypassing `sanitizeConnectionProfiles`. A comment at the
read path records the three preconditions any future attempt needs.

`decryptDesktopSecret`'s non-safeStorage read fallback is untouched — it is what
lets a pre-release or hand-edited config work at all.

Windows still inherits the userData directory ACL rather than an explicit
owner-only one; mode bits are advisory there, so that half is deferred to
PR #77527 rather than growing a second ACL implementation here.

e2e: `at-rest-connection-token.spec.ts` asserts the at-rest contract
implementation-independently — the token's plaintext value (and its base64 form)
must not appear in a raw-bytes scan of any file under userData or HERMES_HOME,
AND the app must still put the exact original token on the wire after a restart,
so a fix that simply drops the token cannot pass. Proven non-vacuous by mutation:
writing `{ encoding: 'plain', value }` still fails the scan while the
file-exists and gateway-URL guards pass. The migration case is a documented
`test.fixme` naming its three blockers.

Electron project 928 -> 924 tests (-9 migration, +5 new guard and
mechanism-isolation). Two of those five exist because reverting either owner-only
mechanism alone initially scored zero failures — they were masking each other, so
either could have been deleted green.

(cherry picked from commit 6e01add6578f08f015a567d3a7a7378f2ec3e768)
2026-08-12 22:38:17 -07:00
Teknium 4c8e326500 Merge PR #62319 (keyring-less Linux token storage) onto current main 2026-08-12 16:38:35 -07:00
Brooklyn Nicholson 254aeda122 fix(desktop): dedicated reader for large remote attachments
Keep Settings-configurable preview/image loads on readFileDataUrl, and
route remote non-image attach through a 256 MiB IPC so uploads are not
stuck on the 16 MiB default after #73221.

Co-authored-by: Börje <borje@dqsverige.se>
2026-07-28 19:20:14 -05:00
Adolanium b5dc471152 feat(desktop): make attachment data-URL size limit configurable (#73221)
Hard 16 MB cap on readFileDataUrl blocked larger local attaches with no way to raise it. Settings -> Chat now has a free-form MB field. Main process owns the persisted value and clamps only absurd inputs.
2026-07-28 09:10:21 -04:00
Zeus-Deus af400e1d1c test(desktop): pin the plain text opt-in propagation and the basic store startup
Review feedback asked for regression coverage of the main process pieces:
the connection-config save and apply IPC path that carries
allowPlainTextToken down to encryptDesktopSecret, and the Linux
--password-store=basic startup branch.

main.ts has no exports, so both pieces now live as small injected helpers
in hardening.ts next to encryptDesktopSecret. The whenReady block became
enableBasicPasswordStoreEncryption, which only acts on linux with the
exact basic switch value, tolerates a missing or throwing
setUsePlainTextEncryption, and reports whether it actually flipped the
flag. The token persistence ternary became resolvePersistedRemoteToken,
which owns the strict opt-in coercion in one place: a truthy value that is
not exactly true never enables plain text storage. main.ts passes the raw
payload field through, so the strictness itself is what the tests pin.

hardening.test.ts grows behavioral cases for both helpers, including the
full path through the real encryptDesktopSecret for the opt-in, the
never downgrade rule when the keyring is available, and the transient
test connection passthrough. The wiring inside main.ts (save and apply
routing through coerceDesktopConnectionConfig, the raw field handoff, the
startup call ordered before createWindow, and the secureTokenStorage and
remoteTokenPlainText fields in the sanitized response) is pinned with the
repo's source assertion pattern.
2026-07-19 20:25:34 +02:00
Zeus-Deus e879d133da fix(desktop): allow remote gateway token storage on keyring-less Linux
On Linux without a Secret Service keyring (e.g. Hyprland/Sway with no
GNOME Keyring or KWallet), safeStorage.isEncryptionAvailable() is false,
so saving a remote gateway session token from Settings -> Gateway failed
hard with no in-app way forward.

- encryptDesktopSecret gains an explicit allowPlainText opt-in: when
  secure storage is unavailable and the user confirmed the prompt, the
  token persists as { encoding: 'plain' } in connection.json (which
  decryptDesktopSecret already round-trips).
- Settings -> Gateway now surfaces the opt-in: a destructive confirm
  dialog before persisting a token in plain text, and a persistent
  warning banner while the saved token is stored unencrypted. Localized
  in en/ja/zh/zh-hant.
- The connection-config IPC response reports secureTokenStorage and
  remoteTokenPlainText so the renderer can drive both affordances.
- Launching with --password-store=basic now works: on Linux the app
  calls safeStorage.setUsePlainTextEncryption(true) at startup when the
  switch is set, which Electron requires for the basic backend to count
  as available.
- The no-opt-in error now spells out all three remedies (enable an OS
  keyring, confirm plain-text storage, or use HERMES_DESKTOP_REMOTE_URL/
  HERMES_DESKTOP_REMOTE_TOKEN).

Fixes #62294
2026-07-19 20:25:03 +02:00
ethernet 955c5b73c5 test(desktop): move node tests to vitest as well 2026-07-13 17:22:17 -04:00
ethernet 56a8e81d33 cleanup(desktop): npm run fix for fmtting
we should run this as part of merges at some point :)
2026-07-08 16:24:16 -07:00
ethernet 39d09453f9 feat(desktop): ts-ify everything 2026-07-08 16:24:16 -07:00