diff --git a/.changeset/fix-anthropic-subscription-fragment-login.md b/.changeset/fix-anthropic-subscription-fragment-login.md new file mode 100644 index 0000000000..85d2c35fc6 --- /dev/null +++ b/.changeset/fix-anthropic-subscription-fragment-login.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. +category: fix +dev: Normalizes pasted OAuth callback fragments before resolving dashboard manual-code login prompts. diff --git a/packages/dashboard/src/routes/register-auth-routes.ts b/packages/dashboard/src/routes/register-auth-routes.ts index 5117fc3e71..7e7b10f924 100644 --- a/packages/dashboard/src/routes/register-auth-routes.ts +++ b/packages/dashboard/src/routes/register-auth-routes.ts @@ -251,6 +251,66 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { return providerId === "github-copilot"; } + function normalizeManualOAuthInputForProvider(providerId: string, input: string): string { + if (providerId !== "anthropic" && providerId !== "openai-codex") { + return input.trim(); + } + + const trimmed = input.trim(); + if (!trimmed) { + return trimmed; + } + + try { + const url = new URL(trimmed); + const searchCode = url.searchParams.get("code"); + if (searchCode) { + return trimmed; + } + + const hash = url.hash.replace(/^#/, "").replace(/^\?/, ""); + if (!hash) { + return trimmed; + } + const hashParams = new URLSearchParams(hash); + const hashCode = hashParams.get("code"); + if (!hashCode) { + return trimmed; + } + + /* + FNXC:ProviderAuth 2026-07-04-00:00: + Anthropic subscription and Codex pasted-login flows must accept the exact browser address bar after redirect, including providers/browsers that place OAuth `code` and `state` in the URL fragment. + The upstream CLI parser treats a syntactically valid URL as search-only, so normalize fragment callbacks to query-param text before resolving the pending manual-code prompt. + */ + const normalized = new URLSearchParams(); + normalized.set("code", hashCode); + const hashState = hashParams.get("state"); + if (hashState) { + normalized.set("state", hashState); + } + return normalized.toString(); + } catch { + const queryStart = trimmed.indexOf("?"); + if (queryStart >= 0) { + const queryEnd = trimmed.indexOf("#", queryStart); + const query = trimmed.slice(queryStart + 1, queryEnd >= 0 ? queryEnd : undefined); + const params = new URLSearchParams(query); + const code = params.get("code"); + if (code) { + const normalized = new URLSearchParams(); + normalized.set("code", code); + const state = params.get("state"); + if (state) { + normalized.set("state", state); + } + return normalized.toString(); + } + } + return trimmed; + } + } + function selectOauthOption( providerId: string, prompt: { options: Array<{ id: string; label?: string }> }, @@ -1158,7 +1218,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => { } activeLogin.inputSubmitted = true; - activeLogin.resolveInput(code.trim()); + activeLogin.resolveInput(normalizeManualOAuthInputForProvider(toOauthLoginProviderId(provider), code)); res.json({ success: true, submitted: true }); } catch (err: unknown) { if (err instanceof ApiError) {