Mercurial
view auth/auth_store.h @ 273:e02e2036ef84 default tip
add Layer 2 JRPG component system
Add reusable content and window modals, an isolated component sandbox, shared cyberpunk scroll areas, production-safe cache freshness, and server-rendered JRPG panel state.
Co-authored-by: Copilot <[email protected]>
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 08 Aug 2026 02:08:08 -0700 |
| parents | 04fee26ecce0 |
| children |
line wrap: on
line source
#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