docs(v1.1.8): CHANGELOG entry + v1.1.7-first upgrade note
Mirror the v1.1.7 entry shape: load-bearing upgrade-path warning up top, then Added sections per feature (users SDK / sessions / email verification / password reset / invitations / per-app roles / admin HTTP + dashboard), Changed sections for the two follow-ups (F1 drop plaintext signing key, F3 session realtime auth_mode), Notes with env vars / SDK schema bump / version bumps / @picloud/client unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
163
CHANGELOG.md
163
CHANGELOG.md
@@ -1,5 +1,168 @@
|
||||
# PiCloud Changelog
|
||||
|
||||
## v1.1.8 — User Management (unreleased)
|
||||
|
||||
Per-app data-plane user management — a `users::*` SDK for scripts
|
||||
plus an admin HTTP surface and dashboard tab — together with three
|
||||
load-bearing follow-ups from v1.1.7 (dropping the plaintext realtime
|
||||
signing-key column, `--all-targets` clippy discipline, and adding a
|
||||
session-based realtime SSE auth mode).
|
||||
|
||||
**UPGRADE PATH IS LOAD-BEARING.** v1.1.8 requires v1.1.7 to have
|
||||
been applied first. Operators upgrading directly from v1.1.6 or
|
||||
earlier MUST apply v1.1.7 (and let its startup encryption sweep
|
||||
complete) before applying v1.1.8 — migration 0032's guard refuses
|
||||
to drop the plaintext `realtime_signing_key` column while any row
|
||||
still needs encryption.
|
||||
|
||||
### Added — `users::*` SDK (data-plane user management)
|
||||
|
||||
- **`users::create(#{...})` / `users::get(id)` / `users::find_by_email(...)`
|
||||
/ `users::update(id, #{...})` / `users::delete(id)` /
|
||||
`users::list(#{ "$limit", cursor })`** — CRUD on per-app end-users
|
||||
(distinct from the control-plane `admin_users` table). Identity tuple
|
||||
is `(app_id, user_id)`; uniqueness is on `(app_id, lower(email))` so
|
||||
the same email can exist across two apps but not twice within one.
|
||||
Password hash is Argon2id PHC (same algorithm as `admin_users`);
|
||||
the public AppUser record never carries the hash.
|
||||
- **`users::login(email, password)`** returns a session token string,
|
||||
or `()` on bad credentials. The bad-email and bad-password branches
|
||||
share wall-clock cost via a timing-flat dummy Argon2id verify on
|
||||
email miss. **Required for security**, not polish.
|
||||
- **`users::verify(token)`** returns the user on success (bumping the
|
||||
sliding TTL), `()` on missing / expired / revoked / cross-app.
|
||||
- **`users::logout(token)`** revokes the session row.
|
||||
- `migrations/0026_app_users.sql` + `0027_app_user_sessions.sql`.
|
||||
|
||||
### Added — Sessions
|
||||
|
||||
- `app_user_sessions` table with SHA-256 token hash, sliding TTL
|
||||
(`PICLOUD_APP_USER_SESSION_TTL_HOURS`, default 24h), hard cap
|
||||
(`PICLOUD_APP_USER_SESSION_ABSOLUTE_HOURS`, default 720h = 30d),
|
||||
and explicit `revoked_at` for logout / admin revoke-all / password-
|
||||
reset side-effects. Lookups reject revoked/expired rows
|
||||
immediately, before the weekly GC sweep runs.
|
||||
|
||||
### Added — Email verification
|
||||
|
||||
- **`users::send_verification_email(user_id, #{ link_base, from,
|
||||
subject, body_template })`** mints a 32-byte token, stores its
|
||||
SHA-256 in `app_user_email_verifications`, and dispatches via
|
||||
`email::send` with `{link}` in the body substituted for
|
||||
`link_base?token=<raw>`. `EmailError::NotConfigured` propagates as
|
||||
`UsersError::EmailNotConfigured` so scripts already handling the
|
||||
v1.1.7 email-disabled mode don't need new branches.
|
||||
- **`users::verify_email(token)`** atomically consumes the one-shot
|
||||
token, sets `email_verified_at = NOW()`, emits
|
||||
`users::email_verified`.
|
||||
- `migrations/0028_app_user_email_verifications.sql`.
|
||||
|
||||
### Added — Password reset
|
||||
|
||||
- **`users::request_password_reset(email, #{ template })`** returns
|
||||
`Ok(())` regardless of whether the email matched — no
|
||||
existence-leak signal in script-land. Email-not-configured does
|
||||
surface (operators need to know); other email failures are silently
|
||||
swallowed and logged server-side.
|
||||
- **`users::complete_password_reset(token, new_password)`** atomically
|
||||
consumes the token, updates the Argon2id hash, and **revokes every
|
||||
active session** for the user (stale tokens shouldn't ride out the
|
||||
reset). TTL `PICLOUD_APP_USER_PASSWORD_RESET_TTL_HOURS` (default 1h).
|
||||
- `migrations/0029_app_user_password_resets.sql`.
|
||||
|
||||
### Added — Invitations
|
||||
|
||||
- **`users::invite(email, #{ template?, display_name?, roles? })`**
|
||||
gated on `AppUsersAdmin`. Omitting the template skips the email
|
||||
send so an admin can issue invitations for out-of-band delivery.
|
||||
- **`users::accept_invite(token, password, display_name?)`** atomically
|
||||
consumes the invitation, creates the user with pre-staged roles
|
||||
applied, and mints a fresh session — caller can return both the
|
||||
user and the session token in one round trip. TTL
|
||||
`PICLOUD_APP_USER_INVITATION_TTL_DAYS` (default 7d).
|
||||
- `migrations/0030_app_user_invitations.sql`.
|
||||
|
||||
### Added — Per-app roles
|
||||
|
||||
- **`users::add_role(id, role)` / `users::remove_role(id, role)` /
|
||||
`users::has_role(id, role)`** — string-tagged, per-app. The script
|
||||
app decides what `"admin"` / `"editor"` / `"viewer"` mean. Stored
|
||||
in `app_user_roles` (composite PK so `add` is idempotent).
|
||||
- The full `AppUser` record returned by every `users::*` getter now
|
||||
carries a `roles` array.
|
||||
- `migrations/0031_app_user_roles.sql`.
|
||||
- **Role permission matrices / hierarchy / role registry are
|
||||
explicitly v1.2 work** — v1.1.8 does not pre-commit to a model
|
||||
that would lock in a wrong API.
|
||||
|
||||
### Added — Admin HTTP surface
|
||||
|
||||
- `GET / POST / PATCH / DELETE /api/v1/admin/apps/{id}/users` and
|
||||
`/users/{user_id}` — list, get, create, update, delete.
|
||||
- `POST /users/{user_id}/reset-password` — returns a one-shot reset
|
||||
token in the response body (TTL 1h). No email is sent — that's
|
||||
the SDK's `users::request_password_reset`.
|
||||
- `POST /users/{user_id}/revoke-sessions` — bulk revoke for that
|
||||
user.
|
||||
- `GET / POST / DELETE /api/v1/admin/apps/{id}/invitations` and
|
||||
`/invitations/{invite_id}` — list, create, revoke pending.
|
||||
- Capability gating: `AppUsersRead` for reads, `AppUsersWrite` for
|
||||
CRUD, `AppUsersAdmin` for admin-mediated verbs + invitations. All
|
||||
three map to existing scopes (`script:read` / `script:write`) —
|
||||
**no new scope** (seven-scope commitment).
|
||||
- DTOs never include password hashes, session tokens, or unrotated
|
||||
reset tokens.
|
||||
|
||||
### Added — Dashboard Users tab
|
||||
|
||||
- `apps/[slug]/users/+page.svelte` — list, create form, edit modal,
|
||||
revoke-sessions, reset-password (one-shot token displayed once),
|
||||
delete. Sub-route `users/invitations/+page.svelte` for pending
|
||||
invitations (list, create with optional inline email template,
|
||||
revoke).
|
||||
- The main app-detail tab strip gains a Users entry above Files.
|
||||
- `@picloud/dashboard` 0.13.0 → 0.14.0.
|
||||
|
||||
### Changed — Realtime: drop plaintext signing-key column (F1)
|
||||
|
||||
- `migrations/0032_drop_plaintext_realtime_signing_key.sql` —
|
||||
guard query + `ALTER TABLE app_secrets DROP COLUMN IF EXISTS
|
||||
realtime_signing_key`. The guard refuses to apply if any row still
|
||||
has plaintext but no encrypted counterpart.
|
||||
- v1.1.7's `migrate_plaintext_keys` startup task and its call from
|
||||
`build_app` are deleted; the read path now consults only the
|
||||
encrypted columns.
|
||||
|
||||
### Changed — Realtime: `auth_mode = 'session'` (F3)
|
||||
|
||||
- `migrations/0033_topics_auth_mode_session.sql` widens the
|
||||
`topics_auth_mode_check` CHECK to allow `'session'` alongside
|
||||
`'public'` and `'token'`.
|
||||
- `RealtimeAuthorityImpl` gains a `UsersService` dependency; the
|
||||
Session branch of `authorize_subscribe` delegates to
|
||||
`verify_session_for_realtime(app_id, token)` (same DB lookup +
|
||||
sliding-TTL bump as the SDK's `users::verify`, but app_id is
|
||||
taken from the topic row, not a script).
|
||||
- Dashboard Topics tab gains `session` as a third radio option.
|
||||
- `token` (HMAC) validator is unchanged; both coexist per topic.
|
||||
|
||||
### Notes
|
||||
|
||||
- New env vars (all with documented defaults):
|
||||
- `PICLOUD_APP_USER_SESSION_TTL_HOURS` (24)
|
||||
- `PICLOUD_APP_USER_SESSION_ABSOLUTE_HOURS` (720 = 30d)
|
||||
- `PICLOUD_APP_USER_VERIFICATION_TTL_HOURS` (48)
|
||||
- `PICLOUD_APP_USER_PASSWORD_RESET_TTL_HOURS` (1)
|
||||
- `PICLOUD_APP_USER_INVITATION_TTL_DAYS` (7)
|
||||
- SDK schema 1.8 → 1.9 (additive surface: `users::*` + `session`
|
||||
topic auth mode).
|
||||
- Workspace version 1.1.7 → 1.1.8.
|
||||
- `@picloud/client` does NOT bump in v1.1.8 — its `auth.login` /
|
||||
`auth.logout` helpers already call dev-defined endpoints;
|
||||
nothing to change in the client.
|
||||
- New weekly GC sweep (`spawn_app_user_token_gc`) prunes expired /
|
||||
revoked / consumed rows across the four new token tables.
|
||||
|
||||
## v1.1.7 — Configuration & Email (unreleased)
|
||||
|
||||
The operational-config layer: **encrypted per-app secrets**, **outbound
|
||||
|
||||
Reference in New Issue
Block a user