From 46066d7a5f17692828d24b689db97c296e8a826a Mon Sep 17 00:00:00 2001 From: digua Date: Tue, 23 Jun 2026 19:53:28 +0800 Subject: [PATCH] fix(ai): clean up old auth profile when rename + key change shifts profile name When a model service is renamed and its API key changed simultaneously, onApiKeyCreated derives a new profile name from the new service name and writes a new auth profile, but the old profile (keyed by the old name) was never deleted, leaving an orphaned credential on disk. Capture the old authProfile before applying updates. If onApiKeyCreated returns a different profile name, call onApiKeyDeleted on the old config to trigger cleanup before persisting the new profile. No cleanup is needed for key-only updates (profile name unchanged) or name-only updates (onApiKeyCreated is not called). Co-Authored-By: Claude Sonnet 4.6 --- .../src/ai/__tests__/llm-config-store.test.ts | 40 +++++++++++++++++++ .../node-runtime/src/ai/llm-config-store.ts | 8 +++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts b/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts index 1dcc6323..b14c358f 100644 --- a/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts +++ b/packages/node-runtime/src/ai/__tests__/llm-config-store.test.ts @@ -200,6 +200,46 @@ describe('LLMConfigStore', () => { assert.equal(storeWithHook.getAllConfigs().length, 0, 'config should be removed') }) + it('calls onApiKeyDeleted for old profile when rename + key change causes profile name to change', () => { + const deleted: string[] = [] + const storeWithHook = new LLMConfigStore(storage, { + generateId: () => `id-${++idCounter}`, + onApiKeyCreated: (config, apiKey) => { + const name = config.name.toLowerCase().replace(/\s+/g, '-') + ;(config as unknown as Record).authProfile = name + return name + }, + onApiKeyDeleted: (config) => { + const profile = (config as unknown as Record).authProfile as string | undefined + if (profile) deleted.push(profile) + }, + }) + storeWithHook.addConfig({ name: 'My OpenAI', provider: 'openai', apiKey: 'sk-old' }) + const id = storeWithHook.getAllConfigs()[0].id + storeWithHook.updateConfig(id, { name: 'Work OpenAI', apiKey: 'sk-new' }) + assert.deepEqual(deleted, ['my-openai'], 'old profile should be cleaned up when profile name changes') + }) + + it('does not call onApiKeyDeleted when key changes but profile name stays the same', () => { + const deleted: string[] = [] + const storeWithHook = new LLMConfigStore(storage, { + generateId: () => `id-${++idCounter}`, + onApiKeyCreated: (config, apiKey) => { + const name = config.name.toLowerCase().replace(/\s+/g, '-') + ;(config as unknown as Record).authProfile = name + return name + }, + onApiKeyDeleted: (config) => { + const profile = (config as unknown as Record).authProfile as string | undefined + if (profile) deleted.push(profile) + }, + }) + storeWithHook.addConfig({ name: 'My OpenAI', provider: 'openai', apiKey: 'sk-old' }) + const id = storeWithHook.getAllConfigs()[0].id + storeWithHook.updateConfig(id, { apiKey: 'sk-new' }) + assert.deepEqual(deleted, [], 'no cleanup needed when profile name is unchanged') + }) + it('resolves correct key for same-provider configs with different authProfiles', () => { const profiles = new Map() const storeWithAuth = new LLMConfigStore(storage, { diff --git a/packages/node-runtime/src/ai/llm-config-store.ts b/packages/node-runtime/src/ai/llm-config-store.ts index 714769a8..383650a7 100644 --- a/packages/node-runtime/src/ai/llm-config-store.ts +++ b/packages/node-runtime/src/ai/llm-config-store.ts @@ -211,8 +211,11 @@ export class LLMConfigStore { return { success: false, error: this.t('llm.configNotFound') } } + const oldConfig = store.configs[index] + const oldProfileName = (oldConfig as unknown as Record).authProfile as string | undefined + const updated = { - ...store.configs[index], + ...oldConfig, ...updates, updatedAt: Date.now(), } @@ -221,6 +224,9 @@ export class LLMConfigStore { if (updates.apiKey && this.onApiKeyCreated) { const profileName = this.onApiKeyCreated(updated, updates.apiKey) if (profileName) { + if (oldProfileName && oldProfileName !== profileName) { + this.onApiKeyDeleted?.(oldConfig) + } ;(store.configs[index] as unknown as Record).authProfile = profileName } }