--- phase: 31-puter.js-zero-config-cloud plan: "02" subsystem: server/google-oauth tags: [oauth, pkce, google, gemini, api-keys, secrets] dependency_graph: requires: [] provides: [googleOAuthService, googleOAuthRoutes, google-oauth-routes] affects: [server/src/app.ts] tech_stack: added: [] patterns: [PKCE-S256, pendingTokens-claim-pattern, secretService-upsert] key_files: created: - server/src/services/google-oauth.ts - server/src/routes/google-oauth.ts - server/src/__tests__/31-google-oauth.test.ts modified: - server/src/app.ts decisions: - pendingPkce stores only verifier (no companyId) — company does not exist at authorize time - pendingTokens uses stateId as key — claim endpoint links tokens to companyId post-company-creation - Google token exchange uses fetch (Node built-in) not axios - secretService upsert pattern: getByName -> rotate if exists, create if not metrics: duration: "3 minutes 22 seconds" completed: "2026-04-03T00:36:57Z" tasks_completed: 3 files_created: 3 files_modified: 1 requirements: [CLOUD-03, CLOUD-05] --- # Phase 31 Plan 02: Google OAuth PKCE Service and Routes Summary **One-liner:** Google OAuth PKCE flow for Gemini free tier access — generateAuthUrl/exchangeCode/storeTokens service plus authorize/callback/claim/api-keys routes with in-memory pendingTokens pattern separating callback from company creation. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | googleOAuthService — PKCE generation, code exchange, token storage | 72045513 | server/src/services/google-oauth.ts | | 2 | googleOAuthRoutes (pendingTokens pattern) + API key route + mount in app.ts | c41ec162 | server/src/routes/google-oauth.ts, server/src/app.ts | | 3 | Unit tests for Google OAuth service and routes | d750d15f | server/src/__tests__/31-google-oauth.test.ts | ## What Was Built ### googleOAuthService (`server/src/services/google-oauth.ts`) - `generatePkce()` — crypto.randomBytes(32).toString("base64url") verifier, SHA256 base64url challenge - `generateAuthUrl(redirectUri, state)` — builds Google OAuth URL with PKCE S256, Gemini scopes (openid + cloud-platform + generative-language.retriever), access_type=offline, prompt=consent - `exchangeCode(code, redirectUri, verifier)` — POSTs to https://oauth2.googleapis.com/token with authorization_code grant and code_verifier - `storeTokens(companyId, tokens)` — upserts JSON-stringified tokens under name "google_gemini_oauth_token" via secretService - `resolveTokens(companyId)` — retrieves and JSON.parses stored tokens ### googleOAuthRoutes (`server/src/routes/google-oauth.ts`) - `POST /oauth/google/authorize` — assertBoard, generates UUID state, stores PKCE verifier in pendingPkce (NO companyId), returns `{ url, stateId }` - `GET /oauth/google/callback` — exchanges code via pendingPkce verifier, parks tokens in pendingTokens by stateId, redirects to `/?google_oauth=success&state=...` - `POST /oauth/google/claim` — assertBoard + assertCompanyAccess, moves tokens from pendingTokens to secretService under real companyId, returns `{ ok: true }` - `POST /api-keys/store` — assertBoard + assertCompanyAccess, upserts `${provider}_api_key` secret (openai/anthropic/groq), returns `{ ok: true }` - Cleanup of entries older than 10 minutes on each request ### Test Coverage (`server/src/__tests__/31-google-oauth.test.ts`) All 11 tests pass: - Tests 1-2: PKCE generation format and auth URL contents - Test 3: token exchange HTTP call - Tests 4-5: storeTokens create and rotate paths - Test 6: authorize returns { url, stateId } with no companyId - Test 7: callback exchanges code and redirects with success - Test 8: callback with invalid state returns 400 - Test 9: full authorize→callback→claim flow - Test 10: claim with missing stateId returns 404 - Test 11: api-keys/store upsert ## Decisions Made 1. **pendingPkce stores only `{ verifier, createdAt }`** — no companyId stored at authorize time because the company has not been created yet during onboarding step 3. This was the key design requirement. 2. **pendingTokens pattern** — callback stores tokens in memory by stateId; claim endpoint later links them to a real companyId. This separates the OAuth callback timing from company creation. 3. **Client ID is public** — `812546505895-ag9nvbqvf8cpqk3mfem1glig0jtl5i31.apps.googleusercontent.com` is the publicly documented Gemini CLI installed-app client_id (per RESEARCH.md). PKCE flow is appropriate for public clients. ## Deviations from Plan None — plan executed exactly as written. ## Known Stubs None — all functionality is fully wired. ## Self-Check: PASSED Files created: - server/src/services/google-oauth.ts: EXISTS - server/src/routes/google-oauth.ts: EXISTS - server/src/__tests__/31-google-oauth.test.ts: EXISTS Commits: - 72045513: feat(31-02): add googleOAuthService with PKCE generation and token management - c41ec162: feat(31-02): add googleOAuthRoutes with pendingTokens pattern and mount in app.ts - d750d15f: test(31-02): add 11 unit tests for Google OAuth service and routes