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(credentials.has("default")).toBe(false);
|
||||||
expect(defaultId).toBe("acct-first");
|
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 legacySubscription;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
/*
|
||||||
reload: () => mergedAuthStorage.reload(),
|
FNXC:ProviderAuth 2026-08-15-21:46:
|
||||||
getOAuthProviders: () =>
|
This is the ONLY seam allowed to start an OAuth login: it maps the split Anthropic ids
|
||||||
mergedAuthStorage
|
(`anthropic` / `anthropic-subscription`) onto the upstream `anthropic` runtime login before
|
||||||
.getOAuthProviders()
|
relocating the credential to the subscription storage row. Instance-scoped login must reuse it —
|
||||||
.map((provider) => provider.id === ANTHROPIC_STORAGE_PROVIDER_ID
|
calling `mergedAuthStorage.login("anthropic-subscription", ...)` directly reaches
|
||||||
? ({ id: ANTHROPIC_STORAGE_PROVIDER_ID, name: "Anthropic Subscription" })
|
`ModelRuntime.login` with a storage-only id pi does not register and fails with
|
||||||
: ({ id: provider.id, name: provider.name })),
|
`Unknown provider: anthropic-subscription` (GitHub #3462).
|
||||||
hasAuth: (provider) => provider === ANTHROPIC_STORAGE_PROVIDER_ID || provider === ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID
|
*/
|
||||||
? Boolean(getAnthropicSubscriptionCredential())
|
const login = async (providerId: string, callbacks: LoginCallbacks): Promise<void> => {
|
||||||
: mergedAuthStorage.hasAuth(provider),
|
|
||||||
login: async (providerId, callbacks) => {
|
|
||||||
if (providerId !== ANTHROPIC_STORAGE_PROVIDER_ID && providerId !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
if (providerId !== ANTHROPIC_STORAGE_PROVIDER_ID && providerId !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
||||||
await mergedAuthStorage.login(
|
await mergedAuthStorage.login(
|
||||||
providerId,
|
providerId,
|
||||||
@@ -181,7 +179,20 @@ export function wrapAuthStorageWithApiKeyProviders(
|
|||||||
await authStorage.remove(ANTHROPIC_STORAGE_PROVIDER_ID);
|
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) => {
|
logout: async (provider) => {
|
||||||
if (provider !== ANTHROPIC_STORAGE_PROVIDER_ID && provider !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
if (provider !== ANTHROPIC_STORAGE_PROVIDER_ID && provider !== ANTHROPIC_SUBSCRIPTION_STORAGE_PROVIDER_ID) {
|
||||||
await mergedAuthStorage.logout(provider);
|
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
|
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
|
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.
|
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);
|
const credential = mergedAuthStorage.get(providerId);
|
||||||
if (!credential) return;
|
if (!credential) return;
|
||||||
await mergedAuthStorage.setInstance(target, { ...credential, ...(label ? { label } : {}) });
|
await mergedAuthStorage.setInstance(target, { ...credential, ...(label ? { label } : {}) });
|
||||||
|
|||||||
Reference in New Issue
Block a user