Delete guest accounts left idle for five days
The app had no cleanup of any kind: in multi-user mode every first visit mints a users row, so the demo has been accumulating one permanent account per visitor along with everything they generated. cleanup.py sweeps guests idle for AIDND_GUEST_RETENTION_DAYS (default 5), once at startup and then every few hours. Startup is the load-bearing trigger — the free tier sleeps after ~15 minutes, so a long timer rarely gets to fire. Idle is COALESCE(last_seen_at, created_at), not last_seen_at: _touch only writes that column hourly, and a guest minted by /auth/me has it NULL until its second request, so the simpler query would have deleted brand-new visitors mid-session. It's one Core DELETE rather than db.delete(user), which would SELECT every adventure, action and memory into Python purely to delete them — the same egress pattern as the 189x fix. Every FK from users down is ON DELETE CASCADE, so the database does the whole graph and returns a count. The filter requires is_guest AND email IS NULL, so registered users (who upgrade in place) and local mode's implicit user are both out of reach, and is_public is output-only so a guest can never own content another user can see. Session cookies have no expiry and can outlive a swept row; that path 401s and the frontend's existing retry re-mints a session. Guests are told: /auth/me serves guest_retention_days and the signup modal states the window, sourced from the server so it can't drift from what is enforced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CYEJKobJ2Re4Dv7qUoSA7
This commit is contained in:
co-authored by
Claude Opus 5
parent
c500203270
commit
bbcb07c6be
+16
-1
@@ -732,6 +732,21 @@ guest survives with no re-parenting and no migration step. Three kinds of row sh
|
||||
users table: local (email NULL, not guest), guest (email NULL, guest), registered (email
|
||||
set).
|
||||
|
||||
**Guests expire; accounts don't.** One row per curious visitor adds up, so `cleanup.py`
|
||||
deletes guests idle for `AIDND_GUEST_RETENTION_DAYS` (default 5) — measured as
|
||||
`COALESCE(last_seen_at, created_at)`, because `_touch` only writes `last_seen_at` hourly
|
||||
and a guest minted by `/auth/me` has NULL until its second request. The filter requires
|
||||
both `is_guest` *and* `email IS NULL`, so upgrading in place is also how you opt out of
|
||||
expiry. It runs once at startup (the reliable trigger on a host that sleeps) and then
|
||||
every few hours.
|
||||
|
||||
It's a single Core `DELETE`, not `db.delete(user)`: the ORM path would SELECT every
|
||||
adventure, action and memory into Python purely to delete them, and the FK graph is
|
||||
`ON DELETE CASCADE` from `users` all the way down, so the database can do the whole graph
|
||||
in one statement. Nothing a guest owns is visible to anyone else either — `is_public` is
|
||||
output-only, so shared content is exactly the seeded scenarios, which have `user_id NULL`
|
||||
and never match the filter.
|
||||
|
||||
## 3.2 The shared demo key
|
||||
|
||||
The demo lets people play with no signup and no API key, on a key the server pays for. That
|
||||
@@ -768,7 +783,7 @@ Everything derives from one server-side secret (`AIDND_SECRET_KEY`).
|
||||
| Thing | Mechanism |
|
||||
|---|---|
|
||||
| Passwords | `hashlib.scrypt`, N=2^14, r=8, p=1, per-password salt, constant-time compare. Stdlib, so no extra dependency. |
|
||||
| Sessions | `v1.<user_id>.<HMAC-SHA256>`, no expiry — long-lived guest sessions are the point. |
|
||||
| Sessions | `v1.<user_id>.<HMAC-SHA256>`, no expiry — long-lived guest sessions are the point. A cookie can outlive a swept guest row; that resolves to a 401, which the frontend already turns into a fresh session. |
|
||||
| Stored LLM API keys | Fernet (AES) encryption at rest, key derived from the secret, `enc:` prefix so legacy plaintext rows are recognisable and migratable. |
|
||||
|
||||
The secret auto-generates into a file next to the database for local installs (zero config),
|
||||
|
||||
Reference in New Issue
Block a user