You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Self-service auth realms (PR #130). Subscriber registration, email
verification, password recovery, rotating refresh tokens, per-realm session
TTLs, and SDK silent auto-refresh — with server-side role resolution and
audience-pinned tokens. See ADR-011 and the Security section below.
Cloudflare Pages deploys repaired. The Pages pipeline had been failing
since v0.18.0. The apps/marketplace submodule is now decoupled from the
pnpm workspace and built standalone (authenticated with a PAT), the docs
deploy verification matches the current prerendered-404 SPA design, and the
release checkout no longer aborts on private sibling submodules. See Fixed.
Added
Self-service auth realms. Subscriber registration (/auth/register) with email verification (/auth/verify-email, /auth/resend-verification), password recovery (/auth/forgot-password, /auth/reset-password), and an admin primitive to grant subscribers read on collections (/api/v1/users/subscriber-access). Tokens carry a per-realm aud (studio/frontend); withStudioAccess hard-rejects frontend tokens. See ADR-011.
Per-realm session TTLs. Separate access-token lifetimes for staff vs subscribers (STUDIO_SESSION_TTL12h / FRONTEND_SESSION_TTL30d).
Rotating refresh tokens (new table lumibase_refresh_tokens, migration 0005). Silent renewal via /auth/refresh, /auth/logout; one-time-use rotation with family-wide reuse detection; tokens stored only as sha256. Per-realm refresh TTL (STUDIO_REFRESH_TTL30d / FRONTEND_REFRESH_TTL90d). Delivered as an httpOnly cookie and in the body.
Cross-domain refresh cookie config (REFRESH_COOKIE_SAMESITE/REFRESH_COOKIE_DOMAIN/REFRESH_COOKIE_SECURE) with a CSRF brake (X-LumiBase-Refresh header required for cookie-sourced refresh/logout).
Authenticated account self-service:POST /api/v1/me/change-password and session management (GET/DELETE /api/v1/me/sessions[/:id]).
Hourly prune of expired refresh tokens on the existing audit-rotation cron (Workers scheduled + Node node-cron).
SDK silent auto-refresh.createLumiClient accepts refreshToken + onTokensRefreshed; a 401 transparently refreshes and retries once (parallel 401s coalesce into one refresh). Studio wires this end to end (login persists the refresh token, logout revokes it server-side).
Security
Public /auth/register is safe by construction (supersedes the admin-only stopgap from #190): the endpoint is intentionally unauthenticated self-service, but the role is resolved server-side to a zero-privilege subscriber (appAccess=false, adminAccess=false) — the request body can never choose a role — and the account starts invited until email verification. Per-IP rate-limited and anti-enumeration (uniform 202).
Password change/reset kills every outstanding session: both handlers stamp users.password_changed_at (migration 0006), bump tokenVersion (so all prior access JWTs die immediately, CWE-613/620), and revoke all refresh tokens. A reset token whose iat predates password_changed_at is rejected → single-use reset links (review finding H1).
Global unique email (review finding H3): unique index on lower(email) (migration 0006) closes the check-then-insert registration race that could create duplicate accounts for one email; registration maps the constraint violation to the same generic 202.
Atomic refresh-token rotation (review finding M1): rotation claims the row with a conditional UPDATE ... WHERE revoked_at IS NULL, so two concurrent /refresh calls can no longer both succeed — the loser is treated as reuse and the family is revoked.
Session Bearer verifier pins audience (review finding M5): verifyCustomJwt requires aud ∈ {studio, frontend}, so a single-purpose email-verify/password-reset JWT can never be replayed as a session token even if its claim shape changes.
/auth/refresh re-checks tenant membership + recomputes realm (review finding M4): a user removed from the site, or whose role lost appAccess, no longer keeps minting stale-audience access tokens. Renewed access JWTs embed the current tokenVersion.
lumibase_refresh_tokens under RLS (review finding M6): added to rls-policies.sqlsite_isolation alongside the other tenant tables.
Notes
Run pnpm -F @lumibase/database migrate to apply migrations 0005 (adds lumibase_refresh_tokens) and 0006 (adds users.password_changed_atand the unique lower(email) index). Migration 0006 fails if the users table already contains case-insensitive duplicate emails — de-duplicate first; see the migration header. No other backfill required.
Known limitations (tracked follow-ups, not fixed here): per-IP rate limiting relies on LUMIBASE_TRUSTED_PROXIES being configured (and, off Cloudflare, a wired remote-address resolver) — the same limitation the login-guard already carries (review finding H2); refresh rotation grants a fresh TTL per hop with no absolute session cap (M2); the refresh cookie is one host-scoped name across tenants on a shared host (L2). See docs/en/security/user-management.md.
Fixed
Cloudflare Pages deploys (broken since v0.18.0). Four defects kept the
Pages pipeline red:
apps/marketplace (a private lumibase-ai/marketplace submodule) is now decoupled from the pnpm workspace (!apps/marketplace) and built
standalone (pnpm install --no-frozen-lockfile --ignore-workspace), so the
root --frozen-lockfile install no longer breaks when the submodule is
present. It is versioned/released independently and dropped from version:sync.
The submodule clone now authenticates with the MARKETPLACE_SUBMODULE_TOKEN
PAT (the runner's GITHUB_TOKEN cannot read another org's repo).
release.yml no longer uses a blanket submodules: true checkout, which
aborted on the private enterprise/extensions submodules; it inits only
the marketplace path.
The docs deploy verification now asserts the current design — an unmatched
deep-link serves the prerendered 404.html SPA shell (HTTP 404 + shell
body) — instead of the removed /* /index.html 200 catch-all.
Migrations
0005_refresh_tokens.sql — adds lumibase_refresh_tokens (rotating refresh
tokens, under RLS).
0006_password_changed_at_and_email_unique.sql — adds users.password_changed_at and a unique index on lower(email). Fails if
the users table already contains case-insensitive duplicate emails —
de-duplicate first (see the migration header). Apply with pnpm -F @lumibase/database migrate.