0xanshu feat/multiple projects system#89
Conversation
Update dev
…0xanshu/Scrawn into 0xanshu-feat/multiple-projects-system
Greptile SummaryThis PR introduces a multi-project system by adding a
Confidence Score: 3/5Not safe to merge without fixing the race condition in The multi-project security fixes are thorough and well-structured, but two new correctness issues were introduced.
Important Files Changed
|
| const existing = await db | ||
| .select({ project_id: projectTable.project_id }) | ||
| .from(projectTable) | ||
| .where(eq(projectTable.project_id, project_id)) | ||
| .limit(1); | ||
|
|
||
| if (existing[0]) { | ||
| await db | ||
| .update(projectTable) | ||
| .set({ product_id }) | ||
| .where(eq(projectTable.project_id, existing[0].project_id)); | ||
| return; | ||
| } | ||
|
|
||
| await db.insert(projectTable).values({ project_id, product_id }); |
There was a problem hiding this comment.
TOCTOU race — concurrent inserts will throw
StorageError.insertFailed
The select-then-insert pattern has no locking, so two concurrent callers that both reach line 21 with no existing row will each attempt the insert on line 31. The second insert hits a primary-key violation, which is caught and re-thrown as StorageError.insertFailed. This can be triggered during onboarding if the client retries or if two requests race. Replace the select/branch/insert with a single INSERT … ON CONFLICT (project_id) DO UPDATE SET product_id = EXCLUDED.product_id, which is atomic and idempotent.
| if (Object.keys(setValues).length === 0) return; | ||
|
|
||
| const insertValues: typeof metadataTable.$inferInsert = { | ||
| ...setValues, | ||
| } as typeof metadataTable.$inferInsert; | ||
| await txn.insert(metadataTable).values(insertValues); | ||
| await txn | ||
| .insert(metadataTable) | ||
| .values({ | ||
| project_id: input.project_id, | ||
| dodo_live_api_key: requireField( | ||
| input.dodo_live_api_key, | ||
| "dodo_live_api_key" | ||
| ), | ||
| dodo_test_api_key: requireField( | ||
| input.dodo_test_api_key, | ||
| "dodo_test_api_key" | ||
| ), | ||
| dodo_live_product_id: requireField( | ||
| input.dodo_live_product_id, | ||
| "dodo_live_product_id" | ||
| ), | ||
| dodo_test_product_id: requireField( | ||
| input.dodo_test_product_id, | ||
| "dodo_test_product_id" | ||
| ), | ||
| dodo_live_webhook_secret: requireField( | ||
| input.dodo_live_webhook_secret, | ||
| "dodo_live_webhook_secret" | ||
| ), | ||
| dodo_test_webhook_secret: requireField( | ||
| input.dodo_test_webhook_secret, | ||
| "dodo_test_webhook_secret" | ||
| ), | ||
| redirect_url: requireField(input.redirect_url, "redirect_url"), | ||
| currency: input.currency, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: metadataTable.project_id, | ||
| set: setValues, | ||
| }); |
There was a problem hiding this comment.
requireField gates on insert path break future partial-update callers
All requireField calls are evaluated unconditionally before the SQL is dispatched. A caller that passes only currency (to update one field on an existing row) will get a StorageError.insertFailed("Missing required field 'dodo_live_api_key'") even though the conflict branch would never need that value. The UpsertMetadataInput type marks every credential field as optional, advertising that partial updates are supported — but the runtime behavior contradicts this. Today the only caller (handleOnboarding) always supplies all fields, so nothing is broken right now. Move requireField into an explicit insert-only helper and keep setValues as the unconditional update payload.
No description provided.