All files / src/services authService.ts

40.9% Statements 18/44
38.88% Branches 7/18
25% Functions 2/8
39.02% Lines 16/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101                                                                                                                                        6x 6x 6x 6x       6x 1x   5x           6x 6x       9x 9x 9x 2x 2x 2x 1x          
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { fetchModels, getUserInfo } from '../api/appStoreClient'
import { login } from '../api/authClient'
import { getEnvConfig } from '../api/config'
import { readAccessToken } from '../api/credentials'
 
export class AuthService {
  userInfo: { displayName: string; username: string; avatarUrl: string } | null = null
 
  constructor(private postToWebview: (msg: unknown) => void) {}
 
  async handleLogin(userName: string, password: string, restartAgent: () => void): Promise<void> {
    const { loginUrl, authUrl } = getEnvConfig()
    try {
      const { accessToken, refreshToken } = await login(loginUrl, userName, password)
      writeCredentials(authUrl, accessToken, refreshToken)
      this.userInfo = await getUserInfo(accessToken)
      this.postToWebview({ type: 'loginSuccess', userInfo: this.userInfo })
      restartAgent()
    } catch (err) {
      this.postToWebview({
        type: 'loginError',
        error: err instanceof Error ? err.message : 'Login failed',
      })
    }
  }
 
  handleLogout(): void {
    const { authUrl } = getEnvConfig()
    writeCredentials(authUrl, null, null)
    this.userInfo = null
    this.postToWebview({ type: 'loggedOut' })
  }
 
  async fetchAndSendUserInfo(): Promise<void> {
    const accessToken = readAccessToken()
    if (!accessToken) return
    try {
      this.userInfo = await getUserInfo(accessToken)
      this.postToWebview({ type: 'userInfo', userInfo: this.userInfo })
    } catch {
      // token expired or invalid — silently stay logged out
    }
  }
 
  async fetchAndSendModels(): Promise<void> {
    const accessToken = readAccessToken()
    try {
      const models = await fetchModels(accessToken ?? undefined)
      this.postToWebview({
        type: 'availableModels',
        models: models.map((m) => ({
          name: m.codename,
          displayName: m.displayName,
          vendor: m.vendor,
          ...(m.contextWindow !== undefined && { contextWindow: m.contextWindow }),
          ...(m.tokenScalingFactor !== undefined && { tokenScalingFactor: m.tokenScalingFactor }),
        })),
      })
    } catch {
      // silently ignore — model dropdown will stay empty
    }
  }
}
 
export function writeCredentials(authUrl: string, accessToken: string | null, refreshToken: string | null): void {
  const credPath = path.join(os.homedir(), '.boltzbit', 'credentials.json')
  let store: Record<string, { accessToken?: string; refreshToken?: string; expiresAt?: number }> = {}
  try {
    store = JSON.parse(fs.readFileSync(credPath, 'utf-8'))
  } catch {
    // file doesn't exist yet — start fresh
  }
  if (accessToken === null) {
    delete store[authUrl]
  } else {
    store[authUrl] = {
      accessToken,
      refreshToken: refreshToken ?? undefined,
      expiresAt: decodeJwtExpiry(accessToken),
    }
  }
  fs.mkdirSync(path.dirname(credPath), { recursive: true })
  fs.writeFileSync(credPath, JSON.stringify(store, null, 2))
}
 
export function decodeJwtExpiry(token: string): number | undefined {
  try {
    const payload = token.split('.')[1]
    if (!payload) return undefined
    const json = Buffer.from(payload, 'base64url').toString('utf-8')
    const { exp } = JSON.parse(json) as { exp?: number }
    if (typeof exp !== 'number') return undefined
    return exp * 1000
  } catch {
    return undefined
  }
}