source dump of claude code
at main 137 lines 4.9 kB view raw
1import axios from 'axios' 2import { getOauthConfig } from '../../constants/oauth.js' 3import { getOauthAccountInfo } from '../../utils/auth.js' 4import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' 5import { logError } from '../../utils/log.js' 6import { isEssentialTrafficOnly } from '../../utils/privacyLevel.js' 7import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js' 8 9export type OverageCreditGrantInfo = { 10 available: boolean 11 eligible: boolean 12 granted: boolean 13 amount_minor_units: number | null 14 currency: string | null 15} 16 17type CachedGrantEntry = { 18 info: OverageCreditGrantInfo 19 timestamp: number 20} 21 22const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour 23 24/** 25 * Fetch the current user's overage credit grant eligibility from the backend. 26 * The backend resolves tier-specific amounts and role-based claim permission, 27 * so the CLI just reads the response without replicating that logic. 28 */ 29async function fetchOverageCreditGrant(): Promise<OverageCreditGrantInfo | null> { 30 try { 31 const { accessToken, orgUUID } = await prepareApiRequest() 32 const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/overage_credit_grant` 33 const response = await axios.get<OverageCreditGrantInfo>(url, { 34 headers: getOAuthHeaders(accessToken), 35 }) 36 return response.data 37 } catch (err) { 38 logError(err) 39 return null 40 } 41} 42 43/** 44 * Get cached grant info. Returns null if no cache or cache is stale. 45 * Callers should render nothing (not block) when this returns null — 46 * refreshOverageCreditGrantCache fires lazily to populate it. 47 */ 48export function getCachedOverageCreditGrant(): OverageCreditGrantInfo | null { 49 const orgId = getOauthAccountInfo()?.organizationUuid 50 if (!orgId) return null 51 const cached = getGlobalConfig().overageCreditGrantCache?.[orgId] 52 if (!cached) return null 53 if (Date.now() - cached.timestamp > CACHE_TTL_MS) return null 54 return cached.info 55} 56 57/** 58 * Drop the current org's cached entry so the next read refetches. 59 * Leaves other orgs' entries intact. 60 */ 61export function invalidateOverageCreditGrantCache(): void { 62 const orgId = getOauthAccountInfo()?.organizationUuid 63 if (!orgId) return 64 const cache = getGlobalConfig().overageCreditGrantCache 65 if (!cache || !(orgId in cache)) return 66 saveGlobalConfig(prev => { 67 const next = { ...prev.overageCreditGrantCache } 68 delete next[orgId] 69 return { ...prev, overageCreditGrantCache: next } 70 }) 71} 72 73/** 74 * Fetch and cache grant info. Fire-and-forget; call when an upsell surface 75 * is about to render and the cache is empty. 76 */ 77export async function refreshOverageCreditGrantCache(): Promise<void> { 78 if (isEssentialTrafficOnly()) return 79 const orgId = getOauthAccountInfo()?.organizationUuid 80 if (!orgId) return 81 const info = await fetchOverageCreditGrant() 82 if (!info) return 83 // Skip rewriting info if grant data is unchanged — avoids config write 84 // amplification (inc-4552 pattern). Still refresh the timestamp so the 85 // TTL-based staleness check in getCachedOverageCreditGrant doesn't keep 86 // re-triggering API calls on every component mount. 87 saveGlobalConfig(prev => { 88 // Derive from prev (lock-fresh) rather than a pre-lock getGlobalConfig() 89 // read — saveConfigWithLock re-reads config from disk under the file lock, 90 // so another CLI instance may have written between any outer read and lock 91 // acquire. 92 const prevCached = prev.overageCreditGrantCache?.[orgId] 93 const existing = prevCached?.info 94 const dataUnchanged = 95 existing && 96 existing.available === info.available && 97 existing.eligible === info.eligible && 98 existing.granted === info.granted && 99 existing.amount_minor_units === info.amount_minor_units && 100 existing.currency === info.currency 101 // When data is unchanged and timestamp is still fresh, skip the write entirely 102 if ( 103 dataUnchanged && 104 prevCached && 105 Date.now() - prevCached.timestamp <= CACHE_TTL_MS 106 ) { 107 return prev 108 } 109 const entry: CachedGrantEntry = { 110 info: dataUnchanged ? existing : info, 111 timestamp: Date.now(), 112 } 113 return { 114 ...prev, 115 overageCreditGrantCache: { 116 ...prev.overageCreditGrantCache, 117 [orgId]: entry, 118 }, 119 } 120 }) 121} 122 123/** 124 * Format the grant amount for display. Returns null if amount isn't available 125 * (not eligible, or currency we don't know how to format). 126 */ 127export function formatGrantAmount(info: OverageCreditGrantInfo): string | null { 128 if (info.amount_minor_units == null || !info.currency) return null 129 // For now only USD; backend may expand later 130 if (info.currency.toUpperCase() === 'USD') { 131 const dollars = info.amount_minor_units / 100 132 return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}` 133 } 134 return null 135} 136 137export type { CachedGrantEntry as OverageCreditGrantCacheEntry }