fix: the UI kept reporting successes that never happened - #1
Merged
Merged
Conversation
Replace the Tauri/Vite scaffold artwork with the actual LanBeam identity: every app icon size, the .icns/.ico bundles, the Windows Store logos, and the favicon. Also adds a macOS menu-bar TEMPLATE icon, rasterised from the brand kit's tray mark. macOS template icons are alpha-only — the system paints them black on a light menu bar and white on a dark one — so the per-arc opacity of the mark is baked into the alpha channel rather than into a colour. BrandMark renders the logo as an inline SVG rather than an <img>: a real element can take the theme's colours, and `pointer-events: none` keeps it from eating the window's drag region. The design source folder stays out of the repo; the assets actually derived from it and shipped are versioned under assets/brand.
An audit of every falsifiable claim in the code turned up almost no crashes.
Nearly every real defect was the same shape: something reported a success that
had not happened, or asked for a confirmation that could not be given.
And the tests pinned the lies. `loopback_pairing_mutually_trusts...`,
`untrusted_text_..._is_dropped_but_acked`, and "migrates trust to the new key
when the codes match" each NAMED and guarded the broken behaviour. A green
suite meant nothing.
--- Security model ---
Pairing granted trust inside the handshake, before any human compared the SAS
-- and only the HOST could see that SAS: the joiner got it back from
`join_by_code` and threw it away. A code only one side can read is not a check;
it is a formality that ends in a yes. Both ends now show the same SAS, and
trust is recorded only once a person confirms it, through the same
`set_trusted` path the trust circle uses. So a paired device is trusted AND
auto-accepting, exactly like one dragged into the ring, and the two notions of
trust can no longer drift apart. `trust::grant()` is deleted outright: leaving
a backend-side "trust without asking" helper lying around is leaving a door for
someone to wire it back up.
The fingerprint-changed alert offered to "re-verify and restore trust" by
dialling the suspect key and showing the SAS that handshake produced -- while
the other device displayed nothing at all. There was no second number to
compare, so pressing "they match" migrated trust onto the very key the alert
existed to make you suspicious of. That flow is gone. The alert reports, and
offers the two honest exits: delete the old record, or pair with the new key
like the new device it is (pairing is the only flow that puts the same code on
both screens).
It also fired on a NAME match -- and the default device name is the machine's
hostname, so two un-renamed installs raised an impersonation alarm at each
other. A name now only counts when it is unambiguous AND worn by a device you
do not already know.
You could file YOURSELF as a peer by dialling your own address. A self-entry is
uniquely un-removable: discovery drops its own announces, so it can never be
expired away. Refused on every path, filtered out of the device list, and
evicted from the trust store at startup.
--- Things that could not be deleted ---
`forget_device`: deleting a device now drops its trust row AND the
manually-added address that kept it in the list. `remove_trusted` was all the
UI had, so "delete" cleared the trust row while the peer sat untouched in the
manual address table -- back on the very next list, with nothing anywhere able
to take it out.
--- Quiet failures ---
- A checksum mismatch deleted the WHOLE batch, verified siblings included,
while the message spoke of one file. A file that passed its digest is
finished; a sibling's bad hash says nothing about it.
- Quick text from an untrusted sender was dropped and ACKed anyway, so
`send_text` -- whose contract is "resolves only once the peer confirms it
received it" -- resolved successfully for a message thrown away on arrival.
The ack can now say no (additive `delivered`/`reason`, wire-compatible).
- Transfer failures were silent. Only "the peer declined" toasted, while
success got a toast AND an OS notification.
- Batch-forward swallowed every failure in a bare `.catch(() => {})` and then
reported "forwarded N items".
- The backend's pause is bounded: it un-parks itself after 50s and emits
`transfer_resumed`. Nothing listened, so the row read "Paused" while the
progress bar crept along underneath it.
- TransferDetail fell back to the backend's raw English diagnostic --
"protocol: peer closed connection", and local absolute paths.
--- Controls that did nothing ---
- "Open the folder when done" was persisted, commanded, and exported in the
diagnostics bundle -- and nothing anywhere acted on it. It defaults ON.
- "History retention" and "Effective network" were wired to nothing. Retention
is now real. The SSID dropdown is removed: its only honest implementation is
a Windows-only WLAN call that would be silently dead on macOS and Linux -- a
new lie in place of an old one.
- Conflict policy "ask" never asked under auto-accept -- which, under the
default settings, is the ONLY path. Auto-accept answers "do I want anything
from this device", not "what happens to my existing file".
--- Browser share ---
A live share is files being served over HTTP on your LAN. Closing the panel
does not stop it, deliberately -- a link you handed someone should not die
because you closed the panel you copied it from -- but reopening the panel
minted a NEW share instead of adopting the live one, so the only Stop button in
the app could never reach the old one again. A forgotten share went on serving,
invisible and unstoppable. It is now always shown in the sidebar, adopted on
reopen, and closing says what it did.
"LAN only" was a claim, not a rule: the server binds 0.0.0.0 and has to (a DHCP
renewal would otherwise kill every live share). The boundary is now enforced
per request, in middleware, before any handler runs.
A single-file share 302'd straight to the download, skipping the branded
landing page in the most common case there is -- the page whose entire job is
to tell the recipient this is LanBeam, against a browser saying "not secure".
--- Backend work with no way in ---
`list_partials`: a half-written file is saved under its FINAL name -- 1.2 GB of
a 4 GB holiday.mp4, which looks perfectly ordinary in the folder and plays for
thirty seconds. The backend has tracked these all along (it is what makes
resume work) and could always delete them; nothing ever asked it what it was
holding. Settings lists them and can clear them.
`self_test_secure_channel` was pure scaffolding. Removed.
--- Where files live ---
Data and logs move out of Tauri's bundle-id folder
(%LOCALAPPDATA%\app.lanbeam.desktop\logs) into a product-named one, on each
platform's own conventions. The identifier itself stays -- macOS needs a
reverse-DNS CFBundleIdentifier, and the installer and `lanbeam://` registration
key off it. It is just the wrong thing to name a folder a person has to open.
With a real migration: the trust store comes along. Renaming a folder and
quietly abandoning the user's trust circle to make a path prettier is exactly
the class of thing this whole change set is about.
--- Polish ---
A <select> is a native widget, and macOS means it: WKWebView draws an Aqua
popup button and ignores the border, background, radius and height, so every
dropdown in the app looked lifted from another program. Chromium honours them,
which is precisely why the mismatch is invisible from the machine this was
built on. `appearance: none` everywhere, with our own chevron (a <select> is a
replaced element and cannot carry a ::after) and a per-theme colour, since a
data-URI SVG cannot reach a CSS variable. Keyboard focus gets a ring back --
`outline: none` had left the app with no focus indicator at all.
Interface scale (80-150%), applied to the webview AND to the window's minimum
size: a zoom shrinks the CSS viewport, so a floor that ignored it would let the
user scale the interface straight off the edge of its own window. Ctrl +/-/0
drive the app's own setting; the webview's native zoom stays off.
Status dots go through one place. Colour is the decision (fingerprint changed /
trusted / not), fill is presence (here / away). "Away" used to paint over
"trusted", so a sleeping laptop you trust rendered identically to a stranger --
on the one page whose entire job is showing trust -- and a fingerprint-change
alert, the loudest thing this app can say, came out as a faded grey dot.
The keyring crate was compiled with `windows-native` and nothing else. keyring 3 picks its backend at COMPILE time and falls back to an in-memory MOCK store when no feature matches the target -- so on macOS and Linux `Identity::load_or_create` minted a brand-new X25519 key on every launch, silently, and every fingerprint every peer had pinned went stale on restart. No error, no log, no degraded-mode event. Adds the Apple and secret-service backends. The repo's own `identity_is_stable_across_loads` proves that in one second. It had simply never been run anywhere but Windows -- which is why CI now runs the backend suite on Windows, macOS AND Linux, with a real dbus + gnome-keyring on the Linux job so the identity tests cannot "pass" against nothing. The matrix is not box-ticking; it exists because a bug of exactly that shape was invisible from the machine this was built on. Also: - bundle targets were hard-coded to `nsis`, so macOS and Linux produced no artifact at all -- and `lanbeam://` could never be registered on macOS, whose only registration path is an Info.plist that only the macOS bundler writes. - Cargo metadata was still the scaffold's: `authors = ["you"]`, `description = "A Tauri App"`. Those become the publisher and description of the installer. - `productName` was lowercase, so the .app, the menu-bar name and the Start-menu shortcut all read "lanbeam". - macOS gets an NSLocalNetworkUsageDescription. Discovery is IPv4 multicast and broadcast, which macOS gates behind the local-network permission -- without the key the system has nothing to tell the user when it asks, and a denial looks from inside the app like LanBeam is simply broken: no devices, ever, with nothing in the UI to say why. - Linux deb dependencies: the `tray-icon` feature pulls in libappindicator, and without it the app starts with no tray at all.
A pass over every falsifiable claim in the READMEs and the roadmap, checked against the code rather than against memory. Five statements were the wrong way round, and five things the app now does were not mentioned at all. Wrong: - "Pairing pins fingerprints on both sides" -- the handshake grants no trust; both screens show the same SAS and a person confirms it. - "a fingerprint-changed alert that auto-revokes trust" -- nothing is revoked automatically. The new key was never trusted; it is a different device. - "The browser-share server binds the LAN" -- it binds 0.0.0.0, and LAN-only is enforced per request in middleware. - "emits a single NSIS installer" -- every target for the host platform. - "M4-M9 (all shipped)" -- M9's updater is still pending. Missing: deleting a device (and that LanBeam refuses to file itself as a peer); that quick text to an untrusted receiver now fails honestly rather than reporting delivery; that the share's download cap is PER FILE; that closing the share panel does not stop the share; three-platform CI; the interface scale. Roadmap test counts were stale (148 unit -> 250 + 13 integration + 365 frontend), and the SSID item is marked dropped rather than pending, with the reason.
`tauri::image::Image::from_bytes` is gated behind the `image-png` feature, and we only enabled `tray-icon`. The call is in tray.rs's macOS menu-bar TEMPLATE icon path, inside a `cfg(target_os = "macos")` block -- so a Windows build never even type-checked it, and neither did anyone. The macOS job did, immediately. This is exactly the class of bug the matrix exists to make loud. Two bugs in the workflow itself, mine: - `libappindicator3-dev` and `libayatana-appindicator3-dev` CONFLICT; apt refuses to install both. Ayatana is the one Tauri 2 wants on a current Ubuntu. libxdo-dev and libssl-dev were missing from the list entirely. - biome is not vendored -- package.json fetches it through `npx --yes` -- so `pnpm exec biome` found nothing. CI now runs the project's own script, which also means CI and a developer's machine cannot end up on different versions. Verified against the tauri 2.11.5 source rather than by another CI round-trip: `from_bytes` is `cfg(any(feature = "image-ico", feature = "image-png"))`, `icon_as_template` carries no platform gate, and the `config_dir`/`home_dir`/ `local_data_dir` that paths.rs uses on macOS and Linux are ungated on desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An audit of every falsifiable claim in the app — the code, the settings, the docs — turned up almost no crashes. Nearly every real defect was the same shape:
And the tests were pinning the lies.
loopback_pairing_mutually_trusts…,untrusted_text_…_is_dropped_but_acked, and "migrates trust to the new key when the codes match" each named and guarded the broken behaviour. A green suite meant nothing.The security ones
Pairing granted trust inside the handshake, before any human compared the SAS — and only the host could see that SAS. The joiner got it back from
join_by_codeand threw it away, while the host's screen said "compare this with the other device". A code only one side can read is not a check; it is a formality that ends in a yes.Both ends now show the same SAS, and trust is recorded only once a person confirms it — through the same
set_trustedpath the trust circle uses, so a paired device is trusted and auto-accepting, exactly like one dragged into the ring, and the two notions of trust can no longer drift apart.trust::grant()is deleted outright: leaving a backend-side "trust without asking" helper lying around is leaving a door for someone to wire it back up.The fingerprint-changed alert offered to "re-verify and restore trust" by dialling the suspect key and showing the SAS that handshake produced — while the other device displayed nothing at all. There was no second number to compare, so pressing they match migrated trust onto the very key the alert existed to make you suspicious of. That flow is gone; the alert now reports, and offers the two honest exits (delete the old record, or pair with the new key like the new device it is).
It also fired on a name match — and the default device name is the machine's hostname, so two un-renamed installs raised an impersonation alarm at each other.
You could file yourself as a peer by dialling your own address, and a self-entry is uniquely un-removable: discovery drops its own announces, so it can never be expired away. Refused on every path, filtered from the device list, and evicted from the trust store at startup.
Things that could not be deleted
forget_device— "delete" used to clear the trust row while the peer sat untouched in the manual address table, back on the very next list, with nothing anywhere able to take it out.Quiet failures
send_text— whose contract is "resolves only once the peer confirms it received it" — resolved successfully for a message thrown away on arrival.transfer_resumed. Nothing listened, so the row read "Paused" while the progress bar crept along underneath it.TransferDetailfell back to the backend's raw English diagnostic, local absolute paths and all.Controls that did nothing
Browser share
A live share is files being served over HTTP on your LAN. Closing the panel doesn't stop it (deliberately — a link you handed someone shouldn't die because you closed the panel you copied it from), but reopening minted a new share instead of adopting the live one, so the only Stop button in the app could never reach the old one again. A forgotten share went on serving: invisible, and unstoppable.
Also: "LAN only" was a claim, not a rule (now enforced per request, in middleware); and a single-file share skipped the branded landing page — in the most common case there is.
Cross-platform
keyringwas compiled withwindows-nativeand nothing else, so on macOS and Linux it fell back to keyring's in-memory mock store: a brand-new device identity on every launch, silently, invalidating every fingerprint every peer had pinned.The repo's own
identity_is_stable_across_loadsproves that in one second. It had simply never been run anywhere but Windows — which is why CI now runs the backend suite on Windows, macOS and Linux, with a real dbus + gnome-keyring on the Linux job so the identity tests can't "pass" against nothing.Plus:
bundle.targetswas hard-coded tonsis(no macOS/Linux artifact at all, andlanbeam://could never be registered on macOS); Cargo metadata was stillauthors = ["you"].Also
%LOCALAPPDATA%\app.lanbeam.desktop\into a product-named folder — with a real migration, so the trust store comes along.<select>is a native widget and macOS means it: WKWebView draws an Aqua popup and ignores the border/background/radius, so every dropdown looked lifted from another program. Chromium honours them — which is exactly why it was invisible from the machine this was built on.Verification
Rust 266 tests (250 unit + 13 integration + the new ones) · clippy 0 · fmt clean
Frontend 376 tests / 29 files · tsc clean · biome clean · build passes
Notes for review
store.ts,commands.rs,transfer.rs,lib.rsand both i18n files are touched by nearly every fix; splitting them into "atomic" commits would take hunk-level surgery and produce intermediate trees that don't compile and were never green. Inventing a history that never existed is worse than one honest commit.tauri devmust be restarted — the Rust side changed. On first launch the app migratessettings.json/trusted.jsonto the new folder and logs what it moved, and evicts any self-record from the trust store.