{"version":3,"file":"rate-limiter.mjs","names":["constants"],"sources":["../../../../src/libs/http/middleware/rate-limiter.ts"],"sourcesContent":["import { createMiddleware } from \"hono/factory\";\nimport constants from \"../../../constants/constants.js\";\nimport type { LucidHonoContext } from \"../../../types.js\";\nimport { LucidAPIError } from \"../../../utils/errors/index.js\";\nimport { copy } from \"../../i18n/index.js\";\nimport cacheKeys from \"../../kv/cache-keys.js\";\nimport { supportsKVIncrement } from \"../../kv/utils.js\";\nimport createServiceContext from \"../utils/create-service-context.js\";\n\ntype RateLimitMode = \"ip\" | \"user\" | \"client\";\n\ntype RateLimitOptions = {\n\t/**\n\t * What to use as the rate limit key\n\t * - \"ip\": Client IP address (default)\n\t * - \"user\": Authenticated user ID\n\t * - \"client\": Client integration ID\n\t */\n\tmode: RateLimitMode;\n\t/** Time window in milliseconds */\n\twindowMs: number;\n\t/** Maximum number of requests allowed within the window */\n\tlimit: number;\n\t/**\n\t * Scope for the rate limit. Creates separate rate limit groups per scope value. Useful for per-route or per-action limits.\n\t */\n\tscope?: string | ((c: LucidHonoContext) => string | Promise<string>);\n\t/**\n\t * Custom key generator function. Takes precedence over mode and scope.\n\t */\n\tkeyGenerator?: (c: LucidHonoContext) => string | Promise<string>;\n\t/**\n\t * Function to skip rate limiting for certain requests.\n\t * Return true to bypass rate limiting.\n\t */\n\tskip?: (c: LucidHonoContext) => boolean | Promise<boolean>;\n};\n\ntype RateLimitRecord = {\n\tcount: number;\n\tresetTime: number;\n};\n\nconst resolveScope = async (\n\tc: LucidHonoContext,\n\tscope: RateLimitOptions[\"scope\"],\n): Promise<string | undefined> => {\n\tif (!scope) return undefined;\n\treturn typeof scope === \"function\" ? scope(c) : scope;\n};\n\nconst rateLimiter = (options: RateLimitOptions) =>\n\tcreateMiddleware(async (c: LucidHonoContext, next) => {\n\t\tif (options.skip && (await options.skip(c))) {\n\t\t\tawait next();\n\t\t\treturn;\n\t\t}\n\n\t\tconst context = createServiceContext(c);\n\t\tconst kv = context.kv;\n\n\t\tconst scope = await resolveScope(c, options.scope);\n\t\tlet key: string;\n\n\t\tif (options.keyGenerator) {\n\t\t\tconst generatedKey = await options.keyGenerator(c);\n\t\t\tkey = cacheKeys.rateLimit.record(generatedKey);\n\t\t} else {\n\t\t\tswitch (options.mode) {\n\t\t\t\tcase \"user\": {\n\t\t\t\t\tconst auth = c.get(\"auth\");\n\t\t\t\t\tif (!auth) {\n\t\t\t\t\t\tthrow new LucidAPIError({\n\t\t\t\t\t\t\ttype: \"authorisation\",\n\t\t\t\t\t\t\tcode: \"authorisation\",\n\t\t\t\t\t\t\tmessage: copy(\"server:core.rate.limit.authentication.required\"),\n\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tkey = cacheKeys.rateLimit.user(auth.id);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"client\": {\n\t\t\t\t\tconst clientIntegration = c.get(\"clientIntegrationAuth\");\n\t\t\t\t\tif (!clientIntegration) {\n\t\t\t\t\t\tthrow new LucidAPIError({\n\t\t\t\t\t\t\ttype: \"authorisation\",\n\t\t\t\t\t\t\tmessage: copy(\"server:core.rate.limit.authentication.required\"),\n\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tkey = cacheKeys.rateLimit.client(clientIntegration.id);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\tconst runtimeContext = c.get(\"runtimeContext\");\n\t\t\t\t\tconst connectionInfo = runtimeContext.getConnectionInfo(c);\n\t\t\t\t\tif (!connectionInfo.address) {\n\t\t\t\t\t\tthrow new LucidAPIError({\n\t\t\t\t\t\t\ttype: \"authorisation\",\n\t\t\t\t\t\t\tmessage: copy(\"server:core.rate.limit.ip.address.required\"),\n\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tkey = cacheKeys.rateLimit.ip(connectionInfo.address);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (scope) key += `:${scope}`;\n\t\t}\n\n\t\tlet count: number;\n\t\tlet resetSeconds: number;\n\n\t\tif (supportsKVIncrement(kv)) {\n\t\t\tconst ttl = Math.max(1, Math.ceil(options.windowMs / 1000));\n\t\t\tconst result = await kv.increment(context, {\n\t\t\t\tkey: `${key}:counter`,\n\t\t\t\texpirationTtl: ttl,\n\t\t\t});\n\n\t\t\tcount = result.value;\n\t\t\tresetSeconds = result.expirationTtl ?? ttl;\n\t\t} else {\n\t\t\tconst now = Date.now();\n\t\t\tconst existing = await kv.get<RateLimitRecord>(context, { key });\n\n\t\t\tlet record: RateLimitRecord;\n\t\t\tif (existing && existing.resetTime > now) {\n\t\t\t\trecord = {\n\t\t\t\t\tcount: existing.count + 1,\n\t\t\t\t\tresetTime: existing.resetTime,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\trecord = {\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tresetTime: now + options.windowMs,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst ttl = Math.max(\n\t\t\t\t1,\n\t\t\t\tMath.ceil((record.resetTime - now) / 1000) +\n\t\t\t\t\tconstants.rateLimit.ttlBufferSeconds,\n\t\t\t);\n\t\t\tawait kv.set(context, { key, value: record, expirationTtl: ttl });\n\n\t\t\tcount = record.count;\n\t\t\tresetSeconds = Math.max(0, Math.ceil((record.resetTime - now) / 1000));\n\t\t}\n\n\t\tconst remaining = Math.max(0, options.limit - count);\n\n\t\tc.header(\"RateLimit-Limit\", options.limit.toString());\n\t\tc.header(\"RateLimit-Remaining\", remaining.toString());\n\t\tc.header(\"RateLimit-Reset\", resetSeconds.toString());\n\n\t\tif (count > options.limit) {\n\t\t\tc.header(\"Retry-After\", resetSeconds.toString());\n\t\t\tthrow new LucidAPIError({\n\t\t\t\ttype: \"rate_limit\",\n\t\t\t\tcode: \"rate_limit\",\n\t\t\t\tmessage: copy(\"server:core.rate.limit.exceeded.message\", {\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tresetSeconds,\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t\tstatus: 429,\n\t\t\t});\n\t\t}\n\n\t\tawait next();\n\t});\n\nexport default rateLimiter;\n"],"mappings":"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"}