Mercurial
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/auth/auth_store.h Fri Aug 07 07:34:12 2026 -0700 @@ -0,0 +1,462 @@ +#ifndef ZENBU_AUTH_STORE_H +#define ZENBU_AUTH_STORE_H + +#include "dowa/dowa.h" +#include "auth/auth_crypto.h" + +#define AUTH_STORE_USERNAME_MIN 3 +#define AUTH_STORE_USERNAME_MAX 32 + +typedef struct Auth_Store Auth_Store; + +typedef enum { + AUTH_STORE_ERROR = -1, + AUTH_STORE_OK = 0, + AUTH_STORE_NOT_FOUND = 1, + AUTH_STORE_CONFLICT = 2, + AUTH_STORE_EXPIRED = 3, + AUTH_STORE_REVOKED = 4, + AUTH_STORE_USER_DISABLED = 5, + AUTH_STORE_STALE_PASSWORD = 6, + AUTH_STORE_LAST_ADMIN = 7, + AUTH_STORE_INVALID_ARG = 8, +} Auth_Store_Result; + +typedef enum { + AUTH_STORE_BOOTSTRAP_CREATED = 0, + AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT = 1, +} Auth_Store_Bootstrap_Result; + +/* + * Public user record — password_hash is never included here. + * All fields are fixed-size; no arena pointer is needed. + */ +typedef struct { + char id[37]; + char username[AUTH_STORE_USERNAME_MAX + 1]; + char normalized_username[AUTH_STORE_USERNAME_MAX + 1]; + char role[8]; /* "admin" or "member" */ + char status[9]; /* "active" or "disabled" */ + boolean must_change_password; + int64 password_changed_at; + int64 created_at; + int64 updated_at; +} Auth_User_Record; + +/* + * Authentication lookup record — includes the encoded password hash. + * Callers must zero this struct after use; never log the hash field. + */ +typedef struct { + Auth_User_Record user; + char password_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE]; +} Auth_User_Auth_Record; + +/* + * Session record — token/CSRF digests are not exposed here; the caller + * already holds them before calling Create_Session or Find_Session. + */ +typedef struct { + char user_id[37]; + int64 created_at; + int64 last_seen_at; + int64 idle_expires_at; + int64 absolute_expires_at; + int64 password_changed_at_snapshot; +} Auth_Session_Record; + +typedef struct { + char id[37]; + int64 created_at; + int64 last_seen_at; + int64 expires_at; +} Auth_Guest_Identity_Record; + +/* --- Store lifecycle --- */ + +Auth_Store *Auth_Store_Create(const char *database_path); +void Auth_Store_Destroy(Auth_Store *p_store); + +/* --- Username utilities --- */ + +/* + * Trim ASCII spaces, lowercase, validate character set and length. + * Writes the normalized form to 'normalized' (capacity must include NUL). + * Returns TRUE on success; FALSE if invalid or buffer too small. + */ +boolean Auth_Store_Normalize_Username( + const char *username, + char *normalized, + size_t capacity); + +/* + * Validate a pre-normalized username (lowercase, no leading/trailing spaces). + * Returns TRUE iff length and character set are within policy. + */ +boolean Auth_Store_Validate_Username(const char *normalized_username); + +/* --- User management --- */ + +Auth_Store_Result Auth_Store_Create_User( + Auth_Store *p_store, + const char *username, + const char *encoded_hash, + const char *role, + boolean must_change_password, + char output_id[37]); + +/* Create a user and its audit record in one transaction. */ +Auth_Store_Result Auth_Store_Create_User_Audited( + Auth_Store *p_store, + const char *username, + const char *encoded_hash, + const char *role, + boolean must_change_password, + const char *actor_user_id, + char output_id[37]); + +/* + * Idempotent: creates an admin user only when no admin exists at all. + * p_bootstrap_result receives CREATED or ALREADY_PRESENT. + * output_id receives the UUID (new or existing) and may be NULL. + */ +Auth_Store_Result Auth_Store_Bootstrap_Admin( + Auth_Store *p_store, + const char *username, + const char *encoded_hash, + Auth_Store_Bootstrap_Result *p_bootstrap_result, + char output_id[37]); + +/* Includes password_hash for authentication — zero record after use. */ +Auth_Store_Result Auth_Store_Find_User_By_Username( + Auth_Store *p_store, + const char *username, + Auth_User_Auth_Record *p_record); + +Auth_Store_Result Auth_Store_Get_User( + Auth_Store *p_store, + const char *user_id, + Auth_User_Record *p_record); + +/* + * List all users into a Dowa arena-backed array. + * After a successful call, use Dowa_Array_Length(*pp_records) for count. + */ +Auth_Store_Result Auth_Store_List_Users( + Auth_Store *p_store, + Auth_User_Record **pp_records, + Dowa_Arena *p_arena); + +/* + * new_status must be "active" or "disabled". + * Fails with AUTH_STORE_LAST_ADMIN if disabling the last active admin. + */ +Auth_Store_Result Auth_Store_Update_User_Status( + Auth_Store *p_store, + const char *user_id, + const char *new_status, + const char *actor_user_id); + +/* Atomically enable a user and write audit; revoked sessions stay revoked. */ +Auth_Store_Result Auth_Store_Enable_User( + Auth_Store *p_store, + const char *user_id, + const char *actor_user_id); + +/* Atomically enforce last-admin policy, disable, revoke, and write audit. */ +Auth_Store_Result Auth_Store_Disable_User_And_Revoke_Sessions( + Auth_Store *p_store, + const char *user_id, + const char *actor_user_id); + +/* + * new_role must be "admin" or "member". + * Fails with AUTH_STORE_LAST_ADMIN if demoting the last active admin. + */ +Auth_Store_Result Auth_Store_Update_User_Role( + Auth_Store *p_store, + const char *user_id, + const char *new_role, + const char *actor_user_id); + +/* Atomically enforce last-admin policy, update role, revoke, and audit. */ +Auth_Store_Result Auth_Store_Update_Role_And_Revoke_Sessions( + Auth_Store *p_store, + const char *user_id, + const char *new_role, + const char *actor_user_id); + +Auth_Store_Result Auth_Store_Set_Must_Change_Password( + Auth_Store *p_store, + const char *user_id, + boolean value, + const char *actor_user_id); + +/* + * Updates the encoded password hash; clears must_change_password. + * If revoke_other_sessions is TRUE, all sessions except keep_token_digest + * (which may be NULL) are revoked atomically in the same transaction. + */ +Auth_Store_Result Auth_Store_Update_Password( + Auth_Store *p_store, + const char *user_id, + const char *new_encoded_hash, + boolean revoke_other_sessions, + const char *keep_token_digest); + +/* --- Session management --- */ + +/* + * Creates a new session. Fails if the user is disabled. + * current_unix is the caller-supplied Unix timestamp. + */ +Auth_Store_Result Auth_Store_Create_Session( + Auth_Store *p_store, + const char *user_id, + const char *token_digest, + const char *csrf_digest, + int64 idle_ttl_secs, + int64 absolute_ttl_secs, + int64 current_unix, + Auth_Session_Record *p_record); + +/* + * CAS session create: only creates a session if the user's current + * password_hash still exactly matches expected_password_hash. + * Returns AUTH_STORE_STALE_PASSWORD when an administrator changed it. + * The caller must cleanse expected_password_hash after this call. + */ +Auth_Store_Result Auth_Store_Create_Session_CAS( + Auth_Store *p_store, + const char *user_id, + const char *expected_password_hash, + const char *token_digest, + const char *csrf_digest, + int64 idle_ttl_secs, + int64 absolute_ttl_secs, + int64 current_unix, + Auth_Session_Record *p_record); + +/* + * Resolves a session by token_digest. + * Returns: + * OK — session is valid; p_session and p_user are populated. + * NOT_FOUND — no such session. + * REVOKED — session was explicitly revoked. + * EXPIRED — idle or absolute expiry exceeded. + * USER_DISABLED — session owner has been disabled. + * STALE_PASSWORD — password changed after session was created. + */ +Auth_Store_Result Auth_Store_Find_Session( + Auth_Store *p_store, + const char *token_digest, + int64 current_unix, + Auth_Session_Record *p_session, + Auth_User_Record *p_user); + +/* Extends the idle expiry; does nothing if the session is revoked. */ +Auth_Store_Result Auth_Store_Touch_Session( + Auth_Store *p_store, + const char *token_digest, + int64 current_unix, + int64 idle_ttl_secs); + +Auth_Store_Result Auth_Store_Revoke_Session( + Auth_Store *p_store, + const char *token_digest); + +/* + * Revokes all sessions for user_id. + * If except_token_digest is non-NULL, that session is preserved. + */ +Auth_Store_Result Auth_Store_Revoke_All_Sessions( + Auth_Store *p_store, + const char *user_id, + const char *except_token_digest); + +/* Atomically revoke sessions and write one audit record. */ +Auth_Store_Result Auth_Store_Revoke_All_Sessions_Audited( + Auth_Store *p_store, + const char *user_id, + const char *except_token_digest, + const char *actor_user_id); + +/* + * Atomically create a new session and revoke the old one in a single + * transaction. Use this after a password change to rotate the current + * session without any window where neither or both sessions are valid. + * + * old_token_digest: the existing session to revoke (must not be NULL or ""). + * If old_token_digest is already revoked the rotation still succeeds and + * the new session is created. + * All other parameters are the same as Auth_Store_Create_Session. + */ +Auth_Store_Result Auth_Store_Rotate_Session( + Auth_Store *p_store, + const char *user_id, + const char *old_token_digest, + const char *new_token_digest, + const char *new_csrf_digest, + int64 idle_ttl_secs, + int64 absolute_ttl_secs, + int64 current_unix, + Auth_Session_Record *p_record); + +/* + * Atomically compare-and-swap the password hash, clear forced change, + * revoke every existing session, and create one replacement session. + * On AUTH_STORE_STALE_PASSWORD no state is changed. The caller must cleanse + * both password hashes after this call. + */ +Auth_Store_Result Auth_Store_Self_Change_Password( + Auth_Store *p_store, + const char *user_id, + const char *old_encoded_hash, + const char *new_encoded_hash, + const char *new_token_digest, + const char *new_csrf_digest, + int64 idle_ttl_secs, + int64 absolute_ttl_secs, + int64 current_unix, + Auth_Session_Record *p_record); + +/* --- Guest quota --- */ + +typedef enum { + AUTH_STORE_GUEST_QUOTA_OK = 0, + AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED = 1, + AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED = 2, + AUTH_STORE_GUEST_QUOTA_ERROR = -1, +} Auth_Store_Guest_Quota_Result; + +typedef struct { + int64 turns_used; + int64 output_tokens_used; + int64 output_tokens_reserved; +} Auth_Store_Guest_Usage; + +/* + * Read current daily usage for a guest window. + * window_start: UTC midnight Unix timestamp for the current day. + * Returns OK with zeroed usage if no row exists yet, or ERROR. + */ +Auth_Store_Result Auth_Store_Guest_Get_Usage( + Auth_Store *p_store, + const char *guest_id, + int64 window_start, + Auth_Store_Guest_Usage *p_usage); + +/* + * Atomically reserve quota for one inference turn. + * On success: increments turns_used by 1 and output_tokens_reserved by + * max_output_tokens in the usage row for window_start. + * Invariant after every reservation: + * output_tokens_used + output_tokens_reserved <= tokens_limit. + * Returns TURNS_EXHAUSTED or TOKENS_EXHAUSTED when the limit would be exceeded. + * reservation_expires: Unix timestamp after which the reservation may be + * discarded by cleanup; the turn charge (turns_used) persists regardless. + */ +Auth_Store_Guest_Quota_Result Auth_Store_Guest_Reserve( + Auth_Store *p_store, + const char *guest_id, + const char *request_id, + int64 window_start, + int64 max_output_tokens, + int64 turns_limit, + int64 tokens_limit, + int64 reservation_expires); + +/* + * Reconcile a completed turn with the actual provider output token count. + * actual_output_tokens is capped to the reserved amount (fail closed; if + * the provider reports more than reserved, the reserved amount is charged). + * Adds capped tokens to output_tokens_used; subtracts reserved from + * output_tokens_reserved; deletes the reservation row. + * Idempotent by request_id: safe to call when already reconciled. + */ +Auth_Store_Result Auth_Store_Guest_Reconcile( + Auth_Store *p_store, + const char *request_id, + int64 actual_output_tokens); + +/* + * Release a reservation on failure or abort. + * Decrements output_tokens_reserved by the reserved amount; does NOT + * change turns_used (the turn charge is retained). + * Deletes the reservation row. + * Idempotent by request_id. + */ +Auth_Store_Result Auth_Store_Guest_Release( + Auth_Store *p_store, + const char *request_id); + +/* + * Clear all outstanding reservations for a guest. + * Used after a successful guest→user login transfer. + * Decrements output_tokens_reserved for each affected usage window. + * Does NOT change turns_used. + */ +Auth_Store_Result Auth_Store_Guest_Clear_Reservations( + Auth_Store *p_store, + const char *guest_id); + +/* + * Atomically reap expired reservations: decrement output_tokens_reserved + * in the matching guest_usage rows and delete expired reservation rows, all + * in one transaction. Uses MAX(0,...) to avoid underflow. + * Idempotent: safe to call repeatedly with the same or decreasing current_unix. + * Called at store startup and before quota read/reserve operations. + */ +Auth_Store_Result Auth_Store_Guest_Reap_Expired( + Auth_Store *p_store, + int64 current_unix); + +/* --- Guest identity --- */ + +/* + * Insert or refresh a guest identity. + * ip_binding_digest must be an HMAC digest — never the raw IP address. + * last_seen_at is updated to now; expires_at is refreshed. + */ +Auth_Store_Result Auth_Store_Upsert_Guest_Identity( + Auth_Store *p_store, + const char *guest_id, + const char *ip_binding_digest, + int64 expires_at, + Auth_Guest_Identity_Record *p_record); + +Auth_Store_Result Auth_Store_Find_Guest_Identity( + Auth_Store *p_store, + const char *guest_id, + int64 current_unix, + Auth_Guest_Identity_Record *p_record); + +/* --- Audit log --- */ + +/* + * Append one bounded audit entry. actor/target/detail may be NULL. + * Never include passwords, hashes, tokens, or session digests. + * Returns OK or ERROR. + */ +Auth_Store_Result Auth_Store_Insert_Audit_Log( + Auth_Store *p_store, + const char *actor_user_id, + const char *action, + const char *target_user_id, + const char *detail); + +/* --- Admin operations --- */ + +/* + * Atomically: update password hash, set must_change_password=1, + * revoke all target sessions, write audit row. + * Use for admin-initiated temporary-password reset only. + * new_encoded_hash must be a valid encoded zenbu-scrypt hash. + */ +Auth_Store_Result Auth_Store_Admin_Reset_Password( + Auth_Store *p_store, + const char *user_id, + const char *new_encoded_hash, + const char *actor_user_id); + +#endif