feat: model an api key's remaining lifetime as a single value

This commit is contained in:
2026-09-08 14:03:49 +00:00
parent 0166b17299
commit 5e4c8afdd1
3 changed files with 889 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
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;
/** 0100, 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,
};
}