From 214df47bf51282da56d94928e2d2e33eba2fba11 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Fri, 3 Apr 2026 00:22:37 +0000 Subject: [PATCH] docs(31): create phase plan for Puter.js Zero-Config Cloud 4 plans across 3 waves covering all 5 CLOUD requirements: - Plan 01 (W1): Puter proxy service, routes, tests (CLOUD-01, CLOUD-02) - Plan 02 (W1): Google OAuth PKCE + API key storage (CLOUD-03, CLOUD-05) - Plan 03 (W2): Provider Selection UI, 4-step wizard (CLOUD-01/03/04/05) - Plan 04 (W3): OAuth claim endpoint + human verification Co-Authored-By: Claude Opus 4.6 (1M context) --- .planning/ROADMAP.md | 10 +- .../31-01-PLAN.md | 253 ++++++++++++ .../31-02-PLAN.md | 210 ++++++++++ .../31-03-PLAN.md | 368 ++++++++++++++++++ .../31-04-PLAN.md | 135 +++++++ 5 files changed, 974 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/31-puter.js-zero-config-cloud/31-01-PLAN.md create mode 100644 .planning/phases/31-puter.js-zero-config-cloud/31-02-PLAN.md create mode 100644 .planning/phases/31-puter.js-zero-config-cloud/31-03-PLAN.md create mode 100644 .planning/phases/31-puter.js-zero-config-cloud/31-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2ebad08e..1f07f86a 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -124,7 +124,13 @@ Plans: 3. Token cost for Puter responses appears in the cost tracking view, attributed correctly per conversation 4. A user with Hermes, Claude Code, or OpenClaw already installed sees those tools pre-filled in the provider configuration step with no manual entry 5. A user clicking "Sign in with Google" for Gemini completes PKCE OAuth and gets a Gemini-backed chat response; the UI displays a policy-risk note that Google OAuth may trigger abuse detection -**Plans**: TBD +**Plans**: 4 plans + +Plans: +- [ ] 31-01-PLAN.md — Puter proxy service, routes, unit tests, and app.ts wiring +- [ ] 31-02-PLAN.md — Google OAuth PKCE service, routes, API key storage route +- [ ] 31-03-PLAN.md — Provider Selection UI step, PuterAuthButton, GoogleOAuthButton, ApiKeyEntryForm, 4-step wizard wiring +- [ ] 31-04-PLAN.md — Google OAuth claim endpoint, human verification of full onboarding flow **UI hint**: yes ### Phase 32: Multi-Step Onboarding Wizard @@ -216,7 +222,7 @@ All 21 v1.5 requirements are mapped to exactly one phase. No orphans. | 28. Ollama Integration & Agent Surface | v1.4 | 3/3 | Complete | 2026-04-02 | | 29. Default Provider & End-to-End | v1.4 | 2/2 | Complete | 2026-04-02 | | 30. Hardware Detection + Mode Selection | v1.5 | 2/2 | Complete | 2026-04-03 | -| 31. Puter.js Zero-Config Cloud | v1.5 | 0/TBD | Not started | - | +| 31. Puter.js Zero-Config Cloud | v1.5 | 0/4 | Not started | - | | 32. Multi-Step Onboarding Wizard | v1.5 | 0/TBD | Not started | - | | 33. Persistent Memory + Personal Assistant Mode | v1.5 | 0/TBD | Not started | - | | 34. Voice | v1.5 | 0/TBD | Not started | - | diff --git a/.planning/phases/31-puter.js-zero-config-cloud/31-01-PLAN.md b/.planning/phases/31-puter.js-zero-config-cloud/31-01-PLAN.md new file mode 100644 index 00000000..1d1379f0 --- /dev/null +++ b/.planning/phases/31-puter.js-zero-config-cloud/31-01-PLAN.md @@ -0,0 +1,253 @@ +--- +phase: 31-puter.js-zero-config-cloud +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - server/src/services/puter-proxy.ts + - server/src/routes/puter-proxy.ts + - server/src/__tests__/31-puter-proxy.test.ts + - server/src/app.ts +autonomous: true +requirements: [CLOUD-01, CLOUD-02] + +must_haves: + truths: + - "Puter auth token can be stored server-side via secretService and retrieved for API calls" + - "POST /api/puter-proxy/chat relays messages to Puter OpenAI-compat endpoint with correct Bearer header" + - "POST /api/puter-proxy/chat streams SSE tokens back to the client" + - "Cost events are recorded after every Puter chat completion with provider=puter and billingType=subscription_included" + - "POST /api/puter-proxy/token stores (or rotates) the Puter auth token idempotently" + artifacts: + - path: "server/src/services/puter-proxy.ts" + provides: "puterProxyService with resolveToken, chatStream, storeToken methods" + exports: ["puterProxyService"] + - path: "server/src/routes/puter-proxy.ts" + provides: "Express routes for POST /api/puter-proxy/chat and POST /api/puter-proxy/token" + exports: ["puterProxyRoutes"] + - path: "server/src/__tests__/31-puter-proxy.test.ts" + provides: "Unit tests for token storage, proxy streaming, cost recording" + key_links: + - from: "server/src/routes/puter-proxy.ts" + to: "server/src/services/puter-proxy.ts" + via: "import puterProxyService" + pattern: "puterProxyService" + - from: "server/src/services/puter-proxy.ts" + to: "server/src/services/secrets.ts" + via: "secretService getByName/create/rotate" + pattern: "secretService.*getByName|create|rotate" + - from: "server/src/services/puter-proxy.ts" + to: "server/src/services/costs.ts" + via: "costService.createEvent with provider=puter" + pattern: "createEvent.*puter" + - from: "server/src/app.ts" + to: "server/src/routes/puter-proxy.ts" + via: "api.use(puterProxyRoutes(db))" + pattern: "puterProxyRoutes" +--- + + +Build the server-side Puter proxy service and routes that relay AI chat requests to Puter's OpenAI-compatible endpoint, store/rotate auth tokens via secretService, stream SSE responses, and record cost events. + +Purpose: This is the core backend for CLOUD-01 (zero-config AI via Puter) and CLOUD-02 (server-proxied adapter with cost tracking). Without this, the UI has nothing to call. +Output: puterProxyService, puterProxyRoutes, unit tests, app.ts wiring + + + +@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md +@/home/mikkel/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md + + + + +From server/src/services/secrets.ts: +```typescript +export function secretService(db: Db) { + // ... + return { + getByName: async (companyId: string, name: string) => row | null, + create: async (companyId: string, input: { name: string; provider: SecretProvider; value: string; description?: string | null }, actor?) => secret, + rotate: async (secretId: string, input: { value: string }, actor?) => secret, + resolveSecretValue: async (companyId: string, secretId: string, version: number | "latest") => string, + // ... + }; +} +``` + +From server/src/services/costs.ts: +```typescript +export function costService(db: Db, budgetHooks?) { + return { + createEvent: async (companyId: string, data: { + agentId: string; + provider: string; + biller?: string; + billingType?: string; + model: string; + inputTokens: number; + outputTokens: number; + costCents: number; + occurredAt: Date; + cachedInputTokens?: number; + }) => event, + }; +} +``` + +From server/src/routes/authz.ts: +```typescript +export function assertBoard(req: Request): void; +export function assertCompanyAccess(req: Request, companyId: string): void; +``` + +From server/src/routes/chat.ts (SSE pattern, lines 96-102): +```typescript +res.setHeader("Content-Type", "text/event-stream"); +res.setHeader("Cache-Control", "no-cache"); +res.setHeader("Connection", "keep-alive"); +res.setHeader("X-Accel-Buffering", "no"); +res.flushHeaders(); +res.write(":ok\n\n"); +``` + +From server/src/app.ts (route mounting, lines 155-158): +```typescript +api.use(secretRoutes(db)); +api.use(costRoutes(db)); +// ... new routes go here, inside the api Router (after boardMutationGuard) +``` + + + + + + + Task 1: puterProxyService + puterProxyRoutes + unit tests + + server/src/services/puter-proxy.ts, + server/src/routes/puter-proxy.ts, + server/src/__tests__/31-puter-proxy.test.ts + + + server/src/services/secrets.ts, + server/src/services/costs.ts, + server/src/routes/chat.ts, + server/src/routes/secrets.ts, + server/src/routes/authz.ts + + + - Test 1: storeToken creates a new secret via secretService when none exists (name: "puter_auth_token", provider: "local_encrypted") + - Test 2: storeToken rotates an existing secret when "puter_auth_token" already exists (calls rotate, not create) + - Test 3: resolveToken retrieves the stored token value via secretService.resolveSecretValue + - Test 4: chatStream sends POST to https://api.puter.com/puterai/openai/v1/chat/completions with Authorization Bearer header, stream: true, stream_options: { include_usage: true } + - Test 5: chatStream yields content strings from SSE data chunks + - Test 6: chatStream records a cost event with provider="puter", billingType="subscription_included", costCents=0 after stream completes + - Test 7: POST /api/puter-proxy/token stores token and returns 200 + - Test 8: POST /api/puter-proxy/chat sets SSE headers and streams response chunks + + + **Create server/src/services/puter-proxy.ts:** + + Factory function `puterProxyService(db: Db)` that uses `secretService(db)` and `costService(db)` internally. Returns: + + - `storeToken(companyId: string, token: string)`: Call `secretService.getByName(companyId, "puter_auth_token")`. If exists, call `secretService.rotate(existing.id, { value: token })`. If not, call `secretService.create(companyId, { name: "puter_auth_token", provider: "local_encrypted", value: token, description: "Puter.com auth token for AI proxy" })`. Return the secret record. + + - `resolveToken(companyId: string)`: Call `secretService.getByName(companyId, "puter_auth_token")`. Throw `unprocessable("Puter auth token not configured")` if null. Call `secretService.resolveSecretValue(companyId, secret.id, "latest")` and return string. + + - `async *chatStream(companyId, agentId, messages, model, signal)`: AsyncGenerator. + - Constants: `PUTER_BASE_URL = "https://api.puter.com/puterai/openai/v1"`, `PUTER_DEFAULT_MODEL = "claude-3-5-haiku-20241022"`. + - Call `resolveToken(companyId)` to get bearer token. + - `fetch(`${PUTER_BASE_URL}/chat/completions`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: model ?? PUTER_DEFAULT_MODEL, messages, stream: true, stream_options: { include_usage: true } }), signal })`. + - If `!response.ok || !response.body`, read body text and throw `Error(`Puter API error ${response.status}: ${text}`)`. + - Parse SSE: read from `response.body` using `getReader()`, split on `\n`, look for `data: ` prefix. Skip `[DONE]`. Parse JSON. Yield `chunk.choices?.[0]?.delta?.content`. Accumulate `usage` from chunks that have it. + - In `finally` block: `reader.releaseLock()`. Then record cost event non-blocking: `costService.createEvent(companyId, { agentId, provider: "puter", biller: "puter", billingType: "subscription_included", model, inputTokens, outputTokens, costCents: 0, occurredAt: new Date() }).catch(() => {})`. + + **Create server/src/routes/puter-proxy.ts:** + + Export `puterProxyRoutes(db: Db): Router`. + + - `POST /puter-proxy/token`: `assertBoard(req)`. Extract `companyId` and `token` from `req.body`. Validate both are non-empty strings. `assertCompanyAccess(req, companyId)`. Call `puterProxyService(db).storeToken(companyId, token)`. Return `res.json({ ok: true })`. + + - `POST /puter-proxy/chat`: `assertBoard(req)`. Extract `companyId`, `agentId`, `messages`, `model` from `req.body`. Validate `companyId` is string, `messages` is array with at least one entry. `assertCompanyAccess(req, companyId)`. Set SSE headers (same pattern as chat.ts lines 96-102). Create AbortController, wire `req.on("close")`. Iterate `puterProxyService(db).chatStream(...)` yielding `data: ${JSON.stringify({ token: chunk })}\n\n`. On complete write `data: ${JSON.stringify({ done: true })}\n\n`. On error write `data: ${JSON.stringify({ error: "Puter stream error" })}\n\n`. Always `res.end()`. + + **Create server/src/__tests__/31-puter-proxy.test.ts:** + + Use vitest. Mock `fetch` globally (vi.stubGlobal). Create a mock db object that stubs secretService and costService behavior. Test all 8 behaviors listed above. For SSE streaming test, create a mock ReadableStream that yields pre-formatted SSE chunks. + + Import patterns: `import { puterProxyService } from "../services/puter-proxy.js"` and `import { puterProxyRoutes } from "../routes/puter-proxy.js"`. For route tests, use supertest with a minimal Express app. + + + cd /opt/nexus && pnpm --filter @paperclipai/server test run src/__tests__/31-puter-proxy.test.ts + + + - grep -q "puterProxyService" server/src/services/puter-proxy.ts + - grep -q "puter_auth_token" server/src/services/puter-proxy.ts + - grep -q "api.puter.com/puterai/openai/v1" server/src/services/puter-proxy.ts + - grep -q "subscription_included" server/src/services/puter-proxy.ts + - grep -q "costCents: 0" server/src/services/puter-proxy.ts + - grep -q "stream_options" server/src/services/puter-proxy.ts + - grep -q "puterProxyRoutes" server/src/routes/puter-proxy.ts + - grep -q "puter-proxy/token" server/src/routes/puter-proxy.ts + - grep -q "puter-proxy/chat" server/src/routes/puter-proxy.ts + - grep -q "text/event-stream" server/src/routes/puter-proxy.ts + - grep -q "assertBoard" server/src/routes/puter-proxy.ts + - All 8 tests pass + + + puterProxyService handles token storage (create/rotate idempotent), chatStream relays to Puter with streaming and cost tracking, puterProxyRoutes exposes POST /puter-proxy/token and POST /puter-proxy/chat with board auth, all tests green + + + + + Task 2: Mount puterProxyRoutes in app.ts + server/src/app.ts + server/src/app.ts + + Add import at top of server/src/app.ts: + ```typescript + import { puterProxyRoutes } from "./routes/puter-proxy.js"; + ``` + + Mount inside the `api` Router block (after `boardMutationGuard()`, near the other route mounts around line 158), add: + ```typescript + api.use(puterProxyRoutes(db)); + ``` + + Place it after `api.use(costRoutes(db));` (line 158) to keep related routes together. This ensures the route is protected by boardMutationGuard (correct — Puter proxy requires board auth). + + + cd /opt/nexus && grep -n "puterProxyRoutes" server/src/app.ts + + + - grep -q "import.*puterProxyRoutes.*from.*routes/puter-proxy" server/src/app.ts + - grep -q "api.use(puterProxyRoutes(db))" server/src/app.ts + + puterProxyRoutes mounted inside api Router with board auth protection; server compiles without errors + + + + + +- `cd /opt/nexus && pnpm --filter @paperclipai/server test run src/__tests__/31-puter-proxy.test.ts` — all tests pass +- `cd /opt/nexus && pnpm --filter @paperclipai/server exec tsc --noEmit` — no TypeScript errors +- grep confirms route is mounted in app.ts + + + +- Puter proxy service exists with token storage, streaming chat, and cost tracking +- Routes exist for POST /puter-proxy/token and POST /puter-proxy/chat +- Routes mounted in app.ts behind boardMutationGuard +- All unit tests pass + + + +After completion, create `.planning/phases/31-puter.js-zero-config-cloud/31-01-SUMMARY.md` + diff --git a/.planning/phases/31-puter.js-zero-config-cloud/31-02-PLAN.md b/.planning/phases/31-puter.js-zero-config-cloud/31-02-PLAN.md new file mode 100644 index 00000000..ad5bb8f6 --- /dev/null +++ b/.planning/phases/31-puter.js-zero-config-cloud/31-02-PLAN.md @@ -0,0 +1,210 @@ +--- +phase: 31-puter.js-zero-config-cloud +plan: "02" +type: execute +wave: 1 +depends_on: [] +files_modified: + - server/src/services/google-oauth.ts + - server/src/routes/google-oauth.ts + - server/src/app.ts +autonomous: true +requirements: [CLOUD-03, CLOUD-05] + +must_haves: + truths: + - "Server can generate a Google OAuth PKCE authorization URL with correct client_id, scopes, code_challenge" + - "Server can exchange an authorization code for access/refresh tokens via Google's token endpoint" + - "Google OAuth tokens are stored server-side via secretService as google_gemini_oauth_token" + - "POST /api/puter-proxy/token supports storing arbitrary provider tokens (reused for API key entry)" + artifacts: + - path: "server/src/services/google-oauth.ts" + provides: "googleOAuthService with generateAuthUrl, exchangeCode, storeTokens methods" + exports: ["googleOAuthService"] + - path: "server/src/routes/google-oauth.ts" + provides: "Routes for GET /api/oauth/google/authorize and GET /api/oauth/google/callback" + exports: ["googleOAuthRoutes"] + key_links: + - from: "server/src/services/google-oauth.ts" + to: "server/src/services/secrets.ts" + via: "secretService getByName/create/rotate for google_gemini_oauth_token" + pattern: "google_gemini_oauth_token" + - from: "server/src/routes/google-oauth.ts" + to: "server/src/services/google-oauth.ts" + via: "import googleOAuthService" + pattern: "googleOAuthService" +--- + + +Build the Google OAuth PKCE service and routes for Gemini free tier access, plus ensure the token storage endpoint supports API key entry for subscription providers. + +Purpose: CLOUD-03 (Google OAuth for Gemini) and CLOUD-05 (API key entry). The OAuth callback needs a server-side route to exchange the code for tokens. +Output: googleOAuthService, googleOAuthRoutes, app.ts wiring + + + +@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md +@/home/mikkel/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md + + +From server/src/services/secrets.ts: +```typescript +export function secretService(db: Db) { + return { + getByName: async (companyId: string, name: string) => row | null, + create: async (companyId, { name, provider, value, description }, actor?) => secret, + rotate: async (secretId, { value }, actor?) => secret, + resolveSecretValue: async (companyId, secretId, "latest") => string, + }; +} +``` + +From server/src/routes/authz.ts: +```typescript +export function assertBoard(req): void; +export function assertCompanyAccess(req, companyId): void; +``` + +From server/src/app.ts (route mounting): +```typescript +// Unauthenticated routes mounted before api Router: +app.use("/api", hardwareRoutes()); // line 132 + +// Authenticated routes inside api Router: +api.use(secretRoutes(db)); // line 157 +``` + + + + + + + Task 1: googleOAuthService — PKCE generation, code exchange, token storage + server/src/services/google-oauth.ts + + server/src/services/secrets.ts, + server/src/routes/secrets.ts + + + Create `server/src/services/google-oauth.ts`. Export `googleOAuthService(db: Db)`. + + **Constants:** + ```typescript + const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; + const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; + const GEMINI_CLIENT_ID = "812546505895-ag9nvbqvf8cpqk3mfem1glig0jtl5i31.apps.googleusercontent.com"; + const GEMINI_SCOPES = "openid https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever"; + const GOOGLE_TOKEN_SECRET_NAME = "google_gemini_oauth_token"; + ``` + + The client_id is the publicly documented Gemini CLI installed-app client_id (per RESEARCH.md). + + **Methods:** + + - `generatePkce()`: Use `crypto.randomBytes(32).toString("base64url")` for verifier. `crypto.createHash("sha256").update(verifier).digest("base64url")` for challenge. Return `{ verifier, challenge }`. + + - `generateAuthUrl(redirectUri: string, state: string)`: Generate PKCE. Build URL with params: `client_id=GEMINI_CLIENT_ID`, `redirect_uri=redirectUri`, `response_type=code`, `scope=GEMINI_SCOPES`, `code_challenge=challenge`, `code_challenge_method=S256`, `state=state`, `access_type=offline`, `prompt=consent`. Return `{ url, verifier }`. Caller must store verifier temporarily (in-memory Map keyed by state). + + - `exchangeCode(code: string, redirectUri: string, verifier: string)`: POST to `GOOGLE_TOKEN_URL` with `grant_type=authorization_code`, `client_id=GEMINI_CLIENT_ID`, `code`, `redirect_uri=redirectUri`, `code_verifier=verifier`. Parse JSON response. Return `{ accessToken, refreshToken, expiresIn }`. + + - `storeTokens(companyId: string, tokens: { accessToken: string; refreshToken?: string })`: Store JSON.stringify(tokens) via secretService upsert pattern (getByName → exists ? rotate : create) under name `google_gemini_oauth_token`, provider `local_encrypted`. + + - `resolveTokens(companyId: string)`: Get secret by name, resolve value, JSON.parse, return `{ accessToken, refreshToken }`. + + Use `import crypto from "node:crypto";` for PKCE. Use `fetch` (Node built-in) for token exchange. + + + cd /opt/nexus && grep -c "generatePkce\|exchangeCode\|storeTokens\|resolveTokens\|generateAuthUrl" server/src/services/google-oauth.ts + + + - grep -q "googleOAuthService" server/src/services/google-oauth.ts + - grep -q "google_gemini_oauth_token" server/src/services/google-oauth.ts + - grep -q "812546505895" server/src/services/google-oauth.ts + - grep -q "code_challenge_method" server/src/services/google-oauth.ts + - grep -q "randomBytes" server/src/services/google-oauth.ts + - grep -q "base64url" server/src/services/google-oauth.ts + - grep -q "oauth2.googleapis.com/token" server/src/services/google-oauth.ts + + googleOAuthService generates PKCE auth URLs, exchanges codes for tokens, and stores/retrieves tokens via secretService + + + + Task 2: googleOAuthRoutes + API key storage route + mount in app.ts + + server/src/routes/google-oauth.ts, + server/src/app.ts + + + server/src/app.ts, + server/src/routes/secrets.ts, + server/src/routes/authz.ts + + + **Create server/src/routes/google-oauth.ts:** + + Export `googleOAuthRoutes(db: Db): Router`. + + In-memory Map for PKCE state: `const pendingPkce = new Map()`. Clean up entries older than 10 minutes on each request. + + Routes: + + - `POST /oauth/google/authorize`: `assertBoard(req)`. Extract `companyId` from `req.body`. `assertCompanyAccess(req, companyId)`. Generate random `state` via `crypto.randomUUID()`. Construct `redirectUri` from `req.protocol + "://" + req.get("host") + "/api/oauth/google/callback"`. Call `googleOAuthService(db).generateAuthUrl(redirectUri, state)`. Store `{ verifier, companyId, createdAt: Date.now() }` in `pendingPkce` keyed by `state`. Return `res.json({ url })`. + + - `GET /oauth/google/callback`: Extract `code` and `state` from `req.query`. Look up `pendingPkce.get(state)`. If not found, return `res.status(400).send("Invalid or expired OAuth state")`. Delete from map. Construct `redirectUri` same as above. Call `googleOAuthService(db).exchangeCode(code, redirectUri, verifier)`. Call `googleOAuthService(db).storeTokens(companyId, { accessToken, refreshToken })`. Redirect to a UI success page: `res.redirect("/?google_oauth=success")`. Wrap in try/catch — on error, redirect to `/?google_oauth=error`. + + - `POST /api-keys/store`: `assertBoard(req)`. Extract `companyId`, `provider` (string: "openai" | "anthropic" | "groq"), `apiKey` from `req.body`. Validate all present. `assertCompanyAccess(req, companyId)`. Store via secretService upsert pattern with name `${provider}_api_key` (e.g., "openai_api_key"), provider "local_encrypted". Return `res.json({ ok: true })`. + + **Mount in app.ts:** + + Add import: `import { googleOAuthRoutes } from "./routes/google-oauth.js";` + + The OAuth callback (`GET /oauth/google/callback`) needs to be accessible without boardMutationGuard blocking the GET (GETs pass through boardMutationGuard since it only blocks mutations). The authorize and api-keys/store endpoints need board auth. Mount inside the `api` Router: + + ```typescript + api.use(googleOAuthRoutes(db)); + ``` + + Place after `puterProxyRoutes(db)` (or after `costRoutes(db)` if plan 01 hasn't run yet — the executor should place it near the other new routes). + + + cd /opt/nexus && grep -n "googleOAuthRoutes" server/src/app.ts && grep -c "oauth/google/authorize\|oauth/google/callback\|api-keys/store" server/src/routes/google-oauth.ts + + + - grep -q "googleOAuthRoutes" server/src/routes/google-oauth.ts + - grep -q "oauth/google/authorize" server/src/routes/google-oauth.ts + - grep -q "oauth/google/callback" server/src/routes/google-oauth.ts + - grep -q "api-keys/store" server/src/routes/google-oauth.ts + - grep -q "pendingPkce" server/src/routes/google-oauth.ts + - grep -q "assertBoard" server/src/routes/google-oauth.ts + - grep -q "import.*googleOAuthRoutes" server/src/app.ts + - grep -q "api.use(googleOAuthRoutes" server/src/app.ts + + Google OAuth PKCE routes handle authorize + callback flow, API key storage route accepts provider/key pairs, both mounted in app.ts with board auth + + + + + +- `cd /opt/nexus && pnpm --filter @paperclipai/server exec tsc --noEmit` — no TypeScript errors +- grep confirms all routes and services exist with correct exports +- Google OAuth routes handle full PKCE flow (authorize URL generation, callback code exchange, token storage) +- API key storage route supports OpenAI/Anthropic/Groq key entry + + + +- googleOAuthService generates PKCE auth URLs, exchanges codes, stores tokens +- Routes exist for POST /oauth/google/authorize, GET /oauth/google/callback, POST /api-keys/store +- All routes mounted in app.ts +- Server compiles without TypeScript errors + + + +After completion, create `.planning/phases/31-puter.js-zero-config-cloud/31-02-SUMMARY.md` + diff --git a/.planning/phases/31-puter.js-zero-config-cloud/31-03-PLAN.md b/.planning/phases/31-puter.js-zero-config-cloud/31-03-PLAN.md new file mode 100644 index 00000000..7fedf0e2 --- /dev/null +++ b/.planning/phases/31-puter.js-zero-config-cloud/31-03-PLAN.md @@ -0,0 +1,368 @@ +--- +phase: 31-puter.js-zero-config-cloud +plan: "03" +type: execute +wave: 2 +depends_on: ["31-01", "31-02"] +files_modified: + - ui/src/components/onboarding/ProviderSelectionStep.tsx + - ui/src/components/onboarding/PuterAuthButton.tsx + - ui/src/components/onboarding/GoogleOAuthButton.tsx + - ui/src/components/onboarding/ApiKeyEntryForm.tsx + - ui/src/components/NexusOnboardingWizard.tsx + - ui/src/api/puter-proxy.ts +autonomous: true +requirements: [CLOUD-01, CLOUD-03, CLOUD-04, CLOUD-05] + +must_haves: + truths: + - "User sees a Provider Selection step (Step 3 of 4) in the onboarding wizard with Puter, Google, and API Key options" + - "Clicking 'Continue with Puter' triggers Puter auth popup, stores token server-side, and advances wizard" + - "Google option shows policy-risk warning before enabling the Sign in button (3-second gate)" + - "API key option shows inline form with provider dropdown and key input" + - "Skip for now button is always visible and advances to root directory step" + - "Pre-installed tools (Hermes, Claude Code, OpenClaw) show detected badges via adapter probe" + - "Step indicator shows 'Step N of 4' (updated from 3)" + artifacts: + - path: "ui/src/components/onboarding/ProviderSelectionStep.tsx" + provides: "Provider selection UI with three option cards and skip button" + exports: ["ProviderSelectionStep"] + - path: "ui/src/components/onboarding/PuterAuthButton.tsx" + provides: "Puter auth button that loads CDN script, calls signIn, posts token to server" + exports: ["PuterAuthButton"] + - path: "ui/src/components/onboarding/GoogleOAuthButton.tsx" + provides: "Google OAuth button with 3-second risk warning gate" + exports: ["GoogleOAuthButton"] + - path: "ui/src/components/onboarding/ApiKeyEntryForm.tsx" + provides: "API key entry form with provider dropdown" + exports: ["ApiKeyEntryForm"] + - path: "ui/src/api/puter-proxy.ts" + provides: "API client for puter-proxy and oauth endpoints" + exports: ["puterProxyApi"] + - path: "ui/src/components/NexusOnboardingWizard.tsx" + provides: "4-step wizard with provider selection step inserted" + key_links: + - from: "ui/src/components/onboarding/PuterAuthButton.tsx" + to: "POST /api/puter-proxy/token" + via: "fetch call to store Puter token server-side" + pattern: "puter-proxy/token" + - from: "ui/src/components/onboarding/GoogleOAuthButton.tsx" + to: "POST /api/oauth/google/authorize" + via: "fetch call to get auth URL, then window.open" + pattern: "oauth/google/authorize" + - from: "ui/src/components/onboarding/ApiKeyEntryForm.tsx" + to: "POST /api/api-keys/store" + via: "fetch call to store API key" + pattern: "api-keys/store" + - from: "ui/src/components/NexusOnboardingWizard.tsx" + to: "ui/src/components/onboarding/ProviderSelectionStep.tsx" + via: "import and render in step 3" + pattern: "ProviderSelectionStep" +--- + + +Build the Provider Selection step UI components and wire them into the existing 3-step onboarding wizard (making it 4 steps). This includes the Puter auth button, Google OAuth button with risk warning, API key entry form, and adapter auto-detection badges. + +Purpose: CLOUD-01 (Puter zero-config UI), CLOUD-03 (Google OAuth UI), CLOUD-04 (auto-detect tools), CLOUD-05 (API key entry UI). This is the user-facing surface for all cloud provider paths. +Output: ProviderSelectionStep, PuterAuthButton, GoogleOAuthButton, ApiKeyEntryForm, updated wizard + + + +@/home/mikkel/.claude/get-shit-done/workflows/execute-plan.md +@/home/mikkel/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/31-puter.js-zero-config-cloud/31-RESEARCH.md +@.planning/phases/31-puter.js-zero-config-cloud/31-UI-SPEC.md +@.planning/phases/30-hardware-detection-mode-selection/30-02-SUMMARY.md + + +From ui/src/components/NexusOnboardingWizard.tsx (current state): +```typescript +// 3-step wizard: step 1 = hardware, step 2 = mode, step 3 = root directory +// Step indicator: "Step {step} of 3" +// defaultAdapter state: "claude_local" | "hermes_local" +// probing state for adapter detection +// handleSubmit creates company + agents on step 3 form submit +``` + +From ui/src/components/onboarding/ModeSelector.tsx (selected state pattern): +```typescript +// Cards use: border-primary bg-primary/5 when selected +// Vertical stack with gap-3 +// Each card is a