@@ -11,7 +11,7 @@ A TypeScript library for secure API key management with cryptographic hashing, e
1111- ** Secure by Default** : SHA-256/SHA-512 hashing with optional salt and timing-safe comparison
1212- ** Smart Key Detection** : Automatically extracts keys from ` Authorization ` , ` x-api-key ` , or custom headers
1313- ** Built-in Caching** : Optional in-memory or Redis caching for validated keys
14- - ** Rate Limiting** : Built-in rate limiting with fixed window algorithm
14+ - ** Rate Limiting** : Optional automatic rate limiting on verify calls with atomic counters
1515- ** Flexible Storage** : Memory, Redis, and Drizzle ORM adapters included
1616- ** Scope-based Permissions** : Fine-grained access control
1717- ** Key Management** : Enable/disable, rotate, and soft-revoke keys with audit trails
@@ -81,6 +81,12 @@ const keys = createKeys({
8181 // Usage tracking
8282 autoTrackUsage: true , // Automatically update lastUsedAt on verify
8383
84+ // Rate limiting (opt-in, requires cache)
85+ rateLimit: {
86+ maxRequests: 100 ,
87+ windowMs: 60_000 , // 1 minute window
88+ },
89+
8490 // Header detection
8591 headerNames: [' x-api-key' , ' authorization' ],
8692 extractBearer: true ,
@@ -142,8 +148,13 @@ const result = await keys.verify(headers, {
142148// Check result
143149if (result .valid ) {
144150 console .log (result .record )
151+ // If rate limiting is enabled, result.rateLimit will include rate limit info
152+ if (result .rateLimit ) {
153+ console .log (` ${result .rateLimit .remaining } requests remaining ` )
154+ }
145155} else {
146- console .log (result .error ) // 'Missing API key' | 'Invalid API key' | 'API key has expired' | 'API key is disabled' | 'API key has been revoked'
156+ console .log (result .error ) // 'Missing API key' | 'Invalid API key' | 'API key has expired' | 'API key is disabled' | 'API key has been revoked' | 'Rate limit exceeded'
157+ console .log (result .errorCode ) // 'MISSING_KEY' | 'INVALID_KEY' | 'EXPIRED' | 'DISABLED' | 'REVOKED' | 'RATE_LIMIT_EXCEEDED'
147158}
148159```
149160
@@ -177,98 +188,80 @@ Protect your API from abuse with built-in rate limiting. Uses the same cache inf
177188
178189** Note:** Cache must be enabled to use rate limiting.
179190
191+ #### Automatic Rate Limiting
192+
193+ Enable rate limiting globally on all verify calls by adding the ` rateLimit ` config option:
194+
180195``` typescript
181196const keys = createKeys ({
182197 cache: true , // Required for rate limiting
183- cacheTtl: 60 ,
184- })
185-
186- // Create a rate limiter
187- const rateLimiter = keys .createRateLimiter ({
188- maxRequests: 100 ,
189- windowMs: 60_000 , // 1 minute window
190- keyPrefix: ' ratelimit' , // optional, defaults to 'ratelimit'
198+ rateLimit: {
199+ maxRequests: 100 ,
200+ windowMs: 60_000 , // 1 minute window
201+ },
191202})
192203
193- // Check rate limit
204+ // Rate limiting happens automatically on verify()
194205const result = await keys .verify (headers )
195- if (! result .valid ) {
196- return { error: result .error , status: 401 }
197- }
198206
199- const rateLimit = await rateLimiter .check (result .record )
200- if (! rateLimit .allowed ) {
201- return {
202- error: ' Rate limit exceeded' ,
203- status: 429 ,
204- resetAt: rateLimit .resetAt ,
205- resetMs: rateLimit .resetMs ,
207+ if (! result .valid ) {
208+ if (result .errorCode === ' RATE_LIMIT_EXCEEDED' ) {
209+ return {
210+ error: ' Too many requests' ,
211+ status: 429 ,
212+ resetAt: result .rateLimit .resetAt ,
213+ resetMs: result .rateLimit .resetMs ,
214+ }
206215 }
216+ return { error: result .error , status: 401 }
207217}
208218
209- // Access rate limit info
219+ // Rate limit info is included in successful responses
210220console .log ({
211- current: rateLimit .current , // Current request count
212- limit: rateLimit .limit , // Max requests allowed
213- remaining: rateLimit .remaining , // Remaining requests
214- resetMs: rateLimit .resetMs , // Time until reset (ms)
215- resetAt: rateLimit .resetAt , // ISO timestamp when window resets
221+ current: result . rateLimit .current , // Current request count
222+ limit: result . rateLimit .limit , // Max requests allowed
223+ remaining: result . rateLimit .remaining , // Remaining requests
224+ resetMs: result . rateLimit .resetMs , // Time until reset (ms)
225+ resetAt: result . rateLimit .resetAt , // ISO timestamp when window resets
216226})
217227```
218228
219- ** Dry-run checks** (check without incrementing):
220- ``` typescript
221- const rateLimit = await rateLimiter .check (record , { increment: false })
222- ```
223-
224- ** Custom identifiers** (e.g., per-owner limits instead of per-key):
225- ``` typescript
226- const rateLimit = await rateLimiter .check (record , {
227- identifier: record .metadata .ownerId , // Rate limit by user, not by key
228- })
229- ```
230-
231- ** Manual reset** :
232- ``` typescript
233- await rateLimiter .reset (record )
234- ```
235-
236- ** Get current count without incrementing** :
237- ``` typescript
238- const count = await rateLimiter .getCurrentCount (record )
239- ```
240-
241229** Complete middleware example with rate limit headers** :
242230``` typescript
243231app .use (' /api/*' , async (c , next ) => {
244232 const result = await keys .verify (c .req .raw .headers )
245233
246234 if (! result .valid ) {
235+ if (result .errorCode === ' RATE_LIMIT_EXCEEDED' ) {
236+ c .header (' Retry-After' , Math .ceil (result .rateLimit .resetMs / 1000 ).toString ())
237+ c .header (' X-RateLimit-Limit' , result .rateLimit .limit .toString ())
238+ c .header (' X-RateLimit-Remaining' , ' 0' )
239+ c .header (' X-RateLimit-Reset' , result .rateLimit .resetAt )
240+ return c .json ({ error: ' Too many requests' }, 429 )
241+ }
247242 return c .json ({ error: result .error }, 401 )
248243 }
249244
250- const rateLimit = await rateLimiter .check (result .record )
251-
252- // Set rate limit headers (standard practice)
253- c .header (' X-RateLimit-Limit' , rateLimit .limit .toString ())
254- c .header (' X-RateLimit-Remaining' , rateLimit .remaining .toString ())
255- c .header (' X-RateLimit-Reset' , rateLimit .resetAt )
256-
257- if (! rateLimit .allowed ) {
258- c .header (' Retry-After' , Math .ceil (rateLimit .resetMs / 1000 ).toString ())
259- return c .json ({
260- error: ' Too many requests' ,
261- resetAt: rateLimit .resetAt ,
262- }, 429 )
263- }
245+ // Set rate limit headers on successful requests
246+ c .header (' X-RateLimit-Limit' , result .rateLimit .limit .toString ())
247+ c .header (' X-RateLimit-Remaining' , result .rateLimit .remaining .toString ())
248+ c .header (' X-RateLimit-Reset' , result .rateLimit .resetAt )
264249
265250 c .set (' apiKey' , result .record )
266251 await next ()
267252})
268253```
269254
270- ** Different limits for different endpoints** :
255+ #### Manual Rate Limiting (Advanced)
256+
257+ For custom rate limiting scenarios (e.g., different limits per endpoint), create rate limiters manually:
258+
271259``` typescript
260+ const keys = createKeys ({
261+ cache: true , // Required for rate limiting
262+ })
263+
264+ // Create custom rate limiters
272265const strictLimiter = keys .createRateLimiter ({
273266 maxRequests: 10 ,
274267 windowMs: 60_000 , // 10 requests per minute
@@ -281,17 +274,55 @@ const normalLimiter = keys.createRateLimiter({
281274
282275// Use strict limiter for sensitive endpoints
283276app .post (' /api/sensitive' , async (c ) => {
284- const rateLimit = await strictLimiter .check (c .get (' apiKey' ))
277+ const result = await keys .verify (c .req .raw .headers )
278+ if (! result .valid ) {
279+ return c .json ({ error: result .error }, 401 )
280+ }
281+
282+ const rateLimit = await strictLimiter .check (result .record )
283+ if (! rateLimit .allowed ) {
284+ return c .json ({ error: ' Too many requests' }, 429 )
285+ }
285286 // ...
286287})
287288
288289// Use normal limiter for regular endpoints
289290app .get (' /api/data' , async (c ) => {
290- const rateLimit = await normalLimiter .check (c .get (' apiKey' ))
291+ const result = await keys .verify (c .req .raw .headers )
292+ if (! result .valid ) {
293+ return c .json ({ error: result .error }, 401 )
294+ }
295+
296+ const rateLimit = await normalLimiter .check (result .record )
297+ if (! rateLimit .allowed ) {
298+ return c .json ({ error: ' Too many requests' }, 429 )
299+ }
291300 // ...
292301})
293302```
294303
304+ ** Dry-run checks** (check without incrementing):
305+ ``` typescript
306+ const rateLimit = await rateLimiter .check (record , { increment: false })
307+ ```
308+
309+ ** Custom identifiers** (e.g., per-owner limits instead of per-key):
310+ ``` typescript
311+ const rateLimit = await rateLimiter .check (record , {
312+ identifier: record .metadata .ownerId , // Rate limit by user, not by key
313+ })
314+ ```
315+
316+ ** Manual reset** :
317+ ``` typescript
318+ await rateLimiter .reset (record )
319+ ```
320+
321+ ** Get current count without incrementing** :
322+ ``` typescript
323+ const count = await rateLimiter .getCurrentCount (record )
324+ ```
325+
295326### Helper Methods
296327
297328``` typescript
@@ -501,6 +532,14 @@ interface VerifyResult {
501532 valid: boolean
502533 record? : ApiKeyRecord
503534 error? : string
535+ errorCode? : ApiKeyErrorCode
536+ rateLimit? : {
537+ current: number
538+ limit: number
539+ remaining: number
540+ resetMs: number
541+ resetAt: string
542+ }
504543}
505544
506545interface RateLimitConfig {
0 commit comments