Mercurial
comparison auth/auth_store.h @ 264:04fee26ecce0
add authenticated JRPG conversation platform
Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 07 Aug 2026 07:34:12 -0700 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 #ifndef ZENBU_AUTH_STORE_H | |
| 2 #define ZENBU_AUTH_STORE_H | |
| 3 | |
| 4 #include "dowa/dowa.h" | |
| 5 #include "auth/auth_crypto.h" | |
| 6 | |
| 7 #define AUTH_STORE_USERNAME_MIN 3 | |
| 8 #define AUTH_STORE_USERNAME_MAX 32 | |
| 9 | |
| 10 typedef struct Auth_Store Auth_Store; | |
| 11 | |
| 12 typedef enum { | |
| 13 AUTH_STORE_ERROR = -1, | |
| 14 AUTH_STORE_OK = 0, | |
| 15 AUTH_STORE_NOT_FOUND = 1, | |
| 16 AUTH_STORE_CONFLICT = 2, | |
| 17 AUTH_STORE_EXPIRED = 3, | |
| 18 AUTH_STORE_REVOKED = 4, | |
| 19 AUTH_STORE_USER_DISABLED = 5, | |
| 20 AUTH_STORE_STALE_PASSWORD = 6, | |
| 21 AUTH_STORE_LAST_ADMIN = 7, | |
| 22 AUTH_STORE_INVALID_ARG = 8, | |
| 23 } Auth_Store_Result; | |
| 24 | |
| 25 typedef enum { | |
| 26 AUTH_STORE_BOOTSTRAP_CREATED = 0, | |
| 27 AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT = 1, | |
| 28 } Auth_Store_Bootstrap_Result; | |
| 29 | |
| 30 /* | |
| 31 * Public user record — password_hash is never included here. | |
| 32 * All fields are fixed-size; no arena pointer is needed. | |
| 33 */ | |
| 34 typedef struct { | |
| 35 char id[37]; | |
| 36 char username[AUTH_STORE_USERNAME_MAX + 1]; | |
| 37 char normalized_username[AUTH_STORE_USERNAME_MAX + 1]; | |
| 38 char role[8]; /* "admin" or "member" */ | |
| 39 char status[9]; /* "active" or "disabled" */ | |
| 40 boolean must_change_password; | |
| 41 int64 password_changed_at; | |
| 42 int64 created_at; | |
| 43 int64 updated_at; | |
| 44 } Auth_User_Record; | |
| 45 | |
| 46 /* | |
| 47 * Authentication lookup record — includes the encoded password hash. | |
| 48 * Callers must zero this struct after use; never log the hash field. | |
| 49 */ | |
| 50 typedef struct { | |
| 51 Auth_User_Record user; | |
| 52 char password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; | |
| 53 } Auth_User_Auth_Record; | |
| 54 | |
| 55 /* | |
| 56 * Session record — token/CSRF digests are not exposed here; the caller | |
| 57 * already holds them before calling Create_Session or Find_Session. | |
| 58 */ | |
| 59 typedef struct { | |
| 60 char user_id[37]; | |
| 61 int64 created_at; | |
| 62 int64 last_seen_at; | |
| 63 int64 idle_expires_at; | |
| 64 int64 absolute_expires_at; | |
| 65 int64 password_changed_at_snapshot; | |
| 66 } Auth_Session_Record; | |
| 67 | |
| 68 typedef struct { | |
| 69 char id[37]; | |
| 70 int64 created_at; | |
| 71 int64 last_seen_at; | |
| 72 int64 expires_at; | |
| 73 } Auth_Guest_Identity_Record; | |
| 74 | |
| 75 /* --- Store lifecycle --- */ | |
| 76 | |
| 77 Auth_Store *Auth_Store_Create(const char *database_path); | |
| 78 void Auth_Store_Destroy(Auth_Store *p_store); | |
| 79 | |
| 80 /* --- Username utilities --- */ | |
| 81 | |
| 82 /* | |
| 83 * Trim ASCII spaces, lowercase, validate character set and length. | |
| 84 * Writes the normalized form to 'normalized' (capacity must include NUL). | |
| 85 * Returns TRUE on success; FALSE if invalid or buffer too small. | |
| 86 */ | |
| 87 boolean Auth_Store_Normalize_Username( | |
| 88 const char *username, | |
| 89 char *normalized, | |
| 90 size_t capacity); | |
| 91 | |
| 92 /* | |
| 93 * Validate a pre-normalized username (lowercase, no leading/trailing spaces). | |
| 94 * Returns TRUE iff length and character set are within policy. | |
| 95 */ | |
| 96 boolean Auth_Store_Validate_Username(const char *normalized_username); | |
| 97 | |
| 98 /* --- User management --- */ | |
| 99 | |
| 100 Auth_Store_Result Auth_Store_Create_User( | |
| 101 Auth_Store *p_store, | |
| 102 const char *username, | |
| 103 const char *encoded_hash, | |
| 104 const char *role, | |
| 105 boolean must_change_password, | |
| 106 char output_id[37]); | |
| 107 | |
| 108 /* Create a user and its audit record in one transaction. */ | |
| 109 Auth_Store_Result Auth_Store_Create_User_Audited( | |
| 110 Auth_Store *p_store, | |
| 111 const char *username, | |
| 112 const char *encoded_hash, | |
| 113 const char *role, | |
| 114 boolean must_change_password, | |
| 115 const char *actor_user_id, | |
| 116 char output_id[37]); | |
| 117 | |
| 118 /* | |
| 119 * Idempotent: creates an admin user only when no admin exists at all. | |
| 120 * p_bootstrap_result receives CREATED or ALREADY_PRESENT. | |
| 121 * output_id receives the UUID (new or existing) and may be NULL. | |
| 122 */ | |
| 123 Auth_Store_Result Auth_Store_Bootstrap_Admin( | |
| 124 Auth_Store *p_store, | |
| 125 const char *username, | |
| 126 const char *encoded_hash, | |
| 127 Auth_Store_Bootstrap_Result *p_bootstrap_result, | |
| 128 char output_id[37]); | |
| 129 | |
| 130 /* Includes password_hash for authentication — zero record after use. */ | |
| 131 Auth_Store_Result Auth_Store_Find_User_By_Username( | |
| 132 Auth_Store *p_store, | |
| 133 const char *username, | |
| 134 Auth_User_Auth_Record *p_record); | |
| 135 | |
| 136 Auth_Store_Result Auth_Store_Get_User( | |
| 137 Auth_Store *p_store, | |
| 138 const char *user_id, | |
| 139 Auth_User_Record *p_record); | |
| 140 | |
| 141 /* | |
| 142 * List all users into a Dowa arena-backed array. | |
| 143 * After a successful call, use Dowa_Array_Length(*pp_records) for count. | |
| 144 */ | |
| 145 Auth_Store_Result Auth_Store_List_Users( | |
| 146 Auth_Store *p_store, | |
| 147 Auth_User_Record **pp_records, | |
| 148 Dowa_Arena *p_arena); | |
| 149 | |
| 150 /* | |
| 151 * new_status must be "active" or "disabled". | |
| 152 * Fails with AUTH_STORE_LAST_ADMIN if disabling the last active admin. | |
| 153 */ | |
| 154 Auth_Store_Result Auth_Store_Update_User_Status( | |
| 155 Auth_Store *p_store, | |
| 156 const char *user_id, | |
| 157 const char *new_status, | |
| 158 const char *actor_user_id); | |
| 159 | |
| 160 /* Atomically enable a user and write audit; revoked sessions stay revoked. */ | |
| 161 Auth_Store_Result Auth_Store_Enable_User( | |
| 162 Auth_Store *p_store, | |
| 163 const char *user_id, | |
| 164 const char *actor_user_id); | |
| 165 | |
| 166 /* Atomically enforce last-admin policy, disable, revoke, and write audit. */ | |
| 167 Auth_Store_Result Auth_Store_Disable_User_And_Revoke_Sessions( | |
| 168 Auth_Store *p_store, | |
| 169 const char *user_id, | |
| 170 const char *actor_user_id); | |
| 171 | |
| 172 /* | |
| 173 * new_role must be "admin" or "member". | |
| 174 * Fails with AUTH_STORE_LAST_ADMIN if demoting the last active admin. | |
| 175 */ | |
| 176 Auth_Store_Result Auth_Store_Update_User_Role( | |
| 177 Auth_Store *p_store, | |
| 178 const char *user_id, | |
| 179 const char *new_role, | |
| 180 const char *actor_user_id); | |
| 181 | |
| 182 /* Atomically enforce last-admin policy, update role, revoke, and audit. */ | |
| 183 Auth_Store_Result Auth_Store_Update_Role_And_Revoke_Sessions( | |
| 184 Auth_Store *p_store, | |
| 185 const char *user_id, | |
| 186 const char *new_role, | |
| 187 const char *actor_user_id); | |
| 188 | |
| 189 Auth_Store_Result Auth_Store_Set_Must_Change_Password( | |
| 190 Auth_Store *p_store, | |
| 191 const char *user_id, | |
| 192 boolean value, | |
| 193 const char *actor_user_id); | |
| 194 | |
| 195 /* | |
| 196 * Updates the encoded password hash; clears must_change_password. | |
| 197 * If revoke_other_sessions is TRUE, all sessions except keep_token_digest | |
| 198 * (which may be NULL) are revoked atomically in the same transaction. | |
| 199 */ | |
| 200 Auth_Store_Result Auth_Store_Update_Password( | |
| 201 Auth_Store *p_store, | |
| 202 const char *user_id, | |
| 203 const char *new_encoded_hash, | |
| 204 boolean revoke_other_sessions, | |
| 205 const char *keep_token_digest); | |
| 206 | |
| 207 /* --- Session management --- */ | |
| 208 | |
| 209 /* | |
| 210 * Creates a new session. Fails if the user is disabled. | |
| 211 * current_unix is the caller-supplied Unix timestamp. | |
| 212 */ | |
| 213 Auth_Store_Result Auth_Store_Create_Session( | |
| 214 Auth_Store *p_store, | |
| 215 const char *user_id, | |
| 216 const char *token_digest, | |
| 217 const char *csrf_digest, | |
| 218 int64 idle_ttl_secs, | |
| 219 int64 absolute_ttl_secs, | |
| 220 int64 current_unix, | |
| 221 Auth_Session_Record *p_record); | |
| 222 | |
| 223 /* | |
| 224 * CAS session create: only creates a session if the user's current | |
| 225 * password_hash still exactly matches expected_password_hash. | |
| 226 * Returns AUTH_STORE_STALE_PASSWORD when an administrator changed it. | |
| 227 * The caller must cleanse expected_password_hash after this call. | |
| 228 */ | |
| 229 Auth_Store_Result Auth_Store_Create_Session_CAS( | |
| 230 Auth_Store *p_store, | |
| 231 const char *user_id, | |
| 232 const char *expected_password_hash, | |
| 233 const char *token_digest, | |
| 234 const char *csrf_digest, | |
| 235 int64 idle_ttl_secs, | |
| 236 int64 absolute_ttl_secs, | |
| 237 int64 current_unix, | |
| 238 Auth_Session_Record *p_record); | |
| 239 | |
| 240 /* | |
| 241 * Resolves a session by token_digest. | |
| 242 * Returns: | |
| 243 * OK — session is valid; p_session and p_user are populated. | |
| 244 * NOT_FOUND — no such session. | |
| 245 * REVOKED — session was explicitly revoked. | |
| 246 * EXPIRED — idle or absolute expiry exceeded. | |
| 247 * USER_DISABLED — session owner has been disabled. | |
| 248 * STALE_PASSWORD — password changed after session was created. | |
| 249 */ | |
| 250 Auth_Store_Result Auth_Store_Find_Session( | |
| 251 Auth_Store *p_store, | |
| 252 const char *token_digest, | |
| 253 int64 current_unix, | |
| 254 Auth_Session_Record *p_session, | |
| 255 Auth_User_Record *p_user); | |
| 256 | |
| 257 /* Extends the idle expiry; does nothing if the session is revoked. */ | |
| 258 Auth_Store_Result Auth_Store_Touch_Session( | |
| 259 Auth_Store *p_store, | |
| 260 const char *token_digest, | |
| 261 int64 current_unix, | |
| 262 int64 idle_ttl_secs); | |
| 263 | |
| 264 Auth_Store_Result Auth_Store_Revoke_Session( | |
| 265 Auth_Store *p_store, | |
| 266 const char *token_digest); | |
| 267 | |
| 268 /* | |
| 269 * Revokes all sessions for user_id. | |
| 270 * If except_token_digest is non-NULL, that session is preserved. | |
| 271 */ | |
| 272 Auth_Store_Result Auth_Store_Revoke_All_Sessions( | |
| 273 Auth_Store *p_store, | |
| 274 const char *user_id, | |
| 275 const char *except_token_digest); | |
| 276 | |
| 277 /* Atomically revoke sessions and write one audit record. */ | |
| 278 Auth_Store_Result Auth_Store_Revoke_All_Sessions_Audited( | |
| 279 Auth_Store *p_store, | |
| 280 const char *user_id, | |
| 281 const char *except_token_digest, | |
| 282 const char *actor_user_id); | |
| 283 | |
| 284 /* | |
| 285 * Atomically create a new session and revoke the old one in a single | |
| 286 * transaction. Use this after a password change to rotate the current | |
| 287 * session without any window where neither or both sessions are valid. | |
| 288 * | |
| 289 * old_token_digest: the existing session to revoke (must not be NULL or ""). | |
| 290 * If old_token_digest is already revoked the rotation still succeeds and | |
| 291 * the new session is created. | |
| 292 * All other parameters are the same as Auth_Store_Create_Session. | |
| 293 */ | |
| 294 Auth_Store_Result Auth_Store_Rotate_Session( | |
| 295 Auth_Store *p_store, | |
| 296 const char *user_id, | |
| 297 const char *old_token_digest, | |
| 298 const char *new_token_digest, | |
| 299 const char *new_csrf_digest, | |
| 300 int64 idle_ttl_secs, | |
| 301 int64 absolute_ttl_secs, | |
| 302 int64 current_unix, | |
| 303 Auth_Session_Record *p_record); | |
| 304 | |
| 305 /* | |
| 306 * Atomically compare-and-swap the password hash, clear forced change, | |
| 307 * revoke every existing session, and create one replacement session. | |
| 308 * On AUTH_STORE_STALE_PASSWORD no state is changed. The caller must cleanse | |
| 309 * both password hashes after this call. | |
| 310 */ | |
| 311 Auth_Store_Result Auth_Store_Self_Change_Password( | |
| 312 Auth_Store *p_store, | |
| 313 const char *user_id, | |
| 314 const char *old_encoded_hash, | |
| 315 const char *new_encoded_hash, | |
| 316 const char *new_token_digest, | |
| 317 const char *new_csrf_digest, | |
| 318 int64 idle_ttl_secs, | |
| 319 int64 absolute_ttl_secs, | |
| 320 int64 current_unix, | |
| 321 Auth_Session_Record *p_record); | |
| 322 | |
| 323 /* --- Guest quota --- */ | |
| 324 | |
| 325 typedef enum { | |
| 326 AUTH_STORE_GUEST_QUOTA_OK = 0, | |
| 327 AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED = 1, | |
| 328 AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED = 2, | |
| 329 AUTH_STORE_GUEST_QUOTA_ERROR = -1, | |
| 330 } Auth_Store_Guest_Quota_Result; | |
| 331 | |
| 332 typedef struct { | |
| 333 int64 turns_used; | |
| 334 int64 output_tokens_used; | |
| 335 int64 output_tokens_reserved; | |
| 336 } Auth_Store_Guest_Usage; | |
| 337 | |
| 338 /* | |
| 339 * Read current daily usage for a guest window. | |
| 340 * window_start: UTC midnight Unix timestamp for the current day. | |
| 341 * Returns OK with zeroed usage if no row exists yet, or ERROR. | |
| 342 */ | |
| 343 Auth_Store_Result Auth_Store_Guest_Get_Usage( | |
| 344 Auth_Store *p_store, | |
| 345 const char *guest_id, | |
| 346 int64 window_start, | |
| 347 Auth_Store_Guest_Usage *p_usage); | |
| 348 | |
| 349 /* | |
| 350 * Atomically reserve quota for one inference turn. | |
| 351 * On success: increments turns_used by 1 and output_tokens_reserved by | |
| 352 * max_output_tokens in the usage row for window_start. | |
| 353 * Invariant after every reservation: | |
| 354 * output_tokens_used + output_tokens_reserved <= tokens_limit. | |
| 355 * Returns TURNS_EXHAUSTED or TOKENS_EXHAUSTED when the limit would be exceeded. | |
| 356 * reservation_expires: Unix timestamp after which the reservation may be | |
| 357 * discarded by cleanup; the turn charge (turns_used) persists regardless. | |
| 358 */ | |
| 359 Auth_Store_Guest_Quota_Result Auth_Store_Guest_Reserve( | |
| 360 Auth_Store *p_store, | |
| 361 const char *guest_id, | |
| 362 const char *request_id, | |
| 363 int64 window_start, | |
| 364 int64 max_output_tokens, | |
| 365 int64 turns_limit, | |
| 366 int64 tokens_limit, | |
| 367 int64 reservation_expires); | |
| 368 | |
| 369 /* | |
| 370 * Reconcile a completed turn with the actual provider output token count. | |
| 371 * actual_output_tokens is capped to the reserved amount (fail closed; if | |
| 372 * the provider reports more than reserved, the reserved amount is charged). | |
| 373 * Adds capped tokens to output_tokens_used; subtracts reserved from | |
| 374 * output_tokens_reserved; deletes the reservation row. | |
| 375 * Idempotent by request_id: safe to call when already reconciled. | |
| 376 */ | |
| 377 Auth_Store_Result Auth_Store_Guest_Reconcile( | |
| 378 Auth_Store *p_store, | |
| 379 const char *request_id, | |
| 380 int64 actual_output_tokens); | |
| 381 | |
| 382 /* | |
| 383 * Release a reservation on failure or abort. | |
| 384 * Decrements output_tokens_reserved by the reserved amount; does NOT | |
| 385 * change turns_used (the turn charge is retained). | |
| 386 * Deletes the reservation row. | |
| 387 * Idempotent by request_id. | |
| 388 */ | |
| 389 Auth_Store_Result Auth_Store_Guest_Release( | |
| 390 Auth_Store *p_store, | |
| 391 const char *request_id); | |
| 392 | |
| 393 /* | |
| 394 * Clear all outstanding reservations for a guest. | |
| 395 * Used after a successful guest→user login transfer. | |
| 396 * Decrements output_tokens_reserved for each affected usage window. | |
| 397 * Does NOT change turns_used. | |
| 398 */ | |
| 399 Auth_Store_Result Auth_Store_Guest_Clear_Reservations( | |
| 400 Auth_Store *p_store, | |
| 401 const char *guest_id); | |
| 402 | |
| 403 /* | |
| 404 * Atomically reap expired reservations: decrement output_tokens_reserved | |
| 405 * in the matching guest_usage rows and delete expired reservation rows, all | |
| 406 * in one transaction. Uses MAX(0,...) to avoid underflow. | |
| 407 * Idempotent: safe to call repeatedly with the same or decreasing current_unix. | |
| 408 * Called at store startup and before quota read/reserve operations. | |
| 409 */ | |
| 410 Auth_Store_Result Auth_Store_Guest_Reap_Expired( | |
| 411 Auth_Store *p_store, | |
| 412 int64 current_unix); | |
| 413 | |
| 414 /* --- Guest identity --- */ | |
| 415 | |
| 416 /* | |
| 417 * Insert or refresh a guest identity. | |
| 418 * ip_binding_digest must be an HMAC digest — never the raw IP address. | |
| 419 * last_seen_at is updated to now; expires_at is refreshed. | |
| 420 */ | |
| 421 Auth_Store_Result Auth_Store_Upsert_Guest_Identity( | |
| 422 Auth_Store *p_store, | |
| 423 const char *guest_id, | |
| 424 const char *ip_binding_digest, | |
| 425 int64 expires_at, | |
| 426 Auth_Guest_Identity_Record *p_record); | |
| 427 | |
| 428 Auth_Store_Result Auth_Store_Find_Guest_Identity( | |
| 429 Auth_Store *p_store, | |
| 430 const char *guest_id, | |
| 431 int64 current_unix, | |
| 432 Auth_Guest_Identity_Record *p_record); | |
| 433 | |
| 434 /* --- Audit log --- */ | |
| 435 | |
| 436 /* | |
| 437 * Append one bounded audit entry. actor/target/detail may be NULL. | |
| 438 * Never include passwords, hashes, tokens, or session digests. | |
| 439 * Returns OK or ERROR. | |
| 440 */ | |
| 441 Auth_Store_Result Auth_Store_Insert_Audit_Log( | |
| 442 Auth_Store *p_store, | |
| 443 const char *actor_user_id, | |
| 444 const char *action, | |
| 445 const char *target_user_id, | |
| 446 const char *detail); | |
| 447 | |
| 448 /* --- Admin operations --- */ | |
| 449 | |
| 450 /* | |
| 451 * Atomically: update password hash, set must_change_password=1, | |
| 452 * revoke all target sessions, write audit row. | |
| 453 * Use for admin-initiated temporary-password reset only. | |
| 454 * new_encoded_hash must be a valid encoded zenbu-scrypt hash. | |
| 455 */ | |
| 456 Auth_Store_Result Auth_Store_Admin_Reset_Password( | |
| 457 Auth_Store *p_store, | |
| 458 const char *user_id, | |
| 459 const char *new_encoded_hash, | |
| 460 const char *actor_user_id); | |
| 461 | |
| 462 #endif |