diff --git a/README.md b/README.md index f1f86f5..b9eb76f 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ dotnet run --project src/DodoSSH.Api It listens on `http://localhost:5233`, serving `/healthz/live`, `/healthz/ready` and — in Development — `/openapi/v1.json`. +Development and testing are currently **Windows-only**. Anything known or suspected to differ on +Linux and macOS is tracked in [`docs/platform-flags.md`](docs/platform-flags.md), along with the +deployment gotchas that have already cost time once. Read it before assuming something works +off-Windows. + ### Conventions the build enforces - Warnings are errors. `dotnet format --verify-no-changes` gates CI. diff --git a/docs/platform-flags.md b/docs/platform-flags.md new file mode 100644 index 0000000..14fc3e2 --- /dev/null +++ b/docs/platform-flags.md @@ -0,0 +1,92 @@ +# Platform flags + +Things known or suspected to behave differently outside Windows, plus deployment gotchas that +have already cost time once. **Development and testing are currently Windows-only**, so anything +here marked *unverified* has not run on the platform in question and must not be assumed to work. + +Each entry says what the risk is, why it matters, and what to do about it. Delete an entry when it +has been verified or made moot — not when it merely stops being convenient. + +## Cryptography + +**`ChaCha20Poly1305.IsSupported` is false on macOS**, and on Windows builds before 10.0.20142. +This is why the client uses NSec (libsodium) rather than the BCL for content encryption; see +docs/crypto.md §1. *Already mitigated* — but if a BCL AEAD path is ever added as a fallback it +**must** gate on `IsSupported` rather than assuming availability, or the client will fail to open +any vault on macOS. + +**Argon2id timings are measured on one Windows machine only.** 256 MiB with t=4 took 323 ms here. +The floor and ceiling in `EnrollmentLimits` were chosen against that number. *Unverified +elsewhere:* recalibrate on the slowest target platform before recommending a default profile, +because a cost that is comfortable on a desktop can make unlock unusable on a low-power laptop — +and the parameters are stored per user at enrollment, so a bad default is a per-user migration. + +**libsodium ships native binaries per RID.** This complicates single-file and AOT publishing, and +on macOS every native library (`libsodium`, `libSkiaSharp`, `libHarfBuzzSharp`, `libe_sqlite3`) +must be signed **individually** with `--options runtime --timestamp` before the bundle is signed, +or notarization fails with an error that does not name the offending file. + +## Desktop client + +**The Avalonia WebView on Linux is unproven, and is the single largest risk in the plan.** The +official control uses WPE WebKit (`libwpewebkit-2.0`); the community `NativeWebView` uses WebKitGTK +(`libwebkit2gtk-4.1`), which is far more widely installed. Avalonia's own documentation +contradicts itself on whether offscreen rendering works there. *Unverified:* the spike must cover +Ubuntu on both Wayland and X11, Fedora KDE, and macOS 15. This is why the terminal sits behind +`ITerminalHost` — that abstraction is what preserves the option to swap backends, and it should +not be collapsed away for convenience. + +**SSH.NET's `window-change` on `ShellStream` is unverified.** A terminal that cannot resize is +unusable, so this gates M1's client work. Fall back to `IChannelSession` if it does not work, and +prefer forking and upstreaming a fix over reflection as a long-term answer. + +**MSIX packaging is ruled out, not merely deprioritised.** A packaged app runs WebView2 in an +AppContainer where loopback connections are blocked without a `CheckNetIsolation` exemption. The +terminal data plane *is* a loopback WebSocket, so MSIX would break the product outright. Velopack +for Windows/macOS/AppImage; Flatpak and deb/rpm defer updates to the package manager. + +**Linux ships AppImage and Flatpak first**, specifically so the WebKit runtime is bundled rather +than assumed present on the user's machine. + +## Build and CI + +**Integration tests need a Docker daemon** (Testcontainers). They run on `ubuntu-latest` in CI. +macOS runners have no Docker daemon, and the Windows CI job is deliberately build-only. So +anything proved by an integration test is proved on Linux only — which is the right place for +server code, and no coverage at all for client platform behaviour. + +**`[CallerFilePath]` is rewritten to `/_/...` under `ContinuousIntegrationBuild`.** Any test that +locates a fixture by source path passes locally and fails in CI. Copy fixtures to the output +directory and read them via `AppContext.BaseDirectory` instead; `GoldenVectorTests` shows the +pattern. + +**`dotnet format --verify-no-changes` is part of the CI gate** and exits non-zero on style +warnings, not just whitespace. Run it before pushing; a build with zero warnings can still fail +that step. + +## Deployment + +**PostgreSQL 18 moved its data directory** to `/var/lib/postgresql`, not `/var/lib/postgresql/data` +as in 17 and earlier. A compose file carried over from an older version silently gets an empty +volume — the database appears to work and loses everything on restart. Relevant to any compose +file other than `deploy/docker-compose.dev.yml`, which is already correct. + +**Keycloak in the dev stack listens on host port 18080, not 8080.** On this machine an unrelated +Apache Tomcat holds `127.0.0.1:8080`, and a loopback-specific bind wins over Docker's `0.0.0.0` +publish when resolving `localhost` — so every realm request returned 404 while the container +looked healthy. If discovery fails against a locally-published container, check for another +process bound specifically to loopback before suspecting the container. + +**`Sync:CursorSigningKey` generates an ephemeral per-process key when unset.** Fine for a single +node; on a multi-node deployment cursors issued by one node are rejected by another, so clients +resync from the beginning repeatedly. Must be configured explicitly before running more than one +instance. `WarnOnRiskyConfiguration` logs this at startup. + +**Rate limiting is not implemented yet** (M2). `POST /api/v1/me/enrollment` and the sync endpoints +are reachable by any authenticated caller at any rate. Enrollment requires a valid access token +and is idempotent, so the exposure is resource consumption rather than a credential-guessing +surface — but it is still an unmetered write path. + +**`/api/v1/me` does not update `last_seen_at_utc`.** Deliberate: a GET that writes on every call is +a smell, and nothing depends on the value yet. Revisit when device management lands, since that is +the first feature that needs it.