{"version":3,"file":"rate-limit.mjs","names":[],"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":";;;;AAoCA,SAAS,gBACP,OACA,UACA,MACA,KAC4C;CAC5C,IAAI,CAAC,OACH,OAAO;EAAE,QAAQ;EAAU,gBAAgB;CAAI;CAIjD,MAAM,eADW,MAAM,MAAM,kBAAkB,MACjB;CAG9B,OAAO;EACL,QAHgB,KAAK,IAAI,UAAU,MAAM,SAAS,WAGlC;EAChB,gBAAgB;CAClB;AACF;AAEA,SAAS,mBAAmB,eAAuB,UAAkB,MAAsB;CACzF,IAAI,iBAAiB,UAAU,OAAO;CACtC,OAAO,KAAK,MAAM,WAAW,iBAAiB,IAAI;AACpD;AAEA,SAAS,oBAAoB,eAAuB,WAAmB,MAAsB;CAC3F,IAAI,iBAAiB,WAAW,OAAO;CACvC,OAAO,KAAK,MAAM,YAAY,iBAAiB,IAAI;AACrD;AAEA,eAAsB,eACpB,IACA,YACA,SAO0B;CAC1B,MAAM,EACJ,MACA,UACA,YAAY,GACZ,YAAY,eACZ,YAAY,KAAK,IAAI,MAAM,KAAK,KAAM,WAAW,OAAQ,CAAC,CAAC,MACzD;CAEJ,MAAM,MAAM,GAAG,YAAY;CAC3B,MAAM,MAAM,KAAK,IAAI;CAErB,MAAM,WAAW,MAAM,GAAG,QAAQ,GAAG;CAGrC,MAAM,EAAE,QAAQ,eAAe,mBAAmB,gBAFX,WAAW,KAAK,MAAM,QAAQ,IAAI,MAEA,UAAU,MAAM,GAAG;CAE5F,MAAM,UAAU,iBAAiB;CACjC,MAAM,YAAY,UAAU,gBAAgB,YAAY;CAExD,MAAM,WAA6B;EAAE,QAAQ;EAAW;CAAe;CACvE,MAAM,GAAG,QAAQ,KAAK,KAAK,UAAU,QAAQ,GAAG,SAAS;CAEzD,MAAM,SAA0B;EAC9B;EACA,WAAW,KAAK,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;EAC5C,OAAO;EACP,OAAO,mBAAmB,WAAW,UAAU,IAAI;CACrD;CAEA,IAAI,CAAC,SACH,OAAO,aAAa,oBAAoB,eAAe,WAAW,IAAI;CAGxE,OAAO;AACT;AAEA,SAAgB,UAAU,SAA8C;CACtE,MAAM,EACJ,IACA,MACA,UACA,YAAY,GACZ,YAAY,eACZ,YAAY,KAAK,IAAI,MAAM,KAAK,KAAM,WAAW,OAAQ,CAAC,CAAC,GAC3D,eACA,eACA,SACE;CAEJ,OAAO,OAAO,GAAG,SAAS;EACxB,IAAI,QAAS,MAAM,KAAK,CAAC,GACvB,OAAO,KAAK;EAGd,MAAM,aAAa,gBAAgB,cAAc,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC;EACrE,IAAI,CAAC,YAAY,OAAO,KAAK;EAE7B,MAAM,SAAS,MAAM,eAAe,IAAI,YAAY;GAClD;GACA;GACA;GACA;GACA;EACF,CAAC;EAED,EAAE,OAAO,qBAAqB,OAAO,OAAO,KAAK,CAAC;EAClD,EAAE,OAAO,yBAAyB,OAAO,OAAO,SAAS,CAAC;EAC1D,EAAE,OAAO,qBAAqB,OAAO,OAAO,KAAK,CAAC;EAElD,IAAI,CAAC,OAAO,SAAS;GACnB,EAAE,OAAO,eAAe,OAAO,OAAO,cAAc,CAAC,CAAC;GACtD,IAAI,eAAe,OAAO,cAAc,GAAG,OAAO,cAAc,CAAC;GAEjE,MAAM,UAAU,QAAQ,IAAI,CAAC,CAC1B,UAAU,EAAE,QAAQ,sBAAsB,CAAC,CAAC,CAC5C,UAAU,EAAE,YAAY,OAAO,cAAc,EAAE,CAAC;GAGnD,OAAO,OAAO,kBAAkB,8CAAO,CAAC,CAAC,SAAS,OAAO;EAC3D;EAEA,OAAO,KAAK;CACd;AACF"}