Files
sure/desktop/src
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
..