init claude-code
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import axios from 'axios'
|
||||
import { getOauthConfig } from '../../constants/oauth.js'
|
||||
import { getOauthAccountInfo } from '../../utils/auth.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { isEssentialTrafficOnly } from '../../utils/privacyLevel.js'
|
||||
import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js'
|
||||
|
||||
export type OverageCreditGrantInfo = {
|
||||
available: boolean
|
||||
eligible: boolean
|
||||
granted: boolean
|
||||
amount_minor_units: number | null
|
||||
currency: string | null
|
||||
}
|
||||
|
||||
type CachedGrantEntry = {
|
||||
info: OverageCreditGrantInfo
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
|
||||
|
||||
/**
|
||||
* Fetch the current user's overage credit grant eligibility from the backend.
|
||||
* The backend resolves tier-specific amounts and role-based claim permission,
|
||||
* so the CLI just reads the response without replicating that logic.
|
||||
*/
|
||||
async function fetchOverageCreditGrant(): Promise<OverageCreditGrantInfo | null> {
|
||||
try {
|
||||
const { accessToken, orgUUID } = await prepareApiRequest()
|
||||
const url = `${getOauthConfig().BASE_API_URL}/api/oauth/organizations/${orgUUID}/overage_credit_grant`
|
||||
const response = await axios.get<OverageCreditGrantInfo>(url, {
|
||||
headers: getOAuthHeaders(accessToken),
|
||||
})
|
||||
return response.data
|
||||
} catch (err) {
|
||||
logError(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached grant info. Returns null if no cache or cache is stale.
|
||||
* Callers should render nothing (not block) when this returns null —
|
||||
* refreshOverageCreditGrantCache fires lazily to populate it.
|
||||
*/
|
||||
export function getCachedOverageCreditGrant(): OverageCreditGrantInfo | null {
|
||||
const orgId = getOauthAccountInfo()?.organizationUuid
|
||||
if (!orgId) return null
|
||||
const cached = getGlobalConfig().overageCreditGrantCache?.[orgId]
|
||||
if (!cached) return null
|
||||
if (Date.now() - cached.timestamp > CACHE_TTL_MS) return null
|
||||
return cached.info
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the current org's cached entry so the next read refetches.
|
||||
* Leaves other orgs' entries intact.
|
||||
*/
|
||||
export function invalidateOverageCreditGrantCache(): void {
|
||||
const orgId = getOauthAccountInfo()?.organizationUuid
|
||||
if (!orgId) return
|
||||
const cache = getGlobalConfig().overageCreditGrantCache
|
||||
if (!cache || !(orgId in cache)) return
|
||||
saveGlobalConfig(prev => {
|
||||
const next = { ...prev.overageCreditGrantCache }
|
||||
delete next[orgId]
|
||||
return { ...prev, overageCreditGrantCache: next }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and cache grant info. Fire-and-forget; call when an upsell surface
|
||||
* is about to render and the cache is empty.
|
||||
*/
|
||||
export async function refreshOverageCreditGrantCache(): Promise<void> {
|
||||
if (isEssentialTrafficOnly()) return
|
||||
const orgId = getOauthAccountInfo()?.organizationUuid
|
||||
if (!orgId) return
|
||||
const info = await fetchOverageCreditGrant()
|
||||
if (!info) return
|
||||
// Skip rewriting info if grant data is unchanged — avoids config write
|
||||
// amplification (inc-4552 pattern). Still refresh the timestamp so the
|
||||
// TTL-based staleness check in getCachedOverageCreditGrant doesn't keep
|
||||
// re-triggering API calls on every component mount.
|
||||
saveGlobalConfig(prev => {
|
||||
// Derive from prev (lock-fresh) rather than a pre-lock getGlobalConfig()
|
||||
// read — saveConfigWithLock re-reads config from disk under the file lock,
|
||||
// so another CLI instance may have written between any outer read and lock
|
||||
// acquire.
|
||||
const prevCached = prev.overageCreditGrantCache?.[orgId]
|
||||
const existing = prevCached?.info
|
||||
const dataUnchanged =
|
||||
existing &&
|
||||
existing.available === info.available &&
|
||||
existing.eligible === info.eligible &&
|
||||
existing.granted === info.granted &&
|
||||
existing.amount_minor_units === info.amount_minor_units &&
|
||||
existing.currency === info.currency
|
||||
// When data is unchanged and timestamp is still fresh, skip the write entirely
|
||||
if (
|
||||
dataUnchanged &&
|
||||
prevCached &&
|
||||
Date.now() - prevCached.timestamp <= CACHE_TTL_MS
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
const entry: CachedGrantEntry = {
|
||||
info: dataUnchanged ? existing : info,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
overageCreditGrantCache: {
|
||||
...prev.overageCreditGrantCache,
|
||||
[orgId]: entry,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the grant amount for display. Returns null if amount isn't available
|
||||
* (not eligible, or currency we don't know how to format).
|
||||
*/
|
||||
export function formatGrantAmount(info: OverageCreditGrantInfo): string | null {
|
||||
if (info.amount_minor_units == null || !info.currency) return null
|
||||
// For now only USD; backend may expand later
|
||||
if (info.currency.toUpperCase() === 'USD') {
|
||||
const dollars = info.amount_minor_units / 100
|
||||
return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export type { CachedGrantEntry as OverageCreditGrantCacheEntry }
|
||||
Reference in New Issue
Block a user