v0.49.0 — At-rest encryption + new widgets
Highlights
At-rest encryption for connector secrets. Manifest-declared secret: true settings (HA tokens, plugin API keys) are now AES-GCM-wrapped on disk and unwrapped transparently when the scheduler / push / fetch pipelines read them. The wire format enc:v1:<base64(nonce||ciphertext||tag)> carries a version tag so future algorithm upgrades are mechanical.
Two new widgets. Countdown, Date (N-days hero against a target date with a friendly meta line) and Year, Progress (year-in-weeks or life-in-weeks dot grid with a percentage hero). Both pure client-side, no network.
Key resolution
TESSERAE_SECRET_KEYenv var (64 hex chars = 32 bytes) takes precedence.openssl rand -hex 32is the easy way to mint one.- If unset, the box derives a stable key from the Flask session secret via HKDF-SHA256. The fallback works for the default self-host case; setting the env explicitly is recommended for real installs so rotating the session secret doesn't lock you out of your own connectors.
Upgrade notes
- Upgrading 0.48.x → 0.49.0 needs no action. Existing plaintext secrets keep working (the unwrap path is a no-op for non-prefixed input) and migrate to ciphertext the next time you Save any setting under Plugins / Renderers / Devices.
- Migrating to a new install works out of the box for the default setup. The built-in Backups and Migrate flows include
settings.json, which carries the session secret the fallback key derives from. Same secret on both machines means the same decryption key. - If you set
TESSERAE_SECRET_KEY, copy that env var to the new install before importing the data zip. The key lives in your environment, not indata/, so without it the new machine derives a different key and connector secrets won't decrypt. - Downgrading 0.49.0 → 0.48.x. Any secret re-saved on 0.49 is stored as
enc:v1:<base64>on disk; an older Tesserae would read that literal string as your HA token and fail to authenticate. Restoresettings.jsonfrom a pre-0.49 backup or re-save each affected secret in the older version.
Internals
- New
app.secret_boxmodule (AES-GCM + HKDF via PyCA cryptography). SettingsStoregains an optionalsecret_box=constructor arg and aset_secret_box()injector. Wrap-on-write / unwrap-on-read is transparent to consumers;get_sectionrecursively unwraps any_secret-suffixed string at any depth so plugin server modules that read their own state directly (e.g.ha_core) keep seeing plaintext.- Wrong-key reads raise
SecretBoxError(loud failure, not a silent empty token) so a misconfiguredTESSERAE_SECRET_KEYsurfaces immediately instead of as a 401 from HA. - Added
cryptography>=42,<46as a runtime dependency. Rust-backed primitives, manylinux wheel available so the Docker base image stays slim. - 968 tests passing, ruff + mypy strict clean.
Zero plugin-code changes needed
The encryption layer sits under the existing secret-handling APIs (get_for_runtime, update_for_namespace, get_section). Every existing widget keeps reading its settings the same way and gets plaintext back. No migration script, no breakage.