fix: route Anthropic subscription instance login through upstream anthropic provider
The Authentication cards pass an explicit credential-instance id, which sent dashboard subscription logins through loginInstance. That seam mapped the card to the anthropic-subscription storage row id and passed it verbatim to ModelRuntime.login, which pi rejects with 'Unknown provider: anthropic-subscription' (GitHub #3462) — every subscription login failed while the credential path itself was healthy. loginInstance now reuses the Anthropic-aware login seam (upstream login as 'anthropic', credential relocated to the anthropic-subscription row) with a regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
7
.changeset/fix-anthropic-subscription-instance-login.md
Normal file
7
.changeset/fix-anthropic-subscription-instance-login.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Fix Anthropic Subscription login failing with "Unknown provider: anthropic-subscription".
|
||||
category: fix
|
||||
dev: Instance-scoped OAuth login (`loginInstance`) now reuses the Anthropic-aware login seam, logging in upstream as `anthropic` and persisting to the `anthropic-subscription` storage row, instead of passing the storage-only id to `ModelRuntime.login` (GitHub #3462).
|
||||
@@ -62,4 +62,37 @@ describe("DashboardAuthStorage instance facade", () => {
|
||||
expect(credentials.has("default")).toBe(false);
|
||||
expect(defaultId).toBe("acct-first");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:ProviderAuth 2026-08-15-21:46:
|
||||
Regression for GitHub #3462: instance-scoped Anthropic subscription login must reach the
|
||||
upstream runtime as `anthropic` and persist the OAuth result under the `anthropic-subscription`
|
||||
storage row. Passing the storage row id to the runtime login fails with
|
||||
`Unknown provider: anthropic-subscription` because pi never registers that id as a provider.
|
||||
*/
|
||||
it("routes Anthropic subscription instance login through the upstream anthropic provider", async () => {
|
||||
const rows = new Map<string, { type: string; key?: string; expires?: number }>();
|
||||
const storage = {
|
||||
...storageFixture(),
|
||||
login: vi.fn(async (providerId: string) => {
|
||||
rows.set(providerId, { type: "oauth", expires: Date.now() + 60_000 });
|
||||
}),
|
||||
get: vi.fn((providerId: string) => rows.get(providerId)),
|
||||
set: vi.fn(async (providerId: string, credential: { type: string }) => {
|
||||
rows.set(providerId, credential as never);
|
||||
}),
|
||||
remove: vi.fn(async (providerId: string) => { rows.delete(providerId); }),
|
||||
getDefaultInstance: vi.fn(() => undefined),
|
||||
getInstance: vi.fn(() => undefined),
|
||||
setInstance: vi.fn(),
|
||||
};
|
||||
const facade = wrapAuthStorageWithApiKeyProviders(storage as unknown as FusionAuthStorage, {} as ModelRegistry);
|
||||
await facade.loginInstance?.({ providerId: "anthropic", instanceId: "default" }, {} as never);
|
||||
expect(storage.login).toHaveBeenCalledTimes(1);
|
||||
expect(storage.login.mock.calls[0]?.[0]).toBe("anthropic");
|
||||
expect(storage.setInstance).toHaveBeenCalledWith(
|
||||
{ providerId: "anthropic-subscription", instanceId: "default" },
|
||||
expect.objectContaining({ type: "oauth" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,18 +142,16 @@ export function wrapAuthStorageWithApiKeyProviders(
|
||||
return legacySubscription;
|
||||
};
|
||||
|
||||
return {
|
||||
reload: () => mergedAuthStorage.reload(),
|
||||
getOAuthProviders: () =>
|
||||
mergedAuthStorage
|
||||
.getOAuthProviders()
|
||||
.map((provider) => provider.id === ANTHROPIC_STORAGE_PROVIDER_ID
|
||||
? ({ id: ANTHROPIC_STORAGE_PROVIDER_ID, name: "Anthropic Subscription" })
|
||||
: ({ id: provider.id, name: provider.name })),
|
||||
hasAuth: (provider) => provider === ANTHROPIC_STORAGE_PROVIDER_ID || provider === ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID
|
||||
? Boolean(getAnthropicSubscriptionCredential())
|
||||
: mergedAuthStorage.hasAuth(provider),
|
||||
login: async (providerId, callbacks) => {
|
||||
/*
|
||||
FNXC:ProviderAuth 2026-08-15-21:46:
|
||||
This is the ONLY seam allowed to start an OAuth login: it maps the split Anthropic ids
|
||||
(`anthropic` / `anthropic-subscription`) onto the upstream `anthropic` runtime login before
|
||||
relocating the credential to the subscription storage row. Instance-scoped login must reuse it —
|
||||
calling `mergedAuthStorage.login("anthropic-subscription", ...)` directly reaches
|
||||
`ModelRuntime.login` with a storage-only id pi does not register and fails with
|
||||
`Unknown provider: anthropic-subscription` (GitHub #3462).
|
||||
*/
|
||||
const login = async (providerId: string, callbacks: LoginCallbacks): Promise<void> => {
|
||||
if (providerId !== ANTHROPIC_STORAGE_PROVIDER_ID && providerId !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
||||
await mergedAuthStorage.login(
|
||||
providerId,
|
||||
@@ -181,7 +179,20 @@ export function wrapAuthStorageWithApiKeyProviders(
|
||||
await authStorage.remove(ANTHROPIC_STORAGE_PROVIDER_ID);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
reload: () => mergedAuthStorage.reload(),
|
||||
getOAuthProviders: () =>
|
||||
mergedAuthStorage
|
||||
.getOAuthProviders()
|
||||
.map((provider) => provider.id === ANTHROPIC_STORAGE_PROVIDER_ID
|
||||
? ({ id: ANTHROPIC_STORAGE_PROVIDER_ID, name: "Anthropic Subscription" })
|
||||
: ({ id: provider.id, name: provider.name })),
|
||||
hasAuth: (provider) => provider === ANTHROPIC_STORAGE_PROVIDER_ID || provider === ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID
|
||||
? Boolean(getAnthropicSubscriptionCredential())
|
||||
: mergedAuthStorage.hasAuth(provider),
|
||||
login,
|
||||
logout: async (provider) => {
|
||||
if (provider !== ANTHROPIC_STORAGE_PROVIDER_ID && provider !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
||||
await mergedAuthStorage.logout(provider);
|
||||
@@ -294,8 +305,16 @@ export function wrapAuthStorageWithApiKeyProviders(
|
||||
The runtime OAuth adapter only accepts a bare provider and therefore writes its resolved
|
||||
default slot. Capture and restore that slot around the login before persisting the result to
|
||||
the requested instance, so adding or reauthorizing an account never repoints its credential.
|
||||
|
||||
FNXC:ProviderAuth 2026-08-15-21:46:
|
||||
Instance login must go through the Anthropic-aware `login` seam above, never raw
|
||||
`mergedAuthStorage.login(providerId, ...)`: for the subscription card `providerId` here is the
|
||||
storage row id `anthropic-subscription`, which pi's ModelRuntime does not register as a
|
||||
provider, so the raw call failed every dashboard subscription login with
|
||||
`Unknown provider: anthropic-subscription` (GitHub #3462) once the Authentication cards began
|
||||
passing an explicit credential-instance id.
|
||||
*/
|
||||
await mergedAuthStorage.login(providerId, callbacks);
|
||||
await login(providerId, callbacks);
|
||||
const credential = mergedAuthStorage.get(providerId);
|
||||
if (!credential) return;
|
||||
await mergedAuthStorage.setInstance(target, { ...credential, ...(label ? { label } : {}) });
|
||||
|
||||
Reference in New Issue
Block a user