import { PROVIDER_DEFINITIONS, getProviderDefinition, } from '../../shared/providers/registry'; import type { ProviderAccount, ProviderConfig, ProviderDefinition, } from '../../shared/providers/types'; import type { UserSyncProviderAccount } from '../../../shared/user-sync'; import { ensureProviderStoreMigrated } from './provider-migration'; import { deleteProviderAccount, getDefaultProviderAccountId, getProviderAccount, listProviderAccounts, providerAccountToConfig, providerConfigToAccount, saveProviderAccount, setDefaultProviderAccount, } from './provider-store'; import { deleteApiKey, deleteProvider, getApiKey, hasApiKey, setDefaultProvider, storeApiKey, } from '../../utils/secure-storage'; import type { ProviderWithKeyInfo } from '../../shared/providers/types'; import { logger } from '../../utils/logger'; function maskApiKey(apiKey: string | null): string | null { if (!apiKey) return null; if (apiKey.length > 12) { return `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; } return '*'.repeat(apiKey.length); } const legacyProviderApiWarned = new Set(); function logLegacyProviderApiUsage(method: string, replacement: string): void { if (legacyProviderApiWarned.has(method)) { return; } legacyProviderApiWarned.add(method); logger.warn( `[provider-migration] Legacy provider API "${method}" is deprecated. Migrate to "${replacement}".`, ); } function normalizeSyncedMetadata(metadata: Record): ProviderAccount['metadata'] { const result: ProviderAccount['metadata'] = {}; if (typeof metadata.region === 'string' && metadata.region.trim()) { result.region = metadata.region.trim(); } if (typeof metadata.email === 'string' && metadata.email.trim()) { result.email = metadata.email.trim(); } if (typeof metadata.resourceUrl === 'string' && metadata.resourceUrl.trim()) { result.resourceUrl = metadata.resourceUrl.trim(); } if (Array.isArray(metadata.customModels)) { const customModels = metadata.customModels .filter((model): model is string => typeof model === 'string' && Boolean(model.trim())) .map((model) => model.trim()); if (customModels.length > 0) { result.customModels = customModels; } } return result; } export class ProviderService { async listVendors(): Promise { return PROVIDER_DEFINITIONS; } async listAccounts(): Promise { await ensureProviderStoreMigrated(); return listProviderAccounts(); } async getAccount(accountId: string): Promise { await ensureProviderStoreMigrated(); return getProviderAccount(accountId); } async getDefaultAccountId(): Promise { await ensureProviderStoreMigrated(); return getDefaultProviderAccountId(); } async createAccount(account: ProviderAccount, apiKey?: string): Promise { await ensureProviderStoreMigrated(); await saveProviderAccount(account); if (apiKey !== undefined && apiKey.trim()) { await storeApiKey(account.id, apiKey.trim()); } return (await getProviderAccount(account.id)) ?? account; } async updateAccount( accountId: string, patch: Partial, apiKey?: string, ): Promise { await ensureProviderStoreMigrated(); const existing = await getProviderAccount(accountId); if (!existing) { throw new Error('Provider account not found'); } const nextAccount: ProviderAccount = { ...existing, ...patch, id: accountId, updatedAt: patch.updatedAt ?? new Date().toISOString(), }; await saveProviderAccount(nextAccount); if (apiKey !== undefined) { const trimmedKey = apiKey.trim(); if (trimmedKey) { await storeApiKey(accountId, trimmedKey); } else { await deleteApiKey(accountId); } } return (await getProviderAccount(accountId)) ?? nextAccount; } async upsertSyncedAccountMetadata(account: UserSyncProviderAccount): Promise { await ensureProviderStoreMigrated(); const existing = await getProviderAccount(account.id); const now = new Date().toISOString(); const nextAccount: ProviderAccount = { id: account.id, vendorId: account.vendorId as ProviderAccount['vendorId'], label: account.label, authMode: account.authMode as ProviderAccount['authMode'], baseUrl: account.baseUrl, apiProtocol: account.apiProtocol as ProviderAccount['apiProtocol'] | undefined, headers: existing?.headers, model: account.model, fallbackModels: account.fallbackModels, fallbackAccountIds: account.fallbackAccountIds, enabled: account.enabled, isDefault: account.isDefault, metadata: normalizeSyncedMetadata(account.metadata ?? {}), createdAt: existing?.createdAt ?? now, updatedAt: account.updatedAt ?? now, }; await saveProviderAccount(nextAccount); if (nextAccount.isDefault) { await this.setDefaultAccount(nextAccount.id); } return (await getProviderAccount(nextAccount.id)) ?? nextAccount; } async deleteAccount(accountId: string): Promise { await ensureProviderStoreMigrated(); return deleteProvider(accountId); } /** Internal: list providers in the legacy ProviderConfig shape. */ async _listProvidersFromAccountsInternal(): Promise { const accounts = await this.listAccounts(); return accounts.map(providerAccountToConfig); } /** Internal: list providers with hasKey/keyMasked metadata. */ async _listProvidersWithKeyInfoInternal(): Promise { const providers = await this._listProvidersFromAccountsInternal(); const results: ProviderWithKeyInfo[] = []; for (const provider of providers) { const apiKey = await getApiKey(provider.id); results.push({ ...provider, hasKey: !!apiKey, keyMasked: maskApiKey(apiKey), }); } return results; } /** Internal: resolve a single provider in the legacy ProviderConfig shape. */ async _getProviderInternal(providerId: string): Promise { await ensureProviderStoreMigrated(); const account = await getProviderAccount(providerId); return account ? providerAccountToConfig(account) : null; } /** Internal: upsert a legacy provider config. */ async _saveProviderInternal(config: ProviderConfig): Promise { await ensureProviderStoreMigrated(); const account = providerConfigToAccount(config); const existing = await getProviderAccount(config.id); if (existing) { await this.updateAccount(config.id, account); return; } await this.createAccount(account); } /** Internal: delete a provider account by id. */ async _deleteProviderInternal(providerId: string): Promise { await ensureProviderStoreMigrated(); await deleteProviderAccount(providerId); await deleteApiKey(providerId); return true; } /** Internal: set default account without warning. */ async _setDefaultProviderInternal(providerId: string): Promise { await this.setDefaultAccount(providerId); } /** Internal: read default account id without warning. */ async _getDefaultProviderInternal(): Promise { return this.getDefaultAccountId(); } /** Internal: store an account's api key without warning. */ async _setProviderApiKeyInternal(providerId: string, apiKey: string): Promise { return storeApiKey(providerId, apiKey); } /** Internal: read an account's api key without warning. */ async _getProviderApiKeyInternal(providerId: string): Promise { return getApiKey(providerId); } /** Internal: delete an account's api key without warning. */ async _deleteProviderApiKeyInternal(providerId: string): Promise { return deleteApiKey(providerId); } /** Internal: check if an account has a stored api key. */ async _hasProviderApiKeyInternal(providerId: string): Promise { return hasApiKey(providerId); } /** Return per-account API key status for the account API surface. */ async listAccountsKeyInfo(): Promise> { const accounts = await this.listAccounts(); const results: Array<{ accountId: string; hasKey: boolean; keyMasked: string | null }> = []; for (const account of accounts) { const apiKey = await getApiKey(account.id); results.push({ accountId: account.id, hasKey: !!apiKey, keyMasked: maskApiKey(apiKey), }); } return results; } /** Read an account's API key. */ async getAccountApiKey(accountId: string): Promise { return this._getProviderApiKeyInternal(accountId); } /** Check whether an account has an API key stored. */ async hasAccountApiKey(accountId: string): Promise { return this._hasProviderApiKeyInternal(accountId); } /** Delete only an account's stored API key while keeping its model configuration. */ async deleteAccountApiKey(accountId: string): Promise { await ensureProviderStoreMigrated(); return deleteApiKey(accountId); } /** * @deprecated Use listAccounts() and map account data in callers. */ async listLegacyProviders(): Promise { logLegacyProviderApiUsage('listLegacyProviders', 'listAccounts'); return this._listProvidersFromAccountsInternal(); } /** * @deprecated Use listAccountsKeyInfo() + the account snapshot API. */ async listLegacyProvidersWithKeyInfo(): Promise { logLegacyProviderApiUsage('listLegacyProvidersWithKeyInfo', 'listAccountsKeyInfo'); return this._listProvidersWithKeyInfoInternal(); } /** * @deprecated Use getAccount(accountId). */ async getLegacyProvider(providerId: string): Promise { logLegacyProviderApiUsage('getLegacyProvider', 'getAccount'); return this._getProviderInternal(providerId); } /** * @deprecated Use createAccount()/updateAccount(). */ async saveLegacyProvider(config: ProviderConfig): Promise { logLegacyProviderApiUsage('saveLegacyProvider', 'createAccount/updateAccount'); return this._saveProviderInternal(config); } /** * @deprecated Use deleteAccount(accountId). */ async deleteLegacyProvider(providerId: string): Promise { logLegacyProviderApiUsage('deleteLegacyProvider', 'deleteAccount'); return this._deleteProviderInternal(providerId); } /** * @deprecated Use setDefaultAccount(accountId). */ async setDefaultLegacyProvider(providerId: string): Promise { logLegacyProviderApiUsage('setDefaultLegacyProvider', 'setDefaultAccount'); return this._setDefaultProviderInternal(providerId); } /** * @deprecated Use getDefaultAccountId(). */ async getDefaultLegacyProvider(): Promise { logLegacyProviderApiUsage('getDefaultLegacyProvider', 'getDefaultAccountId'); return this._getDefaultProviderInternal(); } /** * @deprecated Use secret-store APIs by accountId. */ async setLegacyProviderApiKey(providerId: string, apiKey: string): Promise { logLegacyProviderApiUsage('setLegacyProviderApiKey', 'setProviderSecret(accountId, api_key)'); return this._setProviderApiKeyInternal(providerId, apiKey); } /** * @deprecated Use getAccountApiKey(accountId). */ async getLegacyProviderApiKey(providerId: string): Promise { logLegacyProviderApiUsage('getLegacyProviderApiKey', 'getAccountApiKey'); return this._getProviderApiKeyInternal(providerId); } /** * @deprecated Use secret-store APIs by accountId. */ async deleteLegacyProviderApiKey(providerId: string): Promise { logLegacyProviderApiUsage('deleteLegacyProviderApiKey', 'deleteProviderSecret(accountId)'); return this._deleteProviderApiKeyInternal(providerId); } /** * @deprecated Use hasAccountApiKey(accountId). */ async hasLegacyProviderApiKey(providerId: string): Promise { logLegacyProviderApiUsage('hasLegacyProviderApiKey', 'hasAccountApiKey'); return this._hasProviderApiKeyInternal(providerId); } async setDefaultAccount(accountId: string): Promise { await ensureProviderStoreMigrated(); await setDefaultProviderAccount(accountId); await setDefaultProvider(accountId); } getVendorDefinition(vendorId: string): ProviderDefinition | undefined { return getProviderDefinition(vendorId); } } const providerService = new ProviderService(); export function getProviderService(): ProviderService { return providerService; }