Update readme and tests

This commit is contained in:
gsxdsm
2026-07-04 18:58:48 -07:00
parent efd085cc77
commit a7559b0721
2 changed files with 68 additions and 1 deletions

View File

@@ -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.

View File

@@ -251,6 +251,66 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
return providerId === "github-copilot"; 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( function selectOauthOption(
providerId: string, providerId: string,
prompt: { options: Array<{ id: string; label?: string }> }, prompt: { options: Array<{ id: string; label?: string }> },
@@ -1158,7 +1218,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
} }
activeLogin.inputSubmitted = true; activeLogin.inputSubmitted = true;
activeLogin.resolveInput(code.trim()); activeLogin.resolveInput(normalizeManualOAuthInputForProvider(toOauthLoginProviderId(provider), code));
res.json({ success: true, submitted: true }); res.json({ success: true, submitted: true });
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof ApiError) { if (err instanceof ApiError) {