From 297845f10c647b0c99f7beb153358dda7b5a2a70 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 23 Mar 2026 12:03:05 -0400 Subject: [PATCH] fix(auth): fall through to env/fallback when OAuth credential has no registered provider (#2097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2083 When an OpenRouter API key is stored in auth.json as type:"oauth" (instead of type:"api_key"), getApiKey() calls getOAuthProvider("openrouter") which returns undefined — OpenRouter is not a registered OAuth provider. Previously, resolveCredentialApiKey returned undefined and getApiKey returned that directly, never reaching the env-var or fallback-resolver paths. Now, when resolveCredentialApiKey returns undefined, getApiKey falls through to OPENROUTER_API_KEY env var and the fallback resolver instead of silently failing with "Authentication failed." Co-authored-by: Claude Opus 4.6 (1M context) --- .../src/core/auth-storage.test.ts | 68 +++++++++++++++++++ .../pi-coding-agent/src/core/auth-storage.ts | 7 +- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/packages/pi-coding-agent/src/core/auth-storage.test.ts b/packages/pi-coding-agent/src/core/auth-storage.test.ts index f91947ca9..74020a4ec 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.test.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.test.ts @@ -263,6 +263,74 @@ describe("AuthStorage — areAllCredentialsBackedOff", () => { }); }); +// ─── mismatched oauth credential for non-OAuth provider (#2083) ─────────────── + +describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () => { + it("returns undefined when openrouter has type:oauth (no registered OAuth provider)", async () => { + // Simulates the bug: OpenRouter credential stored as type:"oauth" + // but OpenRouter is not a registered OAuth provider. + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + // Before the fix, getApiKey returns undefined because + // resolveCredentialApiKey calls getOAuthProvider("openrouter") → null → undefined. + // The key in the oauth credential is never extracted. + const key = await storage.getApiKey("openrouter"); + // After the fix, the oauth credential with an unrecognised provider + // should be skipped, and getApiKey should fall through to env / fallback. + assert.equal(key, undefined); + }); + + it("falls through to env var when openrouter has type:oauth credential", async () => { + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + // Simulate OPENROUTER_API_KEY being set via env + const origEnv = process.env.OPENROUTER_API_KEY; + try { + process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key"; + const key = await storage.getApiKey("openrouter"); + assert.equal(key, "sk-or-v1-env-key"); + } finally { + if (origEnv === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = origEnv; + } + } + }); + + it("falls through to fallback resolver when openrouter has type:oauth credential", async () => { + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + storage.setFallbackResolver((provider) => + provider === "openrouter" ? "sk-or-v1-fallback" : undefined, + ); + + const key = await storage.getApiKey("openrouter"); + assert.equal(key, "sk-or-v1-fallback"); + }); +}); + // ─── getAll truncation ──────────────────────────────────────────────────────── describe("AuthStorage — getAll()", () => { diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts index c632090a7..5ae286177 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.ts @@ -756,9 +756,12 @@ export class AuthStorage { if (credentials.length > 0) { const index = this.selectCredentialIndex(providerId, credentials, sessionId); if (index >= 0) { - return this.resolveCredentialApiKey(providerId, credentials[index]); + const resolved = await this.resolveCredentialApiKey(providerId, credentials[index]); + if (resolved) return resolved; + // Credential unresolvable (e.g. type:"oauth" for a non-OAuth provider) — + // fall through to env / fallback instead of returning undefined (#2083) } - // All credentials backed off - fall through to env/fallback + // All credentials backed off or unresolvable - fall through to env/fallback } // Fall back to environment variable