limitforge 0.0.0-stage → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type { Request, Response, NextFunction, RequestHandler } from 'express';
2
+
3
+ export type RateLimiterAlgorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
4
+ export type FailStrategy = 'fail-open' | 'fail-closed';
5
+
6
+ export interface StoreConsumeParams {
7
+ key: string;
8
+ limit?: number;
9
+ windowMs?: number;
10
+ algorithm?: RateLimiterAlgorithm;
11
+ capacity?: number;
12
+ refillRate?: number;
13
+ refillIntervalMs?: number;
14
+ cost?: number;
15
+ now?: number;
16
+ }
17
+
18
+ export interface StoreConsumeResult {
19
+ allowed: boolean;
20
+ count?: number;
21
+ remaining?: number;
22
+ reset?: number;
23
+ retryAfter?: number;
24
+ degraded?: boolean;
25
+ circuitState?: string;
26
+ }
27
+
28
+ export interface IRateLimitStore {
29
+ consume(params: StoreConsumeParams): Promise<StoreConsumeResult> | StoreConsumeResult;
30
+ resetKey?(key: string): Promise<void> | void;
31
+ destroy?(): void;
32
+ }
33
+
34
+ export interface RateLimiterOptions {
35
+ limit?: number | ((req: Request) => number | Promise<number>);
36
+ windowMs?: number | ((req: Request) => number | Promise<number>);
37
+ store?: IRateLimitStore;
38
+ fallbackStore?: boolean | IRateLimitStore;
39
+ algorithm?: RateLimiterAlgorithm;
40
+ capacity?: number | ((req: Request) => number | Promise<number>);
41
+ refillRate?: number | ((req: Request) => number | Promise<number>);
42
+ refillIntervalMs?: number;
43
+ cost?: number | ((req: Request) => number | Promise<number>);
44
+ keyGenerator?: (req: Request) => string | Promise<string>;
45
+ hashClientIdentifier?: boolean | ((identifier: string) => string);
46
+ timeoutMs?: number;
47
+ failureThreshold?: number;
48
+ resetTimeoutMs?: number;
49
+ failStrategy?: FailStrategy;
50
+ metrics?: boolean | MetricsCollector;
51
+ openTelemetry?: boolean | { tracer?: any; getSpan?: (req: Request) => any };
52
+ onStoreError?: (err: Error, req: Request, res: Response, next: NextFunction) => void;
53
+ statusCode?: number;
54
+ message?: string | object | ((req: Request, res: Response) => any);
55
+ skip?: (req: Request) => boolean | Promise<boolean>;
56
+ skipFailedRequests?: boolean;
57
+ skipSuccessfulRequests?: boolean;
58
+ }
59
+
60
+ export declare function rateLimiter(options?: RateLimiterOptions): RequestHandler;
61
+ export default rateLimiter;
62
+
63
+ export declare class MemoryStore implements IRateLimitStore {
64
+ constructor(options?: { cleanupIntervalMs?: number });
65
+ consume(params: StoreConsumeParams): StoreConsumeResult;
66
+ resetKey(key: string): void;
67
+ destroy(): void;
68
+ }
69
+
70
+ export interface RedisClientLike {
71
+ eval(script: string, options: { keys: string[]; arguments: (string | number)[] }): Promise<any>;
72
+ sendCommand?(args: string[]): Promise<any>;
73
+ }
74
+
75
+ export declare class RedisStore implements IRateLimitStore {
76
+ constructor(options: { client: RedisClientLike; prefix?: string });
77
+ consume(params: StoreConsumeParams): Promise<StoreConsumeResult>;
78
+ resetKey(key: string): Promise<void>;
79
+ }
80
+
81
+ export interface CircuitBreakerOptions {
82
+ failureThreshold?: number;
83
+ resetTimeoutMs?: number;
84
+ name?: string;
85
+ breakerId?: string;
86
+ onStateChange?: (oldState: string, newState: string) => void;
87
+ }
88
+
89
+ export declare class CircuitBreaker {
90
+ constructor(options?: CircuitBreakerOptions);
91
+ execute<T>(action: () => Promise<T>): Promise<T>;
92
+ getState(): 'CLOSED' | 'OPEN' | 'HALF_OPEN';
93
+ trip(): void;
94
+ reset(): void;
95
+ }
96
+
97
+ export declare class ResilientStore implements IRateLimitStore {
98
+ primaryStore: IRateLimitStore;
99
+ fallbackStore: IRateLimitStore;
100
+ breaker: CircuitBreaker;
101
+ constructor(options: {
102
+ primaryStore: IRateLimitStore;
103
+ fallbackStore?: IRateLimitStore;
104
+ breaker?: CircuitBreaker;
105
+ timeoutMs?: number;
106
+ failureThreshold?: number;
107
+ resetTimeoutMs?: number;
108
+ breakerId?: string;
109
+ });
110
+ consume(params: StoreConsumeParams): Promise<StoreConsumeResult>;
111
+ resetKey(key: string): Promise<void>;
112
+ getStats(): {
113
+ circuitBreaker: { state: string; failureCount: number };
114
+ primaryStore: string;
115
+ fallbackStore: string;
116
+ };
117
+ }
118
+
119
+ export declare class MetricsCollector {
120
+ constructor(options?: { maxSeries?: number; metricMap?: Map<string, any> });
121
+ recordRequest(params: {
122
+ outcome: 'allowed' | 'blocked';
123
+ normalized_route?: string;
124
+ method?: string;
125
+ status_code?: number;
126
+ store?: string;
127
+ degraded?: boolean;
128
+ }): void;
129
+ recordStoreDuration(params: {
130
+ store: string;
131
+ operation: string;
132
+ outcome: 'success' | 'failure' | 'timeout';
133
+ duration_ms: number;
134
+ }): void;
135
+ recordCircuitBreakerState(params: {
136
+ state: 'CLOSED' | 'OPEN' | 'HALF_OPEN';
137
+ breaker?: string;
138
+ }): void;
139
+ getSnapshot(): any;
140
+ reset(): void;
141
+ }
142
+
143
+ export declare const defaultMetricsCollector: MetricsCollector;
144
+
145
+ export declare class OpenTelemetryBridge {
146
+ constructor(options?: { tracer?: any; getSpan?: (req: Request) => any });
147
+ startSpan(name?: string, options?: any): any;
148
+ resolveSpan(req: Request): any;
149
+ recordEvaluation(params: {
150
+ req: Request;
151
+ result: StoreConsumeResult;
152
+ algorithm?: string;
153
+ normalizedRoute?: string;
154
+ store?: string;
155
+ }): void;
156
+ recordStoreError(params: { req: Request; storeError: Error; store?: string }): void;
157
+ }
158
+
159
+ export declare class StoreTimeoutError extends Error {
160
+ code: string;
161
+ timeoutMs: number;
162
+ constructor(timeoutMs: number, operation?: string);
163
+ }
164
+
165
+ export declare class CircuitBreakerOpenError extends Error {
166
+ code: string;
167
+ retryAfter: number;
168
+ constructor(message?: string, retryAfter?: number);
169
+ }
170
+
171
+ export declare const MAX_IDENTIFIER_LENGTH: number;
172
+
173
+ export declare function buildRateLimitKey(params: {
174
+ method?: string;
175
+ route?: string;
176
+ clientIdentifier?: string;
177
+ algorithm?: string;
178
+ prefix?: string;
179
+ hashClientIdentifier?: boolean | ((id: string) => string);
180
+ }): string;
181
+
182
+ export declare function normalizeRoute(path: string): string;
183
+ export declare function sanitizePath(path: string): string;
184
+ export declare function createPrometheusExporter(collector?: MetricsCollector): RequestHandler;
185
+ export declare function formatPrometheusMetrics(snapshot: any): string;
186
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, operation?: string): Promise<T>;
187
+ export declare function runBenchmark(options?: any): Promise<any>;
188
+ export declare function calculatePercentiles(latencies: bigint[]): any;
189
+
190
+ export declare const METRIC_NAMES: Record<string, string>;
191
+ export declare const METRIC_TYPE: Record<string, string>;
192
+ export declare const CIRCUIT_STATE: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "limitforge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "8.0.0",
4
+ "description": "Production-ready distributed rate limiting and resilience middleware for Express.js supporting Redis Lua, Token Bucket, Sliding Window, Circuit Breakers, and Prometheus metrics",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "main": "./src/index.js",
10
+ "types": "./index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./index.d.ts",
14
+ "default": "./src/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "src",
19
+ "index.d.ts",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "scripts": {
24
+ "dev": "node --watch examples/express-demo/server.js",
25
+ "start": "node examples/express-demo/server.js",
26
+ "demo:multi": "node examples/express-demo/multi-instance.js",
27
+ "demo:sliding": "node examples/express-demo/sliding-demo.js",
28
+ "demo:saas": "node examples/express-demo/saas-demo.js",
29
+ "demo:burst": "node examples/express-demo/burst-demo.js",
30
+ "demo:resilience": "node examples/express-demo/resilience-demo.js",
31
+ "demo:observability": "node examples/express-demo/observability-demo.js",
32
+ "demo:stack": "node examples/observability-stack/server.js",
33
+ "demo:simulate": "node examples/observability-stack/simulate-traffic.js",
34
+ "benchmark": "node scripts/benchmark.js",
35
+ "test": "node --test"
36
+ },
37
+ "keywords": [
38
+ "rate-limiter",
39
+ "express",
40
+ "middleware",
41
+ "fixed-window",
42
+ "sliding-window",
43
+ "rolling-window",
44
+ "token-bucket",
45
+ "multi-tenant",
46
+ "dynamic-policy",
47
+ "circuit-breaker",
48
+ "resilience",
49
+ "prometheus",
50
+ "opentelemetry",
51
+ "in-memory",
52
+ "redis",
53
+ "distributed",
54
+ "lua",
55
+ "zset",
56
+ "limitforge"
57
+ ],
58
+ "author": "Anivesh Sikarwar <aniveshsikarwar@gmail.com>",
59
+ "license": "MIT",
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/Anivesh91/RateShield.git"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/Anivesh91/RateShield/issues"
66
+ },
67
+ "homepage": "https://github.com/Anivesh91/RateShield#readme",
68
+ "peerDependencies": {
69
+ "express": "^4.21.2 || ^5.0.0",
70
+ "redis": ">=4.0.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "redis": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "devDependencies": {
78
+ "dotenv": "^16.4.7",
79
+ "express": "^4.21.2",
80
+ "redis": "^6.2.1",
81
+ "supertest": "^7.0.0"
82
+ }
83
+ }
@@ -0,0 +1,235 @@
1
+ import { rateLimiter } from '../limiter/rateLimiter.js';
2
+ import { MemoryStore } from '../stores/memoryStore.js';
3
+ import { MetricsCollector } from '../telemetry/metricsCollector.js';
4
+ import { OpenTelemetryBridge } from '../telemetry/openTelemetryBridge.js';
5
+
6
+ export function calculatePercentiles(samplesNs) {
7
+ if (!Array.isArray(samplesNs) || samplesNs.length === 0) {
8
+ return {
9
+ count: 0,
10
+ minUs: 0,
11
+ maxUs: 0,
12
+ meanUs: 0,
13
+ p50Us: 0,
14
+ p90Us: 0,
15
+ p95Us: 0,
16
+ p99Us: 0,
17
+ minMs: 0,
18
+ maxMs: 0,
19
+ meanMs: 0,
20
+ p50Ms: 0,
21
+ p90Ms: 0,
22
+ p95Ms: 0,
23
+ p99Ms: 0
24
+ };
25
+ }
26
+
27
+ const samplesUs = samplesNs
28
+ .map((ns) => (typeof ns === 'bigint' ? Number(ns) / 1000 : Number(ns) / 1000))
29
+ .sort((a, b) => a - b);
30
+
31
+ const count = samplesUs.length;
32
+ const sumUs = samplesUs.reduce((acc, val) => acc + val, 0);
33
+ const meanUs = sumUs / count;
34
+
35
+ const getPercentile = (p) => {
36
+ const rank = (p / 100) * (count - 1);
37
+ const lowerIndex = Math.floor(rank);
38
+ const upperIndex = Math.ceil(rank);
39
+ const fraction = rank - lowerIndex;
40
+ return samplesUs[lowerIndex] + (samplesUs[upperIndex] - samplesUs[lowerIndex]) * fraction;
41
+ };
42
+
43
+ const minUs = samplesUs[0];
44
+ const maxUs = samplesUs[count - 1];
45
+ const p50Us = getPercentile(50);
46
+ const p90Us = getPercentile(90);
47
+ const p95Us = getPercentile(95);
48
+ const p99Us = getPercentile(99);
49
+
50
+ return {
51
+ count,
52
+ minUs: Number(minUs.toFixed(2)),
53
+ maxUs: Number(maxUs.toFixed(2)),
54
+ meanUs: Number(meanUs.toFixed(2)),
55
+ p50Us: Number(p50Us.toFixed(2)),
56
+ p90Us: Number(p90Us.toFixed(2)),
57
+ p95Us: Number(p95Us.toFixed(2)),
58
+ p99Us: Number(p99Us.toFixed(2)),
59
+ minMs: Number((minUs / 1000).toFixed(4)),
60
+ maxMs: Number((maxUs / 1000).toFixed(4)),
61
+ meanMs: Number((meanUs / 1000).toFixed(4)),
62
+ p50Ms: Number((p50Us / 1000).toFixed(4)),
63
+ p90Ms: Number((p90Us / 1000).toFixed(4)),
64
+ p95Ms: Number((p95Us / 1000).toFixed(4)),
65
+ p99Ms: Number((p99Us / 1000).toFixed(4))
66
+ };
67
+ }
68
+
69
+ function createMockHttpPair(ip = '192.168.1.1', path = '/api/resource', method = 'GET') {
70
+ const req = {
71
+ method,
72
+ originalUrl: path,
73
+ path,
74
+ ip,
75
+ headers: { host: 'localhost:3000' },
76
+ socket: { remoteAddress: ip }
77
+ };
78
+
79
+ const headers = {};
80
+ const res = {
81
+ statusCode: 200,
82
+ headersSent: false,
83
+ setHeader(name, value) {
84
+ headers[name.toLowerCase()] = String(value);
85
+ },
86
+ getHeader(name) {
87
+ return headers[name.toLowerCase()];
88
+ },
89
+ status(code) {
90
+ this.statusCode = code;
91
+ return this;
92
+ },
93
+ json(body) {
94
+ this.body = body;
95
+ return this;
96
+ },
97
+ end() {
98
+ return this;
99
+ }
100
+ };
101
+
102
+ return { req, res };
103
+ }
104
+
105
+ export async function runBenchmark(options = {}) {
106
+ const iterations = options.iterations || 10000;
107
+ const warmup = options.warmup ?? 1000;
108
+ const nextStub = () => {};
109
+
110
+ const scenarios = [
111
+ {
112
+ name: '1. No-op Middleware (next() only)',
113
+ middleware: (req, res, next) => next()
114
+ },
115
+ {
116
+ name: '2. Fixed Window (Memory Store)',
117
+ middleware: rateLimiter({
118
+ limit: 1000000,
119
+ windowMs: 60000,
120
+ algorithm: 'fixed-window',
121
+ store: new MemoryStore()
122
+ })
123
+ },
124
+ {
125
+ name: '3. Sliding Window (Memory Store)',
126
+ middleware: rateLimiter({
127
+ limit: 1000000,
128
+ windowMs: 60000,
129
+ algorithm: 'sliding-window',
130
+ store: new MemoryStore()
131
+ })
132
+ },
133
+ {
134
+ name: '4. Token Bucket (Memory Store)',
135
+ middleware: rateLimiter({
136
+ capacity: 1000000,
137
+ refillRate: 100000,
138
+ algorithm: 'token-bucket',
139
+ store: new MemoryStore()
140
+ })
141
+ },
142
+ {
143
+ name: '5. Sliding Window + Metrics (Telemetry Enabled)',
144
+ middleware: rateLimiter({
145
+ limit: 1000000,
146
+ windowMs: 60000,
147
+ algorithm: 'sliding-window',
148
+ store: new MemoryStore(),
149
+ metricsCollector: new MetricsCollector()
150
+ })
151
+ },
152
+ {
153
+ name: '6. Sliding Window + Metrics + OpenTelemetry Bridge',
154
+ middleware: rateLimiter({
155
+ limit: 1000000,
156
+ windowMs: 60000,
157
+ algorithm: 'sliding-window',
158
+ store: new MemoryStore(),
159
+ metricsCollector: new MetricsCollector(),
160
+ openTelemetry: new OpenTelemetryBridge({
161
+ getSpan: (req) => req.span
162
+ })
163
+ })
164
+ }
165
+ ];
166
+
167
+ const results = [];
168
+
169
+ for (const scenario of scenarios) {
170
+ const mockSpan = {
171
+ attributes: {},
172
+ events: [],
173
+ setAttribute(k, v) { this.attributes[k] = v; },
174
+ addEvent(name, attrs) { this.events.push({ name, attrs }); }
175
+ };
176
+
177
+ // Warmup phase
178
+ for (let i = 0; i < warmup; i++) {
179
+ const { req, res } = createMockHttpPair(`10.0.0.${(i % 50) + 1}`, '/api/benchmark');
180
+ req.span = mockSpan;
181
+ await scenario.middleware(req, res, nextStub);
182
+ }
183
+
184
+ // Measurement phase
185
+ const samplesNs = [];
186
+ const totalStart = process.hrtime.bigint();
187
+
188
+ for (let i = 0; i < iterations; i++) {
189
+ const { req, res } = createMockHttpPair(`10.0.0.${(i % 100) + 1}`, '/api/benchmark');
190
+ req.span = mockSpan;
191
+
192
+ const start = process.hrtime.bigint();
193
+ await scenario.middleware(req, res, nextStub);
194
+ const durationNs = process.hrtime.bigint() - start;
195
+
196
+ samplesNs.push(durationNs);
197
+ }
198
+
199
+ const totalElapsedNs = process.hrtime.bigint() - totalStart;
200
+ const totalElapsedSec = Number(totalElapsedNs) / 1e9;
201
+ const opsPerSec = Math.round(iterations / totalElapsedSec);
202
+ const stats = calculatePercentiles(samplesNs);
203
+
204
+ results.push({
205
+ scenario: scenario.name,
206
+ iterations,
207
+ opsPerSec,
208
+ stats
209
+ });
210
+ }
211
+
212
+ const baselineStats = results[0].stats;
213
+ const enrichedResults = results.map((r, idx) => {
214
+ if (idx === 0) {
215
+ return { ...r, overheadP50Us: 0, overheadP99Us: 0 };
216
+ }
217
+ const overheadP50Us = Number((r.stats.p50Us - baselineStats.p50Us).toFixed(2));
218
+ const overheadP99Us = Number((r.stats.p99Us - baselineStats.p99Us).toFixed(2));
219
+ return {
220
+ ...r,
221
+ overheadP50Us: Math.max(0, overheadP50Us),
222
+ overheadP99Us: Math.max(0, overheadP99Us)
223
+ };
224
+ });
225
+
226
+ return {
227
+ timestamp: new Date().toISOString(),
228
+ nodeVersion: process.version,
229
+ platform: process.platform,
230
+ arch: process.arch,
231
+ iterations,
232
+ warmup,
233
+ scenarios: enrichedResults
234
+ };
235
+ }
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ export { default, rateLimiter } from './limiter/rateLimiter.js';
2
+ export { MemoryStore } from './stores/memoryStore.js';
3
+ export { RedisStore } from './stores/redisStore.js';
4
+ export { ResilientStore } from './stores/resilientStore.js';
5
+
6
+ export { buildRateLimitKey, MAX_IDENTIFIER_LENGTH } from './utils/keyBuilder.js';
7
+ export { normalizeRoute, sanitizePath } from './utils/routeNormalizer.js';
8
+
9
+ export { withTimeout } from './resilience/timeoutGuard.js';
10
+ export { CircuitBreaker, CIRCUIT_STATE } from './resilience/circuitBreaker.js';
11
+ export { StoreTimeoutError, CircuitBreakerOpenError } from './resilience/errors.js';
12
+
13
+ export { MetricsCollector, defaultMetricsCollector } from './telemetry/metricsCollector.js';
14
+ export { OpenTelemetryBridge } from './telemetry/openTelemetryBridge.js';
15
+ export {
16
+ createPrometheusExporter,
17
+ formatPrometheusMetrics,
18
+ escapeLabelValue,
19
+ escapeHelpString,
20
+ formatLabels
21
+ } from './telemetry/prometheusExporter.js';
22
+ export {
23
+ METRIC_NAMES,
24
+ METRIC_TYPE,
25
+ METRIC_METADATA,
26
+ CIRCUIT_STATE_GAUGE_VALUES,
27
+ DEFAULT_STORE_DURATION_BUCKETS
28
+ } from './telemetry/telemetryEvents.js';
29
+
30
+ export { runBenchmark, calculatePercentiles } from './benchmark/benchmarkRunner.js';