FN-7292: bridge Anthropic subscription auth into runtime requests

Anthropic model requests now resolve subscription OAuth credentials when no raw API key is present.

- Route Anthropic runtime auth through raw API keys, legacy OAuth, subscription OAuth, models.json, and fallback resolver sources with explicit logout precedence.
- Refresh subscription OAuth using the Anthropic OAuth provider while persisting rotated tokens under the subscription storage id.
- Cover subscription alias resolution, logout behavior, fallback visibility, refresh persistence, and reload behavior with auth-storage tests.
- Add a patch changeset for the published Fusion CLI package.

Files changed:
 .../fn-7292-anthropic-subscription-runtime-auth.md |   7 +
 packages/engine/src/__tests__/auth-storage.test.ts | 357 +++++++++++++++++++++
 packages/engine/src/auth-storage.ts                | 266 ++++++++++++---
 3 files changed, 577 insertions(+), 53 deletions(-)

Fusion-Task-Id: FN-7292

Fusion-Task-Lineage: c98c2f1c-8297-4473-ba66-5b6320bc0c00

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 12:09:09 -07:00
parent 70d7dcaa6c
commit 9460497dd5
3 changed files with 577 additions and 53 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Let Anthropic subscription login power Anthropic model requests without a raw API key.
category: fix
dev: Bridges runtime provider `anthropic` to OAuth credentials stored under `anthropic-subscription` while preserving raw API-key precedence.

View File

@@ -17,6 +17,12 @@ function createJwt(payload: Record<string, unknown>): string {
].join(".");
}
function writeFusionAuth(homeDir: string, credentials: Record<string, unknown>): void {
const fusionAgentDir = join(homeDir, ".fusion", "agent");
mkdirSync(fusionAgentDir, { recursive: true });
writeFileSync(getFusionAuthPath(homeDir), JSON.stringify(credentials));
}
describe("createFusionAuthStorage", () => {
// HOME override required — createFusionAuthStorage() has no dir parameter
const originalHome = process.env.HOME;
@@ -168,6 +174,357 @@ describe("createFusionAuthStorage", () => {
});
});
describe("Anthropic subscription runtime auth alias", () => {
it("returns undefined for Anthropic model runtime auth when no credential exists", async () => {
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
expect(authStorage.hasAuth("anthropic")).toBe(false);
});
it("preserves Anthropic fallback resolver auth when no stored credential exists", async () => {
const authStorage = createFusionAuthStorage();
(authStorage as unknown as { setFallbackResolver(resolver: (provider: string) => string | undefined): void })
.setFallbackResolver((provider) => (provider === "anthropic" ? "fallback-anthropic-runtime-key" : undefined));
expect(await authStorage.getApiKey("anthropic")).toBe("fallback-anthropic-runtime-key");
expect(authStorage.hasAuth("anthropic")).toBe(true);
});
it("suppresses Anthropic fallback resolver auth after raw provider logout", async () => {
const authStorage = createFusionAuthStorage();
(authStorage as unknown as { setFallbackResolver(resolver: (provider: string) => string | undefined): void })
.setFallbackResolver((provider) => (provider === "anthropic" ? "fallback-anthropic-runtime-key" : undefined));
expect(await authStorage.getApiKey("anthropic")).toBe("fallback-anthropic-runtime-key");
authStorage.logout("anthropic");
expect(authStorage.hasAuth("anthropic")).toBe(false);
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
});
it("uses raw Anthropic API-key credentials for model runtime auth", async () => {
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("sk-ant-api03-runtime-key");
expect(authStorage.get("anthropic")).toEqual({ type: "api_key", key: "sk-ant-api03-runtime-key" });
expect(authStorage.hasAuth("anthropic")).toBe(true);
});
it("uses Anthropic subscription OAuth for model runtime auth when no raw API key exists", async () => {
writeFusionAuth(homeDir, {
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("subscription-access-token");
expect(authStorage.hasAuth("anthropic")).toBe(true);
expect(authStorage.list()).toEqual(expect.arrayContaining(["anthropic", "anthropic-subscription"]));
});
it("keeps raw Anthropic API-key precedence when subscription OAuth also exists", async () => {
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("sk-ant-api03-runtime-key");
expect(await authStorage.getApiKey("anthropic-subscription")).toBe("subscription-access-token");
});
it("keeps raw Anthropic API-key precedence over legacy OAuth hydration", async () => {
const legacyAgentDir = join(homeDir, ".pi", "agent");
mkdirSync(legacyAgentDir, { recursive: true });
writeFileSync(
join(legacyAgentDir, "auth.json"),
JSON.stringify({
anthropic: {
type: "oauth",
access: "legacy-subscription-access-token",
refresh: "legacy-subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
}),
);
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("sk-ant-api03-runtime-key");
});
it("refreshes expired Anthropic subscription OAuth and persists it under the subscription storage id", async () => {
writeFusionAuth(homeDir, {
"anthropic-subscription": {
type: "oauth",
access: "expired-subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() - 60_000,
scopes: ["user:profile", "org:create_api_key"],
},
});
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
access_token: "refreshed-subscription-access-token",
refresh_token: "rotated-subscription-refresh-token",
expires_in: 3600,
scope: "user:profile org:create_api_key",
}),
} as Response);
globalThis.fetch = fetchMock as typeof fetch;
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("refreshed-subscription-access-token");
expect(fetchMock).toHaveBeenCalledWith(
"https://platform.claude.com/v1/oauth/token",
expect.objectContaining({
method: "POST",
body: expect.stringContaining("\"scope\":\"user:profile org:create_api_key\""),
}),
);
expect(authStorage.get("anthropic-subscription")).toEqual({
type: "oauth",
access: "refreshed-subscription-access-token",
refresh: "rotated-subscription-refresh-token",
expires: expect.any(Number),
scopes: ["user:profile", "org:create_api_key"],
});
expect(authStorage.get("anthropic")).toBeUndefined();
const persisted = JSON.parse(readFileSync(getFusionAuthPath(homeDir), "utf-8"));
expect(persisted["anthropic-subscription"]).toEqual({
type: "oauth",
access: "refreshed-subscription-access-token",
refresh: "rotated-subscription-refresh-token",
expires: expect.any(Number),
scopes: ["user:profile", "org:create_api_key"],
});
expect(persisted.anthropic).toBeUndefined();
});
it("does not resurrect stale Anthropic subscription OAuth after failed refresh", async () => {
writeFusionAuth(homeDir, {
"anthropic-subscription": {
type: "oauth",
access: "expired-subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() - 60_000,
},
});
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false } as Response) as typeof fetch;
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
const persisted = JSON.parse(readFileSync(getFusionAuthPath(homeDir), "utf-8"));
expect(persisted["anthropic-subscription"]).toEqual({
type: "oauth",
access: "expired-subscription-access-token",
refresh: "subscription-refresh-token",
expires: expect.any(Number),
});
});
it("keeps subscription OAuth available for runtime auth after raw Anthropic logout", async () => {
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
authStorage.logout("anthropic");
expect(authStorage.get("anthropic")).toBeUndefined();
expect(authStorage.has("anthropic")).toBe(true);
expect(authStorage.hasAuth("anthropic")).toBe(true);
expect(authStorage.list()).toEqual(expect.arrayContaining(["anthropic", "anthropic-subscription"]));
expect(await authStorage.getApiKey("anthropic")).toBe("subscription-access-token");
});
it("uses a newly set subscription credential for runtime auth after raw Anthropic logout", async () => {
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
});
const authStorage = createFusionAuthStorage();
authStorage.logout("anthropic");
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
authStorage.set("anthropic-subscription", {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
});
expect(authStorage.get("anthropic")).toBeUndefined();
expect(await authStorage.getApiKey("anthropic")).toBe("subscription-access-token");
});
it("keeps raw Anthropic API keys visible when subscription logout suppresses OAuth aliases", async () => {
writeFusionAuth(homeDir, {
anthropic: { type: "api_key", key: "sk-ant-api03-runtime-key" },
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
authStorage.logout("anthropic-subscription");
expect(authStorage.get("anthropic-subscription")).toBeUndefined();
expect(authStorage.get("anthropic")).toEqual({ type: "api_key", key: "sk-ant-api03-runtime-key" });
expect(await authStorage.getApiKey("anthropic")).toBe("sk-ant-api03-runtime-key");
});
it("suppresses legacy Anthropic OAuth after subscription logout", async () => {
writeFusionAuth(homeDir, {
anthropic: {
type: "oauth",
access: "legacy-subscription-access-token",
refresh: "legacy-subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBe("legacy-subscription-access-token");
authStorage.logout("anthropic-subscription");
expect(authStorage.get("anthropic")).toBeUndefined();
expect(authStorage.has("anthropic")).toBe(false);
expect(authStorage.hasAuth("anthropic")).toBe(false);
expect(authStorage.list()).not.toContain("anthropic");
expect(authStorage.getAll()).not.toHaveProperty("anthropic");
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
});
it("suppresses supplemental legacy Anthropic OAuth status after subscription logout", async () => {
const legacyAgentDir = join(homeDir, ".pi", "agent");
mkdirSync(legacyAgentDir, { recursive: true });
writeFileSync(
join(legacyAgentDir, "auth.json"),
JSON.stringify({
anthropic: {
type: "oauth",
access: "legacy-subscription-access-token",
refresh: "legacy-subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
}),
);
const authStorage = createFusionAuthStorage();
expect(authStorage.hasAuth("anthropic")).toBe(true);
expect(authStorage.list()).toContain("anthropic");
authStorage.logout("anthropic-subscription");
expect(authStorage.get("anthropic")).toBeUndefined();
expect(authStorage.has("anthropic")).toBe(false);
expect(authStorage.hasAuth("anthropic")).toBe(false);
expect(authStorage.list()).not.toContain("anthropic");
expect(authStorage.getAll()).not.toHaveProperty("anthropic");
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
});
it("keeps models.json Anthropic fallback visible after subscription logout", async () => {
const fusionAgentDir = join(homeDir, ".fusion", "agent");
mkdirSync(fusionAgentDir, { recursive: true });
writeFileSync(
join(fusionAgentDir, "models.json"),
JSON.stringify({ providers: { anthropic: { apiKey: "models-runtime-key" } } }),
);
writeFusionAuth(homeDir, {
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
const authStorage = createFusionAuthStorage();
authStorage.logout("anthropic-subscription");
expect(authStorage.has("anthropic")).toBe(true);
expect(authStorage.hasAuth("anthropic")).toBe(true);
expect(authStorage.list()).toContain("anthropic");
expect(authStorage.get("anthropic")).toBeUndefined();
expect(await authStorage.getApiKey("anthropic")).toBe("models-runtime-key");
});
it("suppresses models.json Anthropic fallback after raw provider logout", async () => {
const fusionAgentDir = join(homeDir, ".fusion", "agent");
mkdirSync(fusionAgentDir, { recursive: true });
writeFileSync(
join(fusionAgentDir, "models.json"),
JSON.stringify({ providers: { anthropic: { apiKey: "models-runtime-key" } } }),
);
const authStorage = createFusionAuthStorage();
expect(authStorage.has("anthropic")).toBe(true);
expect(await authStorage.getApiKey("anthropic")).toBe("models-runtime-key");
authStorage.logout("anthropic");
expect(authStorage.has("anthropic")).toBe(false);
expect(authStorage.hasAuth("anthropic")).toBe(false);
expect(authStorage.list()).not.toContain("anthropic");
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
});
it("reloads Anthropic subscription OAuth alias state for model runtime auth", async () => {
writeFusionAuth(homeDir, {});
const authStorage = createFusionAuthStorage();
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
writeFusionAuth(homeDir, {
"anthropic-subscription": {
type: "oauth",
access: "subscription-access-token",
refresh: "subscription-refresh-token",
expires: Date.now() + 3_600_000,
},
});
authStorage.reload();
expect(await authStorage.getApiKey("anthropic")).toBe("subscription-access-token");
});
});
it("refreshes and persists expired Claude OAuth credentials from Claude credential files", async () => {
const claudeDir = join(homeDir, ".claude");
mkdirSync(claudeDir, { recursive: true });

View File

@@ -17,6 +17,8 @@ import type { OAuthCredentials } from "@earendil-works/pi-ai/oauth";
type StoredCredential = StoredAuthCredential;
const OAUTH_REFRESH_BUFFER_MS = 60_000;
const ANTHROPIC_PROVIDER_ID = "anthropic";
const ANTHROPIC_SUBSCRIPTION_PROVIDER_ID = "anthropic-subscription";
const ANTHROPIC_TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
const ANTHROPIC_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
const ANTHROPIC_DEFAULT_SCOPES = ["user:profile"];
@@ -101,6 +103,10 @@ function resolveStoredApiKey(key: string | undefined): string | undefined {
return process.env[key] ?? key;
}
function getOAuthResolutionProviderId(providerId: string): string {
return providerId === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID ? ANTHROPIC_PROVIDER_ID : providerId;
}
function resolveOAuthApiKey(providerId: string, credential: StoredCredential): string | undefined {
if (
credential.type !== "oauth" ||
@@ -112,7 +118,7 @@ function resolveOAuthApiKey(providerId: string, credential: StoredCredential): s
return undefined;
}
return getOAuthProvider(providerId)?.getApiKey(credential as OAuthCredentials);
return getOAuthProvider(getOAuthResolutionProviderId(providerId))?.getApiKey(credential as OAuthCredentials);
}
function shouldRefreshOAuthCredential(credential: StoredCredential): boolean {
@@ -249,7 +255,7 @@ async function refreshOAuthCredential(providerId: string, credential: StoredCred
if (!shouldRefreshOAuthCredential(credential)) {
return credential;
}
if (providerId !== "anthropic") {
if (getOAuthResolutionProviderId(providerId) !== ANTHROPIC_PROVIDER_ID) {
return undefined;
}
return refreshAnthropicOAuthCredential(credential);
@@ -342,7 +348,7 @@ export function createFusionAuthStorage(): AuthStorage {
};
const refreshProviderOAuthCredential = async (
provider: string,
storageProvider: string,
credential: StoredCredential,
): Promise<StoredCredential | undefined> => {
if (!shouldRefreshOAuthCredential(credential)) {
@@ -350,37 +356,184 @@ export function createFusionAuthStorage(): AuthStorage {
}
const now = Date.now();
const cooldownUntil = oauthRefreshCooldownUntil.get(provider);
const cooldownUntil = oauthRefreshCooldownUntil.get(storageProvider);
if (cooldownUntil && cooldownUntil > now) {
return undefined;
}
const existing = oauthRefreshInFlight.get(provider);
const existing = oauthRefreshInFlight.get(storageProvider);
if (existing) {
return existing;
}
const refreshPromise = refreshOAuthCredential(provider, credential)
const refreshPromise = refreshOAuthCredential(storageProvider, credential)
.then((refreshed) => {
if (refreshed) {
oauthRefreshCooldownUntil.delete(provider);
oauthRefreshCooldownUntil.delete(storageProvider);
} else {
oauthRefreshCooldownUntil.set(provider, Date.now() + OAUTH_REFRESH_FAILURE_COOLDOWN_MS);
oauthRefreshCooldownUntil.set(storageProvider, Date.now() + OAUTH_REFRESH_FAILURE_COOLDOWN_MS);
}
return refreshed;
})
.catch(() => {
oauthRefreshCooldownUntil.set(provider, Date.now() + OAUTH_REFRESH_FAILURE_COOLDOWN_MS);
oauthRefreshCooldownUntil.set(storageProvider, Date.now() + OAUTH_REFRESH_FAILURE_COOLDOWN_MS);
return undefined;
})
.finally(() => {
oauthRefreshInFlight.delete(provider);
oauthRefreshInFlight.delete(storageProvider);
});
oauthRefreshInFlight.set(provider, refreshPromise);
oauthRefreshInFlight.set(storageProvider, refreshPromise);
return refreshPromise;
};
const selectStoredCredential = (provider: string) => choosePreferredStoredCredential(
primary.get(provider) as StoredCredential | undefined,
supplementalCredentials[provider],
);
const selectStoredCredentialByType = (
provider: string,
type: StoredCredential["type"],
) => choosePreferredStoredCredential(
((primary.get(provider) as StoredCredential | undefined)?.type === type
? primary.get(provider) as StoredCredential
: undefined),
supplementalCredentials[provider]?.type === type ? supplementalCredentials[provider] : undefined,
);
const isAnthropicSubscriptionLoggedOut = () => loggedOutProviders.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID);
const isAnthropicRawProviderLoggedOut = () => loggedOutProviders.has(ANTHROPIC_PROVIDER_ID);
const selectVisibleStoredCredential = (provider: string) => {
if (loggedOutProviders.has(provider)) {
return undefined;
}
if (provider === ANTHROPIC_PROVIDER_ID && isAnthropicSubscriptionLoggedOut()) {
return selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "api_key");
}
return selectStoredCredential(provider);
};
const resolveTargetFallbackApiKey = (provider: string): string | undefined => {
const fallbackResolver = (primary as unknown as {
fallbackResolver?: (provider: string) => string | undefined;
}).fallbackResolver;
return fallbackResolver?.(provider);
};
const hasTargetFallbackAuth = (provider: string): boolean => Boolean(resolveTargetFallbackApiKey(provider));
const hasVisibleAnthropicCredential = () => {
const hasVisibleRawAnthropicApiKey = !isAnthropicRawProviderLoggedOut()
&& (Boolean(selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "api_key"))
|| modelsJsonApiKeys.has(ANTHROPIC_PROVIDER_ID));
const hasVisibleLegacyAnthropicOAuth = !isAnthropicRawProviderLoggedOut()
&& Boolean(selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "oauth"));
const hasVisibleSubscriptionCredential = !isAnthropicSubscriptionLoggedOut()
&& (primary.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
|| ANTHROPIC_SUBSCRIPTION_PROVIDER_ID in supplementalCredentials);
const hasVisibleAnthropicFallback = !isAnthropicRawProviderLoggedOut()
&& hasTargetFallbackAuth(ANTHROPIC_PROVIDER_ID);
if (!isAnthropicSubscriptionLoggedOut()) {
/*
FNXC:ProviderAuth 2026-06-30-12:23:
Logging out of the raw Anthropic API-key provider must suppress only the raw/legacy `anthropic` storage slot.
Model-runtime reads for `anthropic` still need to see an independently logged-in `anthropic-subscription` credential from the separated subscription card.
FNXC:ProviderAuth 2026-06-30-12:47:
Anthropic's subscription alias is an extra runtime credential source, not a replacement for AuthStorage's existing fallback-resolver contract.
Keep custom fallback auth visible after explicit raw/subscription credentials are checked so ModelRegistry provider request configs still work for `anthropic` like every other provider.
*/
return hasVisibleRawAnthropicApiKey
|| hasVisibleLegacyAnthropicOAuth
|| hasVisibleSubscriptionCredential
|| hasVisibleAnthropicFallback;
}
/*
FNXC:ProviderAuth 2026-06-30-12:05:
Logging out of the Anthropic subscription must suppress legacy `anthropic` OAuth aliases from status/list reads as well as model-runtime resolution.
Keep raw API-key credentials and models.json fallback visible so the separate API-key card is not hidden by subscription logout.
*/
return hasVisibleRawAnthropicApiKey || hasVisibleAnthropicFallback;
};
const resolveRefreshableCredentialApiKey = async (
storageProvider: string,
credential: StoredCredential | undefined,
): Promise<string | undefined> => {
if (!credential) {
return undefined;
}
const refreshWasNeeded = shouldRefreshOAuthCredential(credential);
const refreshedCredential = await refreshProviderOAuthCredential(storageProvider, credential);
if (refreshedCredential?.type === "oauth" && refreshedCredential.access) {
if (refreshWasNeeded) {
/*
FNXC:ClaudeOAuth 2026-06-13-22:46:
A manual re-login or replacement credential must win over an older in-flight refresh response.
Re-check the credential identity before persisting so a delayed refresh cannot restore stale OAuth material after the user already fixed auth.
*/
const latestCredential = selectStoredCredential(storageProvider);
if (!isSameOAuthCredentialIdentity(latestCredential, credential)) {
return resolveStoredCredentialApiKey(storageProvider, latestCredential);
}
}
primary.set(storageProvider, refreshedCredential as AuthCredential);
loggedOutProviders.delete(storageProvider);
return resolveStoredCredentialApiKey(storageProvider, refreshedCredential);
}
return resolveStoredCredentialApiKey(storageProvider, credential);
};
const resolveAnthropicRuntimeApiKey = async (): Promise<string | undefined> => {
const rawProviderLoggedOut = isAnthropicRawProviderLoggedOut();
if (!rawProviderLoggedOut) {
const anthropicApiKeyCredential = selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "api_key");
if (anthropicApiKeyCredential) {
return resolveStoredCredentialApiKey(ANTHROPIC_PROVIDER_ID, anthropicApiKeyCredential);
}
}
const subscriptionLoggedOut = loggedOutProviders.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID);
const legacyAnthropicOAuthCredential = rawProviderLoggedOut
? undefined
: selectStoredCredentialByType(ANTHROPIC_PROVIDER_ID, "oauth");
if (!subscriptionLoggedOut && legacyAnthropicOAuthCredential) {
const legacyKey = await resolveRefreshableCredentialApiKey(ANTHROPIC_PROVIDER_ID, legacyAnthropicOAuthCredential);
if (legacyKey) return legacyKey;
}
if (!subscriptionLoggedOut) {
const subscriptionCredential = selectStoredCredential(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID);
if (subscriptionCredential?.type === "oauth") {
/*
FNXC:ProviderAuth 2026-06-30-11:26:
Anthropic model execution still requests provider `anthropic`, but the separated subscription login now stores OAuth material under `anthropic-subscription` so the API-key card can remain raw-key-only.
Resolve and refresh the subscription credential with the upstream Anthropic OAuth provider id while persisting rotated tokens back to `anthropic-subscription`.
*/
const subscriptionKey = await resolveRefreshableCredentialApiKey(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID, subscriptionCredential);
if (subscriptionKey) return subscriptionKey;
}
}
if (!rawProviderLoggedOut) {
/*
FNXC:ProviderAuth 2026-06-30-13:28:
Logging out of the raw Anthropic provider must suppress raw-key sources consistently across status and runtime resolution.
Treat models.json Anthropic keys and ModelRegistry fallback resolver keys as raw-key fallback material, while subscription OAuth remains governed by the separate `anthropic-subscription` logout state above.
*/
const modelsJsonApiKey = modelsJsonApiKeys.get(ANTHROPIC_PROVIDER_ID);
if (modelsJsonApiKey) return modelsJsonApiKey;
return resolveTargetFallbackApiKey(ANTHROPIC_PROVIDER_ID);
}
return undefined;
};
syncSupplementalOauthCredentials();
return new Proxy(primary, {
@@ -398,6 +551,12 @@ export function createFusionAuthStorage(): AuthStorage {
return (provider: string) => {
target.logout(provider);
loggedOutProviders.add(provider);
if (provider === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) {
const legacyAnthropicCredential = target.get(ANTHROPIC_PROVIDER_ID) as StoredCredential | undefined;
if (legacyAnthropicCredential?.type === "oauth") {
target.logout(ANTHROPIC_PROVIDER_ID);
}
}
};
}
@@ -405,6 +564,12 @@ export function createFusionAuthStorage(): AuthStorage {
return (provider: string) => {
target.remove(provider);
loggedOutProviders.add(provider);
if (provider === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) {
const legacyAnthropicCredential = target.get(ANTHROPIC_PROVIDER_ID) as StoredCredential | undefined;
if (legacyAnthropicCredential?.type === "oauth") {
target.remove(ANTHROPIC_PROVIDER_ID);
}
}
};
}
@@ -426,19 +591,14 @@ export function createFusionAuthStorage(): AuthStorage {
}
if (prop === "get") {
return (provider: string) => {
if (loggedOutProviders.has(provider)) {
return undefined;
}
return choosePreferredStoredCredential(
target.get(provider) as StoredCredential | undefined,
supplementalCredentials[provider],
);
};
return (provider: string) => selectVisibleStoredCredential(provider);
}
if (prop === "has") {
return (provider: string) => {
if (provider === ANTHROPIC_PROVIDER_ID) {
return hasVisibleAnthropicCredential();
}
if (loggedOutProviders.has(provider)) {
return false;
}
@@ -448,6 +608,9 @@ export function createFusionAuthStorage(): AuthStorage {
if (prop === "hasAuth") {
return (provider: string) => {
if (provider === ANTHROPIC_PROVIDER_ID) {
return hasVisibleAnthropicCredential();
}
if (loggedOutProviders.has(provider)) {
return false;
}
@@ -465,13 +628,7 @@ export function createFusionAuthStorage(): AuthStorage {
]);
const merged: Record<string, StoredCredential> = {};
for (const providerId of providerIds) {
if (loggedOutProviders.has(providerId)) {
continue;
}
const credential = choosePreferredStoredCredential(
(target.get(providerId) as StoredCredential | undefined),
supplementalCredentials[providerId],
);
const credential = selectVisibleStoredCredential(providerId);
if (credential) {
merged[providerId] = credential;
}
@@ -493,46 +650,49 @@ export function createFusionAuthStorage(): AuthStorage {
providers.add(p);
}
}
return Array.from(providers).filter((p) => !loggedOutProviders.has(p));
if (
!loggedOutProviders.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID)
&& (providers.has(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) || supplementalCredentials[ANTHROPIC_SUBSCRIPTION_PROVIDER_ID])
) {
providers.add(ANTHROPIC_PROVIDER_ID);
}
return Array.from(providers).filter((p) => {
if (p === ANTHROPIC_PROVIDER_ID) {
return hasVisibleAnthropicCredential();
}
if (loggedOutProviders.has(p)) {
return false;
}
return true;
});
};
}
if (prop === "getApiKey") {
return async (provider: string) => {
if (provider === ANTHROPIC_PROVIDER_ID) {
return resolveAnthropicRuntimeApiKey();
}
if (loggedOutProviders.has(provider)) {
return undefined;
}
if (provider === ANTHROPIC_SUBSCRIPTION_PROVIDER_ID) {
const subscriptionCredential = selectStoredCredential(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID);
return subscriptionCredential?.type === "oauth"
? resolveRefreshableCredentialApiKey(ANTHROPIC_SUBSCRIPTION_PROVIDER_ID, subscriptionCredential)
: undefined;
}
// 1. Primary Fusion auth
const primaryKey = await target.getApiKey(provider);
if (primaryKey) return primaryKey;
// 2. Supplemental auth.json credentials (.pi + .codex)
const refreshCandidate = choosePreferredStoredCredential(
target.get(provider) as StoredCredential | undefined,
supplementalCredentials[provider],
) ?? {};
const refreshWasNeeded = shouldRefreshOAuthCredential(refreshCandidate);
const refreshedCredential = await refreshProviderOAuthCredential(provider, refreshCandidate);
if (refreshedCredential?.type === "oauth" && refreshedCredential.access) {
if (refreshWasNeeded) {
/*
FNXC:ClaudeOAuth 2026-06-13-22:46:
A manual re-login or replacement credential must win over an older in-flight refresh response.
Re-check the credential identity before persisting so a delayed refresh cannot restore stale OAuth material after the user already fixed auth.
*/
const latestCredential = choosePreferredStoredCredential(
target.get(provider) as StoredCredential | undefined,
supplementalCredentials[provider],
);
if (!isSameOAuthCredentialIdentity(latestCredential, refreshCandidate)) {
return resolveStoredCredentialApiKey(provider, latestCredential);
}
}
target.set(provider, refreshedCredential as AuthCredential);
loggedOutProviders.delete(provider);
return resolveStoredCredentialApiKey(provider, refreshedCredential);
}
const refreshCandidate = selectStoredCredential(provider);
const refreshedKey = await resolveRefreshableCredentialApiKey(provider, refreshCandidate);
if (refreshedKey) return refreshedKey;
const supplementalKey = resolveStoredCredentialApiKey(provider, supplementalCredentials[provider]);
if (supplementalKey) return supplementalKey;