/ src / hooks / useApiKeyVerification.ts
useApiKeyVerification.ts
 1  import { useCallback, useState } from 'react'
 2  import { getIsNonInteractiveSession } from '../bootstrap/state.js'
 3  import { verifyApiKey } from '../services/api/claude.js'
 4  import {
 5    getAnthropicApiKeyWithSource,
 6    getApiKeyFromApiKeyHelper,
 7    isAnthropicAuthEnabled,
 8    isClaudeAISubscriber,
 9  } from '../utils/auth.js'
10  
11  export type VerificationStatus =
12    | 'loading'
13    | 'valid'
14    | 'invalid'
15    | 'missing'
16    | 'error'
17  
18  export type ApiKeyVerificationResult = {
19    status: VerificationStatus
20    reverify: () => Promise<void>
21    error: Error | null
22  }
23  
24  export function useApiKeyVerification(): ApiKeyVerificationResult {
25    const [status, setStatus] = useState<VerificationStatus>(() => {
26      if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
27        return 'valid'
28      }
29      // Use skipRetrievingKeyFromApiKeyHelper to avoid executing apiKeyHelper
30      // before trust dialog is shown (security: prevents RCE via settings.json)
31      const { key, source } = getAnthropicApiKeyWithSource({
32        skipRetrievingKeyFromApiKeyHelper: true,
33      })
34      // If apiKeyHelper is configured, we have a key source even though we
35      // haven't executed it yet - return 'loading' to indicate we'll verify later
36      if (key || source === 'apiKeyHelper') {
37        return 'loading'
38      }
39      return 'missing'
40    })
41    const [error, setError] = useState<Error | null>(null)
42  
43    const verify = useCallback(async (): Promise<void> => {
44      if (!isAnthropicAuthEnabled() || isClaudeAISubscriber()) {
45        setStatus('valid')
46        return
47      }
48      // Warm the apiKeyHelper cache (no-op if not configured), then read from
49      // all sources. getAnthropicApiKeyWithSource() reads the now-warm cache.
50      await getApiKeyFromApiKeyHelper(getIsNonInteractiveSession())
51      const { key: apiKey, source } = getAnthropicApiKeyWithSource()
52      if (!apiKey) {
53        if (source === 'apiKeyHelper') {
54          setStatus('error')
55          setError(new Error('API key helper did not return a valid key'))
56          return
57        }
58        const newStatus = 'missing'
59        setStatus(newStatus)
60        return
61      }
62  
63      try {
64        const isValid = await verifyApiKey(apiKey, false)
65        const newStatus = isValid ? 'valid' : 'invalid'
66        setStatus(newStatus)
67        return
68      } catch (error) {
69        // This happens when there an error response from the API but it's not an invalid API key error
70        // In this case, we still mark the API key as invalid - but we also log the error so we can
71        // display it to the user to be more helpful
72        setError(error as Error)
73        const newStatus = 'error'
74        setStatus(newStatus)
75        return
76      }
77    }, [])
78  
79    return {
80      status,
81      reverify: verify,
82      error,
83    }
84  }