import type { ApiToken } from "@/lib/api"; /** * How much of an API key's issued life is left, as one value. * * The list draws expiry as a bar rather than a date, so the calculation behind * it stopped being a formatting detail of one cell and became the only real * logic on the page. It lives here so it can be read in one sitting, and so the * ledger and the posture strip cannot disagree about what "expiring soon" means. */ const DAY_MS = 24 * 60 * 60 * 1000; const SEVEN_DAYS_MS = 7 * DAY_MS; export type LifetimeState = "healthy" | "soon" | "expired" | "eternal"; export type Lifetime = { state: LifetimeState; /** 0–100, the share of the token's issued life still to run. `eternal` is 100. */ remainingPct: number; /** e.g. "64 days left · 12 Nov", "Expired 2 Sep", "No expiry". */ label: string; /** True when the instance cap has tightened since this token was issued. */ outsidePolicy: boolean; }; function shortDate(d: Date) { return d.toLocaleDateString(undefined, { day: "numeric", month: "short", year: d.getFullYear() === new Date().getFullYear() ? undefined : "numeric" }); } /** * `capDays` is the instance's current `api_token_max_days`, 0 when unset. It is * never applied retroactively — a token issued before the cap tightened keeps * working, and `outsidePolicy` is a prompt to rotate rather than a failure of * any kind. Copy built on this flag must not imply the key has stopped working. */ export function keyLifetime(token: ApiToken, capDays = 0, now = Date.now()): Lifetime { const outsidePolicy = capDays > 0 && (!token.expires_at || new Date(token.expires_at).getTime() > now + capDays * DAY_MS); if (!token.expires_at) { // Not "healthy": a key that runs forever is the state the posture strip // counts as a risk, so it gets its own name rather than the good one. return { state: "eternal", remainingPct: 100, label: "No expiry", outsidePolicy }; } const expiresAt = new Date(token.expires_at).getTime(); const issuedAt = new Date(token.created_at).getTime(); const remainingMs = expiresAt - now; if (remainingMs <= 0) { return { state: "expired", remainingPct: 0, label: `Expired ${shortDate(new Date(expiresAt))}`, outsidePolicy }; } // Measured against the token's own issued span, not against the instance // cap: a 30-day key at day 15 is half gone, a 365-day key at day 15 is // barely started, and one bar has to say which. A zero-length span is not // reachable through the UI but is cheap to survive. const span = expiresAt - issuedAt; const rawPct = span > 0 ? (remainingMs / span) * 100 : 100; const remainingPct = Math.min(100, Math.max(0, rawPct)); const days = Math.ceil(remainingMs / DAY_MS); const label = `${days} day${days === 1 ? "" : "s"} left · ${shortDate(new Date(expiresAt))}`; return { state: remainingMs <= SEVEN_DAYS_MS ? "soon" : "healthy", remainingPct, label, outsidePolicy, }; }