Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/elysia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@
"elysia": ">=1.0.0"
},
"packageManager": "pnpm@10.15.0"
}
}
2 changes: 1 addition & 1 deletion packages/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@
"express": ">=4.0.0"
},
"packageManager": "pnpm@10.15.0"
}
}
2 changes: 1 addition & 1 deletion packages/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,4 @@
"hono": ">=4.0.0"
},
"packageManager": "pnpm@10.15.0"
}
}
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@
"react-dom": ">=19.0.0"
},
"packageManager": "pnpm@10.15.0"
}
}
2 changes: 2 additions & 0 deletions packages/rate-limiter/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- Added support for the `sliding-window` rate-limiter algorithm via the `createSlidingWindowAlgorithm` function. The function can be used standalone or integrated with the centralized `createRateLimiter` function. [#186](https://github.com/aura-stack-ts/auth/pull/186)

- Added support for the `leaky-bucket` rate-limiter algorithm via the `createLeakyBucketAlgorithm` function. The function can be used standalone or integrated with the centralized `createRateLimiter` function. [#186](https://github.com/aura-stack-ts/auth/pull/186)

- Added support for the `fixed-window` rate-limiter algorithm via the `createFixedWindowAlgorithm` function. The function can be used standalone or integrated with the centralized `createRateLimiter` function. [#185](https://github.com/aura-stack-ts/auth/pull/185)
Expand Down
1 change: 1 addition & 0 deletions packages/rate-limiter/src/algorithms/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { createTokenBucketAlgorithm } from "@/algorithms/token-bucket.ts"
export { createFixedWindowAlgorithm } from "@/algorithms/fixed-window.ts"
export { createLeakyBucketAlgorithm } from "@/algorithms/leaky-bucket.ts"
export { createSlidingWindowAlgorithm } from "@/algorithms/sliding-window.ts"
83 changes: 83 additions & 0 deletions packages/rate-limiter/src/algorithms/sliding-window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { toContent } from "@/utils.ts"
import { createMemoryStorage } from "@/memory.ts"
import type { RateLimiterAlgorithm, RateLimitResult, SlidingWindowRule } from "@/types.ts"

/**
* Sliding Window Counter
*
* Interpolates between the previous fixed window's count and the current one
* to approximate a true rolling window without storing per-request timestamps.
*
* Formula:
* weight = elapsed time in current window / windowMs
* estimate = previousCount * (1 - weight) + currentCount
*
* O(1) storage — two counters per key — with smooth, burst-free throttling.
*
* Recommended for: security-sensitive endpoints (signIn, signOut, verifyToken).
*/
export const createSlidingWindowAlgorithm = <RequestInit = Request>(
rule: SlidingWindowRule<RequestInit>
): RateLimiterAlgorithm<RequestInit> => {
const { limit, windowMs, storage = createMemoryStorage() } = rule

const getBoundary = (now: number) => Math.floor(now / windowMs) * windowMs

const windowKeys = (baseKey: string, now: number) => {
const currentBoundary = getBoundary(now)
return {
current: `${baseKey}:sw:${currentBoundary}`,
previous: `${baseKey}:sw:${currentBoundary - windowMs}`,
}
}

const estimate = async (baseKey: string, now: number): Promise<{ count: number; resetAt: number }> => {
const boundary = getBoundary(now)
const weight = (now - boundary) / windowMs
const { current, previous } = windowKeys(baseKey, now)

const [currentEntry, previousEntry] = await Promise.all([storage.get(current), storage.get(previous)])

const count = (previousEntry?.value ?? 0) * (1 - weight) + (currentEntry?.value ?? 0)
return { count, resetAt: boundary + windowMs }
}

const check = async (request: RequestInit): Promise<RateLimitResult> => {
const now = Date.now()
const boundary = getBoundary(now)
const reset = boundary + windowMs
const key = rule.keyGenerator(request)
const { current, previous } = windowKeys(key, now)

const newCount = await storage.increment(current, windowMs * 2)
const weight = (now - boundary) / windowMs
const previousEntry = await storage.get(previous)
const estimatedCount = (previousEntry?.value ?? 0) * (1 - weight) + newCount
const ok = estimatedCount <= limit

return toContent({
ok,
limit,
remaining: Math.max(0, Math.floor(limit - estimatedCount)),
resetAt: reset,
retryAfter: ok ? 0 : reset - now,
})
}

const peek = async (request: RequestInit): Promise<RateLimitResult> => {
const now = Date.now()
const key = rule.keyGenerator(request)
const { count, resetAt } = await estimate(key, now)
const ok = count <= limit

return toContent({
ok,
limit,
remaining: Math.max(0, Math.floor(limit - count)),
resetAt,
retryAfter: ok ? 0 : resetAt - now,
})
}

return { check, peek }
}
12 changes: 11 additions & 1 deletion packages/rate-limiter/src/rate-limiter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createMemoryStorage } from "@/memory.ts"
import { createTokenBucketAlgorithm, createFixedWindowAlgorithm, createLeakyBucketAlgorithm } from "@/algorithms/index.ts"
import {
createTokenBucketAlgorithm,
createFixedWindowAlgorithm,
createLeakyBucketAlgorithm,
createSlidingWindowAlgorithm,
} from "@/algorithms/index.ts"
import type { InferRules, RateLimiter, RateLimiterAlgorithm, RateLimiterConfig, RateLimiterRule } from "@/types.ts"

/**
Expand All @@ -14,6 +19,8 @@ const buildAlgorithm = <RequestInit = Request>(rule: RateLimiterRule<RequestInit
return createFixedWindowAlgorithm(rule)
case "leaky-bucket":
return createLeakyBucketAlgorithm(rule)
case "sliding-window":
return createSlidingWindowAlgorithm(rule)
default: {
throw new Error(`[rate-limiter] Unknown algorithm: "${String((rule as { algorithm?: string }).algorithm)}"`)
}
Expand All @@ -28,6 +35,9 @@ const resetKeys = (rule: RateLimiterRule, key: string): string[] => {
return [`${key}:fw`]
case "leaky-bucket":
return [`${key}:lb:tokens`, `${key}:lb:lastLeak`]
case "sliding-window":
const boundary = Math.floor(Date.now() / rule.windowMs) * rule.windowMs
return [`${key}:sw:${boundary}`, `${key}:sw:${boundary - rule.windowMs}`]
}
}

Expand Down
15 changes: 12 additions & 3 deletions packages/rate-limiter/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface RateLimiterAlgorithm<RequestInit = Request> {
check(request: RequestInit): Promise<RateLimitResult>
}

export type AlgorithmType = "token-bucket" | "fixed-window" | "leaky-bucket"
export type AlgorithmType = "token-bucket" | "fixed-window" | "leaky-bucket" | "sliding-window"

interface BaseRule<RequestInit = Request> {
algorithm: AlgorithmType
Expand All @@ -83,15 +83,15 @@ export type TokenBucketRule<RequestInit = Request> = BaseRule<RequestInit> & {
refillRate: number
}

export type FixedWindowRule<RequestInit = Request> = BaseRule<RequestInit> & {
export interface FixedWindowRule<RequestInit = Request> extends BaseRule<RequestInit> {
algorithm: "fixed-window"
/** Maximum requests allowed per window. */
limit: number
/** Window duration in milliseconds. Hard resets at each boundary. */
windowMs: number
}

export type LeakyBucketRule<RequestInit = Request> = BaseRule<RequestInit> & {
export interface LeakyBucketRule<RequestInit = Request> extends BaseRule<RequestInit> {
algorithm: "leaky-bucket"
/**
* The maximum queue size (burst capacity). When the bucket is full,
Expand All @@ -106,10 +106,19 @@ export type LeakyBucketRule<RequestInit = Request> = BaseRule<RequestInit> & {
leakRatePerMs: number
}

export interface SlidingWindowRule<RequestInit = Request> extends BaseRule<RequestInit> {
algorithm: "sliding-window"
/** Maximum requests allowed per window. */
limit: number
/** Window duration in milliseconds. */
windowMs: number
}

export type RateLimiterRule<RequestInit = Request> =
| TokenBucketRule<RequestInit>
| FixedWindowRule<RequestInit>
| LeakyBucketRule<RequestInit>
| SlidingWindowRule<RequestInit>

export interface RateLimiterConfig<Rules extends Record<string, RateLimiterRule>> {
storage?: RateLimiterStorage
Expand Down
Loading
Loading