source dump of claude code
at main 111 lines 4.8 kB view raw
1/** 2 * Lightweight helpers shared between keychainPrefetch.ts and 3 * macOsKeychainStorage.ts. 4 * 5 * This module MUST NOT import execa, execFileNoThrow, or 6 * execFileNoThrowPortable. keychainPrefetch.ts fires at the very top of 7 * main.tsx (before the ~65ms of module evaluation it parallelizes), and Bun's 8 * __esm wrapper evaluates the ENTIRE module when any symbol is accessed — 9 * so a heavy transitive import here defeats the prefetch. The execa → 10 * human-signals → cross-spawn chain alone is ~58ms of synchronous init. 11 * 12 * The imports below (envUtils, oauth constants, crypto, os) are already 13 * evaluated by startupProfiler.ts at main.tsx:5, so they add no module-init 14 * cost when keychainPrefetch.ts pulls this file in. 15 */ 16 17import { createHash } from 'crypto' 18import { userInfo } from 'os' 19import { getOauthConfig } from 'src/constants/oauth.js' 20import { getClaudeConfigHomeDir } from '../envUtils.js' 21import type { SecureStorageData } from './types.js' 22 23// Suffix distinguishing the OAuth credentials keychain entry from the legacy 24// API key entry (which uses no suffix). Both share the service name base. 25// DO NOT change this value — it's part of the keychain lookup key and would 26// orphan existing stored credentials. 27export const CREDENTIALS_SERVICE_SUFFIX = '-credentials' 28 29export function getMacOsKeychainStorageServiceName( 30 serviceSuffix: string = '', 31): string { 32 const configDir = getClaudeConfigHomeDir() 33 const isDefaultDir = !process.env.CLAUDE_CONFIG_DIR 34 35 // Use a hash of the config dir path to create a unique but stable suffix 36 // Only add suffix for non-default directories to maintain backwards compatibility 37 const dirHash = isDefaultDir 38 ? '' 39 : `-${createHash('sha256').update(configDir).digest('hex').substring(0, 8)}` 40 return `Claude Code${getOauthConfig().OAUTH_FILE_SUFFIX}${serviceSuffix}${dirHash}` 41} 42 43export function getUsername(): string { 44 try { 45 return process.env.USER || userInfo().username 46 } catch { 47 return 'claude-code-user' 48 } 49} 50 51// -- 52 53// Cache for keychain reads to avoid repeated expensive security CLI calls. 54// TTL bounds staleness for cross-process scenarios (another CC instance 55// refreshing/invalidating tokens) without forcing a blocking spawnSync on 56// every read. In-process writes invalidate via clearKeychainCache() directly. 57// 58// The sync read() path takes ~500ms per `security` spawn. With 50+ claude.ai 59// MCP connectors authenticating at startup, a short TTL expires mid-storm and 60// triggers repeat sync reads — observed as a 5.5s event-loop stall 61// (go/ccshare/adamj-20260326-212235). 30s of cross-process staleness is fine: 62// OAuth tokens expire in hours, and the only cross-process writer is another 63// CC instance's /login or refresh. 64// 65// Lives here (not in macOsKeychainStorage.ts) so keychainPrefetch.ts can 66// prime it without pulling in execa. Wrapped in an object because ES module 67// `let` bindings aren't writable across module boundaries — both this file 68// and macOsKeychainStorage.ts need to mutate all three fields. 69export const KEYCHAIN_CACHE_TTL_MS = 30_000 70 71export const keychainCacheState: { 72 cache: { data: SecureStorageData | null; cachedAt: number } // cachedAt 0 = invalid 73 // Incremented on every cache invalidation. readAsync() captures this before 74 // spawning and skips its cache write if a newer generation exists, preventing 75 // a stale subprocess result from overwriting fresh data written by update(). 76 generation: number 77 // Deduplicates concurrent readAsync() calls so TTL expiry under load spawns 78 // one subprocess, not N. Cleared on invalidation so fresh reads don't join 79 // a stale in-flight promise. 80 readInFlight: Promise<SecureStorageData | null> | null 81} = { 82 cache: { data: null, cachedAt: 0 }, 83 generation: 0, 84 readInFlight: null, 85} 86 87export function clearKeychainCache(): void { 88 keychainCacheState.cache = { data: null, cachedAt: 0 } 89 keychainCacheState.generation++ 90 keychainCacheState.readInFlight = null 91} 92 93/** 94 * Prime the keychain cache from a prefetch result (keychainPrefetch.ts). 95 * Only writes if the cache hasn't been touched yet — if sync read() or 96 * update() already ran, their result is authoritative and we discard this. 97 */ 98export function primeKeychainCacheFromPrefetch(stdout: string | null): void { 99 if (keychainCacheState.cache.cachedAt !== 0) return 100 let data: SecureStorageData | null = null 101 if (stdout) { 102 try { 103 // eslint-disable-next-line custom-rules/no-direct-json-operations -- jsonParse() pulls slowOperations (lodash-es/cloneDeep) into the early-startup import chain; see file header 104 data = JSON.parse(stdout) 105 } catch { 106 // malformed prefetch result — let sync read() re-fetch 107 return 108 } 109 } 110 keychainCacheState.cache = { data, cachedAt: Date.now() } 111}