mirror of
https://github.com/jarmuine/claude-code.git
synced 2026-04-26 06:41:17 +03:00
Squash the current repository state back into one baseline commit while preserving the README reframing and repository contents. Constraint: User explicitly requested a single squashed commit with subject "asdf" Confidence: high Scope-risk: broad Reversibility: clean Directive: This commit intentionally rewrites published history; coordinate before future force-pushes Tested: git status clean; local history rewritten to one commit; force-pushed main to origin and instructkr Not-tested: Fresh clone verification after push
211 lines
7.6 KiB
TypeScript
211 lines
7.6 KiB
TypeScript
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)}`)
|
|
}
|
|
}
|