the claude code sourcemaps leaked march 31
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 210 lines 7.8 kB view raw
1import axios from 'axios' 2import memoize from 'lodash-es/memoize.js' 3import { hostname } from 'os' 4import { getOauthConfig } from '../constants/oauth.js' 5import { 6 checkGate_CACHED_OR_BLOCKING, 7 getFeatureValue_CACHED_MAY_BE_STALE, 8} from '../services/analytics/growthbook.js' 9import { logForDebugging } from '../utils/debug.js' 10import { errorMessage } from '../utils/errors.js' 11import { isEssentialTrafficOnly } from '../utils/privacyLevel.js' 12import { getSecureStorage } from '../utils/secureStorage/index.js' 13import { jsonStringify } from '../utils/slowOperations.js' 14 15/** 16 * Trusted device token source for bridge (remote-control) sessions. 17 * 18 * Bridge sessions have SecurityTier=ELEVATED on the server (CCR v2). 19 * The server gates ConnectBridgeWorker on its own flag 20 * (sessions_elevated_auth_enforcement in Anthropic Main); this CLI-side 21 * flag controls whether the CLI sends X-Trusted-Device-Token at all. 22 * Two flags so rollout can be staged: flip CLI-side first (headers 23 * start flowing, server still no-ops), then flip server-side. 24 * 25 * Enrollment (POST /auth/trusted_devices) is gated server-side by 26 * account_session.created_at < 10min, so it must happen during /login. 27 * Token is persistent (90d rolling expiry) and stored in keychain. 28 * 29 * See anthropics/anthropic#274559 (spec), #310375 (B1b tenant RPCs), 30 * #295987 (B2 Python routes), #307150 (C1' CCR v2 gate). 31 */ 32 33const TRUSTED_DEVICE_GATE = 'tengu_sessions_elevated_auth_enforcement' 34 35function isGateEnabled(): boolean { 36 return getFeatureValue_CACHED_MAY_BE_STALE(TRUSTED_DEVICE_GATE, false) 37} 38 39// Memoized — secureStorage.read() spawns a macOS `security` subprocess (~40ms). 40// bridgeApi.ts calls this from getHeaders() on every poll/heartbeat/ack. 41// Cache cleared after enrollment (below) and on logout (clearAuthRelatedCaches). 42// 43// Only the storage read is memoized — the GrowthBook gate is checked live so 44// that a gate flip after GrowthBook refresh takes effect without a restart. 45const readStoredToken = memoize((): string | undefined => { 46 // Env var takes precedence for testing/canary. 47 const envToken = process.env.CLAUDE_TRUSTED_DEVICE_TOKEN 48 if (envToken) { 49 return envToken 50 } 51 return getSecureStorage().read()?.trustedDeviceToken 52}) 53 54export function getTrustedDeviceToken(): string | undefined { 55 if (!isGateEnabled()) { 56 return undefined 57 } 58 return readStoredToken() 59} 60 61export function clearTrustedDeviceTokenCache(): void { 62 readStoredToken.cache?.clear?.() 63} 64 65/** 66 * Clear the stored trusted device token from secure storage and the memo cache. 67 * Called before enrollTrustedDevice() during /login so a stale token from the 68 * previous account isn't sent as X-Trusted-Device-Token while enrollment is 69 * in-flight (enrollTrustedDevice is async — bridge API calls between login and 70 * enrollment completion would otherwise still read the old cached token). 71 */ 72export function clearTrustedDeviceToken(): void { 73 if (!isGateEnabled()) { 74 return 75 } 76 const secureStorage = getSecureStorage() 77 try { 78 const data = secureStorage.read() 79 if (data?.trustedDeviceToken) { 80 delete data.trustedDeviceToken 81 secureStorage.update(data) 82 } 83 } catch { 84 // Best-effort — don't block login if storage is inaccessible 85 } 86 readStoredToken.cache?.clear?.() 87} 88 89/** 90 * Enroll this device via POST /auth/trusted_devices and persist the token 91 * to keychain. Best-effort — logs and returns on failure so callers 92 * (post-login hooks) don't block the login flow. 93 * 94 * The server gates enrollment on account_session.created_at < 10min, so 95 * this must be called immediately after a fresh /login. Calling it later 96 * (e.g. lazy enrollment on /bridge 403) will fail with 403 stale_session. 97 */ 98export async function enrollTrustedDevice(): Promise<void> { 99 try { 100 // checkGate_CACHED_OR_BLOCKING awaits any in-flight GrowthBook re-init 101 // (triggered by refreshGrowthBookAfterAuthChange in login.tsx) before 102 // reading the gate, so we get the post-refresh value. 103 if (!(await checkGate_CACHED_OR_BLOCKING(TRUSTED_DEVICE_GATE))) { 104 logForDebugging( 105 `[trusted-device] Gate ${TRUSTED_DEVICE_GATE} is off, skipping enrollment`, 106 ) 107 return 108 } 109 // If CLAUDE_TRUSTED_DEVICE_TOKEN is set (e.g. by an enterprise wrapper), 110 // skip enrollment — the env var takes precedence in readStoredToken() so 111 // any enrolled token would be shadowed and never used. 112 if (process.env.CLAUDE_TRUSTED_DEVICE_TOKEN) { 113 logForDebugging( 114 '[trusted-device] CLAUDE_TRUSTED_DEVICE_TOKEN env var is set, skipping enrollment (env var takes precedence)', 115 ) 116 return 117 } 118 // Lazy require — utils/auth.ts transitively pulls ~1300 modules 119 // (config → file → permissions → sessionStorage → commands). Daemon callers 120 // of getTrustedDeviceToken() don't need this; only /login does. 121 /* eslint-disable @typescript-eslint/no-require-imports */ 122 const { getClaudeAIOAuthTokens } = 123 require('../utils/auth.js') as typeof import('../utils/auth.js') 124 /* eslint-enable @typescript-eslint/no-require-imports */ 125 const accessToken = getClaudeAIOAuthTokens()?.accessToken 126 if (!accessToken) { 127 logForDebugging('[trusted-device] No OAuth token, skipping enrollment') 128 return 129 } 130 // Always re-enroll on /login — the existing token may belong to a 131 // different account (account-switch without /logout). Skipping enrollment 132 // would send the old account's token on the new account's bridge calls. 133 const secureStorage = getSecureStorage() 134 135 if (isEssentialTrafficOnly()) { 136 logForDebugging( 137 '[trusted-device] Essential traffic only, skipping enrollment', 138 ) 139 return 140 } 141 142 const baseUrl = getOauthConfig().BASE_API_URL 143 let response 144 try { 145 response = await axios.post<{ 146 device_token?: string 147 device_id?: string 148 }>( 149 `${baseUrl}/api/auth/trusted_devices`, 150 { display_name: `Claude Code on ${hostname()} · ${process.platform}` }, 151 { 152 headers: { 153 Authorization: `Bearer ${accessToken}`, 154 'Content-Type': 'application/json', 155 }, 156 timeout: 10_000, 157 validateStatus: s => s < 500, 158 }, 159 ) 160 } catch (err: unknown) { 161 logForDebugging( 162 `[trusted-device] Enrollment request failed: ${errorMessage(err)}`, 163 ) 164 return 165 } 166 167 if (response.status !== 200 && response.status !== 201) { 168 logForDebugging( 169 `[trusted-device] Enrollment failed ${response.status}: ${jsonStringify(response.data).slice(0, 200)}`, 170 ) 171 return 172 } 173 174 const token = response.data?.device_token 175 if (!token || typeof token !== 'string') { 176 logForDebugging( 177 '[trusted-device] Enrollment response missing device_token field', 178 ) 179 return 180 } 181 182 try { 183 const storageData = secureStorage.read() 184 if (!storageData) { 185 logForDebugging( 186 '[trusted-device] Cannot read storage, skipping token persist', 187 ) 188 return 189 } 190 storageData.trustedDeviceToken = token 191 const result = secureStorage.update(storageData) 192 if (!result.success) { 193 logForDebugging( 194 `[trusted-device] Failed to persist token: ${result.warning ?? 'unknown'}`, 195 ) 196 return 197 } 198 readStoredToken.cache?.clear?.() 199 logForDebugging( 200 `[trusted-device] Enrolled device_id=${response.data.device_id ?? 'unknown'}`, 201 ) 202 } catch (err: unknown) { 203 logForDebugging( 204 `[trusted-device] Storage write failed: ${errorMessage(err)}`, 205 ) 206 } 207 } catch (err: unknown) { 208 logForDebugging(`[trusted-device] Enrollment error: ${errorMessage(err)}`) 209 } 210}