diff --git a/.changeset/data-driven-auth-provider-options.md b/.changeset/data-driven-auth-provider-options.md new file mode 100644 index 0000000..ac49865 --- /dev/null +++ b/.changeset/data-driven-auth-provider-options.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Offer API-key and OAuth login options for every provider the agent backend supports each method for, instead of a curated hardcoded list. Providers that support both methods (such as Anthropic and GitHub Copilot) now surface both an API-key and an OAuth login option, driven purely by what the backend reports. diff --git a/src/server/sessions/authProviderOptions.test.ts b/src/server/sessions/authProviderOptions.test.ts index 2180f68..7ed820f 100644 --- a/src/server/sessions/authProviderOptions.test.ts +++ b/src/server/sessions/authProviderOptions.test.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from "vitest"; -import { getLoginProviderOptions, getLogoutProviderOptions, isApiKeyLoginProvider, type AuthProviderRuntime } from "./authProviderOptions"; +import { getLoginProviderOptions, getLogoutProviderOptions, type AuthProviderRuntime } from "./authProviderOptions"; function runtime(): AuthProviderRuntime { const credentials = [{ providerId: "openai", type: "api_key" as const }]; + // Auth shapes mirror what the Pi SDK actually reports for these providers: + // github-copilot supports both methods, openai-codex is oauth-only. const providers = [ { id: "anthropic", name: "Anthropic", auth: { oauth: {}, apiKey: {} } }, - { id: "github-copilot", name: "GitHub Copilot", auth: { oauth: {} } }, - { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", auth: { oauth: {}, apiKey: {} } }, + { id: "github-copilot", name: "GitHub Copilot", auth: { oauth: {}, apiKey: {} } }, + { id: "openai-codex", name: "ChatGPT Plus/Pro (Codex Subscription)", auth: { oauth: {} } }, { id: "openai", name: "OpenAI", auth: { apiKey: {} } }, { id: "custom", name: "Custom", auth: { apiKey: {} } }, ]; @@ -18,21 +20,22 @@ function runtime(): AuthProviderRuntime { } describe("auth provider options", () => { - it("keeps OAuth-only providers out of API key login options", () => { - expect(isApiKeyLoginProvider("openai-codex", new Set(["openai-codex"]))).toBe(false); - expect(isApiKeyLoginProvider("github-copilot", new Set(["github-copilot"]))).toBe(false); - expect(isApiKeyLoginProvider("openai", new Set(["openai-codex"]))).toBe(true); - }); - - it("builds login options for OAuth-only, dual-auth, and API-key providers", () => { + it("offers both api-key and oauth login options for every provider the backend supports each method for", () => { const options = getLoginProviderOptions(runtime()); expect(options).toEqual(expect.arrayContaining([ + // Dual-capable providers surface both login methods, driven purely by SDK data. expect.objectContaining({ id: "anthropic", authType: "oauth" }), expect.objectContaining({ id: "anthropic", authType: "api_key" }), - expect.objectContaining({ id: "openai", authType: "api_key", status: { configured: true, source: "stored" } }), + expect.objectContaining({ id: "github-copilot", authType: "oauth" }), + expect.objectContaining({ id: "github-copilot", authType: "api_key" }), + // OAuth-only provider surfaces only oauth. expect.objectContaining({ id: "openai-codex", authType: "oauth" }), + // API-key-only providers surface only api_key. + expect.objectContaining({ id: "openai", authType: "api_key", status: { configured: true, source: "stored" } }), + expect.objectContaining({ id: "custom", authType: "api_key" }), ])); expect(options).not.toEqual(expect.arrayContaining([expect.objectContaining({ id: "openai-codex", authType: "api_key" })])); + expect(options).not.toEqual(expect.arrayContaining([expect.objectContaining({ id: "openai", authType: "oauth" })])); }); it("returns only currently stored credentials for logout", async () => { diff --git a/src/server/sessions/authProviderOptions.ts b/src/server/sessions/authProviderOptions.ts index 94cc528..58409c6 100644 --- a/src/server/sessions/authProviderOptions.ts +++ b/src/server/sessions/authProviderOptions.ts @@ -1,7 +1,5 @@ import type { AuthProviderOption, AuthProviderStatus, AuthType } from "../../shared/apiTypes.js"; -const OAUTH_ONLY_PROVIDERS = new Set(["github-copilot", "openai-codex"]); - /** Minimal provider shape needed to enumerate login/logout options. */ interface AuthProviderInfo { id: string; @@ -29,7 +27,6 @@ export interface AuthProviderRuntime { export function getLoginProviderOptions(runtime: AuthProviderRuntime, authType?: AuthType): AuthProviderOption[] { const providers = runtime.getProviders(); - const oauthProviderIds = new Set(providers.filter((provider) => provider.auth.oauth !== undefined).map((provider) => provider.id)); const options: AuthProviderOption[] = []; for (const provider of providers) { @@ -44,7 +41,6 @@ export function getLoginProviderOptions(runtime: AuthProviderRuntime, authType?: for (const provider of providers) { if (provider.auth.apiKey === undefined) continue; - if (!isApiKeyLoginProvider(provider.id, oauthProviderIds)) continue; options.push({ id: provider.id, name: provider.name, @@ -70,13 +66,6 @@ export async function getLogoutProviderOptions(runtime: AuthProviderRuntime): Pr return filterAndSort(options); } -export function isApiKeyLoginProvider(providerId: string, oauthProviderIds: ReadonlySet): boolean { - if (OAUTH_ONLY_PROVIDERS.has(providerId)) return false; - if (providerId === "anthropic") return true; - if (oauthProviderIds.has(providerId)) return false; - return true; -} - function filterAndSort(options: AuthProviderOption[], authType?: AuthType): AuthProviderOption[] { const filtered = authType === undefined ? options : options.filter((option) => option.authType === authType); return filtered.sort((a, b) => a.name.localeCompare(b.name) || a.authType.localeCompare(b.authType) || a.id.localeCompare(b.id));