@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/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