export async function validateApiKey()

in packages/better-auth/src/plugins/api-key/routes/verify-api-key.ts [13:186]


export async function validateApiKey({
	hashedKey,
	ctx,
	opts,
	schema,
	permissions,
}: {
	hashedKey: string;
	opts: PredefinedApiKeyOptions;
	schema: ReturnType<typeof apiKeySchema>;
	permissions?: Record<string, string[]>;
	ctx: GenericEndpointContext;
}) {
	const apiKey = await ctx.context.adapter.findOne<ApiKey>({
		model: API_KEY_TABLE_NAME,
		where: [
			{
				field: "key",
				value: hashedKey,
			},
		],
	});

	if (!apiKey) {
		throw new APIError("UNAUTHORIZED", {
			message: ERROR_CODES.INVALID_API_KEY,
		});
	}

	if (apiKey.enabled === false) {
		throw new APIError("UNAUTHORIZED", {
			message: ERROR_CODES.KEY_DISABLED,
			code: "KEY_DISABLED" as const,
		});
	}

	if (apiKey.expiresAt) {
		const now = new Date().getTime();
		const expiresAt = apiKey.expiresAt.getTime();
		if (now > expiresAt) {
			try {
				ctx.context.adapter.delete({
					model: API_KEY_TABLE_NAME,
					where: [
						{
							field: "id",
							value: apiKey.id,
						},
					],
				});
			} catch (error) {
				ctx.context.logger.error(`Failed to delete expired API keys:`, error);
			}

			throw new APIError("UNAUTHORIZED", {
				message: ERROR_CODES.KEY_EXPIRED,
				code: "KEY_EXPIRED" as const,
			});
		}
	}

	if (permissions) {
		const apiKeyPermissions = apiKey.permissions
			? safeJSONParse<{
					[key: string]: string[];
				}>(
					//@ts-ignore - from DB, this value is always a string
					apiKey.permissions,
				)
			: null;

		if (!apiKeyPermissions) {
			throw new APIError("UNAUTHORIZED", {
				message: ERROR_CODES.KEY_NOT_FOUND,
				code: "KEY_NOT_FOUND" as const,
			});
		}
		const r = role(apiKeyPermissions as any);
		const result = r.authorize(permissions);
		if (!result.success) {
			throw new APIError("UNAUTHORIZED", {
				message: ERROR_CODES.KEY_NOT_FOUND,
				code: "KEY_NOT_FOUND" as const,
			});
		}
	}

	let remaining = apiKey.remaining;
	let lastRefillAt = apiKey.lastRefillAt;

	if (apiKey.remaining === 0 && apiKey.refillAmount === null) {
		// if there is no more remaining requests, and there is no refill amount, than the key is revoked
		try {
			ctx.context.adapter.delete({
				model: API_KEY_TABLE_NAME,
				where: [
					{
						field: "id",
						value: apiKey.id,
					},
				],
			});
		} catch (error) {
			ctx.context.logger.error(`Failed to delete expired API keys:`, error);
		}

		throw new APIError("UNAUTHORIZED", {
			message: ERROR_CODES.USAGE_EXCEEDED,
			code: "USAGE_EXCEEDED" as const,
		});
	} else if (remaining !== null) {
		let now = new Date().getTime();
		const refillInterval = apiKey.refillInterval;
		const refillAmount = apiKey.refillAmount;
		let lastTime = (lastRefillAt ?? apiKey.createdAt).getTime();

		if (refillInterval && refillAmount) {
			// if they provide refill info, then we should refill once the interval is reached.

			const timeSinceLastRequest = (now - lastTime) / (1000 * 60 * 60 * 24); // in days
			if (timeSinceLastRequest > refillInterval) {
				remaining = refillAmount;
				lastRefillAt = new Date();
			}
		}

		if (remaining === 0) {
			// if there are no more remaining requests, than the key is invalid

			throw new APIError("UNAUTHORIZED", {
				message: ERROR_CODES.USAGE_EXCEEDED,
				code: "USAGE_EXCEEDED" as const,
			});
		} else {
			remaining--;
		}
	}

	const { message, success, update, tryAgainIn } = isRateLimited(apiKey, opts);

	const newApiKey = await ctx.context.adapter.update<ApiKey>({
		model: API_KEY_TABLE_NAME,
		where: [
			{
				field: "id",
				value: apiKey.id,
			},
		],
		update: {
			...update,
			remaining,
			lastRefillAt,
		},
	});

	if (!newApiKey) {
		throw new APIError("INTERNAL_SERVER_ERROR", {
			message: ERROR_CODES.FAILED_TO_UPDATE_API_KEY,
			code: "INTERNAL_SERVER_ERROR" as const,
		});
	}

	if (success === false) {
		throw new APIError("UNAUTHORIZED", {
			message: message ?? undefined,
			code: "RATE_LIMITED" as const,
			details: {
				tryAgainIn,
			},
		});
	}

	return newApiKey;
}