@better-auth/api-key 1.7.0-beta.0 → 1.7.0-beta.10
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/client.d.mts +2 -2
- package/dist/client.mjs +1 -1
- package/dist/{index-CajK1bx0.d.mts → index-K0I_tcSP.d.mts} +15 -851
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +374 -198
- package/dist/{types-CCe5L05Y.d.mts → types-CbtANSbR.d.mts} +2 -14
- package/dist/types.d.mts +1 -1
- package/dist/{version-B1Zw_NlB.mjs → version--lF5Z0S8.mjs} +1 -1
- package/package.json +7 -6
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version
|
|
1
|
+
import { n as API_KEY_ERROR_CODES, t as PACKAGE_VERSION } from "./version--lF5Z0S8.mjs";
|
|
2
2
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
3
|
+
import { getIP } from "@better-auth/core/utils/ip";
|
|
3
4
|
import { base64Url } from "@better-auth/utils/base64";
|
|
4
5
|
import { createHash } from "@better-auth/utils/hash";
|
|
5
6
|
import { BetterAuthError } from "better-auth";
|
|
@@ -10,11 +11,11 @@ import { APIError as APIError$1 } from "@better-auth/core/error";
|
|
|
10
11
|
import { generateId } from "@better-auth/core/utils/id";
|
|
11
12
|
import { safeJSONParse } from "@better-auth/core/utils/json";
|
|
12
13
|
import * as z from "zod";
|
|
13
|
-
import {
|
|
14
|
-
import { isValidIP, normalizeIP } from "@better-auth/core/utils/ip";
|
|
14
|
+
import { mapConcurrent } from "@better-auth/core/utils/async";
|
|
15
15
|
import { role } from "better-auth/plugins/access";
|
|
16
16
|
import { parseJSON } from "better-auth/client";
|
|
17
17
|
//#region src/adapter.ts
|
|
18
|
+
const STORAGE_CONCURRENCY = 10;
|
|
18
19
|
/**
|
|
19
20
|
* Parses double-stringified metadata synchronously without updating the database.
|
|
20
21
|
* Use this for reading metadata, then call migrateLegacyMetadataInBackground for DB updates.
|
|
@@ -165,49 +166,79 @@ async function getApiKeyByIdFromStorage(ctx, id, storage) {
|
|
|
165
166
|
return deserializeApiKey(await storage.get(key));
|
|
166
167
|
}
|
|
167
168
|
/**
|
|
168
|
-
*
|
|
169
|
+
* Serializes reference-list mutations per `refKey` within a single process.
|
|
170
|
+
* Each new mutation chains onto the previous one for the same key, so the
|
|
171
|
+
* read/modify/write below never interleaves with another mutation of the same
|
|
172
|
+
* list. This closes the lost-update race in secondary-storage-only mode, where
|
|
173
|
+
* the serialized list is the source of truth for listing.
|
|
174
|
+
*
|
|
175
|
+
* Limitation: the lock is in-process only. Across multiple server instances
|
|
176
|
+
* sharing one secondary storage, concurrent writers can still lose updates.
|
|
177
|
+
* Secondary storage with `fallbackToDatabase` avoids this by treating the list
|
|
178
|
+
* as an invalidate-only cache with the database as the source of truth.
|
|
179
|
+
* FIXME(api-key-reflist-durable): on `next`, drop the source-of-truth reference
|
|
180
|
+
* list entirely and make the database authoritative for listing, removing this
|
|
181
|
+
* in-process lock.
|
|
182
|
+
*/
|
|
183
|
+
const refListLocks = /* @__PURE__ */ new Map();
|
|
184
|
+
function withRefListLock(refKey, task) {
|
|
185
|
+
const tracked = (refListLocks.get(refKey) ?? Promise.resolve()).then(task, task).finally(() => {
|
|
186
|
+
if (refListLocks.get(refKey) === tracked) refListLocks.delete(refKey);
|
|
187
|
+
});
|
|
188
|
+
refListLocks.set(refKey, tracked);
|
|
189
|
+
return tracked;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Read-modify-write the ref list:
|
|
193
|
+
* used only when the list is the source of truth.
|
|
169
194
|
*/
|
|
170
|
-
async function
|
|
195
|
+
async function modifyRefList(storage, refKey, modify) {
|
|
196
|
+
await withRefListLock(refKey, async () => {
|
|
197
|
+
const refListData = await storage.get(refKey);
|
|
198
|
+
let keyIds = [];
|
|
199
|
+
if (refListData && typeof refListData === "string") try {
|
|
200
|
+
keyIds = JSON.parse(refListData);
|
|
201
|
+
} catch {
|
|
202
|
+
keyIds = [];
|
|
203
|
+
}
|
|
204
|
+
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
205
|
+
const next = modify(keyIds);
|
|
206
|
+
if (next.length === 0) await storage.delete(refKey);
|
|
207
|
+
else await storage.set(refKey, JSON.stringify(next));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function setApiKeyInStorage(_ctx, apiKey, storage, ttl, opts) {
|
|
171
211
|
const serialized = serializeApiKey(apiKey);
|
|
172
|
-
const hashedKey = apiKey.key;
|
|
173
|
-
const id = apiKey.id;
|
|
174
|
-
await storage.set(getStorageKeyByHashedKey(hashedKey), serialized, ttl);
|
|
175
|
-
await storage.set(getStorageKeyById(id), serialized, ttl);
|
|
176
212
|
const refKey = getStorageKeyByReferenceId(apiKey.referenceId);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
185
|
-
if (!keyIds.includes(id)) {
|
|
186
|
-
keyIds.push(id);
|
|
187
|
-
await storage.set(refKey, JSON.stringify(keyIds));
|
|
213
|
+
if (opts.fallbackToDatabase) {
|
|
214
|
+
await Promise.all([
|
|
215
|
+
storage.set(getStorageKeyByHashedKey(apiKey.key), serialized, ttl),
|
|
216
|
+
storage.set(getStorageKeyById(apiKey.id), serialized, ttl),
|
|
217
|
+
storage.delete(refKey)
|
|
218
|
+
]);
|
|
219
|
+
return;
|
|
188
220
|
}
|
|
221
|
+
await Promise.all([storage.set(getStorageKeyByHashedKey(apiKey.key), serialized, ttl), storage.set(getStorageKeyById(apiKey.id), serialized, ttl)]);
|
|
222
|
+
await modifyRefList(storage, refKey, (ids) => ids.includes(apiKey.id) ? ids : [...ids, apiKey.id]);
|
|
189
223
|
}
|
|
190
224
|
/**
|
|
191
225
|
* Delete API key from secondary storage
|
|
192
226
|
*/
|
|
193
|
-
async function deleteApiKeyFromStorage(ctx, apiKey, storage) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (refListData && typeof refListData === "string") try {
|
|
203
|
-
keyIds = JSON.parse(refListData);
|
|
204
|
-
} catch {
|
|
205
|
-
keyIds = [];
|
|
227
|
+
async function deleteApiKeyFromStorage(ctx, apiKey, storage, opts) {
|
|
228
|
+
const refKey = getStorageKeyByReferenceId(apiKey.referenceId);
|
|
229
|
+
if (opts.fallbackToDatabase) {
|
|
230
|
+
await Promise.all([
|
|
231
|
+
storage.delete(getStorageKeyByHashedKey(apiKey.key)),
|
|
232
|
+
storage.delete(getStorageKeyById(apiKey.id)),
|
|
233
|
+
storage.delete(refKey)
|
|
234
|
+
]);
|
|
235
|
+
return;
|
|
206
236
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
237
|
+
await Promise.all([
|
|
238
|
+
storage.delete(getStorageKeyByHashedKey(apiKey.key)),
|
|
239
|
+
storage.delete(getStorageKeyById(apiKey.id)),
|
|
240
|
+
modifyRefList(storage, refKey, (ids) => ids.filter((keyId) => keyId !== apiKey.id))
|
|
241
|
+
]);
|
|
211
242
|
}
|
|
212
243
|
/**
|
|
213
244
|
* Unified getter for API keys with support for all storage modes
|
|
@@ -233,7 +264,7 @@ async function getApiKey$1(ctx, hashedKey, opts) {
|
|
|
233
264
|
value: hashedKey
|
|
234
265
|
}]
|
|
235
266
|
});
|
|
236
|
-
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey));
|
|
267
|
+
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey), opts);
|
|
237
268
|
return dbKey;
|
|
238
269
|
}
|
|
239
270
|
if (opts.storage === "secondary-storage") {
|
|
@@ -272,7 +303,7 @@ async function getApiKeyById(ctx, id, opts) {
|
|
|
272
303
|
value: id
|
|
273
304
|
}]
|
|
274
305
|
});
|
|
275
|
-
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey));
|
|
306
|
+
if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey), opts);
|
|
276
307
|
return dbKey;
|
|
277
308
|
}
|
|
278
309
|
if (opts.storage === "secondary-storage") {
|
|
@@ -296,7 +327,7 @@ async function setApiKey(ctx, apiKey, opts) {
|
|
|
296
327
|
if (opts.storage === "database") return;
|
|
297
328
|
if (opts.storage === "secondary-storage") {
|
|
298
329
|
if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'");
|
|
299
|
-
await setApiKeyInStorage(ctx, apiKey, storage, ttl);
|
|
330
|
+
await setApiKeyInStorage(ctx, apiKey, storage, ttl, opts);
|
|
300
331
|
return;
|
|
301
332
|
}
|
|
302
333
|
}
|
|
@@ -308,7 +339,7 @@ async function deleteApiKey$1(ctx, apiKey, opts) {
|
|
|
308
339
|
if (opts.storage === "database") return;
|
|
309
340
|
if (opts.storage === "secondary-storage") {
|
|
310
341
|
if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'");
|
|
311
|
-
await deleteApiKeyFromStorage(ctx, apiKey, storage);
|
|
342
|
+
await deleteApiKeyFromStorage(ctx, apiKey, storage, opts);
|
|
312
343
|
return;
|
|
313
344
|
}
|
|
314
345
|
}
|
|
@@ -378,11 +409,7 @@ async function listApiKeys$1(ctx, referenceId, opts, paginationOpts) {
|
|
|
378
409
|
}
|
|
379
410
|
else if (Array.isArray(refListData)) keyIds = refListData;
|
|
380
411
|
if (keyIds.length > 0) {
|
|
381
|
-
const apiKeys =
|
|
382
|
-
for (const id of keyIds) {
|
|
383
|
-
const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
384
|
-
if (apiKey) apiKeys.push(apiKey);
|
|
385
|
-
}
|
|
412
|
+
const apiKeys = (await mapConcurrent(keyIds, (id) => getApiKeyByIdFromStorage(ctx, id, storage), { concurrency: STORAGE_CONCURRENCY })).filter((key) => key !== null && key !== void 0);
|
|
386
413
|
return {
|
|
387
414
|
apiKeys: applySortingAndPagination(apiKeys, sortBy, sortDirection, limit, offset),
|
|
388
415
|
total: apiKeys.length
|
|
@@ -409,11 +436,8 @@ async function listApiKeys$1(ctx, referenceId, opts, paginationOpts) {
|
|
|
409
436
|
}]
|
|
410
437
|
})]);
|
|
411
438
|
if (storage && dbKeys.length > 0) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
await setApiKeyInStorage(ctx, apiKey, storage, calculateTTL(apiKey));
|
|
415
|
-
keyIds.push(apiKey.id);
|
|
416
|
-
}
|
|
439
|
+
await mapConcurrent(dbKeys, (apiKey) => setApiKeyInStorage(ctx, apiKey, storage, calculateTTL(apiKey), opts), { concurrency: STORAGE_CONCURRENCY });
|
|
440
|
+
const keyIds = dbKeys.map((apiKey) => apiKey.id);
|
|
417
441
|
await storage.set(refKey, JSON.stringify(keyIds));
|
|
418
442
|
}
|
|
419
443
|
return {
|
|
@@ -442,11 +466,7 @@ async function listApiKeys$1(ctx, referenceId, opts, paginationOpts) {
|
|
|
442
466
|
apiKeys: [],
|
|
443
467
|
total: 0
|
|
444
468
|
};
|
|
445
|
-
const apiKeys =
|
|
446
|
-
for (const id of keyIds) {
|
|
447
|
-
const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage);
|
|
448
|
-
if (apiKey) apiKeys.push(apiKey);
|
|
449
|
-
}
|
|
469
|
+
const apiKeys = (await mapConcurrent(keyIds, (id) => getApiKeyByIdFromStorage(ctx, id, storage), { concurrency: STORAGE_CONCURRENCY })).filter((key) => key !== null && key !== void 0);
|
|
450
470
|
return {
|
|
451
471
|
apiKeys: applySortingAndPagination(apiKeys, sortBy, sortDirection, limit, offset),
|
|
452
472
|
total: apiKeys.length
|
|
@@ -555,21 +575,6 @@ const getDate = (span, unit = "ms") => {
|
|
|
555
575
|
function isAPIError(error) {
|
|
556
576
|
return error instanceof APIError || error instanceof APIError$1 || error?.name === "APIError";
|
|
557
577
|
}
|
|
558
|
-
const LOCALHOST_IP = "127.0.0.1";
|
|
559
|
-
function getIp(req, options) {
|
|
560
|
-
if (options.advanced?.ipAddress?.disableIpTracking) return null;
|
|
561
|
-
const headers = "headers" in req ? req.headers : req;
|
|
562
|
-
const ipHeaders = options.advanced?.ipAddress?.ipAddressHeaders || ["x-forwarded-for"];
|
|
563
|
-
for (const key of ipHeaders) {
|
|
564
|
-
const value = "get" in headers ? headers.get(key) : headers[key];
|
|
565
|
-
if (typeof value === "string") {
|
|
566
|
-
const ip = value.split(",")[0].trim();
|
|
567
|
-
if (isValidIP(ip)) return normalizeIP(ip, { ipv6Subnet: options.advanced?.ipAddress?.ipv6Subnet });
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (isTest() || isDevelopment()) return LOCALHOST_IP;
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
578
|
//#endregion
|
|
574
579
|
//#region src/routes/create-api-key.ts
|
|
575
580
|
const createApiKeyBodySchema = z.object({
|
|
@@ -724,7 +729,7 @@ function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllEx
|
|
|
724
729
|
const { configId, name, expiresIn, prefix, remaining, metadata, refillAmount, refillInterval, permissions, rateLimitMax, rateLimitTimeWindow, rateLimitEnabled } = ctx.body;
|
|
725
730
|
const opts = resolveConfiguration(ctx.context, configurations, configId);
|
|
726
731
|
const keyGenerator = opts.customKeyGenerator || defaultKeyGenerator;
|
|
727
|
-
const session = await getSessionFromCtx(ctx);
|
|
732
|
+
const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
|
|
728
733
|
const isClientRequest = ctx.request || ctx.headers;
|
|
729
734
|
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);
|
|
730
735
|
if (ctx.request && ctx.body.userId !== void 0) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
@@ -856,7 +861,7 @@ function createApiKey({ defaultKeyGenerator, configurations, schema, deleteAllEx
|
|
|
856
861
|
//#endregion
|
|
857
862
|
//#region src/routes/delete-all-expired-api-keys.ts
|
|
858
863
|
function deleteAllExpiredApiKeysEndpoint({ deleteAllExpiredApiKeys }) {
|
|
859
|
-
return createAuthEndpoint({ method: "POST" }, async (ctx) => {
|
|
864
|
+
return createAuthEndpoint.serverOnly({ method: "POST" }, async (ctx) => {
|
|
860
865
|
try {
|
|
861
866
|
await deleteAllExpiredApiKeys(ctx.context, true);
|
|
862
867
|
} catch (error) {
|
|
@@ -1285,18 +1290,16 @@ function listApiKeys({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
|
1285
1290
|
const storageKey = getStorageIdentifier(config);
|
|
1286
1291
|
if (!storageGroups.has(storageKey)) storageGroups.set(storageKey, config);
|
|
1287
1292
|
}
|
|
1293
|
+
const groupResults = await Promise.all([...storageGroups.values()].map((opts) => listApiKeys$1(ctx, referenceId, opts, {
|
|
1294
|
+
limit: void 0,
|
|
1295
|
+
offset: void 0,
|
|
1296
|
+
sortBy: ctx.query?.sortBy,
|
|
1297
|
+
sortDirection: ctx.query?.sortDirection
|
|
1298
|
+
})));
|
|
1288
1299
|
const seenIds = /* @__PURE__ */ new Set();
|
|
1289
|
-
for (const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
offset: void 0,
|
|
1293
|
-
sortBy: ctx.query?.sortBy,
|
|
1294
|
-
sortDirection: ctx.query?.sortDirection
|
|
1295
|
-
});
|
|
1296
|
-
for (const key of apiKeys) if (!seenIds.has(key.id)) {
|
|
1297
|
-
seenIds.add(key.id);
|
|
1298
|
-
allApiKeys.push(key);
|
|
1299
|
-
}
|
|
1300
|
+
for (const { apiKeys } of groupResults) for (const key of apiKeys) if (!seenIds.has(key.id)) {
|
|
1301
|
+
seenIds.add(key.id);
|
|
1302
|
+
allApiKeys.push(key);
|
|
1300
1303
|
}
|
|
1301
1304
|
}
|
|
1302
1305
|
let filteredApiKeys = allApiKeys.filter((key) => {
|
|
@@ -1473,7 +1476,7 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
|
1473
1476
|
} }
|
|
1474
1477
|
}, async (ctx) => {
|
|
1475
1478
|
const { configId, keyId, expiresIn, enabled, metadata, refillAmount, refillInterval, remaining, name, permissions, rateLimitEnabled, rateLimitTimeWindow, rateLimitMax } = ctx.body;
|
|
1476
|
-
const session = await getSessionFromCtx(ctx);
|
|
1479
|
+
const session = await getSessionFromCtx(ctx, { disableCookieCache: true });
|
|
1477
1480
|
const authRequired = ctx.request || ctx.headers;
|
|
1478
1481
|
const user = authRequired && !session ? null : session?.user || { id: ctx.body.userId };
|
|
1479
1482
|
if (!user?.id) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.UNAUTHORIZED_SESSION);
|
|
@@ -1571,76 +1574,63 @@ function updateApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
|
1571
1574
|
//#endregion
|
|
1572
1575
|
//#region src/rate-limit.ts
|
|
1573
1576
|
/**
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1577
|
-
* a message and updated ApiKey data.
|
|
1577
|
+
* Decides how the current request affects the per-key rate-limit counter, based
|
|
1578
|
+
* on the read-in-memory ApiKey. The verify route applies the result atomically;
|
|
1579
|
+
* this function performs no writes.
|
|
1578
1580
|
*/
|
|
1579
|
-
function
|
|
1581
|
+
function evaluateRateLimit(apiKey, opts) {
|
|
1580
1582
|
const now = /* @__PURE__ */ new Date();
|
|
1581
1583
|
const lastRequest = apiKey.lastRequest;
|
|
1582
1584
|
const rateLimitTimeWindow = apiKey.rateLimitTimeWindow;
|
|
1583
1585
|
const rateLimitMax = apiKey.rateLimitMax;
|
|
1584
|
-
let requestCount = apiKey.requestCount;
|
|
1585
1586
|
if (opts.rateLimit.enabled === false) return {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
update: { lastRequest: now },
|
|
1589
|
-
tryAgainIn: null
|
|
1587
|
+
type: "skip",
|
|
1588
|
+
lastRequest: now
|
|
1590
1589
|
};
|
|
1591
1590
|
if (apiKey.rateLimitEnabled === false) return {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
update: { lastRequest: now },
|
|
1595
|
-
tryAgainIn: null
|
|
1591
|
+
type: "skip",
|
|
1592
|
+
lastRequest: now
|
|
1596
1593
|
};
|
|
1597
1594
|
if (rateLimitTimeWindow === null || rateLimitMax === null) return {
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
update: null,
|
|
1601
|
-
tryAgainIn: null
|
|
1595
|
+
type: "skip",
|
|
1596
|
+
lastRequest: null
|
|
1602
1597
|
};
|
|
1603
1598
|
if (lastRequest === null) return {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
update: {
|
|
1607
|
-
lastRequest: now,
|
|
1608
|
-
requestCount: 1
|
|
1609
|
-
},
|
|
1610
|
-
tryAgainIn: null
|
|
1599
|
+
type: "start",
|
|
1600
|
+
now
|
|
1611
1601
|
};
|
|
1612
1602
|
const timeSinceLastRequest = now.getTime() - new Date(lastRequest).getTime();
|
|
1613
1603
|
if (timeSinceLastRequest > rateLimitTimeWindow) return {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
lastRequest: now,
|
|
1618
|
-
requestCount: 1
|
|
1619
|
-
},
|
|
1620
|
-
tryAgainIn: null
|
|
1604
|
+
type: "reset",
|
|
1605
|
+
now,
|
|
1606
|
+
windowStart: new Date(now.getTime() - rateLimitTimeWindow)
|
|
1621
1607
|
};
|
|
1622
|
-
if (requestCount >= rateLimitMax) return {
|
|
1623
|
-
|
|
1608
|
+
if (apiKey.requestCount >= rateLimitMax) return {
|
|
1609
|
+
type: "deny",
|
|
1624
1610
|
message: API_KEY_ERROR_CODES.RATE_LIMIT_EXCEEDED.message,
|
|
1625
|
-
update: null,
|
|
1626
1611
|
tryAgainIn: Math.ceil(rateLimitTimeWindow - timeSinceLastRequest)
|
|
1627
1612
|
};
|
|
1628
|
-
requestCount++;
|
|
1629
1613
|
return {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
lastRequest: now,
|
|
1635
|
-
requestCount
|
|
1636
|
-
}
|
|
1614
|
+
type: "increment",
|
|
1615
|
+
now,
|
|
1616
|
+
max: rateLimitMax,
|
|
1617
|
+
windowStart: new Date(now.getTime() - rateLimitTimeWindow)
|
|
1637
1618
|
};
|
|
1638
1619
|
}
|
|
1639
1620
|
//#endregion
|
|
1640
1621
|
//#region src/routes/verify-api-key.ts
|
|
1641
|
-
async function validateApiKey({
|
|
1642
|
-
const
|
|
1622
|
+
async function validateApiKey({ key, ctx, lookupOpts, configurations, schema, permissions, expectedConfigId, runCustomValidator }) {
|
|
1623
|
+
const hashedKey = lookupOpts.disableKeyHashing ? key : await defaultKeyHasher(key);
|
|
1624
|
+
const apiKey = await getApiKey$1(ctx, hashedKey, lookupOpts);
|
|
1643
1625
|
if (!apiKey) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1626
|
+
if (expectedConfigId !== void 0 && !configIdMatches(apiKey.configId, expectedConfigId)) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1627
|
+
const opts = resolveConfiguration(ctx.context, configurations, apiKey.configId);
|
|
1628
|
+
if (runCustomValidator && opts.customAPIKeyValidator) {
|
|
1629
|
+
if (!await opts.customAPIKeyValidator({
|
|
1630
|
+
ctx,
|
|
1631
|
+
key
|
|
1632
|
+
})) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1633
|
+
}
|
|
1644
1634
|
if (apiKey.enabled === false) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_DISABLED);
|
|
1645
1635
|
if (apiKey.expiresAt) {
|
|
1646
1636
|
if (Date.now() > new Date(apiKey.expiresAt).getTime()) {
|
|
@@ -1675,8 +1665,6 @@ async function validateApiKey({ hashedKey, ctx, opts, schema, permissions }) {
|
|
|
1675
1665
|
if (!apiKeyPermissions) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1676
1666
|
if (!role(apiKeyPermissions).authorize(permissions).success) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.KEY_NOT_FOUND);
|
|
1677
1667
|
}
|
|
1678
|
-
let remaining = apiKey.remaining;
|
|
1679
|
-
let lastRefillAt = apiKey.lastRefillAt;
|
|
1680
1668
|
if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
|
|
1681
1669
|
const deleteExhaustedKey = async () => {
|
|
1682
1670
|
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) {
|
|
@@ -1702,89 +1690,271 @@ async function validateApiKey({ hashedKey, ctx, opts, schema, permissions }) {
|
|
|
1702
1690
|
}));
|
|
1703
1691
|
else await deleteExhaustedKey();
|
|
1704
1692
|
throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1693
|
+
}
|
|
1694
|
+
return {
|
|
1695
|
+
apiKey: opts.storage === "database" || opts.storage === "secondary-storage" && opts.fallbackToDatabase ? await claimUsageInDatabase({
|
|
1696
|
+
ctx,
|
|
1697
|
+
apiKey,
|
|
1698
|
+
opts,
|
|
1699
|
+
hashedKey
|
|
1700
|
+
}) : await claimUsageInSecondaryStorage({
|
|
1701
|
+
ctx,
|
|
1702
|
+
apiKey,
|
|
1703
|
+
opts,
|
|
1704
|
+
hashedKey
|
|
1705
|
+
}),
|
|
1706
|
+
opts
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Atomically consume quota and a rate-limit slot against the database row, the
|
|
1711
|
+
* source of truth for `database` and `secondary-storage` + `fallbackToDatabase`
|
|
1712
|
+
* modes. Each guarded `incrementOne` only mutates the row while the guard still
|
|
1713
|
+
* holds, so concurrent verifications cannot drive `remaining` below zero or push
|
|
1714
|
+
* `requestCount` past the configured max. The cache (when present) is refreshed
|
|
1715
|
+
* from the resulting row.
|
|
1716
|
+
*/
|
|
1717
|
+
async function claimUsageInDatabase({ ctx, apiKey, opts, hashedKey }) {
|
|
1718
|
+
let row = apiKey;
|
|
1719
|
+
if (apiKey.remaining !== null) row = await consumeRemaining(ctx, apiKey);
|
|
1720
|
+
row = await consumeRateLimit(ctx, row, opts);
|
|
1721
|
+
const finalRow = await ctx.context.adapter.update({
|
|
1722
|
+
model: API_KEY_TABLE_NAME,
|
|
1723
|
+
where: [{
|
|
1724
|
+
field: "id",
|
|
1725
|
+
value: row.id
|
|
1726
|
+
}],
|
|
1727
|
+
update: { updatedAt: /* @__PURE__ */ new Date() }
|
|
1728
|
+
});
|
|
1729
|
+
if (!finalRow) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1730
|
+
if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) await setApiKey(ctx, finalRow, opts);
|
|
1731
|
+
return finalRow;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Guarded quota consumption. When a refill is due, exactly one verification wins
|
|
1735
|
+
* the refill (compare-and-swap on the observed `lastRefillAt`); any concurrent
|
|
1736
|
+
* verification falls through to the plain guarded decrement against the refilled
|
|
1737
|
+
* value. The decrement only applies while `remaining > 0`, so it can never go
|
|
1738
|
+
* negative. Returns the updated row; throws when the quota is exhausted.
|
|
1739
|
+
*/
|
|
1740
|
+
async function consumeRemaining(ctx, apiKey) {
|
|
1741
|
+
const now = /* @__PURE__ */ new Date();
|
|
1742
|
+
const { refillInterval, refillAmount } = apiKey;
|
|
1743
|
+
if (refillInterval && refillAmount) {
|
|
1744
|
+
const lastTime = new Date(apiKey.lastRefillAt ?? apiKey.createdAt).getTime();
|
|
1745
|
+
if (now.getTime() - lastTime > refillInterval) {
|
|
1746
|
+
const refilled = await ctx.context.adapter.incrementOne({
|
|
1747
|
+
model: API_KEY_TABLE_NAME,
|
|
1748
|
+
where: [{
|
|
1749
|
+
field: "id",
|
|
1750
|
+
value: apiKey.id
|
|
1751
|
+
}, {
|
|
1752
|
+
field: "lastRefillAt",
|
|
1753
|
+
value: apiKey.lastRefillAt
|
|
1754
|
+
}],
|
|
1755
|
+
increment: {},
|
|
1756
|
+
set: {
|
|
1757
|
+
remaining: refillAmount - 1,
|
|
1758
|
+
lastRefillAt: now
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
if (refilled) return refilled;
|
|
1715
1762
|
}
|
|
1716
|
-
if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1717
|
-
else remaining--;
|
|
1718
1763
|
}
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1764
|
+
const decremented = await ctx.context.adapter.incrementOne({
|
|
1765
|
+
model: API_KEY_TABLE_NAME,
|
|
1766
|
+
where: [{
|
|
1767
|
+
field: "id",
|
|
1768
|
+
value: apiKey.id
|
|
1769
|
+
}, {
|
|
1770
|
+
field: "remaining",
|
|
1771
|
+
operator: "gt",
|
|
1772
|
+
value: 0
|
|
1773
|
+
}],
|
|
1774
|
+
increment: { remaining: -1 }
|
|
1775
|
+
});
|
|
1776
|
+
if (!decremented) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1777
|
+
return decremented;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Guarded rate-limit consumption. The common in-window path increments
|
|
1781
|
+
* `requestCount` only while it is below the max (compare-and-swap), so a burst
|
|
1782
|
+
* of concurrent verifications can never exceed the limit. Window resets and the
|
|
1783
|
+
* first request in a window are guarded conditional sets; a request that loses
|
|
1784
|
+
* every guard within an active window is rejected. Returns the updated row, or
|
|
1785
|
+
* the unchanged row when rate limiting does not apply.
|
|
1786
|
+
*/
|
|
1787
|
+
async function consumeRateLimit(ctx, apiKey, opts) {
|
|
1788
|
+
const decision = evaluateRateLimit(apiKey, opts);
|
|
1789
|
+
if (decision.type === "deny") throw new APIError$1("TOO_MANY_REQUESTS", {
|
|
1790
|
+
message: decision.message,
|
|
1722
1791
|
code: "RATE_LIMITED",
|
|
1723
|
-
details: { tryAgainIn }
|
|
1792
|
+
details: { tryAgainIn: decision.tryAgainIn }
|
|
1724
1793
|
});
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
lastRefillAt,
|
|
1730
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1731
|
-
};
|
|
1732
|
-
const performUpdate = async () => {
|
|
1733
|
-
if (opts.storage === "database") return ctx.context.adapter.update({
|
|
1734
|
-
model: API_KEY_TABLE_NAME,
|
|
1794
|
+
if (decision.type === "skip") {
|
|
1795
|
+
if (decision.lastRequest === null) return apiKey;
|
|
1796
|
+
return await ctx.context.adapter.update({
|
|
1797
|
+
model: "apikey",
|
|
1735
1798
|
where: [{
|
|
1736
1799
|
field: "id",
|
|
1737
1800
|
value: apiKey.id
|
|
1738
1801
|
}],
|
|
1739
|
-
update: {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
where: [{
|
|
1802
|
+
update: { lastRequest: decision.lastRequest }
|
|
1803
|
+
}) ?? apiKey;
|
|
1804
|
+
}
|
|
1805
|
+
if (decision.type === "increment") {
|
|
1806
|
+
const incremented = await ctx.context.adapter.incrementOne({
|
|
1807
|
+
model: API_KEY_TABLE_NAME,
|
|
1808
|
+
where: [
|
|
1809
|
+
{
|
|
1748
1810
|
field: "id",
|
|
1749
1811
|
value: apiKey.id
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1812
|
+
},
|
|
1813
|
+
{
|
|
1814
|
+
field: "lastRequest",
|
|
1815
|
+
operator: "gt",
|
|
1816
|
+
value: decision.windowStart
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
field: "requestCount",
|
|
1820
|
+
operator: "lt",
|
|
1821
|
+
value: decision.max
|
|
1754
1822
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1823
|
+
],
|
|
1824
|
+
increment: { requestCount: 1 },
|
|
1825
|
+
set: { lastRequest: decision.now }
|
|
1826
|
+
});
|
|
1827
|
+
if (incremented) return incremented;
|
|
1828
|
+
const fresh = await ctx.context.adapter.findOne({
|
|
1829
|
+
model: API_KEY_TABLE_NAME,
|
|
1830
|
+
where: [{
|
|
1831
|
+
field: "id",
|
|
1832
|
+
value: apiKey.id
|
|
1833
|
+
}]
|
|
1834
|
+
});
|
|
1835
|
+
if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1836
|
+
return consumeRateLimit(ctx, fresh, opts);
|
|
1837
|
+
}
|
|
1838
|
+
const windowGuard = decision.type === "reset" ? {
|
|
1839
|
+
field: "lastRequest",
|
|
1840
|
+
operator: "lte",
|
|
1841
|
+
value: decision.windowStart
|
|
1842
|
+
} : {
|
|
1843
|
+
field: "lastRequest",
|
|
1844
|
+
operator: "eq",
|
|
1845
|
+
value: null
|
|
1846
|
+
};
|
|
1847
|
+
const started = await ctx.context.adapter.incrementOne({
|
|
1848
|
+
model: API_KEY_TABLE_NAME,
|
|
1849
|
+
where: [{
|
|
1850
|
+
field: "id",
|
|
1851
|
+
value: apiKey.id
|
|
1852
|
+
}, windowGuard],
|
|
1853
|
+
increment: {},
|
|
1854
|
+
set: {
|
|
1855
|
+
requestCount: 1,
|
|
1856
|
+
lastRequest: decision.now
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
if (started) return started;
|
|
1860
|
+
const fresh = await ctx.context.adapter.findOne({
|
|
1861
|
+
model: API_KEY_TABLE_NAME,
|
|
1862
|
+
where: [{
|
|
1863
|
+
field: "id",
|
|
1864
|
+
value: apiKey.id
|
|
1865
|
+
}]
|
|
1866
|
+
});
|
|
1867
|
+
if (!fresh) throw APIError$1.from("UNAUTHORIZED", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
1868
|
+
return consumeRateLimit(ctx, fresh, opts);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Secondary-storage-only mode has no database row to guard, so quota and
|
|
1872
|
+
* rate-limit consumption stays a read-modify-write merge over the serialized
|
|
1873
|
+
* key. This is the residual non-atomic path; strict enforcement requires the
|
|
1874
|
+
* database (use `fallbackToDatabase`) or an atomic secondary-storage primitive.
|
|
1875
|
+
* FIXME(api-key-secondary-atomic): back this with SecondaryStorage.increment on
|
|
1876
|
+
* `next` so secondary-storage-only mode enforces quota and rate limits atomically.
|
|
1877
|
+
*/
|
|
1878
|
+
async function claimUsageInSecondaryStorage({ ctx, apiKey, opts, hashedKey }) {
|
|
1879
|
+
let remaining = apiKey.remaining;
|
|
1880
|
+
let lastRefillAt = apiKey.lastRefillAt;
|
|
1881
|
+
if (remaining !== null) {
|
|
1882
|
+
const now = Date.now();
|
|
1883
|
+
const { refillInterval, refillAmount } = apiKey;
|
|
1884
|
+
const lastTime = new Date(lastRefillAt ?? apiKey.createdAt).getTime();
|
|
1885
|
+
if (refillInterval && refillAmount && now - lastTime > refillInterval) {
|
|
1886
|
+
remaining = refillAmount;
|
|
1887
|
+
lastRefillAt = /* @__PURE__ */ new Date();
|
|
1761
1888
|
}
|
|
1889
|
+
if (remaining === 0) throw APIError$1.from("TOO_MANY_REQUESTS", API_KEY_ERROR_CODES.USAGE_EXCEEDED);
|
|
1890
|
+
remaining--;
|
|
1891
|
+
}
|
|
1892
|
+
const mutations = {
|
|
1893
|
+
...applyRateLimitToSnapshot(apiKey, opts),
|
|
1894
|
+
remaining,
|
|
1895
|
+
lastRefillAt,
|
|
1896
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1897
|
+
};
|
|
1898
|
+
const performUpdate = async () => {
|
|
1899
|
+
const fresh = await getApiKey$1(ctx, hashedKey, opts);
|
|
1900
|
+
if (!fresh) return null;
|
|
1901
|
+
const merged = {
|
|
1902
|
+
...fresh,
|
|
1903
|
+
...mutations
|
|
1904
|
+
};
|
|
1905
|
+
await setApiKey(ctx, merged, opts);
|
|
1906
|
+
return merged;
|
|
1762
1907
|
};
|
|
1763
|
-
let newApiKey = null;
|
|
1764
1908
|
if (opts.deferUpdates) {
|
|
1765
1909
|
ctx.context.runInBackground(performUpdate().catch((error) => {
|
|
1766
1910
|
ctx.context.logger.error("Failed to update API key:", error);
|
|
1767
1911
|
}));
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1912
|
+
return {
|
|
1913
|
+
...apiKey,
|
|
1914
|
+
...mutations
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
const updated = await performUpdate();
|
|
1918
|
+
if (!updated) throw APIError$1.from("INTERNAL_SERVER_ERROR", API_KEY_ERROR_CODES.FAILED_TO_UPDATE_API_KEY);
|
|
1919
|
+
return updated;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Translate a rate-limit decision into a counter snapshot for the
|
|
1923
|
+
* secondary-storage merge write. Denials throw before any write.
|
|
1924
|
+
*/
|
|
1925
|
+
function applyRateLimitToSnapshot(apiKey, opts) {
|
|
1926
|
+
const decision = evaluateRateLimit(apiKey, opts);
|
|
1927
|
+
switch (decision.type) {
|
|
1928
|
+
case "deny": throw new APIError$1("TOO_MANY_REQUESTS", {
|
|
1929
|
+
message: decision.message,
|
|
1930
|
+
code: "RATE_LIMITED",
|
|
1931
|
+
details: { tryAgainIn: decision.tryAgainIn }
|
|
1932
|
+
});
|
|
1933
|
+
case "skip": return decision.lastRequest === null ? {} : { lastRequest: decision.lastRequest };
|
|
1934
|
+
case "start":
|
|
1935
|
+
case "reset": return {
|
|
1936
|
+
lastRequest: decision.now,
|
|
1937
|
+
requestCount: 1
|
|
1938
|
+
};
|
|
1939
|
+
case "increment": return {
|
|
1940
|
+
lastRequest: decision.now,
|
|
1941
|
+
requestCount: apiKey.requestCount + 1
|
|
1942
|
+
};
|
|
1772
1943
|
}
|
|
1773
|
-
return newApiKey;
|
|
1774
1944
|
}
|
|
1775
1945
|
const verifyApiKeyBodySchema = z.object({
|
|
1776
|
-
configId: z.string().meta({ description: "
|
|
1946
|
+
configId: z.string().meta({ description: "Configuration ID to scope verification to. When omitted, the key is validated against its own configuration." }).optional(),
|
|
1777
1947
|
key: z.string().meta({ description: "The key to verify" }),
|
|
1778
1948
|
permissions: z.record(z.string(), z.array(z.string())).meta({ description: "The permissions to verify." }).optional()
|
|
1779
1949
|
});
|
|
1780
1950
|
function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
1781
|
-
return createAuthEndpoint({
|
|
1951
|
+
return createAuthEndpoint.serverOnly({
|
|
1782
1952
|
method: "POST",
|
|
1783
1953
|
body: verifyApiKeyBodySchema
|
|
1784
1954
|
}, async (ctx) => {
|
|
1785
1955
|
const { configId, key } = ctx.body;
|
|
1786
1956
|
const lookupOpts = resolveConfiguration(ctx.context, configurations, configId);
|
|
1787
|
-
if (lookupOpts.customAPIKeyValidator) {
|
|
1957
|
+
if (configId !== void 0 && lookupOpts.customAPIKeyValidator) {
|
|
1788
1958
|
if (!await lookupOpts.customAPIKeyValidator({
|
|
1789
1959
|
ctx,
|
|
1790
1960
|
key
|
|
@@ -1797,17 +1967,22 @@ function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
|
1797
1967
|
key: null
|
|
1798
1968
|
});
|
|
1799
1969
|
}
|
|
1800
|
-
const hashed = lookupOpts.disableKeyHashing ? key : await defaultKeyHasher(key);
|
|
1801
1970
|
let apiKey = null;
|
|
1971
|
+
let opts;
|
|
1802
1972
|
try {
|
|
1803
|
-
|
|
1804
|
-
|
|
1973
|
+
const result = await validateApiKey({
|
|
1974
|
+
key,
|
|
1805
1975
|
permissions: ctx.body.permissions,
|
|
1806
1976
|
ctx,
|
|
1807
|
-
|
|
1808
|
-
|
|
1977
|
+
lookupOpts,
|
|
1978
|
+
configurations,
|
|
1979
|
+
schema,
|
|
1980
|
+
expectedConfigId: configId,
|
|
1981
|
+
runCustomValidator: configId === void 0
|
|
1809
1982
|
});
|
|
1810
|
-
|
|
1983
|
+
apiKey = result.apiKey;
|
|
1984
|
+
opts = result.opts;
|
|
1985
|
+
if (opts.deferUpdates) ctx.context.runInBackground(deleteAllExpiredApiKeys(ctx.context).catch((err) => {
|
|
1811
1986
|
ctx.context.logger.error("Failed to delete expired API keys:", err);
|
|
1812
1987
|
}));
|
|
1813
1988
|
} catch (error) {
|
|
@@ -1834,7 +2009,6 @@ function verifyApiKey({ configurations, schema, deleteAllExpiredApiKeys }) {
|
|
|
1834
2009
|
key: 1,
|
|
1835
2010
|
permissions: void 0
|
|
1836
2011
|
};
|
|
1837
|
-
const opts = apiKey ? resolveConfiguration(ctx.context, configurations, apiKey.configId) : lookupOpts;
|
|
1838
2012
|
let migratedMetadata = null;
|
|
1839
2013
|
if (apiKey) migratedMetadata = await migrateDoubleStringifiedMetadata(ctx, apiKey, opts);
|
|
1840
2014
|
returningApiKey.permissions = returningApiKey.permissions ? safeJSONParse(returningApiKey.permissions) : null;
|
|
@@ -2163,11 +2337,13 @@ function apiKey(_configurations, _options) {
|
|
|
2163
2337
|
key
|
|
2164
2338
|
})) throw APIError.from("FORBIDDEN", API_KEY_ERROR_CODES.INVALID_API_KEY);
|
|
2165
2339
|
}
|
|
2166
|
-
const apiKey = await validateApiKey({
|
|
2167
|
-
|
|
2340
|
+
const { apiKey } = await validateApiKey({
|
|
2341
|
+
key,
|
|
2168
2342
|
ctx,
|
|
2169
|
-
|
|
2170
|
-
|
|
2343
|
+
lookupOpts: config,
|
|
2344
|
+
configurations,
|
|
2345
|
+
schema,
|
|
2346
|
+
expectedConfigId: config.configId
|
|
2171
2347
|
});
|
|
2172
2348
|
const cleanupTask = deleteAllExpiredApiKeys(ctx.context).catch((err) => {
|
|
2173
2349
|
ctx.context.logger.error("Failed to delete expired API keys:", err);
|
|
@@ -2189,7 +2365,7 @@ function apiKey(_configurations, _options) {
|
|
|
2189
2365
|
token: key,
|
|
2190
2366
|
userId: apiKey.referenceId,
|
|
2191
2367
|
userAgent: ctx.request?.headers.get("user-agent") ?? null,
|
|
2192
|
-
ipAddress: ctx.request ?
|
|
2368
|
+
ipAddress: ctx.request ? getIP(ctx.request, ctx.context.options) : null,
|
|
2193
2369
|
createdAt: /* @__PURE__ */ new Date(),
|
|
2194
2370
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
2195
2371
|
expiresAt: apiKey.expiresAt || getDate(ctx.context.options.session?.expiresIn || 3600 * 24 * 7, "ms")
|