diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index 22885a43d2..9a23e5ff11 100755 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -755,79 +755,6 @@ export async function POST( } } - if (action === "import-token") { - const { token, connectionId } = body; - - if (!IMPORT_TOKEN_PROVIDERS.has(provider)) { - return NextResponse.json( - { - error: `import-token not supported for provider: ${provider}. Supported: ${[...IMPORT_TOKEN_PROVIDERS].join(", ")}`, - }, - { status: 400 } - ); - } - - try { - // Map the raw token via the provider's mapTokens() — skips the HTTP exchange entirely. - const providerData = getProvider(provider); - const tokenData = providerData.mapTokens({ accessToken: token }); - - // Normalize: if name is missing, use email as fallback display label - if (!tokenData.name && (tokenData.email || tokenData.displayName)) { - tokenData.name = tokenData.email || tokenData.displayName; - } - - const expiresAt = tokenData.expiresIn - ? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString() - : null; - - let connection: any; - if (tokenData.email) { - const existing = await getProviderConnections({ provider }); - const match = existing.find((c: any) => { - if (c.id && safeEqual(connectionId, c.id)) return true; - if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false; - return true; - }); - const matchId = typeof match?.id === "string" ? match.id : null; - if (matchId) { - connection = await updateProviderConnection(matchId, { - ...tokenData, - expiresAt, - testStatus: "active", - isActive: true, - }); - } - } - if (!connection) { - connection = await createProviderConnection({ - provider, - authType: "oauth", - ...tokenData, - expiresAt, - testStatus: "active", - }); - } - - await syncToCloudIfEnabled(); - - return NextResponse.json({ - success: true, - connection: { - id: connection.id, - provider: connection.provider, - email: connection.email, - displayName: connection.displayName, - }, - }); - } catch (importErr: any) { - return NextResponse.json( - { success: false, error: sanitizeErrorMessage(importErr.message) || "Import failed" }, - { status: 500 } - ); - } - } - return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (error) { console.error("OAuth POST error:", error); diff --git a/src/lib/oauth/providers/windsurf.ts b/src/lib/oauth/providers/windsurf.ts index b334bc095f..0328d9764a 100644 --- a/src/lib/oauth/providers/windsurf.ts +++ b/src/lib/oauth/providers/windsurf.ts @@ -39,12 +39,20 @@ export const windsurf = { /** * Map a pasted import token onto the connection record. The token IS the * Windsurf API key; there is no exchange step. + * + * NOTE: callers in `src/app/api/oauth/[provider]/[action]/route.ts` invoke + * this with `{ accessToken: token }` (object), matching the cursor/kiro + * signature. The earlier signature was `mapTokens(token: string)`, which + * caused the route to spread `{ accessToken: { accessToken: "sk-..." } }` + * into the DB layer and crashed the SQLite bind step: + * `SQLite3 can only bind numbers, strings, bigints, buffers, and null`. + * Keep the object signature. */ - mapTokens(token: string) { + mapTokens(tokens: { accessToken: string }) { return { - accessToken: token, + accessToken: tokens.accessToken, refreshToken: null, - expiresAt: null, + expiresIn: null as number | null, }; }, }; diff --git a/tests/unit/windsurf-devin-executors.test.ts b/tests/unit/windsurf-devin-executors.test.ts index 339a5bac9b..9d89c45679 100644 --- a/tests/unit/windsurf-devin-executors.test.ts +++ b/tests/unit/windsurf-devin-executors.test.ts @@ -316,3 +316,23 @@ test("OAuth route: GET codex/authorize is NOT retired (regression check)", async } as never); assert.notEqual(response.status, 410); }); + +// ─── Regression: mapTokens accepts {accessToken} object, returns string accessToken ─ +// Earlier signature was `mapTokens(token: string)` which crashed the SQLite +// bind layer when the route called `mapTokens({ accessToken })`: the object +// got stored as accessToken and SQLite rejected it with +// "SQLite3 can only bind numbers, strings, bigints, buffers, and null". +test("windsurf mapTokens: accepts object {accessToken} and returns string accessToken", () => { + const provider = getProvider("windsurf"); + const mapped = provider.mapTokens({ accessToken: "sk-ws-test-token-1234567890" }); + assert.equal(typeof mapped.accessToken, "string"); + assert.equal(mapped.accessToken, "sk-ws-test-token-1234567890"); + assert.equal(mapped.refreshToken, null); +}); + +test("devin-cli mapTokens: accepts object {accessToken} and returns string accessToken", () => { + const provider = getProvider("devin-cli"); + const mapped = provider.mapTokens({ accessToken: "sk-devin-test-token-1234567890" }); + assert.equal(typeof mapped.accessToken, "string"); + assert.equal(mapped.accessToken, "sk-devin-test-token-1234567890"); +});