## v0.2.1 (2026-04-22) Security-focused release on top of v0.2.0. Hardens the restore/backup flow, CSRF/SSRF surfaces, JWT revocation on role change, and template-context leakage; adds a new **per-tracking-config quiet hours** feature with app-level IANA timezone support; plus a handful of performance fixes. ### Features - **Per-tracking-config quiet hours with app-level IANA timezone** — new `Timezone` app setting (defaults to `UTC`) and a `Quiet Hours` section on the Immich tracking-config form. HH:MM windows (including overnight, e.g. `22:00–07:00`) are interpreted in the configured timezone and suppress all notifications for that tracker. ([6c3dd67](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6c3dd67)) ### Security - **Signed & verified pending-restore bundles** — SHA256 stored in `AppSetting` and checked on startup apply; files outside `data_dir` are refused and permissions tightened to `0600`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Same-origin check on `POST /api/backup/apply-restart`** — Bearer-in-localStorage was CSRF-reachable from any XSS'd admin tab; require matching `Origin`/`Referer`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **JWT `token_version` bumps on demotion** — role/username change and admin password reset now bump `token_version` so already-issued tokens lose admin. Last-admin TOCTOU guarded by `COUNT` + post-commit recheck that rolls back on race. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **SSRF guard extended** to `ImmichClient.__init__` and the `external_domain` setter — admin-mutable URLs were bypassing the check that webhook / Slack / Discord paths already used. Dev `scripts/restart-backend.sh` now sets `NOTIFY_BRIDGE_ALLOW_PRIVATE_URLS=1` so homelab Immich instances still work. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Redact & cap Immich error bodies** (~120 chars) before they flow into `ActionExecution.error` / `EventLog.details` (both UI-visible). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Deny-list sensitive keys** (`api_key`, `token`, `secret`, `password`, `authorization`, `cookie`, …) in template-context merges so a rogue template cannot exfiltrate provider creds via `{{ api_key }}`. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Cap user-controlled Immich search params** — `query` ≤ 256, `person_ids` ≤ 50, `size` ≤ 100 — so a Telegram listener cannot DoS upstream. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Stream upload reads** with a running byte counter + `Content-Length` precheck instead of buffering the full body and then rejecting. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Log Telegram `parse_mode` fallbacks** instead of swallowing silently — template escape bugs now surface in server logs. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Rollback partial imports** on pending-restore failure (error recorded on a fresh session). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) ### Performance - **Fix N+1** in `_refresh_telegram_chat_titles` — single `IN` query instead of `session.get` per chat. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Parallelize album & shared-link fetches** in `test_dispatch` via `asyncio.gather`, and per-receiver Telegram test sends in the notifier with a semaphore of 5. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Early-exit `collect_scheduled_assets(limit=0)`** so the periodic-summary test path skips the full per-album filter/sample (was O(album_assets)). ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Explicit `CREATE INDEX IF NOT EXISTS`** for `event_log` (`user_id` / `action_id` / `provider_id`) so the first boot after upgrade isn't left unindexed for the dashboard query. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) - **Add `AbortController` timeout (120s)** to `fetchAuth` so uploads/downloads don't hang indefinitely. ([56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2)) ---
All Commits - [6c3dd67](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/6c3dd67) — feat(tracking): per-config quiet hours with app-level IANA timezone _(alexei.dolgolyov)_ - [56993d2](https://git.dolgolyov-family.by/alexei.dolgolyov/notify-bridge/commit/56993d2) — fix(security,perf): harden restore, CSRF, token_version + perf pass _(alexei.dolgolyov)_