Skip to content

Commit af0db0f

Browse files
committed
Drop OAuth settings entries without a live profile
1 parent 611b475 commit af0db0f

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

src/config/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,30 @@ function applyPersistedOAuthDefaults(
101101
return merged;
102102
}
103103

104+
// OAuth entries in settings.json carry no credentials; they are only usable
105+
// while a matching auth-store profile exists. Drop orphans in memory so a
106+
// removed profile does not pin resolution to an unauthenticatable provider.
107+
function dropOrphanedOAuthEntries(
108+
settings: Settings | null,
109+
projected: Record<string, ProviderSettings>,
110+
): Settings | null {
111+
if (settings === null) return null;
112+
const providers = Object.fromEntries(
113+
Object.entries(settings.providers).filter(
114+
([name]) =>
115+
(!isCodexProviderName(name) && !isXaiProviderName(name)) || projected[name] !== undefined,
116+
),
117+
);
118+
const { defaultProvider, ...rest } = settings;
119+
return {
120+
...rest,
121+
providers,
122+
...(defaultProvider !== undefined && providers[defaultProvider] !== undefined
123+
? { defaultProvider }
124+
: {}),
125+
};
126+
}
127+
104128
function hasExaEntry(servers: MCPServerSettingsEntry[] | undefined): boolean {
105129
return servers?.some((server) => server.name === EXA_MCP_SERVER_NAME) === true;
106130
}
@@ -719,13 +743,16 @@ export async function loadConfig(
719743
dangerouslySkipPermissions =
720744
dangerouslySkipPermissions || settings?.dangerouslySkipPermissions === true;
721745
projectedOAuthProviders = applyPersistedOAuthDefaults(settings, projectedOAuthProviders);
746+
const liveSettings = useOAuthProfiles
747+
? dropOrphanedOAuthEntries(settings, projectedOAuthProviders)
748+
: settings;
722749
const settingsForResolution: Settings | null =
723750
Object.keys(projectedOAuthProviders).length > 0
724751
? {
725-
...(settings ?? { providers: {} }),
726-
providers: { ...(settings?.providers ?? {}), ...projectedOAuthProviders },
752+
...(liveSettings ?? { providers: {} }),
753+
providers: { ...(liveSettings?.providers ?? {}), ...projectedOAuthProviders },
727754
}
728-
: settings;
755+
: liveSettings;
729756

730757
// The per-repo selection file still applies on top of a --config source: that
731758
// file supplies provider definitions, while .corbits/settings.json supplies

tests/unit/config.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,49 @@ test("aliased-home restart preserves a non-default OAuth model", async () => {
376376
}
377377
});
378378

379+
test("loadConfig ignores a persisted OAuth entry whose auth profile is gone", async () => {
380+
const fakeHome = await mkdtemp(join(tmpdir(), "ic-unit-config-oauth-orphan-home-"));
381+
try {
382+
await mkdir(join(fakeHome, ".corbits"), { recursive: true });
383+
const settingsPath = join(fakeHome, ".corbits", "settings.json");
384+
const original = JSON.stringify({
385+
defaultProvider: "xai/gone",
386+
providers: {
387+
"xai/gone": {
388+
baseURL: "https://api.x.ai/v1",
389+
models: ["grok-4"],
390+
defaultModel: "grok-4",
391+
},
392+
openai: {
393+
baseURL: "https://api.openai.com/v1",
394+
apiKey: "test-key",
395+
models: ["gpt-5"],
396+
},
397+
},
398+
});
399+
await writeFile(settingsPath, original);
400+
401+
await withMockedModuleDuring(
402+
import.meta.resolve("node:os"),
403+
(real: typeof import("node:os")) => ({ ...real, homedir: () => fakeHome }),
404+
async () => {
405+
const { impl } = offlineFetch();
406+
const config = await loadConfig(["--cwd", fakeHome, "do something"], {
407+
pricing: { fetchImpl: impl },
408+
});
409+
expect(config.configured).toBe(true);
410+
if (config.configured) {
411+
expect(config.providerName).toBe("openai");
412+
expect(config.providers.some((p) => p.name === "xai/gone")).toBe(false);
413+
}
414+
},
415+
);
416+
expect(await readFile(settingsPath, "utf8")).toBe(original);
417+
} finally {
418+
await rm(fakeHome, { recursive: true, force: true });
419+
}
420+
});
421+
379422
// Regression: OAuth credentials for an xai/<profile> provider are never read
380423
// from settings.json — home-level auth stores are the source of truth, and
381424
// loadConfig merges them into the catalog it hands to resolveProvider (see

0 commit comments

Comments
 (0)