{"_id":"@diabolicallabs/rate-limiter","_rev":"4-8d363e22a58d45b633ee993c0684cd97","name":"@diabolicallabs/rate-limiter","dist-tags":{"latest":"1.0.1"},"versions":{"0.0.1":{"name":"@diabolicallabs/rate-limiter","version":"0.0.1","author":{"url":"https://deeismail.com","name":"Diana Ismail","email":"diana@deeismail.com"},"license":"UNLICENSED","_id":"@diabolicallabs/rate-limiter@0.0.1","maintainers":[{"name":"shackled78","email":"shackled78@gmail.com"}],"homepage":"https://github.com/mannism/dlabs-toolkit#readme","bugs":{"url":"https://github.com/mannism/dlabs-toolkit/issues"},"dist":{"shasum":"3e1c97dd40ab48372688be6c28e25a7ac7a9032e","tarball":"https://registry.npmjs.org/@diabolicallabs/rate-limiter/-/rate-limiter-0.0.1.tgz","fileCount":5,"integrity":"sha512-ug2T62xTWzfUhBj2uBhz61wjP3iU9JGJFHiLnz3liZuQYwtT99jpiWP9x4L5+qSqC+yCKuPk0xQAfMKXD+JFKg==","signatures":[{"sig":"MEYCIQDXvAAq+RmAIQgLyrIUqTsm8QbjDXiAucYuyeHjCWB1AAIhALFgeVfiE+XaJ+khq4OkG2eT4L28YttGsXRr7MPlv6JO","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":8788},"main":"./dist/index.js","type":"module","_from":"file:diabolicallabs-rate-limiter-0.0.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"lint":"biome check ./src && eslint ./src","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"shackled78","email":"shackled78@gmail.com"},"_resolved":"/tmp/404dae90a5eba8bb301da03d863febf1/diabolicallabs-rate-limiter-0.0.1.tgz","publisher":"Diabolical Labs","_integrity":"sha512-ug2T62xTWzfUhBj2uBhz61wjP3iU9JGJFHiLnz3liZuQYwtT99jpiWP9x4L5+qSqC+yCKuPk0xQAfMKXD+JFKg==","repository":{"url":"git+https://github.com/mannism/dlabs-toolkit.git","type":"git","directory":"packages/rate-limiter"},"_npmVersion":"10.8.2","description":"Redis sliding-window rate limiter. Sorted-set pipeline, fail-closed on Redis outage. © Diabolical Labs","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^4.1.5","ioredis":"^5.4.1","@vitest/coverage-v8":"^4.1.5"},"peerDependencies":{"ioredis":">=5"},"_npmOperationalInternal":{"tmp":"tmp/rate-limiter_0.0.1_1778050952453_0.5306319304629992","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@diabolicallabs/rate-limiter","version":"0.0.2","author":{"url":"https://deeismail.com","name":"Diana Ismail","email":"diana@deeismail.com"},"license":"MIT","_id":"@diabolicallabs/rate-limiter@0.0.2","maintainers":[{"name":"shackled78","email":"shackled78@gmail.com"}],"homepage":"https://github.com/mannism/dlabs-toolkit#readme","bugs":{"url":"https://github.com/mannism/dlabs-toolkit/issues"},"dist":{"shasum":"051f8cc53e7f8262266c140b9cfe4e6de089d87c","tarball":"https://registry.npmjs.org/@diabolicallabs/rate-limiter/-/rate-limiter-0.0.2.tgz","fileCount":6,"integrity":"sha512-p0dM0VfTQ+mTppIWJ9r+1GtMxHri+uAmmVe2uyZAsxcyIB1hB0rUqnY73zVz1Z3CfHNZRhJ+M9vsAAtxntYD8A==","signatures":[{"sig":"MEQCIEOQqGTIx7S/5Mmcu/bSoak9QrIqp+J0/cdqo6HqK5TBAiA/xMwJc1jAFXkFSUYMODCtVrBS7nVhTvc9fgv9jSCTxg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":9877},"main":"./dist/index.js","type":"module","_from":"file:diabolicallabs-rate-limiter-0.0.2.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"lint":"biome check ./src && eslint ./src","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"shackled78","email":"shackled78@gmail.com"},"_resolved":"/tmp/8c54924f66e2b2cb5b30407e51787b97/diabolicallabs-rate-limiter-0.0.2.tgz","publisher":"Diabolical Labs","_integrity":"sha512-p0dM0VfTQ+mTppIWJ9r+1GtMxHri+uAmmVe2uyZAsxcyIB1hB0rUqnY73zVz1Z3CfHNZRhJ+M9vsAAtxntYD8A==","repository":{"url":"git+https://github.com/mannism/dlabs-toolkit.git","type":"git","directory":"packages/rate-limiter"},"_npmVersion":"10.8.2","description":"Redis sliding-window rate limiter. Sorted-set pipeline, fail-closed on Redis outage. © Diabolical Labs","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^4.1.5","ioredis":"^5.4.1","@vitest/coverage-v8":"^4.1.5"},"peerDependencies":{"ioredis":">=5"},"_npmOperationalInternal":{"tmp":"tmp/rate-limiter_0.0.2_1778059931935_0.95218755017188","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@diabolicallabs/rate-limiter","version":"1.0.0","author":{"url":"https://deeismail.com","name":"Diana Ismail","email":"diana@deeismail.com"},"license":"MIT","_id":"@diabolicallabs/rate-limiter@1.0.0","maintainers":[{"name":"shackled78","email":"shackled78@gmail.com"}],"homepage":"https://github.com/mannism/dlabs-toolkit#readme","bugs":{"url":"https://github.com/mannism/dlabs-toolkit/issues"},"dist":{"shasum":"77e20e4203f9543f7b690f5763eee65b57d12681","tarball":"https://registry.npmjs.org/@diabolicallabs/rate-limiter/-/rate-limiter-1.0.0.tgz","fileCount":6,"integrity":"sha512-vkZa0Z3uFCLXmiN1DS+18OZ1T97dg37fdBDwG36DfKEGCmXCMssUI2e4SDy+ztchyx5Kp09+GhnhU1uEWzCakQ==","signatures":[{"sig":"MEUCIQCE9XLf6a3d0sSBJ1kXSI5aizTpX7vWc0CUCCodMq3EbQIgKKaGlBAVbAX6LH5yDYr/TxLnvlut2oXHmxC+HzQt/YE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42604},"main":"./dist/index.js","type":"module","_from":"file:diabolicallabs-rate-limiter-1.0.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"lint":"biome check ./src && eslint ./src","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"shackled78","email":"shackled78@gmail.com"},"_resolved":"/tmp/d566760d6fc5a0a2767c51dfdcf317b8/diabolicallabs-rate-limiter-1.0.0.tgz","publisher":"Diabolical Labs","_integrity":"sha512-vkZa0Z3uFCLXmiN1DS+18OZ1T97dg37fdBDwG36DfKEGCmXCMssUI2e4SDy+ztchyx5Kp09+GhnhU1uEWzCakQ==","repository":{"url":"git+https://github.com/mannism/dlabs-toolkit.git","type":"git","directory":"packages/rate-limiter"},"_npmVersion":"10.8.2","description":"Redis sliding-window rate limiter. Sorted-set pipeline, fail-closed on Redis outage. © Diabolical Labs","directories":{},"_nodeVersion":"20.20.2","dependencies":{"uuid":"^14.0.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^4.1.6","ioredis":"^5.4.1","@types/node":"^25.9.0","@types/uuid":"^11.0.0","@vitest/coverage-v8":"^4.1.6"},"peerDependencies":{"ioredis":">=5"},"_npmOperationalInternal":{"tmp":"tmp/rate-limiter_1.0.0_1779586990665_0.5422149736334521","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@diabolicallabs/rate-limiter","version":"1.0.1","description":"Redis sliding-window rate limiter. Sorted-set pipeline, fail-closed on Redis outage. © Diabolical Labs","author":{"name":"Diana Ismail","email":"diana@deeismail.com","url":"https://deeismail.com"},"publisher":"Diabolical Labs","license":"MIT","type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts"}},"main":"./dist/index.js","types":"./dist/index.d.ts","repository":{"type":"git","url":"git+https://github.com/mannism/dlabs-toolkit.git","directory":"packages/rate-limiter"},"homepage":"https://github.com/mannism/dlabs-toolkit#readme","bugs":{"url":"https://github.com/mannism/dlabs-toolkit/issues"},"engines":{"node":">=20"},"peerDependencies":{"ioredis":">=5"},"devDependencies":{"@types/node":"^26.1.2","@types/uuid":"^11.0.0","@vitest/coverage-v8":"^4.1.10","ioredis":"^5.11.1","tsup":"^8.3.5","vitest":"^4.1.10"},"dependencies":{"uuid":"^14.0.1"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","lint":"biome check ./src && eslint ./src","test":"vitest run","test:watch":"vitest"},"_id":"@diabolicallabs/rate-limiter@1.0.1","_integrity":"sha512-wconpQ52TowZb1+5nxdzWw/RF039e/yUb2OYylJAFBni7N3kJsGnnCdSmFpGfzQcze1XY70nkEB8yupIWb5NKA==","_resolved":"/tmp/2b7719b52a11d81a86a102e11b785ea1/diabolicallabs-rate-limiter-1.0.1.tgz","_from":"file:diabolicallabs-rate-limiter-1.0.1.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-wconpQ52TowZb1+5nxdzWw/RF039e/yUb2OYylJAFBni7N3kJsGnnCdSmFpGfzQcze1XY70nkEB8yupIWb5NKA==","shasum":"8d8a9c71365b808d621c31a26f17ea0061fd41a9","tarball":"https://registry.npmjs.org/@diabolicallabs/rate-limiter/-/rate-limiter-1.0.1.tgz","fileCount":6,"unpackedSize":47649,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFv9Hla38MkKVa5lOn1cLo4QBsyIWTOcI8Bvvm4UkCUWAiEAu4r3cnOO5O5csClROQmUgJOi8skjegaaKEyVkISLoeE="}]},"_npmUser":{"name":"shackled78","email":"shackled78@gmail.com"},"directories":{},"maintainers":[{"name":"shackled78","email":"shackled78@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rate-limiter_1.0.1_1786869319603_0.30901309115949416"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-06T07:02:32.343Z","modified":"2026-08-16T08:35:19.952Z","0.0.1":"2026-05-06T07:02:32.584Z","0.0.2":"2026-05-06T09:32:12.071Z","1.0.0":"2026-05-24T01:43:10.814Z","1.0.1":"2026-08-16T08:35:19.755Z"},"bugs":{"url":"https://github.com/mannism/dlabs-toolkit/issues"},"author":{"name":"Diana Ismail","email":"diana@deeismail.com","url":"https://deeismail.com"},"license":"MIT","homepage":"https://github.com/mannism/dlabs-toolkit#readme","repository":{"type":"git","url":"git+https://github.com/mannism/dlabs-toolkit.git","directory":"packages/rate-limiter"},"description":"Redis sliding-window rate limiter. Sorted-set pipeline, fail-closed on Redis outage. © Diabolical Labs","maintainers":[{"name":"shackled78","email":"shackled78@gmail.com"}],"readme":"# @diabolicallabs/rate-limiter\n\nRedis sliding-window rate limiter. Lua EVAL/EVALSHA atomicity, fail-closed on Redis outage. © Diabolical Labs\n\n## Install\n\n```bash\npnpm add @diabolicallabs/rate-limiter\n# ioredis is a peerDependency — install separately if you don't have it\npnpm add ioredis\n```\n\n## Usage\n\n```typescript\nimport Redis from 'ioredis';\nimport { createRateLimiter, fromIoredis, RateLimitError } from '@diabolicallabs/rate-limiter';\n\n// Provide your existing ioredis singleton — the limiter does not manage connections\nconst redis = new Redis(process.env['REDIS_URL']!);\n\nconst limiter = createRateLimiter({\n  redis: fromIoredis(redis), // ioredis has no scriptLoad() — fromIoredis() adapts it\n  windowMs: 60_000,     // 1-minute sliding window\n  maxRequests: 100,     // 100 requests per window\n  keyPrefix: 'rl:api:', // optional, default: 'rl:'\n});\n\n// Non-throwing check — returns RateLimitResult\nconst result = await limiter.check('user:abc123');\nif (!result.allowed) {\n  return Response.json(\n    { error: 'Rate limit exceeded' },\n    { status: 429, headers: { 'Retry-After': String(Math.ceil(result.resetMs / 1000)) } }\n  );\n}\n\n// Throwing enforce — useful in middleware\ntry {\n  await limiter.enforce('ip:1.2.3.4');\n} catch (err) {\n  if (err instanceof RateLimitError) {\n    // err.kind: 'exceeded' | 'unavailable'\n    // err.remaining, err.resetMs\n  }\n}\n```\n\n## Multi-tier usage\n\nInstantiate one limiter per tier:\n\n```typescript\nconst freeLimiter = createRateLimiter({ redis, windowMs: 60_000, maxRequests: 10 });\nconst paidLimiter = createRateLimiter({ redis, windowMs: 60_000, maxRequests: 1_000 });\n```\n\n## Fail-closed behavior\n\nIf Redis is unreachable, the limiter **rejects the request** by default. This is the correct behavior for any public-facing API rate limiter.\n\nOverride with `onRedisError: 'open'` to allow requests through on Redis failure (and log `RL_REDIS_ERROR`):\n\n```typescript\nconst limiter = createRateLimiter({\n  redis,\n  windowMs: 60_000,\n  maxRequests: 100,\n  onRedisError: 'open', // allow through on Redis failure\n});\n```\n\n## API\n\n### `createRateLimiter(config): RateLimiter`\n\n| Config field | Type | Default | Description |\n|---|---|---|---|\n| `redis` | `RedisExecutor` | required | Any object with `eval`, `evalsha`, `scriptLoad` — wrap a raw ioredis client with `fromIoredis()` |\n| `windowMs` | `number` | required | Sliding window duration in milliseconds |\n| `maxRequests` | `number` | required | Max requests allowed within the window |\n| `keyPrefix` | `string` | `'rl:'` | Redis key prefix |\n| `onRedisError` | `'closed' \\| 'open'` | `'closed'` | Fail policy on Redis error |\n| `logger` | `Logger` | stdout JSON | Pluggable structured logger |\n\n### `RateLimiter` interface\n\n| Method | Return | Description |\n|---|---|---|\n| `check(key)` | `Promise<RateLimitResult>` | Returns result. Never throws. |\n| `enforce(key)` | `Promise<void>` | Throws `RateLimitError` if not allowed. |\n\n### `RateLimitResult`\n\n```typescript\ninterface RateLimitResult {\n  allowed: boolean;\n  remaining: number; // requests remaining in the current window\n  resetMs: number;   // ms until the window resets\n}\n```\n\n### `RateLimitError`\n\n```typescript\nclass RateLimitError extends Error {\n  readonly kind: 'exceeded' | 'unavailable'; // exceeded = limit hit; unavailable = Redis error\n  readonly remaining: number; // always 0\n  readonly resetMs: number;   // ms until window resets\n}\n```\n\n### `setRateLimiterLogger(logger: Logger): void`\n\nOverride the module-level logger. Default: structured JSON to stdout.\n\n### `fromIoredis(client): RedisExecutor`\n\nWraps a raw `ioredis` client into a `RedisExecutor`, routing `scriptLoad()` through ioredis's `.script(\"LOAD\", ...)`. `eval`/`evalsha` pass through unchanged.\n\n## RedisExecutor interface\n\nThe `redis` config option accepts any object satisfying:\n\n```typescript\ninterface RedisExecutor {\n  eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n  evalsha(sha: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n  scriptLoad(script: string): Promise<string>;\n}\n```\n\nAn `ioredis` `Redis` instance does **not** satisfy this interface directly — ioredis has no `scriptLoad()` method, only `.script(\"LOAD\", script)`. Wrap it with `fromIoredis()`:\n\n```typescript\nimport Redis from 'ioredis';\nimport { fromIoredis } from '@diabolicallabs/rate-limiter';\n\nconst redis = new Redis(process.env['REDIS_URL']!);\nconst executor = fromIoredis(redis); // satisfies RedisExecutor\n```\n\nA hand-rolled `RedisExecutor` implementation (e.g. for `node-redis`) that already provides a real `scriptLoad()` needs no adapter — pass it directly.\n\n## Implementation notes\n\n**Algorithm:** sliding-window-log using a Redis sorted set per key. Each request is a member with score = timestamp. On each check:\n\n1. `TIME` — get authoritative server-side timestamp (no app-clock drift)\n2. `ZREMRANGEBYSCORE` — evict entries outside the window\n3. `ZCARD` — count entries in the current window\n4. `ZADD` — record the request if admitted\n5. `EXPIRE` — prevent idle-key memory leaks\n\nAll five operations execute in a single Lua script via `EVAL`/`EVALSHA` — atomically, with no interleaving between concurrent clients. `MULTI/EXEC` cannot provide this guarantee.\n\n**EVALSHA optimization:** The Lua script SHA is pre-warmed at construction via `SCRIPT LOAD`. Subsequent calls use the faster `EVALSHA`. On `NOSCRIPT` (Redis script cache flushed), the limiter falls back to `EVAL` and reloads the SHA transparently.\n","readmeFilename":"README.md"}