73 lines
3.0 KiB
TypeScript
73 lines
3.0 KiB
TypeScript
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,
|
||
};
|
||
}
|