π File detail
services/api/overageCreditGrant.ts
π― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes OverageCreditGrantInfo, getCachedOverageCreditGrant, invalidateOverageCreditGrantCache, refreshOverageCreditGrantCache, and formatGrantAmount β mainly functions, hooks, or classes. Dependencies touch HTTP client. It composes internal code from constants and utils (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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'
π€ Exports (heuristic)
OverageCreditGrantInfogetCachedOverageCreditGrantinvalidateOverageCreditGrantCacherefreshOverageCreditGrantCacheformatGrantAmount
π External import roots
Package roots from from "β¦" (relative paths omitted).
axios
π₯οΈ Source preview
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 }