docs: update CLAUDE.md with Google OAuth architecture and gotchas

Document non-obvious details from Steps 1-5 implementation:
- adiuvAI: deep-link protocol workaround, requestSingleInstanceLock,
  backup-key.ts device-bound key, loginWithOAuth fetch() vs get()
- api: _pending_states in-memory limitation, nullable password_hash,
  OAUTH_REDIRECT_URI pointing to API not website, 409 unverified-email
  guard, OAuth testing patterns with AsyncMock + monkeypatch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-04-10 18:39:20 +02:00
parent 92648472d7
commit bc2c76d2bb

View File

@@ -76,6 +76,9 @@ Main Process (Node.js)
- `src/main/db/schema.ts` — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
- `src/renderer/routes/` — File-based routing (TanStack Router auto-generates `routeTree.gen.ts`)
- `src/renderer/components/ui/` — shadcn/ui primitives (new-york theme, neutral colors)
- `src/main/auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
- `src/main/auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
- `src/main/ai/token.ts` — Two-tier token storage: safeStorage + electron-store fallback
**Non-obvious details**:
- `electron-trpc` is NOT used — custom IPC bridge in `ipc.ts` + `ipcLink.ts` because electron-trpc bundles tRPC v10 internals
@@ -85,6 +88,13 @@ Main Process (Node.js)
- Timestamps are milliseconds (JavaScript `Date.getTime()`), not ISO strings
- Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)
**Google OAuth (adiuvAI side)**:
- `adiuvai://` is NOT accepted by Google as a redirect URI — Google only accepts `http://localhost` or `https://`. The API backend exposes `GET /auth/oauth/google/web-callback` which receives the Google redirect and immediately bounces to `adiuvai://oauth/callback?...`. The redirect URI registered in Google Cloud Console points to the backend, not the Electron app.
- `app.requestSingleInstanceLock()` is required for the `second-instance` event to fire on Windows/Linux. If it returns `false`, call `app.quit()` immediately (another instance is already running).
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as the third argument so the OS protocol registration includes the entry script.
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) because the authorize endpoint is public — `get()` throws when not authenticated.
- The backup key in `backup-key.ts` is stored in `encryptedTokens` under the key `backup_key`, reusing `getToken/setToken` from `token.ts`. It is device-bound and never password-derived, so social-login users can use backup features without issue.
---
## api (FastAPI Backend)
@@ -172,6 +182,15 @@ FastAPI app (app/main.py)
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
- **Vector search on encrypted data is not semantic**: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — a known trade-off
**Google OAuth (api side)**:
- OAuth routes live in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
- Provider abstraction in `app/auth/oauth_providers.py``GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 is implemented manually via `generate_pkce_pair()`.
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev but does not survive restarts and does not scale to multiple workers. Replace with Redis in production.
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` is required before creating a linked `OAuthAccount` to populate `new_user.id` before commit.
- `OAUTH_REDIRECT_URI` must point to the **API backend** (e.g. `https://api.adiuvai.com/...`), not the website domain. `adiuvai.com` is a static site with no server-side routing.
- **Unverified email + existing account = 409**: if `email_verified=False` and the email is already registered, the callback returns 409. Without this guard, branch 3 would attempt to INSERT a duplicate email and crash with a DB constraint violation (500).
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates a new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restarting the app.
### Tier System
| Feature | Free | Pro | Power | Team |