Files
sure/desktop
ServaTilis 77a55fff5b fix(desktop): support a server hosted under a sub-path (#3127)
* 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.
2026-08-26 08:15:50 +02:00
..

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:

  1. Drag Sure to Applications and try to open it; dismiss the warning.
  2. 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

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:

  1. An Apple Developer ID Application certificate in your login keychain.
  2. Tauri signing config in src-tauri/tauri.conf.json under bundle.macOS: "signingIdentity": "Developer ID Application: <NAME> (<TEAMID>)", "hardenedRuntime": true, and an entitlements plist if needed.
  3. 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.