chore(v1.1.7): version bumps + CHANGELOG
- workspace 1.1.6 → 1.1.7 - SDK schema 1.7 → 1.8 (SecretsService, EmailService, TriggerEvent::Email) - dashboard 0.12.0 → 0.13.0 - CHANGELOG entry: secrets, outbound email, inbound email, retroactive dead_letter fix note, realtime-key encryption migration (+ v1.1.8 drop) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
CHANGELOG.md
97
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
|
||||
|
||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<dyn
|
||||
/// SecretsService>` and `email: Arc<dyn EmailService>`.
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "picloud-dashboard",
|
||||
"version": "0.12.0",
|
||||
"version": "0.13.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user