credential-service.ts
1 import type { Credential } from '@/types' 2 3 import { genId } from '@/lib/id' 4 import { 5 deleteCredential, 6 decryptKey, 7 encryptKey, 8 loadCredential, 9 loadCredentials, 10 saveCredential, 11 } from '@/lib/server/credentials/credential-repository' 12 import { log } from '@/lib/server/logger' 13 14 const TAG = 'credential-service' 15 16 export type CredentialSummary = Pick<Credential, 'id' | 'provider' | 'name' | 'createdAt'> 17 18 function clean(value: string | null | undefined): string { 19 return typeof value === 'string' ? value.trim() : '' 20 } 21 22 function toCredentialSummary(credential: Credential | null | undefined): CredentialSummary | null { 23 if (!credential) return null 24 return { 25 id: credential.id, 26 provider: credential.provider, 27 name: credential.name, 28 createdAt: credential.createdAt, 29 } 30 } 31 32 export function listCredentialSummaries(): Record<string, CredentialSummary> { 33 const credentials = loadCredentials() 34 const summaries: Record<string, CredentialSummary> = {} 35 for (const [id, credential] of Object.entries(credentials)) { 36 const summary = toCredentialSummary(credential) 37 if (summary) summaries[id] = summary 38 } 39 return summaries 40 } 41 42 export function getCredentialSummary(id: string): CredentialSummary | null { 43 return toCredentialSummary(loadCredential(id)) 44 } 45 46 export function listCredentialIdsByProvider(provider: string): string[] { 47 const normalizedProvider = clean(provider) 48 if (!normalizedProvider) return [] 49 return Object.entries(loadCredentials()) 50 .filter(([, credential]) => credential?.provider === normalizedProvider) 51 .map(([id]) => id) 52 } 53 54 export function resolveCredentialSecret(credentialId: string | null | undefined): string | null { 55 const id = clean(credentialId) 56 if (!id) return null 57 const credential = loadCredential(id) 58 if (!credential?.encryptedKey) return null 59 try { 60 return decryptKey(credential.encryptedKey) 61 } catch (err) { 62 log.warn(TAG, `Failed to decrypt credential "${id}" — CREDENTIAL_SECRET may have changed since this key was stored. Re-add the API key to fix.`, { 63 credentialId: id, 64 provider: credential.provider, 65 error: err instanceof Error ? err.message : String(err), 66 }) 67 return null 68 } 69 } 70 71 export function requireCredentialSecret( 72 credentialId: string | null | undefined, 73 missingMessage = 'Credential secret not found.', 74 ): string { 75 const id = clean(credentialId) 76 if (!id) throw new Error(missingMessage) 77 const credential = loadCredential(id) 78 if (!credential?.encryptedKey) throw new Error(missingMessage) 79 try { 80 return decryptKey(credential.encryptedKey) 81 } catch { 82 throw new Error(missingMessage) 83 } 84 } 85 86 export function createCredentialRecord(input: { 87 provider: string 88 name?: string | null 89 apiKey: string 90 }): CredentialSummary { 91 const provider = clean(input.provider) 92 const apiKey = clean(input.apiKey) 93 if (!provider || !apiKey) { 94 throw new Error('provider and apiKey are required') 95 } 96 const id = `cred_${genId(6)}` 97 const createdAt = Date.now() 98 const credentialName = clean(input.name) || `${provider} key` 99 saveCredential(id, { 100 id, 101 provider, 102 name: credentialName, 103 encryptedKey: encryptKey(apiKey), 104 createdAt, 105 }) 106 return { 107 id, 108 provider, 109 name: credentialName, 110 createdAt, 111 } 112 } 113 114 export function deleteCredentialRecord(id: string): boolean { 115 const credentialId = clean(id) 116 if (!credentialId) return false 117 if (!loadCredential(credentialId)) return false 118 deleteCredential(credentialId) 119 return true 120 }