syncCache.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. /**
  2. * Eligibility check for remote managed settings.
  3. *
  4. * The cache state itself lives in syncCacheState.ts (a leaf, no auth import).
  5. * This file keeps isRemoteManagedSettingsEligible — the one function that
  6. * needs auth.ts — plus resetSyncCache wrapped to clear the local eligibility
  7. * mirror alongside the leaf's state.
  8. */
  9. import { CLAUDE_AI_INFERENCE_SCOPE } from '../../constants/oauth.js'
  10. import {
  11. getAnthropicApiKeyWithSource,
  12. getClaudeAIOAuthTokens,
  13. } from '../../utils/auth.js'
  14. import {
  15. getAPIProvider,
  16. isFirstPartyAnthropicBaseUrl,
  17. } from '../../utils/model/providers.js'
  18. import {
  19. resetSyncCache as resetLeafCache,
  20. setEligibility,
  21. } from './syncCacheState.js'
  22. let cached: boolean | undefined
  23. export function resetSyncCache(): void {
  24. cached = undefined
  25. resetLeafCache()
  26. }
  27. /**
  28. * Check if the current user is eligible for remote managed settings
  29. *
  30. * Eligibility:
  31. * - Console users (API key): All eligible (must have actual key, not just apiKeyHelper)
  32. * - OAuth users with known subscriptionType: Only Enterprise/C4E and Team
  33. * - OAuth users with subscriptionType === null (externally-injected tokens via
  34. * CLAUDE_CODE_OAUTH_TOKEN / FD, or keychain tokens missing metadata): Eligible —
  35. * the API returns empty settings for ineligible orgs, so the cost of a false
  36. * positive is one round-trip
  37. *
  38. * This is a pre-check to determine if we should query the API.
  39. * The API will return empty settings for users without managed settings.
  40. *
  41. * IMPORTANT: This function must NOT call getSettings() or any function that calls
  42. * getSettings() to avoid circular dependencies during settings loading.
  43. */
  44. export function isRemoteManagedSettingsEligible(): boolean {
  45. if (cached !== undefined) return cached
  46. if (process.env.CLAUDE_CODE_LOCAL_SKIP_REMOTE_PREFETCH === '1') {
  47. return (cached = setEligibility(false))
  48. }
  49. // 3p provider users should not hit the settings endpoint
  50. if (getAPIProvider() !== 'firstParty') {
  51. return (cached = setEligibility(false))
  52. }
  53. // Custom base URL users should not hit the settings endpoint
  54. if (!isFirstPartyAnthropicBaseUrl()) {
  55. return (cached = setEligibility(false))
  56. }
  57. // Cowork runs in a VM with its own permission model; server-managed settings
  58. // (designed for CLI/CCD) don't apply there, and per-surface settings don't
  59. // exist yet. MDM/file-based managed settings still apply via settings.ts —
  60. // those require physical deployment and a different IT intent.
  61. if (process.env.CLAUDE_CODE_ENTRYPOINT === 'local-agent') {
  62. return (cached = setEligibility(false))
  63. }
  64. // Check OAuth first: most Claude.ai users have no API key in the keychain.
  65. // The API key check spawns `security find-generic-password` (~20-50ms) which
  66. // returns null for OAuth-only users. Checking OAuth first short-circuits
  67. // that subprocess for the common case.
  68. const tokens = getClaudeAIOAuthTokens()
  69. // Externally-injected tokens (CCD via CLAUDE_CODE_OAUTH_TOKEN, CCR via
  70. // CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR, Agent SDK, CI) carry no
  71. // subscriptionType metadata — getClaudeAIOAuthTokens() constructs them with
  72. // subscriptionType: null. The token itself is valid; let the API decide.
  73. // fetchRemoteManagedSettings handles 204/404 gracefully (returns {}), and
  74. // settings.ts falls through to MDM/file when remote is empty, so ineligible
  75. // orgs pay one round-trip and nothing else changes.
  76. if (tokens?.accessToken && tokens.subscriptionType === null) {
  77. return (cached = setEligibility(true))
  78. }
  79. if (
  80. tokens?.accessToken &&
  81. tokens.scopes?.includes(CLAUDE_AI_INFERENCE_SCOPE) &&
  82. (tokens.subscriptionType === 'enterprise' ||
  83. tokens.subscriptionType === 'team')
  84. ) {
  85. return (cached = setEligibility(true))
  86. }
  87. // Console users (API key) are eligible if we can get the actual key
  88. // Skip apiKeyHelper to avoid circular dependency with getSettings()
  89. // Wrap in try-catch because getAnthropicApiKeyWithSource throws in CI/test environments
  90. // when no API key is available
  91. try {
  92. const { key: apiKey } = getAnthropicApiKeyWithSource({
  93. skipRetrievingKeyFromApiKeyHelper: true,
  94. })
  95. if (apiKey) {
  96. return (cached = setEligibility(true))
  97. }
  98. } catch {
  99. // No API key available (e.g., CI/test environment)
  100. }
  101. return (cached = setEligibility(false))
  102. }