@better-auth/api-key 1.5.0-beta.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +20 -0
- package/README.md +17 -0
- package/dist/client.d.mts +53 -0
- package/dist/client.mjs +19 -0
- package/dist/client.mjs.map +1 -0
- package/dist/error-codes-vwTWW2ez.mjs +40 -0
- package/dist/error-codes-vwTWW2ez.mjs.map +1 -0
- package/dist/error-codes-xCoIeQ-k.d.mts +50 -0
- package/dist/index.d.mts +1267 -0
- package/dist/index.mjs +2232 -0
- package/dist/index.mjs.map +1 -0
- package/dist/schema-D2f7--fy.d.mts +201 -0
- package/dist/types.d.mts +358 -0
- package/dist/types.mjs +1 -0
- package/package.json +80 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2232 @@
|
|
|
1
|
+
import { t as API_KEY_ERROR_CODES } from "./error-codes-vwTWW2ez.mjs";
|
|
2
|
+
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
3
|
+
import { base64Url } from "@better-auth/utils/base64";
|
|
4
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
5
|
+
import { BetterAuthError } from "better-auth";
|
|
6
|
+
import { APIError, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
|
|
7
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
8
|
+
import { mergeSchema } from "better-auth/db";
|
|
9
|
+
import { APIError as APIError$1 } from "@better-auth/core/error";
|
|
10
|
+
import { generateId } from "@better-auth/core/utils/id";
|
|
11
|
+
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
12
|
+
import * as z$1 from "zod";
|
|
13
|
+
import { isDevelopment, isTest } from "@better-auth/core/env";
|
|
14
|
+
import { isValidIP, normalizeIP } from "@better-auth/core/utils/ip";
|
|
15
|
+
import * as z from "zod/v4";
|
|
16
|
+
import { role } from "better-auth/plugins/access";
|
|
17
|
+
import { parseJSON } from "better-auth/client";
|
|
18
|
+
|
|
19
|
+
//#region src/adapter.ts
|
|
20
|
+
/**
|
|
21
|
+
* Parses double-stringified metadata synchronously without updating the database.
|
|
22
|
+
* Use this for reading metadata, then call migrateLegacyMetadataInBackground for DB updates.
|
|
23
|
+
*
|
|
24
|
+
* @returns The properly parsed metadata object, or the original if already an object
|
|
25
|
+
*/
|
|
26
|
+
function parseDoubleStringifiedMetadata(metadata) {
|
|
27
|
+
if (metadata == null) return null;
|
|
28
|
+
if (typeof metadata === "object") return metadata;
|
|
29
|
+
return safeJSONParse(metadata);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Checks if metadata needs migration (is a string instead of object)
|
|
33
|
+
*/
|
|
34
|
+
function needsMetadataMigration(metadata) {
|
|
35
|
+
return metadata != null && typeof metadata === "string";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Batch migrates double-stringified metadata for multiple API keys.
|
|
39
|
+
* Runs all updates in parallel to avoid N sequential database calls.
|
|
40
|
+
*/
|
|
41
|
+
async function batchMigrateLegacyMetadata(ctx, apiKeys, opts) {
|
|
42
|
+
if (opts.storage !== "database" && !opts.fallbackToDatabase) return;
|
|
43
|
+
const keysToMigrate = apiKeys.filter((key) => needsMetadataMigration(key.metadata));
|
|
44
|
+
if (keysToMigrate.length === 0) return;
|
|
45
|
+
const migrationPromises = keysToMigrate.map(async (apiKey) => {
|
|
46
|
+
const parsed = parseDoubleStringifiedMetadata(apiKey.metadata);
|
|
47
|
+
try {
|
|
48
|
+
await ctx.context.adapter.update({
|
|
49
|
+
model: "apikey",
|
|
50
|
+
where: [{
|
|
51
|
+
field: "id",
|
|
52
|
+
value: apiKey.id
|
|
53
|
+
}],
|
|
54
|
+
update: { metadata: parsed }
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
ctx.context.logger.warn(`Failed to migrate double-stringified metadata for API key ${apiKey.id}:`, error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
await Promise.all(migrationPromises);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Migrates double-stringified metadata to properly parsed object.
|
|
64
|
+
*
|
|
65
|
+
* This handles legacy data where metadata was incorrectly double-stringified.
|
|
66
|
+
* If metadata is a string (should be object after adapter's transform.output),
|
|
67
|
+
* it parses it and optionally updates the database.
|
|
68
|
+
*
|
|
69
|
+
* @returns The properly parsed metadata object
|
|
70
|
+
*/
|
|
71
|
+
async function migrateDoubleStringifiedMetadata(ctx, apiKey, opts) {
|
|
72
|
+
const parsed = parseDoubleStringifiedMetadata(apiKey.metadata);
|
|
73
|
+
if (needsMetadataMigration(apiKey.metadata) && (opts.storage === "database" || opts.fallbackToDatabase)) try {
|
|
74
|
+
await ctx.context.adapter.update({
|
|
75
|
+
model: "apikey",
|
|
76
|
+
where: [{
|
|
77
|
+
field: "id",
|
|
78
|
+
value: apiKey.id
|
|
79
|
+
}],
|
|
80
|
+
update: { metadata: parsed }
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
ctx.context.logger.warn(`Failed to migrate double-stringified metadata for API key ${apiKey.id}:`, error);
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Generate storage key for API key by hashed key
|
|
89
|
+
*/
|
|
90
|
+
function getStorageKeyByHashedKey(hashedKey) {
|
|
91
|
+
return `api-key:${hashedKey}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Generate storage key for API key by ID
|
|
95
|
+
*/
|
|
96
|
+
function getStorageKeyById(id) {
|
|
97
|
+
return `api-key:by-id:${id}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generate storage key for reference's API key list (user or org)
|
|
101
|
+
*/
|
|
102
|
+
function getStorageKeyByReferenceId(referenceId) {
|
|
103
|
+
return `api-key:by-ref:${referenceId}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Serialize API key for storage
|
|
107
|
+
*/
|
|
108
|
+
function serializeApiKey(apiKey) {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
...apiKey,
|
|
111
|
+
createdAt: apiKey.createdAt.toISOString(),
|
|
112
|
+
updatedAt: apiKey.updatedAt.toISOString(),
|
|
113
|
+
expiresAt: apiKey.expiresAt?.toISOString() ?? null,
|
|
114
|
+
lastRefillAt: apiKey.lastRefillAt?.toISOString() ?? null,
|
|
115
|
+
lastRequest: apiKey.lastRequest?.toISOString() ?? null
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Deserialize API key from storage
|
|
120
|
+
*/
|
|
121
|
+
function deserializeApiKey(data) {
|
|
122
|
+
if (!data || typeof data !== "string") return null;
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(data);
|
|
125
|
+
return {
|
|
126
|
+
...parsed,
|
|
127
|
+
createdAt: new Date(parsed.createdAt),
|
|
128
|
+
updatedAt: new Date(parsed.updatedAt),
|
|
129
|
+
expiresAt: parsed.expiresAt ? new Date(parsed.expiresAt) : null,
|
|
130
|
+
lastRefillAt: parsed.lastRefillAt ? new Date(parsed.lastRefillAt) : null,
|
|
131
|
+
lastRequest: parsed.lastRequest ? new Date(parsed.lastRequest) : null
|
|
132
|
+
};
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Get the storage instance to use (custom methods take precedence)
|
|
139
|
+
*/
|
|
140
|
+
function getStorageInstance(ctx, opts) {
|
|
141
|
+
if (opts.customStorage) return opts.customStorage;
|
|
142
|
+
return ctx.context.secondaryStorage || null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Calculate TTL in seconds for an API key
|
|
146
|
+
*/
|
|
147
|
+
function calculateTTL(apiKey) {
|
|
148
|
+
if (apiKey.expiresAt) {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const expiresAt = new Date(apiKey.expiresAt).getTime();
|
|
151
|
+
const ttlSeconds = Math.floor((expiresAt - now) / 1e3);
|
|
152
|
+
if (ttlSeconds > 0) return ttlSeconds;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get API key from secondary storage by hashed key
|
|
157
|
+
*/
|
|
158
|
+
async function getApiKeyFromStorage(ctx, hashedKey, storage) {
|
|
159
|
+
const key = getStorageKeyByHashedKey(hashedKey);
|
|
160
|
+
return deserializeApiKey(await storage.get(key));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get API key from secondary storage by ID
|
|
164
|
+
*/
|
|
165
|
+
async function getApiKeyByIdFromStorage(ctx, id, storage) {
|
|
166
|
+
const key = getStorageKeyById(id);
|
|
167
|
+
return deserializeApiKey(await storage.get(key));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Store API key in secondary storage
|
|
171
|
+
*/
|
|
172
|
+
async function setApiKeyInStorage(ctx, apiKey, storage, ttl) {
|
|
173
|
+
const serialized = serializeApiKey(apiKey);
|
|
174
|
+
const hashedKey = apiKey.key;
|
|
175
|
+
const id = apiKey.id;
|
|
176
|
+
await storage.set(getStorageKeyByHashedKey(hashedKey), serialized, ttl);
|
|
177
|
+
await storage.set(getStorageKeyById(id), serialized, ttl);
|
|
178
|
+
const refKey = getStorageKeyByReferenceId(apiKey.referenceId);
|
|
179
|
+
const refListData = await storage.get(refKey);
|
|
180
|
+
let keyIds = [];
|
|
181
|
+
if (refListData && typeof refListData === "string") try {
|
|
182
|
+
keyIds = JSON.parse(refListData);
|
|
183
|
+
} catch {
|
|
184
|
+
keyIds = [];
|
|
185
|
+
}
|
|
186
|
+
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
187
|
+
if (!keyIds.includes(id)) {
|
|
188
|
+
keyIds.push(id);
|
|
189
|
+
await storage.set(refKey, JSON.stringify(keyIds));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Delete API key from secondary storage
|
|
194
|
+
*/
|
|
195
|
+
async function deleteApiKeyFromStorage(ctx, apiKey, storage) {
|
|
196
|
+
const hashedKey = apiKey.key;
|
|
197
|
+
const id = apiKey.id;
|
|
198
|
+
const referenceId = apiKey.referenceId;
|
|
199
|
+
await storage.delete(getStorageKeyByHashedKey(hashedKey));
|
|
200
|
+
await storage.delete(getStorageKeyById(id));
|
|
201
|
+
const refKey = getStorageKeyByReferenceId(referenceId);
|
|
202
|
+
const refListData = await storage.get(refKey);
|
|
203
|
+
let keyIds = [];
|
|
204
|
+
if (refListData && typeof refListData === "string") try {
|
|
205
|
+
keyIds = JSON.parse(refListData);
|
|
206
|
+
} catch {
|
|
207
|
+
keyIds = [];
|
|
208
|
+
}
|
|
209
|
+
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
210
|
+
const filteredIds = keyIds.filter((keyId) => keyId !== id);
|
|
211
|
+
if (filteredIds.length === 0) await storage.delete(refKey);
|
|
212
|
+
else await storage.set(refKey, JSON.stringify(filteredIds));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Unified getter for API keys with support for all storage modes
|
|
216
|
+
*/
|
|
217
|
+
async function getApiKey$1(ctx, hashedKey, opts) {
|
|
218
|
+
const storage = getStorageInstance(ctx, opts);
|
|
219
|
+
if (opts.storage === "database") return await ctx.context.adapter.findOne({
|
|
220
|
+
model: "apikey",
|
|
221
|
+
where: [{
|
|
222
|
+
field: "key",
|
|
223
|
+
value: hashedKey
|
|
224
|
+
}]
|
|
225
|
+
});
|
|
226
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
227
|
+
if (storage) {
|
|
228
|
+
const cached = await getApiKeyFromStorage(ctx, hashedKey, storage);
|
|
229
|
+
if (cached) return cached;
|
|
230
|
+
}
|
|
231
|
+
const dbKey = await ctx.context.adapter.findOne({
|
|
232
|
+
model: "apikey",
|
|
233
|
+
where: [{
|
|
234
|
+
field: "key",
|
|
235
|
+
value: hashedKey
|
|
236
|
+
}]
|
|
237
|
+
});
|
|
238
|
+
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey));
|
|
239
|
+
return dbKey;
|
|
240
|
+
}
|
|
241
|
+
if (opts.storage === "secondary-storage") {
|
|
242
|
+
if (!storage) return null;
|
|
243
|
+
return await getApiKeyFromStorage(ctx, hashedKey, storage);
|
|
244
|
+
}
|
|
245
|
+
return await ctx.context.adapter.findOne({
|
|
246
|
+
model: "apikey",
|
|
247
|
+
where: [{
|
|
248
|
+
field: "key",
|
|
249
|
+
value: hashedKey
|
|
250
|
+
}]
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Unified getter for API keys by ID
|
|
255
|
+
*/
|
|
256
|
+
async function getApiKeyById(ctx, id, opts) {
|
|
257
|
+
const storage = getStorageInstance(ctx, opts);
|
|
258
|
+
if (opts.storage === "database") return await ctx.context.adapter.findOne({
|
|
259
|
+
model: "apikey",
|
|
260
|
+
where: [{
|
|
261
|
+
field: "id",
|
|
262
|
+
value: id
|
|
263
|
+
}]
|
|
264
|
+
});
|
|
265
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
266
|
+
if (storage) {
|
|
267
|
+
const cached = await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
268
|
+
if (cached) return cached;
|
|
269
|
+
}
|
|
270
|
+
const dbKey = await ctx.context.adapter.findOne({
|
|
271
|
+
model: "apikey",
|
|
272
|
+
where: [{
|
|
273
|
+
field: "id",
|
|
274
|
+
value: id
|
|
275
|
+
}]
|
|
276
|
+
});
|
|
277
|
+
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey));
|
|
278
|
+
return dbKey;
|
|
279
|
+
}
|
|
280
|
+
if (opts.storage === "secondary-storage") {
|
|
281
|
+
if (!storage) return null;
|
|
282
|
+
return await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
283
|
+
}
|
|
284
|
+
return await ctx.context.adapter.findOne({
|
|
285
|
+
model: "apikey",
|
|
286
|
+
where: [{
|
|
287
|
+
field: "id",
|
|
288
|
+
value: id
|
|
289
|
+
}]
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Unified setter for API keys with support for all storage modes
|
|
294
|
+
*/
|
|
295
|
+
async function setApiKey(ctx, apiKey, opts) {
|
|
296
|
+
const storage = getStorageInstance(ctx, opts);
|
|
297
|
+
const ttl = calculateTTL(apiKey);
|
|
298
|
+
if (opts.storage === "database") return;
|
|
299
|
+
if (opts.storage === "secondary-storage") {
|
|
300
|
+
if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'");
|
|
301
|
+
await setApiKeyInStorage(ctx, apiKey, storage, ttl);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Unified deleter for API keys with support for all storage modes
|
|
307
|
+
*/
|
|
308
|
+
async function deleteApiKey$1(ctx, apiKey, opts) {
|
|
309
|
+
const storage = getStorageInstance(ctx, opts);
|
|
310
|
+
if (opts.storage === "database") return;
|
|
311
|
+
if (opts.storage === "secondary-storage") {
|
|
312
|
+
if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'");
|
|
313
|
+
await deleteApiKeyFromStorage(ctx, apiKey, storage);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Apply sorting and pagination to an array of API keys in memory
|
|
319
|
+
* Used for secondary storage mode where we can't rely on database operations
|
|
320
|
+
*/
|
|
321
|
+
function applySortingAndPagination(apiKeys, sortBy, sortDirection, limit, offset) {
|
|
322
|
+
let result = [...apiKeys];
|
|
323
|
+
if (sortBy) {
|
|
324
|
+
const direction = sortDirection || "asc";
|
|
325
|
+
result.sort((a, b) => {
|
|
326
|
+
const aValue = a[sortBy];
|
|
327
|
+
const bValue = b[sortBy];
|
|
328
|
+
if (aValue == null && bValue == null) return 0;
|
|
329
|
+
if (aValue == null) return direction === "asc" ? -1 : 1;
|
|
330
|
+
if (bValue == null) return direction === "asc" ? 1 : -1;
|
|
331
|
+
if (aValue < bValue) return direction === "asc" ? -1 : 1;
|
|
332
|
+
if (aValue > bValue) return direction === "asc" ? 1 : -1;
|
|
333
|
+
return 0;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (offset !== void 0) result = result.slice(offset);
|
|
337
|
+
if (limit !== void 0) result = result.slice(0, limit);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* List API keys for a reference (user or org) with support for all storage modes
|
|
342
|
+
*/
|
|
343
|
+
async function listApiKeys$1(ctx, referenceId, opts, paginationOpts) {
|
|
344
|
+
const storage = getStorageInstance(ctx, opts);
|
|
345
|
+
const { limit, offset, sortBy, sortDirection } = paginationOpts || {};
|
|
346
|
+
if (opts.storage === "database") {
|
|
347
|
+
const [apiKeys, total] = await Promise.all([ctx.context.adapter.findMany({
|
|
348
|
+
model: "apikey",
|
|
349
|
+
where: [{
|
|
350
|
+
field: "referenceId",
|
|
351
|
+
value: referenceId
|
|
352
|
+
}],
|
|
353
|
+
limit,
|
|
354
|
+
offset,
|
|
355
|
+
sortBy: sortBy ? {
|
|
356
|
+
field: sortBy,
|
|
357
|
+
direction: sortDirection || "asc"
|
|
358
|
+
} : void 0
|
|
359
|
+
}), ctx.context.adapter.count({
|
|
360
|
+
model: "apikey",
|
|
361
|
+
where: [{
|
|
362
|
+
field: "referenceId",
|
|
363
|
+
value: referenceId
|
|
364
|
+
}]
|
|
365
|
+
})]);
|
|
366
|
+
return {
|
|
367
|
+
apiKeys,
|
|
368
|
+
total
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
372
|
+
const refKey = getStorageKeyByReferenceId(referenceId);
|
|
373
|
+
if (storage) {
|
|
374
|
+
const refListData = await storage.get(refKey);
|
|
375
|
+
let keyIds = [];
|
|
376
|
+
if (refListData && typeof refListData === "string") try {
|
|
377
|
+
keyIds = JSON.parse(refListData);
|
|
378
|
+
} catch {
|
|
379
|
+
keyIds = [];
|
|
380
|
+
}
|
|
381
|
+
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
382
|
+
if (keyIds.length > 0) {
|
|
383
|
+
const apiKeys = [];
|
|
384
|
+
for (const id of keyIds) {
|
|
385
|
+
const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
386
|
+
if (apiKey) apiKeys.push(apiKey);
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
apiKeys: applySortingAndPagination(apiKeys, sortBy, sortDirection, limit, offset),
|
|
390
|
+
total: apiKeys.length
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const [dbKeys, total] = await Promise.all([ctx.context.adapter.findMany({
|
|
395
|
+
model: "apikey",
|
|
396
|
+
where: [{
|
|
397
|
+
field: "referenceId",
|
|
398
|
+
value: referenceId
|
|
399
|
+
}],
|
|
400
|
+
limit,
|
|
401
|
+
offset,
|
|
402
|
+
sortBy: sortBy ? {
|
|
403
|
+
field: sortBy,
|
|
404
|
+
direction: sortDirection || "asc"
|
|
405
|
+
} : void 0
|
|
406
|
+
}), ctx.context.adapter.count({
|
|
407
|
+
model: "apikey",
|
|
408
|
+
where: [{
|
|
409
|
+
field: "referenceId",
|
|
410
|
+
value: referenceId
|
|
411
|
+
}]
|
|
412
|
+
})]);
|
|
413
|
+
if (storage && dbKeys.length > 0) {
|
|
414
|
+
const keyIds = [];
|
|
415
|
+
for (const apiKey of dbKeys) {
|
|
416
|
+
await setApiKeyInStorage(ctx, apiKey, storage, calculateTTL(apiKey));
|
|
417
|
+
keyIds.push(apiKey.id);
|
|
418
|
+
}
|
|
419
|
+
await storage.set(refKey, JSON.stringify(keyIds));
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
apiKeys: dbKeys,
|
|
423
|
+
total
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (opts.storage === "secondary-storage") {
|
|
427
|
+
if (!storage) return {
|
|
428
|
+
apiKeys: [],
|
|
429
|
+
total: 0
|
|
430
|
+
};
|
|
431
|
+
const refKey = getStorageKeyByReferenceId(referenceId);
|
|
432
|
+
const refListData = await storage.get(refKey);
|
|
433
|
+
let keyIds = [];
|
|
434
|
+
if (refListData && typeof refListData === "string") try {
|
|
435
|
+
keyIds = JSON.parse(refListData);
|
|
436
|
+
} catch {
|
|
437
|
+
return {
|
|
438
|
+
apiKeys: [],
|
|
439
|
+
total: 0
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
443
|
+
else return {
|
|
444
|
+
apiKeys: [],
|
|
445
|
+
total: 0
|
|
446
|
+
};
|
|
447
|
+
const apiKeys = [];
|
|
448
|
+
for (const id of keyIds) {
|
|
449
|
+
const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
450
|
+
if (apiKey) apiKeys.push(apiKey);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
apiKeys: applySortingAndPagination(apiKeys, sortBy, sortDirection, limit, offset),
|
|
454
|
+
total: apiKeys.length
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const [apiKeys, total] = await Promise.all([ctx.context.adapter.findMany({
|
|
458
|
+
model: "apikey",
|
|
459
|
+
where: [{
|
|
460
|
+
field: "referenceId",
|
|
461
|
+
value: referenceId
|
|
462
|
+
}],
|
|
463
|
+
limit,
|
|
464
|
+
offset,
|
|
465
|
+
sortBy: sortBy ? {
|
|
466
|
+
field: sortBy,
|
|
467
|
+
direction: sortDirection || "asc"
|
|
468
|
+
} : void 0
|
|
469
|
+
}), ctx.context.adapter.count({
|
|
470
|
+
model: "apikey",
|
|
471
|
+
where: [{
|
|
472
|
+
field: "referenceId",
|
|
473
|
+
value: referenceId
|
|
474
|
+
}]
|
|
475
|
+
})]);
|
|
476
|
+
return {
|
|
477
|
+
apiKeys,
|
|
478
|
+
total
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/org-authorization.ts
|
|
484
|
+
/**
|
|
485
|
+
* Gets the organization plugin options from the context.
|
|
486
|
+
* Returns null if the organization plugin is not installed.
|
|
487
|
+
*/
|
|
488
|
+
function getOrgOptions(ctx) {
|
|
489
|
+
if (ctx.context.orgOptions) return ctx.context.orgOptions;
|
|
490
|
+
const orgPlugin = ctx.context.getPlugin?.("organization");
|
|
491
|
+
if (orgPlugin && "options" in orgPlugin) return orgPlugin.options;
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Checks if a user is a member of an organization and has the required permission.
|
|
496
|
+
* This is used for organization-owned API keys to validate access.
|
|
497
|
+
*
|
|
498
|
+
* @param ctx - The endpoint context
|
|
499
|
+
* @param userId - The ID of the user to check
|
|
500
|
+
* @param organizationId - The ID of the organization (from API key's referenceId)
|
|
501
|
+
* @param requiredAction - The action the user is trying to perform (create, read, update, delete)
|
|
502
|
+
* @returns The member object if authorized
|
|
503
|
+
* @throws APIError if not authorized
|
|
504
|
+
*/
|
|
505
|
+
async function checkOrgApiKeyPermission(ctx, userId, organizationId, requiredAction) {
|
|
506
|
+
const orgOptions = getOrgOptions(ctx);
|
|
507
|
+
if (!orgOptions) {
|
|
508
|
+
const msg = API_KEY_ERROR_CODES.ORGANIZATION_PLUGIN_REQUIRED;
|
|
509
|
+
throw APIError$1.from("INTERNAL_SERVER_ERROR", msg);
|
|
510
|
+
}
|
|
511
|
+
const member = await ctx.context.adapter.findOne({
|
|
512
|
+
model: "member",
|
|
513
|
+
where: [{
|
|
514
|
+
field: "userId",
|
|
515
|
+
value: userId
|
|
516
|
+
}, {
|
|
517
|
+
field: "organizationId",
|
|
518
|
+
value: organizationId
|
|
519
|
+
}]
|
|
520
|
+
});
|
|
521
|
+
if (!member) {
|
|
522
|
+
const msg = API_KEY_ERROR_CODES.USER_NOT_MEMBER_OF_ORGANIZATION;
|
|
523
|
+
throw APIError$1.from("FORBIDDEN", msg);
|
|
524
|
+
}
|
|
525
|
+
if (!await checkPermission(ctx, member.role, organizationId, requiredAction, orgOptions)) {
|
|
526
|
+
const msg = API_KEY_ERROR_CODES.INSUFFICIENT_API_KEY_PERMISSIONS;
|
|
527
|
+
throw APIError$1.from("FORBIDDEN", msg);
|
|
528
|
+
}
|
|
529
|
+
return member;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Checks if a role has the required permission for API key operations.
|
|
533
|
+
* Uses the organization's access control system.
|
|
534
|
+
*
|
|
535
|
+
* Organization owners (determined by orgOptions.creatorRole, default "owner")
|
|
536
|
+
* are granted full access to API key operations.
|
|
537
|
+
*/
|
|
538
|
+
async function checkPermission(ctx, role, organizationId, action, orgOptions) {
|
|
539
|
+
const { hasPermission } = await import("better-auth/plugins/organization");
|
|
540
|
+
try {
|
|
541
|
+
return await hasPermission({
|
|
542
|
+
role,
|
|
543
|
+
options: orgOptions,
|
|
544
|
+
permissions: { apiKey: [action] },
|
|
545
|
+
organizationId,
|
|
546
|
+
allowCreatorAllPermissions: true
|
|
547
|
+
}, ctx);
|
|
548
|
+
} catch {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/utils.ts
|
|
555
|
+
const getDate = (span, unit = "ms") => {
|
|
556
|
+
return new Date(Date.now() + (unit === "sec" ? span * 1e3 : span));
|
|
557
|
+
};
|
|
558
|
+
function isAPIError(error) {
|
|
559
|
+
return error instanceof APIError || error instanceof APIError$1 || error?.name === "APIError";
|
|
560
|
+
}
|
|
561
|
+
const LOCALHOST_IP = "127.0.0.1";
|
|
562
|
+
function getIp(req, options) {
|
|
563
|
+
if (options.advanced?.ipAddress?.disableIpTracking) return null;
|
|
564
|
+
const headers = "headers" in req ? req.headers : req;
|
|
565
|
+
const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"];
|
|
566
|
+
for (const key of ipHeaders) {
|
|
567
|
+
const value = "get" in headers ? headers.get(key) : headers[key];
|
|
568
|
+
if (typeof value === "string") {
|
|
569
|
+
const ip = value.split(",")[0].trim();
|
|
570
|
+
if (isValidIP(ip)) return normalizeIP(ip, { ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (isTest() || isDevelopment()) return LOCALHOST_IP;
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/routes/create-api-key.ts
|
|
579
|
+
const createApiKeyBodySchema = z$1.object({
|
|
580
|
+
configId: z$1.string().meta({ description: "The configuration ID to use for the API key. If not provided, the default configuration will be used." }).optional(),
|
|
581
|
+
name: z$1.string().meta({ description: "Name of the Api Key" }).optional(),
|
|
582
|
+
expiresIn: z$1.number().meta({ description: "Expiration time of the Api Key in seconds" }).min(1).optional().nullable().default(null),
|
|
583
|
+
prefix: z$1.string().meta({ description: "Prefix of the Api Key" }).regex(/^[a-zA-Z0-9_-]+$/, { message: "Invalid prefix format, must be alphanumeric and contain only underscores and hyphens." }).optional(),
|
|
584
|
+
remaining: z$1.number().meta({ description: "Remaining number of requests. Server side only" }).min(0).optional().nullable().default(null),
|
|
585
|
+
metadata: z$1.any().optional(),
|
|
586
|
+
refillAmount: z$1.number().meta({ description: "Amount to refill the remaining count of the Api Key. server-only. Eg: 100" }).min(1).optional(),
|
|
587
|
+
refillInterval: z$1.number().meta({ description: "Interval to refill the Api Key in milliseconds. server-only. Eg: 1000" }).optional(),
|
|
588
|
+
rateLimitTimeWindow: z$1.number().meta({ description: "The duration in milliseconds where each request is counted. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 1000" }).optional(),
|
|
589
|
+
rateLimitMax: z$1.number().meta({ description: "Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100" }).optional(),
|
|
590
|
+
rateLimitEnabled: z$1.boolean().meta({ description: "Whether the key has rate limiting enabled. server-only. Eg: true" }).optional(),
|
|
591
|
+
permissions: z$1.record(z$1.string(), z$1.array(z$1.string())).meta({ description: "Permissions of the Api Key." }).optional(),
|
|
592
|
+
userId: z$1.coerce.string().meta({ description: "User Id of the user that the Api Key belongs to. server-only. Eg: \"user-id\"" }).optional(),
|
|
593
|
+
organizationId: z$1.coerce.string().meta({ description: "Organization Id of the organization that the Api Key belongs to. Eg: 'org-id'" }).optional()
|
|
594
|
+
});
|
|
595
|
+
function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllExpiredApiKeys }) {
|
|
596
|
+
return createAuthEndpoint("/api-key/create", {
|
|
597
|
+
method: "POST",
|
|
598
|
+
body: createApiKeyBodySchema,
|
|
599
|
+
metadata: { openapi: {
|
|
600
|
+
description: "Create a new API key for a user",
|
|
601
|
+
responses: { "200": {
|
|
602
|
+
description: "API key created successfully",
|
|
603
|
+
content: { "application/json": { schema: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
id: {
|
|
607
|
+
type: "string",
|
|
608
|
+
description: "Unique identifier of the API key"
|
|
609
|
+
},
|
|
610
|
+
createdAt: {
|
|
611
|
+
type: "string",
|
|
612
|
+
format: "date-time",
|
|
613
|
+
description: "Creation timestamp"
|
|
614
|
+
},
|
|
615
|
+
updatedAt: {
|
|
616
|
+
type: "string",
|
|
617
|
+
format: "date-time",
|
|
618
|
+
description: "Last update timestamp"
|
|
619
|
+
},
|
|
620
|
+
name: {
|
|
621
|
+
type: "string",
|
|
622
|
+
nullable: true,
|
|
623
|
+
description: "Name of the API key"
|
|
624
|
+
},
|
|
625
|
+
prefix: {
|
|
626
|
+
type: "string",
|
|
627
|
+
nullable: true,
|
|
628
|
+
description: "Prefix of the API key"
|
|
629
|
+
},
|
|
630
|
+
start: {
|
|
631
|
+
type: "string",
|
|
632
|
+
nullable: true,
|
|
633
|
+
description: "Starting characters of the key (if configured)"
|
|
634
|
+
},
|
|
635
|
+
key: {
|
|
636
|
+
type: "string",
|
|
637
|
+
description: "The full API key (only returned on creation)"
|
|
638
|
+
},
|
|
639
|
+
enabled: {
|
|
640
|
+
type: "boolean",
|
|
641
|
+
description: "Whether the key is enabled"
|
|
642
|
+
},
|
|
643
|
+
expiresAt: {
|
|
644
|
+
type: "string",
|
|
645
|
+
format: "date-time",
|
|
646
|
+
nullable: true,
|
|
647
|
+
description: "Expiration timestamp"
|
|
648
|
+
},
|
|
649
|
+
referenceId: {
|
|
650
|
+
type: "string",
|
|
651
|
+
description: "ID of the reference owning the key"
|
|
652
|
+
},
|
|
653
|
+
lastRefillAt: {
|
|
654
|
+
type: "string",
|
|
655
|
+
format: "date-time",
|
|
656
|
+
nullable: true,
|
|
657
|
+
description: "Last refill timestamp"
|
|
658
|
+
},
|
|
659
|
+
lastRequest: {
|
|
660
|
+
type: "string",
|
|
661
|
+
format: "date-time",
|
|
662
|
+
nullable: true,
|
|
663
|
+
description: "Last request timestamp"
|
|
664
|
+
},
|
|
665
|
+
metadata: {
|
|
666
|
+
type: "object",
|
|
667
|
+
nullable: true,
|
|
668
|
+
additionalProperties: true,
|
|
669
|
+
description: "Metadata associated with the key"
|
|
670
|
+
},
|
|
671
|
+
rateLimitMax: {
|
|
672
|
+
type: "number",
|
|
673
|
+
nullable: true,
|
|
674
|
+
description: "Maximum requests in time window"
|
|
675
|
+
},
|
|
676
|
+
rateLimitTimeWindow: {
|
|
677
|
+
type: "number",
|
|
678
|
+
nullable: true,
|
|
679
|
+
description: "Rate limit time window in milliseconds"
|
|
680
|
+
},
|
|
681
|
+
remaining: {
|
|
682
|
+
type: "number",
|
|
683
|
+
nullable: true,
|
|
684
|
+
description: "Remaining requests"
|
|
685
|
+
},
|
|
686
|
+
refillAmount: {
|
|
687
|
+
type: "number",
|
|
688
|
+
nullable: true,
|
|
689
|
+
description: "Amount to refill"
|
|
690
|
+
},
|
|
691
|
+
refillInterval: {
|
|
692
|
+
type: "number",
|
|
693
|
+
nullable: true,
|
|
694
|
+
description: "Refill interval in milliseconds"
|
|
695
|
+
},
|
|
696
|
+
rateLimitEnabled: {
|
|
697
|
+
type: "boolean",
|
|
698
|
+
description: "Whether rate limiting is enabled"
|
|
699
|
+
},
|
|
700
|
+
requestCount: {
|
|
701
|
+
type: "number",
|
|
702
|
+
description: "Current request count in window"
|
|
703
|
+
},
|
|
704
|
+
permissions: {
|
|
705
|
+
type: "object",
|
|
706
|
+
nullable: true,
|
|
707
|
+
additionalProperties: {
|
|
708
|
+
type: "array",
|
|
709
|
+
items: { type: "string" }
|
|
710
|
+
},
|
|
711
|
+
description: "Permissions associated with the key"
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
required: [
|
|
715
|
+
"id",
|
|
716
|
+
"createdAt",
|
|
717
|
+
"updatedAt",
|
|
718
|
+
"key",
|
|
719
|
+
"enabled",
|
|
720
|
+
"referenceId",
|
|
721
|
+
"rateLimitEnabled",
|
|
722
|
+
"requestCount"
|
|
723
|
+
]
|
|
724
|
+
} } }
|
|
725
|
+
} }
|
|
726
|
+
} }
|
|
727
|
+
}, async (ctx) => {
|
|
728
|
+
const { configId, name, expiresIn, prefix, remaining, metadata, refillAmount, refillInterval, permissions, rateLimitMax, rateLimitTimeWindow, rateLimitEnabled } = ctx.body;
|
|
729
|
+
const opts = resolveConfiguration(ctx.context, configurations, configId);
|
|
730
|
+
const keyGenerator = opts.customKeyGenerator || defaultKeyGenerator;
|
|
731
|
+
const session = await getSessionFromCtx(ctx);
|
|
732
|
+
const isClientRequest = ctx.request || ctx.headers;
|
|
733
|
+
if (isClientRequest && (refillAmount !== void 0 || refillInterval !== void 0 || rateLimitMax !== void 0 || rateLimitTimeWindow !== void 0 || rateLimitEnabled !== void 0 || permissions !== void 0 || remaining !== null)) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.SERVER_ONLY_PROPERTY);
|
|
734
|
+
if (ctx.request && ctx.body.userId !== void 0) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
735
|
+
const referencesType = opts.references ?? "user";
|
|
736
|
+
let referenceId;
|
|
737
|
+
if (referencesType === "organization") {
|
|
738
|
+
const orgId = ctx.body.organizationId;
|
|
739
|
+
if (!orgId) {
|
|
740
|
+
const msg = API_KEY_ERROR_CODES.ORGANIZATION_ID_REQUIRED;
|
|
741
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
742
|
+
}
|
|
743
|
+
const userId = session?.user.id || ctx.body.userId;
|
|
744
|
+
if (!userId) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
745
|
+
await checkOrgApiKeyPermission(ctx, userId, orgId, "create");
|
|
746
|
+
referenceId = orgId;
|
|
747
|
+
} else if (isClientRequest) {
|
|
748
|
+
if (!session?.user.id) {
|
|
749
|
+
const msg = API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION;
|
|
750
|
+
throw APIError$1.from("UNAUTHORIZED", msg);
|
|
751
|
+
}
|
|
752
|
+
referenceId = session.user.id;
|
|
753
|
+
} else {
|
|
754
|
+
const ctxUserId = ctx.body.userId;
|
|
755
|
+
const sessionUserId = session?.user.id;
|
|
756
|
+
if (!sessionUserId && !ctxUserId) {
|
|
757
|
+
const msg = API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION;
|
|
758
|
+
throw APIError$1.from("UNAUTHORIZED", msg);
|
|
759
|
+
}
|
|
760
|
+
if (session && ctxUserId && sessionUserId !== ctxUserId) {
|
|
761
|
+
const msg = API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION;
|
|
762
|
+
throw APIError$1.from("UNAUTHORIZED", msg);
|
|
763
|
+
}
|
|
764
|
+
referenceId = sessionUserId || ctxUserId;
|
|
765
|
+
}
|
|
766
|
+
if (metadata) {
|
|
767
|
+
if (opts.enableMetadata === false) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.METADATA_DISABLED);
|
|
768
|
+
if (typeof metadata !== "object") throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_METADATA_TYPE);
|
|
769
|
+
}
|
|
770
|
+
if (refillAmount && !refillInterval) {
|
|
771
|
+
const msg = API_KEY_ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED;
|
|
772
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
773
|
+
}
|
|
774
|
+
if (refillInterval && !refillAmount) {
|
|
775
|
+
const msg = API_KEY_ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED;
|
|
776
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
777
|
+
}
|
|
778
|
+
if (expiresIn) {
|
|
779
|
+
if (opts.keyExpiration.disableCustomExpiresTime === true) {
|
|
780
|
+
const msg = API_KEY_ERROR_CODES.KEY_DISABLED_EXPIRATION;
|
|
781
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
782
|
+
}
|
|
783
|
+
const expiresIn_in_days = expiresIn / (3600 * 24);
|
|
784
|
+
if (opts.keyExpiration.minExpiresIn > expiresIn_in_days) {
|
|
785
|
+
const msg = API_KEY_ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL;
|
|
786
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
787
|
+
} else if (opts.keyExpiration.maxExpiresIn < expiresIn_in_days) {
|
|
788
|
+
const msg = API_KEY_ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE;
|
|
789
|
+
throw APIError$1.from("BAD_REQUEST", msg);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (prefix) {
|
|
793
|
+
if (prefix.length < opts.minimumPrefixLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_PREFIX_LENGTH);
|
|
794
|
+
if (prefix.length > opts.maximumPrefixLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_PREFIX_LENGTH);
|
|
795
|
+
}
|
|
796
|
+
if (name) {
|
|
797
|
+
if (name.length < opts.minimumNameLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_NAME_LENGTH);
|
|
798
|
+
if (name.length > opts.maximumNameLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_NAME_LENGTH);
|
|
799
|
+
} else if (opts.requireName) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.NAME_REQUIRED);
|
|
800
|
+
deleteAllExpiredApiKeys(ctx.context);
|
|
801
|
+
const key = await keyGenerator({
|
|
802
|
+
length: opts.defaultKeyLength,
|
|
803
|
+
prefix: prefix || opts.defaultPrefix
|
|
804
|
+
});
|
|
805
|
+
const hashed = opts.disableKeyHashing ? key : await defaultKeyHasher(key);
|
|
806
|
+
let start = null;
|
|
807
|
+
if (opts.startingCharactersConfig.shouldStore) start = key.substring(0, opts.startingCharactersConfig.charactersLength);
|
|
808
|
+
const defaultPermissions = opts.permissions?.defaultPermissions ? typeof opts.permissions.defaultPermissions === "function" ? await opts.permissions.defaultPermissions(referenceId, ctx) : opts.permissions.defaultPermissions : void 0;
|
|
809
|
+
const permissionsToApply = permissions ? JSON.stringify(permissions) : defaultPermissions ? JSON.stringify(defaultPermissions) : void 0;
|
|
810
|
+
const data = {
|
|
811
|
+
configId: opts.configId ?? "default",
|
|
812
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
813
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
814
|
+
name: name ?? null,
|
|
815
|
+
prefix: prefix ?? opts.defaultPrefix ?? null,
|
|
816
|
+
start,
|
|
817
|
+
key: hashed,
|
|
818
|
+
enabled: true,
|
|
819
|
+
expiresAt: expiresIn ? getDate(expiresIn, "sec") : opts.keyExpiration.defaultExpiresIn ? getDate(opts.keyExpiration.defaultExpiresIn, "sec") : null,
|
|
820
|
+
referenceId,
|
|
821
|
+
lastRefillAt: null,
|
|
822
|
+
lastRequest: null,
|
|
823
|
+
metadata: null,
|
|
824
|
+
rateLimitMax: rateLimitMax ?? opts.rateLimit.maxRequests ?? null,
|
|
825
|
+
rateLimitTimeWindow: rateLimitTimeWindow ?? opts.rateLimit.timeWindow ?? null,
|
|
826
|
+
remaining: remaining === null ? remaining : remaining ?? refillAmount ?? null,
|
|
827
|
+
refillAmount: refillAmount ?? null,
|
|
828
|
+
refillInterval: refillInterval ?? null,
|
|
829
|
+
rateLimitEnabled: rateLimitEnabled === void 0 ? opts.rateLimit.enabled ?? true : rateLimitEnabled,
|
|
830
|
+
requestCount: 0,
|
|
831
|
+
permissions: permissionsToApply
|
|
832
|
+
};
|
|
833
|
+
if (metadata) data.metadata = metadata;
|
|
834
|
+
let apiKey;
|
|
835
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
836
|
+
apiKey = await ctx.context.adapter.create({
|
|
837
|
+
model: API_KEY_TABLE_NAME,
|
|
838
|
+
data
|
|
839
|
+
});
|
|
840
|
+
await setApiKey(ctx, apiKey, opts);
|
|
841
|
+
} else if (opts.storage === "secondary-storage") {
|
|
842
|
+
const id = ctx.context.generateId({ model: API_KEY_TABLE_NAME }) || generateId();
|
|
843
|
+
apiKey = {
|
|
844
|
+
...data,
|
|
845
|
+
id
|
|
846
|
+
};
|
|
847
|
+
await setApiKey(ctx, apiKey, opts);
|
|
848
|
+
} else apiKey = await ctx.context.adapter.create({
|
|
849
|
+
model: API_KEY_TABLE_NAME,
|
|
850
|
+
data
|
|
851
|
+
});
|
|
852
|
+
return ctx.json({
|
|
853
|
+
...apiKey,
|
|
854
|
+
key,
|
|
855
|
+
metadata: metadata ?? null,
|
|
856
|
+
permissions: apiKey.permissions ? safeJSONParse(apiKey.permissions) : null
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/routes/delete-all-expired-api-keys.ts
|
|
863
|
+
function deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys }) {
|
|
864
|
+
return createAuthEndpoint({ method: "POST" }, async (ctx) => {
|
|
865
|
+
try {
|
|
866
|
+
await deleteAllExpiredApiKeys(ctx.context, true);
|
|
867
|
+
} catch (error) {
|
|
868
|
+
ctx.context.logger.error("[API KEY PLUGIN] Failed to delete expired API keys:", error);
|
|
869
|
+
return ctx.json({
|
|
870
|
+
success: false,
|
|
871
|
+
error
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return ctx.json({
|
|
875
|
+
success: true,
|
|
876
|
+
error: null
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
//#endregion
|
|
882
|
+
//#region src/routes/delete-api-key.ts
|
|
883
|
+
const deleteApiKeyBodySchema = z$1.object({
|
|
884
|
+
configId: z$1.string().meta({ description: "The configuration ID to use for the API key lookup. If not provided, the default configuration will be used." }).optional(),
|
|
885
|
+
keyId: z$1.string().meta({ description: "The id of the Api Key" })
|
|
886
|
+
});
|
|
887
|
+
function deleteApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
888
|
+
return createAuthEndpoint("/api-key/delete", {
|
|
889
|
+
method: "POST",
|
|
890
|
+
body: deleteApiKeyBodySchema,
|
|
891
|
+
use: [sessionMiddleware],
|
|
892
|
+
metadata: { openapi: {
|
|
893
|
+
description: "Delete an existing API key",
|
|
894
|
+
requestBody: { content: { "application/json": { schema: {
|
|
895
|
+
type: "object",
|
|
896
|
+
properties: { keyId: {
|
|
897
|
+
type: "string",
|
|
898
|
+
description: "The id of the API key to delete"
|
|
899
|
+
} },
|
|
900
|
+
required: ["keyId"]
|
|
901
|
+
} } } },
|
|
902
|
+
responses: { "200": {
|
|
903
|
+
description: "API key deleted successfully",
|
|
904
|
+
content: { "application/json": { schema: {
|
|
905
|
+
type: "object",
|
|
906
|
+
properties: { success: {
|
|
907
|
+
type: "boolean",
|
|
908
|
+
description: "Indicates if the API key was successfully deleted"
|
|
909
|
+
} },
|
|
910
|
+
required: ["success"]
|
|
911
|
+
} } }
|
|
912
|
+
} }
|
|
913
|
+
} }
|
|
914
|
+
}, async (ctx) => {
|
|
915
|
+
const { configId, keyId } = ctx.body;
|
|
916
|
+
const session = ctx.context.session;
|
|
917
|
+
if (session.user.banned === true) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.USER_BANNED);
|
|
918
|
+
const lookupOpts = resolveConfiguration(ctx.context, configurations, configId);
|
|
919
|
+
let apiKey = null;
|
|
920
|
+
apiKey = await getApiKeyById(ctx, keyId, lookupOpts);
|
|
921
|
+
if (!apiKey) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
922
|
+
if (!configIdMatches(apiKey.configId, lookupOpts.configId)) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
923
|
+
const opts = resolveConfiguration(ctx.context, configurations, apiKey.configId);
|
|
924
|
+
if ((opts.references ?? "user") === "organization") await checkOrgApiKeyPermission(ctx, session.user.id, apiKey.referenceId, "delete");
|
|
925
|
+
else if (apiKey.referenceId !== session.user.id) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
926
|
+
try {
|
|
927
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
928
|
+
await deleteApiKey$1(ctx, apiKey, opts);
|
|
929
|
+
await ctx.context.adapter.delete({
|
|
930
|
+
model: API_KEY_TABLE_NAME,
|
|
931
|
+
where: [{
|
|
932
|
+
field: "id",
|
|
933
|
+
value: apiKey.id
|
|
934
|
+
}]
|
|
935
|
+
});
|
|
936
|
+
} else if (opts.storage === "database") await ctx.context.adapter.delete({
|
|
937
|
+
model: API_KEY_TABLE_NAME,
|
|
938
|
+
where: [{
|
|
939
|
+
field: "id",
|
|
940
|
+
value: apiKey.id
|
|
941
|
+
}]
|
|
942
|
+
});
|
|
943
|
+
else await deleteApiKey$1(ctx, apiKey, opts);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
throw APIError$1.fromStatus("INTERNAL_SERVER_ERROR", { message: error?.message });
|
|
946
|
+
}
|
|
947
|
+
deleteAllExpiredApiKeys(ctx.context);
|
|
948
|
+
return ctx.json({ success: true });
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
//#endregion
|
|
953
|
+
//#region src/routes/get-api-key.ts
|
|
954
|
+
const getApiKeyQuerySchema = z$1.object({
|
|
955
|
+
configId: z$1.string().meta({ description: "The configuration ID to use for the API key lookup. If not provided, the default configuration will be used." }).optional(),
|
|
956
|
+
id: z$1.string().meta({ description: "The id of the Api Key" })
|
|
957
|
+
});
|
|
958
|
+
function getApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
959
|
+
return createAuthEndpoint("/api-key/get", {
|
|
960
|
+
method: "GET",
|
|
961
|
+
query: getApiKeyQuerySchema,
|
|
962
|
+
use: [sessionMiddleware],
|
|
963
|
+
metadata: { openapi: {
|
|
964
|
+
description: "Retrieve an existing API key by ID",
|
|
965
|
+
responses: { "200": {
|
|
966
|
+
description: "API key retrieved successfully",
|
|
967
|
+
content: { "application/json": { schema: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
id: {
|
|
971
|
+
type: "string",
|
|
972
|
+
description: "ID"
|
|
973
|
+
},
|
|
974
|
+
name: {
|
|
975
|
+
type: "string",
|
|
976
|
+
nullable: true,
|
|
977
|
+
description: "The name of the key"
|
|
978
|
+
},
|
|
979
|
+
start: {
|
|
980
|
+
type: "string",
|
|
981
|
+
nullable: true,
|
|
982
|
+
description: "Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key."
|
|
983
|
+
},
|
|
984
|
+
prefix: {
|
|
985
|
+
type: "string",
|
|
986
|
+
nullable: true,
|
|
987
|
+
description: "The API Key prefix. Stored as plain text."
|
|
988
|
+
},
|
|
989
|
+
userId: {
|
|
990
|
+
type: "string",
|
|
991
|
+
description: "The owner of the user id"
|
|
992
|
+
},
|
|
993
|
+
refillInterval: {
|
|
994
|
+
type: "number",
|
|
995
|
+
nullable: true,
|
|
996
|
+
description: "The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)"
|
|
997
|
+
},
|
|
998
|
+
refillAmount: {
|
|
999
|
+
type: "number",
|
|
1000
|
+
nullable: true,
|
|
1001
|
+
description: "The amount to refill"
|
|
1002
|
+
},
|
|
1003
|
+
lastRefillAt: {
|
|
1004
|
+
type: "string",
|
|
1005
|
+
format: "date-time",
|
|
1006
|
+
nullable: true,
|
|
1007
|
+
description: "The last refill date"
|
|
1008
|
+
},
|
|
1009
|
+
enabled: {
|
|
1010
|
+
type: "boolean",
|
|
1011
|
+
description: "Sets if key is enabled or disabled",
|
|
1012
|
+
default: true
|
|
1013
|
+
},
|
|
1014
|
+
rateLimitEnabled: {
|
|
1015
|
+
type: "boolean",
|
|
1016
|
+
description: "Whether the key has rate limiting enabled"
|
|
1017
|
+
},
|
|
1018
|
+
rateLimitTimeWindow: {
|
|
1019
|
+
type: "number",
|
|
1020
|
+
nullable: true,
|
|
1021
|
+
description: "The duration in milliseconds"
|
|
1022
|
+
},
|
|
1023
|
+
rateLimitMax: {
|
|
1024
|
+
type: "number",
|
|
1025
|
+
nullable: true,
|
|
1026
|
+
description: "Maximum amount of requests allowed within a window"
|
|
1027
|
+
},
|
|
1028
|
+
requestCount: {
|
|
1029
|
+
type: "number",
|
|
1030
|
+
description: "The number of requests made within the rate limit time window"
|
|
1031
|
+
},
|
|
1032
|
+
remaining: {
|
|
1033
|
+
type: "number",
|
|
1034
|
+
nullable: true,
|
|
1035
|
+
description: "Remaining requests (every time api key is used this should updated and should be updated on refill as well)"
|
|
1036
|
+
},
|
|
1037
|
+
lastRequest: {
|
|
1038
|
+
type: "string",
|
|
1039
|
+
format: "date-time",
|
|
1040
|
+
nullable: true,
|
|
1041
|
+
description: "When last request occurred"
|
|
1042
|
+
},
|
|
1043
|
+
expiresAt: {
|
|
1044
|
+
type: "string",
|
|
1045
|
+
format: "date-time",
|
|
1046
|
+
nullable: true,
|
|
1047
|
+
description: "Expiry date of a key"
|
|
1048
|
+
},
|
|
1049
|
+
createdAt: {
|
|
1050
|
+
type: "string",
|
|
1051
|
+
format: "date-time",
|
|
1052
|
+
description: "created at"
|
|
1053
|
+
},
|
|
1054
|
+
updatedAt: {
|
|
1055
|
+
type: "string",
|
|
1056
|
+
format: "date-time",
|
|
1057
|
+
description: "updated at"
|
|
1058
|
+
},
|
|
1059
|
+
metadata: {
|
|
1060
|
+
type: "object",
|
|
1061
|
+
nullable: true,
|
|
1062
|
+
additionalProperties: true,
|
|
1063
|
+
description: "Extra metadata about the apiKey"
|
|
1064
|
+
},
|
|
1065
|
+
permissions: {
|
|
1066
|
+
type: "string",
|
|
1067
|
+
nullable: true,
|
|
1068
|
+
description: "Permissions for the api key (stored as JSON string)"
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
required: [
|
|
1072
|
+
"id",
|
|
1073
|
+
"userId",
|
|
1074
|
+
"enabled",
|
|
1075
|
+
"rateLimitEnabled",
|
|
1076
|
+
"requestCount",
|
|
1077
|
+
"createdAt",
|
|
1078
|
+
"updatedAt"
|
|
1079
|
+
]
|
|
1080
|
+
} } }
|
|
1081
|
+
} }
|
|
1082
|
+
} }
|
|
1083
|
+
}, async (ctx) => {
|
|
1084
|
+
const { configId, id } = ctx.query;
|
|
1085
|
+
const session = ctx.context.session;
|
|
1086
|
+
const lookupOpts = resolveConfiguration(ctx.context, configurations, configId);
|
|
1087
|
+
let apiKey = null;
|
|
1088
|
+
apiKey = await getApiKeyById(ctx, id, lookupOpts);
|
|
1089
|
+
if (!apiKey) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1090
|
+
if (!configIdMatches(apiKey.configId, lookupOpts.configId)) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1091
|
+
const opts = resolveConfiguration(ctx.context, configurations, apiKey.configId);
|
|
1092
|
+
if ((opts.references ?? "user") === "organization") await checkOrgApiKeyPermission(ctx, session.user.id, apiKey.referenceId, "read");
|
|
1093
|
+
else if (apiKey.referenceId !== session.user.id) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1094
|
+
deleteAllExpiredApiKeys(ctx.context);
|
|
1095
|
+
const metadata = await migrateDoubleStringifiedMetadata(ctx, apiKey, opts);
|
|
1096
|
+
const { key: _key, ...returningApiKey } = apiKey;
|
|
1097
|
+
return ctx.json({
|
|
1098
|
+
...returningApiKey,
|
|
1099
|
+
metadata,
|
|
1100
|
+
permissions: returningApiKey.permissions ? safeJSONParse(returningApiKey.permissions) : null
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/routes/list-api-keys.ts
|
|
1107
|
+
/**
|
|
1108
|
+
* Generate a unique identifier for a configuration's storage backend.
|
|
1109
|
+
* Used to group configurations that share the same storage and avoid duplicate queries.
|
|
1110
|
+
*/
|
|
1111
|
+
function getStorageIdentifier(config) {
|
|
1112
|
+
if (config.storage === "database") return "database";
|
|
1113
|
+
if (config.customStorage) return `custom:${config.configId ?? "default"}`;
|
|
1114
|
+
return config.fallbackToDatabase ? "secondary-storage-with-fallback" : "secondary-storage";
|
|
1115
|
+
}
|
|
1116
|
+
const listApiKeysQuerySchema = z.object({
|
|
1117
|
+
configId: z.string().meta({ description: "Filter by configuration ID. If not provided, returns keys from all configurations." }).optional(),
|
|
1118
|
+
organizationId: z.string().meta({ description: "Organization ID to list keys for. If provided, returns organization-owned keys. If not provided, returns user-owned keys." }).optional(),
|
|
1119
|
+
limit: z.coerce.number().int().nonnegative().meta({ description: "The number of API keys to return" }).optional(),
|
|
1120
|
+
offset: z.coerce.number().int().nonnegative().meta({ description: "The offset to start from" }).optional(),
|
|
1121
|
+
sortBy: z.string().meta({ description: "The field to sort by (e.g., createdAt, name, expiresAt)" }).optional(),
|
|
1122
|
+
sortDirection: z.enum(["asc", "desc"]).meta({ description: "The direction to sort by" }).optional()
|
|
1123
|
+
}).optional();
|
|
1124
|
+
function listApiKeys({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
1125
|
+
return createAuthEndpoint("/api-key/list", {
|
|
1126
|
+
method: "GET",
|
|
1127
|
+
use: [sessionMiddleware],
|
|
1128
|
+
query: listApiKeysQuerySchema,
|
|
1129
|
+
metadata: { openapi: {
|
|
1130
|
+
description: "List all API keys for the authenticated user or for a specific organization",
|
|
1131
|
+
responses: { "200": {
|
|
1132
|
+
description: "API keys retrieved successfully",
|
|
1133
|
+
content: { "application/json": { schema: {
|
|
1134
|
+
type: "object",
|
|
1135
|
+
properties: {
|
|
1136
|
+
apiKeys: {
|
|
1137
|
+
type: "array",
|
|
1138
|
+
items: {
|
|
1139
|
+
type: "object",
|
|
1140
|
+
properties: {
|
|
1141
|
+
id: {
|
|
1142
|
+
type: "string",
|
|
1143
|
+
description: "ID"
|
|
1144
|
+
},
|
|
1145
|
+
name: {
|
|
1146
|
+
type: "string",
|
|
1147
|
+
nullable: true,
|
|
1148
|
+
description: "The name of the key"
|
|
1149
|
+
},
|
|
1150
|
+
start: {
|
|
1151
|
+
type: "string",
|
|
1152
|
+
nullable: true,
|
|
1153
|
+
description: "Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key."
|
|
1154
|
+
},
|
|
1155
|
+
prefix: {
|
|
1156
|
+
type: "string",
|
|
1157
|
+
nullable: true,
|
|
1158
|
+
description: "The API Key prefix. Stored as plain text."
|
|
1159
|
+
},
|
|
1160
|
+
userId: {
|
|
1161
|
+
type: "string",
|
|
1162
|
+
description: "The owner of the user id"
|
|
1163
|
+
},
|
|
1164
|
+
refillInterval: {
|
|
1165
|
+
type: "number",
|
|
1166
|
+
nullable: true,
|
|
1167
|
+
description: "The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)"
|
|
1168
|
+
},
|
|
1169
|
+
refillAmount: {
|
|
1170
|
+
type: "number",
|
|
1171
|
+
nullable: true,
|
|
1172
|
+
description: "The amount to refill"
|
|
1173
|
+
},
|
|
1174
|
+
lastRefillAt: {
|
|
1175
|
+
type: "string",
|
|
1176
|
+
format: "date-time",
|
|
1177
|
+
nullable: true,
|
|
1178
|
+
description: "The last refill date"
|
|
1179
|
+
},
|
|
1180
|
+
enabled: {
|
|
1181
|
+
type: "boolean",
|
|
1182
|
+
description: "Sets if key is enabled or disabled",
|
|
1183
|
+
default: true
|
|
1184
|
+
},
|
|
1185
|
+
rateLimitEnabled: {
|
|
1186
|
+
type: "boolean",
|
|
1187
|
+
description: "Whether the key has rate limiting enabled"
|
|
1188
|
+
},
|
|
1189
|
+
rateLimitTimeWindow: {
|
|
1190
|
+
type: "number",
|
|
1191
|
+
nullable: true,
|
|
1192
|
+
description: "The duration in milliseconds"
|
|
1193
|
+
},
|
|
1194
|
+
rateLimitMax: {
|
|
1195
|
+
type: "number",
|
|
1196
|
+
nullable: true,
|
|
1197
|
+
description: "Maximum amount of requests allowed within a window"
|
|
1198
|
+
},
|
|
1199
|
+
requestCount: {
|
|
1200
|
+
type: "number",
|
|
1201
|
+
description: "The number of requests made within the rate limit time window"
|
|
1202
|
+
},
|
|
1203
|
+
remaining: {
|
|
1204
|
+
type: "number",
|
|
1205
|
+
nullable: true,
|
|
1206
|
+
description: "Remaining requests (every time api key is used this should updated and should be updated on refill as well)"
|
|
1207
|
+
},
|
|
1208
|
+
lastRequest: {
|
|
1209
|
+
type: "string",
|
|
1210
|
+
format: "date-time",
|
|
1211
|
+
nullable: true,
|
|
1212
|
+
description: "When last request occurred"
|
|
1213
|
+
},
|
|
1214
|
+
expiresAt: {
|
|
1215
|
+
type: "string",
|
|
1216
|
+
format: "date-time",
|
|
1217
|
+
nullable: true,
|
|
1218
|
+
description: "Expiry date of a key"
|
|
1219
|
+
},
|
|
1220
|
+
createdAt: {
|
|
1221
|
+
type: "string",
|
|
1222
|
+
format: "date-time",
|
|
1223
|
+
description: "created at"
|
|
1224
|
+
},
|
|
1225
|
+
updatedAt: {
|
|
1226
|
+
type: "string",
|
|
1227
|
+
format: "date-time",
|
|
1228
|
+
description: "updated at"
|
|
1229
|
+
},
|
|
1230
|
+
metadata: {
|
|
1231
|
+
type: "object",
|
|
1232
|
+
nullable: true,
|
|
1233
|
+
additionalProperties: true,
|
|
1234
|
+
description: "Extra metadata about the apiKey"
|
|
1235
|
+
},
|
|
1236
|
+
permissions: {
|
|
1237
|
+
type: "string",
|
|
1238
|
+
nullable: true,
|
|
1239
|
+
description: "Permissions for the api key (stored as JSON string)"
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
required: [
|
|
1243
|
+
"id",
|
|
1244
|
+
"userId",
|
|
1245
|
+
"enabled",
|
|
1246
|
+
"rateLimitEnabled",
|
|
1247
|
+
"requestCount",
|
|
1248
|
+
"createdAt",
|
|
1249
|
+
"updatedAt"
|
|
1250
|
+
]
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
total: {
|
|
1254
|
+
type: "number",
|
|
1255
|
+
description: "Total number of API keys"
|
|
1256
|
+
},
|
|
1257
|
+
limit: {
|
|
1258
|
+
type: "number",
|
|
1259
|
+
nullable: true,
|
|
1260
|
+
description: "The limit used for pagination"
|
|
1261
|
+
},
|
|
1262
|
+
offset: {
|
|
1263
|
+
type: "number",
|
|
1264
|
+
nullable: true,
|
|
1265
|
+
description: "The offset used for pagination"
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
required: ["apiKeys", "total"]
|
|
1269
|
+
} } }
|
|
1270
|
+
} }
|
|
1271
|
+
} }
|
|
1272
|
+
}, async (ctx) => {
|
|
1273
|
+
const session = ctx.context.session;
|
|
1274
|
+
const configId = ctx.query?.configId;
|
|
1275
|
+
const organizationId = ctx.query?.organizationId;
|
|
1276
|
+
const limit = ctx.query?.limit != null ? Number(ctx.query.limit) : void 0;
|
|
1277
|
+
const offset = ctx.query?.offset != null ? Number(ctx.query.offset) : void 0;
|
|
1278
|
+
if (organizationId) await checkOrgApiKeyPermission(ctx, session.user.id, organizationId, "read");
|
|
1279
|
+
const referenceId = organizationId ?? session.user.id;
|
|
1280
|
+
const expectedReferencesType = organizationId ? "organization" : "user";
|
|
1281
|
+
let allApiKeys = [];
|
|
1282
|
+
if (configId) {
|
|
1283
|
+
const { apiKeys } = await listApiKeys$1(ctx, referenceId, resolveConfiguration(ctx.context, configurations, configId), {
|
|
1284
|
+
limit: void 0,
|
|
1285
|
+
offset: void 0,
|
|
1286
|
+
sortBy: ctx.query?.sortBy,
|
|
1287
|
+
sortDirection: ctx.query?.sortDirection
|
|
1288
|
+
});
|
|
1289
|
+
allApiKeys = apiKeys;
|
|
1290
|
+
} else {
|
|
1291
|
+
const storageGroups = /* @__PURE__ */ new Map();
|
|
1292
|
+
for (const config of configurations) {
|
|
1293
|
+
const storageKey = getStorageIdentifier(config);
|
|
1294
|
+
if (!storageGroups.has(storageKey)) storageGroups.set(storageKey, config);
|
|
1295
|
+
}
|
|
1296
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1297
|
+
for (const opts of storageGroups.values()) {
|
|
1298
|
+
const { apiKeys } = await listApiKeys$1(ctx, referenceId, opts, {
|
|
1299
|
+
limit: void 0,
|
|
1300
|
+
offset: void 0,
|
|
1301
|
+
sortBy: ctx.query?.sortBy,
|
|
1302
|
+
sortDirection: ctx.query?.sortDirection
|
|
1303
|
+
});
|
|
1304
|
+
for (const key of apiKeys) if (!seenIds.has(key.id)) {
|
|
1305
|
+
seenIds.add(key.id);
|
|
1306
|
+
allApiKeys.push(key);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
let filteredApiKeys = allApiKeys.filter((key) => {
|
|
1311
|
+
return (configurations.find((c) => {
|
|
1312
|
+
if (isDefaultConfigId(key.configId)) return isDefaultConfigId(c.configId);
|
|
1313
|
+
return c.configId === key.configId;
|
|
1314
|
+
})?.references ?? "user") === expectedReferencesType && key.referenceId === referenceId;
|
|
1315
|
+
});
|
|
1316
|
+
if (configId) filteredApiKeys = filteredApiKeys.filter((key) => configIdMatches(key.configId, configId));
|
|
1317
|
+
const total = filteredApiKeys.length;
|
|
1318
|
+
let paginatedApiKeys = filteredApiKeys;
|
|
1319
|
+
if (offset !== void 0) paginatedApiKeys = paginatedApiKeys.slice(offset);
|
|
1320
|
+
if (limit !== void 0) paginatedApiKeys = paginatedApiKeys.slice(0, limit);
|
|
1321
|
+
deleteAllExpiredApiKeys(ctx.context);
|
|
1322
|
+
const returningApiKeys = paginatedApiKeys.map((apiKey) => {
|
|
1323
|
+
const { key: _key, ...rest } = apiKey;
|
|
1324
|
+
return {
|
|
1325
|
+
...rest,
|
|
1326
|
+
metadata: parseDoubleStringifiedMetadata(apiKey.metadata),
|
|
1327
|
+
permissions: rest.permissions ? safeJSONParse(rest.permissions) : null
|
|
1328
|
+
};
|
|
1329
|
+
});
|
|
1330
|
+
const dbConfig = configurations.find((c) => c.storage === "database" || c.fallbackToDatabase);
|
|
1331
|
+
if (dbConfig) await ctx.context.runInBackgroundOrAwait(batchMigrateLegacyMetadata(ctx, paginatedApiKeys, dbConfig));
|
|
1332
|
+
return ctx.json({
|
|
1333
|
+
apiKeys: returningApiKeys,
|
|
1334
|
+
total,
|
|
1335
|
+
limit,
|
|
1336
|
+
offset
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
//#endregion
|
|
1342
|
+
//#region src/routes/update-api-key.ts
|
|
1343
|
+
const updateApiKeyBodySchema = z$1.object({
|
|
1344
|
+
configId: z$1.string().meta({ description: "The configuration ID to use for the API key lookup. If not provided, the default configuration will be used." }).optional(),
|
|
1345
|
+
keyId: z$1.string().meta({ description: "The id of the Api Key" }),
|
|
1346
|
+
userId: z$1.coerce.string().meta({ description: "The id of the user which the api key belongs to. server-only. Eg: \"some-user-id\"" }).optional(),
|
|
1347
|
+
name: z$1.string().meta({ description: "The name of the key" }).optional(),
|
|
1348
|
+
enabled: z$1.boolean().meta({ description: "Whether the Api Key is enabled or not" }).optional(),
|
|
1349
|
+
remaining: z$1.number().meta({ description: "The number of remaining requests" }).min(1).optional(),
|
|
1350
|
+
refillAmount: z$1.number().meta({ description: "The refill amount" }).optional(),
|
|
1351
|
+
refillInterval: z$1.number().meta({ description: "The refill interval" }).optional(),
|
|
1352
|
+
metadata: z$1.any().optional(),
|
|
1353
|
+
expiresIn: z$1.number().meta({ description: "Expiration time of the Api Key in seconds" }).min(1).optional().nullable(),
|
|
1354
|
+
rateLimitEnabled: z$1.boolean().meta({ description: "Whether the key has rate limiting enabled." }).optional(),
|
|
1355
|
+
rateLimitTimeWindow: z$1.number().meta({ description: "The duration in milliseconds where each request is counted. server-only. Eg: 1000" }).optional(),
|
|
1356
|
+
rateLimitMax: z$1.number().meta({ description: "Maximum amount of requests allowed within a window. Once the `maxRequests` is reached, the request will be rejected until the `timeWindow` has passed, at which point the `timeWindow` will be reset. server-only. Eg: 100" }).optional(),
|
|
1357
|
+
permissions: z$1.record(z$1.string(), z$1.array(z$1.string())).meta({ description: "Update the permissions on the API Key. server-only." }).optional().nullable()
|
|
1358
|
+
});
|
|
1359
|
+
function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
1360
|
+
return createAuthEndpoint("/api-key/update", {
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
body: updateApiKeyBodySchema,
|
|
1363
|
+
metadata: { openapi: {
|
|
1364
|
+
description: "Update an existing API key by ID",
|
|
1365
|
+
responses: { "200": {
|
|
1366
|
+
description: "API key updated successfully",
|
|
1367
|
+
content: { "application/json": { schema: {
|
|
1368
|
+
type: "object",
|
|
1369
|
+
properties: {
|
|
1370
|
+
id: {
|
|
1371
|
+
type: "string",
|
|
1372
|
+
description: "ID"
|
|
1373
|
+
},
|
|
1374
|
+
name: {
|
|
1375
|
+
type: "string",
|
|
1376
|
+
nullable: true,
|
|
1377
|
+
description: "The name of the key"
|
|
1378
|
+
},
|
|
1379
|
+
start: {
|
|
1380
|
+
type: "string",
|
|
1381
|
+
nullable: true,
|
|
1382
|
+
description: "Shows the first few characters of the API key, including the prefix. This allows you to show those few characters in the UI to make it easier for users to identify the API key."
|
|
1383
|
+
},
|
|
1384
|
+
prefix: {
|
|
1385
|
+
type: "string",
|
|
1386
|
+
nullable: true,
|
|
1387
|
+
description: "The API Key prefix. Stored as plain text."
|
|
1388
|
+
},
|
|
1389
|
+
userId: {
|
|
1390
|
+
type: "string",
|
|
1391
|
+
description: "The owner of the user id"
|
|
1392
|
+
},
|
|
1393
|
+
refillInterval: {
|
|
1394
|
+
type: "number",
|
|
1395
|
+
nullable: true,
|
|
1396
|
+
description: "The interval in milliseconds between refills of the `remaining` count. Example: 3600000 // refill every hour (3600000ms = 1h)"
|
|
1397
|
+
},
|
|
1398
|
+
refillAmount: {
|
|
1399
|
+
type: "number",
|
|
1400
|
+
nullable: true,
|
|
1401
|
+
description: "The amount to refill"
|
|
1402
|
+
},
|
|
1403
|
+
lastRefillAt: {
|
|
1404
|
+
type: "string",
|
|
1405
|
+
format: "date-time",
|
|
1406
|
+
nullable: true,
|
|
1407
|
+
description: "The last refill date"
|
|
1408
|
+
},
|
|
1409
|
+
enabled: {
|
|
1410
|
+
type: "boolean",
|
|
1411
|
+
description: "Sets if key is enabled or disabled",
|
|
1412
|
+
default: true
|
|
1413
|
+
},
|
|
1414
|
+
rateLimitEnabled: {
|
|
1415
|
+
type: "boolean",
|
|
1416
|
+
description: "Whether the key has rate limiting enabled"
|
|
1417
|
+
},
|
|
1418
|
+
rateLimitTimeWindow: {
|
|
1419
|
+
type: "number",
|
|
1420
|
+
nullable: true,
|
|
1421
|
+
description: "The duration in milliseconds"
|
|
1422
|
+
},
|
|
1423
|
+
rateLimitMax: {
|
|
1424
|
+
type: "number",
|
|
1425
|
+
nullable: true,
|
|
1426
|
+
description: "Maximum amount of requests allowed within a window"
|
|
1427
|
+
},
|
|
1428
|
+
requestCount: {
|
|
1429
|
+
type: "number",
|
|
1430
|
+
description: "The number of requests made within the rate limit time window"
|
|
1431
|
+
},
|
|
1432
|
+
remaining: {
|
|
1433
|
+
type: "number",
|
|
1434
|
+
nullable: true,
|
|
1435
|
+
description: "Remaining requests (every time api key is used this should updated and should be updated on refill as well)"
|
|
1436
|
+
},
|
|
1437
|
+
lastRequest: {
|
|
1438
|
+
type: "string",
|
|
1439
|
+
format: "date-time",
|
|
1440
|
+
nullable: true,
|
|
1441
|
+
description: "When last request occurred"
|
|
1442
|
+
},
|
|
1443
|
+
expiresAt: {
|
|
1444
|
+
type: "string",
|
|
1445
|
+
format: "date-time",
|
|
1446
|
+
nullable: true,
|
|
1447
|
+
description: "Expiry date of a key"
|
|
1448
|
+
},
|
|
1449
|
+
createdAt: {
|
|
1450
|
+
type: "string",
|
|
1451
|
+
format: "date-time",
|
|
1452
|
+
description: "created at"
|
|
1453
|
+
},
|
|
1454
|
+
updatedAt: {
|
|
1455
|
+
type: "string",
|
|
1456
|
+
format: "date-time",
|
|
1457
|
+
description: "updated at"
|
|
1458
|
+
},
|
|
1459
|
+
metadata: {
|
|
1460
|
+
type: "object",
|
|
1461
|
+
nullable: true,
|
|
1462
|
+
additionalProperties: true,
|
|
1463
|
+
description: "Extra metadata about the apiKey"
|
|
1464
|
+
},
|
|
1465
|
+
permissions: {
|
|
1466
|
+
type: "string",
|
|
1467
|
+
nullable: true,
|
|
1468
|
+
description: "Permissions for the api key (stored as JSON string)"
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
required: [
|
|
1472
|
+
"id",
|
|
1473
|
+
"userId",
|
|
1474
|
+
"enabled",
|
|
1475
|
+
"rateLimitEnabled",
|
|
1476
|
+
"requestCount",
|
|
1477
|
+
"createdAt",
|
|
1478
|
+
"updatedAt"
|
|
1479
|
+
]
|
|
1480
|
+
} } }
|
|
1481
|
+
} }
|
|
1482
|
+
} }
|
|
1483
|
+
}, async (ctx) => {
|
|
1484
|
+
const { configId, keyId, expiresIn, enabled, metadata, refillAmount, refillInterval, remaining, name, permissions, rateLimitEnabled, rateLimitTimeWindow, rateLimitMax } = ctx.body;
|
|
1485
|
+
const session = await getSessionFromCtx(ctx);
|
|
1486
|
+
const authRequired = ctx.request || ctx.headers;
|
|
1487
|
+
const user = authRequired && !session ? null : session?.user || { id: ctx.body.userId };
|
|
1488
|
+
if (!user?.id) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
1489
|
+
if (session && ctx.body.userId && session?.user.id !== ctx.body.userId) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
1490
|
+
if (authRequired) {
|
|
1491
|
+
if (refillAmount !== void 0 || refillInterval !== void 0 || rateLimitMax !== void 0 || rateLimitTimeWindow !== void 0 || rateLimitEnabled !== void 0 || remaining !== void 0 || permissions !== void 0) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.SERVER_ONLY_PROPERTY);
|
|
1492
|
+
}
|
|
1493
|
+
const lookupOpts = resolveConfiguration(ctx.context, configurations, configId);
|
|
1494
|
+
let apiKey = null;
|
|
1495
|
+
apiKey = await getApiKeyById(ctx, keyId, lookupOpts);
|
|
1496
|
+
if (!apiKey) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1497
|
+
if (!configIdMatches(apiKey.configId, lookupOpts.configId)) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1498
|
+
const opts = resolveConfiguration(ctx.context, configurations, apiKey.configId);
|
|
1499
|
+
if ((opts.references ?? "user") === "organization") await checkOrgApiKeyPermission(ctx, user.id, apiKey.referenceId, "update");
|
|
1500
|
+
else if (apiKey.referenceId !== user.id) throw APIError$1.from("NOT_FOUND", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1501
|
+
const newValues = {};
|
|
1502
|
+
if (name !== void 0) {
|
|
1503
|
+
if (name.length < opts.minimumNameLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_NAME_LENGTH);
|
|
1504
|
+
else if (name.length > opts.maximumNameLength) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_NAME_LENGTH);
|
|
1505
|
+
newValues.name = name;
|
|
1506
|
+
}
|
|
1507
|
+
if (enabled !== void 0) newValues.enabled = enabled;
|
|
1508
|
+
if (expiresIn !== void 0) {
|
|
1509
|
+
if (opts.keyExpiration.disableCustomExpiresTime === true) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.KEY_DISABLED_EXPIRATION);
|
|
1510
|
+
if (expiresIn !== null) {
|
|
1511
|
+
const expiresIn_in_days = expiresIn / (3600 * 24);
|
|
1512
|
+
if (expiresIn_in_days < opts.keyExpiration.minExpiresIn) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.EXPIRES_IN_IS_TOO_SMALL);
|
|
1513
|
+
else if (expiresIn_in_days > opts.keyExpiration.maxExpiresIn) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.EXPIRES_IN_IS_TOO_LARGE);
|
|
1514
|
+
}
|
|
1515
|
+
newValues.expiresAt = expiresIn ? getDate(expiresIn, "sec") : null;
|
|
1516
|
+
}
|
|
1517
|
+
if (metadata !== void 0 && opts.enableMetadata === true) {
|
|
1518
|
+
if (typeof metadata !== "object") throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_METADATA_TYPE);
|
|
1519
|
+
newValues.metadata = metadata;
|
|
1520
|
+
}
|
|
1521
|
+
if (remaining !== void 0) newValues.remaining = remaining;
|
|
1522
|
+
if (refillAmount !== void 0 || refillInterval !== void 0) {
|
|
1523
|
+
if (refillAmount !== void 0 && refillInterval === void 0) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.REFILL_AMOUNT_AND_INTERVAL_REQUIRED);
|
|
1524
|
+
else if (refillInterval !== void 0 && refillAmount === void 0) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.REFILL_INTERVAL_AND_AMOUNT_REQUIRED);
|
|
1525
|
+
newValues.refillAmount = refillAmount;
|
|
1526
|
+
newValues.refillInterval = refillInterval;
|
|
1527
|
+
}
|
|
1528
|
+
if (rateLimitEnabled !== void 0) newValues.rateLimitEnabled = rateLimitEnabled;
|
|
1529
|
+
if (rateLimitTimeWindow !== void 0) newValues.rateLimitTimeWindow = rateLimitTimeWindow;
|
|
1530
|
+
if (rateLimitMax !== void 0) newValues.rateLimitMax = rateLimitMax;
|
|
1531
|
+
if (permissions !== void 0) newValues.permissions = JSON.stringify(permissions);
|
|
1532
|
+
if (Object.keys(newValues).length === 0) throw APIError$1.from("BAD_REQUEST", API_KEY_ERROR_CODES.NO_VALUES_TO_UPDATE);
|
|
1533
|
+
let newApiKey = apiKey;
|
|
1534
|
+
try {
|
|
1535
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
1536
|
+
const dbUpdated = await ctx.context.adapter.update({
|
|
1537
|
+
model: API_KEY_TABLE_NAME,
|
|
1538
|
+
where: [{
|
|
1539
|
+
field: "id",
|
|
1540
|
+
value: apiKey.id
|
|
1541
|
+
}],
|
|
1542
|
+
update: newValues
|
|
1543
|
+
});
|
|
1544
|
+
if (dbUpdated) {
|
|
1545
|
+
await setApiKey(ctx, dbUpdated, opts);
|
|
1546
|
+
newApiKey = dbUpdated;
|
|
1547
|
+
}
|
|
1548
|
+
} else if (opts.storage === "database") {
|
|
1549
|
+
const result = await ctx.context.adapter.update({
|
|
1550
|
+
model: API_KEY_TABLE_NAME,
|
|
1551
|
+
where: [{
|
|
1552
|
+
field: "id",
|
|
1553
|
+
value: apiKey.id
|
|
1554
|
+
}],
|
|
1555
|
+
update: newValues
|
|
1556
|
+
});
|
|
1557
|
+
if (result) newApiKey = result;
|
|
1558
|
+
} else {
|
|
1559
|
+
const updated = {
|
|
1560
|
+
...apiKey,
|
|
1561
|
+
...newValues,
|
|
1562
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1563
|
+
};
|
|
1564
|
+
await setApiKey(ctx, updated, opts);
|
|
1565
|
+
newApiKey = updated;
|
|
1566
|
+
}
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
throw APIError$1.fromStatus("INTERNAL_SERVER_ERROR", { message: error?.message });
|
|
1569
|
+
}
|
|
1570
|
+
deleteAllExpiredApiKeys(ctx.context);
|
|
1571
|
+
const migratedMetadata = await migrateDoubleStringifiedMetadata(ctx, newApiKey, opts);
|
|
1572
|
+
const { key: _key, ...returningApiKey } = newApiKey;
|
|
1573
|
+
return ctx.json({
|
|
1574
|
+
...returningApiKey,
|
|
1575
|
+
metadata: migratedMetadata,
|
|
1576
|
+
permissions: returningApiKey.permissions ? safeJSONParse(returningApiKey.permissions) : null
|
|
1577
|
+
});
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/rate-limit.ts
|
|
1583
|
+
/**
|
|
1584
|
+
* Determines if a request is allowed based on rate limiting parameters.
|
|
1585
|
+
*
|
|
1586
|
+
* @returns An object indicating whether the request is allowed and, if not,
|
|
1587
|
+
* a message and updated ApiKey data.
|
|
1588
|
+
*/
|
|
1589
|
+
function isRateLimited(apiKey, opts) {
|
|
1590
|
+
const now = /* @__PURE__ */ new Date();
|
|
1591
|
+
const lastRequest = apiKey.lastRequest;
|
|
1592
|
+
const rateLimitTimeWindow = apiKey.rateLimitTimeWindow;
|
|
1593
|
+
const rateLimitMax = apiKey.rateLimitMax;
|
|
1594
|
+
let requestCount = apiKey.requestCount;
|
|
1595
|
+
if (opts.rateLimit.enabled === false) return {
|
|
1596
|
+
success: true,
|
|
1597
|
+
message: null,
|
|
1598
|
+
update: { lastRequest: now },
|
|
1599
|
+
tryAgainIn: null
|
|
1600
|
+
};
|
|
1601
|
+
if (apiKey.rateLimitEnabled === false) return {
|
|
1602
|
+
success: true,
|
|
1603
|
+
message: null,
|
|
1604
|
+
update: { lastRequest: now },
|
|
1605
|
+
tryAgainIn: null
|
|
1606
|
+
};
|
|
1607
|
+
if (rateLimitTimeWindow === null || rateLimitMax === null) return {
|
|
1608
|
+
success: true,
|
|
1609
|
+
message: null,
|
|
1610
|
+
update: null,
|
|
1611
|
+
tryAgainIn: null
|
|
1612
|
+
};
|
|
1613
|
+
if (lastRequest === null) return {
|
|
1614
|
+
success: true,
|
|
1615
|
+
message: null,
|
|
1616
|
+
update: {
|
|
1617
|
+
lastRequest: now,
|
|
1618
|
+
requestCount: 1
|
|
1619
|
+
},
|
|
1620
|
+
tryAgainIn: null
|
|
1621
|
+
};
|
|
1622
|
+
const timeSinceLastRequest = now.getTime() - new Date(lastRequest).getTime();
|
|
1623
|
+
if (timeSinceLastRequest > rateLimitTimeWindow) return {
|
|
1624
|
+
success: true,
|
|
1625
|
+
message: null,
|
|
1626
|
+
update: {
|
|
1627
|
+
lastRequest: now,
|
|
1628
|
+
requestCount: 1
|
|
1629
|
+
},
|
|
1630
|
+
tryAgainIn: null
|
|
1631
|
+
};
|
|
1632
|
+
if (requestCount >= rateLimitMax) return {
|
|
1633
|
+
success: false,
|
|
1634
|
+
message: API_KEY_ERROR_CODES.RATE_LIMIT_EXCEEDED.message,
|
|
1635
|
+
update: null,
|
|
1636
|
+
tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest)
|
|
1637
|
+
};
|
|
1638
|
+
requestCount++;
|
|
1639
|
+
return {
|
|
1640
|
+
success: true,
|
|
1641
|
+
message: null,
|
|
1642
|
+
tryAgainIn: null,
|
|
1643
|
+
update: {
|
|
1644
|
+
lastRequest: now,
|
|
1645
|
+
requestCount
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/routes/verify-api-key.ts
|
|
1652
|
+
async function validateApiKey({ hashedKey, ctx, opts, schema, permissions }) {
|
|
1653
|
+
const apiKey = await getApiKey$1(ctx, hashedKey, opts);
|
|
1654
|
+
if (!apiKey) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1655
|
+
if (apiKey.enabled === false) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_DISABLED);
|
|
1656
|
+
if (apiKey.expiresAt) {
|
|
1657
|
+
if (Date.now() > new Date(apiKey.expiresAt).getTime()) {
|
|
1658
|
+
const deleteExpiredKey = async () => {
|
|
1659
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
1660
|
+
await deleteApiKey$1(ctx, apiKey, opts);
|
|
1661
|
+
await ctx.context.adapter.delete({
|
|
1662
|
+
model: API_KEY_TABLE_NAME,
|
|
1663
|
+
where: [{
|
|
1664
|
+
field: "id",
|
|
1665
|
+
value: apiKey.id
|
|
1666
|
+
}]
|
|
1667
|
+
});
|
|
1668
|
+
} else if (opts.storage === "secondary-storage") await deleteApiKey$1(ctx, apiKey, opts);
|
|
1669
|
+
else await ctx.context.adapter.delete({
|
|
1670
|
+
model: API_KEY_TABLE_NAME,
|
|
1671
|
+
where: [{
|
|
1672
|
+
field: "id",
|
|
1673
|
+
value: apiKey.id
|
|
1674
|
+
}]
|
|
1675
|
+
});
|
|
1676
|
+
};
|
|
1677
|
+
if (opts.deferUpdates) ctx.context.runInBackground(deleteExpiredKey().catch((error) => {
|
|
1678
|
+
ctx.context.logger.error("Deferred update failed:", error);
|
|
1679
|
+
}));
|
|
1680
|
+
else await deleteExpiredKey();
|
|
1681
|
+
throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_EXPIRED);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (permissions) {
|
|
1685
|
+
const apiKeyPermissions = apiKey.permissions ? safeJSONParse(apiKey.permissions) : null;
|
|
1686
|
+
if (!apiKeyPermissions) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1687
|
+
if (!role(apiKeyPermissions).authorize(permissions).success) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1688
|
+
}
|
|
1689
|
+
let remaining = apiKey.remaining;
|
|
1690
|
+
let lastRefillAt = apiKey.lastRefillAt;
|
|
1691
|
+
if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
|
|
1692
|
+
const deleteExhaustedKey = async () => {
|
|
1693
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
1694
|
+
await deleteApiKey$1(ctx, apiKey, opts);
|
|
1695
|
+
await ctx.context.adapter.delete({
|
|
1696
|
+
model: API_KEY_TABLE_NAME,
|
|
1697
|
+
where: [{
|
|
1698
|
+
field: "id",
|
|
1699
|
+
value: apiKey.id
|
|
1700
|
+
}]
|
|
1701
|
+
});
|
|
1702
|
+
} else if (opts.storage === "secondary-storage") await deleteApiKey$1(ctx, apiKey, opts);
|
|
1703
|
+
else await ctx.context.adapter.delete({
|
|
1704
|
+
model: API_KEY_TABLE_NAME,
|
|
1705
|
+
where: [{
|
|
1706
|
+
field: "id",
|
|
1707
|
+
value: apiKey.id
|
|
1708
|
+
}]
|
|
1709
|
+
});
|
|
1710
|
+
};
|
|
1711
|
+
if (opts.deferUpdates) ctx.context.runInBackground(deleteExhaustedKey().catch((error) => {
|
|
1712
|
+
ctx.context.logger.error("Deferred update failed:", error);
|
|
1713
|
+
}));
|
|
1714
|
+
else await deleteExhaustedKey();
|
|
1715
|
+
throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1716
|
+
} else if (remaining !== null) {
|
|
1717
|
+
const now = Date.now();
|
|
1718
|
+
const refillInterval = apiKey.refillInterval;
|
|
1719
|
+
const refillAmount = apiKey.refillAmount;
|
|
1720
|
+
const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
|
|
1721
|
+
if (refillInterval && refillAmount) {
|
|
1722
|
+
if (now - lastTime > refillInterval) {
|
|
1723
|
+
remaining = refillAmount;
|
|
1724
|
+
lastRefillAt = /* @__PURE__ */ new Date();
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1728
|
+
else remaining--;
|
|
1729
|
+
}
|
|
1730
|
+
const { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);
|
|
1731
|
+
if (success === false) throw new APIError$1("UNAUTHORIZED", {
|
|
1732
|
+
message: message ?? void 0,
|
|
1733
|
+
code: "RATE_LIMITED",
|
|
1734
|
+
details: { tryAgainIn }
|
|
1735
|
+
});
|
|
1736
|
+
const updated = {
|
|
1737
|
+
...apiKey,
|
|
1738
|
+
...update,
|
|
1739
|
+
remaining,
|
|
1740
|
+
lastRefillAt,
|
|
1741
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1742
|
+
};
|
|
1743
|
+
const performUpdate = async () => {
|
|
1744
|
+
if (opts.storage === "database") return ctx.context.adapter.update({
|
|
1745
|
+
model: API_KEY_TABLE_NAME,
|
|
1746
|
+
where: [{
|
|
1747
|
+
field: "id",
|
|
1748
|
+
value: apiKey.id
|
|
1749
|
+
}],
|
|
1750
|
+
update: {
|
|
1751
|
+
...updated,
|
|
1752
|
+
id: void 0
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
else if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
1756
|
+
const dbUpdated = await ctx.context.adapter.update({
|
|
1757
|
+
model: API_KEY_TABLE_NAME,
|
|
1758
|
+
where: [{
|
|
1759
|
+
field: "id",
|
|
1760
|
+
value: apiKey.id
|
|
1761
|
+
}],
|
|
1762
|
+
update: {
|
|
1763
|
+
...updated,
|
|
1764
|
+
id: void 0
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
if (dbUpdated) await setApiKey(ctx, dbUpdated, opts);
|
|
1768
|
+
return dbUpdated;
|
|
1769
|
+
} else {
|
|
1770
|
+
await setApiKey(ctx, updated, opts);
|
|
1771
|
+
return updated;
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
let newApiKey = null;
|
|
1775
|
+
if (opts.deferUpdates) {
|
|
1776
|
+
ctx.context.runInBackground(performUpdate().catch((error) => {
|
|
1777
|
+
ctx.context.logger.error("Failed to update API key:", error);
|
|
1778
|
+
}));
|
|
1779
|
+
newApiKey = updated;
|
|
1780
|
+
} else {
|
|
1781
|
+
newApiKey = await performUpdate();
|
|
1782
|
+
if (!newApiKey) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
|
|
1783
|
+
}
|
|
1784
|
+
return newApiKey;
|
|
1785
|
+
}
|
|
1786
|
+
const verifyApiKeyBodySchema = z$1.object({
|
|
1787
|
+
configId: z$1.string().meta({ description: "The configuration ID to use for verification. If not provided, the default configuration will be used." }).optional(),
|
|
1788
|
+
key: z$1.string().meta({ description: "The key to verify" }),
|
|
1789
|
+
permissions: z$1.record(z$1.string(), z$1.array(z$1.string())).meta({ description: "The permissions to verify." }).optional()
|
|
1790
|
+
});
|
|
1791
|
+
function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
1792
|
+
return createAuthEndpoint({
|
|
1793
|
+
method: "POST",
|
|
1794
|
+
body: verifyApiKeyBodySchema
|
|
1795
|
+
}, async (ctx) => {
|
|
1796
|
+
const { configId, key } = ctx.body;
|
|
1797
|
+
const lookupOpts = resolveConfiguration(ctx.context, configurations, configId);
|
|
1798
|
+
if (lookupOpts.customAPIKeyValidator) {
|
|
1799
|
+
if (!await lookupOpts.customAPIKeyValidator({
|
|
1800
|
+
ctx,
|
|
1801
|
+
key
|
|
1802
|
+
})) return ctx.json({
|
|
1803
|
+
valid: false,
|
|
1804
|
+
error: {
|
|
1805
|
+
message: API_KEY_ERROR_CODES.INVALID_API_KEY,
|
|
1806
|
+
code: "KEY_NOT_FOUND"
|
|
1807
|
+
},
|
|
1808
|
+
key: null
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
const hashed = lookupOpts.disableKeyHashing ? key : await defaultKeyHasher(key);
|
|
1812
|
+
let apiKey = null;
|
|
1813
|
+
try {
|
|
1814
|
+
apiKey = await validateApiKey({
|
|
1815
|
+
hashedKey: hashed,
|
|
1816
|
+
permissions: ctx.body.permissions,
|
|
1817
|
+
ctx,
|
|
1818
|
+
opts: lookupOpts,
|
|
1819
|
+
schema
|
|
1820
|
+
});
|
|
1821
|
+
if ((apiKey ? resolveConfiguration(ctx.context, configurations, apiKey.configId) : lookupOpts).deferUpdates) ctx.context.runInBackground(deleteAllExpiredApiKeys(ctx.context).catch((err) => {
|
|
1822
|
+
ctx.context.logger.error("Failed to delete expired API keys:", err);
|
|
1823
|
+
}));
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
ctx.context.logger.error("Failed to validate API key:", error);
|
|
1826
|
+
if (isAPIError(error)) return ctx.json({
|
|
1827
|
+
valid: false,
|
|
1828
|
+
error: {
|
|
1829
|
+
...error.body,
|
|
1830
|
+
message: error.body?.message,
|
|
1831
|
+
code: error.body?.code
|
|
1832
|
+
},
|
|
1833
|
+
key: null
|
|
1834
|
+
});
|
|
1835
|
+
return ctx.json({
|
|
1836
|
+
valid: false,
|
|
1837
|
+
error: {
|
|
1838
|
+
message: API_KEY_ERROR_CODES.INVALID_API_KEY,
|
|
1839
|
+
code: "INVALID_API_KEY"
|
|
1840
|
+
},
|
|
1841
|
+
key: null
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
const { key: _, ...returningApiKey } = apiKey ?? {
|
|
1845
|
+
key: 1,
|
|
1846
|
+
permissions: void 0
|
|
1847
|
+
};
|
|
1848
|
+
const opts = apiKey ? resolveConfiguration(ctx.context, configurations, apiKey.configId) : lookupOpts;
|
|
1849
|
+
let migratedMetadata = null;
|
|
1850
|
+
if (apiKey) migratedMetadata = await migrateDoubleStringifiedMetadata(ctx, apiKey, opts);
|
|
1851
|
+
returningApiKey.permissions = returningApiKey.permissions ? safeJSONParse(returningApiKey.permissions) : null;
|
|
1852
|
+
return ctx.json({
|
|
1853
|
+
valid: true,
|
|
1854
|
+
error: null,
|
|
1855
|
+
key: apiKey === null ? null : {
|
|
1856
|
+
...returningApiKey,
|
|
1857
|
+
metadata: migratedMetadata
|
|
1858
|
+
}
|
|
1859
|
+
});
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
//#endregion
|
|
1864
|
+
//#region src/routes/index.ts
|
|
1865
|
+
function resolveConfiguration(authContext, configurations, configId) {
|
|
1866
|
+
const getDefaultConfig = () => {
|
|
1867
|
+
const defaultConfig = configurations.find((c) => !c.configId || c.configId === "default");
|
|
1868
|
+
if (!defaultConfig) {
|
|
1869
|
+
authContext.logger.error("No default api-key configuration found. Either provide an api-key configuration with configId 'default' or provide a configuration with no `configId` set.");
|
|
1870
|
+
const error = API_KEY_ERROR_CODES.NO_DEFAULT_API_KEY_CONFIGURATION_FOUND;
|
|
1871
|
+
throw APIError.from("BAD_REQUEST", error);
|
|
1872
|
+
}
|
|
1873
|
+
return {
|
|
1874
|
+
...defaultConfig,
|
|
1875
|
+
configId: "default"
|
|
1876
|
+
};
|
|
1877
|
+
};
|
|
1878
|
+
if (!configId) return getDefaultConfig();
|
|
1879
|
+
return configurations.find((c) => c.configId === configId) ?? getDefaultConfig();
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Checks if a configId value represents the default configuration.
|
|
1883
|
+
* Treats null, undefined, and "default" as equivalent (all are default).
|
|
1884
|
+
* This handles backward compatibility for keys created before the configId field existed.
|
|
1885
|
+
*/
|
|
1886
|
+
function isDefaultConfigId(configId) {
|
|
1887
|
+
return !configId || configId === "default";
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Checks if two configId values match, treating null/undefined as "default".
|
|
1891
|
+
* This handles backward compatibility for keys created before the configId field existed.
|
|
1892
|
+
*/
|
|
1893
|
+
function configIdMatches(keyConfigId, expectedConfigId) {
|
|
1894
|
+
if (isDefaultConfigId(keyConfigId) && isDefaultConfigId(expectedConfigId)) return true;
|
|
1895
|
+
return keyConfigId === expectedConfigId;
|
|
1896
|
+
}
|
|
1897
|
+
let lastChecked = null;
|
|
1898
|
+
async function deleteAllExpiredApiKeys(ctx, byPassLastCheckTime = false) {
|
|
1899
|
+
if (lastChecked && !byPassLastCheckTime) {
|
|
1900
|
+
if ((/* @__PURE__ */ new Date()).getTime() - lastChecked.getTime() < 1e4) return;
|
|
1901
|
+
}
|
|
1902
|
+
lastChecked = /* @__PURE__ */ new Date();
|
|
1903
|
+
await ctx.adapter.deleteMany({
|
|
1904
|
+
model: API_KEY_TABLE_NAME,
|
|
1905
|
+
where: [{
|
|
1906
|
+
field: "expiresAt",
|
|
1907
|
+
operator: "lt",
|
|
1908
|
+
value: /* @__PURE__ */ new Date()
|
|
1909
|
+
}, {
|
|
1910
|
+
field: "expiresAt",
|
|
1911
|
+
operator: "ne",
|
|
1912
|
+
value: null
|
|
1913
|
+
}]
|
|
1914
|
+
}).catch((error) => {
|
|
1915
|
+
ctx.logger.error(`Failed to delete expired API keys:`, error);
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
function createApiKeyRoutes({ defaultKeyGenerator, configurations, schema }) {
|
|
1919
|
+
return {
|
|
1920
|
+
createApiKey: createApiKey({
|
|
1921
|
+
defaultKeyGenerator,
|
|
1922
|
+
configurations,
|
|
1923
|
+
schema,
|
|
1924
|
+
deleteAllExpiredApiKeys
|
|
1925
|
+
}),
|
|
1926
|
+
verifyApiKey: verifyApiKey({
|
|
1927
|
+
configurations,
|
|
1928
|
+
schema,
|
|
1929
|
+
deleteAllExpiredApiKeys
|
|
1930
|
+
}),
|
|
1931
|
+
getApiKey: getApiKey({
|
|
1932
|
+
configurations,
|
|
1933
|
+
schema,
|
|
1934
|
+
deleteAllExpiredApiKeys
|
|
1935
|
+
}),
|
|
1936
|
+
updateApiKey: updateApiKey({
|
|
1937
|
+
configurations,
|
|
1938
|
+
schema,
|
|
1939
|
+
deleteAllExpiredApiKeys
|
|
1940
|
+
}),
|
|
1941
|
+
deleteApiKey: deleteApiKey({
|
|
1942
|
+
configurations,
|
|
1943
|
+
schema,
|
|
1944
|
+
deleteAllExpiredApiKeys
|
|
1945
|
+
}),
|
|
1946
|
+
listApiKeys: listApiKeys({
|
|
1947
|
+
configurations,
|
|
1948
|
+
schema,
|
|
1949
|
+
deleteAllExpiredApiKeys
|
|
1950
|
+
}),
|
|
1951
|
+
deleteAllExpiredApiKeys: deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys })
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
//#endregion
|
|
1956
|
+
//#region src/schema.ts
|
|
1957
|
+
const apiKeySchema = ({ defaultRateLimitMax, defaultTimeWindow }) => ({ apikey: { fields: {
|
|
1958
|
+
configId: {
|
|
1959
|
+
type: "string",
|
|
1960
|
+
required: true,
|
|
1961
|
+
defaultValue: "default",
|
|
1962
|
+
input: false,
|
|
1963
|
+
index: true
|
|
1964
|
+
},
|
|
1965
|
+
name: {
|
|
1966
|
+
type: "string",
|
|
1967
|
+
required: false,
|
|
1968
|
+
input: false
|
|
1969
|
+
},
|
|
1970
|
+
start: {
|
|
1971
|
+
type: "string",
|
|
1972
|
+
required: false,
|
|
1973
|
+
input: false
|
|
1974
|
+
},
|
|
1975
|
+
referenceId: {
|
|
1976
|
+
type: "string",
|
|
1977
|
+
required: true,
|
|
1978
|
+
input: false,
|
|
1979
|
+
index: true
|
|
1980
|
+
},
|
|
1981
|
+
prefix: {
|
|
1982
|
+
type: "string",
|
|
1983
|
+
required: false,
|
|
1984
|
+
input: false
|
|
1985
|
+
},
|
|
1986
|
+
key: {
|
|
1987
|
+
type: "string",
|
|
1988
|
+
required: true,
|
|
1989
|
+
input: false,
|
|
1990
|
+
index: true
|
|
1991
|
+
},
|
|
1992
|
+
refillInterval: {
|
|
1993
|
+
type: "number",
|
|
1994
|
+
required: false,
|
|
1995
|
+
input: false
|
|
1996
|
+
},
|
|
1997
|
+
refillAmount: {
|
|
1998
|
+
type: "number",
|
|
1999
|
+
required: false,
|
|
2000
|
+
input: false
|
|
2001
|
+
},
|
|
2002
|
+
lastRefillAt: {
|
|
2003
|
+
type: "date",
|
|
2004
|
+
required: false,
|
|
2005
|
+
input: false
|
|
2006
|
+
},
|
|
2007
|
+
enabled: {
|
|
2008
|
+
type: "boolean",
|
|
2009
|
+
required: false,
|
|
2010
|
+
input: false,
|
|
2011
|
+
defaultValue: true
|
|
2012
|
+
},
|
|
2013
|
+
rateLimitEnabled: {
|
|
2014
|
+
type: "boolean",
|
|
2015
|
+
required: false,
|
|
2016
|
+
input: false,
|
|
2017
|
+
defaultValue: true
|
|
2018
|
+
},
|
|
2019
|
+
rateLimitTimeWindow: {
|
|
2020
|
+
type: "number",
|
|
2021
|
+
required: false,
|
|
2022
|
+
input: false,
|
|
2023
|
+
defaultValue: defaultTimeWindow
|
|
2024
|
+
},
|
|
2025
|
+
rateLimitMax: {
|
|
2026
|
+
type: "number",
|
|
2027
|
+
required: false,
|
|
2028
|
+
input: false,
|
|
2029
|
+
defaultValue: defaultRateLimitMax
|
|
2030
|
+
},
|
|
2031
|
+
requestCount: {
|
|
2032
|
+
type: "number",
|
|
2033
|
+
required: false,
|
|
2034
|
+
input: false,
|
|
2035
|
+
defaultValue: 0
|
|
2036
|
+
},
|
|
2037
|
+
remaining: {
|
|
2038
|
+
type: "number",
|
|
2039
|
+
required: false,
|
|
2040
|
+
input: false
|
|
2041
|
+
},
|
|
2042
|
+
lastRequest: {
|
|
2043
|
+
type: "date",
|
|
2044
|
+
required: false,
|
|
2045
|
+
input: false
|
|
2046
|
+
},
|
|
2047
|
+
expiresAt: {
|
|
2048
|
+
type: "date",
|
|
2049
|
+
required: false,
|
|
2050
|
+
input: false
|
|
2051
|
+
},
|
|
2052
|
+
createdAt: {
|
|
2053
|
+
type: "date",
|
|
2054
|
+
required: true,
|
|
2055
|
+
input: false
|
|
2056
|
+
},
|
|
2057
|
+
updatedAt: {
|
|
2058
|
+
type: "date",
|
|
2059
|
+
required: true,
|
|
2060
|
+
input: false
|
|
2061
|
+
},
|
|
2062
|
+
permissions: {
|
|
2063
|
+
type: "string",
|
|
2064
|
+
required: false,
|
|
2065
|
+
input: false
|
|
2066
|
+
},
|
|
2067
|
+
metadata: {
|
|
2068
|
+
type: "string",
|
|
2069
|
+
required: false,
|
|
2070
|
+
input: true,
|
|
2071
|
+
transform: {
|
|
2072
|
+
input(value) {
|
|
2073
|
+
return JSON.stringify(value);
|
|
2074
|
+
},
|
|
2075
|
+
output(value) {
|
|
2076
|
+
if (!value) return null;
|
|
2077
|
+
return parseJSON(value);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
} } });
|
|
2082
|
+
|
|
2083
|
+
//#endregion
|
|
2084
|
+
//#region src/index.ts
|
|
2085
|
+
const defaultKeyHasher = async (key) => {
|
|
2086
|
+
const hash = await createHash("SHA-256").digest(new TextEncoder().encode(key));
|
|
2087
|
+
return base64Url.encode(new Uint8Array(hash), { padding: false });
|
|
2088
|
+
};
|
|
2089
|
+
const API_KEY_TABLE_NAME = "apikey";
|
|
2090
|
+
function apiKey(_configurations, _options) {
|
|
2091
|
+
if (Array.isArray(_configurations) && _configurations.length > 0) {
|
|
2092
|
+
if (!_configurations.every((option) => option.configId)) throw new BetterAuthError("configId is required for each API key configuration in the api-key plugin.");
|
|
2093
|
+
const configIds = _configurations.map((option) => option.configId);
|
|
2094
|
+
if (new Set(configIds).size !== configIds.length) throw new BetterAuthError("configId must be unique for each API key configuration in the api-key plugin.");
|
|
2095
|
+
}
|
|
2096
|
+
const options = _options ?? { schema: Array.isArray(_configurations) ? void 0 : _configurations?.schema };
|
|
2097
|
+
const configurations = [...(Array.isArray(_configurations) ? _configurations : [_configurations]).map((config) => ({
|
|
2098
|
+
...config,
|
|
2099
|
+
apiKeyHeaders: config?.apiKeyHeaders ?? "x-api-key",
|
|
2100
|
+
defaultKeyLength: config?.defaultKeyLength || 64,
|
|
2101
|
+
maximumPrefixLength: config?.maximumPrefixLength ?? 32,
|
|
2102
|
+
minimumPrefixLength: config?.minimumPrefixLength ?? 1,
|
|
2103
|
+
maximumNameLength: config?.maximumNameLength ?? 32,
|
|
2104
|
+
minimumNameLength: config?.minimumNameLength ?? 1,
|
|
2105
|
+
enableMetadata: config?.enableMetadata ?? false,
|
|
2106
|
+
disableKeyHashing: config?.disableKeyHashing ?? false,
|
|
2107
|
+
requireName: config?.requireName ?? false,
|
|
2108
|
+
storage: config?.storage ?? "database",
|
|
2109
|
+
rateLimit: {
|
|
2110
|
+
enabled: config?.rateLimit?.enabled === void 0 ? true : config?.rateLimit?.enabled,
|
|
2111
|
+
timeWindow: config?.rateLimit?.timeWindow ?? 1e3 * 60 * 60 * 24,
|
|
2112
|
+
maxRequests: config?.rateLimit?.maxRequests ?? 10
|
|
2113
|
+
},
|
|
2114
|
+
keyExpiration: {
|
|
2115
|
+
defaultExpiresIn: config?.keyExpiration?.defaultExpiresIn ?? null,
|
|
2116
|
+
disableCustomExpiresTime: config?.keyExpiration?.disableCustomExpiresTime ?? false,
|
|
2117
|
+
maxExpiresIn: config?.keyExpiration?.maxExpiresIn ?? 365,
|
|
2118
|
+
minExpiresIn: config?.keyExpiration?.minExpiresIn ?? 1
|
|
2119
|
+
},
|
|
2120
|
+
startingCharactersConfig: {
|
|
2121
|
+
shouldStore: config?.startingCharactersConfig?.shouldStore ?? true,
|
|
2122
|
+
charactersLength: config?.startingCharactersConfig?.charactersLength ?? 6
|
|
2123
|
+
},
|
|
2124
|
+
enableSessionForAPIKeys: config?.enableSessionForAPIKeys ?? false,
|
|
2125
|
+
fallbackToDatabase: config?.fallbackToDatabase ?? false,
|
|
2126
|
+
customStorage: config?.customStorage,
|
|
2127
|
+
deferUpdates: config?.deferUpdates ?? false
|
|
2128
|
+
}))];
|
|
2129
|
+
const schema = mergeSchema(apiKeySchema({
|
|
2130
|
+
defaultRateLimitMax: (configurations.length === 1 ? configurations[0]?.rateLimit.maxRequests : void 0) ?? 10,
|
|
2131
|
+
defaultTimeWindow: (configurations.length === 1 ? configurations[0]?.rateLimit.timeWindow : void 0) ?? 1e3 * 60 * 60 * 24
|
|
2132
|
+
}), options.schema);
|
|
2133
|
+
const defaultKeyGenerator = async (opts) => {
|
|
2134
|
+
const key = generateRandomString(opts.length, "a-z", "A-Z");
|
|
2135
|
+
return `${opts.prefix || ""}${key}`;
|
|
2136
|
+
};
|
|
2137
|
+
function getApiKeyFromConfig(ctx, config) {
|
|
2138
|
+
if (config.customAPIKeyGetter) return config.customAPIKeyGetter(ctx);
|
|
2139
|
+
if (Array.isArray(config.apiKeyHeaders)) {
|
|
2140
|
+
for (const header of config.apiKeyHeaders) {
|
|
2141
|
+
const value = ctx.headers?.get(header);
|
|
2142
|
+
if (value) return value;
|
|
2143
|
+
}
|
|
2144
|
+
return null;
|
|
2145
|
+
}
|
|
2146
|
+
return ctx.headers?.get(config.apiKeyHeaders) ?? null;
|
|
2147
|
+
}
|
|
2148
|
+
function findApiKeyAndConfig(ctx) {
|
|
2149
|
+
for (const config of configurations) {
|
|
2150
|
+
if (!config.enableSessionForAPIKeys) continue;
|
|
2151
|
+
const key = getApiKeyFromConfig(ctx, config);
|
|
2152
|
+
if (key) return {
|
|
2153
|
+
key,
|
|
2154
|
+
config
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
return null;
|
|
2158
|
+
}
|
|
2159
|
+
const routes = createApiKeyRoutes({
|
|
2160
|
+
defaultKeyGenerator,
|
|
2161
|
+
configurations,
|
|
2162
|
+
schema
|
|
2163
|
+
});
|
|
2164
|
+
return {
|
|
2165
|
+
id: "api-key",
|
|
2166
|
+
$ERROR_CODES: API_KEY_ERROR_CODES,
|
|
2167
|
+
hooks: { before: [{
|
|
2168
|
+
matcher: (ctx) => !!findApiKeyAndConfig(ctx),
|
|
2169
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
2170
|
+
const { key, config } = findApiKeyAndConfig(ctx);
|
|
2171
|
+
if (typeof key !== "string") throw APIError.from("BAD_REQUEST", API_KEY_ERROR_CODES.INVALID_API_KEY_GETTER_RETURN_TYPE);
|
|
2172
|
+
if (key.length < config.defaultKeyLength) throw APIError.from("FORBIDDEN", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
2173
|
+
if (config.customAPIKeyValidator) {
|
|
2174
|
+
if (!await config.customAPIKeyValidator({
|
|
2175
|
+
ctx,
|
|
2176
|
+
key
|
|
2177
|
+
})) throw APIError.from("FORBIDDEN", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
2178
|
+
}
|
|
2179
|
+
const apiKey = await validateApiKey({
|
|
2180
|
+
hashedKey: config.disableKeyHashing ? key : await defaultKeyHasher(key),
|
|
2181
|
+
ctx,
|
|
2182
|
+
opts: config,
|
|
2183
|
+
schema
|
|
2184
|
+
});
|
|
2185
|
+
const cleanupTask = deleteAllExpiredApiKeys(ctx.context).catch((err) => {
|
|
2186
|
+
ctx.context.logger.error("Failed to delete expired API keys:", err);
|
|
2187
|
+
});
|
|
2188
|
+
if (config.deferUpdates) ctx.context.runInBackground(cleanupTask);
|
|
2189
|
+
if ((config.references ?? "user") !== "user") {
|
|
2190
|
+
const msg = API_KEY_ERROR_CODES.INVALID_REFERENCE_ID_FROM_API_KEY;
|
|
2191
|
+
throw APIError.from("UNAUTHORIZED", msg);
|
|
2192
|
+
}
|
|
2193
|
+
const user = await ctx.context.internalAdapter.findUserById(apiKey.referenceId);
|
|
2194
|
+
if (!user) {
|
|
2195
|
+
const msg = API_KEY_ERROR_CODES.INVALID_REFERENCE_ID_FROM_API_KEY;
|
|
2196
|
+
throw APIError.from("UNAUTHORIZED", msg);
|
|
2197
|
+
}
|
|
2198
|
+
const session = {
|
|
2199
|
+
user,
|
|
2200
|
+
session: {
|
|
2201
|
+
id: apiKey.id,
|
|
2202
|
+
token: key,
|
|
2203
|
+
userId: apiKey.referenceId,
|
|
2204
|
+
userAgent: ctx.request?.headers.get("user-agent") ?? null,
|
|
2205
|
+
ipAddress: ctx.request ? getIp(ctx.request, ctx.context.options) : null,
|
|
2206
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2207
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
2208
|
+
expiresAt: apiKey.expiresAt || getDate(ctx.context.options.session?.expiresIn || 3600 * 24 * 7, "ms")
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
ctx.context.session = session;
|
|
2212
|
+
if (ctx.path === "/get-session") return session;
|
|
2213
|
+
else return { context: ctx };
|
|
2214
|
+
})
|
|
2215
|
+
}] },
|
|
2216
|
+
endpoints: {
|
|
2217
|
+
createApiKey: routes.createApiKey,
|
|
2218
|
+
verifyApiKey: routes.verifyApiKey,
|
|
2219
|
+
getApiKey: routes.getApiKey,
|
|
2220
|
+
updateApiKey: routes.updateApiKey,
|
|
2221
|
+
deleteApiKey: routes.deleteApiKey,
|
|
2222
|
+
listApiKeys: routes.listApiKeys,
|
|
2223
|
+
deleteAllExpiredApiKeys: routes.deleteAllExpiredApiKeys
|
|
2224
|
+
},
|
|
2225
|
+
schema,
|
|
2226
|
+
configurations
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
//#endregion
|
|
2231
|
+
export { API_KEY_ERROR_CODES, API_KEY_TABLE_NAME, apiKey, defaultKeyHasher };
|
|
2232
|
+
//# sourceMappingURL=index.mjs.map
|