halt-rate 0.1.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/dist/index.js ADDED
@@ -0,0 +1,782 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, { get: all[name], enumerable: true });
7
+ };
8
+
9
+ // src/core/policy.ts
10
+ var KeyStrategy = /* @__PURE__ */ ((KeyStrategy2) => {
11
+ KeyStrategy2["IP"] = "ip";
12
+ KeyStrategy2["USER"] = "user";
13
+ KeyStrategy2["API_KEY"] = "api_key";
14
+ KeyStrategy2["COMPOSITE"] = "composite";
15
+ KeyStrategy2["CUSTOM"] = "custom";
16
+ return KeyStrategy2;
17
+ })(KeyStrategy || {});
18
+ var Algorithm = /* @__PURE__ */ ((Algorithm2) => {
19
+ Algorithm2["TOKEN_BUCKET"] = "token_bucket";
20
+ Algorithm2["FIXED_WINDOW"] = "fixed_window";
21
+ Algorithm2["SLIDING_WINDOW"] = "sliding_window";
22
+ Algorithm2["LEAKY_BUCKET"] = "leaky_bucket";
23
+ return Algorithm2;
24
+ })(Algorithm || {});
25
+ function normalizePolicy(policy) {
26
+ const normalized = {
27
+ name: policy.name,
28
+ limit: policy.limit,
29
+ window: policy.window,
30
+ algorithm: policy.algorithm ?? "token_bucket" /* TOKEN_BUCKET */,
31
+ keyStrategy: policy.keyStrategy ?? "ip" /* IP */,
32
+ burst: policy.burst ?? Math.floor(policy.limit * 1.2),
33
+ cost: policy.cost ?? 1,
34
+ blockDuration: policy.blockDuration ?? void 0,
35
+ keyExtractor: policy.keyExtractor ?? void 0,
36
+ exemptions: policy.exemptions ?? []
37
+ };
38
+ if (normalized.limit <= 0) {
39
+ throw new Error("limit must be positive");
40
+ }
41
+ if (normalized.window <= 0) {
42
+ throw new Error("window must be positive");
43
+ }
44
+ if (normalized.cost <= 0) {
45
+ throw new Error("cost must be positive");
46
+ }
47
+ if (normalized.burst < normalized.limit) {
48
+ throw new Error("burst must be >= limit");
49
+ }
50
+ return normalized;
51
+ }
52
+
53
+ // src/core/extractors.ts
54
+ var extractors_exports = {};
55
+ __export(extractors_exports, {
56
+ HEALTH_CHECK_PATHS: () => HEALTH_CHECK_PATHS,
57
+ extractApiKey: () => extractApiKey,
58
+ extractIp: () => extractIp,
59
+ extractPath: () => extractPath,
60
+ extractUserId: () => extractUserId,
61
+ isHealthCheck: () => isHealthCheck,
62
+ isPrivateIp: () => isPrivateIp
63
+ });
64
+ var HEALTH_CHECK_PATHS = /* @__PURE__ */ new Set([
65
+ "/health",
66
+ "/ping",
67
+ "/ready",
68
+ "/healthz",
69
+ "/livez"
70
+ ]);
71
+ var PRIVATE_IP_PATTERNS = [
72
+ /^127\./,
73
+ /^10\./,
74
+ /^172\.(1[6-9]|2\d|3[01])\./,
75
+ /^192\.168\./,
76
+ /^::1$/,
77
+ /^fc00:/
78
+ ];
79
+ function extractIp(request, trustedProxies = []) {
80
+ let clientIp = null;
81
+ if (request.socket?.remoteAddress) {
82
+ clientIp = request.socket.remoteAddress;
83
+ } else if (request.ip) {
84
+ clientIp = request.ip;
85
+ } else if (request.connection?.remoteAddress) {
86
+ clientIp = request.connection.remoteAddress;
87
+ }
88
+ if (!clientIp) {
89
+ return null;
90
+ }
91
+ clientIp = clientIp.replace(/^::ffff:/, "");
92
+ if (trustedProxies.length > 0 && isTrustedProxy(clientIp, trustedProxies)) {
93
+ const forwarded = getForwardedFor(request);
94
+ if (forwarded) {
95
+ return forwarded;
96
+ }
97
+ }
98
+ return clientIp;
99
+ }
100
+ function getForwardedFor(request) {
101
+ const headers = request.headers || {};
102
+ const forwarded = headers["x-forwarded-for"] || headers["X-Forwarded-For"];
103
+ if (forwarded) {
104
+ return forwarded.split(",")[0].trim();
105
+ }
106
+ return null;
107
+ }
108
+ function isTrustedProxy(ip, trustedProxies) {
109
+ return trustedProxies.some((proxy) => {
110
+ if (proxy.includes("/")) {
111
+ return ip.startsWith(proxy.split("/")[0].split(".").slice(0, -1).join("."));
112
+ }
113
+ return ip === proxy;
114
+ });
115
+ }
116
+ function isPrivateIp(ip) {
117
+ return PRIVATE_IP_PATTERNS.some((pattern) => pattern.test(ip));
118
+ }
119
+ function isHealthCheck(path) {
120
+ return HEALTH_CHECK_PATHS.has(path);
121
+ }
122
+ function extractUserId(request) {
123
+ if (request.user?.id) {
124
+ return String(request.user.id);
125
+ }
126
+ if (request.userId) {
127
+ return String(request.userId);
128
+ }
129
+ return null;
130
+ }
131
+ function extractApiKey(request) {
132
+ const headers = request.headers || {};
133
+ const apiKey = headers["x-api-key"] || headers["X-API-Key"] || headers["authorization"] || headers["Authorization"];
134
+ if (apiKey) {
135
+ if (apiKey.startsWith("Bearer ")) {
136
+ return apiKey.substring(7);
137
+ }
138
+ return apiKey;
139
+ }
140
+ return null;
141
+ }
142
+ function extractPath(request) {
143
+ if (request.nextUrl?.pathname) {
144
+ return request.nextUrl.pathname;
145
+ }
146
+ if (request.path) {
147
+ return request.path;
148
+ }
149
+ if (request.url) {
150
+ try {
151
+ const url = new URL(request.url, "http://localhost");
152
+ return url.pathname;
153
+ } catch {
154
+ return request.url;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+
160
+ // src/algorithms/token-bucket.ts
161
+ var TokenBucket = class {
162
+ constructor(capacity, rate, window) {
163
+ this.capacity = capacity;
164
+ this.rate = rate / window;
165
+ this.window = window;
166
+ }
167
+ /**
168
+ * Check if request is allowed and consume tokens.
169
+ */
170
+ checkAndConsume(currentTokens, lastRefill, cost, now = Date.now() / 1e3) {
171
+ const elapsed = now - lastRefill;
172
+ const refillAmount = elapsed * this.rate;
173
+ const newTokens = Math.min(this.capacity, currentTokens + refillAmount);
174
+ const tokensNeeded = this.capacity - newTokens;
175
+ const resetAt = Math.floor(now + tokensNeeded / this.rate);
176
+ if (newTokens >= cost) {
177
+ const tokensAfterConsume = newTokens - cost;
178
+ const remaining = Math.floor(tokensAfterConsume);
179
+ return {
180
+ decision: {
181
+ allowed: true,
182
+ limit: Math.floor(this.rate * this.window),
183
+ remaining,
184
+ resetAt
185
+ },
186
+ newTokens: tokensAfterConsume,
187
+ newLastRefill: now
188
+ };
189
+ } else {
190
+ const tokensDeficit = cost - newTokens;
191
+ const retryAfter = Math.floor(tokensDeficit / this.rate) + 1;
192
+ return {
193
+ decision: {
194
+ allowed: false,
195
+ limit: Math.floor(this.rate * this.window),
196
+ remaining: 0,
197
+ resetAt,
198
+ retryAfter
199
+ },
200
+ newTokens,
201
+ newLastRefill: lastRefill
202
+ // Don't update last_refill on rejection
203
+ };
204
+ }
205
+ }
206
+ /**
207
+ * Get initial state for a new key.
208
+ */
209
+ initialState(now = Date.now() / 1e3) {
210
+ return {
211
+ tokens: this.capacity,
212
+ lastRefill: now
213
+ };
214
+ }
215
+ };
216
+
217
+ // src/algorithms/fixed-window.ts
218
+ var FixedWindow = class {
219
+ constructor(limit, window) {
220
+ this.limit = limit;
221
+ this.window = window;
222
+ }
223
+ /**
224
+ * Check if request is allowed and increment counter.
225
+ */
226
+ checkAndConsume(currentCount, windowStart, cost, now = Date.now() / 1e3) {
227
+ const timeSinceStart = now - windowStart;
228
+ if (timeSinceStart >= this.window) {
229
+ currentCount = 0;
230
+ windowStart = now;
231
+ }
232
+ const resetAt = Math.floor(windowStart + this.window);
233
+ if (currentCount + cost <= this.limit) {
234
+ const newCount = currentCount + cost;
235
+ const remaining = this.limit - newCount;
236
+ return {
237
+ decision: {
238
+ allowed: true,
239
+ limit: this.limit,
240
+ remaining,
241
+ resetAt
242
+ },
243
+ newCount,
244
+ newWindowStart: windowStart
245
+ };
246
+ } else {
247
+ const retryAfter = Math.floor(resetAt - now) + 1;
248
+ return {
249
+ decision: {
250
+ allowed: false,
251
+ limit: this.limit,
252
+ remaining: 0,
253
+ resetAt,
254
+ retryAfter
255
+ },
256
+ newCount: currentCount,
257
+ newWindowStart: windowStart
258
+ };
259
+ }
260
+ }
261
+ /**
262
+ * Get initial state for a new key.
263
+ */
264
+ initialState(now = Date.now() / 1e3) {
265
+ return {
266
+ count: 0,
267
+ windowStart: now
268
+ };
269
+ }
270
+ };
271
+
272
+ // src/algorithms/sliding-window.ts
273
+ var SlidingWindow = class {
274
+ constructor(limit, window, precision = 10) {
275
+ this.limit = limit;
276
+ this.window = window;
277
+ this.precision = precision;
278
+ this.bucketSize = window / precision;
279
+ }
280
+ /**
281
+ * Check if request is allowed and update buckets.
282
+ */
283
+ checkAndConsume(buckets, cost, now = Date.now() / 1e3) {
284
+ const currentBucket = Math.floor(now / this.bucketSize);
285
+ const cutoffBucket = currentBucket - this.precision;
286
+ const newBuckets = {};
287
+ for (const [bucketIdStr, count] of Object.entries(buckets)) {
288
+ const bucketId = parseInt(bucketIdStr, 10);
289
+ if (bucketId > cutoffBucket) {
290
+ newBuckets[bucketId] = count;
291
+ }
292
+ }
293
+ const totalCount = Object.values(newBuckets).reduce((sum, count) => sum + count, 0);
294
+ const bucketIds = Object.keys(newBuckets).map((id) => parseInt(id, 10));
295
+ const oldestBucket = bucketIds.length > 0 ? Math.min(...bucketIds) : currentBucket;
296
+ const resetAt = Math.floor((oldestBucket + this.precision + 1) * this.bucketSize);
297
+ if (totalCount + cost <= this.limit) {
298
+ newBuckets[currentBucket] = (newBuckets[currentBucket] || 0) + cost;
299
+ const remaining = this.limit - (totalCount + cost);
300
+ return {
301
+ decision: {
302
+ allowed: true,
303
+ limit: this.limit,
304
+ remaining,
305
+ resetAt
306
+ },
307
+ newBuckets
308
+ };
309
+ } else {
310
+ const retryAfter = Math.floor(this.bucketSize) + 1;
311
+ return {
312
+ decision: {
313
+ allowed: false,
314
+ limit: this.limit,
315
+ remaining: 0,
316
+ resetAt,
317
+ retryAfter
318
+ },
319
+ newBuckets
320
+ };
321
+ }
322
+ }
323
+ /**
324
+ * Get initial state for a new key.
325
+ */
326
+ initialState() {
327
+ return {};
328
+ }
329
+ };
330
+
331
+ // src/algorithms/leaky-bucket.ts
332
+ var LeakyBucket = class {
333
+ constructor(capacity, leakRate, window) {
334
+ this.capacity = capacity;
335
+ this.leakRate = leakRate;
336
+ this.window = window;
337
+ }
338
+ /**
339
+ * Check if request is allowed and update bucket level.
340
+ */
341
+ checkAndConsume(currentLevel, lastLeak, cost, now = Date.now() / 1e3) {
342
+ const elapsed = now - lastLeak;
343
+ const leaked = elapsed * this.leakRate;
344
+ let newLevel = Math.max(0, currentLevel - leaked);
345
+ let resetAt;
346
+ if (newLevel > 0) {
347
+ const timeToEmpty = newLevel / this.leakRate;
348
+ resetAt = Math.floor(now + timeToEmpty);
349
+ } else {
350
+ resetAt = Math.floor(now);
351
+ }
352
+ if (newLevel + cost <= this.capacity) {
353
+ newLevel += cost;
354
+ const remaining = Math.floor(this.capacity - newLevel);
355
+ return {
356
+ decision: {
357
+ allowed: true,
358
+ limit: this.capacity,
359
+ remaining,
360
+ resetAt
361
+ },
362
+ newLevel,
363
+ newLastLeak: now
364
+ };
365
+ } else {
366
+ const spaceNeeded = newLevel + cost - this.capacity;
367
+ const retryAfter = Math.floor(spaceNeeded / this.leakRate) + 1;
368
+ return {
369
+ decision: {
370
+ allowed: false,
371
+ limit: this.capacity,
372
+ remaining: 0,
373
+ resetAt,
374
+ retryAfter
375
+ },
376
+ newLevel,
377
+ newLastLeak: now
378
+ };
379
+ }
380
+ }
381
+ /**
382
+ * Get initial state for a new key.
383
+ */
384
+ initialState(now = Date.now() / 1e3) {
385
+ return {
386
+ level: 0,
387
+ lastLeak: now
388
+ };
389
+ }
390
+ };
391
+
392
+ // src/core/limiter.ts
393
+ var RateLimiter = class {
394
+ constructor(options) {
395
+ this.store = options.store;
396
+ this.policyOrResolver = options.policy;
397
+ this.trustedProxies = options.trustedProxies ?? [];
398
+ this.exemptPrivateIps = options.exemptPrivateIps ?? true;
399
+ this.algorithmCache = /* @__PURE__ */ new Map();
400
+ this.otelTracer = options.otelTracer;
401
+ this.metricsRecorder = options.metricsRecorder;
402
+ }
403
+ /**
404
+ * Check if request is allowed under rate limit.
405
+ */
406
+ /**
407
+ * Check if request is allowed under rate limit.
408
+ * This method is async because policy resolution may be async (e.g. DB lookup).
409
+ */
410
+ async check(request, cost) {
411
+ const resolved = typeof this.policyOrResolver === "function" ? await this.policyOrResolver(request) : this.policyOrResolver;
412
+ const policy = normalizePolicy(resolved);
413
+ const requestCost = cost ?? policy.cost;
414
+ if (this.isExempt(request, policy)) {
415
+ const resp = {
416
+ allowed: true,
417
+ limit: policy.limit,
418
+ remaining: policy.limit,
419
+ resetAt: Math.floor(Date.now() / 1e3 + policy.window)
420
+ };
421
+ this.metricsRecorder?.("halt.request.exempt", { policy: policy.name }, 1);
422
+ return resp;
423
+ }
424
+ const key = this.extractKey(request, policy);
425
+ if (!key) {
426
+ const resp = {
427
+ allowed: true,
428
+ limit: policy.limit,
429
+ remaining: policy.limit,
430
+ resetAt: Math.floor(Date.now() / 1e3 + policy.window)
431
+ };
432
+ this.metricsRecorder?.("halt.request.no_key", { policy: policy.name }, 1);
433
+ return resp;
434
+ }
435
+ const storageKey = `halt:${policy.name}:${key}`;
436
+ let algorithm = this.algorithmCache.get(policy.name);
437
+ if (!algorithm) {
438
+ if (policy.algorithm === "token_bucket" /* TOKEN_BUCKET */) {
439
+ algorithm = new TokenBucket(policy.burst, policy.limit, policy.window);
440
+ } else if (policy.algorithm === "fixed_window" /* FIXED_WINDOW */) {
441
+ algorithm = new FixedWindow(policy.limit, policy.window);
442
+ } else if (policy.algorithm === "sliding_window" /* SLIDING_WINDOW */) {
443
+ algorithm = new SlidingWindow(policy.limit, policy.window);
444
+ } else if (policy.algorithm === "leaky_bucket" /* LEAKY_BUCKET */) {
445
+ const leakRate = policy.limit / policy.window;
446
+ algorithm = new LeakyBucket(policy.burst, leakRate, policy.window);
447
+ } else {
448
+ throw new Error(`Algorithm ${policy.algorithm} not implemented`);
449
+ }
450
+ this.algorithmCache.set(policy.name, algorithm);
451
+ }
452
+ const span = this.otelTracer?.startSpan?.("halt.check", { attributes: { policy: policy.name, key } });
453
+ const state = this.store.get(storageKey);
454
+ let decision;
455
+ if (algorithm instanceof TokenBucket) {
456
+ let tokens;
457
+ let lastRefill;
458
+ if (!state) {
459
+ const initialState = algorithm.initialState();
460
+ tokens = initialState.tokens;
461
+ lastRefill = initialState.lastRefill;
462
+ } else {
463
+ tokens = state.tokens;
464
+ lastRefill = state.lastRefill;
465
+ }
466
+ const result = algorithm.checkAndConsume(tokens, lastRefill, requestCost);
467
+ decision = result.decision;
468
+ const ttl = policy.window * 2;
469
+ this.store.set(storageKey, { tokens: result.newTokens, lastRefill: result.newLastRefill }, ttl);
470
+ } else if (algorithm instanceof FixedWindow) {
471
+ let count;
472
+ let windowStart;
473
+ if (!state) {
474
+ const initialState = algorithm.initialState();
475
+ count = initialState.count;
476
+ windowStart = initialState.windowStart;
477
+ } else {
478
+ count = state.count;
479
+ windowStart = state.windowStart;
480
+ }
481
+ const result = algorithm.checkAndConsume(count, windowStart, requestCost);
482
+ decision = result.decision;
483
+ const ttl = policy.window * 2;
484
+ this.store.set(storageKey, { count: result.newCount, windowStart: result.newWindowStart }, ttl);
485
+ } else if (algorithm instanceof SlidingWindow) {
486
+ const buckets = state || algorithm.initialState();
487
+ const result = algorithm.checkAndConsume(buckets, requestCost);
488
+ decision = result.decision;
489
+ const ttl = policy.window * 2;
490
+ this.store.set(storageKey, result.newBuckets, ttl);
491
+ } else if (algorithm instanceof LeakyBucket) {
492
+ let level;
493
+ let lastLeak;
494
+ if (!state) {
495
+ const initialState = algorithm.initialState();
496
+ level = initialState.level;
497
+ lastLeak = initialState.lastLeak;
498
+ } else {
499
+ level = state.level;
500
+ lastLeak = state.lastLeak;
501
+ }
502
+ const result = algorithm.checkAndConsume(level, lastLeak, requestCost);
503
+ decision = result.decision;
504
+ const ttl = policy.window * 2;
505
+ this.store.set(storageKey, { level: result.newLevel, lastLeak: result.newLastLeak }, ttl);
506
+ } else {
507
+ throw new Error(`Algorithm ${typeof algorithm} not supported`);
508
+ }
509
+ this.metricsRecorder?.("halt.request.checked", { policy: policy.name, allowed: String(decision.allowed) }, 1);
510
+ if (decision.allowed) {
511
+ this.metricsRecorder?.("halt.request.allowed", { policy: policy.name }, 1);
512
+ } else {
513
+ this.metricsRecorder?.("halt.request.blocked", { policy: policy.name }, 1);
514
+ }
515
+ span?.end?.();
516
+ return decision;
517
+ }
518
+ /**
519
+ * Extract rate limit key from request based on policy strategy.
520
+ */
521
+ extractKey(request, policy) {
522
+ if (policy.keyExtractor) {
523
+ return policy.keyExtractor(request);
524
+ }
525
+ if (policy.keyStrategy === "ip" /* IP */) {
526
+ return extractIp(request, this.trustedProxies);
527
+ }
528
+ if (policy.keyStrategy === "user" /* USER */) {
529
+ return extractUserId(request);
530
+ }
531
+ if (policy.keyStrategy === "api_key" /* API_KEY */) {
532
+ return extractApiKey(request);
533
+ }
534
+ if (policy.keyStrategy === "composite" /* COMPOSITE */) {
535
+ const user = extractUserId(request);
536
+ const apiKey = extractApiKey(request);
537
+ const ip = extractIp(request, this.trustedProxies);
538
+ if (user && ip) {
539
+ return `${user}:${ip}`;
540
+ } else if (apiKey && ip) {
541
+ return `${apiKey}:${ip}`;
542
+ } else if (user) {
543
+ return user;
544
+ } else if (apiKey) {
545
+ return apiKey;
546
+ } else {
547
+ return ip;
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+ /**
553
+ * Check if request is exempt from rate limiting.
554
+ */
555
+ isExempt(request, policy) {
556
+ const path = extractPath(request);
557
+ if (path && isHealthCheck(path)) {
558
+ return true;
559
+ }
560
+ if (path && (policy.exemptions ?? []).includes(path)) {
561
+ return true;
562
+ }
563
+ if (this.exemptPrivateIps) {
564
+ const ip2 = extractIp(request, this.trustedProxies);
565
+ if (ip2 && isPrivateIp(ip2)) {
566
+ return true;
567
+ }
568
+ }
569
+ const ip = extractIp(request, this.trustedProxies);
570
+ if (ip && (policy.exemptions ?? []).includes(ip)) {
571
+ return true;
572
+ }
573
+ return false;
574
+ }
575
+ };
576
+
577
+ // src/core/decision.ts
578
+ function toHeaders(decision) {
579
+ const headers = {
580
+ "RateLimit-Limit": String(decision.limit),
581
+ "RateLimit-Remaining": String(Math.max(0, decision.remaining)),
582
+ "RateLimit-Reset": String(decision.resetAt)
583
+ };
584
+ if (!decision.allowed && decision.retryAfter !== void 0) {
585
+ headers["Retry-After"] = String(decision.retryAfter);
586
+ }
587
+ return headers;
588
+ }
589
+
590
+ // src/stores/memory.ts
591
+ var InMemoryStore = class {
592
+ constructor() {
593
+ this.data = /* @__PURE__ */ new Map();
594
+ }
595
+ get(key) {
596
+ this.cleanupExpired(key);
597
+ const entry = this.data.get(key);
598
+ return entry?.value ?? null;
599
+ }
600
+ set(key, value, ttl) {
601
+ const entry = { value };
602
+ if (ttl !== void 0) {
603
+ entry.expiry = Date.now() + ttl * 1e3;
604
+ }
605
+ this.data.set(key, entry);
606
+ }
607
+ increment(key, delta = 1, ttl) {
608
+ this.cleanupExpired(key);
609
+ const entry = this.data.get(key);
610
+ const current = typeof entry?.value === "number" ? entry.value : 0;
611
+ const newValue = current + delta;
612
+ const newEntry = { value: newValue };
613
+ if (!entry && ttl !== void 0) {
614
+ newEntry.expiry = Date.now() + ttl * 1e3;
615
+ } else if (entry?.expiry) {
616
+ newEntry.expiry = entry.expiry;
617
+ }
618
+ this.data.set(key, newEntry);
619
+ return newValue;
620
+ }
621
+ delete(key) {
622
+ this.data.delete(key);
623
+ }
624
+ cleanupExpired(key) {
625
+ const entry = this.data.get(key);
626
+ if (entry?.expiry && Date.now() >= entry.expiry) {
627
+ this.data.delete(key);
628
+ }
629
+ }
630
+ /**
631
+ * Clean up all expired keys.
632
+ */
633
+ cleanupAllExpired() {
634
+ const now = Date.now();
635
+ let count = 0;
636
+ for (const [key, entry] of this.data.entries()) {
637
+ if (entry.expiry && now >= entry.expiry) {
638
+ this.data.delete(key);
639
+ count++;
640
+ }
641
+ }
642
+ return count;
643
+ }
644
+ };
645
+
646
+ // src/presets/index.ts
647
+ var presets_exports = {};
648
+ __export(presets_exports, {
649
+ AUTH_ENDPOINTS: () => AUTH_ENDPOINTS,
650
+ EXPENSIVE_OPS: () => EXPENSIVE_OPS,
651
+ GENEROUS_API: () => GENEROUS_API,
652
+ PLAN_BUSINESS: () => PLAN_BUSINESS,
653
+ PLAN_ENTERPRISE: () => PLAN_ENTERPRISE,
654
+ PLAN_FREE: () => PLAN_FREE,
655
+ PLAN_PRO: () => PLAN_PRO,
656
+ PLAN_STARTER: () => PLAN_STARTER,
657
+ PLAN_TIERS: () => PLAN_TIERS,
658
+ PUBLIC_API: () => PUBLIC_API,
659
+ STRICT_API: () => STRICT_API,
660
+ getPlanPolicy: () => getPlanPolicy
661
+ });
662
+ var PUBLIC_API = {
663
+ name: "public_api",
664
+ limit: 100,
665
+ window: 60,
666
+ // 1 minute
667
+ burst: 120,
668
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
669
+ keyStrategy: "ip" /* IP */
670
+ };
671
+ var AUTH_ENDPOINTS = {
672
+ name: "auth_endpoints",
673
+ limit: 5,
674
+ window: 60,
675
+ // 1 minute
676
+ burst: 10,
677
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
678
+ keyStrategy: "ip" /* IP */,
679
+ blockDuration: 300
680
+ // 5 minute cooldown after limit exceeded
681
+ };
682
+ var EXPENSIVE_OPS = {
683
+ name: "expensive_ops",
684
+ limit: 10,
685
+ window: 3600,
686
+ // 1 hour
687
+ burst: 15,
688
+ cost: 10,
689
+ // Each request costs 10 tokens
690
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
691
+ keyStrategy: "user" /* USER */
692
+ };
693
+ var STRICT_API = {
694
+ name: "strict_api",
695
+ limit: 20,
696
+ window: 60,
697
+ // 1 minute
698
+ burst: 25,
699
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
700
+ keyStrategy: "api_key" /* API_KEY */
701
+ };
702
+ var GENEROUS_API = {
703
+ name: "generous_api",
704
+ limit: 1e3,
705
+ window: 60,
706
+ // 1 minute
707
+ burst: 1200,
708
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
709
+ keyStrategy: "ip" /* IP */
710
+ };
711
+ var PLAN_FREE = {
712
+ name: "free_plan",
713
+ limit: 100,
714
+ window: 3600,
715
+ // 100 requests per hour
716
+ burst: 120,
717
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
718
+ keyStrategy: "user" /* USER */
719
+ };
720
+ var PLAN_STARTER = {
721
+ name: "starter_plan",
722
+ limit: 500,
723
+ window: 3600,
724
+ // 500 requests per hour
725
+ burst: 600,
726
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
727
+ keyStrategy: "user" /* USER */
728
+ };
729
+ var PLAN_PRO = {
730
+ name: "pro_plan",
731
+ limit: 2e3,
732
+ window: 3600,
733
+ // 2000 requests per hour
734
+ burst: 2500,
735
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
736
+ keyStrategy: "user" /* USER */
737
+ };
738
+ var PLAN_BUSINESS = {
739
+ name: "business_plan",
740
+ limit: 5e3,
741
+ window: 3600,
742
+ // 5000 requests per hour
743
+ burst: 6e3,
744
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
745
+ keyStrategy: "user" /* USER */
746
+ };
747
+ var PLAN_ENTERPRISE = {
748
+ name: "enterprise_plan",
749
+ limit: 2e4,
750
+ window: 3600,
751
+ // 20000 requests per hour
752
+ burst: 25e3,
753
+ algorithm: "token_bucket" /* TOKEN_BUCKET */,
754
+ keyStrategy: "user" /* USER */
755
+ };
756
+ var PLAN_TIERS = {
757
+ free: PLAN_FREE,
758
+ starter: PLAN_STARTER,
759
+ pro: PLAN_PRO,
760
+ business: PLAN_BUSINESS,
761
+ enterprise: PLAN_ENTERPRISE
762
+ };
763
+ function getPlanPolicy(planName) {
764
+ const normalized = planName.toLowerCase();
765
+ if (!(normalized in PLAN_TIERS)) {
766
+ throw new Error(
767
+ `Invalid plan: ${planName}. Valid plans: ${Object.keys(PLAN_TIERS).join(", ")}`
768
+ );
769
+ }
770
+ return PLAN_TIERS[normalized];
771
+ }
772
+
773
+ exports.Algorithm = Algorithm;
774
+ exports.InMemoryStore = InMemoryStore;
775
+ exports.KeyStrategy = KeyStrategy;
776
+ exports.RateLimiter = RateLimiter;
777
+ exports.extractors = extractors_exports;
778
+ exports.normalizePolicy = normalizePolicy;
779
+ exports.presets = presets_exports;
780
+ exports.toHeaders = toHeaders;
781
+ //# sourceMappingURL=index.js.map
782
+ //# sourceMappingURL=index.js.map