{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hono-rate-limit",
  "type": "registry:block",
  "title": "tneemo/hono-rate-limit",
  "description": "In-memory rate limiting middleware for Hono with standard RateLimit and Retry-After headers. Per IP by default, or any key you choose.",
  "author": "tneemo",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/middleware/rate-limit.ts",
      "type": "registry:file",
      "target": "~/src/middleware/rate-limit.ts",
      "content": "import type { Context, MiddlewareHandler } from \"hono\";\n\nexport interface RateLimitOptions {\n  /** Window length in milliseconds (default 60 000). */\n  windowMs?: number;\n  /** Requests allowed per key per window (default 60). */\n  limit?: number;\n  /** How to identify a client. Defaults to the client IP from common proxy headers. */\n  key?: (c: Context) => string;\n}\n\ninterface Bucket {\n  count: number;\n  resetAt: number;\n}\n\nfunction clientIp(c: Context): string {\n  const forwarded = c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim();\n  return c.req.header(\"cf-connecting-ip\") ?? c.req.header(\"x-real-ip\") ?? forwarded ?? \"unknown\";\n}\n\n/**\n * Fixed-window rate limiter kept in process memory. Responds 429 with Retry-After\n * when a key goes over the limit. Use one instance per route group.\n * For several server instances, put a shared store (e.g. Redis) behind the same interface.\n */\nexport function rateLimit(options: RateLimitOptions = {}): MiddlewareHandler {\n  const windowMs = options.windowMs ?? 60_000;\n  const limit = options.limit ?? 60;\n  const keyOf = options.key ?? clientIp;\n  const buckets = new Map<string, Bucket>();\n  let lastSweep = Date.now();\n\n  return async (c, next) => {\n    const now = Date.now();\n    if (now - lastSweep > windowMs) {\n      for (const [k, b] of buckets) if (b.resetAt <= now) buckets.delete(k);\n      lastSweep = now;\n    }\n    const key = keyOf(c);\n    let bucket = buckets.get(key);\n    if (!bucket || bucket.resetAt <= now) {\n      bucket = { count: 0, resetAt: now + windowMs };\n      buckets.set(key, bucket);\n    }\n    bucket.count++;\n    const remaining = Math.max(0, limit - bucket.count);\n    const resetSeconds = Math.ceil((bucket.resetAt - now) / 1000);\n    c.header(\"RateLimit-Limit\", String(limit));\n    c.header(\"RateLimit-Remaining\", String(remaining));\n    c.header(\"RateLimit-Reset\", String(resetSeconds));\n    if (bucket.count > limit) {\n      c.header(\"Retry-After\", String(resetSeconds));\n      return c.json({ error: \"Too many requests\" }, 429);\n    }\n    await next();\n  };\n}\n"
    },
    {
      "path": "test/rate-limit.test.ts",
      "type": "registry:file",
      "target": "~/test/rate-limit.test.ts",
      "content": "import { Hono } from \"hono\";\nimport { describe, expect, it } from \"vitest\";\nimport { rateLimit } from \"../src/middleware/rate-limit\";\n\ndescribe(\"rateLimit\", () => {\n  it(\"allows up to the limit, then responds 429 with Retry-After\", async () => {\n    const app = new Hono();\n    app.use(rateLimit({ limit: 2, windowMs: 60_000 }));\n    app.get(\"/\", (c) => c.text(\"ok\"));\n    const hit = () => app.request(\"/\", { headers: { \"x-forwarded-for\": \"203.0.113.7\" } });\n\n    expect((await hit()).status).toBe(200);\n    const second = await hit();\n    expect(second.status).toBe(200);\n    expect(second.headers.get(\"RateLimit-Remaining\")).toBe(\"0\");\n    const third = await hit();\n    expect(third.status).toBe(429);\n    expect(Number(third.headers.get(\"Retry-After\"))).toBeGreaterThan(0);\n  });\n\n  it(\"tracks keys separately\", async () => {\n    const app = new Hono();\n    app.use(rateLimit({ limit: 1, key: (c) => c.req.header(\"x-api-key\") ?? \"anon\" }));\n    app.get(\"/\", (c) => c.text(\"ok\"));\n    expect((await app.request(\"/\", { headers: { \"x-api-key\": \"a\" } })).status).toBe(200);\n    expect((await app.request(\"/\", { headers: { \"x-api-key\": \"b\" } })).status).toBe(200);\n    expect((await app.request(\"/\", { headers: { \"x-api-key\": \"a\" } })).status).toBe(429);\n  });\n});\n"
    }
  ],
  "envVars": {},
  "docs": "Apply the middleware where routes are mounted (`src/app.ts` in `tneemo/hono-api`):\n\n```ts\nimport { rateLimit } from \"./middleware/rate-limit\";\n\n// Everything: 100 requests per minute per IP.\napp.use(rateLimit({ limit: 100, windowMs: 60_000 }));\n\n// Stricter limit for login and signup (before app.route(\"/auth\", ...)).\napp.use(\"/auth/*\", rateLimit({ limit: 10, windowMs: 15 * 60_000 }));\n```\n\nRegister middleware before the routes it should protect. To limit per signed-in user instead of per IP, pass `key: (c) => String(c.get(\"user\")?.id ?? \"anon\")` and apply it after the auth middleware.\n",
  "categories": [
    "rate-limit",
    "ratelimit",
    "throttle",
    "throttling",
    "middleware",
    "abuse",
    "brute-force",
    "security"
  ],
  "meta": {
    "pkg": "https://pkg.tneemo.com/tneemo/hono-rate-limit",
    "version": "1.0.0",
    "license": "MIT"
  }
}