@extend-therapy/elysia-db-ratelimiter 0.0.7 → 0.0.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog for @extend-therapy/elysia-db-ratelimiter
2
2
 
3
+ ## 0.0.8
4
+
5
+ - simplify package.json
6
+ - add more tests
7
+ - add encryption to cookie
8
+ - make encryption or hashing optional and called cookieObfuscation strategy
9
+ - add alwaysCheckCookieValue option to check if the cookie value matches the IP address (set to true for `none` strategy)
10
+ - add cookie and ratelimit key transfer on IP change
11
+ - add cookie and ratelimit key transfer on invalid cookie value (if cookie is invalid, we generate a new key and transfer the cookie and ratelimit key to the new key)
12
+
3
13
  ## 0.0.7
4
14
 
5
15
  - add declarationDir to tsconfig.json and types to package.json
package/README.md CHANGED
@@ -37,7 +37,7 @@ const app = new Elysia()
37
37
 
38
38
  ### Using Redis
39
39
 
40
- Inject your Redis client directly into the options.
40
+ Inject your Redis client directly into the options. Or use a global redis client for all instances of dbRateLimiter.
41
41
 
42
42
  ```typescript
43
43
  import { Elysia } from 'elysia';
@@ -75,6 +75,57 @@ const app = new Elysia()
75
75
  | `message` | `string` | `'Too many requests'` | Response body on limit exceed. |
76
76
  | `seed` | `string` | `undefined` | Optional seed for ID generation. |
77
77
  | `loggerOptions` | `LoggerOptions` | `undefined` | Custom Pino logger configuration. |
78
+ | `cookieObfuscation` | `'aes-gcm' \| 'hash' \| 'none'` | `'aes-gcm'` | Cookie value obfuscation strategy. |
79
+ | `alwaysCheckCookieValue` | `boolean` | `false` (except `none`) | Verifies cookie matches IP. Transfers count if mismatch. |
80
+
81
+ ## IP Privacy & Cookie Obfuscation
82
+
83
+ The plugin uses a cookie-based identification system to track rate limits. To protect user privacy, the `rateLimitCookie` value is obfuscated using one of three strategies:
84
+
85
+ ### Obfuscation Strategies
86
+
87
+ | Strategy | Description | Use Case |
88
+ | :--- | :--- | :--- |
89
+ | `aes-gcm` (Default) | AES-256-GCM encryption. Reversible and most secure. | Production (recommended) |
90
+ | `hash` | SHA-256 hashing. One-way function, cannot retrieve original value. | When you don't need to recover the original IP |
91
+ | `none` | No obfuscation. Plaintext storage. | Development/testing only |
92
+
93
+ ### Configuration of Cookie Obfuscation
94
+
95
+ ```typescript
96
+ app.use(dbRateLimiter({
97
+ cookieObfuscation: 'hash', // Use hashing instead of encryption
98
+ limit: 100,
99
+ window: 60 * 1000
100
+ }));
101
+ ```
102
+
103
+ ### Cookie Verification (`alwaysCheckCookieValue`)
104
+
105
+ When enabled, the plugin verifies that the cookie value matches the current IP address. This is especially important for the `none` strategy (where it defaults to `true`) to prevent users from tampering with their cookies since it is easy to know the value and meaning of their cookie.
106
+
107
+ **Behavior when cookie doesn't match IP (and `alwaysCheckCookieValue` is true):**
108
+
109
+ 1. The rate limit count is transferred from the old cookie to a new one
110
+ 2. A new cookie is set with the current IP's processed value
111
+ 3. The user continues with their existing count (not reset to zero)
112
+
113
+ ```typescript
114
+ app.use(dbRateLimiter({
115
+ cookieObfuscation: 'none',
116
+ alwaysCheckCookieValue: true, // Defaults to true for 'none' strategy
117
+ limit: 100,
118
+ window: 60 * 1000
119
+ }));
120
+ ```
121
+
122
+ ### Environment Variables
123
+
124
+ When using `hash` strategy, you should set environment variable to ensure cookies remain valid across server restarts:
125
+
126
+ - **`DB_RATE_LIMIT_HASH_PADDING`** (for `hash`): Set to any string value used as padding before hashing. If not provided, a random 16-byte to 32-byte padding is generated and cookies will be invalidated on restart.
127
+
128
+ **Security Note**: If this environment variable is not set, the plugin generates random transient values. This ensures security but means all existing rate limit cookies will be invalidated whenever the server restarts.
78
129
 
79
130
  ## Identification Patterns (`pattern`)
80
131
 
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './src/dbRateLimiter';
2
2
  export * from './src/dbRateLimitHandler';
3
+ export * from './src/dbRateLimitEncrypt';
3
4
  export * from './src/defaults';
4
5
  export * from './src/helpers/redisStore';
5
6
  export * from './src/helpers/sqliteStore';
@@ -0,0 +1,97 @@
1
+ import { CryptoHasher } from "bun";
2
+ // No top-level guard here to allow for lazy-loaded fallback key if env is missing
3
+ // Lazy load the hash padding
4
+ let _hashPadding = null;
5
+ const getHashPadding = () => {
6
+ if (!_hashPadding) {
7
+ const envPadding = Bun.env.DB_RATE_LIMIT_HASH_PADDING;
8
+ if (envPadding) {
9
+ _hashPadding = envPadding;
10
+ }
11
+ else {
12
+ // Generate a random padding if not provided
13
+ console.info("INFO: DB_RATE_LIMIT_HASH_PADDING environment variable is not set. Using transient random padding. Hashed cookies will be invalidated on server restart.");
14
+ const randomSize = Math.floor(Math.random() * (32 - 16)) + 16;
15
+ _hashPadding = Buffer.from(crypto.getRandomValues(new Uint8Array(randomSize))).toString("base64");
16
+ }
17
+ }
18
+ return _hashPadding;
19
+ };
20
+ export const resetKey = () => {
21
+ _hashPadding = null;
22
+ };
23
+ /**
24
+ * Hash a value using CryptoHasher("sha512-256") with padding
25
+ * This is a one-way function - the original value cannot be retrieved
26
+ */
27
+ export const dbRateLimitHash = async (value) => {
28
+ const padding = getHashPadding();
29
+ // Prepend padding to the value before hashing
30
+ const hasher = new CryptoHasher("sha512-256");
31
+ hasher.update(padding + value);
32
+ return hasher.digest("base64");
33
+ };
34
+ /**
35
+ * Process a cookie value based on the obfuscation strategy
36
+ * @param value - The value to process
37
+ * @param strategy - The obfuscation strategy to use
38
+ * @returns The processed value (always base64 encoded)
39
+ */
40
+ export const processCookieValue = async (value, strategy) => {
41
+ switch (strategy) {
42
+ case "none":
43
+ return Buffer.from(value).toString("base64");
44
+ case "hash":
45
+ default: {
46
+ return dbRateLimitHash(value);
47
+ }
48
+ }
49
+ };
50
+ /**
51
+ * Validate that a value is valid base64 without extracting/decoding it
52
+ * @param value - The value to validate
53
+ * @returns Whether the value is valid base64
54
+ */
55
+ export const isValidBase64 = (value) => {
56
+ try {
57
+ // Check if it's valid base64 by attempting to create a buffer
58
+ // This will throw if the string is not valid base64
59
+ const buffer = Buffer.from(value, "base64");
60
+ // Verify round-trip to ensure it's actually valid base64
61
+ return buffer.toString("base64") === value;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ };
67
+ /**
68
+ * Verify that a cookie value matches the provided IP address
69
+ * @param cookieValue - The value from the cookie (base64 encoded)
70
+ * @param ip - The IP address to verify against
71
+ * @param strategy - The obfuscation strategy used
72
+ * @returns Whether the cookie value is valid for this IP
73
+ */
74
+ export const verifyCookieValue = async (cookieValue, ip, strategy) => {
75
+ switch (strategy) {
76
+ case "none": {
77
+ try {
78
+ const decoded = Buffer.from(cookieValue, "base64").toString("utf8");
79
+ return decoded === ip;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ case "hash":
86
+ default: {
87
+ try {
88
+ const expectedHash = await dbRateLimitHash(ip);
89
+ return cookieValue === expectedHash;
90
+ }
91
+ catch {
92
+ // Verification failed, cookie is invalid
93
+ return false;
94
+ }
95
+ }
96
+ }
97
+ };
@@ -1,7 +1,11 @@
1
1
  import { InternalServerError } from "elysia";
2
2
  import { getIP } from "elysia-ip";
3
+ import { isValidBase64, processCookieValue, verifyCookieValue } from "./dbRateLimitEncrypt";
3
4
  export const dbRateLimitHandler = (options) => {
4
5
  return async ({ cookie, log, path, request, set, query, ..._rest }) => {
6
+ const cookieObfuscation = options.cookieObfuscation || "hash";
7
+ // Default to true for 'none' strategy, false for others if alwaysCheckCookieValue is not explicitly set
8
+ const alwaysCheckCookieValue = options.alwaysCheckCookieValue ?? cookieObfuscation === "none";
5
9
  let currentLimit = options.limit;
6
10
  let currentWindow = options.window;
7
11
  let currentPattern = options.pattern;
@@ -33,48 +37,143 @@ export const dbRateLimitHandler = (options) => {
33
37
  currentPattern = pathConfig.pattern;
34
38
  }
35
39
  }
36
- let baseId = cookie.rateLimitCookie?.value;
37
- if (!baseId) {
40
+ let cookieValue = cookie.rateLimitCookie?.value;
41
+ let oldCookieValue;
42
+ // Helper function to get IP lazily
43
+ const getClientIP = () => {
38
44
  const ip = getIP(request.headers);
39
- if (!ip) {
40
- // In test environment, use a default IP address to avoid log noise
41
- const isTest = process.env.NODE_ENV === "test" || process.env.isTest === "true";
42
- if (isTest) {
43
- baseId = "127.0.0.1";
44
- log.debug("Using default test IP for rate limiting");
45
+ if (ip)
46
+ return ip;
47
+ // In test environment, use a default IP address to avoid log noise
48
+ const isTest = Bun.env.NODE_ENV === "test" || Bun.env.isTest === "true";
49
+ if (isTest) {
50
+ log.warn("Using default test IP for rate limiting");
51
+ return "127.0.0.1";
52
+ }
53
+ const errorMsg = "Could not get IP address for rate limiting";
54
+ log.error(errorMsg);
55
+ if (options.failOpen === false) {
56
+ throw new InternalServerError(errorMsg);
57
+ }
58
+ // create a random id for their cookie and we'll try to use that if it's there
59
+ return Bun.randomUUIDv7("base64url").replaceAll("-", "");
60
+ };
61
+ // The rate limit identifier is always the cookie value itself
62
+ // For 'hash': the cookie contains the hash, which IS the identifier
63
+ // For 'none': the cookie contains base64(IP), which IS the identifier
64
+ let rateLimitIdentifier;
65
+ if (!cookieValue) {
66
+ // No cookie exists - get IP and create new identifier
67
+ const ip = getClientIP();
68
+ rateLimitIdentifier = await processCookieValue(ip, cookieObfuscation);
69
+ }
70
+ else {
71
+ // Cookie exists - validate it and use it directly as the identifier
72
+ let isValid = false;
73
+ if (cookieObfuscation === "hash") {
74
+ // For hash, verify by hashing current IP and comparing (if alwaysCheckCookieValue)
75
+ // Otherwise trust the cookie value
76
+ if (alwaysCheckCookieValue) {
77
+ const ip = getClientIP();
78
+ isValid = await verifyCookieValue(cookieValue, ip, cookieObfuscation);
79
+ if (!isValid) {
80
+ oldCookieValue = cookieValue;
81
+ rateLimitIdentifier = await processCookieValue(ip, cookieObfuscation);
82
+ }
83
+ else {
84
+ rateLimitIdentifier = cookieValue;
85
+ }
45
86
  }
46
87
  else {
47
- const errorMsg = "Could not get IP address for rate limiting";
48
- log.error(errorMsg);
49
- if (options.failOpen === false) {
50
- throw new InternalServerError(errorMsg);
51
- }
52
- return;
88
+ isValid = true;
89
+ rateLimitIdentifier = cookieValue;
53
90
  }
54
91
  }
55
92
  else {
56
- baseId = ip;
93
+ // For 'none' obfuscation
94
+ // First validate it's valid base64
95
+ isValid = isValidBase64(cookieValue);
96
+ if (isValid) {
97
+ if (alwaysCheckCookieValue) {
98
+ const ip = getClientIP();
99
+ const expectedValue = await processCookieValue(ip, cookieObfuscation);
100
+ if (cookieValue !== expectedValue) {
101
+ oldCookieValue = cookieValue;
102
+ rateLimitIdentifier = expectedValue;
103
+ }
104
+ else {
105
+ rateLimitIdentifier = cookieValue;
106
+ }
107
+ }
108
+ else {
109
+ rateLimitIdentifier = cookieValue;
110
+ }
111
+ }
112
+ else {
113
+ // Invalid base64 - treat as new cookie
114
+ oldCookieValue = cookieValue;
115
+ const ip = getClientIP();
116
+ rateLimitIdentifier = await processCookieValue(ip, cookieObfuscation);
117
+ }
118
+ }
119
+ }
120
+ // Set the new cookie value (which is the rate limit identifier)
121
+ if (cookie.rateLimitCookie) {
122
+ cookie.rateLimitCookie.value = rateLimitIdentifier;
123
+ }
124
+ // If we have an old cookie value, transfer the rate limit to the new identifier
125
+ if (oldCookieValue && options.rateLimitStore) {
126
+ try {
127
+ const queryStr = new URLSearchParams(query).toString();
128
+ // Build the old rate limit ID (using old identifier)
129
+ const oldRateLimitId = `${oldCookieValue}:${path}${queryStr ? "?" + queryStr : ""}`;
130
+ const oldRateLimit = await options.rateLimitStore.get(oldRateLimitId);
131
+ if (oldRateLimit) {
132
+ // Build the new rate limit ID
133
+ let newRateLimitId;
134
+ switch (currentPattern) {
135
+ case "IP":
136
+ newRateLimitId = rateLimitIdentifier;
137
+ break;
138
+ case "Route":
139
+ newRateLimitId = path;
140
+ break;
141
+ case "IPRouteNoParams":
142
+ newRateLimitId = `${rateLimitIdentifier}:${path}`;
143
+ break;
144
+ case "IPFullRoute":
145
+ default:
146
+ newRateLimitId = `${rateLimitIdentifier}:${path}${queryStr ? "?" + queryStr : ""}`;
147
+ }
148
+ // Transfer the rate limit count and reset time
149
+ const now = Date.now();
150
+ await options.rateLimitStore.set(newRateLimitId, {
151
+ count: oldRateLimit.count,
152
+ resetTime: oldRateLimit.resetTime,
153
+ }, Math.ceil((oldRateLimit.resetTime - now) / 1000));
154
+ log.debug(`Transferred rate limit from ${oldCookieValue} to new identifier`);
155
+ }
57
156
  }
58
- if (cookie.rateLimitCookie && baseId) {
59
- cookie.rateLimitCookie.value = baseId;
157
+ catch (e) {
158
+ log.warn(`Failed to transfer rate limit from old cookie: ${e}`);
60
159
  }
61
160
  }
161
+ // Build the final rate limit ID using the consistent identifier
62
162
  let finalRateLimitId;
63
- const safeBaseId = baseId;
64
163
  switch (currentPattern) {
65
164
  case "IP":
66
- finalRateLimitId = safeBaseId;
165
+ finalRateLimitId = rateLimitIdentifier;
67
166
  break;
68
167
  case "Route":
69
168
  finalRateLimitId = path;
70
169
  break;
71
170
  case "IPRouteNoParams":
72
- finalRateLimitId = `${safeBaseId}:${path}`;
171
+ finalRateLimitId = `${rateLimitIdentifier}:${path}`;
73
172
  break;
74
173
  case "IPFullRoute":
75
174
  default: {
76
175
  const queryStr = new URLSearchParams(query).toString();
77
- finalRateLimitId = `${safeBaseId}:${path}${queryStr ? "?" + queryStr : ""}`;
176
+ finalRateLimitId = `${rateLimitIdentifier}:${path}${queryStr ? "?" + queryStr : ""}`;
78
177
  break;
79
178
  }
80
179
  }
@@ -11,4 +11,5 @@ export const defaultOptions = {
11
11
  window: 60 * 1000, // 1 minute
12
12
  failOpen: true,
13
13
  whitelistMode: false,
14
+ cookieObfuscation: "hash",
14
15
  };
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { dbRateLimitHash, isValidBase64, processCookieValue, resetKey, } from "./dbRateLimitEncrypt";
3
+ describe("dbRateLimitEncrypt", () => {
4
+ beforeEach(() => {
5
+ resetKey();
6
+ });
7
+ describe("dbRateLimitHash", () => {
8
+ it("should produce consistent hashes for the same input", async () => {
9
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "test-padding-value";
10
+ const value = "127.0.0.1";
11
+ const hash1 = await dbRateLimitHash(value);
12
+ const hash2 = await dbRateLimitHash(value);
13
+ expect(hash1).toBe(hash2);
14
+ expect(typeof hash1).toBe("string");
15
+ expect(hash1.length).toBeGreaterThan(0);
16
+ delete Bun.env.DB_RATE_LIMIT_HASH_PADDING;
17
+ });
18
+ it("should produce different hashes for different inputs", async () => {
19
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "test-padding-value";
20
+ const hash1 = await dbRateLimitHash("127.0.0.1");
21
+ const hash2 = await dbRateLimitHash("127.0.0.2");
22
+ expect(hash1).not.toBe(hash2);
23
+ delete Bun.env.DB_RATE_LIMIT_HASH_PADDING;
24
+ });
25
+ it("should use DB_RATE_LIMIT_HASH_PADDING when provided", async () => {
26
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "my-secret-padding";
27
+ const value = "127.0.0.1";
28
+ const hash = await dbRateLimitHash(value);
29
+ // Reset and use same padding again
30
+ resetKey();
31
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "my-secret-padding";
32
+ const hash2 = await dbRateLimitHash(value);
33
+ expect(hash).toBe(hash2);
34
+ // Different padding should produce different hash
35
+ resetKey();
36
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "different-padding";
37
+ const hash3 = await dbRateLimitHash(value);
38
+ expect(hash).not.toBe(hash3);
39
+ delete Bun.env.DB_RATE_LIMIT_HASH_PADDING;
40
+ });
41
+ });
42
+ describe("processCookieValue", () => {
43
+ it("should return base64 encoded value for 'none' strategy", async () => {
44
+ const value = "127.0.0.1";
45
+ const result = await processCookieValue(value, "none");
46
+ expect(result).toBe(Buffer.from(value).toString("base64"));
47
+ });
48
+ it("should hash value for 'hash' strategy", async () => {
49
+ Bun.env.DB_RATE_LIMIT_HASH_PADDING = "test-padding";
50
+ const value = "127.0.0.1";
51
+ const result = await processCookieValue(value, "hash");
52
+ expect(result).not.toBe(value);
53
+ // Hash should be consistent
54
+ const result2 = await processCookieValue(value, "hash");
55
+ expect(result).toBe(result2);
56
+ delete Bun.env.DB_RATE_LIMIT_HASH_PADDING;
57
+ });
58
+ });
59
+ describe("isValidBase64", () => {
60
+ it("should return true for valid base64 strings", () => {
61
+ const value = "127.0.0.1";
62
+ const encoded = Buffer.from(value).toString("base64");
63
+ expect(isValidBase64(encoded)).toBe(true);
64
+ });
65
+ it("should return false for invalid base64 strings", () => {
66
+ expect(isValidBase64("not-valid-base64!!!")).toBe(false);
67
+ expect(isValidBase64("hello world")).toBe(false);
68
+ });
69
+ it("should return true for empty base64 string", () => {
70
+ expect(isValidBase64("")).toBe(true);
71
+ });
72
+ it("should return true for base64 encoded binary data", () => {
73
+ const binary = Buffer.from([0x00, 0x01, 0x02, 0x03]);
74
+ const encoded = binary.toString("base64");
75
+ expect(isValidBase64(encoded)).toBe(true);
76
+ });
77
+ });
78
+ });
@@ -17,7 +17,7 @@ export class RedisRateLimitStore {
17
17
  async set(key, value, ttlSeconds) {
18
18
  if (!this.client)
19
19
  return;
20
- await this.client.set(`rl:${key}`, JSON.stringify(value), 'EX', ttlSeconds);
20
+ await this.client.set(`rl:${key}`, JSON.stringify(value), "EX", ttlSeconds);
21
21
  }
22
22
  /**
23
23
  * Clears all rate limit data from Redis by deleting keys with rl: prefix
@@ -25,7 +25,7 @@ export class RedisRateLimitStore {
25
25
  async reset() {
26
26
  if (!this.client)
27
27
  return;
28
- const keys = await this.client.keys('rl:*');
28
+ const keys = await this.client.keys("rl:*");
29
29
  if (keys && keys.length > 0) {
30
30
  await this.client.del(...keys);
31
31
  }
@@ -1,5 +1,6 @@
1
1
  export * from './src/dbRateLimiter';
2
2
  export * from './src/dbRateLimitHandler';
3
+ export * from './src/dbRateLimitEncrypt';
3
4
  export * from './src/defaults';
4
5
  export * from './src/helpers/redisStore';
5
6
  export * from './src/helpers/sqliteStore';
@@ -0,0 +1,28 @@
1
+ import type { DBRLCookieObfuscation } from "./types";
2
+ export declare const resetKey: () => void;
3
+ /**
4
+ * Hash a value using CryptoHasher("sha512-256") with padding
5
+ * This is a one-way function - the original value cannot be retrieved
6
+ */
7
+ export declare const dbRateLimitHash: (value: string) => Promise<string>;
8
+ /**
9
+ * Process a cookie value based on the obfuscation strategy
10
+ * @param value - The value to process
11
+ * @param strategy - The obfuscation strategy to use
12
+ * @returns The processed value (always base64 encoded)
13
+ */
14
+ export declare const processCookieValue: (value: string, strategy: DBRLCookieObfuscation) => Promise<string>;
15
+ /**
16
+ * Validate that a value is valid base64 without extracting/decoding it
17
+ * @param value - The value to validate
18
+ * @returns Whether the value is valid base64
19
+ */
20
+ export declare const isValidBase64: (value: string) => boolean;
21
+ /**
22
+ * Verify that a cookie value matches the provided IP address
23
+ * @param cookieValue - The value from the cookie (base64 encoded)
24
+ * @param ip - The IP address to verify against
25
+ * @param strategy - The obfuscation strategy used
26
+ * @returns Whether the cookie value is valid for this IP
27
+ */
28
+ export declare const verifyCookieValue: (cookieValue: string, ip: string, strategy: DBRLCookieObfuscation) => Promise<boolean>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { RateLimitStore, RateLimitStoreValue } from '../types';
1
+ import type { RateLimitStore, RateLimitStoreValue } from "../types";
2
2
  export declare class RedisRateLimitStore implements RateLimitStore<Bun.RedisClient> {
3
3
  client: Bun.RedisClient;
4
4
  constructor(client: Bun.RedisClient);
@@ -23,6 +23,12 @@ export type DBRLPattern = "IPFullRoute" | "IPRouteNoParams" | "IP" | "Route";
23
23
  * - 'auto': Try Redis first, fallback to SQLite (default behavior)
24
24
  */
25
25
  export type DBRLBackingDb = "redis" | "sqlite" | "auto";
26
+ /**
27
+ * Strategy for cookie value obfuscation (encryption/hashing).
28
+ * - 'hash': Hash the cookie value using sha512-256 (one-way, cannot retrieve original value)
29
+ * - 'none': Store cookie value as base64 encoded IP (reversible, not recommended for production)
30
+ */
31
+ export type DBRLCookieObfuscation = "hash" | "none";
26
32
  /**
27
33
  * Data structure stored in the rate limit store.
28
34
  */
@@ -106,9 +112,20 @@ export type DBRLOptions = {
106
112
  */
107
113
  whitelistMode?: boolean;
108
114
  /**
109
- * Whether to allow the request if the rate limiter encounters an internal error (Default: true).
110
-
111
- * Set to false for a more strict security posture.
112
- */
115
+ * Cookie obfuscation strategy (Default: 'hash').
116
+ * - 'hash': Hash using sha512-256 (one-way, cannot retrieve original value)
117
+ * - 'none': No obfuscation (base64 encoded IP, not recommended for production)
118
+ */
119
+ cookieObfuscation?: DBRLCookieObfuscation;
120
+ /**
121
+ * Whether to verify the cookie value matches the current IP address (Default: false for 'hash', true for 'none').
122
+ * When true and the cookie value doesn't match the IP, the rate limit count is transferred to a new cookie.
123
+ */
124
+ alwaysCheckCookieValue?: boolean;
125
+ /**
126
+ * Whether to allow the request if the rate limiter encounters an internal error (Default: true).
127
+
128
+ * Set to false for a more strict security posture.
129
+ */
113
130
  failOpen?: boolean;
114
131
  };
package/package.json CHANGED
@@ -6,27 +6,25 @@
6
6
  },
7
7
  "private": false,
8
8
  "type": "module",
9
- "version": "0.0.7",
9
+ "version": "0.0.8",
10
10
  "author": "Eli Selkin <eli@extendtherapy.com>",
11
11
  "license": "MIT",
12
12
  "main": "dist/index.js",
13
13
  "types": "dist/types/index.d.ts",
14
14
  "devDependencies": {
15
- "@types/bun": "latest"
15
+ "@types/bun": "^1.3.9",
16
+ "typescript": "^5"
16
17
  },
17
18
  "scripts": {
18
19
  "prepublishOnly": "rm -rf dist || true && bunx tsc"
19
20
  },
20
21
  "peerDependencies": {
21
- "@types/bun": "^1.3.0",
22
- "typescript": "^5",
23
22
  "elysia": "^1.4.22",
24
23
  "pino": "^10.0.0",
25
24
  "elysia-ip": "^1.0.0"
26
25
  },
27
26
  "files": [
28
27
  "dist/**/*",
29
- "AGENTS.md",
30
28
  "README.md",
31
29
  "package.json",
32
30
  "LICENSE",
package/AGENTS.md DELETED
@@ -1,51 +0,0 @@
1
- # About the project
2
-
3
- - This is a Bun project so ALWAYS use `bun` or `bunx` instead of `node`, `npm`, or `npx`!
4
- - This is an [Elysia](https://elysiajs.com/) [plugin](https://elysiajs.com/essential/plugin.html).
5
- - All Elysia methods (like `derive` or `macro` or `use`) should be chained and not separated into a declaration or assignment and then applied to the variable.
6
- For example:
7
-
8
- Do **NOT** do this:
9
-
10
- ```ts
11
- const elysiaplugin = new Elysia();
12
- elysiaplugin.use(someplugin);
13
- elysiaplugin.resolve((ctx) => {
14
- return { something: "abc" };
15
- });
16
- ```
17
-
18
- **DO THIS** instead:
19
-
20
- ```ts
21
- const elysiaplugin = new Elysia().use(someplugin).resolve((ctx) => {
22
- return { somethiong: "abc" };
23
- });
24
- ```
25
-
26
- ## Commands
27
-
28
- - Use `bun test` instead of `jest` or `vitest`
29
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
30
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
31
- - Bun automatically loads .env, so don't use dotenv.
32
-
33
- ## APIs
34
-
35
- - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
36
- - `Bun.redis` for Redis. Don't use `ioredis`.
37
- - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
38
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
39
- - Bun.$`ls` instead of execa.
40
-
41
- ## Testing
42
-
43
- Use `bun test` to run tests.
44
-
45
- ```ts#index.test.ts
46
- import { test, expect } from "bun:test";
47
-
48
- test("hello world", () => {
49
- expect(1).toBe(1);
50
- });
51
- ```