File size: 4,938 Bytes
4e7af9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
/**
 * CLI Auth Provider REST API
 *
 * GET  /api/cli-auth          — read current CLI auth provider config from ~/.claude.json
 * PUT  /api/cli-auth          — update CLI auth provider config in ~/.claude.json
 * POST /api/cli-auth/invalidate — invalidate the cliProviderModelCache
 */

import { homedir } from 'node:os'
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { ApiError, errorResponse } from '../middleware/errorHandler.js'

type AuthProvider = 'anthropic' | 'openai' | 'openrouter' | 'local' | 'opencode' | 'nvidia'

type CliAuthConfig = {
  authProvider: AuthProvider | null
  nvidiaApiKey?: string
  openRouterApiKey?: string
  openAiApiKey?: string
  openAiAccessToken?: string
  openCodeApiKey?: string
  openCodeModelName?: string
  localBaseUrl?: string
  localModelName?: string
}

function getClaudeJsonPath(): string {
  return join(homedir(), '.claude.json')
}

function readCliAuthConfig(): CliAuthConfig {
  try {
    const raw = readFileSync(getClaudeJsonPath(), 'utf8')
    const config = JSON.parse(raw) as Record<string, unknown>
    return {
      authProvider: (config.authProvider as AuthProvider) || null,
      nvidiaApiKey: config.nvidiaApiKey as string | undefined,
      openRouterApiKey: config.openRouterApiKey as string | undefined,
      openAiApiKey: config.openAiApiKey as string | undefined,
      openAiAccessToken: config.openAiAccessToken as string | undefined,
      openCodeApiKey: config.openCodeApiKey as string | undefined,
      openCodeModelName: config.openCodeModelName as string | undefined,
      localBaseUrl: config.localBaseUrl as string | undefined,
      localModelName: config.localModelName as string | undefined,
    }
  } catch {
    return { authProvider: null }
  }
}

function writeCliAuthConfig(updates: Partial<CliAuthConfig>): void {
  const filePath = getClaudeJsonPath()
  let config: Record<string, unknown> = {}
  try {
    const raw = readFileSync(filePath, 'utf8')
    config = JSON.parse(raw) as Record<string, unknown>
  } catch {
    // file doesn't exist yet, start fresh
  }

  if (updates.authProvider !== undefined) {
    config.authProvider = updates.authProvider || undefined
  }

  if (updates.nvidiaApiKey !== undefined) {
    config.nvidiaApiKey = updates.nvidiaApiKey || undefined
  }

  if (updates.openRouterApiKey !== undefined) {
    config.openRouterApiKey = updates.openRouterApiKey || undefined
  }

  if (updates.openAiApiKey !== undefined) {
    config.openAiApiKey = updates.openAiApiKey || undefined
    config.openAiAccessToken = undefined
    config.openAiRefreshToken = undefined
    config.openAiTokenExpiresAt = undefined
    config.openAiWorkspaceId = undefined
  }

  if (updates.openAiAccessToken !== undefined) {
    config.openAiAccessToken = updates.openAiAccessToken || undefined
    config.openAiApiKey = undefined
    config.openAiRefreshToken = undefined
    config.openAiTokenExpiresAt = undefined
    config.openAiWorkspaceId = undefined
  }

  if (updates.openCodeApiKey !== undefined) {
    config.openCodeApiKey = updates.openCodeApiKey || undefined
  }

  if (updates.openCodeModelName !== undefined) {
    config.openCodeModelName = updates.openCodeModelName || undefined
  }

  if (updates.localBaseUrl !== undefined) {
    config.localBaseUrl = updates.localBaseUrl || undefined
  }

  if (updates.localModelName !== undefined) {
    config.localModelName = updates.localModelName || undefined
  }

  writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n')
}

export async function handleCliAuthApi(
  req: Request,
  _url: URL,
  segments: string[],
): Promise<Response> {
  try {
    const sub = segments[2]

    if (sub === 'invalidate') {
      if (req.method !== 'POST') throw methodNotAllowed(req.method)
      const { invalidateCliProviderModelCache } = await import('./models.js')
      invalidateCliProviderModelCache()
      return Response.json({ ok: true })
    }

    if (!sub) {
      if (req.method === 'GET') {
        const config = readCliAuthConfig()
        return Response.json(config)
      }

      if (req.method === 'PUT') {
        const body = await parseJsonBody(req)
        writeCliAuthConfig(body as Partial<CliAuthConfig>)
        const { invalidateCliProviderModelCache } = await import('./models.js')
        invalidateCliProviderModelCache()
        return Response.json({ ok: true })
      }

      throw methodNotAllowed(req.method)
    }

    throw ApiError.notFound(`Unknown cli-auth endpoint: ${sub}`)
  } catch (error) {
    return errorResponse(error)
  }
}

async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
  try {
    return (await req.json()) as Record<string, unknown>
  } catch {
    throw ApiError.badRequest('Invalid JSON body')
  }
}

function methodNotAllowed(method: string): ApiError {
  return new ApiError(405, `Method ${method} not allowed`, 'METHOD_NOT_ALLOWED')
}