* fix(desktop): support a server hosted under a sub-path `normalize_server_url` rebuilt whatever you typed as `scheme://host[:port]` and dropped the path, so a self-hosted Sure behind a reverse proxy that mounts it under a prefix — `https://home.example.com/sure` — could never be added: the health check hit `{origin}/up`, where something else (or nothing) answers, and the app reported "Couldn't reach a Sure server at that address." The same bare origin was then used for the navigation, the SSO hand-off and the IPC capability grant, so even a passing check would have loaded the wrong site. The path was being dropped for a reason — people paste the URL they have in the clipboard, which is usually a deep link like `/sessions/new`. So rather than guess, let the server say where it is: keep the path in the canonical form, and have `check_server` probe `/up` from the address as typed back up to the origin, returning the first base that answers 200. A root-hosted server still resolves to its origin (one extra candidate, tried only after the specific one 404s), and a transport-level failure returns immediately instead of retrying a host that is not there. `check_server` now answers with that resolved base instead of a bool, and both callers save it rather than the raw input. Tests: the sub-path base, trailing slash / query / fragment trimming, the walk-up candidate list, ports preserved on every candidate, and the cap that still keeps the origin. * fix(desktop): carry the mounted base through SSO and deep links Follow-up to the sub-path support in the previous commit: saving a base with a path fixed adding and loading such a server, but left two flows resolving against the bare origin. The injected bridge only intercepted a form action shaped exactly `/auth/{provider}`, so a mounted server's `/sure/auth/{provider}` was never intercepted, and it emitted `location.origin`, which `begin_sso` would then reject as an unknown server. Match the prefix that precedes `/auth/` and emit the base it implies — empty at a domain root, so root-hosted deployments are unaffected. Deep links were gated on `target.server`, which `deep_link::parse` builds from host and port alone. Gate on the destination instead, and let `is_known_server` accept a URL that a saved base covers. `base_covers` matches only at a path boundary, so a saved `https://sure.example.com` does not cover `https://sure.example.com.evil.test`. Reported by automated review on the PR. * fix(desktop): cap candidates from the deep middle, not the shallow end The cap kept the deepest MAX_BASE_CANDIDATES - 1 candidates plus the origin, which discards the shallow ones in between — and a mount point is shallow. A server mounted at /sure, reached by a link pasted three segments into the app (`/sure/transactions/123/edit`), produced `.../edit`, `.../123`, `/sure/transactions`, origin: `/sure`, the only base that answers, was never probed, so the connection failed with "Couldn't reach a Sure server at that address" against a live server. Keep the address as typed plus the shallowest bases instead. Both readings survive the cap: the base typed exactly, and the mount a deep link sits under. Reported by @jjmata in review. * fix(desktop): bound discovery by mount depth, not candidate count Capping the candidate list drops whole mount depths silently: with a budget of four, `https://host/a/b/c/d/e` never probed `https://host/a/b/c`, so a server mounted there was undiscoverable. Bound the supported mount depth instead. The address as typed is always probed first, so a base entered exactly still works at any depth; the rest are every depth up to MAX_MOUNT_DEPTH, which keeps a pasted deep link finite (typed + 4 probes) without skipping a depth in between. Reported by coderabbitai in review.
Sure Desktop (macOS)
Native macOS shell (Tauri 2 + WKWebView) that renders the full Sure web app and wraps it in real Mac chrome. It always talks to a Sure server you already run (self-hosted or managed) — same trust model as a browser.
Requirements
- Rust (stable), Node 18+, Xcode command line tools, macOS 12+.
Run in development
cd desktop
npm install
npm run build # builds the injected bridge.js + onboarding assets
npm run tauri dev
On first launch, enter your Sure server URL (e.g. http://localhost:3000 when
running bin/dev). The app health-checks {server}/up, then loads the real
/sessions/new where you sign in with password or SSO (MFA supported).
Build a release .dmg (unsigned)
cd desktop
# Single-arch (host only):
npm run tauri build
# Universal (Apple Silicon + Intel) — what releases ship:
rustup target add aarch64-apple-darwin x86_64-apple-darwin
npm run tauri build -- --target universal-apple-darwin
# Output: src-tauri/target/universal-apple-darwin/release/bundle/dmg/Sure_<ver>_universal.dmg
Publishing a release
The desktop build runs automatically as part of the normal Sure v* release.
The version comes from .sure-version and must match the release tag; it is
stamped into desktop/package.json and desktop/src-tauri/tauri.conf.json
only while building. The universal .dmg is attached to that same GitHub
Release—there is no separate desktop action, tag, or version.
Installing an unsigned build (end users)
The published .dmg is not code-signed, so macOS Gatekeeper blocks the first
launch. To open it:
- Drag Sure to Applications and try to open it; dismiss the warning.
- System Settings → Privacy & Security, scroll down, click Open Anyway, and confirm. (On macOS 15 Sequoia the old right-click→Open shortcut is gone; this Settings path is the way.)
If macOS instead says the app is "damaged", the download was quarantined — strip it once in Terminal:
xattr -cr /Applications/Sure.app
Signing + notarization (below) removes this friction entirely.
Rust tests
cd desktop/src-tauri
cargo test
Deep links
Registered scheme: sure://{host}[:port]/{path} → opens the app to that
server/page. Example: open "sure://localhost:3000/accounts". (Works from the
bundled .app, not tauri dev.)
Code signing & notarization (required for distribution — NOT wired up)
No Apple Developer credentials are needed to build/run locally. To ship a
distributable, signed, notarized .dmg, add:
- An Apple Developer ID Application certificate in your login keychain.
- Tauri signing config in
src-tauri/tauri.conf.jsonunderbundle.macOS:"signingIdentity": "Developer ID Application: <NAME> (<TEAMID>)","hardenedRuntime": true, and anentitlementsplist if needed. - Notarization after build:
VERSION="<release-version>" xcrun notarytool submit "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Sure_${VERSION}_universal.dmg" \ --apple-id "<APPLE_ID>" --team-id "<TEAMID>" --password "<APP_SPECIFIC_PW>" --wait xcrun stapler staple "src-tauri/target/universal-apple-darwin/release/bundle/dmg/Sure_${VERSION}_universal.dmg"
These steps require an Apple Developer account and are intentionally left as a documented follow-up.
Not built yet (see spec §9)
- Balance-with-sparkline glance widget (Tauri floating panel and/or a WidgetKit
Notification Center widget with App Group data sharing), fed by an
auto-provisioned read-only API key polling
/api/v1. Deferred by design.