---
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
