{"version":3,"sources":["../../src/hono/rate-limit.ts"],"sourcesContent":["import type { Context, MiddlewareHandler } from 'hono';\nimport { Details } from '../error/detail';\nimport { Status } from '../error/status';\nimport { geolocation } from './geolocation';\n\nexport interface KV {\n  setItem(key: string, value: string, expiresIn?: number): Promise<void>;\n  getItem(key: string): Promise<string | null>;\n  removeItem(key: string): Promise<void>;\n}\n\ninterface TokenBucketState {\n  tokens: number;\n  lastRefillTime: number;\n}\n\nexport interface RateLimitOptions {\n  kv: KV;\n  rate: number;\n  capacity: number;\n  requested?: number;\n  keyPrefix?: string;\n  expiresIn?: number;\n  getIdentifier?: (c: Context) => string | undefined;\n  onRateLimited?: (c: Context, retryAfter: number) => Response | Promise<Response>;\n  skip?: (c: Context) => boolean | Promise<boolean>;\n}\n\nexport interface RateLimitResult {\n  allowed: boolean;\n  remaining: number;\n  limit: number;\n  reset: number;\n  retryAfter?: number;\n}\n\nfunction calculateTokens(\n  state: TokenBucketState | null,\n  capacity: number,\n  rate: number,\n  now: number\n): { tokens: number; lastRefillTime: number } {\n  if (!state) {\n    return { tokens: capacity, lastRefillTime: now };\n  }\n\n  const elapsed = (now - state.lastRefillTime) / 1000;\n  const tokensToAdd = elapsed * rate;\n  const newTokens = Math.min(capacity, state.tokens + tokensToAdd);\n\n  return {\n    tokens: newTokens,\n    lastRefillTime: now,\n  };\n}\n\nfunction calculateResetTime(currentTokens: number, capacity: number, rate: number): number {\n  if (currentTokens >= capacity) return 0;\n  return Math.ceil((capacity - currentTokens) / rate);\n}\n\nfunction calculateRetryAfter(currentTokens: number, requested: number, rate: number): number {\n  if (currentTokens >= requested) return 0;\n  return Math.ceil((requested - currentTokens) / rate);\n}\n\nexport async function checkRateLimit(\n  kv: KV,\n  identifier: string,\n  options: {\n    rate: number;\n    capacity: number;\n    requested?: number;\n    keyPrefix?: string;\n    expiresIn?: number;\n  }\n): Promise<RateLimitResult> {\n  const {\n    rate,\n    capacity,\n    requested = 1,\n    keyPrefix = 'rate-limit:',\n    expiresIn = Math.max(3600, Math.ceil((capacity / rate) * 2)),\n  } = options;\n\n  const key = `${keyPrefix}${identifier}`;\n  const now = Date.now();\n\n  const stateStr = await kv.getItem(key);\n  const state: TokenBucketState | null = stateStr ? JSON.parse(stateStr) : null;\n\n  const { tokens: currentTokens, lastRefillTime } = calculateTokens(state, capacity, rate, now);\n\n  const allowed = currentTokens >= requested;\n  const newTokens = allowed ? currentTokens - requested : currentTokens;\n\n  const newState: TokenBucketState = { tokens: newTokens, lastRefillTime };\n  await kv.setItem(key, JSON.stringify(newState), expiresIn);\n\n  const result: RateLimitResult = {\n    allowed,\n    remaining: Math.floor(Math.max(0, newTokens)),\n    limit: capacity,\n    reset: calculateResetTime(newTokens, capacity, rate),\n  };\n\n  if (!allowed) {\n    result.retryAfter = calculateRetryAfter(currentTokens, requested, rate);\n  }\n\n  return result;\n}\n\nexport function rateLimit(options: RateLimitOptions): MiddlewareHandler {\n  const {\n    kv,\n    rate,\n    capacity,\n    requested = 1,\n    keyPrefix = 'rate-limit:',\n    expiresIn = Math.max(3600, Math.ceil((capacity / rate) * 2)),\n    getIdentifier,\n    onRateLimited,\n    skip,\n  } = options;\n\n  return async (c, next) => {\n    if (skip && (await skip(c))) {\n      return next();\n    }\n\n    const identifier = getIdentifier ? getIdentifier(c) : geolocation(c).ip_address;\n    if (!identifier) return next();\n\n    const result = await checkRateLimit(kv, identifier, {\n      rate,\n      capacity,\n      requested,\n      keyPrefix,\n      expiresIn,\n    });\n\n    c.header('X-RateLimit-Limit', String(result.limit));\n    c.header('X-RateLimit-Remaining', String(result.remaining));\n    c.header('X-RateLimit-Reset', String(result.reset));\n\n    if (!result.allowed) {\n      c.header('Retry-After', String(result.retryAfter ?? 1));\n      if (onRateLimited) return onRateLimited(c, result.retryAfter ?? 1);\n\n      const details = Details.new()\n        .errorInfo({ reason: 'RATE_LIMIT_EXCEEDED' })\n        .retryInfo({ retryDelay: result.retryAfter ?? 1 });\n\n      const message = 'Rate limit exceeded. Please try again later.';\n      return Status.resourceExhausted(message).response(details);\n    }\n\n    return next();\n  };\n}\n"],"mappings":"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","names":[]}