@upstash/ratelimit
Version:
[](https://www.npmjs.com/package/@upstash/ratelimit) [](https://github.com/upstash/ratelimit/actions/workf
1 lines • 109 kB
Source Map (JSON)
{"version":3,"sources":["../src/analytics.ts","../src/cache.ts","../src/duration.ts","../src/hash.ts","../src/lua-scripts/single.ts","../src/lua-scripts/multi.ts","../src/lua-scripts/reset.ts","../src/lua-scripts/hash.ts","../src/types.ts","../src/deny-list/scripts.ts","../src/deny-list/ip-deny-list.ts","../src/deny-list/time.ts","../src/deny-list/deny-list.ts","../src/ratelimit.ts","../src/multi.ts","../src/single.ts"],"sourcesContent":["import type { Aggregate } from \"@upstash/core-analytics\";\nimport { Analytics as CoreAnalytics } from \"@upstash/core-analytics\";\nimport type { Redis } from \"./types\";\n\nexport type Geo = {\n country?: string;\n city?: string;\n region?: string;\n ip?: string;\n};\n\n/**\n * denotes the success field in the analytics submission.\n * Set to true when ratelimit check passes. False when request is ratelimited.\n * Set to \"denied\" when some request value is in deny list.\n */\nexport type EventSuccess = boolean | \"denied\"\n\nexport type Event = Geo & {\n identifier: string;\n time: number;\n success: EventSuccess;\n};\n\nexport type AnalyticsConfig = {\n redis: Redis;\n prefix?: string;\n};\n\n/**\n * The Analytics package is experimental and can change at any time.\n */\nexport class Analytics {\n private readonly analytics: CoreAnalytics;\n private readonly table = \"events\";\n\n constructor(config: AnalyticsConfig) {\n this.analytics = new CoreAnalytics({\n // @ts-expect-error we need to fix the types in core-analytics, it should only require the methods it needs, not the whole sdk\n redis: config.redis,\n window: \"1h\",\n prefix: config.prefix ?? \"@upstash/ratelimit\",\n retention: \"90d\",\n });\n }\n\n /**\n * Try to extract the geo information from the request\n *\n * This handles Vercel's `req.geo` and and Cloudflare's `request.cf` properties\n * @param req\n * @returns\n */\n public extractGeo(req: { geo?: Geo; cf?: Geo }): Geo {\n if (req.geo !== undefined) {\n return req.geo;\n }\n if (req.cf !== undefined) {\n return req.cf;\n }\n\n return {};\n }\n\n public async record(event: Event): Promise<void> {\n await this.analytics.ingest(this.table, event);\n }\n\n public async series<TFilter extends keyof Omit<Event, \"time\">>(\n filter: TFilter,\n cutoff: number,\n ): Promise<Aggregate[]> {\n const timestampCount = Math.min(\n (\n this.analytics.getBucket(Date.now())\n - this.analytics.getBucket(cutoff)\n ) / (60 * 60 * 1000),\n 256\n )\n return this.analytics.aggregateBucketsWithPipeline(this.table, filter, timestampCount)\n }\n\n public async getUsage(cutoff = 0): Promise<Record<string, { success: number; blocked: number }>> {\n \n const timestampCount = Math.min(\n (\n this.analytics.getBucket(Date.now())\n - this.analytics.getBucket(cutoff)\n ) / (60 * 60 * 1000),\n 256\n )\n const records = await this.analytics.getAllowedBlocked(this.table, timestampCount)\n return records;\n }\n\n public async getUsageOverTime<TFilter extends keyof Omit<Event, \"time\">>(\n timestampCount: number, groupby: TFilter\n ): Promise<Aggregate[]> {\n const result = await this.analytics.aggregateBucketsWithPipeline(this.table, groupby, timestampCount)\n return result\n }\n\n public async getMostAllowedBlocked(timestampCount: number, getTop?: number, checkAtMost?: number) {\n getTop = getTop ?? 5\n const timestamp = undefined // let the analytics handle getting the timestamp\n return this.analytics.getMostAllowedBlocked(this.table, timestampCount, getTop, timestamp, checkAtMost)\n }\n}\n","import type { EphemeralCache } from \"./types\";\n\nexport class Cache implements EphemeralCache {\n /**\n * Stores identifier -> reset (in milliseconds)\n */\n private readonly cache: Map<string, number>;\n\n constructor(cache: Map<string, number>) {\n this.cache = cache;\n }\n\n public isBlocked(identifier: string): { blocked: boolean; reset: number } {\n if (!this.cache.has(identifier)) {\n return { blocked: false, reset: 0 };\n }\n const reset = this.cache.get(identifier)!;\n if (reset < Date.now()) {\n this.cache.delete(identifier);\n return { blocked: false, reset: 0 };\n }\n\n return { blocked: true, reset: reset };\n }\n\n public blockUntil(identifier: string, reset: number): void {\n this.cache.set(identifier, reset);\n }\n\n public set(key: string, value: number): void {\n this.cache.set(key, value);\n }\n public get(key: string): number | null {\n return this.cache.get(key) || null;\n }\n\n public incr(key: string): number {\n let value = this.cache.get(key) ?? 0;\n value += 1;\n this.cache.set(key, value);\n return value;\n }\n\n public pop(key: string): void {\n this.cache.delete(key)\n }\n\n public empty(): void {\n this.cache.clear()\n }\n\n public size(): number {\n return this.cache.size;\n }\n}\n","type Unit = \"ms\" | \"s\" | \"m\" | \"h\" | \"d\";\nexport type Duration = `${number} ${Unit}` | `${number}${Unit}`;\n\n/**\n * Convert a human readable duration to milliseconds\n */\nexport function ms(d: Duration): number {\n const match = d.match(/^(\\d+)\\s?(ms|s|m|h|d)$/);\n if (!match) {\n throw new Error(`Unable to parse window size: ${d}`);\n }\n const time = Number.parseInt(match[1]);\n const unit = match[2] as Unit;\n\n switch (unit) {\n case \"ms\": {\n return time;\n }\n case \"s\": {\n return time * 1000;\n }\n case \"m\": {\n return time * 1000 * 60;\n }\n case \"h\": {\n return time * 1000 * 60 * 60;\n }\n case \"d\": {\n return time * 1000 * 60 * 60 * 24;\n }\n\n default: {\n throw new Error(`Unable to parse window size: ${d}`);\n }\n }\n}\n","import type { ScriptInfo } from \"./lua-scripts/hash\";\nimport type { RegionContext } from \"./types\";\n\n/**\n * Runs the specified script with EVALSHA using the scriptHash parameter.\n * \n * If the EVALSHA fails, loads the script to redis and runs again with the\n * hash returned from Redis.\n * \n * @param ctx Regional or multi region context\n * @param script ScriptInfo of script to run. Contains the script and its hash\n * @param keys eval keys\n * @param args eval args\n */\nexport const safeEval = async (\n ctx: RegionContext,\n script: ScriptInfo,\n keys: any[],\n args: any[],\n) => {\n try {\n return await ctx.redis.evalsha(script.hash, keys, args)\n } catch (error) {\n if (`${error}`.includes(\"NOSCRIPT\")) {\n const hash = await ctx.redis.scriptLoad(script.script)\n\n if (hash !== script.hash) {\n console.warn(\n \"Upstash Ratelimit: Expected hash and the hash received from Redis\"\n + \" are different. Ratelimit will work as usual but performance will\"\n + \" be reduced.\"\n );\n }\n\n return await ctx.redis.evalsha(hash, keys, args)\n }\n throw error;\n }\n}","export const fixedWindowLimitScript = `\n local key = KEYS[1]\n local window = ARGV[1]\n local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1\n\n local r = redis.call(\"INCRBY\", key, incrementBy)\n if r == tonumber(incrementBy) then\n -- The first time this key is set, the value will be equal to incrementBy.\n -- So we only need the expire command once\n redis.call(\"PEXPIRE\", key, window)\n end\n\n return r\n`;\n\nexport const fixedWindowRemainingTokensScript = `\n local key = KEYS[1]\n local tokens = 0\n\n local value = redis.call('GET', key)\n if value then\n tokens = value\n end\n return tokens\n `;\n\nexport const slidingWindowLimitScript = `\n local currentKey = KEYS[1] -- identifier including prefixes\n local previousKey = KEYS[2] -- key of the previous bucket\n local tokens = tonumber(ARGV[1]) -- tokens per window\n local now = ARGV[2] -- current timestamp in milliseconds\n local window = ARGV[3] -- interval in milliseconds\n local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1\n\n local requestsInCurrentWindow = redis.call(\"GET\", currentKey)\n if requestsInCurrentWindow == false then\n requestsInCurrentWindow = 0\n end\n\n local requestsInPreviousWindow = redis.call(\"GET\", previousKey)\n if requestsInPreviousWindow == false then\n requestsInPreviousWindow = 0\n end\n local percentageInCurrent = ( now % window ) / window\n -- weighted requests to consider from the previous window\n requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)\n if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then\n return -1\n end\n\n local newValue = redis.call(\"INCRBY\", currentKey, incrementBy)\n if newValue == tonumber(incrementBy) then\n -- The first time this key is set, the value will be equal to incrementBy.\n -- So we only need the expire command once\n redis.call(\"PEXPIRE\", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second\n end\n return tokens - ( newValue + requestsInPreviousWindow )\n`;\n\nexport const slidingWindowRemainingTokensScript = `\n local currentKey = KEYS[1] -- identifier including prefixes\n local previousKey = KEYS[2] -- key of the previous bucket\n local now = ARGV[1] -- current timestamp in milliseconds\n local window = ARGV[2] -- interval in milliseconds\n\n local requestsInCurrentWindow = redis.call(\"GET\", currentKey)\n if requestsInCurrentWindow == false then\n requestsInCurrentWindow = 0\n end\n\n local requestsInPreviousWindow = redis.call(\"GET\", previousKey)\n if requestsInPreviousWindow == false then\n requestsInPreviousWindow = 0\n end\n\n local percentageInCurrent = ( now % window ) / window\n -- weighted requests to consider from the previous window\n requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)\n\n return requestsInPreviousWindow + requestsInCurrentWindow\n`;\n\nexport const tokenBucketLimitScript = `\n local key = KEYS[1] -- identifier including prefixes\n local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens\n local interval = tonumber(ARGV[2]) -- size of the window in milliseconds\n local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval\n local now = tonumber(ARGV[4]) -- current timestamp in milliseconds\n local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1\n \n local bucket = redis.call(\"HMGET\", key, \"refilledAt\", \"tokens\")\n \n local refilledAt\n local tokens\n\n if bucket[1] == false then\n refilledAt = now\n tokens = maxTokens\n else\n refilledAt = tonumber(bucket[1])\n tokens = tonumber(bucket[2])\n end\n \n if now >= refilledAt + interval then\n local numRefills = math.floor((now - refilledAt) / interval)\n tokens = math.min(maxTokens, tokens + numRefills * refillRate)\n\n refilledAt = refilledAt + numRefills * interval\n end\n\n if tokens == 0 then\n return {-1, refilledAt + interval}\n end\n\n local remaining = tokens - incrementBy\n local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval\n \n redis.call(\"HSET\", key, \"refilledAt\", refilledAt, \"tokens\", remaining)\n redis.call(\"PEXPIRE\", key, expireAt)\n return {remaining, refilledAt + interval}\n`;\n\nexport const tokenBucketIdentifierNotFound = -1\n\nexport const tokenBucketRemainingTokensScript = `\n local key = KEYS[1]\n local maxTokens = tonumber(ARGV[1])\n \n local bucket = redis.call(\"HMGET\", key, \"refilledAt\", \"tokens\")\n\n if bucket[1] == false then\n return {maxTokens, ${tokenBucketIdentifierNotFound}}\n end\n \n return {tonumber(bucket[2]), tonumber(bucket[1])}\n`;\n\nexport const cachedFixedWindowLimitScript = `\n local key = KEYS[1]\n local window = ARGV[1]\n local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1\n\n local r = redis.call(\"INCRBY\", key, incrementBy)\n if r == incrementBy then\n -- The first time this key is set, the value will be equal to incrementBy.\n -- So we only need the expire command once\n redis.call(\"PEXPIRE\", key, window)\n end\n \n return r\n`;\n\nexport const cachedFixedWindowRemainingTokenScript = `\n local key = KEYS[1]\n local tokens = 0\n\n local value = redis.call('GET', key)\n if value then\n tokens = value\n end\n return tokens\n`;\n","export const fixedWindowLimitScript = `\n\tlocal key = KEYS[1]\n\tlocal id = ARGV[1]\n\tlocal window = ARGV[2]\n\tlocal incrementBy = tonumber(ARGV[3])\n\n\tredis.call(\"HSET\", key, id, incrementBy)\n\tlocal fields = redis.call(\"HGETALL\", key)\n\tif #fields == 2 and tonumber(fields[2])==incrementBy then\n\t-- The first time this key is set, and the value will be equal to incrementBy.\n\t-- So we only need the expire command once\n\t redis.call(\"PEXPIRE\", key, window)\n\tend\n\n\treturn fields\n`;\nexport const fixedWindowRemainingTokensScript = `\n local key = KEYS[1]\n local tokens = 0\n\n local fields = redis.call(\"HGETALL\", key)\n\n return fields\n `;\n\nexport const slidingWindowLimitScript = `\n\tlocal currentKey = KEYS[1] -- identifier including prefixes\n\tlocal previousKey = KEYS[2] -- key of the previous bucket\n\tlocal tokens = tonumber(ARGV[1]) -- tokens per window\n\tlocal now = ARGV[2] -- current timestamp in milliseconds\n\tlocal window = ARGV[3] -- interval in milliseconds\n\tlocal requestId = ARGV[4] -- uuid for this request\n\tlocal incrementBy = tonumber(ARGV[5]) -- custom rate, default is 1\n\n\tlocal currentFields = redis.call(\"HGETALL\", currentKey)\n\tlocal requestsInCurrentWindow = 0\n\tfor i = 2, #currentFields, 2 do\n\trequestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])\n\tend\n\n\tlocal previousFields = redis.call(\"HGETALL\", previousKey)\n\tlocal requestsInPreviousWindow = 0\n\tfor i = 2, #previousFields, 2 do\n\trequestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])\n\tend\n\n\tlocal percentageInCurrent = ( now % window) / window\n\tif requestsInPreviousWindow * (1 - percentageInCurrent ) + requestsInCurrentWindow >= tokens then\n\t return {currentFields, previousFields, false}\n\tend\n\n\tredis.call(\"HSET\", currentKey, requestId, incrementBy)\n\n\tif requestsInCurrentWindow == 0 then \n\t -- The first time this key is set, the value will be equal to incrementBy.\n\t -- So we only need the expire command once\n\t redis.call(\"PEXPIRE\", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second\n\tend\n\treturn {currentFields, previousFields, true}\n`;\n\nexport const slidingWindowRemainingTokensScript = `\n\tlocal currentKey = KEYS[1] -- identifier including prefixes\n\tlocal previousKey = KEYS[2] -- key of the previous bucket\n\tlocal now \t= ARGV[1] -- current timestamp in milliseconds\n \tlocal window \t= ARGV[2] -- interval in milliseconds\n\n\tlocal currentFields = redis.call(\"HGETALL\", currentKey)\n\tlocal requestsInCurrentWindow = 0\n\tfor i = 2, #currentFields, 2 do\n\trequestsInCurrentWindow = requestsInCurrentWindow + tonumber(currentFields[i])\n\tend\n\n\tlocal previousFields = redis.call(\"HGETALL\", previousKey)\n\tlocal requestsInPreviousWindow = 0\n\tfor i = 2, #previousFields, 2 do\n\trequestsInPreviousWindow = requestsInPreviousWindow + tonumber(previousFields[i])\n\tend\n\n\tlocal percentageInCurrent = ( now % window) / window\n \trequestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)\n\t\n\treturn requestsInCurrentWindow + requestsInPreviousWindow\n`;\n","export const resetScript = `\n local pattern = KEYS[1]\n\n -- Initialize cursor to start from 0\n local cursor = \"0\"\n\n repeat\n -- Scan for keys matching the pattern\n local scan_result = redis.call('SCAN', cursor, 'MATCH', pattern)\n\n -- Extract cursor for the next iteration\n cursor = scan_result[1]\n\n -- Extract keys from the scan result\n local keys = scan_result[2]\n\n for i=1, #keys do\n redis.call('DEL', keys[i])\n end\n\n -- Continue scanning until cursor is 0 (end of keyspace)\n until cursor == \"0\"\n `;\n","import * as Single from \"./single\"\nimport * as Multi from \"./multi\"\nimport { resetScript } from \"./reset\"\n\nexport type ScriptInfo = {\n script: string,\n hash: string\n}\n\ntype Algorithm = {\n limit: ScriptInfo,\n getRemaining: ScriptInfo,\n}\n\ntype AlgorithmKind = \n | \"fixedWindow\"\n | \"slidingWindow\"\n | \"tokenBucket\"\n | \"cachedFixedWindow\"\n\nexport const SCRIPTS: {\n singleRegion: Record<AlgorithmKind, Algorithm>,\n multiRegion: Record<Exclude<AlgorithmKind, \"tokenBucket\" | \"cachedFixedWindow\">, Algorithm>,\n} = {\n singleRegion: {\n fixedWindow: {\n limit: {\n script: Single.fixedWindowLimitScript,\n hash: \"b13943e359636db027ad280f1def143f02158c13\"\n },\n getRemaining: {\n script: Single.fixedWindowRemainingTokensScript,\n hash: \"8c4c341934502aee132643ffbe58ead3450e5208\"\n },\n },\n slidingWindow: {\n limit: {\n script: Single.slidingWindowLimitScript,\n hash: \"e1391e429b699c780eb0480350cd5b7280fd9213\"\n },\n getRemaining: {\n script: Single.slidingWindowRemainingTokensScript,\n hash: \"65a73ac5a05bf9712903bc304b77268980c1c417\"\n },\n },\n tokenBucket: {\n limit: {\n script: Single.tokenBucketLimitScript,\n hash: \"5bece90aeef8189a8cfd28995b479529e270b3c6\"\n },\n getRemaining: {\n script: Single.tokenBucketRemainingTokensScript,\n hash: \"a15be2bb1db2a15f7c82db06146f9d08983900d0\"\n },\n },\n cachedFixedWindow: {\n limit: {\n script: Single.cachedFixedWindowLimitScript,\n hash: \"c26b12703dd137939b9a69a3a9b18e906a2d940f\"\n },\n getRemaining: {\n script: Single.cachedFixedWindowRemainingTokenScript,\n hash: \"8e8f222ccae68b595ee6e3f3bf2199629a62b91a\"\n },\n }\n },\n multiRegion: {\n fixedWindow: {\n limit: {\n script: Multi.fixedWindowLimitScript,\n hash: \"a8c14f3835aa87bd70e5e2116081b81664abcf5c\"\n },\n getRemaining: {\n script: Multi.fixedWindowRemainingTokensScript,\n hash: \"8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd\"\n },\n },\n slidingWindow: {\n limit: {\n script: Multi.slidingWindowLimitScript,\n hash: \"cb4fdc2575056df7c6d422764df0de3a08d6753b\"\n },\n getRemaining: {\n script: Multi.slidingWindowRemainingTokensScript,\n hash: \"558c9306b7ec54abb50747fe0b17e5d44bd24868\"\n },\n },\n }\n}\n\n/** COMMON */\nexport const RESET_SCRIPT: ScriptInfo = {\n script: resetScript,\n hash: \"54bd274ddc59fb3be0f42deee2f64322a10e2b50\"\n}","import type { Redis as RedisCore } from \"@upstash/redis\";\nimport type { Geo } from \"./analytics\";\n\n/**\n * EphemeralCache is used to block certain identifiers right away in case they have already exceeded the ratelimit.\n */\nexport type EphemeralCache = {\n isBlocked: (identifier: string) => { blocked: boolean; reset: number };\n blockUntil: (identifier: string, reset: number) => void;\n\n set: (key: string, value: number) => void;\n get: (key: string) => number | null;\n\n incr: (key: string) => number;\n\n pop: (key: string) => void;\n empty: () => void;\n\n size: () => number;\n}\n\nexport type RegionContext = {\n redis: Redis;\n cache?: EphemeralCache,\n};\nexport type MultiRegionContext = { regionContexts: Omit<RegionContext[], \"cache\">; cache?: EphemeralCache };\n\nexport type RatelimitResponseType = \"timeout\" | \"cacheBlock\" | \"denyList\"\n\nexport type Context = RegionContext | MultiRegionContext;\nexport type RatelimitResponse = {\n /**\n * Whether the request may pass(true) or exceeded the limit(false)\n */\n success: boolean;\n /**\n * Maximum number of requests allowed within a window.\n */\n limit: number;\n /**\n * How many requests the user has left within the current window.\n */\n remaining: number;\n /**\n * Unix timestamp in milliseconds when the limits are reset.\n */\n reset: number;\n\n /**\n * For the MultiRegion setup we do some synchronizing in the background, after returning the current limit.\n * Or when analytics is enabled, we send the analytics asynchronously after returning the limit.\n * In most case you can simply ignore this.\n *\n * On Vercel Edge or Cloudflare workers, you need to explicitly handle the pending Promise like this:\n *\n * ```ts\n * const { pending } = await ratelimit.limit(\"id\")\n * context.waitUntil(pending)\n * ```\n *\n * See `waitUntil` documentation in\n * [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#contextwaituntil)\n * and [Vercel](https://vercel.com/docs/functions/edge-middleware/middleware-api#waituntil)\n * for more details.\n * ```\n */\n pending: Promise<unknown>;\n\n /**\n * Reason behind the result in `success` field.\n * - Is set to \"timeout\" when request times out\n * - Is set to \"cacheBlock\" when an identifier is blocked through cache without calling redis because it was\n * rate limited previously.\n * - Is set to \"denyList\" when identifier or one of ip/user-agent/country parameters is in deny list. To enable\n * deny list, see `enableProtection` parameter. To edit the deny list, see the Upstash Ratelimit Dashboard\n * at https://console.upstash.com/ratelimit.\n * - Is set to undefined if rate limit check had to use Redis. This happens in cases when `success` field in\n * the response is true. It can also happen the first time sucecss is false.\n */\n reason?: RatelimitResponseType;\n\n /**\n * The value which was in the deny list if reason: \"denyList\"\n */\n deniedValue?: DeniedValue\n};\n\nexport type Algorithm<TContext> = () => {\n limit: (\n ctx: TContext,\n identifier: string,\n rate?: number,\n opts?: {\n cache?: EphemeralCache;\n },\n ) => Promise<RatelimitResponse>;\n getRemaining: (ctx: TContext, identifier: string) => Promise<{\n remaining: number,\n reset: number\n }>;\n resetTokens: (ctx: TContext, identifier: string) => Promise<void>;\n};\n\nexport type IsDenied = 0 | 1;\n\nexport type DeniedValue = string | undefined;\nexport type DenyListResponse = { deniedValue: DeniedValue, invalidIpDenyList: boolean }\n\nexport const DenyListExtension = \"denyList\" as const\nexport const IpDenyListKey = \"ipDenyList\" as const\nexport const IpDenyListStatusKey = \"ipDenyListStatus\" as const\n\nexport type LimitPayload = [RatelimitResponse, DenyListResponse];\nexport type LimitOptions = {\n geo?: Geo,\n rate?: number,\n ip?: string,\n userAgent?: string,\n country?: string\n}\n\nexport type Redis = RedisCore\n","export const checkDenyListScript = `\n -- Checks if values provideed in ARGV are present in the deny lists.\n -- This is done using the allDenyListsKey below.\n\n -- Additionally, checks the status of the ip deny list using the\n -- ipDenyListStatusKey below. Here are the possible states of the\n -- ipDenyListStatusKey key:\n -- * status == -1: set to \"disabled\" with no TTL\n -- * status == -2: not set, meaning that is was set before but expired\n -- * status > 0: set to \"valid\", with a TTL\n --\n -- In the case of status == -2, we set the status to \"pending\" with\n -- 30 second ttl. During this time, the process which got status == -2\n -- will update the ip deny list.\n\n local allDenyListsKey = KEYS[1]\n local ipDenyListStatusKey = KEYS[2]\n\n local results = redis.call('SMISMEMBER', allDenyListsKey, unpack(ARGV))\n local status = redis.call('TTL', ipDenyListStatusKey)\n if status == -2 then\n redis.call('SETEX', ipDenyListStatusKey, 30, \"pending\")\n end\n\n return { results, status }\n`","import type { Redis } from \"../types\";\nimport { DenyListExtension, IpDenyListKey, IpDenyListStatusKey } from \"../types\"\nimport { getIpListTTL } from \"./time\"\n\nconst baseUrl = \"https://raw.githubusercontent.com/stamparm/ipsum/master/levels\"\n\nexport class ThresholdError extends Error {\n constructor(threshold: number) {\n super(`Allowed threshold values are from 1 to 8, 1 and 8 included. Received: ${threshold}`);\n this.name = \"ThresholdError\";\n }\n}\n\n/**\n * Fetches the ips from the ipsum.txt at github\n * \n * In the repo we are using, 30+ ip lists are aggregated. The results are\n * stores in text files from 1 to 8.\n * https://github.com/stamparm/ipsum/tree/master/levels\n * \n * X.txt file holds ips which are in at least X of the lists.\n *\n * @param threshold ips with less than or equal to the threshold are not included\n * @returns list of ips\n */\nconst getIpDenyList = async (threshold: number) => {\n if (typeof threshold !== \"number\" || threshold < 1 || threshold > 8) {\n throw new ThresholdError(threshold)\n }\n\n try {\n // Fetch data from the URL\n const response = await fetch(`${baseUrl}/${threshold}.txt`)\n if (!response.ok) {\n throw new Error(`Error fetching data: ${response.statusText}`)\n }\n const data = await response.text()\n\n // Process the data\n const lines = data.split(\"\\n\")\n return lines.filter((value) => value.length > 0) // remove empty values\n } catch (error) {\n throw new Error(`Failed to fetch ip deny list: ${error}`)\n }\n}\n\n/**\n * Gets the list of ips from the github source which are not in the\n * deny list already\n * \n * First, gets the ip list from github using the threshold. Then, calls redis with\n * a transaction which does the following:\n * - subtract the current ip deny list from all\n * - delete current ip deny list\n * - recreate ip deny list with the ips from github. Ips already in the users own lists\n * are excluded.\n * - status key is set to valid with ttl until next 2 AM UTC, which is a bit later than\n * when the list is updated on github.\n *\n * @param redis redis instance\n * @param prefix ratelimit prefix\n * @param threshold ips with less than or equal to the threshold are not included\n * @param ttl time to live in milliseconds for the status flag. Optional. If not\n * passed, ttl is infferred from current time.\n * @returns list of ips which are not in the deny list\n */\nexport const updateIpDenyList = async (\n redis: Redis,\n prefix: string,\n threshold: number,\n ttl?: number\n) => {\n const allIps = await getIpDenyList(threshold)\n\n const allDenyLists = [prefix, DenyListExtension, \"all\"].join(\":\")\n const ipDenyList = [prefix, DenyListExtension, IpDenyListKey].join(\":\")\n const statusKey = [prefix, IpDenyListStatusKey].join(\":\")\n\n const transaction = redis.multi()\n\n // remove the old ip deny list from the all set\n transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList)\n\n // delete the old ip deny list and create new one\n transaction.del(ipDenyList)\n\n transaction.sadd(ipDenyList, allIps.at(0), ...allIps.slice(1))\n\n // make all deny list and ip deny list disjoint by removing duplicate\n // ones from ip deny list\n transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists)\n\n // add remaining ips to all list\n transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList)\n\n // set status key with ttl\n transaction.set(statusKey, \"valid\", {px: ttl ?? getIpListTTL()})\n\n return await transaction.exec()\n}\n\n/**\n * Disables the ip deny list by removing the ip deny list from the all\n * set and removing the ip deny list. Also sets the status key to disabled\n * with no ttl.\n * \n * @param redis redis instance\n * @param prefix ratelimit prefix\n * @returns \n */\nexport const disableIpDenyList = async (redis: Redis, prefix: string) => {\n const allDenyListsKey = [prefix, DenyListExtension, \"all\"].join(\":\")\n const ipDenyListKey = [prefix, DenyListExtension, IpDenyListKey].join(\":\")\n const statusKey = [prefix, IpDenyListStatusKey].join(\":\")\n\n const transaction = redis.multi()\n\n // remove the old ip deny list from the all set\n transaction.sdiffstore(allDenyListsKey, allDenyListsKey, ipDenyListKey)\n\n // delete the old ip deny list\n transaction.del(ipDenyListKey)\n\n // set to disabled\n // this way, the TTL command in checkDenyListScript will return -1.\n transaction.set(statusKey, \"disabled\")\n\n return await transaction.exec()\n}\n","\n// Number of milliseconds in one hour\nconst MILLISECONDS_IN_HOUR = 60 * 60 * 1000;\n\n// Number of milliseconds in one day\nconst MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;\n\n// Number of milliseconds from the current time to 2 AM UTC\nconst MILLISECONDS_TO_2AM = 2 * MILLISECONDS_IN_HOUR;\n\nexport const getIpListTTL = (time?: number) => {\n const now = time || Date.now();\n\n // Time since the last 2 AM UTC\n const timeSinceLast2AM = (now - MILLISECONDS_TO_2AM) % MILLISECONDS_IN_DAY;\n\n // Remaining time until the next 2 AM UTC\n return MILLISECONDS_IN_DAY - timeSinceLast2AM;\n}\n ","import type { DeniedValue, DenyListResponse, LimitPayload} from \"../types\";\nimport { DenyListExtension, IpDenyListStatusKey } from \"../types\"\nimport type { RatelimitResponse, Redis } from \"../types\"\nimport { Cache } from \"../cache\";\nimport { checkDenyListScript } from \"./scripts\";\nimport { updateIpDenyList } from \"./ip-deny-list\";\n\n\nconst denyListCache = new Cache(new Map());\n\n/**\n * Checks items in members list and returns the first denied member\n * in denyListCache if there are any.\n * \n * @param members list of values to check against the cache\n * @returns a member from the cache. If there is none, returns undefined\n */\nexport const checkDenyListCache = (members: string[]): DeniedValue => {\n return members.find(\n member => denyListCache.isBlocked(member).blocked\n );\n}\n\n/**\n * Blocks a member for 1 minute.\n * \n * If there are more than 1000 elements in the cache, empties\n * it so that the cache doesn't grow in size indefinetely.\n * \n * @param member member to block\n */\nconst blockMember = (member: string) => {\n if (denyListCache.size() > 1000) denyListCache.empty();\n denyListCache.blockUntil(member, Date.now() + 60_000);\n}\n\n/**\n * Checks if identifier or any of the values are in any of\n * the denied lists in Redis.\n * \n * If some value is in a deny list, we block the identifier for a minute.\n * \n * @param redis redis client\n * @param prefix ratelimit prefix\n * @param members List of values (identifier, ip, user agent, country)\n * @returns true if a member is in deny list at Redis\n */\nexport const checkDenyList = async (\n redis: Redis,\n prefix: string,\n members: string[]\n): Promise<DenyListResponse> => {\n const [ deniedValues, ipDenyListStatus ] = await redis.eval(\n checkDenyListScript,\n [\n [prefix, DenyListExtension, \"all\"].join(\":\"),\n [prefix, IpDenyListStatusKey].join(\":\"),\n ],\n members\n ) as [boolean[], number];\n\n let deniedValue: DeniedValue = undefined;\n deniedValues.map((memberDenied, index) => {\n if (memberDenied) {\n blockMember(members[index])\n deniedValue = members[index]\n }\n })\n\n return {\n deniedValue,\n invalidIpDenyList: ipDenyListStatus === -2\n };\n};\n\n/**\n * Overrides the rate limit response if deny list\n * response indicates that value is in deny list.\n * \n * @param ratelimitResponse \n * @param denyListResponse \n * @returns \n */\nexport const resolveLimitPayload = (\n redis: Redis,\n prefix: string,\n [ratelimitResponse, denyListResponse]: LimitPayload,\n threshold: number\n): RatelimitResponse => {\n\n if (denyListResponse.deniedValue) {\n ratelimitResponse.success = false;\n ratelimitResponse.remaining = 0;\n ratelimitResponse.reason = \"denyList\";\n ratelimitResponse.deniedValue = denyListResponse.deniedValue\n }\n\n if (denyListResponse.invalidIpDenyList) {\n const updatePromise = updateIpDenyList(redis, prefix, threshold)\n ratelimitResponse.pending = Promise.all([\n ratelimitResponse.pending,\n updatePromise\n ])\n }\n\n return ratelimitResponse;\n};\n\n/**\n * \n * @returns Default response to return when some item\n * is in deny list.\n */\nexport const defaultDeniedResponse = (deniedValue: string): RatelimitResponse => {\n return {\n success: false,\n limit: 0,\n remaining: 0,\n reset: 0,\n pending: Promise.resolve(),\n reason: \"denyList\",\n deniedValue: deniedValue\n }\n}\n","import { Analytics } from \"./analytics\";\nimport { Cache } from \"./cache\";\nimport type { Algorithm, Context, LimitOptions, LimitPayload, RatelimitResponse, Redis } from \"./types\";\nimport { checkDenyList, checkDenyListCache, defaultDeniedResponse, resolveLimitPayload } from \"./deny-list/index\";\n\nexport class TimeoutError extends Error {\n constructor() {\n super(\"Timeout\");\n this.name = \"TimeoutError\";\n }\n}\nexport type RatelimitConfig<TContext> = {\n /**\n * The ratelimiter function to use.\n *\n * Choose one of the predefined ones or implement your own.\n * Available algorithms are exposed via static methods:\n * - Ratelimiter.fixedWindow\n * - Ratelimiter.slidingWindow\n * - Ratelimiter.tokenBucket\n */\n\n limiter: Algorithm<TContext>;\n\n ctx: TContext;\n /**\n * All keys in redis are prefixed with this.\n *\n * @default `@upstash/ratelimit`\n */\n prefix?: string;\n\n /**\n * If enabled, the ratelimiter will keep a global cache of identifiers, that have\n * exhausted their ratelimit. In serverless environments this is only possible if\n * you create the ratelimiter instance outside of your handler function. While the\n * function is still hot, the ratelimiter can block requests without having to\n * request data from redis, thus saving time and money.\n *\n * Whenever an identifier has exceeded its limit, the ratelimiter will add it to an\n * internal list together with its reset timestamp. If the same identifier makes a\n * new request before it is reset, we can immediately reject it.\n *\n * Set to `false` to disable.\n *\n * If left undefined, a map is created automatically, but it can only work\n * if the map or the ratelimit instance is created outside your serverless function handler.\n */\n ephemeralCache?: Map<string, number> | false;\n\n /**\n * If set, the ratelimiter will allow requests to pass after this many milliseconds.\n *\n * Use this if you want to allow requests in case of network problems\n *\n * @default 5000\n */\n timeout?: number;\n\n /**\n * If enabled, the ratelimiter will store analytics data in redis, which you can check out at\n * https://console.upstash.com/ratelimit\n *\n * @default false\n */\n analytics?: boolean;\n\n /**\n * Enables deny list. If set to true, requests with identifier or ip/user-agent/countrie\n * in the deny list will be rejected automatically. To edit the deny list, check out the\n * ratelimit dashboard at https://console.upstash.com/ratelimit\n * \n * @default false\n */\n enableProtection?: boolean\n\n denyListThreshold?: number\n};\n\n/**\n * Ratelimiter using serverless redis from https://upstash.com/\n *\n * @example\n * ```ts\n * const { limit } = new Ratelimit({\n * redis: Redis.fromEnv(),\n * limiter: Ratelimit.slidingWindow(\n * 10, // Allow 10 requests per window of 30 minutes\n * \"30 m\", // interval of 30 minutes\n * ),\n * })\n *\n * ```\n */\nexport abstract class Ratelimit<TContext extends Context> {\n protected readonly limiter: Algorithm<TContext>;\n\n protected readonly ctx: TContext;\n\n protected readonly prefix: string;\n\n protected readonly timeout: number;\n\n protected readonly primaryRedis: Redis;\n\n protected readonly analytics?: Analytics;\n\n protected readonly enableProtection: boolean;\n\n protected readonly denyListThreshold: number\n\n constructor(config: RatelimitConfig<TContext>) {\n this.ctx = config.ctx;\n this.limiter = config.limiter;\n this.timeout = config.timeout ?? 5000;\n this.prefix = config.prefix ?? \"@upstash/ratelimit\";\n\n this.enableProtection = config.enableProtection ?? false;\n this.denyListThreshold = config.denyListThreshold ?? 6;\n\n this.primaryRedis = (\"redis\" in this.ctx) ? this.ctx.redis : this.ctx.regionContexts[0].redis\n this.analytics = config.analytics\n ? new Analytics({\n redis: this.primaryRedis,\n prefix: this.prefix,\n })\n : undefined;\n\n if (config.ephemeralCache instanceof Map) {\n this.ctx.cache = new Cache(config.ephemeralCache);\n } else if (config.ephemeralCache === undefined) {\n this.ctx.cache = new Cache(new Map());\n }\n }\n\n /**\n * Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.\n *\n * Use this if you want to reject all requests that you can not handle right now.\n *\n * @example\n * ```ts\n * const ratelimit = new Ratelimit({\n * redis: Redis.fromEnv(),\n * limiter: Ratelimit.slidingWindow(10, \"10 s\")\n * })\n *\n * const { success } = await ratelimit.limit(id)\n * if (!success){\n * return \"Nope\"\n * }\n * return \"Yes\"\n * ```\n *\n * @param req.rate - The rate at which tokens will be added or consumed from the token bucket. A higher rate allows for more requests to be processed. Defaults to 1 token per interval if not specified.\n *\n * Usage with `req.rate`\n * @example\n * ```ts\n * const ratelimit = new Ratelimit({\n * redis: Redis.fromEnv(),\n * limiter: Ratelimit.slidingWindow(100, \"10 s\")\n * })\n *\n * const { success } = await ratelimit.limit(id, {rate: 10})\n * if (!success){\n * return \"Nope\"\n * }\n * return \"Yes\"\n * ```\n */\n public limit = async (\n identifier: string,\n req?: LimitOptions,\n ): Promise<RatelimitResponse> => {\n\n let timeoutId: any = null;\n try {\n const response = this.getRatelimitResponse(identifier, req);\n const { responseArray, newTimeoutId } = this.applyTimeout(response);\n timeoutId = newTimeoutId;\n\n const timedResponse = await Promise.race(responseArray);\n const finalResponse = this.submitAnalytics(timedResponse, identifier, req);\n return finalResponse;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n };\n\n /**\n * Block until the request may pass or timeout is reached.\n *\n * This method returns a promise that resolves as soon as the request may be processed\n * or after the timeout has been reached.\n *\n * Use this if you want to delay the request until it is ready to get processed.\n *\n * @example\n * ```ts\n * const ratelimit = new Ratelimit({\n * redis: Redis.fromEnv(),\n * limiter: Ratelimit.slidingWindow(10, \"10 s\")\n * })\n *\n * const { success } = await ratelimit.blockUntilReady(id, 60_000)\n * if (!success){\n * return \"Nope\"\n * }\n * return \"Yes\"\n * ```\n */\n public blockUntilReady = async (\n /**\n * An identifier per user or api.\n * Choose a userID, or api token, or ip address.\n *\n * If you want to limit your api across all users, you can set a constant string.\n */\n identifier: string,\n /**\n * Maximum duration to wait in milliseconds.\n * After this time the request will be denied.\n */\n timeout: number,\n ): Promise<RatelimitResponse> => {\n if (timeout <= 0) {\n throw new Error(\"timeout must be positive\");\n }\n let res: RatelimitResponse;\n\n const deadline = Date.now() + timeout;\n while (true) {\n res = await this.limit(identifier);\n if (res.success) {\n break;\n }\n if (res.reset === 0) {\n throw new Error(\"This should not happen\");\n }\n\n const wait = Math.min(res.reset, deadline) - Date.now();\n await new Promise((r) => setTimeout(r, wait));\n\n if (Date.now() > deadline) {\n break;\n }\n }\n return res!;\n };\n\n public resetUsedTokens = async (identifier: string) => {\n const pattern = [this.prefix, identifier].join(\":\");\n await this.limiter().resetTokens(this.ctx, pattern);\n };\n\n /**\n * Returns the remaining token count together with a reset timestamps\n * \n * @param identifier identifir to check\n * @returns object with `remaining` and reset fields. `remaining` denotes\n * the remaining tokens and reset denotes the timestamp when the\n * tokens reset.\n */\n public getRemaining = async (identifier: string): Promise<{\n remaining: number;\n reset: number;\n }> => {\n const pattern = [this.prefix, identifier].join(\":\");\n\n return await this.limiter().getRemaining(this.ctx, pattern);\n };\n\n /**\n * Checks if the identifier or the values in req are in the deny list cache.\n * If so, returns the default denied response.\n * \n * Otherwise, calls redis to check the rate limit and deny list. Returns after\n * resolving the result. Resolving is overriding the rate limit result if\n * the some value is in deny list.\n * \n * @param identifier identifier to block\n * @param req options with ip, user agent, country, rate and geo info\n * @returns rate limit response\n */\n private getRatelimitResponse = async (\n identifier: string,\n req?: LimitOptions\n ): Promise<RatelimitResponse> => {\n const key = this.getKey(identifier);\n const definedMembers = this.getDefinedMembers(identifier, req);\n\n const deniedValue = checkDenyListCache(definedMembers)\n\n const result: LimitPayload = deniedValue ? [defaultDeniedResponse(deniedValue), { deniedValue, invalidIpDenyList: false }] : (await Promise.all([\n this.limiter().limit(this.ctx, key, req?.rate),\n this.enableProtection\n ? checkDenyList(this.primaryRedis, this.prefix, definedMembers)\n : { deniedValue: undefined, invalidIpDenyList: false }\n ]));\n\n return resolveLimitPayload(this.primaryRedis, this.prefix, result, this.denyListThreshold)\n };\n\n /**\n * Creates an array with the original response promise and a timeout promise\n * if this.timeout > 0.\n * \n * @param response Ratelimit response promise\n * @returns array with the response and timeout promise. also includes the timeout id\n */\n private applyTimeout = (response: Promise<RatelimitResponse>) => {\n let newTimeoutId: any = null;\n const responseArray: Array<Promise<RatelimitResponse>> = [response];\n\n if (this.timeout > 0) {\n const timeoutResponse = new Promise<RatelimitResponse>((resolve) => {\n newTimeoutId = setTimeout(() => {\n resolve({\n success: true,\n limit: 0,\n remaining: 0,\n reset: 0,\n pending: Promise.resolve(),\n reason: \"timeout\"\n });\n }, this.timeout);\n })\n responseArray.push(timeoutResponse);\n }\n\n return {\n responseArray,\n newTimeoutId,\n }\n }\n\n /**\n * submits analytics if this.analytics is set\n * \n * @param ratelimitResponse final rate limit response\n * @param identifier identifier to submit\n * @param req limit options\n * @returns rate limit response after updating the .pending field\n */\n private submitAnalytics = (\n ratelimitResponse: RatelimitResponse,\n identifier: string,\n req?: Pick<LimitOptions, \"geo\">,\n ) => {\n if (this.analytics) {\n try {\n const geo = req ? this.analytics.extractGeo(req) : undefined;\n const analyticsP = this.analytics\n .record({\n identifier: ratelimitResponse.reason === \"denyList\" // if in denyList, use denied value as identifier\n ? ratelimitResponse.deniedValue!\n : identifier,\n time: Date.now(),\n success: ratelimitResponse.reason === \"denyList\" // if in denyList, label success as \"denied\"\n ? \"denied\"\n : ratelimitResponse.success,\n ...geo,\n })\n .catch((error) => {\n let errorMessage = \"Failed to record analytics\"\n if (`${error}`.includes(\"WRONGTYPE\")) {\n errorMessage = `\n Failed to record analytics. See the information below:\n\n This can occur when you uprade to Ratelimit version 1.1.2\n or later from an earlier version.\n\n This occurs simply because the way we store analytics data\n has changed. To avoid getting this error, disable analytics\n for *an hour*, then simply enable it back.\\n\n `\n }\n console.warn(errorMessage, error);\n });\n ratelimitResponse.pending = Promise.all([ratelimitResponse.pending, analyticsP]);\n } catch (error) {\n console.warn(\"Failed to record analytics\", error);\n };\n };\n return ratelimitResponse;\n }\n\n private getKey = (identifier: string): string => {\n return [this.prefix, identifier].join(\":\");\n }\n\n /**\n * returns a list of defined values from\n * [identifier, req.ip, req.userAgent, req.country]\n * \n * @param identifier identifier\n * @param req limit options\n * @returns list of defined values\n */\n private getDefinedMembers = (\n identifier: string,\n req?: Pick<LimitOptions, \"ip\" | \"userAgent\" | \"country\">\n ): string[] => {\n const members = [identifier, req?.ip, req?.userAgent, req?.country];\n return (members as string[]).filter(Boolean);\n }\n}\n","import { Cache } from \"./cache\";\nimport type { Duration } from \"./duration\";\nimport { ms } from \"./duration\";\nimport { safeEval } from \"./hash\";\nimport { RESET_SCRIPT, SCRIPTS } from \"./lua-scripts/hash\";\n\n\nimport { Ratelimit } from \"./ratelimit\";\nimport type { Algorithm, MultiRegionContext } from \"./types\";\n\nimport type { Redis } from \"./types\";\n\nfunction randomId(): string {\n let result = \"\";\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\";\n const charactersLength = characters.length;\n for (let i = 0; i < 16; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n}\n\nexport type MultiRegionRatelimitConfig = {\n /**\n * Instances of `@upstash/redis`\n * @see https://github.com/upstash/upstash-redis#quick-start\n */\n redis: Redis[];\n /**\n * The ratelimiter function to use.\n *\n * Choose one of the predefined ones or implement your own.\n * Available algorithms are exposed via static methods:\n * - MultiRegionRatelimit.fixedWindow\n */\n limiter: Algorithm<MultiRegionContext>;\n /**\n * All keys in redis are prefixed with this.\n *\n * @default `@upstash/ratelimit`\n */\n prefix?: string;\n\n /**\n * If enabled, the ratelimiter will keep a global cache of identifiers, that have\n * exhausted their ratelimit. In serverless environments this is only possible if\n * you create the ratelimiter instance outside of your handler function. While the\n * function is still hot, the ratelimiter can block requests without having to\n * request data from redis, thus saving time and money.\n *\n * Whenever an identifier has exceeded its limit, the ratelimiter will add it to an\n * internal list together with its reset timestamp. If the same identifier makes a\n * new request before it is reset, we can immediately reject it.\n *\n * Set to `false` to disable.\n *\n * If left undefined, a map is created automatically, but it can only work\n * if the map or the ratelimit instance is created outside your serverless function handler.\n */\n ephemeralCache?: Map<string, number> | false;\n\n /**\n * If set, the ratelimiter will allow requests to pass after this many milliseconds.\n *\n * Use this if you want to allow requests in case of network problems\n */\n timeout?: number;\n\n /**\n * If enabled, the ratelimiter will store analytics data in redis, which you can check out at\n * https://console.upstash.com/ratelimit\n *\n * @default false\n */\n analytics?: boolean;\n\n /**\n * If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request.\n * In the subsequent requests, hash of the script will be used to invoke it\n * \n * @default true\n */\n cacheScripts?: boolean;\n};\n\n/**\n * Ratelimiter using serverless redis from https://upstash.com/\n *\n * @example\n * ```ts\n * const { limit } = new MultiRegionRatelimit({\n * redis: Redis.fromEnv(),\n * limiter: MultiRegionRatelimit.fixedWindow(\n * 10, // Allow 10 requests per window of 30 minutes\n * \"30 m\", // interval of 30 minutes\n * )\n * })\n *\n * ```\n */\nexport class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {\n /**\n * Create a new Ratelimit instance by providing a `@upstash/redis` instance and the algorithn of your choice.\n */\n constructor(config: MultiRegionRatelimitConfig) {\n super({\n prefix: config.prefix,\n limi