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