12 KiB
12 KiB
Tasks: enterprise-rate-limiter
Change ID: enterprise-rate-limiter
Package: @suaveplan/rate-limiter
Target path: universal/auth/rate-limiter
Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/logger
1. Scaffold
- Create directory
universal/auth/rate-limiter/withsrc/,tests/,src/__benchmarks__/, and all feature subdirectories per the layout inproposal.md. - Write
package.jsonat version0.1.0with:"type": "module","sideEffects": falseexportsmap with top-level"."plus subpaths:"./core/token-bucket/token-bucket","./core/sliding-window/sliding-window","./core/fixed-window/fixed-window","./core/rate-limiter/rate-limiter","./storage/storage-adapter/storage-adapter","./storage/memory-storage/memory-storage","./middleware/rate-limit-middleware/rate-limit-middleware","./schemas/rate-limiter.schemas","./types/rate-limiter.types"dependencies:@suaveplan/error workspace:^,@suaveplan/types workspace:^,@suaveplan/events workspace:^,@suaveplan/logger workspace:^,zod catalog:forms,@opentelemetry/api catalog:observability,tslib catalog:builddevDependencies:vitest catalog:testing,@vitest/coverage-istanbul catalog:testing,fast-check catalog:testing,mitata catalog:testing,@suaveplan/testing workspace:^
- Write
tsconfig.jsonextending@suaveplan/typescript-config/library.json. - Write
vitest.config.tsusingcreateTestConfig()from@suaveplan/testing/config(pool:forks). - Add
lint,typecheck,test,test:coverage,build,docs:check,benchscripts topackage.json.
2. Types and errors
2a. Types (src/types/rate-limiter.types.ts + .md)
- Define all public interfaces from
proposal.md:RateLimitResult,RateLimitConfig,RateLimitRule,RateLimitCondition,RateLimitContext,RateLimiterOptions,TokenBucketState,SlidingWindowState,CounterState,RateLimitState,RateLimitAlgorithm,TenantRateLimitConfig,RateLimiterEvents,RateLimitedPayload,RateLimitResetPayload,RateLimiterInstance,RateLimiterMiddlewareOptions,MiddlewareFn. - Write
rate-limiter.types.md(≥200 words, API reference for every type).
2b. Error codes (src/error-codes.ts)
- Define
RateLimiterErrorCodeunion — four codes:RATE_STORAGE_ERROR,RATE_INVALID_CONFIG,RATE_KEY_MISSING,RATE_STATE_CORRUPT. - Define
RATE_ERROR_METADATAwithhttpStatus,severity,retryable,userMessageKey,category,descriptionper code. - Export
registerRateLimiterErrorCodes()as idempotent eager side-effect. - Export
RateLimiterError extends SuaveplanError<RateLimiterErrorCode>.
2c. Zod schemas (src/schemas/rate-limiter.schemas.ts + .md)
- Write Zod schemas:
RateLimitConfigSchema,RateLimitRuleSchema,RateLimitContextSchema,RateLimiterOptionsSchema,TokenBucketOptionsSchema,SlidingWindowOptionsSchema,FixedWindowOptionsSchema,MemoryStorageOptionsSchema. - Write
rate-limiter.schemas.md. - Write
rate-limiter.schemas.test.ts— parse valid/invalid for each schema.
3. Storage adapter (src/storage/storage-adapter/)
- Define
RateLimitStorageinterface:get,set,delete,clear. - Write
storage-adapter.types.ts. - Write
storage-adapter.md.
4. MemoryStorage (src/storage/memory-storage/)
- Implement
MemoryStorageusing aMap<string, { state: RateLimitState; expiresAt: number }>:get(key)— returnsnullif missing or expired (lazy check via injectableclock).set(key, state, ttl)— stores withexpiresAt = clock.now() + ttl.delete(key)— removes entry.clear()— wipes map.maxSizeenforcement: onset, if map exceedsmaxSize, evict the entry with the soonestexpiresAt.
- Write
memory-storage.test.ts— get/set round-trip, TTL expiry with FakeClock, maxSize eviction, delete, clear. Eachithas{ timeout: 5000 }. - Write
memory-storage.md.
5. TokenBucket (src/core/token-bucket/)
- Implement
TokenBucket(options):- Internal state:
tokens: number,lastRefill: number(epoch ms viaclock). consume(tokens?):- First, compute elapsed ms since
lastRefillviaclock.now(); addelapsed * refillRatetokens (capped atcapacity). - If
tokensavailable, decrement and return{ allowed: true, remaining: ..., limit: capacity, resetIn: timeUntilNextToken }. - If insufficient, return
{ allowed: false, remaining: 0, limit: capacity, resetIn: ..., retryAfter: ... }.
- First, compute elapsed ms since
refill(tokens?)— explicitly adds tokens (used in tests and manual top-ups).- Getters:
available,capacity. - Validate options with
TokenBucketOptionsSchema; throwRateLimiterErrorwithRATE_INVALID_CONFIGon invalid options.
- Internal state:
- Write
token-bucket.test.ts— consume within capacity, consume exhausted, automatic refill via FakeClock, explicit refill,availablegetter, capacity cap, invalid options error. Eachithas{ timeout: 5000 }. - Write
token-bucket.md.
6. SlidingWindow (src/core/sliding-window/)
- Implement
SlidingWindow(options):- Internal state:
Map<string, number[]>— per-key array of request timestamps. check(key):- Prune timestamps older than
clock.now() - windowMs. - If
remaining > 0, push current timestamp, return{ allowed: true, remaining: ... }. - Otherwise return
{ allowed: false, remaining: 0, retryAfter: oldest_ts + windowMs - now }.
- Prune timestamps older than
reset(key)— clears the key's timestamp array.- Validate options with
SlidingWindowOptionsSchema.
- Internal state:
- Write
sliding-window.test.ts— allow up to limit, deny on exceed, timestamps pruned outside window (FakeClock), reset, multiple keys isolated. Eachithas{ timeout: 5000 }. - Write
sliding-window.md.
7. FixedWindow (src/core/fixed-window/)
- Implement
FixedWindow(options):- Internal state:
Map<string, { count: number; windowStart: number }>. check(key):- Compute current window start =
Math.floor(clock.now() / windowMs) * windowMs. - If stored
windowStart !== currentWindowStart, reset count to 0 and updatewindowStart. - If
count < limit, increment and return{ allowed: true, remaining: limit - count - 1, resetIn: currentWindowStart + windowMs - clock.now() }. - Otherwise return
{ allowed: false, remaining: 0, retryAfter: currentWindowStart + windowMs - clock.now() }.
- Compute current window start =
reset(key)— removes the key's state.- Validate options with
FixedWindowOptionsSchema.
- Internal state:
- Write
fixed-window.test.ts— allow within limit, deny on exceed, window rolls over (FakeClock), reset, multiple keys. Eachithas{ timeout: 5000 }. - Write
fixed-window.md.
8. RateLimiter orchestrator (src/core/rate-limiter/)
- Implement
createRateLimiter(options)returningRateLimiterInstance:check(context):- Evaluate
options.rulesin descending priority order; first matching rule (conditions met) wins. - If a rule has
bypass: true, return{ allowed: true, remaining: Infinity, limit: Infinity, resetIn: 0 }. - Apply
tenantMultipliersto effective limit ifcontext.tenantIdmatches. - Build a composite storage key:
${keyPrefix}:${context.key}. - Delegate to the selected algorithm (default:
sliding-window; configurable per-rule viarule.algorithm). - If
!result.allowed, emitrate:limit:exceededonbus. - Instrument with
rate_limiter.checkspan,rate_limiter.checks_totalcounter,rate_limiter.check_duration_mshistogram.
- Evaluate
reset(key)— deletes all storage keys matching the composite key pattern; emitsrate:limit:reset.getState(key)— delegates tostorage.get.
- Write
rate-limiter.test.ts(≤500 lines) — default algorithm, rule priority selection, bypass rule, tenant multiplier, bus event on denied, storage delegation, reset, getState.{ timeout: 5000 }/{ timeout: 30000 }. - Write
rate-limiter.md.
9. Middleware helpers (src/middleware/rate-limit-middleware/)
- Implement
createRateLimitChecker(options)— returns(context) => Promise<RateLimitResult>closure over aRateLimiterInstance. - Implement
createIpRateLimiter(config, storage?)— creates a limiter keyed on IP with default sliding-window. - Implement
createRouteRateLimiter(routes, storage?)— creates a limiter with per-routeRateLimitConfig, keyed onpath. - Implement
getRateLimitResult(context, storage, config)— one-shot check without creating a fullRateLimiterInstance. - All functions return
MiddlewareFn(framework-agnostic); includecreateHonoAdapterbridge. - Write
rate-limit-middleware.test.ts— each helper: allowed path, denied path, IP extraction, route matching, Hono adapter. Eachithas{ timeout: 5000 }. - Write
rate-limit-middleware.md.
10. OTel instrumentation (cross-cutting review)
- Confirm all spans and counters from
proposal.md §4are wired. - Add
tests/otel.test.ts— in-memory exporter; assertrate_limiter.checks_totalincrements,rate_limiter.exceeded_totalincrements on denied checks,rate_limiter.check_duration_msrecords.
11. Property-based tests (tests/property.test.ts)
Use fast-check. All test timeouts { timeout: 30000 }.
- TokenBucket
availableinvariant: after any sequence ofconsumeandrefillcalls,availableis always in[0, capacity]. - SlidingWindow monotonicity: within a window,
remainingnever increases without aresetor window rollover. - Tenant multiplier correctness: effective limit =
config.limit * multiplier;remainingat the effective limit is 0. - Rule priority: when two rules both match a context, the higher-priority rule's config is applied.
- Storage round-trip:
MemoryStorage.set(k, s, ttl)thenMemoryStorage.get(k)returns a structurally equal state (within TTL).
12. Benchmarks (src/__benchmarks__/rate-limiter.bench.ts)
Use mitata.
token_bucket_consume: 1 000 000consume()calls (pure in-memory, no storage).sliding_window_check: 100 000check()calls with a 1 000-timestamp window.fixed_window_check: 100 000check()calls.rate_limiter_memory_storage: 100 000check()calls viacreateRateLimiter+MemoryStorage.memory_storage_get_set: 1 000 000 round-trips.- Document placeholder numbers in
README.mdunder## Benchmarks.
13. Docs
Per-file .md (all ≥200 words):
src/types/rate-limiter.types.mdsrc/storage/storage-adapter/storage-adapter.mdsrc/storage/memory-storage/memory-storage.mdsrc/core/token-bucket/token-bucket.mdsrc/core/sliding-window/sliding-window.mdsrc/core/fixed-window/fixed-window.mdsrc/core/rate-limiter/rate-limiter.mdsrc/middleware/rate-limit-middleware/rate-limit-middleware.mdsrc/schemas/rate-limiter.schemas.md
Root README:
- Write
universal/auth/rate-limiter/README.md: purpose, install, quick-start (token-bucket + sliding-window comparison), multi-tenant guide, middleware adapter guide, modules table, benchmarks placeholder.
14. Verify and publish
- Run
bunx turbo lint --filter=@suaveplan/rate-limiter— zero errors. - Run
bunx turbo typecheck --filter=@suaveplan/rate-limiter— zero errors. - Run
bunx turbo test --filter=@suaveplan/rate-limiter— all tests pass. - Run
bunx turbo test:coverage --filter=@suaveplan/rate-limiter— 100/100/100/100. - Run
bun run docs:check --filter=@suaveplan/rate-limiter— zero errors. - Confirm no
TODO,FIXME,@deprecated, untypedany,ts-ignore, or.skipinsrc/ortests/. - Confirm
"sideEffects": falseinpackage.json. - Run
bun scripts/publish.ts --package @suaveplan/rate-limiter. - Update
CLAUDE.mdpackage listing to include@suaveplan/rate-limiter. - Archive: move
openspec/changes/enterprise-rate-limiter/toopenspec/archive/2026-04-24-enterprise-rate-limiter/and fold spec delta intoopenspec/specs/rate-limiter/spec.md.