UNPKG

@unkey/ratelimit

Version:

<div align="center"> <h1 align="center">@unkey/ratelimit</h1> <h5>@unkey/ratelimit is a library for fast global ratelimiting in serverless functions.</h5> </div>

1 lines 13.3 kB
{"version":3,"sources":["../src/noop.ts","../src/ratelimit.ts","../src/duration.ts","../src/overrides.ts"],"sourcesContent":["import type { Ratelimiter } from \"./interface\";\nimport type { LimitOptions, RatelimitResponse } from \"./types\";\n\nexport class NoopRatelimit implements Ratelimiter {\n public limit(\n _identifier: string,\n _opts?: LimitOptions,\n ): Promise<RatelimitResponse> {\n return Promise.resolve({\n limit: 0,\n remaining: 0,\n reset: 0,\n success: true,\n });\n }\n}\n","import { Unkey } from \"@unkey/api\";\nimport { UnkeyError } from \"@unkey/api/models/errors/unkeyerror\";\nimport { type Duration, ms } from \"./duration\";\nimport type { Ratelimiter } from \"./interface\";\nimport type { Cache, Limit, LimitOptions, RatelimitResponse } from \"./types\";\n\nexport type RatelimitConfig = Limit & {\n /**\n * @default https://api.unkey.com\n */\n baseUrl?: string;\n\n /**\n * The unkey root key. You can create one at https://unkey.dev/app/settings/root-keys\n *\n * Make sure the root key has permissions to use ratelimiting.\n */\n rootKey: string;\n\n /**\n * Namespaces allow you to separate different areas of your app and have isolated limits.\n *\n * @example tRPC-routes\n */\n namespace: string;\n\n /**\n * Configure a timeout to prevent network issues from blocking your function for too long.\n *\n * Disable it by setting `timeout: false`\n *\n * @default\n * ```ts\n * {\n * // 5 seconds\n * ms: 5000,\n * fallback: { success: false, limit: 0, remaining: 0, reset: Date.now()}\n * }\n * ```\n */\n timeout?:\n | {\n /**\n * Time in milliseconds until the response is returned\n */\n ms: number | Duration;\n\n /**\n * A custom response to return when the timeout is reached.\n *\n * The important bit is the `success` value, choose whether you want to let requests pass or not.\n *\n * @example With a static response\n * ```ts\n * {\n * // 5 seconds\n * ms: 5000\n * fallback: () => ({ success: true, limit: 0, remaining: 0, reset: 0 })\n * }\n * ```\n * @example With a dynamic response\n * ```ts\n * {\n * // 5 seconds\n * ms: 5000\n * fallback: (identifier: string) => {\n * if (someCheck(identifier)) {\n * return { success: false, limit: 0, remaining: 0, reset: 0 }\n * }\n * return { success: true, limit: 0, remaining: 0, reset: 0 }\n * }\n * }\n * ```\n */\n fallback:\n | RatelimitResponse\n | ((\n identifier: string,\n ) => RatelimitResponse | Promise<RatelimitResponse>);\n }\n | false /**\n * Configure what happens for unforeseen errors\n *\n * @example Letting requests pass\n * ```ts\n * onError: () => ({ success: true, limit: 0, remaining: 0, reset: 0 })\n * ```\n *\n * @example Rejecting the request\n * ```ts\n * onError: () => ({ success: true, limit: 0, remaining: 0, reset: 0 })\n * ```\n *\n * @example Dynamic response\n * ```ts\n * onError: (error, identifier) => {\n * if (someCheck(error, identifier)) {\n * return { success: false, limit: 0, remaining: 0, reset: 0 }\n * }\n * return { success: true, limit: 0, remaining: 0, reset: 0 }\n * }\n * ```\n */;\n onError?: (\n err: Error,\n identifier: string,\n ) => RatelimitResponse | Promise<RatelimitResponse> /**\n * Cache abusive identifiers and block them immediately without a network request.\n *\n * ```ts\n * // in global scope\n * const cache = new Map()\n *\n * const unkey = new Ratelimit({\n * // ...\n * cache: cache,\n * })\n * ````\n */;\n cache?: Cache;\n\n /**\n *\n * By default telemetry data is enabled, and sends:\n * runtime (Node.js / Edge)\n * platform (Node.js / Vercel / AWS)\n * SDK version\n */\n disableTelemetry?: boolean;\n};\n\nexport class Ratelimit implements Ratelimiter {\n private readonly config: RatelimitConfig;\n private readonly unkey: Unkey;\n private readonly cache: Cache;\n\n constructor(config: RatelimitConfig) {\n this.config = config;\n this.unkey = new Unkey({\n serverURL: config.baseUrl,\n rootKey: config.rootKey,\n });\n this.cache = config.cache ?? new Map();\n }\n\n /**\n * Limit a specific identifier, you can override a lot of things about this specific request using\n * the 2nd argument.\n *\n * @example\n * ```ts\n * const identifier = getIpAddress() // or userId or anything else\n * const res = await unkey.limit(identifier)\n *\n * if (!res.success){\n * // reject request\n * }\n * // handle request\n * ```\n */\n public async limit(\n identifier: string,\n opts?: LimitOptions,\n ): Promise<RatelimitResponse> {\n try {\n return await this._limit(\n identifier,\n opts?.limit?.limit ?? this.config.limit,\n ms(opts?.limit?.duration ?? this.config.duration),\n opts?.cost ?? 1,\n );\n } catch (e) {\n if (typeof this.config.onError !== \"function\") {\n throw e;\n }\n\n const err =\n e instanceof UnkeyError\n ? new Error(e.message)\n : e instanceof Error\n ? e\n : new Error(String(e));\n\n return await this.config.onError(err, identifier);\n }\n }\n\n // _limit just handles the racing and caching. It must not handle errors, those are handled by limit\n private async _limit(\n identifier: string,\n limit: number,\n duration: number,\n cost: number,\n ): Promise<RatelimitResponse> {\n const cacheKey = `${this.config.namespace}:${identifier}:${limit}:${duration}`;\n const naughty = this.cache.get(cacheKey);\n if (naughty) {\n if (naughty.reset > Date.now()) {\n return naughty;\n } else {\n this.cache.delete(cacheKey);\n }\n }\n\n const timeout =\n this.config.timeout === false\n ? null\n : (this.config.timeout ?? {\n ms: 5000,\n fallback: () => ({\n success: false,\n limit: 0,\n remaining: 0,\n reset: Date.now(),\n }),\n });\n\n let timeoutId: any = null;\n try {\n const ps: Promise<RatelimitResponse>[] = [\n this.unkey.ratelimit\n .limit({\n namespace: this.config.namespace,\n identifier,\n limit,\n duration,\n cost,\n })\n .then(async (res) => {\n return res.data;\n }),\n ];\n if (timeout) {\n ps.push(\n new Promise((resolve) => {\n timeoutId = setTimeout(async () => {\n const resolvedValue =\n typeof timeout.fallback === \"function\"\n ? await timeout.fallback(identifier)\n : timeout.fallback;\n resolve(resolvedValue);\n }, ms(timeout.ms));\n }),\n );\n }\n\n const res = await Promise.race(ps);\n if (!res.success) {\n this.cache.set(cacheKey, res);\n }\n\n return res;\n } finally {\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n }\n}\n","type Unit = \"ms\" | \"s\" | \"m\" | \"h\" | \"d\";\n\nexport type Duration = `${number} ${Unit}` | `${number}${Unit}`;\n\n/**\n * Convert a human readable duration to milliseconds\n */\nexport function ms(d: Duration | number): number {\n if (typeof d === \"number\") {\n return d;\n }\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 case \"s\":\n return time * 1000;\n case \"m\":\n return time * 1000 * 60;\n case \"h\":\n return time * 1000 * 60 * 60;\n case \"d\":\n return time * 1000 * 60 * 60 * 24;\n\n default:\n throw new Error(`Unable to parse window size: ${d}`);\n }\n}\n","import { Unkey } from \"@unkey/api\";\nimport type { RequestOptions } from \"@unkey/api/lib/sdks\";\nimport type {\n V2RatelimitDeleteOverrideRequestBody,\n V2RatelimitDeleteOverrideResponseBody,\n V2RatelimitGetOverrideRequestBody,\n V2RatelimitGetOverrideResponseBody,\n V2RatelimitListOverridesRequestBody,\n V2RatelimitListOverridesResponseBody,\n V2RatelimitSetOverrideRequestBody,\n V2RatelimitSetOverrideResponseBody,\n} from \"@unkey/api/models/components\";\n\nexport type OverrideConfig = {\n /**\n * @default https://api.unkey.com\n */\n baseUrl?: string;\n\n /**\n * The unkey root key. You can create one at https://app.unkey.com/settings/root-keys\n *\n * Make sure the root key has permissions to use overrides.\n */\n rootKey: string;\n};\n\nexport class Overrides {\n private readonly unkey: Unkey;\n\n constructor(config: OverrideConfig) {\n this.unkey = new Unkey({\n serverURL: config.baseUrl,\n rootKey: config.rootKey,\n });\n }\n\n public getOverride(\n request: V2RatelimitGetOverrideRequestBody,\n options?: RequestOptions,\n ): Promise<V2RatelimitGetOverrideResponseBody> {\n return this.unkey.ratelimit.getOverride(request, options);\n }\n\n public setOverride(\n request: V2RatelimitSetOverrideRequestBody,\n options?: RequestOptions,\n ): Promise<V2RatelimitSetOverrideResponseBody> {\n return this.unkey.ratelimit.setOverride(request, options);\n }\n\n public deleteOverride(\n request: V2RatelimitDeleteOverrideRequestBody,\n options?: RequestOptions,\n ): Promise<V2RatelimitDeleteOverrideResponseBody> {\n return this.unkey.ratelimit.deleteOverride(request, options);\n }\n\n public listOverrides(\n request: V2RatelimitListOverridesRequestBody,\n options?: RequestOptions,\n ): Promise<V2RatelimitListOverridesResponseBody> {\n return this.unkey.ratelimit.listOverrides(request, options);\n }\n}\n"],"mappings":"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","names":["res","Unkey"]}