1
14
15export const API_KEY_SCOPES = ["READ", "WRITE", "ADMIN"] as const;
16export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
17
18
22export const SCOPE_GRANTS: Record<ApiKeyScope, string> = {
23 READ: "Read workspace content.",
24 WRITE: "Read and create/update content.",
25 ADMIN: "Full access, including admin operations.",
26};
27
28
29export const SCOPE_IMPLIES: Record<ApiKeyScope, ApiKeyScope[]> = {
30 READ: ["READ"],
31 WRITE: ["WRITE", "READ"],
32 ADMIN: ["ADMIN", "WRITE", "READ"],
33};
34
35
40export function minimalScopes(selected: readonly ApiKeyScope[]): ApiKeyScope[] {
41 const set = new Set(selected);
42 return API_KEY_SCOPES.filter((s) => {
43 if (!set.has(s)) return false;
44
45 const impliedByOther = selected.some((o) => o !== s && SCOPE_IMPLIES[o].includes(s));
46 return !impliedByOther;
47 });
48}
49
50
51export function grants(selected: readonly ApiKeyScope[], need: ApiKeyScope): boolean {
52 return selected.some((s) => SCOPE_IMPLIES[s].includes(need));
53}
54
55
61export function curlExample(
62 prefix: string,
63 method: "GET" | "POST" | "PATCH" | "DELETE",
64 url: string,
65 body?: string,
66): string {
67 const lines = [`curl -X ${method} \\`];
68 lines.push(` -H "Authorization: Bearer ${prefix}…(your key)" \\`);
69 if (body) {
70 lines.push(` -H "Content-Type: application/json" \\`);
71 lines.push(` -d '${body}' \\`);
72 }
73 lines.push(` ${url}`);
74 return lines.join("\n");
75}
76
77export type KeyExpiryState = "never" | "active" | "soon" | "expired";
78
79const DAY_MS = 24 * 60 * 60 * 1000;
80
81
86export function expiryState(expiresAt: string | null | undefined, now: number): KeyExpiryState {
87 if (!expiresAt) return "never";
88 const t = new Date(expiresAt).getTime();
89 if (Number.isNaN(t)) return "expired";
90 if (t <= now) return "expired";
91 if (t - now <= 7 * DAY_MS) return "soon";
92 return "active";
93}
94
95
96export function keyStatus(
97 row: { revokedAt: string | null; expiresAt: string | null },
98 now: number,
99): "Active" | "Revoked" | "Expired" {
100 if (row.revokedAt) return "Revoked";
101 if (expiryState(row.expiresAt, now) === "expired") return "Expired";
102 return "Active";
103}
104
105
106export function relativeTime(iso: string | null | undefined, now: number): string {
107 if (!iso) return "never";
108 const t = new Date(iso).getTime();
109 if (Number.isNaN(t)) return "never";
110 const diff = now - t;
111 if (diff < 0) return "just now";
112 if (diff < 45_000) return "just now";
113 const min = Math.floor(diff / 60_000);
114 if (min < 60) return `${min} m ago`;
115 const h = Math.floor(min / 60);
116 if (h < 24) return `${h} h ago`;
117 const d = Math.floor(h / 24);
118 if (d < 30) return `${d} d ago`;
119 const mo = Math.floor(d / 30);
120 if (mo < 12) return `${mo} mo ago`;
121 return `${Math.floor(mo / 12)} y ago`;
122}
123
124
125export function shortDate(iso: string): string {
126 const d = new Date(iso);
127 if (Number.isNaN(d.getTime())) return "—";
128 return d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
129}
130
131
132export const KEY_NAME_MAX = 100;
133export const RATE_LIMIT_MIN = 1;
134export const RATE_LIMIT_MAX = 10_000;
135
136export function expiryBounds(now: number): { min: string; max: string } {
137 const toInput = (ms: number) => new Date(ms).toISOString().slice(0, 16);
138 return { min: toInput(now + 3_600_000), max: toInput(now + 365 * 24 * 3_600_000) };
139}
140