()
| 96 | * (e.g. lazy enrollment on /bridge 403) will fail with 403 stale_session. |
| 97 | */ |
| 98 | export 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 | }, |
no test coverage detected