* feat(clientip): expose trusted proxy CIDRs in the admin UI and via env var
Trusted reverse-proxy CIDRs (clientip.trusted_proxies) previously required
hand-editing server_settings via SQL and a restart. Now:
- Admin UI: a Network > Trusted Proxies field on the General settings page,
with server-side CIDR validation and normalization on save.
- Env var: SILO_TRUSTED_PROXIES is validated at startup and persisted to
server_settings (re-applied on every boot while set), so Docker operators
never touch the database and the UI shows the effective value.
- Hot reload: the setting now rides the nodeconfig watcher snapshot, so
changes apply without restart on Redis-less deployments too (previously
reload only worked via the Redis event bus, and only when rate limiting
was enabled).
Closes#300
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(clientip): keep key-scoped event-bus reload alongside the config watcher
A malformed unrelated setting fails the whole-config watcher reload; the
direct subscription re-reads only clientip.trusted_proxies so the trust
boundary still updates on Redis-backed multi-instance deployments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(clientip): key-scoped same-process reload in OnServerSettingUpdated
Covers the Redis-less path: an unrelated malformed setting that fails the
whole-config watcher reload can no longer leave stale trusted-proxy CIDRs
after a successful admin save.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(clientip): reload with a fresh context in OnServerSettingUpdated
The setting is already persisted when the hook runs; a canceled admin
request must not skip the trust-boundary reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style(web): wrap long trusted-proxies hint to the 100-char width
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add guidance tip for trusted proxy ranges
Explains that the setting replaces the private-network defaults, the
recommended /32 pattern, CDN multi-range caveats (Cloudflare), and why
0.0.0.0/0 is unsafe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds Discord direct messages as a notification channel. Users link their
Discord account via OAuth2 (identify scope only, one-time server-side
state rows); a bot delivers their inbox notifications as DMs.
- Extract the email channel's watermark sweep into a generic
account-channel engine; email and Discord are now thin adapters, so
the SKIP LOCKED claim / watermark-after-send durability logic exists
once.
- New internal/discord REST client (token exchange, identity, open DM,
send message) — no Gateway connection, no new dependencies.
- Opt-in master switch (notifications.discord_enabled, default off)
gates delivery, linking, capability, and the admin settings reveal.
- Admin UI: credentials (secret + bot token encrypted at rest), dev
portal setup checklist, bot invite link buttons, and a test button
that bypasses the settings read cache and is disabled while
credential edits are unsaved.
- DM failures from missing shared guild (Discord 50007) surface as link
health in user settings and self-heal via capped backoff.
- New combined mode (per_episode_and_digest) for email and Discord:
instant sends all day plus a daily digest recapping the whole window
since the previous digest.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(security): encrypt server-owned credentials at rest
Introduce AES-256-GCM at-rest encryption (HKDF-derived from a required
SECRET_KEY) for server-owned credentials, with row-bound AAD, a versioned
enc:v1: envelope, and an idempotent startup backfill.
- internal/secret: cipher + RowAAD/SettingsAAD + the startup backfill engine.
- SECRET_KEY required at bootstrap; cipher threaded as an explicit dependency.
- server_settings: EncryptedSettingsRepo decorator over the audited
SensitiveSettingKeys (also drives admin redaction); the config watcher and
watch-sync settings reads decrypt too.
- Arr keys inline-encrypted; the ambiguous SecretResolver indirection removed
from requests/autoscan.
- Per-table columns encrypted: subtitles, watch-sync, webhook-sync (not
webhook_secret), history-import, and the jellycompat session's bridged Silo
access/refresh tokens.
- Startup backfill (resolve-then-encrypt for arr refs) is best-effort and
primary-node gated.
Equality-looked-up secrets and plugin_runtime_configs.config_value are out of
scope (need hashing / cross-repo design) — see
docs/architecture/secret-encryption.md.
Refs #45
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(compose): require SECRET_KEY in docker-compose
The server now fatals without SECRET_KEY, so the integrated service (and the
commented distributed proxy/transcode examples) pass it through with a
fail-fast guard matching the existing MEDIA_ROOT pattern. Distributed worker
nodes must use the SAME key as the primary to decrypt shared data.
Generate with: openssl rand -base64 48.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): encrypt history import session credentials
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>