mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(oauth): windsurf import-token mapTokens signature mismatch
The route at `src/app/api/oauth/[provider]/[action]/route.ts` invokes
`providerData.mapTokens({ accessToken: token })` (object), matching the
cursor/kiro signature. The windsurf provider was declared with
`mapTokens(token: string)` instead, so the entire object was stored as
`accessToken`. When the connection record reached the SQLite layer it
crashed with:
SQLite3 can only bind numbers, strings, bigints, buffers, and null
Fix by aligning windsurf's `mapTokens` signature with the route caller
and the cursor/kiro convention. Also dedupe a copy-pasted second
`if (action === "import-token")` block in the route handler — the
second block was unreachable but identical to the first.
Adds two regression tests asserting that
`provider.mapTokens({ accessToken })` returns a string `accessToken` for
both windsurf and devin-cli, so a future signature drift trips the gate
instead of the SQLite bind error in production.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user