codeburn/app/electron/preload.ts
iamtoruk 880f84d593 feat(app): live quota fetch module + getQuota bridge (Phase 4 backend)
Port the menu bar's OAuth quota fetch to the Electron main:
- app/electron/quota/{claude,codex,index,security}.ts reads the local Claude/
  Codex credentials, hits the usage APIs, normalizes to QuotaProvider (5h/weekly
  windows, auto-detected tier, resets-at) with 429 backoff + single-flight
- getQuota IPC bridge (main + preload + renderer types); needs a full relaunch

Security: tokens are never returned or logged (redact Bearer/sk-ant/sk-/JWT,
strip NUL, truncate 240); credential reads refuse symlinks (O_NOFOLLOW/lstat) and
insecure modes; Anthropic refresh is never POSTed (single-use, shared with CLI).

typecheck clean; 147/147 tests pass; Electron build passes.
2026-07-12 16:43:53 -07:00

52 lines
3.3 KiB
TypeScript

import { contextBridge, ipcRenderer } from 'electron'
// Handlers resolve with { ok, value } | { ok, error } so the structured error
// `kind` survives the contextBridge boundary. `import type` is erased at build,
// so this shares main.ts's declaration without pulling its runtime in.
import type { Envelope } from './main'
type DateRange = { from: string; to: string }
async function invoke<T>(channel: string, ...args: unknown[]): Promise<T> {
const res = (await ipcRenderer.invoke(channel, ...args)) as Envelope<T>
if (res.ok) return res.value
// Reject with a plain object so `kind` is preserved (Error subclasses lose
// custom fields when cloned across worlds).
return Promise.reject(res.error)
}
// Shape matches CodeburnBridge (app/renderer/lib/types.ts); typing is enforced
// renderer-side where `window.codeburn` is declared as CodeburnBridge.
const bridge = {
getQuota: () => invoke('codeburn:getQuota'),
getOverview: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getOverview', period, provider, range),
getPlans: (period: string) => invoke('codeburn:getPlans', period),
getActReport: () => invoke('codeburn:getActReport'),
getModels: (period: string, provider: string, byTask: boolean, range?: DateRange) => invoke('codeburn:getModels', period, provider, byTask, range),
getSessions: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getSessions', period, provider, range),
getCompareModels: (period: string, provider: string) => invoke('codeburn:getCompareModels', period, provider),
getCompare: (period: string, provider: string, modelA: string, modelB: string) => invoke('codeburn:getCompare', period, provider, modelA, modelB),
getYield: (period: string, range?: DateRange) => invoke('codeburn:getYield', period, range),
getSpendFlow: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getSpendFlow', period, provider, range),
getOptimizeReport: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getOptimizeReport', period, provider, range),
getDevices: (period: string) => invoke('codeburn:getDevices', period),
getDevicesScan: () => invoke('codeburn:getDevicesScan'),
getShareStatus: () => invoke('codeburn:getShareStatus'),
getIdentity: () => invoke('codeburn:getIdentity'),
getAliases: () => invoke('codeburn:getAliases'),
getProxyPaths: () => invoke('codeburn:getProxyPaths'),
setCurrency: (code: string) => invoke('codeburn:setCurrency', code),
resetCurrency: () => invoke('codeburn:resetCurrency'),
addAlias: (from: string, to: string) => invoke('codeburn:addAlias', from, to),
removeAlias: (from: string) => invoke('codeburn:removeAlias', from),
removeDevice: (name: string) => invoke('codeburn:removeDevice', name),
setPlan: (id: string, provider: string) => invoke('codeburn:setPlan', id, provider),
resetPlan: (provider: string) => invoke('codeburn:resetPlan', provider),
exportData: (format: string, provider: string, outPath: string) => invoke('codeburn:exportData', format, provider, outPath),
chooseDirectory: () => invoke('codeburn:chooseDirectory'),
cliStatus: () => invoke('codeburn:cliStatus'),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
platform: process.platform,
}
contextBridge.exposeInMainWorld('codeburn', bridge)