Files
vantage-app/web/lib/keyLifetime.ts
T

73 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
};
}