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
|
# 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)
|
## v1.1.6 — Realtime Channels & Client Library (unreleased)
|
||||||
|
|
||||||
The first **external realtime surface** and the first **frontend
|
The first **external realtime surface** and the first **frontend
|
||||||
|
|||||||
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -1754,7 +1754,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud"
|
name = "picloud"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1783,7 +1783,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-cli"
|
name = "picloud-cli"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
@@ -1804,7 +1804,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-executor"
|
name = "picloud-executor"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"picloud-executor-core",
|
"picloud-executor-core",
|
||||||
@@ -1816,7 +1816,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-executor-core"
|
name = "picloud-executor-core"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -1840,7 +1840,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-manager"
|
name = "picloud-manager"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"picloud-manager-core",
|
"picloud-manager-core",
|
||||||
@@ -1852,7 +1852,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-manager-core"
|
name = "picloud-manager-core"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1883,7 +1883,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-orchestrator"
|
name = "picloud-orchestrator"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"picloud-orchestrator-core",
|
"picloud-orchestrator-core",
|
||||||
@@ -1895,7 +1895,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-orchestrator-core"
|
name = "picloud-orchestrator-core"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -1918,7 +1918,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "picloud-shared"
|
name = "picloud-shared"
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.1.6"
|
version = "1.1.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.92"
|
rust-version = "1.92"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -58,7 +58,15 @@ pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
/// `RealtimeBroadcaster` / `RealtimeEvent` / `RealtimeAuthority` traits;
|
/// `RealtimeBroadcaster` / `RealtimeEvent` / `RealtimeAuthority` traits;
|
||||||
/// the `topics` registry + admin endpoints; the `@picloud/client`
|
/// the `topics` registry + admin endpoints; the `@picloud/client`
|
||||||
/// TypeScript package).
|
/// 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}/...`.
|
/// HTTP API major version. Appears in URL paths as `/api/v{N}/...`.
|
||||||
/// Bump (new integer + new URL prefix) when the request/response
|
/// Bump (new integer + new URL prefix) when the request/response
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picloud-dashboard",
|
"name": "picloud-dashboard",
|
||||||
"version": "0.12.0",
|
"version": "0.13.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user