comparison connectors/store.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents
children
comparison
equal deleted inserted replaced
278:8d560f50ed4c 279:b3b547563ec7
1 #include "connectors/connector.h"
2
3 #include <stdio.h>
4 #include <string.h>
5
6 static boolean execute(Connector_Store *store, const char *sql)
7 {
8 return store && store->connection &&
9 Deita_Query_Execute_Update(store->connection, sql) >= 0;
10 }
11
12 static boolean update(
13 Connector_Store *store, const char *sql, int32 count, const char **values)
14 {
15 return store && store->connection &&
16 Deita_Query_Execute_Update_Prepared(
17 store->connection, sql, count, values) >= 0;
18 }
19
20 boolean Connector_Store_Open(
21 Connector_Store *store, const char *database_path,
22 const Connector_Master_Key *master_key)
23 {
24 if (!store || !database_path || !master_key)
25 return FALSE;
26 memset(store, 0, sizeof(*store));
27 store->connection = Deita_Connection_Create(
28 DEITA_DATABASE_TYPE_SQLITE3, database_path);
29 if (!store->connection)
30 return FALSE;
31 store->master_key = *master_key;
32 return Connector_Store_Migrate(store);
33 }
34
35 void Connector_Store_Close(Connector_Store *store)
36 {
37 if (!store)
38 return;
39 if (store->connection)
40 Deita_Connection_Close(store->connection);
41 memset(store, 0, sizeof(*store));
42 }
43
44 boolean Connector_Store_Migrate(Connector_Store *store)
45 {
46 static const char *migration =
47 "BEGIN;"
48 "CREATE TABLE IF NOT EXISTS connector_migrations("
49 "version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);"
50 "CREATE TABLE IF NOT EXISTS connector_oauth_states("
51 "state TEXT PRIMARY KEY, user_id TEXT NOT NULL, verifier TEXT NOT NULL,"
52 "expires_at INTEGER NOT NULL, consumed_at INTEGER);"
53 "CREATE TABLE IF NOT EXISTS connector_accounts("
54 "account_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, provider TEXT NOT NULL,"
55 "provider_subject TEXT NOT NULL, email TEXT NOT NULL,"
56 "access_token_enc TEXT NOT NULL, refresh_token_enc TEXT NOT NULL,"
57 "expires_at INTEGER NOT NULL, scopes TEXT NOT NULL,"
58 "UNIQUE(user_id, provider, provider_subject));"
59 "CREATE INDEX IF NOT EXISTS connector_accounts_owner "
60 "ON connector_accounts(user_id, account_id);"
61 "CREATE TABLE IF NOT EXISTS connector_policies("
62 "user_id TEXT NOT NULL, action TEXT NOT NULL, policy INTEGER NOT NULL,"
63 "PRIMARY KEY(user_id, action));"
64 "CREATE TABLE IF NOT EXISTS connector_confirmations("
65 "token TEXT PRIMARY KEY, user_id TEXT NOT NULL, digest TEXT NOT NULL,"
66 "expires_at INTEGER NOT NULL, consumed_at INTEGER);"
67 "CREATE TABLE IF NOT EXISTS connector_idempotency("
68 "user_id TEXT NOT NULL, idem_key TEXT NOT NULL, digest TEXT NOT NULL,"
69 "status INTEGER NOT NULL, response_body TEXT NOT NULL,"
70 "created_at INTEGER NOT NULL DEFAULT (unixepoch()),"
71 "PRIMARY KEY(user_id, idem_key));"
72 "CREATE TABLE IF NOT EXISTS connector_audit("
73 "id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL,"
74 "account_id TEXT NOT NULL, operation TEXT NOT NULL, digest TEXT NOT NULL,"
75 "status INTEGER NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()));"
76 "INSERT OR IGNORE INTO connector_migrations(version, applied_at)"
77 "VALUES(1, unixepoch());"
78 "COMMIT;";
79 return execute(store, "PRAGMA foreign_keys=ON;") &&
80 execute(store, migration);
81 }
82
83 boolean Connector_Store_Create_State(
84 Connector_Store *store, const char *user_id, const Connector_OAuth_Start *start,
85 int64 expires_at)
86 {
87 char expiry[32];
88 snprintf(expiry, sizeof(expiry), "%lld", expires_at);
89 const char *values[] = {
90 start ? start->state : NULL, user_id,
91 start ? start->code_verifier : NULL, expiry
92 };
93 return start && user_id && update(
94 store,
95 "INSERT INTO connector_oauth_states"
96 "(state,user_id,verifier,expires_at) VALUES(?,?,?,?)",
97 4, values);
98 }
99
100 boolean Connector_Store_Consume_State(
101 Connector_Store *store, const char *user_id, const char *state, int64 now,
102 char *verifier, size_t verifier_size)
103 {
104 if (!store || !user_id || !state || !verifier || verifier_size == 0)
105 return FALSE;
106 char now_text[32];
107 snprintf(now_text, sizeof(now_text), "%lld", now);
108 const char *values[] = {state, user_id, now_text};
109 Dowa_Arena *arena = Dowa_Arena_Create(4096);
110 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
111 store->connection,
112 "SELECT verifier FROM connector_oauth_states "
113 "WHERE state=? AND user_id=? AND consumed_at IS NULL AND expires_at>=?",
114 3, values, arena);
115 boolean found = result && Deita_Result_Set_Next(result);
116 if (found) {
117 const char *value = Deita_Result_Set_Get_Text(result, 0);
118 if (!value || strlen(value) >= verifier_size)
119 found = FALSE;
120 else
121 strcpy(verifier, value);
122 }
123 if (result)
124 Deita_Result_Set_Free(result);
125 Dowa_Arena_Free(arena);
126 if (!found)
127 return FALSE;
128 const char *consume_values[] = {now_text, state, user_id};
129 return Deita_Query_Execute_Update_Prepared(
130 store->connection,
131 "UPDATE connector_oauth_states SET consumed_at=? "
132 "WHERE state=? AND user_id=? AND consumed_at IS NULL",
133 3, consume_values) == 1;
134 }
135
136 boolean Connector_Store_Save_Account(
137 Connector_Store *store, const Connector_Account *account)
138 {
139 if (!store || !account || !account->account_id[0] || !account->user_id[0])
140 return FALSE;
141 char access[4096], refresh[4096], expires[32];
142 if (!Connector_Encrypt(&store->master_key, account->access_token, access,
143 sizeof(access)) ||
144 !Connector_Encrypt(&store->master_key, account->refresh_token, refresh,
145 sizeof(refresh)))
146 return FALSE;
147 snprintf(expires, sizeof(expires), "%lld", account->expires_at);
148 const char *values[] = {
149 account->account_id, account->user_id, account->provider,
150 account->provider_subject, account->email, access, refresh, expires,
151 account->scopes
152 };
153 return update(
154 store,
155 "INSERT INTO connector_accounts(account_id,user_id,provider,"
156 "provider_subject,email,access_token_enc,refresh_token_enc,expires_at,scopes)"
157 "VALUES(?,?,?,?,?,?,?,?,?) ON CONFLICT(account_id) DO UPDATE SET "
158 "email=excluded.email,access_token_enc=excluded.access_token_enc,"
159 "refresh_token_enc=excluded.refresh_token_enc,"
160 "expires_at=excluded.expires_at,scopes=excluded.scopes "
161 "WHERE connector_accounts.user_id=excluded.user_id",
162 9, values);
163 }
164
165 boolean Connector_Store_Get_Account(
166 Connector_Store *store, const char *user_id, const char *account_id,
167 Connector_Account *account)
168 {
169 if (!store || !user_id || !account_id || !account)
170 return FALSE;
171 const char *values[] = {user_id, account_id};
172 Dowa_Arena *arena = Dowa_Arena_Create(16384);
173 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
174 store->connection,
175 "SELECT account_id,user_id,provider,provider_subject,email,"
176 "access_token_enc,refresh_token_enc,expires_at,scopes "
177 "FROM connector_accounts WHERE user_id=? AND account_id=?",
178 2, values, arena);
179 boolean found = result && Deita_Result_Set_Next(result);
180 if (found) {
181 memset(account, 0, sizeof(*account));
182 #define COPY_COLUMN(field, index) do { \
183 const char *value = Deita_Result_Set_Get_Text(result, index); \
184 if (!value || strlen(value) >= sizeof(account->field)) found = FALSE; \
185 else strcpy(account->field, value); \
186 } while (0)
187 COPY_COLUMN(account_id, 0);
188 COPY_COLUMN(user_id, 1);
189 COPY_COLUMN(provider, 2);
190 COPY_COLUMN(provider_subject, 3);
191 COPY_COLUMN(email, 4);
192 const char *access = Deita_Result_Set_Get_Text(result, 5);
193 const char *refresh = Deita_Result_Set_Get_Text(result, 6);
194 account->expires_at = Deita_Result_Set_Get_Integer(result, 7);
195 COPY_COLUMN(scopes, 8);
196 if (!access || !refresh ||
197 !Connector_Decrypt(&store->master_key, access, account->access_token,
198 sizeof(account->access_token)) ||
199 !Connector_Decrypt(&store->master_key, refresh, account->refresh_token,
200 sizeof(account->refresh_token)))
201 found = FALSE;
202 #undef COPY_COLUMN
203 }
204 if (result)
205 Deita_Result_Set_Free(result);
206 Dowa_Arena_Free(arena);
207 return found;
208 }
209
210 boolean Connector_Store_List_Accounts(
211 Connector_Store *store, const char *user_id,
212 Connector_Account_Summary **accounts, Dowa_Arena *arena)
213 {
214 if (!store || !user_id || !accounts || !arena)
215 return FALSE;
216 *accounts = NULL;
217 const char *values[] = {user_id};
218 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
219 store->connection,
220 "SELECT account_id,provider,email,expires_at,scopes "
221 "FROM connector_accounts WHERE user_id=? ORDER BY provider,email",
222 1, values, arena);
223 if (!result || Deita_Result_Set_Has_Error(result)) {
224 if (result)
225 Deita_Result_Set_Free(result);
226 return FALSE;
227 }
228 while (Deita_Result_Set_Next(result)) {
229 Connector_Account_Summary account = {0};
230 const char *account_id = Deita_Result_Set_Get_Text(result, 0);
231 const char *provider = Deita_Result_Set_Get_Text(result, 1);
232 const char *email = Deita_Result_Set_Get_Text(result, 2);
233 const char *scopes = Deita_Result_Set_Get_Text(result, 4);
234 if (!account_id || !provider || !email || !scopes ||
235 strlen(account_id) >= sizeof(account.account_id) ||
236 strlen(provider) >= sizeof(account.provider) ||
237 strlen(email) >= sizeof(account.email) ||
238 strlen(scopes) >= sizeof(account.scopes)) {
239 Deita_Result_Set_Free(result);
240 return FALSE;
241 }
242 strcpy(account.account_id, account_id);
243 strcpy(account.provider, provider);
244 strcpy(account.email, email);
245 account.expires_at = Deita_Result_Set_Get_Integer(result, 3);
246 strcpy(account.scopes, scopes);
247 Dowa_Array_Push_Arena(*accounts, account, arena);
248 }
249 Deita_Result_Set_Free(result);
250 return TRUE;
251 }
252
253 boolean Connector_Store_Delete_Account(
254 Connector_Store *store, const char *user_id, const char *account_id)
255 {
256 const char *values[] = {user_id, account_id};
257 return user_id && account_id &&
258 Deita_Query_Execute_Update_Prepared(
259 store->connection,
260 "DELETE FROM connector_accounts WHERE user_id=? AND account_id=?",
261 2, values) == 1;
262 }
263
264 boolean Connector_Store_Set_Policy(
265 Connector_Store *store, const char *user_id, const char *action,
266 Connector_Confirmation_Policy policy)
267 {
268 char policy_text[8];
269 snprintf(policy_text, sizeof(policy_text), "%d", policy);
270 const char *values[] = {user_id, action, policy_text};
271 return user_id && action && update(
272 store,
273 "INSERT INTO connector_policies(user_id,action,policy) VALUES(?,?,?) "
274 "ON CONFLICT(user_id,action) DO UPDATE SET policy=excluded.policy",
275 3, values);
276 }
277
278 Connector_Confirmation_Policy Connector_Store_Get_Policy(
279 Connector_Store *store, const char *user_id, const char *action)
280 {
281 Connector_Confirmation_Policy fallback =
282 action && (!strcmp(action, "gmail.send") ||
283 !strcmp(action, "drive.overwrite"))
284 ? CONNECTOR_CONFIRM_ALWAYS : CONNECTOR_CONFIRM_NEVER;
285 if (!store || !user_id || !action)
286 return fallback;
287 const char *values[] = {user_id, action};
288 Dowa_Arena *arena = Dowa_Arena_Create(2048);
289 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
290 store->connection,
291 "SELECT policy FROM connector_policies WHERE user_id=? AND action=?",
292 2, values, arena);
293 if (result && Deita_Result_Set_Next(result))
294 fallback = (Connector_Confirmation_Policy)
295 Deita_Result_Set_Get_Integer(result, 0);
296 if (result)
297 Deita_Result_Set_Free(result);
298 Dowa_Arena_Free(arena);
299 return fallback;
300 }
301
302 boolean Connector_Store_Create_Confirmation(
303 Connector_Store *store, const char *user_id, const char *digest,
304 const char *token, int64 expires_at)
305 {
306 char expiry[32];
307 snprintf(expiry, sizeof(expiry), "%lld", expires_at);
308 const char *values[] = {token, user_id, digest, expiry};
309 return token && user_id && digest && update(
310 store,
311 "INSERT INTO connector_confirmations(token,user_id,digest,expires_at)"
312 "VALUES(?,?,?,?)", 4, values);
313 }
314
315 boolean Connector_Store_Consume_Confirmation(
316 Connector_Store *store, const char *user_id, const char *digest,
317 const char *token, int64 now)
318 {
319 char now_text[32];
320 snprintf(now_text, sizeof(now_text), "%lld", now);
321 const char *values[] = {now_text, token, user_id, digest, now_text};
322 return token && user_id && digest &&
323 Deita_Query_Execute_Update_Prepared(
324 store->connection,
325 "UPDATE connector_confirmations SET consumed_at=? WHERE token=? "
326 "AND user_id=? AND digest=? AND consumed_at IS NULL AND expires_at>=?",
327 5, values) == 1;
328 }
329
330 boolean Connector_Store_Get_Idempotent(
331 Connector_Store *store, const char *user_id, const char *key,
332 const char *digest, int32 *status, char *body, size_t body_size)
333 {
334 if (!store || !user_id || !key || !digest || !status || !body)
335 return FALSE;
336 const char *values[] = {user_id, key, digest};
337 Dowa_Arena *arena = Dowa_Arena_Create(4096 + body_size);
338 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
339 store->connection,
340 "SELECT status,response_body FROM connector_idempotency "
341 "WHERE user_id=? AND idem_key=? AND digest=?",
342 3, values, arena);
343 boolean found = result && Deita_Result_Set_Next(result);
344 if (found) {
345 const char *value = Deita_Result_Set_Get_Text(result, 1);
346 if (!value || strlen(value) >= body_size)
347 found = FALSE;
348 else {
349 *status = (int32)Deita_Result_Set_Get_Integer(result, 0);
350 strcpy(body, value);
351 }
352 }
353 if (result)
354 Deita_Result_Set_Free(result);
355 Dowa_Arena_Free(arena);
356 return found;
357 }
358
359 boolean Connector_Store_Put_Idempotent(
360 Connector_Store *store, const char *user_id, const char *key,
361 const char *digest, int32 status, const char *body)
362 {
363 char status_text[16];
364 snprintf(status_text, sizeof(status_text), "%d", status);
365 const char *values[] = {user_id, key, digest, status_text, body ? body : ""};
366 return user_id && key && digest && update(
367 store,
368 "INSERT OR IGNORE INTO connector_idempotency"
369 "(user_id,idem_key,digest,status,response_body) VALUES(?,?,?,?,?)",
370 5, values);
371 }
372
373 boolean Connector_Store_Idempotency_Conflict(
374 Connector_Store *store, const char *user_id, const char *key,
375 const char *digest)
376 {
377 if (!store || !user_id || !key || !digest)
378 return FALSE;
379 const char *values[] = {user_id, key, digest};
380 Dowa_Arena *arena = Dowa_Arena_Create(2048);
381 Deita_Result_Set *result = Deita_Query_Execute_Prepared(
382 store->connection,
383 "SELECT 1 FROM connector_idempotency "
384 "WHERE user_id=? AND idem_key=? AND digest<>?",
385 3, values, arena);
386 boolean conflict = result && Deita_Result_Set_Next(result);
387 if (result)
388 Deita_Result_Set_Free(result);
389 Dowa_Arena_Free(arena);
390 return conflict;
391 }
392
393 boolean Connector_Store_Audit(
394 Connector_Store *store, const char *user_id, const char *account_id,
395 const char *operation, const char *digest, int32 status)
396 {
397 char status_text[16];
398 snprintf(status_text, sizeof(status_text), "%d", status);
399 const char *values[] = {
400 user_id, account_id, operation, digest, status_text
401 };
402 return user_id && account_id && operation && digest && update(
403 store,
404 "INSERT INTO connector_audit"
405 "(user_id,account_id,operation,digest,status) VALUES(?,?,?,?,?)",
406 5, values);
407 }