{"_id":"@broberg/apikey","_rev":"5-0fc1e699e4d1f6752d8640bbd03420e1","name":"@broberg/apikey","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@broberg/apikey","version":"0.1.0","keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"license":"MIT","_id":"@broberg/apikey@0.1.0","maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"homepage":"https://github.com/broberg-ai/components#readme","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"dist":{"shasum":"7199c77fcc15153561308e4bde91cc69f236db54","tarball":"https://registry.npmjs.org/@broberg/apikey/-/apikey-0.1.0.tgz","fileCount":28,"integrity":"sha512-Zp525Z7CDCiV+yk5beyQx98I2/+CLLVFnwEGaNDJK6/jeo3hqSTCxJs0xS0OaO7vSIO+3FbdNXHr91ghOF6uEw==","signatures":[{"sig":"MEUCIQCpTAV5DyBYaL1ta/0j9m5JnCMB4vGcnHk8YulChfuD7QIgaqXFBAwMl4afnDU6H2NbuiQM+ptlInQsa2SK8Pk81s0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":112251},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./hono":{"types":"./dist/hono.d.ts","import":"./dist/hono.js","require":"./dist/hono.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./authorize":{"types":"./dist/authorize.d.ts","import":"./dist/authorize.js","require":"./dist/authorize.cjs"}},"gitHead":"70717d4c6d07d3b0466e6a962d4763b1e06de8fe","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"cbroberg","email":"cb@webhouse.dk"},"repository":{"url":"git+https://github.com/broberg-ai/components.git","type":"git","directory":"packages/apikey"},"_npmVersion":"11.10.1","description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","directories":{},"sideEffects":false,"_nodeVersion":"25.7.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","tsup":"^8.3.0","vitest":"^2.1.0","typescript":"^5.6.0","@types/node":"^22.7.0"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/apikey_0.1.0_1781554024583_0.3294819507758471","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@broberg/apikey","version":"0.1.1","keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"license":"MIT","_id":"@broberg/apikey@0.1.1","maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"homepage":"https://github.com/broberg-ai/components#readme","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"dist":{"shasum":"38c8661105cf04d91faf41ed033fb63773e7618c","tarball":"https://registry.npmjs.org/@broberg/apikey/-/apikey-0.1.1.tgz","fileCount":28,"integrity":"sha512-GMULGHFtVbFS5jCayCIERGVHSxpFkVYmEyNEt36K12LJx4jcVALjaAs1BYV8krpsgF7j43+eXGKdyFJTrg03DA==","signatures":[{"sig":"MEYCIQDNhKva0dP30aPvd2kE6yjhK+iUFiWtQ/t4zx0jZF7ElgIhAJPFKtjTjrELrkhjolxxahui4DIY3UdY8C4Ii0FVm4ZG","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@broberg%2fapikey@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":115032},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./hono":{"types":"./dist/hono.d.ts","import":"./dist/hono.js","require":"./dist/hono.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./authorize":{"types":"./dist/authorize.d.ts","import":"./dist/authorize.js","require":"./dist/authorize.cjs"}},"gitHead":"55d7046f7fbbd7e828ca05865c07986e0abb10b5","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9665eaf9-8376-455e-8589-b24935a81d41"}},"repository":{"url":"git+https://github.com/broberg-ai/components.git","type":"git","directory":"packages/apikey"},"_npmVersion":"11.17.0","description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","directories":{},"sideEffects":false,"_nodeVersion":"22.22.3","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","tsup":"^8.3.0","vitest":"^2.1.0","typescript":"^5.6.0","@types/node":"^22.7.0"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/apikey_0.1.1_1781557306538_0.7142613085770781","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@broberg/apikey","version":"0.2.0","keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"license":"MIT","_id":"@broberg/apikey@0.2.0","maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"homepage":"https://github.com/broberg-ai/components#readme","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"dist":{"shasum":"ea1e27330b5800ae12618a978657d5a78d31bc79","tarball":"https://registry.npmjs.org/@broberg/apikey/-/apikey-0.2.0.tgz","fileCount":28,"integrity":"sha512-KVVGD4HrFNpglBh1bngSf5PeC6KcM8XvbAKVUPNxkDRYeYT0zMfq3v1XH8OPNTKWRDnkx7aIUHew+T1KMwKCGg==","signatures":[{"sig":"MEYCIQDMjhwZtnqyqzvrtCo91LpHmSnzR9QfzeDmh6oHqMelaQIhAKs2boS6rL69kKHETICXa57TAooCv0AP+iUoJptis7u2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@broberg%2fapikey@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":123368},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./hono":{"types":"./dist/hono.d.ts","import":"./dist/hono.js","require":"./dist/hono.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./authorize":{"types":"./dist/authorize.d.ts","import":"./dist/authorize.js","require":"./dist/authorize.cjs"}},"gitHead":"eaecbbc98a576d74ab3ebfddafd6679a420ae16e","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9665eaf9-8376-455e-8589-b24935a81d41"}},"repository":{"url":"git+https://github.com/broberg-ai/components.git","type":"git","directory":"packages/apikey"},"_npmVersion":"11.5.1","description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","directories":{},"sideEffects":false,"_nodeVersion":"22.23.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","tsup":"^8.3.0","vitest":"^2.1.0","typescript":"^5.6.0","@types/node":"^22.7.0"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/apikey_0.2.0_1786089886736_0.4434506616602736","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@broberg/apikey","version":"0.3.0","keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"license":"MIT","_id":"@broberg/apikey@0.3.0","maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"homepage":"https://github.com/broberg-ai/components#readme","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"dist":{"shasum":"83e16b8d7c600fcbf694c6143735844c5b184fa5","tarball":"https://registry.npmjs.org/@broberg/apikey/-/apikey-0.3.0.tgz","fileCount":30,"integrity":"sha512-45m6EkYt6cKU0WkPNey8YwZB8arkrHMpvSELJ7jeUBseJHSkyqSd692ApukBaPv6aL7QSqhcRBc+425VeTGseQ==","signatures":[{"sig":"MEUCIQCBRPez/yVRAJ0OiVJ0rFBuvpBgalpLqfvlbXwplx7LPAIgQoZUOFgirdjuzanngJo41loU9Ag8fflApJ+O2XQvrqE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@broberg%2fapikey@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":145732},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./hono":{"types":"./dist/hono.d.ts","import":"./dist/hono.js","require":"./dist/hono.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./authorize":{"types":"./dist/authorize.d.ts","import":"./dist/authorize.js","require":"./dist/authorize.cjs"}},"gitHead":"48a9d0c61449a5862c98967b29afa31dc7e316ff","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9665eaf9-8376-455e-8589-b24935a81d41"}},"repository":{"url":"git+https://github.com/broberg-ai/components.git","type":"git","directory":"packages/apikey"},"_npmVersion":"11.5.1","description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"hono":"^4.6.0","tsup":"^8.3.0","vitest":"^2.1.0","typescript":"^5.6.0","@types/node":"^22.7.0"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/apikey_0.3.0_1786090885618_0.2824065453580311","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@broberg/apikey","version":"0.3.1","description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","type":"module","license":"MIT","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./authorize":{"types":"./dist/authorize.d.ts","import":"./dist/authorize.js","require":"./dist/authorize.cjs"},"./next":{"types":"./dist/next.d.ts","import":"./dist/next.js","require":"./dist/next.cjs"},"./hono":{"types":"./dist/hono.d.ts","import":"./dist/hono.js","require":"./dist/hono.cjs"}},"scripts":{"build":"tsup","test":"vitest run","typecheck":"tsc --noEmit"},"peerDependencies":{"hono":">=4"},"peerDependenciesMeta":{"hono":{"optional":true}},"devDependencies":{"@types/node":"^22.7.0","hono":"^4.6.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"repository":{"type":"git","url":"git+https://github.com/broberg-ai/components.git","directory":"packages/apikey"},"publishConfig":{"access":"public"},"_id":"@broberg/apikey@0.3.1","gitHead":"c849a16caae21fdfea2b4193f99b0582031ea893","bugs":{"url":"https://github.com/broberg-ai/components/issues"},"homepage":"https://github.com/broberg-ai/components#readme","_nodeVersion":"22.23.1","_npmVersion":"11.5.1","dist":{"integrity":"sha512-jKKi4BJ7hefMxKr3RyLITshgyYkYeuspRA27qCwZir/i/dljuRxnLVfXv6VoPL+QDkoMaqI8QnEV+oJuexLWpg==","shasum":"9d74a1a3d7cd21400433458a25d9bd0eb08c48d2","tarball":"https://registry.npmjs.org/@broberg/apikey/-/apikey-0.3.1.tgz","fileCount":30,"unpackedSize":146457,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@broberg%2fapikey@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCICpOcr6toejhuINRoBkEPM7RigINsLEb8OslSLzZJTyKAiBgcHfNmhT2EJ7mWjAMwZBWFiYPafmYLhC51j+0YR0jSw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9665eaf9-8376-455e-8589-b24935a81d41"}},"directories":{},"maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/apikey_0.3.1_1786091870361_0.01701132451475651"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-15T20:07:04.394Z","modified":"2026-08-07T08:37:50.858Z","0.1.0":"2026-06-15T20:07:04.708Z","0.1.1":"2026-06-15T21:01:46.670Z","0.2.0":"2026-08-07T08:04:46.921Z","0.3.0":"2026-08-07T08:21:25.763Z","0.3.1":"2026-08-07T08:37:50.514Z"},"bugs":{"url":"https://github.com/broberg-ai/components/issues"},"license":"MIT","homepage":"https://github.com/broberg-ai/components#readme","keywords":["api-key","apikey","rate-limit","rate-limiting","authorization","rbac","cidr","timing-safe","multi-tenant","hono","next","broberg"],"repository":{"type":"git","url":"git+https://github.com/broberg-ai/components.git","directory":"packages/apikey"},"description":"Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter ","maintainers":[{"name":"cbroberg","email":"cb@webhouse.dk"}],"readme":"# @broberg/apikey\n\nFramework-agnostic **inbound API-key primitives** for the broberg.ai fleet. It owns the dangerous-to-get-wrong bits — minting, constant-time verification, rate-limiting, and a Cloudflare-style authorization cascade — and leaves **storage, tenancy, and request-context resolution to you**. Bring your own `lookup`.\n\nDesigned from a 9-repo fleet survey (trail · cardmem · cms · upmetrics · vn): the package never forces hashing, a tenancy model, a fixed prefix, or a rate-limit backend.\n\n```bash\nnpm i @broberg/apikey      # exact-pin for prod-auth deps\n```\n\n## Core (`@broberg/apikey`)\n\n```ts\nimport { generateKey, hashKey, verifyKey, makeKeyPreview, hasScope } from \"@broberg/apikey\";\n\nconst raw = generateKey(\"trail\");           // \"trail_<64 hex>\"  — show ONCE\nconst stored = hashKey(raw);                // sha256 — store this (hash-at-rest)\nconst preview = makeKeyPreview(raw);         // \"trail_0a1b2c3d\" — display/grep anchor\n\n// On each request — YOU do the DB read; the package does the constant-time compare:\nverifyKey(presented, stored);               // hashed (default): timingSafeEqual(sha256(presented), stored)\nverifyKey(presented, stored, { hashed: false }); // plaintext-revealable (upmetrics-style)\n\nhasScope([\"content:*\"], [\"content:write\"]); // true — exact / `*` / `area:*`\n```\n\n`timingSafeEqual(a, b)` is exported too — the length-checked constant-time compare that replaces unsafe `a !== b` token checks.\n\n## Rate limit — pluggable store\n\nIn-memory by default (single-machine). For a **stateless multi-machine** fleet, pass a shared store so the window doesn't leak per machine:\n\n```ts\nimport { SlidingWindowRateLimiter, type RateLimitStore } from \"@broberg/apikey\";\n\nconst limiter = new SlidingWindowRateLimiter({ windowMs: 60_000, max: 100 });\nconst { allowed, remaining, resetAt } = await limiter.check(clientKey);\n\n// One limiter, per-key caps — pass a per-check `max` override (v0.1.1):\nawait limiter.check(clientKey, { max: keyRecord.rateLimitPerHour });\n\n// Shared backend (Turso/Redis): implement one method.\nconst turso: RateLimitStore = {\n  async hit(key, now, windowMs) { /* … */ return { count, oldest }; },\n};\nnew SlidingWindowRateLimiter({ windowMs: 60_000, max: 100, store: turso });\n```\n\n## Authorization cascade (`@broberg/apikey/authorize`)\n\nThe optional rich tier — permission × resource-filter × CIDR × TTL (modelled on cms F134). Simple adopters skip this and use `hasScope`.\n\n```ts\nimport { evaluateToken, type TokenGrant } from \"@broberg/apikey/authorize\";\n\nconst grant: TokenGrant = {\n  permissions: [\"deploy:trigger\"],\n  resources: [{ scope: \"site\", effect: \"include\", targets: [\"fysiodk\"] }],\n  ipFilters: [{ mode: \"in\", cidrs: [\"203.0.113.0/24\"] }],\n  notBefore: Date.parse(\"2026-01-01\"),\n  notAfter: Date.parse(\"2027-01-01\"),\n};\n\nconst decision = evaluateToken(grant, {\n  permission: \"deploy:trigger\",\n  resource: { scope: \"site\", target: \"fysiodk\" },\n  ip: \"203.0.113.5\",\n});\n// → { allowed: true } | { allowed: false, reason: \"expired\" | \"permission_denied\" | \"resource_denied\" | \"ip_denied\" }\n```\n\nCascade order: TTL → permission → resource (**exclude wins**) → CIDR (IPv4 + IPv6, zero-dep). A scope with no filter is unconstrained.\n\n### Tenant selector (trail's selector-not-grant)\n\n```ts\nimport { selectTenant, TenantAccessError } from \"@broberg/apikey/authorize\";\n\n// A `spansAll` key lets the owner pick any tenant they belong to via a header.\n// A non-member slug is a HARD refuse — never a silent fall-back to home.\ntry {\n  const tenant = selectTenant({ requestedSlug, homeTenant, spansAll: true, isMember });\n} catch (e) {\n  if (e instanceof TenantAccessError) return new Response(null, { status: 401 });\n}\n```\n\n## Adapters\n\n```ts\n// Stack B — Hono\nimport { honoApiKeyMiddleware, honoRateLimit } from \"@broberg/apikey/hono\";\napp.use(\"/api/*\", honoApiKeyMiddleware({ lookup, authorize }));   // 401/403; c.get(\"apiKey\")\napp.use(\"/api/*\", honoRateLimit(limiter));                         // 429 + Retry-After\n\n// Stack A — Next.js (Web-standard Request/Response, edge-safe, no `next` dep)\nimport { withApiKeyAuth, nextRateLimit } from \"@broberg/apikey/next\";\nexport const POST = withApiKeyAuth(async (req, record) => Response.json({ ok: true }), { lookup });\n```\n\n### Your error contract, not ours (v0.2.0)\n\nThe default bodies are `{ error: \"missing_api_key\" }` etc. — a **string**. If your\nAPI answers something else (e.g. `{ error: { code, message } }`, which a machine\nclient can switch on), render it yourself instead of rewriting the middleware:\n\n```ts\napp.use(\"/api/*\", honoApiKeyMiddleware({\n  lookup,\n  authorize,\n  onUnauthorized: (c, reason) =>            // reason: \"missing\" | \"invalid\"\n    c.json({ error: { code: `unauthorized_${reason}` } }, 401),\n  onForbidden: (c, record) =>\n    c.json({ error: { code: \"forbidden\", at: record.id } }, 403),\n}));\n\napp.use(\"/api/*\", honoRateLimit(limiter, keyFn, {\n  onLimited: (c, r) => c.json({ error: { code: \"rate_limited\", retryAt: r.resetAt } }, 429),\n}));\n```\n\nThe middleware still decides **what** happened; the hook only decides how it is\nwritten down. Status codes and the `X-RateLimit-Remaining` / `Retry-After`\nheaders are unchanged. Omit the hooks and the responses are byte-identical to\nv0.1.1.\n\n`reason` is worth acting on: **401** means *\"I don't know who you are\"* (no\ntoken, unknown token, just-revoked) and is fixed by fetching a token; **403**\nmeans *\"I know exactly who you are, and this isn't yours\"* and is fixed by\nrequesting a different role. The response is the only thing that tells the\ncaller which.\n\n> **Rate-limit bucket key.** With no `keyFn`, the limiter keys on\n> `x-forwarded-for`, falling back to `\"unknown\"` when the header is absent. On a\n> loopback-bound service with no proxy in front that means **every caller shares\n> one bucket** — pass a `keyFn` that keys on the token/record id instead.\n\n### Different caps per route, one limiter (v0.3.0)\n\n`keyFn` may return `{ key, max }` instead of a bare string, so a single limiter\nenforces a different cap per route — you do not need a limiter instance per cap:\n\n```ts\napp.use(\"/admin/*\", honoRateLimit(limiter, (c) => ({ key: tokenId(c), max: 600 })));\napp.use(\"/write/*\", honoRateLimit(limiter, (c) => ({ key: tokenId(c), max: 300 })));\n```\n\nBoth adapters set **`X-RateLimit-Limit`** as well as `X-RateLimit-Remaining` —\n`Limit` reports the *effective* cap for that request (the per-route `max` when\none applies), because a remaining count you can't interpret is not much use.\n`RateLimitResult.limit` carries the same number if you drive the limiter directly.\n\n### What reaches your handler (v0.3.0)\n\nBy default the **whole** looked-up record is what `c.set(contextKey, …)` stores\n(Hono) and what arrives as the handler's 2nd argument (Next) — including\nwhatever your storage row carries, such as a `hash` column. A hash is not a\nusable credential, so this is not a credential leak, but it is more than a\nhandler needs, and one `c.json(caller)` later it becomes a response body.\n\nPass `project` to choose the caller shape yourself:\n\n```ts\nhonoApiKeyMiddleware({ lookup, project: (r) => ({ id: r.id, role: r.role }) });\nwithApiKeyAuth(handler, { lookup, project: (r) => ({ id: r.id, role: r.role }) });\n```\n\n> **Adapter asymmetry, stated plainly:** the `onUnauthorized` / `onForbidden` /\n> `onLimited` hooks above exist on the **Hono** adapter only. The Next adapter\n> still emits the built-in string bodies. Filing an issue is the right move if\n> you need them there.\n\n> **If you render your own 429 body, quote the same `result`.** The headers come\n> from the middleware and the body comes from your `onLimited`, so they are\n> produced in two places. A caller told *\"1 per 60s\"* in the body while\n> `X-RateLimit-Limit` says `600` cannot tell which to believe. Use\n> `result.limit` / `result.resetAt` in the body rather than a constant you hold\n> separately — the package cannot assert this for you, so it is worth a test on\n> your side.\n\n`Retry-After` is floored at `1` (since v0.3.1): with a shared remote store whose\nround-trip outlasts the remaining window, the raw computation could go negative\nand emit `Retry-After: 0` — telling a client to retry *immediately* while still\nlimited.\n\n`lookup(presented) => record | null` is yours: hash + DB/filesystem read, your storage, your tenancy. The package never sees your store.\n\n## Boundaries (what it deliberately does NOT do)\n\n- **No storage** — no DB/CRUD layer; you own the schema (Drizzle / libSQL / JSON).\n- **No request→tenant resolution** — that's your proxy/router; feed the result into `selectTenant`.\n- **No bundled Redis/Turso** — ships the `RateLimitStore` interface + in-memory only.\n- **Core crypto is Node/Bun** (`node:crypto`). At the edge, hash via Web Crypto inside your `lookup`; the adapters themselves are edge-safe.\n\nMIT · part of the [broberg.ai shared inventory](https://discovery.broberg.ai).\n","readmeFilename":"README.md"}