UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

1 lines • 9.7 kB
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/api/rate-limiter/index.ts"],"sourcesContent":["import type { AuthContext } from \"@better-auth/core\";\nimport { safeJSONParse } from \"@better-auth/core/utils\";\nimport type { RateLimit } from \"../../types\";\nimport { getIp } from \"../../utils/get-request-ip\";\nimport { wildcardMatch } from \"../../utils/wildcard\";\n\nfunction shouldRateLimit(\n\tmax: number,\n\twindow: number,\n\trateLimitData: RateLimit,\n) {\n\tconst now = Date.now();\n\tconst windowInMs = window * 1000;\n\tconst timeSinceLastRequest = now - rateLimitData.lastRequest;\n\treturn timeSinceLastRequest < windowInMs && rateLimitData.count >= max;\n}\n\nfunction rateLimitResponse(retryAfter: number) {\n\treturn new Response(\n\t\tJSON.stringify({\n\t\t\tmessage: \"Too many requests. Please try again later.\",\n\t\t}),\n\t\t{\n\t\t\tstatus: 429,\n\t\t\tstatusText: \"Too Many Requests\",\n\t\t\theaders: {\n\t\t\t\t\"X-Retry-After\": retryAfter.toString(),\n\t\t\t},\n\t\t},\n\t);\n}\n\nfunction getRetryAfter(lastRequest: number, window: number) {\n\tconst now = Date.now();\n\tconst windowInMs = window * 1000;\n\treturn Math.ceil((lastRequest + windowInMs - now) / 1000);\n}\n\nfunction createDBStorage(ctx: AuthContext) {\n\tconst model = \"rateLimit\";\n\tconst db = ctx.adapter;\n\treturn {\n\t\tget: async (key: string) => {\n\t\t\tconst res = await db.findMany<RateLimit>({\n\t\t\t\tmodel,\n\t\t\t\twhere: [{ field: \"key\", value: key }],\n\t\t\t});\n\t\t\tconst data = res[0];\n\n\t\t\tif (typeof data?.lastRequest === \"bigint\") {\n\t\t\t\tdata.lastRequest = Number(data.lastRequest);\n\t\t\t}\n\n\t\t\treturn data;\n\t\t},\n\t\tset: async (\n\t\t\tkey: string,\n\t\t\tvalue: RateLimit,\n\t\t\t_update?: boolean | undefined,\n\t\t) => {\n\t\t\ttry {\n\t\t\t\tif (_update) {\n\t\t\t\t\tawait db.updateMany({\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\twhere: [{ field: \"key\", value: key }],\n\t\t\t\t\t\tupdate: {\n\t\t\t\t\t\t\tcount: value.count,\n\t\t\t\t\t\t\tlastRequest: value.lastRequest,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tawait db.create({\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tkey,\n\t\t\t\t\t\t\tcount: value.count,\n\t\t\t\t\t\t\tlastRequest: value.lastRequest,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tctx.logger.error(\"Error setting rate limit\", e);\n\t\t\t}\n\t\t},\n\t};\n}\n\nconst memory = new Map<string, RateLimit>();\nfunction getRateLimitStorage(\n\tctx: AuthContext,\n\trateLimitSettings?:\n\t\t| {\n\t\t\t\twindow?: number;\n\t\t }\n\t\t| undefined,\n) {\n\tif (ctx.options.rateLimit?.customStorage) {\n\t\treturn ctx.options.rateLimit.customStorage;\n\t}\n\tconst storage = ctx.rateLimit.storage;\n\tif (storage === \"secondary-storage\") {\n\t\treturn {\n\t\t\tget: async (key: string) => {\n\t\t\t\tconst data = await ctx.options.secondaryStorage?.get(key);\n\t\t\t\treturn data ? safeJSONParse<RateLimit>(data) : undefined;\n\t\t\t},\n\t\t\tset: async (\n\t\t\t\tkey: string,\n\t\t\t\tvalue: RateLimit,\n\t\t\t\t_update?: boolean | undefined,\n\t\t\t) => {\n\t\t\t\tconst ttl =\n\t\t\t\t\trateLimitSettings?.window ?? ctx.options.rateLimit?.window ?? 10;\n\t\t\t\tawait ctx.options.secondaryStorage?.set?.(\n\t\t\t\t\tkey,\n\t\t\t\t\tJSON.stringify(value),\n\t\t\t\t\tttl,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t} else if (storage === \"memory\") {\n\t\treturn {\n\t\t\tasync get(key: string) {\n\t\t\t\treturn memory.get(key);\n\t\t\t},\n\t\t\tasync set(key: string, value: RateLimit, _update?: boolean | undefined) {\n\t\t\t\tmemory.set(key, value);\n\t\t\t},\n\t\t};\n\t}\n\treturn createDBStorage(ctx);\n}\n\nexport async function onRequestRateLimit(req: Request, ctx: AuthContext) {\n\tif (!ctx.rateLimit.enabled) {\n\t\treturn;\n\t}\n\tconst path = new URL(req.url).pathname\n\t\t.replace(ctx.options.basePath || \"/api/auth\", \"\")\n\t\t.replace(/\\/+$/, \"\");\n\tlet window = ctx.rateLimit.window;\n\tlet max = ctx.rateLimit.max;\n\tconst ip = getIp(req, ctx.options);\n\tif (!ip) {\n\t\treturn;\n\t}\n\tconst key = ip + path;\n\tconst specialRules = getDefaultSpecialRules();\n\tconst specialRule = specialRules.find((rule) => rule.pathMatcher(path));\n\n\tif (specialRule) {\n\t\twindow = specialRule.window;\n\t\tmax = specialRule.max;\n\t}\n\n\tfor (const plugin of ctx.options.plugins || []) {\n\t\tif (plugin.rateLimit) {\n\t\t\tconst matchedRule = plugin.rateLimit.find((rule) =>\n\t\t\t\trule.pathMatcher(path),\n\t\t\t);\n\t\t\tif (matchedRule) {\n\t\t\t\twindow = matchedRule.window;\n\t\t\t\tmax = matchedRule.max;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (ctx.rateLimit.customRules) {\n\t\tconst _path = Object.keys(ctx.rateLimit.customRules).find((p) => {\n\t\t\tif (p.includes(\"*\")) {\n\t\t\t\tconst isMatch = wildcardMatch(p)(path);\n\t\t\t\treturn isMatch;\n\t\t\t}\n\t\t\treturn p === path;\n\t\t});\n\t\tif (_path) {\n\t\t\tconst customRule = ctx.rateLimit.customRules[_path];\n\t\t\tconst resolved =\n\t\t\t\ttypeof customRule === \"function\" ? await customRule(req) : customRule;\n\t\t\tif (resolved) {\n\t\t\t\twindow = resolved.window;\n\t\t\t\tmax = resolved.max;\n\t\t\t}\n\n\t\t\tif (resolved === false) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst storage = getRateLimitStorage(ctx, {\n\t\twindow,\n\t});\n\tconst data = await storage.get(key);\n\tconst now = Date.now();\n\n\tif (!data) {\n\t\tawait storage.set(key, {\n\t\t\tkey,\n\t\t\tcount: 1,\n\t\t\tlastRequest: now,\n\t\t});\n\t} else {\n\t\tconst timeSinceLastRequest = now - data.lastRequest;\n\n\t\tif (shouldRateLimit(max, window, data)) {\n\t\t\tconst retryAfter = getRetryAfter(data.lastRequest, window);\n\t\t\treturn rateLimitResponse(retryAfter);\n\t\t} else if (timeSinceLastRequest > window * 1000) {\n\t\t\t// Reset the count if the window has passed since the last request\n\t\t\tawait storage.set(\n\t\t\t\tkey,\n\t\t\t\t{\n\t\t\t\t\t...data,\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tlastRequest: now,\n\t\t\t\t},\n\t\t\t\ttrue,\n\t\t\t);\n\t\t} else {\n\t\t\tawait storage.set(\n\t\t\t\tkey,\n\t\t\t\t{\n\t\t\t\t\t...data,\n\t\t\t\t\tcount: data.count + 1,\n\t\t\t\t\tlastRequest: now,\n\t\t\t\t},\n\t\t\t\ttrue,\n\t\t\t);\n\t\t}\n\t}\n}\n\nfunction getDefaultSpecialRules() {\n\tconst specialRules = [\n\t\t{\n\t\t\tpathMatcher(path: string) {\n\t\t\t\treturn (\n\t\t\t\t\tpath.startsWith(\"/sign-in\") ||\n\t\t\t\t\tpath.startsWith(\"/sign-up\") ||\n\t\t\t\t\tpath.startsWith(\"/change-password\") ||\n\t\t\t\t\tpath.startsWith(\"/change-email\")\n\t\t\t\t);\n\t\t\t},\n\t\t\twindow: 10,\n\t\t\tmax: 3,\n\t\t},\n\t];\n\treturn specialRules;\n}\n"],"mappings":"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"}