Auditing the encryption-at-rest work surfaced one missed read site: sites_https_service
passed the DNS provider api_key/api_secret straight to the cert issuer, but those are
now Fernet-encrypted at rest — so DNS-01 wildcard issuance would have received ciphertext
and failed.
- Add DNSProviderService.decrypted_credentials() as the public, canonical way to read a
DNS provider's usable creds (nothing reads .api_key raw anymore); use it in
sites_https_service.
- Add tests/test_provider_secret_encryption.py — 8 proving tests: ciphertext-at-rest +
plaintext-on-read for DNS / cloud / storage / registrar, the idempotent legacy-plaintext
migration, and the wildcard path now receiving decrypted creds (the regression above).
pytest: 17/17 pass (8 new + 9 existing test_sites_https.py). Full suite: 187 passed; the
lone failure (test_agent_poll_e2e command roundtrip) is a pre-existing flake — the agent
registry in-memory singleton leaks across tests in that file (passes solo), unrelated here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Domain-expiry notifications: a daily scheduler (RegistrarService.notify_expiring)
fires through the notification channels when a registrar domain crosses 30/14/7/1
days or expires, de-duped via a small state file so each crossing alerts once.
- S3 image preview: ImageThumb is now S3-aware (loads objects via short-lived
presigned URLs); threaded through FileCard / PreviewDrawer / FileManager.
- Encryption-health banner: the Connections hub consumes GET /api/v1/connections
and warns if any credential is not encrypted at rest (hidden in the happy path)
— the registry's first real consumer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Encrypt the last plaintext credential store: CloudProvider.api_key_encrypted is
now Fernet-encrypted on write and decrypted in _auth_headers, with an idempotent
encrypt_legacy_secrets() migration run at startup. All five stores (source, DNS,
cloud, registrar, storage) now encrypt identically via app.utils.crypto.
- Add ConnectionRegistry + GET /api/v1/connections: one normalized, secret-free
list of every connected external account (the "what's connected" source of
truth). Frontend api.getAllConnections().
- The full write-side "one model replaces five" migration is deliberately not done
— it would risk five working subsystems for no user-facing benefit. The uniform
encryption + read registry deliver #18's practical value.
Verified by booting create_app('testing') and running the registry live (returns
a list; /api/v1/connections present in the URL map).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Encryption-at-rest (#16/#17) for the two provider stores still in plaintext:
- crypto.py: add decrypt_secret_safe (decrypt with plaintext fallback) + is_encrypted.
- DNS providers: encrypt api_key/api_secret on write; decrypt at point-of-use via
_api_key/_api_secret in the Cloudflare/Route53/DigitalOcean/GoDaddy builders.
- Storage: encrypt secrets in save_config; decrypt in get_config.
- Idempotent encrypt_legacy_secrets() migrations run on startup, encrypting any
pre-existing plaintext in place. Legacy values keep working via the decrypt
fallback, so there is no flag day.
Namecheap registrar:
- RegistrarConnection.config_json column for non-secret extras (username, client_ip).
- RegistrarService: Namecheap XML domains.getList (portfolio + expiry), test, and
MM/DD/YYYY -> ISO date parsing.
- Frontend: namecheap tile is live; the registrar form is provider-aware (GoDaddy
key+secret vs Namecheap key + username + whitelisted IP).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- backend: StorageProviderService.s3_* (browse/read/write/upload/delete/
presigned-get) + /api/v1/files/s3/* routes, reusing the configured backup
bucket — no new credentials.
- frontend: an "S3 bucket" target in the File Manager. UI paths stay slash-rooted
and map to S3 keys server-side, so breadcrumbs/history work unchanged; virtual
folders from prefixes; presigned-URL download. Folders/rename/permissions are
disabled and the local-only sidebar is hidden on S3. TargetPicker gained a
backward-compatible extraOptions prop.
- docs: mark #14/#15 done (Phase 4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the connections work already committed in 2e1bdbc + c1d4653
(swept in by a concurrent commit). Commits the two remaining edits:
- docs/CONNECTIONS_ROADMAP.md: mark #8 (SMTP relay) done — a real Postfix
smarthost (relayhost + SASL) with an OS-agnostic smtplib connection test.
- ConnectProviderModal: drop the unused `provider` arg from EmailBody (lint).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce a broad Connections expansion: add RegistrarConnection model, RegistrarService and /api/v1/registrars endpoints to surface domain portfolios and expiry. Add EmailRelayService and API routes plus Postfix relay wiring to configure/test/disable outbound SMTP. Extend DNSProviderService with DigitalOcean and GoDaddy implementations and helpers. Add GitLab support to source connection service and routes. Frontend: expand Connections hub, provider catalog, modal bodies, provider icons, RegistrarPortfolio component, API client methods and styles. Add docs/CONNECTIONS_ROADMAP.md describing roadmap and intent.