/opt/canhelp/node_modules/better-auth/dist/api/rate-limiter
Edit: /opt/canhelp/node_modules/better-auth/dist/api/rate-limiter/index.mjs (6274B)
import { wildcardMatch } from "../../utils/wildcard.mjs";
import { getIp } from "../../utils/get-request-ip.mjs";
import { safeJSONParse } from "@better-auth/core/utils/json";
import { normalizePathname } from "@better-auth/core/utils/url";
import { createRateLimitKey } from "@better-auth/core/utils/ip";
//#region src/api/rate-limiter/index.ts
const memory = /* @__PURE__ */ new Map();
function shouldRateLimit(max, window, rateLimitData) {
const now = Date.now();
const windowInMs = window * 1e3;
return now - rateLimitData.lastRequest < windowInMs && rateLimitData.count >= max;
}
function rateLimitResponse(retryAfter) {
return new Response(JSON.stringify({ message: "Too many requests. Please try again later." }), {
status: 429,
statusText: "Too Many Requests",
headers: { "X-Retry-After": retryAfter.toString() }
});
}
function getRetryAfter(lastRequest, window) {
const now = Date.now();
const windowInMs = window * 1e3;
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
}
function createDatabaseStorageWrapper(ctx) {
const model = "rateLimit";
const db = ctx.adapter;
return {
get: async (key) => {
const data = (await db.findMany({
model,
where: [{
field: "key",
value: key
}]
}))[0];
if (typeof data?.lastRequest === "bigint") data.lastRequest = Number(data.lastRequest);
return data;
},
set: async (key, value, _update) => {
try {
if (_update) await db.updateMany({
model,
where: [{
field: "key",
value: key
}],
update: {
count: value.count,
lastRequest: value.lastRequest
}
});
else await db.create({
model,
data: {
key,
count: value.count,
lastRequest: value.lastRequest
}
});
} catch (e) {
ctx.logger.error("Error setting rate limit", e);
}
}
};
}
function getRateLimitStorage(ctx, rateLimitSettings) {
if (ctx.options.rateLimit?.customStorage) return ctx.options.rateLimit.customStorage;
const storage = ctx.rateLimit.storage;
if (storage === "secondary-storage") return {
get: async (key) => {
const data = await ctx.options.secondaryStorage?.get(key);
return data ? safeJSONParse(data) : null;
},
set: async (key, value, _update) => {
const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
await ctx.options.secondaryStorage?.set?.(key, JSON.stringify(value), ttl);
}
};
else if (storage === "memory") return {
async get(key) {
const entry = memory.get(key);
if (!entry) return null;
if (Date.now() >= entry.expiresAt) {
memory.delete(key);
return null;
}
return entry.data;
},
async set(key, value, _update) {
const ttl = rateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;
const expiresAt = Date.now() + ttl * 1e3;
memory.set(key, {
data: value,
expiresAt
});
}
};
return createDatabaseStorageWrapper(ctx);
}
let ipWarningLogged = false;
async function resolveRateLimitConfig(req, ctx) {
const basePath = new URL(ctx.baseURL).pathname;
const path = normalizePathname(req.url, basePath);
let currentWindow = ctx.rateLimit.window;
let currentMax = ctx.rateLimit.max;
const ip = getIp(req, ctx.options);
if (!ip) {
if (!ipWarningLogged) {
ctx.logger.warn("Rate limiting skipped: could not determine client IP address. If you're behind a reverse proxy, make sure to configure `trustedProxies` in your auth config.");
ipWarningLogged = true;
}
return null;
}
const key = createRateLimitKey(ip, path);
const specialRule = getDefaultSpecialRules().find((rule) => rule.pathMatcher(path));
if (specialRule) {
currentWindow = specialRule.window;
currentMax = specialRule.max;
}
for (const plugin of ctx.options.plugins || []) if (plugin.rateLimit) {
const matchedRule = plugin.rateLimit.find((rule) => rule.pathMatcher(path));
if (matchedRule) {
currentWindow = matchedRule.window;
currentMax = matchedRule.max;
break;
}
}
if (ctx.rateLimit.customRules) {
const _path = Object.keys(ctx.rateLimit.customRules).find((p) => {
if (p.includes("*")) return wildcardMatch(p)(path);
return p === path;
});
if (_path) {
const customRule = ctx.rateLimit.customRules[_path];
const resolved = typeof customRule === "function" ? await customRule(req, {
window: currentWindow,
max: currentMax
}) : customRule;
if (resolved) {
currentWindow = resolved.window;
currentMax = resolved.max;
}
if (resolved === false) return null;
}
}
return {
key,
currentWindow,
currentMax
};
}
async function onRequestRateLimit(req, ctx) {
if (!ctx.rateLimit.enabled) return;
const config = await resolveRateLimitConfig(req, ctx);
if (!config) return;
const { key, currentWindow, currentMax } = config;
const data = await getRateLimitStorage(ctx, { window: currentWindow }).get(key);
if (data && shouldRateLimit(currentMax, currentWindow, data)) return rateLimitResponse(getRetryAfter(data.lastRequest, currentWindow));
}
async function onResponseRateLimit(req, ctx) {
if (!ctx.rateLimit.enabled) return;
const config = await resolveRateLimitConfig(req, ctx);
if (!config) return;
const { key, currentWindow } = config;
const storage = getRateLimitStorage(ctx, { window: currentWindow });
const data = await storage.get(key);
const now = Date.now();
if (!data) await storage.set(key, {
key,
count: 1,
lastRequest: now
});
else if (now - data.lastRequest > currentWindow * 1e3) await storage.set(key, {
...data,
count: 1,
lastRequest: now
}, true);
else await storage.set(key, {
...data,
count: data.count + 1,
lastRequest: now
}, true);
}
function getDefaultSpecialRules() {
return [{
pathMatcher(path) {
return path.startsWith("/sign-in") || path.startsWith("/sign-up") || path.startsWith("/change-password") || path.startsWith("/change-email");
},
window: 10,
max: 3
}, {
pathMatcher(path) {
return path === "/request-password-reset" || path === "/send-verification-email" || path.startsWith("/forget-password") || path === "/email-otp/send-verification-otp" || path === "/email-otp/request-password-reset";
},
window: 60,
max: 3
}];
}
//#endregion
export { onRequestRateLimit, onResponseRateLimit };
//# sourceMappingURL=index.mjs.map