/ src / lib / server / credentials / credential-service.ts
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  }