mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(oauth): return 410 Gone for retired windsurf/devin-cli PKCE actions
start-callback-server, authorize, and poll-callback (GET + POST) now return 410 Gone with a pointer to /import-token. The 410 short-circuit runs before auth so the response is honest about the action being permanently gone, not gated. Codex PKCE flow unchanged. Tests: 5 new assertions cover GET + POST 410 paths and a Codex regression check.
This commit is contained in:
@@ -39,7 +39,17 @@ if (!globalThis.__windsurfCallbackState) {
|
||||
}
|
||||
|
||||
/** Providers that use the PKCE browser callback flow (like Codex). */
|
||||
const PKCE_CALLBACK_PROVIDERS = new Set(["codex", "windsurf", "devin-cli"]);
|
||||
const PKCE_CALLBACK_PROVIDERS = new Set(["codex"]);
|
||||
|
||||
/**
|
||||
* Providers whose PKCE flow has been retired but whose import-token path is
|
||||
* still active. Returning 410 Gone on `authorize` / `start-callback-server` /
|
||||
* `poll-callback` (instead of 400) tells callers the action is permanently
|
||||
* gone and points them at /import-token. windsurf/devin-cli were retired
|
||||
* 2026-05-29 because app.devin.ai/editor/signin returned 404 post-rebrand.
|
||||
* Phase 2 will reintroduce browser login via Firebase OAuth + RegisterUser.
|
||||
*/
|
||||
const RETIRED_PKCE_PROVIDERS = new Set(["windsurf", "devin-cli"]);
|
||||
|
||||
/** Providers that allow direct import of a raw API token (no OAuth exchange). */
|
||||
const IMPORT_TOKEN_PROVIDERS = new Set(["windsurf", "devin-cli"]);
|
||||
@@ -73,6 +83,33 @@ export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ provider: string; action: string }> }
|
||||
) {
|
||||
// Phase 1 hotfix (2026-05-29): retired PKCE flows return 410 Gone BEFORE auth.
|
||||
// The action permanently does not exist for these providers regardless of who
|
||||
// is asking — answering 401 first would mislead callers into thinking the
|
||||
// route is gated rather than gone. See spec
|
||||
// docs/superpowers/specs/2026-05-29-windsurf-login-fix-design.md.
|
||||
try {
|
||||
const earlyParams = await params;
|
||||
if (
|
||||
RETIRED_PKCE_PROVIDERS.has(earlyParams.provider) &&
|
||||
(earlyParams.action === "authorize" ||
|
||||
earlyParams.action === "start-callback-server" ||
|
||||
earlyParams.action === "poll-callback")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
`Browser OAuth disabled for ${earlyParams.provider} — use import-token via ` +
|
||||
`/api/oauth/${earlyParams.provider}/import-token. ` +
|
||||
`Visit https://windsurf.com/show-auth-token to obtain a token.`,
|
||||
},
|
||||
{ status: 410 }
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
/* fall through to normal handling */
|
||||
}
|
||||
|
||||
const authResponse = await requireOAuthRouteAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
@@ -242,11 +279,50 @@ export async function POST(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ provider: string; action: string }> }
|
||||
) {
|
||||
// Phase 1 hotfix (2026-05-29): retired PKCE flows return 410 Gone BEFORE auth.
|
||||
// See GET handler comment.
|
||||
try {
|
||||
const earlyParams = await params;
|
||||
if (
|
||||
RETIRED_PKCE_PROVIDERS.has(earlyParams.provider) &&
|
||||
earlyParams.action === "poll-callback"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
`Browser OAuth disabled for ${earlyParams.provider} — use import-token via ` +
|
||||
`/api/oauth/${earlyParams.provider}/import-token. ` +
|
||||
`Visit https://windsurf.com/show-auth-token to obtain a token.`,
|
||||
},
|
||||
{ status: 410 }
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
/* fall through to normal handling */
|
||||
}
|
||||
|
||||
const authResponse = await requireOAuthRouteAuth(request);
|
||||
if (authResponse) return authResponse;
|
||||
|
||||
try {
|
||||
const { provider, action } = await params;
|
||||
|
||||
// Phase 1 hotfix (2026-05-29): retired PKCE flows return 410 Gone before
|
||||
// body parsing. windsurf/devin-cli `poll-callback` is permanently retired
|
||||
// because the upstream PKCE endpoint returns 404. Use /import-token
|
||||
// (handled later in this same handler) for those providers instead.
|
||||
if (RETIRED_PKCE_PROVIDERS.has(provider) && action === "poll-callback") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
`Browser OAuth disabled for ${provider} — use import-token via ` +
|
||||
`/api/oauth/${provider}/import-token. ` +
|
||||
`Visit https://windsurf.com/show-auth-token to obtain a token.`,
|
||||
},
|
||||
{ status: 410 }
|
||||
);
|
||||
}
|
||||
|
||||
let rawBody: any = {};
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
|
||||
@@ -260,3 +260,59 @@ test("devin-cli provider: generateAuthData returns no authUrl", () => {
|
||||
assert.equal(data.authUrl, undefined);
|
||||
assert.equal(data.supported, false);
|
||||
});
|
||||
|
||||
// ─── Phase 1 hotfix: retired PKCE actions return 410 Gone ────────────────────
|
||||
import { GET as oauthGet, POST as oauthPost } from "@/app/api/oauth/[provider]/[action]/route";
|
||||
|
||||
test("OAuth route: GET 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|show-auth-token/i);
|
||||
});
|
||||
|
||||
test("OAuth route: GET 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);
|
||||
const body = await response.json();
|
||||
assert.match(body.error, /import-token|disabled|410|show-auth-token/i);
|
||||
});
|
||||
|
||||
test("OAuth route: GET windsurf/poll-callback returns 410 Gone", async () => {
|
||||
const url = "http://localhost:20128/api/oauth/windsurf/poll-callback";
|
||||
const request = new Request(url, { method: "GET" });
|
||||
const response = await oauthGet(request, {
|
||||
params: Promise.resolve({ provider: "windsurf", action: "poll-callback" }),
|
||||
} as never);
|
||||
assert.equal(response.status, 410);
|
||||
});
|
||||
|
||||
test("OAuth route: POST windsurf/poll-callback returns 410 Gone", async () => {
|
||||
const url = "http://localhost:20128/api/oauth/windsurf/poll-callback";
|
||||
const request = new Request(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await oauthPost(request, {
|
||||
params: Promise.resolve({ provider: "windsurf", action: "poll-callback" }),
|
||||
} as never);
|
||||
assert.equal(response.status, 410);
|
||||
});
|
||||
|
||||
test("OAuth route: GET codex/authorize is NOT retired (regression check)", async () => {
|
||||
const url = "http://localhost:20128/api/oauth/codex/authorize";
|
||||
const request = new Request(url, { method: "GET" });
|
||||
const response = await oauthGet(request, {
|
||||
params: Promise.resolve({ provider: "codex", action: "authorize" }),
|
||||
} as never);
|
||||
assert.notEqual(response.status, 410);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user