Mercurial
view connectors/store.c @ 281:c57149ad216e default tip
Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Tue, 18 Aug 2026 22:18:15 -0700 |
| parents | b3b547563ec7 |
| children |
line wrap: on
line source
#include "connectors/connector.h" #include <stdio.h> #include <string.h> static boolean execute(Connector_Store *store, const char *sql) { return store && store->connection && Deita_Query_Execute_Update(store->connection, sql) >= 0; } static boolean update( Connector_Store *store, const char *sql, int32 count, const char **values) { return store && store->connection && Deita_Query_Execute_Update_Prepared( store->connection, sql, count, values) >= 0; } boolean Connector_Store_Open( Connector_Store *store, const char *database_path, const Connector_Master_Key *master_key) { if (!store || !database_path || !master_key) return FALSE; memset(store, 0, sizeof(*store)); store->connection = Deita_Connection_Create( DEITA_DATABASE_TYPE_SQLITE3, database_path); if (!store->connection) return FALSE; store->master_key = *master_key; return Connector_Store_Migrate(store); } void Connector_Store_Close(Connector_Store *store) { if (!store) return; if (store->connection) Deita_Connection_Close(store->connection); memset(store, 0, sizeof(*store)); } boolean Connector_Store_Migrate(Connector_Store *store) { static const char *migration = "BEGIN;" "CREATE TABLE IF NOT EXISTS connector_migrations(" "version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);" "CREATE TABLE IF NOT EXISTS connector_oauth_states(" "state TEXT PRIMARY KEY, user_id TEXT NOT NULL, verifier TEXT NOT NULL," "expires_at INTEGER NOT NULL, consumed_at INTEGER);" "CREATE TABLE IF NOT EXISTS connector_accounts(" "account_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, provider TEXT NOT NULL," "provider_subject TEXT NOT NULL, email TEXT NOT NULL," "access_token_enc TEXT NOT NULL, refresh_token_enc TEXT NOT NULL," "expires_at INTEGER NOT NULL, scopes TEXT NOT NULL," "UNIQUE(user_id, provider, provider_subject));" "CREATE INDEX IF NOT EXISTS connector_accounts_owner " "ON connector_accounts(user_id, account_id);" "CREATE TABLE IF NOT EXISTS connector_policies(" "user_id TEXT NOT NULL, action TEXT NOT NULL, policy INTEGER NOT NULL," "PRIMARY KEY(user_id, action));" "CREATE TABLE IF NOT EXISTS connector_confirmations(" "token TEXT PRIMARY KEY, user_id TEXT NOT NULL, digest TEXT NOT NULL," "expires_at INTEGER NOT NULL, consumed_at INTEGER);" "CREATE TABLE IF NOT EXISTS connector_idempotency(" "user_id TEXT NOT NULL, idem_key TEXT NOT NULL, digest TEXT NOT NULL," "status INTEGER NOT NULL, response_body TEXT NOT NULL," "created_at INTEGER NOT NULL DEFAULT (unixepoch())," "PRIMARY KEY(user_id, idem_key));" "CREATE TABLE IF NOT EXISTS connector_audit(" "id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL," "account_id TEXT NOT NULL, operation TEXT NOT NULL, digest TEXT NOT NULL," "status INTEGER NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()));" "INSERT OR IGNORE INTO connector_migrations(version, applied_at)" "VALUES(1, unixepoch());" "COMMIT;"; return execute(store, "PRAGMA foreign_keys=ON;") && execute(store, migration); } boolean Connector_Store_Create_State( Connector_Store *store, const char *user_id, const Connector_OAuth_Start *start, int64 expires_at) { char expiry[32]; snprintf(expiry, sizeof(expiry), "%lld", expires_at); const char *values[] = { start ? start->state : NULL, user_id, start ? start->code_verifier : NULL, expiry }; return start && user_id && update( store, "INSERT INTO connector_oauth_states" "(state,user_id,verifier,expires_at) VALUES(?,?,?,?)", 4, values); } boolean Connector_Store_Consume_State( Connector_Store *store, const char *user_id, const char *state, int64 now, char *verifier, size_t verifier_size) { if (!store || !user_id || !state || !verifier || verifier_size == 0) return FALSE; char now_text[32]; snprintf(now_text, sizeof(now_text), "%lld", now); const char *values[] = {state, user_id, now_text}; Dowa_Arena *arena = Dowa_Arena_Create(4096); Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT verifier FROM connector_oauth_states " "WHERE state=? AND user_id=? AND consumed_at IS NULL AND expires_at>=?", 3, values, arena); boolean found = result && Deita_Result_Set_Next(result); if (found) { const char *value = Deita_Result_Set_Get_Text(result, 0); if (!value || strlen(value) >= verifier_size) found = FALSE; else strcpy(verifier, value); } if (result) Deita_Result_Set_Free(result); Dowa_Arena_Free(arena); if (!found) return FALSE; const char *consume_values[] = {now_text, state, user_id}; return Deita_Query_Execute_Update_Prepared( store->connection, "UPDATE connector_oauth_states SET consumed_at=? " "WHERE state=? AND user_id=? AND consumed_at IS NULL", 3, consume_values) == 1; } boolean Connector_Store_Save_Account( Connector_Store *store, const Connector_Account *account) { if (!store || !account || !account->account_id[0] || !account->user_id[0]) return FALSE; char access[4096], refresh[4096], expires[32]; if (!Connector_Encrypt(&store->master_key, account->access_token, access, sizeof(access)) || !Connector_Encrypt(&store->master_key, account->refresh_token, refresh, sizeof(refresh))) return FALSE; snprintf(expires, sizeof(expires), "%lld", account->expires_at); const char *values[] = { account->account_id, account->user_id, account->provider, account->provider_subject, account->email, access, refresh, expires, account->scopes }; return update( store, "INSERT INTO connector_accounts(account_id,user_id,provider," "provider_subject,email,access_token_enc,refresh_token_enc,expires_at,scopes)" "VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(account_id) DO UPDATE SET " "email=excluded.email,access_token_enc=excluded.access_token_enc," "refresh_token_enc=excluded.refresh_token_enc," "expires_at=excluded.expires_at,scopes=excluded.scopes " "WHERE connector_accounts.user_id=excluded.user_id", 9, values); } boolean Connector_Store_Get_Account( Connector_Store *store, const char *user_id, const char *account_id, Connector_Account *account) { if (!store || !user_id || !account_id || !account) return FALSE; const char *values[] = {user_id, account_id}; Dowa_Arena *arena = Dowa_Arena_Create(16384); Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT account_id,user_id,provider,provider_subject,email," "access_token_enc,refresh_token_enc,expires_at,scopes " "FROM connector_accounts WHERE user_id=? AND account_id=?", 2, values, arena); boolean found = result && Deita_Result_Set_Next(result); if (found) { memset(account, 0, sizeof(*account)); #define COPY_COLUMN(field, index) do { \ const char *value = Deita_Result_Set_Get_Text(result, index); \ if (!value || strlen(value) >= sizeof(account->field)) found = FALSE; \ else strcpy(account->field, value); \ } while (0) COPY_COLUMN(account_id, 0); COPY_COLUMN(user_id, 1); COPY_COLUMN(provider, 2); COPY_COLUMN(provider_subject, 3); COPY_COLUMN(email, 4); const char *access = Deita_Result_Set_Get_Text(result, 5); const char *refresh = Deita_Result_Set_Get_Text(result, 6); account->expires_at = Deita_Result_Set_Get_Integer(result, 7); COPY_COLUMN(scopes, 8); if (!access || !refresh || !Connector_Decrypt(&store->master_key, access, account->access_token, sizeof(account->access_token)) || !Connector_Decrypt(&store->master_key, refresh, account->refresh_token, sizeof(account->refresh_token))) found = FALSE; #undef COPY_COLUMN } if (result) Deita_Result_Set_Free(result); Dowa_Arena_Free(arena); return found; } boolean Connector_Store_List_Accounts( Connector_Store *store, const char *user_id, Connector_Account_Summary **accounts, Dowa_Arena *arena) { if (!store || !user_id || !accounts || !arena) return FALSE; *accounts = NULL; const char *values[] = {user_id}; Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT account_id,provider,email,expires_at,scopes " "FROM connector_accounts WHERE user_id=? ORDER BY provider,email", 1, values, arena); if (!result || Deita_Result_Set_Has_Error(result)) { if (result) Deita_Result_Set_Free(result); return FALSE; } while (Deita_Result_Set_Next(result)) { Connector_Account_Summary account = {0}; const char *account_id = Deita_Result_Set_Get_Text(result, 0); const char *provider = Deita_Result_Set_Get_Text(result, 1); const char *email = Deita_Result_Set_Get_Text(result, 2); const char *scopes = Deita_Result_Set_Get_Text(result, 4); if (!account_id || !provider || !email || !scopes || strlen(account_id) >= sizeof(account.account_id) || strlen(provider) >= sizeof(account.provider) || strlen(email) >= sizeof(account.email) || strlen(scopes) >= sizeof(account.scopes)) { Deita_Result_Set_Free(result); return FALSE; } strcpy(account.account_id, account_id); strcpy(account.provider, provider); strcpy(account.email, email); account.expires_at = Deita_Result_Set_Get_Integer(result, 3); strcpy(account.scopes, scopes); Dowa_Array_Push_Arena(*accounts, account, arena); } Deita_Result_Set_Free(result); return TRUE; } boolean Connector_Store_Delete_Account( Connector_Store *store, const char *user_id, const char *account_id) { const char *values[] = {user_id, account_id}; return user_id && account_id && Deita_Query_Execute_Update_Prepared( store->connection, "DELETE FROM connector_accounts WHERE user_id=? AND account_id=?", 2, values) == 1; } boolean Connector_Store_Set_Policy( Connector_Store *store, const char *user_id, const char *action, Connector_Confirmation_Policy policy) { char policy_text[8]; snprintf(policy_text, sizeof(policy_text), "%d", policy); const char *values[] = {user_id, action, policy_text}; return user_id && action && update( store, "INSERT INTO connector_policies(user_id,action,policy) VALUES(?,?,?) " "ON CONFLICT(user_id,action) DO UPDATE SET policy=excluded.policy", 3, values); } Connector_Confirmation_Policy Connector_Store_Get_Policy( Connector_Store *store, const char *user_id, const char *action) { Connector_Confirmation_Policy fallback = action && (!strcmp(action, "gmail.send") || !strcmp(action, "drive.overwrite")) ? CONNECTOR_CONFIRM_ALWAYS : CONNECTOR_CONFIRM_NEVER; if (!store || !user_id || !action) return fallback; const char *values[] = {user_id, action}; Dowa_Arena *arena = Dowa_Arena_Create(2048); Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT policy FROM connector_policies WHERE user_id=? AND action=?", 2, values, arena); if (result && Deita_Result_Set_Next(result)) fallback = (Connector_Confirmation_Policy) Deita_Result_Set_Get_Integer(result, 0); if (result) Deita_Result_Set_Free(result); Dowa_Arena_Free(arena); return fallback; } boolean Connector_Store_Create_Confirmation( Connector_Store *store, const char *user_id, const char *digest, const char *token, int64 expires_at) { char expiry[32]; snprintf(expiry, sizeof(expiry), "%lld", expires_at); const char *values[] = {token, user_id, digest, expiry}; return token && user_id && digest && update( store, "INSERT INTO connector_confirmations(token,user_id,digest,expires_at)" "VALUES(?,?,?,?)", 4, values); } boolean Connector_Store_Consume_Confirmation( Connector_Store *store, const char *user_id, const char *digest, const char *token, int64 now) { char now_text[32]; snprintf(now_text, sizeof(now_text), "%lld", now); const char *values[] = {now_text, token, user_id, digest, now_text}; return token && user_id && digest && Deita_Query_Execute_Update_Prepared( store->connection, "UPDATE connector_confirmations SET consumed_at=? WHERE token=? " "AND user_id=? AND digest=? AND consumed_at IS NULL AND expires_at>=?", 5, values) == 1; } boolean Connector_Store_Get_Idempotent( Connector_Store *store, const char *user_id, const char *key, const char *digest, int32 *status, char *body, size_t body_size) { if (!store || !user_id || !key || !digest || !status || !body) return FALSE; const char *values[] = {user_id, key, digest}; Dowa_Arena *arena = Dowa_Arena_Create(4096 + body_size); Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT status,response_body FROM connector_idempotency " "WHERE user_id=? AND idem_key=? AND digest=?", 3, values, arena); boolean found = result && Deita_Result_Set_Next(result); if (found) { const char *value = Deita_Result_Set_Get_Text(result, 1); if (!value || strlen(value) >= body_size) found = FALSE; else { *status = (int32)Deita_Result_Set_Get_Integer(result, 0); strcpy(body, value); } } if (result) Deita_Result_Set_Free(result); Dowa_Arena_Free(arena); return found; } boolean Connector_Store_Put_Idempotent( Connector_Store *store, const char *user_id, const char *key, const char *digest, int32 status, const char *body) { char status_text[16]; snprintf(status_text, sizeof(status_text), "%d", status); const char *values[] = {user_id, key, digest, status_text, body ? body : ""}; return user_id && key && digest && update( store, "INSERT OR IGNORE INTO connector_idempotency" "(user_id,idem_key,digest,status,response_body) VALUES(?,?,?,?,?)", 5, values); } boolean Connector_Store_Idempotency_Conflict( Connector_Store *store, const char *user_id, const char *key, const char *digest) { if (!store || !user_id || !key || !digest) return FALSE; const char *values[] = {user_id, key, digest}; Dowa_Arena *arena = Dowa_Arena_Create(2048); Deita_Result_Set *result = Deita_Query_Execute_Prepared( store->connection, "SELECT 1 FROM connector_idempotency " "WHERE user_id=? AND idem_key=? AND digest<>?", 3, values, arena); boolean conflict = result && Deita_Result_Set_Next(result); if (result) Deita_Result_Set_Free(result); Dowa_Arena_Free(arena); return conflict; } boolean Connector_Store_Audit( Connector_Store *store, const char *user_id, const char *account_id, const char *operation, const char *digest, int32 status) { char status_text[16]; snprintf(status_text, sizeof(status_text), "%d", status); const char *values[] = { user_id, account_id, operation, digest, status_text }; return user_id && account_id && operation && digest && update( store, "INSERT INTO connector_audit" "(user_id,account_id,operation,digest,status) VALUES(?,?,?,?,?)", 5, values); }