/ utils / sessionIngressAuth.ts
sessionIngressAuth.ts
  1  import {
  2    getSessionIngressToken,
  3    setSessionIngressToken,
  4  } from '../bootstrap/state.js'
  5  import {
  6    CCR_SESSION_INGRESS_TOKEN_PATH,
  7    maybePersistTokenForSubprocesses,
  8    readTokenFromWellKnownFile,
  9  } from './authFileDescriptor.js'
 10  import { logForDebugging } from './debug.js'
 11  import { errorMessage } from './errors.js'
 12  import { getFsImplementation } from './fsOperations.js'
 13  
 14  /**
 15   * Read token via file descriptor, falling back to well-known file.
 16   * Uses global state to cache the result since file descriptors can only be read once.
 17   */
 18  function getTokenFromFileDescriptor(): string | null {
 19    // Check if we've already attempted to read the token
 20    const cachedToken = getSessionIngressToken()
 21    if (cachedToken !== undefined) {
 22      return cachedToken
 23    }
 24  
 25    const fdEnv = process.env.CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR
 26    if (!fdEnv) {
 27      // No FD env var — either we're not in CCR, or we're a subprocess whose
 28      // parent stripped the (useless) FD env var. Try the well-known file.
 29      const path =
 30        process.env.CLAUDE_SESSION_INGRESS_TOKEN_FILE ??
 31        CCR_SESSION_INGRESS_TOKEN_PATH
 32      const fromFile = readTokenFromWellKnownFile(path, 'session ingress token')
 33      setSessionIngressToken(fromFile)
 34      return fromFile
 35    }
 36  
 37    const fd = parseInt(fdEnv, 10)
 38    if (Number.isNaN(fd)) {
 39      logForDebugging(
 40        `CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR must be a valid file descriptor number, got: ${fdEnv}`,
 41        { level: 'error' },
 42      )
 43      setSessionIngressToken(null)
 44      return null
 45    }
 46  
 47    try {
 48      // Read from the file descriptor
 49      // Use /dev/fd on macOS/BSD, /proc/self/fd on Linux
 50      const fsOps = getFsImplementation()
 51      const fdPath =
 52        process.platform === 'darwin' || process.platform === 'freebsd'
 53          ? `/dev/fd/${fd}`
 54          : `/proc/self/fd/${fd}`
 55  
 56      const token = fsOps.readFileSync(fdPath, { encoding: 'utf8' }).trim()
 57      if (!token) {
 58        logForDebugging('File descriptor contained empty token', {
 59          level: 'error',
 60        })
 61        setSessionIngressToken(null)
 62        return null
 63      }
 64      logForDebugging(`Successfully read token from file descriptor ${fd}`)
 65      setSessionIngressToken(token)
 66      maybePersistTokenForSubprocesses(
 67        CCR_SESSION_INGRESS_TOKEN_PATH,
 68        token,
 69        'session ingress token',
 70      )
 71      return token
 72    } catch (error) {
 73      logForDebugging(
 74        `Failed to read token from file descriptor ${fd}: ${errorMessage(error)}`,
 75        { level: 'error' },
 76      )
 77      // FD env var was set but read failed — typically a subprocess that
 78      // inherited the env var but not the FD (ENXIO). Try the well-known file.
 79      const path =
 80        process.env.CLAUDE_SESSION_INGRESS_TOKEN_FILE ??
 81        CCR_SESSION_INGRESS_TOKEN_PATH
 82      const fromFile = readTokenFromWellKnownFile(path, 'session ingress token')
 83      setSessionIngressToken(fromFile)
 84      return fromFile
 85    }
 86  }
 87  
 88  /**
 89   * Get session ingress authentication token.
 90   *
 91   * Priority order:
 92   *  1. Environment variable (CLAUDE_CODE_SESSION_ACCESS_TOKEN) — set at spawn time,
 93   *     updated in-process via updateSessionIngressAuthToken or
 94   *     update_environment_variables stdin message from the parent bridge process.
 95   *  2. File descriptor (legacy path) — CLAUDE_CODE_WEBSOCKET_AUTH_FILE_DESCRIPTOR,
 96   *     read once and cached.
 97   *  3. Well-known file — CLAUDE_SESSION_INGRESS_TOKEN_FILE env var path, or
 98   *     /home/claude/.claude/remote/.session_ingress_token. Covers subprocesses
 99   *     that can't inherit the FD.
100   */
101  export function getSessionIngressAuthToken(): string | null {
102    // 1. Check environment variable
103    const envToken = process.env.CLAUDE_CODE_SESSION_ACCESS_TOKEN
104    if (envToken) {
105      return envToken
106    }
107  
108    // 2. Check file descriptor (legacy path), with file fallback
109    return getTokenFromFileDescriptor()
110  }
111  
112  /**
113   * Build auth headers for the current session token.
114   * Session keys (sk-ant-sid) use Cookie auth + X-Organization-Uuid;
115   * JWTs use Bearer auth.
116   */
117  export function getSessionIngressAuthHeaders(): Record<string, string> {
118    const token = getSessionIngressAuthToken()
119    if (!token) return {}
120    if (token.startsWith('sk-ant-sid')) {
121      const headers: Record<string, string> = {
122        Cookie: `sessionKey=${token}`,
123      }
124      const orgUuid = process.env.CLAUDE_CODE_ORGANIZATION_UUID
125      if (orgUuid) {
126        headers['X-Organization-Uuid'] = orgUuid
127      }
128      return headers
129    }
130    return { Authorization: `Bearer ${token}` }
131  }
132  
133  /**
134   * Update the session ingress auth token in-process by setting the env var.
135   * Used by the REPL bridge to inject a fresh token after reconnection
136   * without restarting the process.
137   */
138  export function updateSessionIngressAuthToken(token: string): void {
139    process.env.CLAUDE_CODE_SESSION_ACCESS_TOKEN = token
140  }