refactor: use actors for one-time access and signup tokens

Move one-time access tokens and signup tokens onto the Francis actor
framework, so their state is coordinated cluster-wide and expired entries
are purged without the periodic db_cleanup job.

One-time access tokens:
- Each token is its own actor, keyed by the token value, storing the user
  ID and optional device token in actor state with a TTL matching the
  token's expiration. Expired state is purged automatically.
- Exchange consumes the token by invoking the actor (atomic validate +
  delete) outside of any DB transaction, then loads the user and issues an
  access token. On a later failure it compensates by restoring the token
  (best-effort, using a non-cancelable context).
- The one_time_access_tokens table is dropped; the CLI writes token state
  through a minimal, non-running actor host.

Signup tokens:
- A singleton actor holds every signup token in its state and keeps a
  single cleanup alarm scheduled for the earliest expiration; when it
  fires it purges expired tokens and reschedules. Listing uses Peek.
- Sign-up consumes a token by invoking the actor to atomically increment
  its usage count, performs user creation in a transaction, and, on
  failure, compensates by releasing the token (best-effort).
- Existing tokens are migrated from the database into the actor state on
  first startup; the tables are retained only for that migration.

Also removes ClearOneTimeAccessTokens and ClearSignupTokens, and lets the
importer skip tables no longer present in the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBMz9v9s8S8RPYNWzshkBu
This commit is contained in:
Claude
2026-07-20 21:12:46 +00:00
parent 2cfbcb4b67
commit 7c59dcdb82
24 changed files with 1716 additions and 441 deletions
-66
View File
@@ -238,72 +238,6 @@
"user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368"
}
],
"one_time_access_tokens": [
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-25T13:39:02Z",
"id": "bf877753-4ea4-4c9c-bbbd-e198bb201cb8",
"token": "HPe6k6uiDRRVuAQV",
"device_token": null,
"user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e"
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-25T12:39:01Z",
"id": "d3afae24-fe2d-4a98-abec-cf0b8525096a",
"token": "YCGDtftvsvYWiXd0",
"device_token": null,
"user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e"
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-25T13:39:02Z",
"id": "defd5164-9d9b-4228-bbce-708e33f49360",
"token": "one-time-token",
"device_token": null,
"user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e"
}
],
"signup_tokens": [
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-26T12:39:02Z",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"token": "VALID1234567890A",
"usage_count": 0,
"usage_limit": 1
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-12-02T12:39:02Z",
"id": "dc3c9c96-714e-48eb-926e-2d7c7858e6cf",
"token": "PARTIAL567890ABC",
"usage_count": 2,
"usage_limit": 5
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-24T12:39:02Z",
"id": "44de1863-ffa5-4db1-9507-4887cd7a1e3f",
"token": "EXPIRED34567890B",
"usage_count": 1,
"usage_limit": 3
},
{
"created_at": "2025-11-25T12:39:02Z",
"expires_at": "2025-11-26T12:39:02Z",
"id": "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b",
"token": "FULLYUSED567890C",
"usage_count": 1,
"usage_limit": 1
}
],
"signup_tokens_user_groups": [
{
"signup_token_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368"
}
],
"user_authorized_oidc_clients": [
{
"client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055",