diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f3dfc..4558479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,102 @@ # PiCloud Changelog +## v1.1.7 — Configuration & Email (unreleased) + +The operational-config layer: **encrypted per-app secrets**, **outbound +email**, and an **inbound email trigger** — plus the long-missing +**dead-letter handler wiring** and **at-rest encryption of the realtime +signing key**. All at-rest encryption uses a single process master key +(AES-256-GCM); key rotation is deferred to v1.2. + +### Added — Encryption infrastructure + +- **Process master key** from `PICLOUD_SECRET_KEY` (base64 of exactly 32 + bytes). REQUIRED at startup — an unset or malformed key is fatal. + Generate one with `openssl rand -base64 32`. A deterministic in-memory + dev key is used ONLY when `PICLOUD_SECRET_KEY` is unset AND + `PICLOUD_DEV_MODE=true` (with a prominent startup warning); there is no + quiet unencrypted mode. +- **`picloud_shared::crypto`** — `encrypt`/`decrypt` envelope: + `Aes256Gcm`, 96-bit CSPRNG nonce, 128-bit auth tag appended to the + ciphertext (RustCrypto `Aead` layout). Both ciphertext and nonce are + stored. +- **Key rotation is out of scope.** Changing `PICLOUD_SECRET_KEY` between + deploys renders all existing ciphertext undecryptable. v1.2+ adds + key-version columns + a re-encryption pass. + +### Added — Encrypted per-app secrets + +- **`secrets::{get,set,delete,list}(name)`** SDK — collection-less, + per-app. `set` accepts a String/Map/Array (JSON-encoded then encrypted); + `get` returns the same Rhai type back; missing → `()`. 64 KB plaintext + cap (`PICLOUD_SECRET_MAX_VALUE_BYTES`). `migrations/0023_secrets.sql`. +- **Admin API** `GET/POST/DELETE /api/v1/admin/apps/{id}/secrets` — list + returns names + `updated_at` only, **never values**. +- **Dashboard Secrets tab** — list names + last-modified, create/update + (masked value with a confirm-gated reveal), delete with confirm. +- `Capability::AppSecretsRead`/`Write` (→ `script:read` / `script:write`). + No new Scope variants (seven-scope commitment). Secret writes + deliberately do **not** emit trigger events. + +### Added — Outbound email + +- **`email::send` / `email::send_html`** SDK over an SMTP relay + (`lettre`). Config from `PICLOUD_SMTP_HOST/PORT/USER/PASSWORD/TLS/ + TIMEOUT_SECS`; if HOST/USER/PASSWORD aren't all set the service runs in + **disabled mode** (every send throws `NotConfigured`, warned at + startup). Required `to`/`from`/`subject` + one of `text`/`html`; + RFC 5322-ish address validation; 25 MB per-message cap + (`PICLOUD_EMAIL_MAX_MESSAGE_BYTES`); `reply_to` defaults to `from`. + Per-call connection (pooling deferred to v1.2); per-app `from` + validation / SPF / DKIM are the operator's SMTP-relay concern. +- `Capability::AppEmailSend` (→ `script:write`). + +### Added — Inbound email (`email:receive` trigger) + +- **Webhook receiver** `POST /api/v1/email-inbound/{app_id}/{trigger_id}` + — a provider (Mailgun / Postmark / SendGrid / SES) POSTs the generic + JSON shape `{from,to[],cc[],subject,text,html,message_id}`; the + receiver verifies the optional HMAC signature, normalizes to + `TriggerEvent::Email`, and enqueues an outbox row. 202 accepted, 401 + bad/missing signature, 404 missing/wrong-kind/cross-app, 422 malformed. + Handlers see `ctx.event.email`. `migrations/0024_email_triggers.sql`. +- **Admin** `POST /api/v1/admin/apps/{id}/triggers/email` + + dashboard form (with the webhook URL + expected payload). The HMAC + `inbound_secret` is stored **encrypted** via the master key (deviation + from the original plaintext design — see HANDBACK §7). +- Provider-specific payload unmarshallers + inbound attachments → v1.2. + Native SMTP listener → v1.3+. + +### Security/correctness fix (retroactive) — dead_letter handlers + +The `dead_letter` trigger kind has been registerable since v1.1.1 but, +due to missing dispatcher wiring (`list_matching_dead_letter` had no +production caller), handlers have **never fired**. Any deploy running +v1.1.1 through v1.1.6 with `dead_letter` triggers configured has had +silently non-functional handlers. v1.1.7 fixes the wiring; existing +`dead_letters` rows remain (no migration needed) but only NEW +dead-letter events (post-v1.1.7) trigger handlers. To process older +rows, use the existing admin replay surface to re-enqueue them. + +### Changed — Realtime signing key encrypted at rest (two-phase) + +`app_secrets.realtime_signing_key` was stored as 32 plaintext bytes. It +is now encrypted with the master key. `migrations/0025_encrypt_realtime_keys.sql` +adds NULL-able encrypted columns and drops `NOT NULL` on the plaintext +column; a startup task encrypts pre-existing rows; the read path prefers +the encrypted columns and falls back to plaintext during the compat +window. **v1.1.8 will drop the plaintext `realtime_signing_key` +column** — operators should upgrade through v1.1.7 (which performs the +encryption) before v1.1.8. + +### Notes + +- **New deps:** `aes-gcm` (RustCrypto AEAD), `lettre` (SMTP). +- **New env vars:** `PICLOUD_SECRET_KEY` (required), `PICLOUD_DEV_MODE`, + `PICLOUD_SECRET_MAX_VALUE_BYTES`, `PICLOUD_SMTP_HOST/PORT/USER/PASSWORD/ + TLS/TIMEOUT_SECS`, `PICLOUD_EMAIL_MAX_MESSAGE_BYTES`. +- **SDK schema** 1.7 → 1.8; **dashboard** 0.12.0 → 0.13.0. + ## v1.1.6 — Realtime Channels & Client Library (unreleased) The first **external realtime surface** and the first **frontend diff --git a/Cargo.lock b/Cargo.lock index 37d571e..90f6a4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1754,7 +1754,7 @@ dependencies = [ [[package]] name = "picloud" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "async-trait", @@ -1783,7 +1783,7 @@ dependencies = [ [[package]] name = "picloud-cli" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "assert_cmd", @@ -1804,7 +1804,7 @@ dependencies = [ [[package]] name = "picloud-executor" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "picloud-executor-core", @@ -1816,7 +1816,7 @@ dependencies = [ [[package]] name = "picloud-executor-core" -version = "1.1.6" +version = "1.1.7" dependencies = [ "async-trait", "base64", @@ -1840,7 +1840,7 @@ dependencies = [ [[package]] name = "picloud-manager" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "picloud-manager-core", @@ -1852,7 +1852,7 @@ dependencies = [ [[package]] name = "picloud-manager-core" -version = "1.1.6" +version = "1.1.7" dependencies = [ "argon2", "async-trait", @@ -1883,7 +1883,7 @@ dependencies = [ [[package]] name = "picloud-orchestrator" -version = "1.1.6" +version = "1.1.7" dependencies = [ "anyhow", "picloud-orchestrator-core", @@ -1895,7 +1895,7 @@ dependencies = [ [[package]] name = "picloud-orchestrator-core" -version = "1.1.6" +version = "1.1.7" dependencies = [ "async-trait", "axum", @@ -1918,7 +1918,7 @@ dependencies = [ [[package]] name = "picloud-shared" -version = "1.1.6" +version = "1.1.7" dependencies = [ "aes-gcm", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 9e64a16..91639ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ ] [workspace.package] -version = "1.1.6" +version = "1.1.7" edition = "2021" rust-version = "1.92" license = "MIT OR Apache-2.0" diff --git a/crates/shared/src/version.rs b/crates/shared/src/version.rs index e783885..ca92fb2 100644 --- a/crates/shared/src/version.rs +++ b/crates/shared/src/version.rs @@ -58,7 +58,15 @@ pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// `RealtimeBroadcaster` / `RealtimeEvent` / `RealtimeAuthority` traits; /// the `topics` registry + admin endpoints; the `@picloud/client` /// TypeScript package). -pub const SDK_VERSION: &str = "1.7"; +/// +/// 1.8 additions (v1.1.7): `secrets::{get,set,delete,list}(name)` — +/// encrypted per-app secrets (AES-256-GCM at rest under the process +/// master key); `email::{send,send_html}(#{...})` — outbound email via +/// an env-configured SMTP relay; and `ctx.event.email` for +/// `email:receive`-trigger handlers (inbound email POSTed to the webhook +/// receiver). The `Services` bundle gains `secrets: Arc` and `email: Arc`. +pub const SDK_VERSION: &str = "1.8"; /// HTTP API major version. Appears in URL paths as `/api/v{N}/...`. /// Bump (new integer + new URL prefix) when the request/response diff --git a/dashboard/package.json b/dashboard/package.json index da1d364..6464627 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "picloud-dashboard", - "version": "0.12.0", + "version": "0.13.0", "private": true, "type": "module", "scripts": {