UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

311 lines (310 loc) • 8.95 kB
//#region src/plugins/api-key/adapter.ts /** * Generate storage key for API key by hashed key */ function getStorageKeyByHashedKey(hashedKey) { return `api-key:${hashedKey}`; } /** * Generate storage key for API key by ID */ function getStorageKeyById(id) { return `api-key:by-id:${id}`; } /** * Generate storage key for user's API key list */ function getStorageKeyByUserId(userId) { return `api-key:by-user:${userId}`; } /** * Serialize API key for storage */ function serializeApiKey(apiKey) { return JSON.stringify({ ...apiKey, createdAt: apiKey.createdAt.toISOString(), updatedAt: apiKey.updatedAt.toISOString(), expiresAt: apiKey.expiresAt?.toISOString() ?? null, lastRefillAt: apiKey.lastRefillAt?.toISOString() ?? null, lastRequest: apiKey.lastRequest?.toISOString() ?? null }); } /** * Deserialize API key from storage */ function deserializeApiKey(data) { if (!data || typeof data !== "string") return null; try { const parsed = JSON.parse(data); return { ...parsed, createdAt: new Date(parsed.createdAt), updatedAt: new Date(parsed.updatedAt), expiresAt: parsed.expiresAt ? new Date(parsed.expiresAt) : null, lastRefillAt: parsed.lastRefillAt ? new Date(parsed.lastRefillAt) : null, lastRequest: parsed.lastRequest ? new Date(parsed.lastRequest) : null }; } catch { return null; } } /** * Get the storage instance to use (custom methods take precedence) */ function getStorageInstance(ctx, opts) { if (opts.customStorage) return opts.customStorage; return ctx.context.secondaryStorage || null; } /** * Calculate TTL in seconds for an API key */ function calculateTTL(apiKey) { if (apiKey.expiresAt) { const now = Date.now(); const expiresAt = new Date(apiKey.expiresAt).getTime(); const ttlSeconds = Math.floor((expiresAt - now) / 1e3); if (ttlSeconds > 0) return ttlSeconds; } } /** * Get API key from secondary storage by hashed key */ async function getApiKeyFromStorage(ctx, hashedKey, storage) { const key = getStorageKeyByHashedKey(hashedKey); return deserializeApiKey(await storage.get(key)); } /** * Get API key from secondary storage by ID */ async function getApiKeyByIdFromStorage(ctx, id, storage) { const key = getStorageKeyById(id); return deserializeApiKey(await storage.get(key)); } /** * Store API key in secondary storage */ async function setApiKeyInStorage(ctx, apiKey, storage, ttl) { const serialized = serializeApiKey(apiKey); const hashedKey = apiKey.key; const id = apiKey.id; await storage.set(getStorageKeyByHashedKey(hashedKey), serialized, ttl); await storage.set(getStorageKeyById(id), serialized, ttl); const userKey = getStorageKeyByUserId(apiKey.userId); const userListData = await storage.get(userKey); let userIds = []; if (userListData && typeof userListData === "string") try { userIds = JSON.parse(userListData); } catch { userIds = []; } else if (Array.isArray(userListData)) userIds = userListData; if (!userIds.includes(id)) { userIds.push(id); await storage.set(userKey, JSON.stringify(userIds)); } } /** * Delete API key from secondary storage */ async function deleteApiKeyFromStorage(ctx, apiKey, storage) { const hashedKey = apiKey.key; const id = apiKey.id; const userId = apiKey.userId; await storage.delete(getStorageKeyByHashedKey(hashedKey)); await storage.delete(getStorageKeyById(id)); const userKey = getStorageKeyByUserId(userId); const userListData = await storage.get(userKey); let userIds = []; if (userListData && typeof userListData === "string") try { userIds = JSON.parse(userListData); } catch { userIds = []; } else if (Array.isArray(userListData)) userIds = userListData; const filteredIds = userIds.filter((keyId) => keyId !== id); if (filteredIds.length === 0) await storage.delete(userKey); else await storage.set(userKey, JSON.stringify(filteredIds)); } /** * Unified getter for API keys with support for all storage modes */ async function getApiKey(ctx, hashedKey, opts) { const storage = getStorageInstance(ctx, opts); if (opts.storage === "database") return await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "key", value: hashedKey }] }); if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) { if (storage) { const cached = await getApiKeyFromStorage(ctx, hashedKey, storage); if (cached) return cached; } const dbKey = await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "key", value: hashedKey }] }); if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey)); return dbKey; } if (opts.storage === "secondary-storage") { if (!storage) return null; return await getApiKeyFromStorage(ctx, hashedKey, storage); } return await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "key", value: hashedKey }] }); } /** * Unified getter for API keys by ID */ async function getApiKeyById(ctx, id, opts) { const storage = getStorageInstance(ctx, opts); if (opts.storage === "database") return await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "id", value: id }] }); if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) { if (storage) { const cached = await getApiKeyByIdFromStorage(ctx, id, storage); if (cached) return cached; } const dbKey = await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "id", value: id }] }); if (dbKey && storage) await setApiKeyInStorage(ctx, dbKey, storage, calculateTTL(dbKey)); return dbKey; } if (opts.storage === "secondary-storage") { if (!storage) return null; return await getApiKeyByIdFromStorage(ctx, id, storage); } return await ctx.context.adapter.findOne({ model: "apikey", where: [{ field: "id", value: id }] }); } /** * Unified setter for API keys with support for all storage modes */ async function setApiKey(ctx, apiKey, opts) { const storage = getStorageInstance(ctx, opts); const ttl = calculateTTL(apiKey); if (opts.storage === "database") return; if (opts.storage === "secondary-storage") { if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'"); await setApiKeyInStorage(ctx, apiKey, storage, ttl); return; } } /** * Unified deleter for API keys with support for all storage modes */ async function deleteApiKey(ctx, apiKey, opts) { const storage = getStorageInstance(ctx, opts); if (opts.storage === "database") return; if (opts.storage === "secondary-storage") { if (!storage) throw new Error("Secondary storage is required when storage mode is 'secondary-storage'"); await deleteApiKeyFromStorage(ctx, apiKey, storage); return; } } /** * List API keys for a user with support for all storage modes */ async function listApiKeys(ctx, userId, opts) { const storage = getStorageInstance(ctx, opts); if (opts.storage === "database") return await ctx.context.adapter.findMany({ model: "apikey", where: [{ field: "userId", value: userId }] }); if (opts.storage === "secondary-storage" && opts.fallbackToDatabase) { const userKey = getStorageKeyByUserId(userId); if (storage) { const userListData = await storage.get(userKey); let userIds = []; if (userListData && typeof userListData === "string") try { userIds = JSON.parse(userListData); } catch { userIds = []; } else if (Array.isArray(userListData)) userIds = userListData; if (userIds.length > 0) { const apiKeys = []; for (const id of userIds) { const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage); if (apiKey) apiKeys.push(apiKey); } return apiKeys; } } const dbKeys = await ctx.context.adapter.findMany({ model: "apikey", where: [{ field: "userId", value: userId }] }); if (storage && dbKeys.length > 0) { const userIds = []; for (const apiKey of dbKeys) { await setApiKeyInStorage(ctx, apiKey, storage, calculateTTL(apiKey)); userIds.push(apiKey.id); } await storage.set(userKey, JSON.stringify(userIds)); } return dbKeys; } if (opts.storage === "secondary-storage") { if (!storage) return []; const userKey = getStorageKeyByUserId(userId); const userListData = await storage.get(userKey); let userIds = []; if (userListData && typeof userListData === "string") try { userIds = JSON.parse(userListData); } catch { return []; } else if (Array.isArray(userListData)) userIds = userListData; else return []; const apiKeys = []; for (const id of userIds) { const apiKey = await getApiKeyByIdFromStorage(ctx, id, storage); if (apiKey) apiKeys.push(apiKey); } return apiKeys; } return await ctx.context.adapter.findMany({ model: "apikey", where: [{ field: "userId", value: userId }] }); } //#endregion export { deleteApiKey, getApiKey, getApiKeyById, listApiKeys, setApiKey }; //# sourceMappingURL=adapter.mjs.map