Phase 0.2 of the installer plan (docs/installer/PLAN.md §5). The installer has to
drive this binary non-interactively, and today it cannot: the auth token is only
readable by scraping the startup log, the config path can only be named through an
environment variable, and a relative db path follows the process working directory —
which a service manager, not the operator, chooses.
- Add a four-flag CLI (cli.rs): --print-config, --config <PATH>, --version, --help.
Hand-rolled; an argument-parsing dependency would be larger than the code it
replaced. An unrecognized flag exits 2 rather than starting a sidecar that is not
the one that was asked for.
- --print-config resolves the configuration exactly as a normal start does —
including writing a missing config file and generating a blank auth token — and
prints it as JSON on stdout: versions, protocol, both bind addresses, ws path,
resolved db path, and the token. config_created / token_generated let a re-run
tell "read an existing install" from "provisioned a new one". The log subscriber
is deliberately not started in this mode, so the document is the whole output.
- Anchor a relative [store].path to the config file's directory instead of the CWD,
and report resolved absolute paths. A unit pinning UOLINK_CONFIG now keeps its
database beside its config rather than in %SystemRoot%\System32 or a VirtualStore
redirect. Development is unaffected: under cargo run the two directories are the
same. :memory: and file: URIs are left alone.
- Hand the db path to sqlx as a filesystem path instead of formatting it into a
sqlite:// URL, which percent-decodes it and splits it on '?'. An installed path
containing %20 previously opened a different file; verified it now does not.
- Create the config's and the database's parent directories when missing, so a
service can name /var/lib/runicgateway on a host where nothing made it yet.
- 22 unit tests covering argument parsing, path anchoring, token persistence, the
generated config template, and the --print-config document.
No protocol change: PROTOCOL_VERSION stays 3.
Co-Authored-By: Claude <noreply@anthropic.com>
Auth is now impossible to turn off by accident. A blank auth_token is never
allowed even on loopback: config load generates a token, writes it back into
sidecar.toml (preserving the rest of the file), logs it, and continues -- so a
forgotten or cleared token self-heals into a working, authenticated setup instead
of silently disabling auth.
No auth token configured.
Generated new token: cb99...
Saved to sidecar.toml. Authentication is on.
Protocol versioning (PROTOCOL_VERSION = 1) lets the website and sidecar detect a
mismatch immediately when a message shape changes. Every response carries an
X-UOLink-Version header; /health and ws.hello include "protocol"; a request that
declares a different X-UOLink-Version is rejected 409 with both versions so the
mismatch is unambiguous. Bump the constant when a contract changes.
/health is now a real troubleshooting panel: status (ok/degraded), protocol,
plugin_connected (is the shard link up), database (SELECT 1), uptime, and
last_event (the timestamp of the last line from the shard). Unauthenticated so
monitoring can reach it.
Verified: a blank token generates + persists + enforces (401 without, 200 with);
X-UOLink-Version header on every response; 409 on a declared mismatch; /health
reports degraded/plugin_connected:false with no shard, then flips to ok/true and a
populated last_event once the shard connects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
config.rs loads all runtime settings from an external sidecar.toml (path via
$UOLINK_CONFIG), with env-var overrides (UOLINK_WEB_TOKEN, UOLINK_WEB_BIND,
UOLINK_SHARD_BIND, UOLINK_DB_PATH). Nothing is compiled into the binary. On first
run the file is generated with a random 24-byte auth token, so the sidecar is
secured out of the box and the operator just copies the token to the website.
An axum middleware rejects any request to a non-/health route that does not
present the token, as Authorization: Bearer, X-Api-Key, or ?token= (the last so
browser WebSocket clients, which cannot set handshake headers, can authenticate).
The comparison is constant-time. An empty token disables auth and is only
tolerated on a loopback bind; binding to 0.0.0.0 with no token logs a warning.
Verified: /health open (200); /history 401 without a token, 401 with a wrong one,
200 with the right one via either Bearer or X-Api-Key; an authed shard query
falls through to 503 when no shard is connected; WS rejected (401) with a bad
?token= and upgraded (101) with the right one.
sidecar.toml is gitignored (holds the secret); sidecar.toml.example is committed
as the reference.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>