Openstatus
www.openstatus.dev
1import { eq } from "@openstatus/db";
2import { db } from "@openstatus/db";
3import { apiKey } from "@openstatus/db/src/schema";
4import {
5 shouldUpdateLastUsed as checkShouldUpdateLastUsed,
6 generateApiKey as generateKey,
7 verifyApiKeyHash,
8} from "@openstatus/db/src/utils/api-key";
9
10/**
11 * Creates a new API key for a workspace
12 * @param workspaceId - The workspace ID
13 * @param createdById - The ID of the user creating the key
14 * @param name - The name of the API key
15 * @param description - Optional description for the key
16 * @param expiresAt - Optional expiration date
17 * @returns The full token (only shown once) and the created key details
18 */
19export async function createApiKey(
20 workspaceId: number,
21 createdById: number,
22 name: string,
23 description?: string,
24 expiresAt?: Date,
25): Promise<{ token: string; key: typeof apiKey.$inferSelect }> {
26 const { token, prefix, hash } = await generateKey();
27
28 const [key] = await db
29 .insert(apiKey)
30 .values({
31 name,
32 description,
33 prefix,
34 hashedToken: hash,
35 workspaceId,
36 createdById,
37 expiresAt,
38 })
39 .returning();
40
41 if (!key) {
42 throw new Error("Failed to create API key");
43 }
44
45 return { token, key };
46}
47
48/**
49 * Verifies an API key token
50 * @param token - The API key token to verify
51 * @returns The API key details if valid, null otherwise
52 */
53export async function verifyApiKey(
54 token: string,
55): Promise<typeof apiKey.$inferSelect | null> {
56 // Validate token format before database query
57 if (!/^os_[a-f0-9]{32}$/.test(token)) {
58 return null;
59 }
60
61 // Extract prefix from token
62 const prefix = token.slice(0, 11); // "os_" + 8 chars = 11 total
63
64 // Look up key by prefix
65 const key = await db
66 .select()
67 .from(apiKey)
68 .where(eq(apiKey.prefix, prefix))
69 .get();
70
71 if (!key) {
72 return null;
73 }
74
75 // Verify hash using bcrypt-compatible verification
76 if (!(await verifyApiKeyHash(token, key.hashedToken))) {
77 return null;
78 }
79
80 // Check expiration
81 if (key.expiresAt && key.expiresAt < new Date()) {
82 return null;
83 }
84
85 return key;
86}
87
88/**
89 * Revokes (deletes) an API key
90 * @param id - The API key ID
91 * @param workspaceId - The workspace ID for ownership verification
92 * @returns True if successfully revoked, false otherwise
93 */
94export async function revokeApiKey(
95 id: number,
96 workspaceId: number,
97): Promise<boolean> {
98 // First, verify the key exists and belongs to the workspace
99 const key = await db.select().from(apiKey).where(eq(apiKey.id, id)).get();
100
101 if (!key || key.workspaceId !== workspaceId) {
102 return false;
103 }
104
105 // Delete the key
106 await db.delete(apiKey).where(eq(apiKey.id, id));
107
108 return true;
109}
110
111/**
112 * Gets all API keys for a workspace
113 * @param workspaceId - The workspace ID
114 * @returns Array of API keys for the workspace
115 */
116export async function getApiKeys(
117 workspaceId: number,
118): Promise<Array<typeof apiKey.$inferSelect>> {
119 const keys = await db
120 .select()
121 .from(apiKey)
122 .where(eq(apiKey.workspaceId, workspaceId))
123 .all();
124
125 return keys;
126}
127
128/**
129 * Updates the lastUsedAt timestamp for an API key (with debouncing)
130 * @param id - The API key ID
131 * @param lastUsedAt - The current lastUsedAt value (or null)
132 * @returns True if updated, false if skipped due to debounce
133 */
134export async function updateLastUsed(
135 id: number,
136 lastUsedAt: Date | null,
137): Promise<boolean> {
138 // Check if update is needed (5-minute debounce)
139 if (!checkShouldUpdateLastUsed(lastUsedAt)) {
140 return false;
141 }
142
143 await db
144 .update(apiKey)
145 .set({ lastUsedAt: new Date() })
146 .where(eq(apiKey.id, id));
147
148 return true;
149}