diff --git a/src/hooks/useVisualConfig.ts b/src/hooks/useVisualConfig.ts index f4b4f0d..ff78777 100644 --- a/src/hooks/useVisualConfig.ts +++ b/src/hooks/useVisualConfig.ts @@ -47,9 +47,100 @@ function parseApiKeysText(raw: unknown): string { return keys.join('\n'); } +function replaceApiKeyValue(entry: unknown, apiKey: string): unknown { + const record = asRecord(entry); + if (!record) return apiKey; + + if ('api-key' in record) return { ...record, 'api-key': apiKey }; + if ('apiKey' in record) return { ...record, apiKey }; + if ('key' in record) return { ...record, key: apiKey }; + if ('Key' in record) return { ...record, Key: apiKey }; + + return { ...record, 'api-key': apiKey }; +} + +function buildApiKeyEntries( + apiKeys: string[], + metadata: ApiKeysStorageMetadata +): Array> { + return apiKeys.map((apiKey, index) => { + const originalEntry = metadata.originalEntries[index]; + if (metadata.entryMode === 'object') { + const replaced = replaceApiKeyValue(originalEntry, apiKey); + return asRecord(replaced) ?? { 'api-key': apiKey }; + } + + const record = asRecord(originalEntry); + return record ? ({ ...record, ...(replaceApiKeyValue(record, apiKey) as Record) }) : apiKey; + }); +} + +function resolveApiKeysStorage(parsed: Record): { + text: string; + metadata: ApiKeysStorageMetadata; +} { + const legacyEntries = Array.isArray(parsed['api-keys']) ? parsed['api-keys'] : []; + const auth = asRecord(parsed.auth); + const providers = asRecord(auth?.providers); + const configApiKeyProvider = asRecord(providers?.['config-api-key']); + + if (configApiKeyProvider) { + const providerEntries = Array.isArray(configApiKeyProvider['api-key-entries']) + ? configApiKeyProvider['api-key-entries'] + : Array.isArray(configApiKeyProvider['api-keys']) + ? configApiKeyProvider['api-keys'] + : []; + const providerListKey = Array.isArray(configApiKeyProvider['api-key-entries']) + ? 'api-key-entries' + : 'api-keys'; + + return { + text: parseApiKeysText(providerEntries), + metadata: { + source: 'auth-provider', + providerListKey, + entryMode: + providerListKey === 'api-key-entries' || providerEntries.some((entry) => Boolean(asRecord(entry))) + ? 'object' + : 'string', + originalEntries: providerEntries, + syncLegacy: legacyEntries.length > 0, + }, + }; + } + + return { + text: parseApiKeysText(legacyEntries), + metadata: { + source: 'legacy', + entryMode: legacyEntries.some((entry) => Boolean(asRecord(entry))) ? 'object' : 'string', + originalEntries: legacyEntries, + syncLegacy: false, + }, + }; +} + type YamlDocument = ReturnType; type YamlPath = string[]; +type ApiKeysStorageMode = 'legacy' | 'auth-provider'; +type ApiKeysEntryMode = 'string' | 'object'; + +type ApiKeysStorageMetadata = { + source: ApiKeysStorageMode; + providerListKey?: 'api-keys' | 'api-key-entries'; + entryMode: ApiKeysEntryMode; + originalEntries: unknown[]; + syncLegacy: boolean; +}; + +const DEFAULT_API_KEYS_STORAGE_METADATA: ApiKeysStorageMetadata = { + source: 'legacy', + entryMode: 'string', + originalEntries: [], + syncLegacy: false, +}; + function docHas(doc: YamlDocument, path: YamlPath): boolean { return doc.hasIn(path); } @@ -348,6 +439,8 @@ export function useVisualConfig() { ...DEFAULT_VISUAL_VALUES, }); const [visualParseError, setVisualParseError] = useState(null); + const [apiKeysStorageMetadata, setApiKeysStorageMetadata] = + useState(DEFAULT_API_KEYS_STORAGE_METADATA); const visualValidationErrors = useMemo( () => getVisualConfigValidationErrors(visualValues), [visualValues] @@ -378,6 +471,7 @@ export function useVisualConfig() { const routing = asRecord(parsed.routing); const payload = asRecord(parsed.payload); const streaming = asRecord(parsed.streaming); + const apiKeysStorage = resolveApiKeysStorage(parsed); const newValues: VisualConfigValues = { host: typeof parsed.host === 'string' ? parsed.host : '', @@ -399,7 +493,7 @@ export function useVisualConfig() { : '', authDir: typeof parsed['auth-dir'] === 'string' ? parsed['auth-dir'] : '', - apiKeysText: parseApiKeysText(parsed['api-keys']), + apiKeysText: apiKeysStorage.text, debug: Boolean(parsed.debug), commercialMode: Boolean(parsed['commercial-mode']), @@ -434,6 +528,7 @@ export function useVisualConfig() { setVisualValuesState(newValues); setBaselineValues(deepClone(newValues)); + setApiKeysStorageMetadata(apiKeysStorage.metadata); setVisualParseError(null); return { ok: true as const }; } catch (error: unknown) { @@ -497,8 +592,35 @@ export function useVisualConfig() { .split('\n') .map((key) => key.trim()) .filter(Boolean); - if (apiKeys.length > 0) { - doc.setIn(['api-keys'], apiKeys); + const apiKeyEntries = buildApiKeyEntries(apiKeys, apiKeysStorageMetadata); + + if (apiKeysStorageMetadata.source === 'auth-provider') { + ensureMapInDoc(doc, ['auth']); + ensureMapInDoc(doc, ['auth', 'providers']); + ensureMapInDoc(doc, ['auth', 'providers', 'config-api-key']); + + const providerListKey = apiKeysStorageMetadata.providerListKey ?? 'api-key-entries'; + const providerPath = ['auth', 'providers', 'config-api-key', providerListKey]; + + if (apiKeys.length > 0) { + doc.setIn(providerPath, apiKeyEntries); + } else if (docHas(doc, providerPath)) { + doc.deleteIn(providerPath); + } + + deleteIfMapEmpty(doc, ['auth', 'providers', 'config-api-key']); + deleteIfMapEmpty(doc, ['auth', 'providers']); + deleteIfMapEmpty(doc, ['auth']); + + if (apiKeysStorageMetadata.syncLegacy) { + if (apiKeys.length > 0) { + doc.setIn(['api-keys'], apiKeys); + } else if (docHas(doc, ['api-keys'])) { + doc.deleteIn(['api-keys']); + } + } + } else if (apiKeys.length > 0) { + doc.setIn(['api-keys'], apiKeyEntries); } else if (docHas(doc, ['api-keys'])) { doc.deleteIn(['api-keys']); } @@ -600,7 +722,7 @@ export function useVisualConfig() { return currentYaml; } }, - [baselineValues, visualValues] + [apiKeysStorageMetadata, baselineValues, visualValues] ); const setVisualValues = useCallback((newValues: Partial) => {