π File detail
bridge/trustedDevice.ts
π― Use case
This file lives under βbridge/β, which covers the bridge between the UI/shell and the agent (IPC, REPL hooks, permissions, session glue). On the API surface it exposes getTrustedDeviceToken, clearTrustedDeviceTokenCache, clearTrustedDeviceToken, and enrollTrustedDevice β mainly functions, hooks, or classes. Dependencies touch HTTP client, lodash-es, and Node OS/process metadata. It composes internal code from constants, services, and utils (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import axios from 'axios' import memoize from 'lodash-es/memoize.js' import { hostname } from 'os' import { getOauthConfig } from '../constants/oauth.js' import {
π€ Exports (heuristic)
getTrustedDeviceTokenclearTrustedDeviceTokenCacheclearTrustedDeviceTokenenrollTrustedDevice
π External import roots
Package roots from from "β¦" (relative paths omitted).
axioslodash-esos
π₯οΈ Source preview
import axios from 'axios'
import memoize from 'lodash-es/memoize.js'
import { hostname } from 'os'
import { getOauthConfig } from '../constants/oauth.js'
import {
checkGate_CACHED_OR_BLOCKING,
getFeatureValue_CACHED_MAY_BE_STALE,
} from '../services/analytics/growthbook.js'
import { logForDebugging } from '../utils/debug.js'
import { errorMessage } from '../utils/errors.js'
import { isEssentialTrafficOnly } from '../utils/privacyLevel.js'
import { getSecureStorage } from '../utils/secureStorage/index.js'
import { jsonStringify } from '../utils/slowOperations.js'
/**
* Trusted device token source for bridge (remote-control) sessions.
*
* Bridge sessions have SecurityTier=ELEVATED on the server (CCR v2).
* The server gates ConnectBridgeWorker on its own flag
* (sessions_elevated_auth_enforcement in Anthropic Main); this CLI-side
* flag controls whether the CLI sends X-Trusted-Device-Token at all.
* Two flags so rollout can be staged: flip CLI-side first (headers
* start flowing, server still no-ops), then flip server-side.
*
* Enrollment (POST /auth/trusted_devices) is gated server-side by
* account_session.created_at < 10min, so it must happen during /login.
* Token is persistent (90d rolling expiry) and stored in keychain.
*
* See anthropics/anthropic#274559 (spec), #310375 (B1b tenant RPCs),
* #295987 (B2 Python routes), #307150 (C1' CCR v2 gate).
*/
const TRUSTED_DEVICE_GATE = 'tengu_sessions_elevated_auth_enforcement'
function isGateEnabled(): boolean {
return getFeatureValue_CACHED_MAY_BE_STALE(TRUSTED_DEVICE_GATE, false)
}
// Memoized β secureStorage.read() spawns a macOS `security` subprocess (~40ms).
// bridgeApi.ts calls this from getHeaders() on every poll/heartbeat/ack.
// Cache cleared after enrollment (below) and on logout (clearAuthRelatedCaches).
//
// Only the storage read is memoized β the GrowthBook gate is checked live so
// that a gate flip after GrowthBook refresh takes effect without a restart.
const readStoredToken = memoize((): string | undefined => {
// Env var takes precedence for testing/canary.
const envToken = process.env.CLAUDE_TRUSTED_DEVICE_TOKEN
if (envToken) {
return envToken
}
return getSecureStorage().read()?.trustedDeviceToken
})
export function getTrustedDeviceToken(): string | undefined {
if (!isGateEnabled()) {
return undefined
}
return readStoredToken()
}
export function clearTrustedDeviceTokenCache(): void {
readStoredToken.cache?.clear?.()
}
/**
* Clear the stored trusted device token from secure storage and the memo cache.
* Called before enrollTrustedDevice() during /login so a stale token from the
* previous account isn't sent as X-Trusted-Device-Token while enrollment is
* in-flight (enrollTrustedDevice is async β bridge API calls between login and
* enrollment completion would otherwise still read the old cached token).
*/
export function clearTrustedDeviceToken(): void {
if (!isGateEnabled()) {
return
}
const secureStorage = getSecureStorage()
try {
const data = secureStorage.read()
if (data?.trustedDeviceToken) {
delete data.trustedDeviceToken
secureStorage.update(data)
}
} catch {
// Best-effort β don't block login if storage is inaccessible
}
readStoredToken.cache?.clear?.()
}
/**
* Enroll this device via POST /auth/trusted_devices and persist the token
* to keychain. Best-effort β logs and returns on failure so callers
* (post-login hooks) don't block the login flow.
*
* The server gates enrollment on account_session.created_at < 10min, so
* this must be called immediately after a fresh /login. Calling it later
* (e.g. lazy enrollment on /bridge 403) will fail with 403 stale_session.
*/
export async function enrollTrustedDevice(): Promise<void> {
try {
// checkGate_CACHED_OR_BLOCKING awaits any in-flight GrowthBook re-init
// (triggered by refreshGrowthBookAfterAuthChange in login.tsx) before
// reading the gate, so we get the post-refresh value.
if (!(await checkGate_CACHED_OR_BLOCKING(TRUSTED_DEVICE_GATE))) {
logForDebugging(
`[trusted-device] Gate ${TRUSTED_DEVICE_GATE} is off, skipping enrollment`,
)
return
}
// If CLAUDE_TRUSTED_DEVICE_TOKEN is set (e.g. by an enterprise wrapper),
// skip enrollment β the env var takes precedence in readStoredToken() so
// any enrolled token would be shadowed and never used.
if (process.env.CLAUDE_TRUSTED_DEVICE_TOKEN) {
logForDebugging(
'[trusted-device] CLAUDE_TRUSTED_DEVICE_TOKEN env var is set, skipping enrollment (env var takes precedence)',
)
return
}
// Lazy require β utils/auth.ts transitively pulls ~1300 modules
// (config β file β permissions β sessionStorage β commands). Daemon callers
// of getTrustedDeviceToken() don't need this; only /login does.
/* eslint-disable @typescript-eslint/no-require-imports */
const { getClaudeAIOAuthTokens } =
require('../utils/auth.js') as typeof import('../utils/auth.js')
/* eslint-enable @typescript-eslint/no-require-imports */
const accessToken = getClaudeAIOAuthTokens()?.accessToken
if (!accessToken) {
logForDebugging('[trusted-device] No OAuth token, skipping enrollment')
return
}
// Always re-enroll on /login β the existing token may belong to a
// different account (account-switch without /logout). Skipping enrollment
// would send the old account's token on the new account's bridge calls.
const secureStorage = getSecureStorage()
if (isEssentialTrafficOnly()) {
logForDebugging(
'[trusted-device] Essential traffic only, skipping enrollment',
)
return
}
const baseUrl = getOauthConfig().BASE_API_URL
let response
try {
response = await axios.post<{
device_token?: string
device_id?: string
}>(
`${baseUrl}/api/auth/trusted_devices`,
{ display_name: `Claude Code on ${hostname()} Β· ${process.platform}` },
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
timeout: 10_000,
validateStatus: s => s < 500,
},
)
} catch (err: unknown) {
logForDebugging(
`[trusted-device] Enrollment request failed: ${errorMessage(err)}`,
)
return
}
if (response.status !== 200 && response.status !== 201) {
logForDebugging(
`[trusted-device] Enrollment failed ${response.status}: ${jsonStringify(response.data).slice(0, 200)}`,
)
return
}
const token = response.data?.device_token
if (!token || typeof token !== 'string') {
logForDebugging(
'[trusted-device] Enrollment response missing device_token field',
)
return
}
try {
const storageData = secureStorage.read()
if (!storageData) {
logForDebugging(
'[trusted-device] Cannot read storage, skipping token persist',
)
return
}
storageData.trustedDeviceToken = token
const result = secureStorage.update(storageData)
if (!result.success) {
logForDebugging(
`[trusted-device] Failed to persist token: ${result.warning ?? 'unknown'}`,
)
return
}
readStoredToken.cache?.clear?.()
logForDebugging(
`[trusted-device] Enrolled device_id=${response.data.device_id ?? 'unknown'}`,
)
} catch (err: unknown) {
logForDebugging(
`[trusted-device] Storage write failed: ${errorMessage(err)}`,
)
}
} catch (err: unknown) {
logForDebugging(`[trusted-device] Enrollment error: ${errorMessage(err)}`)
}
}