From dd10b4b94c1f5106c587eed9b260792efbe54483 Mon Sep 17 00:00:00 2001 From: yuna amelia <230527278+yunaamelia@users.noreply.github.com> Date: Fri, 29 May 2026 12:31:39 +0800 Subject: [PATCH] docs(plan): Phase 1 windsurf login hotfix implementation plan 10 tasks covering: - TDD assertions for flowType + 410 Gone responses - Provider switch to import_token - Route handler retiring authorize/start-callback-server/poll-callback - OAuthModal UI override - i18n sync - Verification + PR steps --- .../plans/2026-05-29-windsurf-login-hotfix.md | 740 ++++++++++++++++++ 1 file changed, 740 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md diff --git a/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md b/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md new file mode 100644 index 0000000000..0df2ad2932 --- /dev/null +++ b/docs/superpowers/plans/2026-05-29-windsurf-login-hotfix.md @@ -0,0 +1,740 @@ +# Windsurf Login Hotfix (Phase 1) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Hapus PKCE OAuth flow yang rusak (`app.devin.ai/editor/signin` → 404) untuk provider `windsurf` & `devin-cli`, jadikan import-token sebagai satu-satunya path login. + +**Architecture:** Single-branch hotfix di `fix/windsurf-login-2026-05-29`. Modifikasi 5 file source + 1 file test, tidak ada migrasi DB, tidak ada credential baru. Existing connections (api_key tersimpan) tetap jalan tanpa perubahan. + +**Tech Stack:** TypeScript 5.9, Next.js 16 App Router, Node.js test runner (`node --import tsx/esm --test`), Zod validation, React 19. + +**Spec:** `docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md` (Phase 1 section). + +--- + +## File Structure + +| File | Type | Responsibility | +|---|---|---| +| `src/lib/oauth/providers/windsurf.ts` | Modify | Drop PKCE handlers (`buildAuthUrl`, `flowType: "authorization_code_pkce"`). Set `flowType: "import_token"`. Keep `mapTokens` + token validation. | +| `src/lib/oauth/constants/oauth.ts` | Modify | Comment out PKCE-only fields. Keep `inferenceUrl`, `showAuthTokenUrl`, `firebaseApiKey`, `ideName`. | +| `src/lib/oauth/providers/index.ts` | No change | Re-export already correct (`windsurf` + alias `devin-cli`). | +| `src/app/api/oauth/[provider]/[action]/route.ts` | Modify | Remove `windsurf` & `devin-cli` from `PKCE_CALLBACK_PROVIDERS`. Return HTTP 410 Gone for `authorize` & `start-callback-server` actions. | +| `src/shared/components/OAuthModal.tsx` | Modify | Hide PKCE buttons for windsurf/devin-cli, show paste-token panel + "Get token" link. | +| `tests/unit/windsurf-devin-executors.test.ts` | Modify | Add tests asserting PKCE disabled + import-token still works. | + +**Out of scope (Phase 2):** Firebase OAuth, RegisterUser, refresh worker, `WindsurfLoginModal.tsx`, DB migration. See spec Phase 2 section. + +--- + +## Pre-flight + +- [ ] **Step 0.1: Verify branch + identity** + +Run: +```bash +git branch --show-current +git config user.email +``` +Expected: branch is `fix/windsurf-login-2026-05-29`, email is set (any value, just non-empty). + +- [ ] **Step 0.2: Verify spec is committed** + +Run: +```bash +git log --oneline -3 +``` +Expected: top commit is `docs(oauth): add Windsurf login fix design ...`. + +--- + +## Task 1: Test — PKCE auth URL generation throws / returns disabled + +**Files:** +- Test: `tests/unit/windsurf-devin-executors.test.ts` (modify — add test cases at end of describe block) + +- [ ] **Step 1.1: Read existing test structure** + +Run: +```bash +head -30 tests/unit/windsurf-devin-executors.test.ts +``` +Note the existing imports + describe blocks. Match style. + +- [ ] **Step 1.2: Add failing test for PKCE-disabled auth URL** + +Append to `tests/unit/windsurf-devin-executors.test.ts` (inside the existing top-level `describe` or as a new describe at the end): + +```typescript +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { generateAuthData, getProvider } from "@/lib/oauth/providers"; + +test("windsurf provider: flowType is import_token (PKCE disabled post-rebrand)", () => { + const provider = getProvider("windsurf"); + assert.equal(provider.flowType, "import_token"); +}); + +test("devin-cli provider: flowType is import_token (shares windsurf config)", () => { + const provider = getProvider("devin-cli"); + assert.equal(provider.flowType, "import_token"); +}); + +test("windsurf provider: generateAuthData returns no authUrl (PKCE flow disabled)", () => { + const data = generateAuthData("windsurf", "http://localhost:0/auth/callback"); + assert.equal(data.authUrl, undefined); + assert.equal(data.supported, false); + assert.match(data.error ?? "", /import-token|disabled|app\.devin\.ai/i); +}); + +test("devin-cli provider: generateAuthData returns no authUrl", () => { + const data = generateAuthData("devin-cli", "http://localhost:0/auth/callback"); + assert.equal(data.authUrl, undefined); + assert.equal(data.supported, false); +}); +``` + +If file already imports `test` and `assert`, do not duplicate; reuse. + +- [ ] **Step 1.3: Run test — confirm failure** + +Run: +```bash +node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30 +``` +Expected: 4 new tests FAIL — `flowType` is `"authorization_code_pkce"` not `"import_token"`, and `generateAuthData` still returns a real URL. + +--- + +## Task 2: Implementation — switch windsurf provider to import_token flow + +**Files:** +- Modify: `src/lib/oauth/providers/windsurf.ts` (full file rewrite — small file) + +- [ ] **Step 2.1: Read full current file** + +Run: +```bash +wc -l src/lib/oauth/providers/windsurf.ts +``` +Confirm file size before rewriting. Should be < 200 lines. + +- [ ] **Step 2.2: Replace file content** + +Write `src/lib/oauth/providers/windsurf.ts`: + +```typescript +import { WINDSURF_CONFIG } from "../constants/oauth"; + +/** + * Windsurf / Devin CLI OAuth Provider — import-token only (Phase 1 hotfix, 2026-05-29). + * + * The previous PKCE Authorization Code flow targeting `https://app.devin.ai/editor/signin` + * stopped working post-rebrand: that endpoint now returns 404. Until Phase 2 ports the + * Firebase OAuth + RegisterUser flow (see docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md), + * the only supported login path is import-token: + * + * 1. User opens https://windsurf.com/show-auth-token in a browser + * 2. Copies the displayed Windsurf API key (`sk-ws-...` style) + * 3. Pastes it into OmniRoute via /api/oauth/windsurf/import-token + * + * The pasted token is stored as `accessToken` and used directly by `WindsurfExecutor` + * (open-sse/executors/windsurf.ts) as the `Authorization: Bearer ...` header against + * the inference server (`server.self-serve.windsurf.com`). + */ +export const windsurf = { + config: WINDSURF_CONFIG, + flowType: "import_token" as const, + + /** + * Validate a pasted Windsurf API key. Accepts the `sk-ws-...` format issued by + * windsurf.com/show-auth-token and the legacy raw-token format. Empty or + * whitespace-only tokens are rejected. + */ + validateImportToken(token: string): { valid: boolean; reason?: string } { + const trimmed = (token ?? "").trim(); + if (!trimmed) { + return { valid: false, reason: "Token is empty" }; + } + if (trimmed.length < 16) { + return { valid: false, reason: "Token is too short" }; + } + return { valid: true }; + }, + + /** + * Map a pasted import token onto the connection record. The token IS the + * Windsurf API key; there is no exchange step. + */ + mapTokens(token: string) { + return { + accessToken: token, + refreshToken: null, + expiresAt: null, + }; + }, +}; +``` + +- [ ] **Step 2.3: Re-run Task 1 tests** + +Run: +```bash +node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30 +``` +Expected: 4 new tests now FAIL only on `generateAuthData` — `flowType` tests should PASS. `generateAuthData` still depends on the route handler / provider dispatcher, which Task 3 wires. + +If `flowType` tests still fail with `flowType: "authorization_code_pkce"`, re-check that `windsurf.ts` was overwritten. + +--- + +## Task 3: Make `generateAuthData` return disabled stub for windsurf/devin-cli + +**Files:** +- Modify: `src/lib/oauth/providers/index.ts` (add helper export — small) +- OR: Modify the dispatcher inside `src/lib/oauth/providers/` that owns `generateAuthData` (find it) + +- [ ] **Step 3.1: Locate `generateAuthData` definition** + +Run: +```bash +grep -rn "export function generateAuthData\|export const generateAuthData" src/lib/oauth/ +``` +Expected: one hit. Note the file path — call it ``. + +- [ ] **Step 3.2: Read the dispatcher** + +Read ``. Identify the branch where `provider.flowType === "authorization_code_pkce"` builds the auth URL via `provider.buildAuthUrl(...)`. + +- [ ] **Step 3.3: Add early-return for `import_token` flowType** + +In ``, modify `generateAuthData` so that when `provider.flowType === "import_token"` it returns: + +```typescript +if (provider.flowType === "import_token") { + return { + authUrl: undefined, + codeVerifier: undefined, + state: undefined, + supported: false, + error: + provider === windsurf || providerKey === "windsurf" || providerKey === "devin-cli" + ? "Browser login disabled — paste token from https://windsurf.com/show-auth-token instead. Phase 2 will restore Firebase OAuth." + : "This provider only supports import-token flow.", + }; +} +``` + +Match the ``'s actual function signature — if it receives `providerKey: string` and `redirectUri: string`, use those names. The exact field set returned must match what `OAuthModal.tsx` and the `/authorize` route already consume (see Step 1.2 expected fields). Keep `supported: false` and a non-empty `error` string; both are checked by the test. + +- [ ] **Step 3.4: Run all Task 1 tests — must pass now** + +Run: +```bash +node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30 +``` +Expected: all 4 new tests PASS. + +- [ ] **Step 3.5: Commit progress** + +Run: +```bash +git add src/lib/oauth/providers/windsurf.ts src/lib/oauth/providers/index.ts tests/unit/windsurf-devin-executors.test.ts +# Plus the dispatcher file from Step 3.1 if different +git status --short +git commit -m "fix(oauth): switch windsurf provider to import_token flow + +The PKCE auth URL targeting app.devin.ai/editor/signin returns 404 +post-rebrand. Until Phase 2 ports Firebase OAuth + RegisterUser, the +only supported path is import-token via windsurf.com/show-auth-token. + +- windsurf.ts: drop buildAuthUrl, set flowType=import_token +- generateAuthData returns supported:false + helpful error for windsurf/devin-cli +- tests: assert flowType + disabled stub" +``` + +--- + +## Task 4: Test — `start-callback-server` returns 410 Gone for windsurf/devin-cli + +**Files:** +- Test: `tests/unit/windsurf-devin-executors.test.ts` (extend) + +- [ ] **Step 4.1: Add failing test** + +Append to `tests/unit/windsurf-devin-executors.test.ts`: + +```typescript +import { GET as oauthGet } from "@/app/api/oauth/[provider]/[action]/route"; + +test("OAuth route: windsurf/start-callback-server returns 410 Gone", async () => { + const url = "http://localhost:20128/api/oauth/windsurf/start-callback-server"; + const request = new Request(url, { method: "GET" }); + const response = await oauthGet(request, { + params: Promise.resolve({ provider: "windsurf", action: "start-callback-server" }), + } as never); + assert.equal(response.status, 410); + const body = await response.json(); + assert.match(body.error, /import-token|disabled|410/i); +}); + +test("OAuth route: devin-cli/authorize returns 410 Gone", async () => { + const url = "http://localhost:20128/api/oauth/devin-cli/authorize"; + const request = new Request(url, { method: "GET" }); + const response = await oauthGet(request, { + params: Promise.resolve({ provider: "devin-cli", action: "authorize" }), + } as never); + assert.equal(response.status, 410); +}); +``` + +- [ ] **Step 4.2: Run — confirm failure** + +Run: +```bash +node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30 +``` +Expected: 2 new tests FAIL — current handler probably returns 400 or 200. + +--- + +## Task 5: Implementation — return 410 Gone for disabled PKCE actions + +**Files:** +- Modify: `src/app/api/oauth/[provider]/[action]/route.ts:75-160` (`GET` handler) + +- [ ] **Step 5.1: Read existing handler shape** + +Run: +```bash +sed -n '40,50p;75,165p' src/app/api/oauth/[provider]/[action]/route.ts +``` +Confirm `PKCE_CALLBACK_PROVIDERS` set definition + the `if (action === "authorize")` branch. + +- [ ] **Step 5.2: Remove `windsurf` and `devin-cli` from `PKCE_CALLBACK_PROVIDERS`** + +In `src/app/api/oauth/[provider]/[action]/route.ts`, find: + +```typescript +const PKCE_CALLBACK_PROVIDERS = new Set(["codex", "windsurf", "devin-cli"]); +``` + +Replace with: + +```typescript +// windsurf & devin-cli removed 2026-05-29 — PKCE endpoint app.devin.ai/editor/signin +// returns 404 post-rebrand. Phase 2 will reintroduce browser login via Firebase OAuth. +const PKCE_CALLBACK_PROVIDERS = new Set(["codex"]); + +// Providers whose PKCE flow has been retired but whose import-token path is still +// active. The route returns 410 Gone for `authorize` / `start-callback-server` +// and points users at /import-token. +const RETIRED_PKCE_PROVIDERS = new Set(["windsurf", "devin-cli"]); +``` + +- [ ] **Step 5.3: Add 410 short-circuit at top of `GET` handler body** + +Inside `GET`, immediately after `const { provider, action } = await params;` (and before `if (action === "authorize")`), insert: + +```typescript +if ( + RETIRED_PKCE_PROVIDERS.has(provider) && + (action === "authorize" || action === "start-callback-server" || action === "poll-callback") +) { + return NextResponse.json( + { + error: + "Browser OAuth disabled for this provider — use import-token via /api/oauth/" + + provider + + "/import-token. See https://windsurf.com/show-auth-token to obtain a token.", + }, + { status: 410 } + ); +} +``` + +- [ ] **Step 5.4: Run tests — must pass** + +Run: +```bash +node --import tsx/esm --test tests/unit/windsurf-devin-executors.test.ts 2>&1 | tail -30 +``` +Expected: all 6 new tests PASS. + +- [ ] **Step 5.5: Run full unit suite — no regressions** + +Run: +```bash +npm run test:unit 2>&1 | tail -30 +``` +Expected: green. If a Codex-specific test now fails because `PKCE_CALLBACK_PROVIDERS.has("windsurf")` was assumed, fix that test (Codex behaviour didn't change; the assertion did). + +- [ ] **Step 5.6: Commit** + +Run: +```bash +git add src/app/api/oauth/[provider]/[action]/route.ts tests/unit/windsurf-devin-executors.test.ts +git commit -m "fix(oauth): return 410 Gone for retired windsurf/devin-cli PKCE actions + +start-callback-server, authorize, and poll-callback now return 410 +with a pointer to /import-token. Codex PKCE flow unchanged." +``` + +--- + +## Task 6: Clean up `WINDSURF_CONFIG` — annotate PKCE fields as retired + +**Files:** +- Modify: `src/lib/oauth/constants/oauth.ts:328-365` + +- [ ] **Step 6.1: Read full block** + +Run: +```bash +sed -n '328,375p' src/lib/oauth/constants/oauth.ts +``` + +- [ ] **Step 6.2: Update header comment block** + +Replace the comment block above `export const WINDSURF_CONFIG = {` with: + +```typescript +// Windsurf / Devin CLI Configuration +// +// 2026-05-29 (Phase 1 hotfix): +// The browser PKCE flow targeting https://app.devin.ai/editor/signin returned +// 404 post-rebrand. PKCE-only fields (`authorizeUrl`, `codeChallengeMethod`, +// `callbackPort`, `callbackPath`, `apiServerUrl`, `exchangePath`) are kept +// below for archival reference but are NO LONGER consumed by any code path — +// the provider exports flowType="import_token" only. +// +// Phase 2 will reintroduce browser login via Firebase OAuth + RegisterUser. +// Spec: docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md. +// +// Active fields: +// - inferenceUrl → used by WindsurfExecutor (open-sse/executors/windsurf.ts) +// - showAuthTokenUrl → linked from OAuthModal "Get token" button +// - firebaseApiKey → reserved for Phase 2 +// - ideName → sent in extension headers +``` + +Inline-annotate the retired fields (`authorizeUrl`, `codeChallengeMethod`, `callbackPort`, `callbackPath`, `apiServerUrl`, `exchangePath`) with `// retired 2026-05-29` comments. Do **not** delete them — that would break any downstream import; just mark them. + +- [ ] **Step 6.3: Verify typecheck still passes** + +Run: +```bash +npm run typecheck:core 2>&1 | tail -10 +``` +Expected: no errors. + +- [ ] **Step 6.4: Commit** + +Run: +```bash +git add src/lib/oauth/constants/oauth.ts +git commit -m "refactor(oauth): annotate retired PKCE fields in WINDSURF_CONFIG + +No behaviour change — comment-only update documenting that authorizeUrl, +codeChallengeMethod, callbackPort, callbackPath, apiServerUrl, and +exchangePath are no longer consumed." +``` + +--- + +## Task 7: UI — `OAuthModal.tsx` skips PKCE buttons for windsurf/devin-cli + +**Files:** +- Modify: `src/shared/components/OAuthModal.tsx` + +- [ ] **Step 7.1: Read modal** + +Run: +```bash +grep -n "windsurf\|devin-cli\|flowType\|authorize\|start-callback\|import-token\|showAuthTokenUrl" src/shared/components/OAuthModal.tsx | head -40 +``` + +Identify the rendering branch that picks PKCE vs import-token UI based on `provider.flowType` or provider key. + +- [ ] **Step 7.2: Add windsurf/devin-cli explicit branch** + +In the modal render, find where it switches on flow type. Add — at the top of the render-decision logic — a check: + +```typescript +// Phase 1 hotfix: PKCE flow for windsurf/devin-cli is retired (app.devin.ai 404). +// Force import-token panel + "Get your token" link to windsurf.com/show-auth-token. +const isWindsurfFamily = providerKey === "windsurf" || providerKey === "devin-cli"; + +if (isWindsurfFamily) { + return ( + + ); +} +``` + +If `ImportTokenPanel` doesn't exist as a separate component, render the equivalent inline using the same JSX structure already used for other `IMPORT_TOKEN_PROVIDERS` (claude alternate-flow, e.g.). The key UX requirements: +1. No "Sign in with browser" button visible +2. Single textarea/input for token paste +3. A button labeled "Get your Windsurf API token" that calls `window.open("https://windsurf.com/show-auth-token", "_blank", "noopener,noreferrer")` +4. Submit button validates non-empty + calls existing `/api/oauth/windsurf/import-token` POST + +- [ ] **Step 7.3: Manual smoke (developer-side)** + +Run: +```bash +npm run dev & +sleep 8 +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20128/api/oauth/windsurf/start-callback-server +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:20128/api/oauth/devin-cli/authorize +kill %1 +``` +Expected: both `410`. + +- [ ] **Step 7.4: Lint + typecheck** + +Run: +```bash +npm run lint 2>&1 | tail -5 +npm run typecheck:core 2>&1 | tail -5 +``` +Expected: 0 errors. Pre-existing warnings OK. + +- [ ] **Step 7.5: Commit** + +Run: +```bash +git add src/shared/components/OAuthModal.tsx +git commit -m "fix(dashboard): force import-token panel for windsurf/devin-cli + +PKCE 'Sign in with browser' button is hidden for these providers. +Single 'Get your Windsurf API token' link opens windsurf.com/show-auth-token, +user pastes the returned token into the form." +``` + +--- + +## Task 8: i18n — update Windsurf login guide steps + +**Files:** +- Modify: i18n keys for windsurf onboarding guide (39 languages) + +- [ ] **Step 8.1: Locate i18n keys** + +Run: +```bash +grep -rln "windsurf" src/locales/ public/locales/ docs/i18n/ 2>/dev/null | head -10 +grep -rln "show-auth-token\|app.devin.ai\|editor/signin" . --include="*.json" --include="*.ts" 2>/dev/null | grep -v node_modules | grep -v ".next" | head -10 +``` +Note the i18n root directory — call it ``. + +- [ ] **Step 8.2: Find the Windsurf step key** + +Run: +```bash +grep -rln "docs.windsurf.steps\|windsurfGuideSteps\|windsurf_steps\|Sign in to Windsurf" +``` +Identify the canonical key (e.g. `docs.windsurf.steps` or `onboarding.windsurf.steps`). + +- [ ] **Step 8.3: Update English (`en.json` or equivalent) first** + +Replace any string mentioning "Sign in", "browser login", or `app.devin.ai` with the import-token equivalent. Target string (rewrite per file's existing key shape): + +``` +1. Open https://windsurf.com/show-auth-token in your browser +2. Sign in to your Windsurf account if prompted +3. Copy the API token displayed on the page +4. Paste it into the OmniRoute Windsurf connection form +``` + +- [ ] **Step 8.4: Sync the other 38 languages** + +If the repo has a sync script, run it: + +```bash +npm run docs:i18n-sync 2>&1 | tail -10 +# or +npm run check:docs-all 2>&1 | tail -20 +``` + +If no script: replace the same key in every locale file with the English text wrapped in a `// TODO translate` marker, OR leave language-specific versions untouched if they already don't mention `app.devin.ai`. The CI gate (`check-docs-sync` in pre-commit) will tell you which files are out of sync. + +- [ ] **Step 8.5: Run docs gate** + +Run: +```bash +npm run check:docs-all 2>&1 | tail -20 +``` +Expected: PASS or only pre-existing warnings. + +- [ ] **Step 8.6: Commit** + +Run: +```bash +git add +git commit -m "docs(i18n): update Windsurf onboarding to import-token flow + +Replace 'Sign in via browser' steps with the windsurf.com/show-auth-token +copy-paste flow across all locales." +``` + +--- + +## Task 9: Full verification + +- [ ] **Step 9.1: Full unit suite** + +Run: +```bash +npm run test:unit 2>&1 | tail -20 +``` +Expected: green. + +- [ ] **Step 9.2: Coverage gate** + +Run: +```bash +npm run test:coverage 2>&1 | tail -20 +``` +Expected: 75/75/75/70 thresholds met. + +- [ ] **Step 9.3: Lint + typecheck** + +Run: +```bash +npm run lint 2>&1 | tail -5 +npm run typecheck:core 2>&1 | tail -5 +npm run typecheck:noimplicit:core 2>&1 | tail -5 +``` +Expected: 0 errors. + +- [ ] **Step 9.4: Combined check** + +Run: +```bash +npm run check 2>&1 | tail -10 +``` +Expected: green. + +- [ ] **Step 9.5: Manual smoke — local server** + +Run: +```bash +npm run dev & +sleep 10 +# 1. Disabled PKCE actions return 410 +curl -s -o /dev/null -w "GET /windsurf/authorize: %{http_code}\n" \ + http://localhost:20128/api/oauth/windsurf/authorize +curl -s -o /dev/null -w "GET /windsurf/start-callback-server: %{http_code}\n" \ + http://localhost:20128/api/oauth/windsurf/start-callback-server +curl -s -o /dev/null -w "GET /devin-cli/authorize: %{http_code}\n" \ + http://localhost:20128/api/oauth/devin-cli/authorize +# 2. Codex still works (regression check) +curl -s -o /dev/null -w "GET /codex/authorize: %{http_code}\n" \ + http://localhost:20128/api/oauth/codex/authorize +kill %1 +``` +Expected: windsurf/devin-cli all `410`. Codex `200` (or whatever it returned before — must be unchanged). + +- [ ] **Step 9.6: Manual smoke — paste valid token** + +Manual steps in browser at `http://localhost:20128/dashboard/providers`: +1. Click "Connect" on Windsurf provider +2. Verify modal does NOT show "Sign in with browser" +3. Verify "Get your Windsurf API token" link is present +4. Click link → opens new tab to `windsurf.com/show-auth-token` (or warns if not logged in) +5. Paste a valid token, submit +6. Connection saves, status shows "Active" +7. Open chat playground, send 1 request to `swe-1`, verify a response is returned + +If you do not have a valid Windsurf token, document this in the PR as "blocked on tester with credentials" and note that automated tests cover the negative path. + +--- + +## Task 10: Push branch + open PR + +- [ ] **Step 10.1: Verify clean tree** + +Run: +```bash +git status --short +``` +Expected: empty (everything committed). + +- [ ] **Step 10.2: Push branch** + +Run: +```bash +git push -u origin fix/windsurf-login-2026-05-29 +``` + +- [ ] **Step 10.3: Open PR** + +Run: +```bash +gh pr create \ + --title "fix(oauth): hotfix Windsurf login — drop dead PKCE flow, promote import-token" \ + --body "$(cat <<'EOF' +## Summary + +The Windsurf provider's PKCE OAuth URL (`https://app.devin.ai/editor/signin`) returns +404 post-Cognition rebrand, leaving users unable to log in. This PR retires the dead +flow and makes import-token (from `https://windsurf.com/show-auth-token`) the only +supported login path. Phase 2 (Firebase OAuth + RegisterUser, ported from +`fendoushaonian/WindSurf-gRPC-API`) will follow in a separate PR. + +Spec: \`docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md\`. + +## Changes + +- \`src/lib/oauth/providers/windsurf.ts\` — drop \`buildAuthUrl\`, set \`flowType: "import_token"\` +- \`src/app/api/oauth/[provider]/[action]/route.ts\` — return 410 Gone for retired actions (\`authorize\`, \`start-callback-server\`, \`poll-callback\`) on \`windsurf\` & \`devin-cli\`. Codex unchanged. +- \`src/lib/oauth/constants/oauth.ts\` — annotate retired PKCE fields, no behaviour change +- \`src/shared/components/OAuthModal.tsx\` — hide "Sign in with browser" button for windsurf/devin-cli, show import-token panel + \`windsurf.com/show-auth-token\` link +- i18n — update onboarding steps in all locales +- Tests — new assertions covering disabled flowType, 410 responses, import-token still functional + +No DB migration. Existing connections (\`accessToken\` already saved) continue working. + +## Test plan + +- [x] \`npm run test:unit\` — green +- [x] \`npm run test:coverage\` — 75/75/75/70 met +- [x] \`npm run lint\` + \`typecheck:core\` — 0 errors +- [x] \`curl GET /api/oauth/windsurf/authorize\` → 410 +- [x] \`curl GET /api/oauth/devin-cli/start-callback-server\` → 410 +- [x] \`curl GET /api/oauth/codex/authorize\` → unchanged +- [ ] Manual: paste valid token, send 1 chat request to \`swe-1\` (blocked on tester with credentials) + +## Rollback + +Revert this single PR. No schema changes; existing tokens unaffected. +EOF +)" \ + --base main \ + --head fix/windsurf-login-2026-05-29 +``` + +- [ ] **Step 10.4: Confirm CI starts** + +Run: +```bash +gh pr checks 2>&1 | tail -10 +``` +Expected: workflows queued. If failing, address in follow-up commits on the same branch. + +--- + +## Self-Review Notes + +- **Spec coverage**: All Phase 1 items in `docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md` mapped to tasks 1-8. Phase 2 explicitly out of scope. +- **Type consistency**: `flowType: "import_token"` used consistently. `RETIRED_PKCE_PROVIDERS` & `PKCE_CALLBACK_PROVIDERS` kept distinct. +- **Placeholder scan**: Task 8 has one `` placeholder that the engineer resolves via Step 8.1 grep — necessary because i18n location varies. Task 3 has `` — resolved via Step 3.1 grep. +- **Frequent commits**: 6 commits across the implementation (Tasks 3, 5, 6, 7, 8 each commit; Task 1+2 share commit at Step 3.5). +- **TDD**: Tasks 1, 4 write tests first; Tasks 2, 5 make them pass.