ioredis-toolkit 0.0.9 → 0.5.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +1 -1
  3. package/README.md +68 -1058
  4. package/dist/cache/cache.d.ts +30 -0
  5. package/dist/cache/cache.d.ts.map +1 -0
  6. package/dist/cache/cache.js +59 -0
  7. package/dist/cache/cache.js.map +1 -0
  8. package/dist/cache/config.d.ts +12 -0
  9. package/dist/cache/config.d.ts.map +1 -0
  10. package/dist/cache/config.js +13 -0
  11. package/dist/cache/config.js.map +1 -0
  12. package/dist/cache/types.d.ts +32 -0
  13. package/dist/cache/types.d.ts.map +1 -0
  14. package/dist/cache/types.js +5 -0
  15. package/dist/cache/types.js.map +1 -0
  16. package/dist/index.d.ts +43 -51
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +32 -44
  19. package/dist/index.js.map +1 -0
  20. package/dist/lock/config.d.ts +12 -0
  21. package/dist/lock/config.d.ts.map +1 -0
  22. package/dist/lock/config.js +8 -0
  23. package/dist/lock/config.js.map +1 -0
  24. package/dist/lock/lock.d.ts +20 -0
  25. package/dist/lock/lock.d.ts.map +1 -0
  26. package/dist/lock/lock.js +44 -0
  27. package/dist/lock/lock.js.map +1 -0
  28. package/dist/lock/types.d.ts +19 -0
  29. package/dist/lock/types.d.ts.map +1 -0
  30. package/dist/lock/types.js +2 -0
  31. package/dist/lock/types.js.map +1 -0
  32. package/dist/modules-config.d.ts +3 -0
  33. package/dist/modules-config.d.ts.map +1 -0
  34. package/dist/modules-config.js +2 -0
  35. package/dist/modules-config.js.map +1 -0
  36. package/dist/pubsub/config.d.ts +11 -0
  37. package/dist/pubsub/config.d.ts.map +1 -0
  38. package/dist/pubsub/config.js +6 -0
  39. package/dist/pubsub/config.js.map +1 -0
  40. package/dist/pubsub/pubsub.d.ts +20 -0
  41. package/dist/pubsub/pubsub.d.ts.map +1 -0
  42. package/dist/pubsub/pubsub.js +54 -0
  43. package/dist/pubsub/pubsub.js.map +1 -0
  44. package/dist/pubsub/types.d.ts +24 -0
  45. package/dist/pubsub/types.d.ts.map +1 -0
  46. package/dist/pubsub/types.js +2 -0
  47. package/dist/pubsub/types.js.map +1 -0
  48. package/dist/rate-limit/config.d.ts +12 -0
  49. package/dist/rate-limit/config.d.ts.map +1 -0
  50. package/dist/rate-limit/config.js +6 -0
  51. package/dist/rate-limit/config.js.map +1 -0
  52. package/dist/rate-limit/rate-limiter.d.ts +18 -0
  53. package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
  54. package/dist/rate-limit/rate-limiter.js +37 -0
  55. package/dist/rate-limit/rate-limiter.js.map +1 -0
  56. package/dist/rate-limit/types.d.ts +27 -0
  57. package/dist/rate-limit/types.d.ts.map +1 -0
  58. package/dist/rate-limit/types.js +2 -0
  59. package/dist/rate-limit/types.js.map +1 -0
  60. package/dist/redis/client-facade.d.ts +77 -0
  61. package/dist/redis/client-facade.d.ts.map +1 -0
  62. package/dist/redis/client-facade.js +102 -0
  63. package/dist/redis/client-facade.js.map +1 -0
  64. package/dist/redis/client.d.ts +10 -0
  65. package/dist/redis/client.d.ts.map +1 -0
  66. package/dist/redis/client.js +29 -0
  67. package/dist/redis/client.js.map +1 -0
  68. package/dist/redis/cluster.d.ts +7 -0
  69. package/dist/redis/cluster.d.ts.map +1 -0
  70. package/dist/redis/cluster.js +47 -0
  71. package/dist/redis/cluster.js.map +1 -0
  72. package/dist/redis/config.d.ts +39 -0
  73. package/dist/redis/config.d.ts.map +1 -0
  74. package/dist/redis/config.js +52 -0
  75. package/dist/redis/config.js.map +1 -0
  76. package/dist/redis/errors.d.ts +5 -0
  77. package/dist/redis/errors.d.ts.map +1 -0
  78. package/dist/redis/errors.js +5 -0
  79. package/dist/redis/errors.js.map +1 -0
  80. package/dist/redis/types.d.ts +135 -0
  81. package/dist/redis/types.d.ts.map +1 -0
  82. package/dist/redis/types.js +2 -0
  83. package/dist/redis/types.js.map +1 -0
  84. package/dist/redis/wrapper.d.ts +88 -0
  85. package/dist/redis/wrapper.d.ts.map +1 -0
  86. package/dist/redis/wrapper.js +206 -0
  87. package/dist/redis/wrapper.js.map +1 -0
  88. package/dist/session/config.d.ts +47 -0
  89. package/dist/session/config.d.ts.map +1 -0
  90. package/dist/session/config.js +101 -0
  91. package/dist/session/config.js.map +1 -0
  92. package/dist/session/cookie.d.ts +16 -0
  93. package/dist/session/cookie.d.ts.map +1 -0
  94. package/dist/session/cookie.js +28 -0
  95. package/dist/session/cookie.js.map +1 -0
  96. package/dist/session/errors.d.ts +56 -0
  97. package/dist/session/errors.d.ts.map +1 -0
  98. package/dist/session/errors.js +58 -0
  99. package/dist/session/errors.js.map +1 -0
  100. package/dist/session/factory.d.ts +21 -0
  101. package/dist/session/factory.d.ts.map +1 -0
  102. package/dist/session/factory.js +30 -0
  103. package/dist/session/factory.js.map +1 -0
  104. package/dist/session/health.d.ts +12 -0
  105. package/dist/session/health.d.ts.map +1 -0
  106. package/dist/session/health.js +23 -0
  107. package/dist/session/health.js.map +1 -0
  108. package/dist/session/keys.d.ts +23 -0
  109. package/dist/session/keys.d.ts.map +1 -0
  110. package/dist/session/keys.js +27 -0
  111. package/dist/session/keys.js.map +1 -0
  112. package/dist/session/manager.d.ts +34 -0
  113. package/dist/session/manager.d.ts.map +1 -0
  114. package/dist/session/manager.js +31 -0
  115. package/dist/session/manager.js.map +1 -0
  116. package/dist/session/metrics.d.ts +11 -0
  117. package/dist/session/metrics.d.ts.map +1 -0
  118. package/dist/session/metrics.js +10 -0
  119. package/dist/session/metrics.js.map +1 -0
  120. package/dist/session/repository.d.ts +49 -0
  121. package/dist/session/repository.d.ts.map +1 -0
  122. package/dist/session/repository.js +203 -0
  123. package/dist/session/repository.js.map +1 -0
  124. package/dist/session/revocation.d.ts +22 -0
  125. package/dist/session/revocation.d.ts.map +1 -0
  126. package/dist/session/revocation.js +41 -0
  127. package/dist/session/revocation.js.map +1 -0
  128. package/dist/session/script-sources.d.ts +11 -0
  129. package/dist/session/script-sources.d.ts.map +1 -0
  130. package/dist/session/script-sources.js +140 -0
  131. package/dist/session/script-sources.js.map +1 -0
  132. package/dist/session/scripts.d.ts +15 -0
  133. package/dist/session/scripts.d.ts.map +1 -0
  134. package/dist/session/scripts.js +41 -0
  135. package/dist/session/scripts.js.map +1 -0
  136. package/dist/session/serializer.d.ts +12 -0
  137. package/dist/session/serializer.d.ts.map +1 -0
  138. package/dist/session/serializer.js +77 -0
  139. package/dist/session/serializer.js.map +1 -0
  140. package/dist/session/service.d.ts +48 -0
  141. package/dist/session/service.d.ts.map +1 -0
  142. package/dist/session/service.js +235 -0
  143. package/dist/session/service.js.map +1 -0
  144. package/dist/session/token.d.ts +16 -0
  145. package/dist/session/token.d.ts.map +1 -0
  146. package/dist/session/token.js +32 -0
  147. package/dist/session/token.js.map +1 -0
  148. package/dist/session/types.d.ts +134 -0
  149. package/dist/session/types.d.ts.map +1 -0
  150. package/dist/session/types.js +2 -0
  151. package/dist/session/types.js.map +1 -0
  152. package/dist/streams/config.d.ts +12 -0
  153. package/dist/streams/config.d.ts.map +1 -0
  154. package/dist/streams/config.js +6 -0
  155. package/dist/streams/config.js.map +1 -0
  156. package/dist/streams/streams.d.ts +24 -0
  157. package/dist/streams/streams.d.ts.map +1 -0
  158. package/dist/streams/streams.js +55 -0
  159. package/dist/streams/streams.js.map +1 -0
  160. package/dist/streams/types.d.ts +32 -0
  161. package/dist/streams/types.d.ts.map +1 -0
  162. package/dist/streams/types.js +2 -0
  163. package/dist/streams/types.js.map +1 -0
  164. package/docs/ACCEPTANCE-REPORT.md +70 -0
  165. package/docs/ARCHITECTURE.md +61 -0
  166. package/docs/CAPACITY.md +33 -0
  167. package/docs/DEPLOYMENT.md +22 -0
  168. package/docs/README-API.md +15 -0
  169. package/docs/STATE-MACHINE.md +38 -0
  170. package/docs/TESTING.md +37 -0
  171. package/docs/THREAT-MODEL.md +23 -0
  172. package/docs/TYPE-SAFETY.md +34 -0
  173. package/docs/modules/cache/README.md +7 -0
  174. package/docs/modules/cache/usage.md +156 -0
  175. package/docs/modules/lock/README.md +7 -0
  176. package/docs/modules/lock/usage.md +105 -0
  177. package/docs/modules/pubsub/README.md +7 -0
  178. package/docs/modules/pubsub/usage.md +106 -0
  179. package/docs/modules/rate-limit/README.md +7 -0
  180. package/docs/modules/rate-limit/usage.md +100 -0
  181. package/docs/modules/sessions/README.md +7 -0
  182. package/docs/modules/sessions/usage.md +262 -0
  183. package/docs/modules/streams/README.md +7 -0
  184. package/docs/modules/streams/usage.md +141 -0
  185. package/package.json +50 -60
  186. package/src/scripts/cleanup-index.lua +4 -0
  187. package/src/scripts/conditional-update.lua +21 -0
  188. package/src/scripts/consume-session.lua +21 -0
  189. package/src/scripts/create-session.lua +28 -0
  190. package/src/scripts/delete.lua +2 -0
  191. package/src/scripts/destroy-user.lua +13 -0
  192. package/src/scripts/enforce-limit.lua +17 -0
  193. package/src/scripts/revoke-session.lua +13 -0
  194. package/src/scripts/rotate.lua +24 -0
  195. package/src/scripts/touch-session.lua +28 -0
  196. package/src/scripts/update-session.lua +18 -0
  197. package/dist/cache.d.ts +0 -796
  198. package/dist/cache.js +0 -1120
  199. package/dist/client.d.ts +0 -284
  200. package/dist/client.js +0 -1114
  201. package/dist/cluster-slot.d.ts +0 -4
  202. package/dist/cluster-slot.js +0 -31
  203. package/dist/cluster.d.ts +0 -79
  204. package/dist/cluster.js +0 -156
  205. package/dist/errors.d.ts +0 -30
  206. package/dist/errors.js +0 -63
  207. package/dist/health.d.ts +0 -180
  208. package/dist/health.js +0 -239
  209. package/dist/lock.d.ts +0 -248
  210. package/dist/lock.js +0 -397
  211. package/dist/logger.d.ts +0 -12
  212. package/dist/logger.js +0 -40
  213. package/dist/pubsub.d.ts +0 -423
  214. package/dist/pubsub.js +0 -537
  215. package/dist/ratelimiter.d.ts +0 -441
  216. package/dist/ratelimiter.js +0 -539
  217. package/dist/session/index.d.ts +0 -23
  218. package/dist/session/index.js +0 -16
  219. package/dist/session/revocation-store.d.ts +0 -176
  220. package/dist/session/revocation-store.js +0 -318
  221. package/dist/session/scripts/cleanup-index.lua +0 -21
  222. package/dist/session/scripts/conditional-update-encrypted.lua +0 -60
  223. package/dist/session/scripts/conditional-update.lua +0 -63
  224. package/dist/session/scripts/create.lua +0 -83
  225. package/dist/session/scripts/delete-by-user.lua +0 -29
  226. package/dist/session/scripts/delete.lua +0 -15
  227. package/dist/session/scripts/enforce-limit.lua +0 -38
  228. package/dist/session/scripts/revoke.lua +0 -61
  229. package/dist/session/scripts/rotate-encrypted.lua +0 -110
  230. package/dist/session/scripts/rotate.lua +0 -122
  231. package/dist/session/scripts/touch-encrypted.lua +0 -89
  232. package/dist/session/scripts/touch.lua +0 -72
  233. package/dist/session/scripts/validate.lua +0 -90
  234. package/dist/session/session-circuit-breaker.d.ts +0 -42
  235. package/dist/session/session-circuit-breaker.js +0 -129
  236. package/dist/session/session-config.d.ts +0 -335
  237. package/dist/session/session-config.js +0 -162
  238. package/dist/session/session-cookie.d.ts +0 -72
  239. package/dist/session/session-cookie.js +0 -101
  240. package/dist/session/session-encryption.d.ts +0 -87
  241. package/dist/session/session-encryption.js +0 -139
  242. package/dist/session/session-errors.d.ts +0 -85
  243. package/dist/session/session-errors.js +0 -145
  244. package/dist/session/session-health.d.ts +0 -38
  245. package/dist/session/session-health.js +0 -60
  246. package/dist/session/session-keys.d.ts +0 -51
  247. package/dist/session/session-keys.js +0 -113
  248. package/dist/session/session-manager.d.ts +0 -73
  249. package/dist/session/session-manager.js +0 -94
  250. package/dist/session/session-metrics.d.ts +0 -33
  251. package/dist/session/session-metrics.js +0 -112
  252. package/dist/session/session-repository.d.ts +0 -161
  253. package/dist/session/session-repository.js +0 -683
  254. package/dist/session/session-scripts.d.ts +0 -36
  255. package/dist/session/session-scripts.js +0 -130
  256. package/dist/session/session-serializer.d.ts +0 -42
  257. package/dist/session/session-serializer.js +0 -248
  258. package/dist/session/session-service.d.ts +0 -104
  259. package/dist/session/session-service.js +0 -611
  260. package/dist/session/session-token.d.ts +0 -38
  261. package/dist/session/session-token.js +0 -86
  262. package/dist/session/session-types.d.ts +0 -253
  263. package/dist/session/session-types.js +0 -16
  264. package/dist/types.d.ts +0 -924
  265. package/dist/types.js +0 -151
  266. package/dist/utils/deepmerge.d.ts +0 -9
  267. package/dist/utils/deepmerge.js +0 -61
@@ -1,539 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { defaultLogger } from './logger.js';
3
- const CONSUME_SCRIPT = `
4
- local key = KEYS[1]
5
- local now = tonumber(ARGV[1])
6
- local window = tonumber(ARGV[2])
7
- local limit = tonumber(ARGV[3])
8
- local member = ARGV[4]
9
-
10
- redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
11
-
12
- local count = redis.call('ZCARD', key)
13
- if count >= limit then
14
- local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
15
- local retryAfter = 0
16
- if oldest[2] then
17
- retryAfter = math.max(1, math.ceil((tonumber(oldest[2]) + window - now) / 1000))
18
- end
19
- return { 0, count, -1, retryAfter }
20
- end
21
-
22
- redis.call('ZADD', key, now, member)
23
- redis.call('PEXPIRE', key, window)
24
- return { 1, count + 1, limit - count - 1, 0 }
25
- `;
26
- const PEEK_SCRIPT = `
27
- local key = KEYS[1]
28
- local now = tonumber(ARGV[1])
29
- local window = tonumber(ARGV[2])
30
- local limit = tonumber(ARGV[3])
31
-
32
- redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
33
-
34
- local count = redis.call('ZCARD', key)
35
- local retryAfter = 0
36
- if count >= limit then
37
- local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
38
- if oldest[2] then
39
- retryAfter = math.max(1, math.ceil((tonumber(oldest[2]) + window - now) / 1000))
40
- end
41
- end
42
- return { count, retryAfter }
43
- `;
44
- /**
45
- * Generates a "fail-open" result when Redis is unavailable.
46
- *
47
- * **Behavior:**
48
- * - When Redis errors occur during rate limit operations, the limiter "fails open"
49
- * (allows the request) to prevent an outage from taking down the whole application.
50
- * - This helper creates the result structure that would be returned in a fail-open scenario.
51
- *
52
- **Returns:**
53
- * - A {@link RateLimitResult} with `allowed: true` and default values.
54
- *
55
- * **Parameters:**
56
- * - `limit` - The configured maximum request count.
57
- * - `duration` - The window length in seconds.
58
- *
59
- * @internal
60
- */
61
- function failOpenResult(limit, duration) {
62
- return {
63
- allowed: true,
64
- limit,
65
- used: 0,
66
- remaining: limit,
67
- resetAt: Date.now() + duration * 1000,
68
- retryAfter: 0,
69
- };
70
- }
71
- /**
72
- * Generic Redis-backed rate limiter that works for any resource: routes, API
73
- * endpoints, users, IPs, databases, email sending, etc.
74
- *
75
- * Keys are namespaced as `ratelimit:{namespace}:{resource}:{identifier}` so each
76
- * resource + identifier combination is tracked independently. Supports fixed-window
77
- * (`INCR`/`EXPIRE`) and sliding-window (atomic Lua over a sorted set) algorithms.
78
- * Fails open when Redis is unavailable.
79
- *
80
- * @example
81
- * ```ts
82
- * const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
83
- *
84
- * const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
85
- * if (!result.allowed) {
86
- * throw new Error(`Slow down, retry in ${result.retryAfter}s`);
87
- * }
88
- * ```
89
- */
90
- export class RateLimiter {
91
- client;
92
- logger;
93
- defaultLimit;
94
- defaultDuration;
95
- defaultAlgorithm;
96
- defaultNamespace;
97
- /**
98
- * Creates a rate limiter bound to a Redis client.
99
- *
100
- * @param client - The underlying {@link RedisClientWrapper}.
101
- * @param options - Defaults applied when a call does not override them:
102
- * `limit` (default `100`), `duration` in seconds (default `60`),
103
- * `algorithm` (default `'sliding'`), `namespace` (default `'ratelimit'`).
104
- * @param logger - Optional pino-compatible logger; defaults to `console`.
105
- *
106
- * @example
107
- * ```ts
108
- * const limiter = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
109
- * ```
110
- */
111
- constructor(client, options = {}, logger = defaultLogger) {
112
- this.client = client;
113
- this.logger = logger.child({ component: 'RateLimiter' });
114
- this.defaultLimit = options.limit ?? 100;
115
- this.defaultDuration = options.duration ?? 60;
116
- this.defaultAlgorithm = options.algorithm ?? 'sliding';
117
- this.defaultNamespace = options.namespace ?? 'ratelimit';
118
- }
119
- /**
120
- * Creates a rate limiter bound to a Redis client.
121
- *
122
- * **Default Configuration:**
123
- * - `limit`: `100` requests per window
124
- * - `duration`: `60` seconds per window
125
- * - `algorithm`: `'sliding'` (precise rolling window)
126
- * - `namespace`: `'ratelimit'` key prefix
127
- *
128
- * **Example:**
129
- * ```ts
130
- * // Rate limit per route, per IP, 100 requests per 60 seconds (sliding window)
131
- * const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
132
- *
133
- * // Fixed window: 10 requests per 1 second
134
- * const fixed = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
135
- * ```
136
- *
137
- * **Parameters:**
138
- * - `client` - The underlying {@link RedisClientWrapper}. All rate limit operations
139
- * delegate to this client.
140
- * - `options` - Default rate limit settings. Overridden per-call via the `consume`
141
- * and `check` methods.
142
- * - `logger` - Optional pino-compatible logger. Defaults to `console`.
143
- */
144
- /**
145
- * Builds the Redis key for a resource + identifier combination.
146
- *
147
- * @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
148
- * a resource name `'email:send'`.
149
- * @param identifier - The caller identity, e.g. an IP, user id or API key.
150
- * @param namespace - Key prefix (defaults to the limiter's namespace).
151
- * @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
152
- *
153
- * @example
154
- * ```ts
155
- * limiter.makeKey('/api/login', 'ip-10.0.0.1');
156
- * // 'ratelimit:/api/login:ip-10.0.0.1'
157
- * ```
158
- */
159
- /**
160
- * Builds the Redis key for a resource + identifier combination.
161
- *
162
- * **Key Format:**
163
- * The generated key follows the pattern: `${namespace}:${resource}:${identifier}`
164
- * For example: `ratelimit:/api/login:ip-10.0.0.1`
165
- *
166
- * **Example:**
167
- * ```ts
168
- * const key = limiter.makeKey('/api/login', 'ip-10.0.0.1');
169
- * // key === 'ratelimit:/api/login:ip-10.0.0.1'
170
- * ```
171
- *
172
- * **Parameters:**
173
- * - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
174
- * a resource name `'email:send'`.
175
- * - `identifier` - The caller identity, e.g. an IP, user id or API key.
176
- * - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
177
- *
178
- * @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
179
- */
180
- makeKey(resource, identifier, namespace = this.defaultNamespace) {
181
- return `${namespace}:${resource}:${identifier}`;
182
- }
183
- /**
184
- * Consumes one unit of capacity for a resource + identifier and returns the
185
- * resulting limit state.
186
- *
187
- * When the limit is reached the request is not recorded and `allowed` is
188
- * `false` with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
189
- * Fails open (allows the request) if Redis errors.
190
- *
191
- * @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
192
- * a resource name `'db:write'`.
193
- * @param identifier - The caller identity, e.g. an IP, user id or API key.
194
- * @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
195
- * and `namespace`.
196
- * @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
197
- * `resetAt` (epoch ms), `retryAfter` (seconds).
198
- *
199
- * @example
200
- * ```ts
201
- * const result = await limiter.consume('/api/orders', 'user-7', { limit: 5, duration: 60 });
202
- * if (!result.allowed) {
203
- * res.setHeader('Retry-After', String(result.retryAfter));
204
- * return res.status(429).json({ error: 'Too many requests' });
205
- * }
206
- * ```
207
- */
208
- /**
209
- * Consumes one unit of capacity for a resource + identifier and returns the
210
- * resulting limit state.
211
- *
212
- * **Behavior:**
213
- * - When the limit is reached, the request is not recorded and `allowed` is `false`
214
- * with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
215
- * - Fails open (allows the request) if Redis errors occur, so an outage cannot take
216
- * down the whole app.
217
- * - Two algorithm modes are available: `sliding` (default, precise rolling window)
218
- * and `fixed` (simple counter-based).
219
- *
220
- * **Type Parameters:**
221
- * - The return type is {@link RateLimitResult}.
222
- *
223
- * **Returns:**
224
- * - A {@link RateLimitResult} object containing:
225
- * - `allowed`: whether the request may proceed
226
- * - `limit`: the configured max
227
- * - `used`: requests in current window
228
- * - `remaining`: left in the window
229
- * - `resetAt`: epoch ms when window resets
230
- * - `retryAfter`: seconds to wait (0 when allowed)
231
- *
232
- * **Example:**
233
- * ```ts
234
- * const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
235
- * if (!result.allowed) {
236
- * // HTTP 429, set Retry-After: result.retryAfter
237
- * res.setHeader('Retry-After', String(result.retryAfter));
238
- * return res.status(429).json({ error: 'Too many requests' });
239
- * }
240
- * // allowed === true, request may proceed
241
- * ```
242
- *
243
- * **Parameters:**
244
- * - `resource` - The rate-limited resource, e.g. a route `'/api/login'` or
245
- * a resource name `'email:send'`.
246
- * - `identifier` - The caller identity, e.g. an IP, user id or API key.
247
- * - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
248
- *
249
- * @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
250
- * `resetAt` (epoch ms), `retryAfter` (seconds).
251
- */
252
- async consume(resource, identifier, options = {}) {
253
- const limit = options.limit ?? this.defaultLimit;
254
- const duration = options.duration ?? this.defaultDuration;
255
- const algorithm = options.algorithm ?? this.defaultAlgorithm;
256
- const namespace = options.namespace ?? this.defaultNamespace;
257
- const key = this.makeKey(resource, identifier, namespace);
258
- try {
259
- if (algorithm === 'fixed') {
260
- return await this.consumeFixed(key, limit, duration);
261
- }
262
- return await this.consumeSliding(key, limit, duration);
263
- }
264
- catch (error) {
265
- this.logger.error('Rate limit consume failed, failing open', { key, error });
266
- return failOpenResult(limit, duration);
267
- }
268
- }
269
- /**
270
- * Peeks at the current limit state without consuming capacity.
271
- *
272
- * Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
273
- * the actual request). Also fails open on Redis errors.
274
- *
275
- * @param resource - The rate-limited resource.
276
- * @param identifier - The caller identity.
277
- * @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
278
- * and `namespace`.
279
- * @returns The current limit state; `used` is not incremented.
280
- *
281
- * @example
282
- * ```ts
283
- * const state = await limiter.check('/api/search', 'user-1');
284
- * if (state.remaining === 0) {
285
- * // disable the search button
286
- * }
287
- * ```
288
- */
289
- /**
290
- * Peeks at the current limit state without consuming capacity.
291
- *
292
- * **Behavior:**
293
- * - Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
294
- * the actual request).
295
- * - Does not increment the counter; only reads the current state.
296
- * - Fails open (allows the request) if Redis errors occur.
297
- *
298
- * **Type Parameters:**
299
- * - The return type is {@link RateLimitResult}.
300
- *
301
- * **Returns:**
302
- * - A {@link RateLimitResult} object representing the current state.
303
- * `used` is not incremented.
304
- *
305
- * **Example:**
306
- * ```ts
307
- * const state = await limiter.check('/api/search', 'user-1');
308
- * if (state.remaining === 0) {
309
- * // disable the search button
310
- * }
311
- * ```
312
- *
313
- * **Parameters:**
314
- * - `resource` - The rate-limited resource.
315
- * - `identifier` - The caller identity.
316
- * - `options` - Per-call overrides for `limit`, `duration`, `algorithm`, and `namespace`.
317
- *
318
- * @returns The current limit state; `used` is not incremented.
319
- */
320
- async check(resource, identifier, options = {}) {
321
- const limit = options.limit ?? this.defaultLimit;
322
- const duration = options.duration ?? this.defaultDuration;
323
- const algorithm = options.algorithm ?? this.defaultAlgorithm;
324
- const namespace = options.namespace ?? this.defaultNamespace;
325
- const key = this.makeKey(resource, identifier, namespace);
326
- try {
327
- if (algorithm === 'fixed') {
328
- return await this.checkFixed(key, limit, duration);
329
- }
330
- return await this.checkSliding(key, limit, duration);
331
- }
332
- catch (error) {
333
- this.logger.error('Rate limit check failed, failing open', { key, error });
334
- return failOpenResult(limit, duration);
335
- }
336
- }
337
- /**
338
- * Resets the counter for a resource + identifier, granting full capacity again.
339
- *
340
- * @param resource - The rate-limited resource.
341
- * @param identifier - The caller identity.
342
- * @param namespace - Key prefix (defaults to the limiter's namespace).
343
- * @returns `true` if a counter existed and was removed.
344
- *
345
- * @example
346
- * ```ts
347
- * // user upgraded to a premium plan, lift their limits
348
- * await limiter.reset('/api/export', 'user-7');
349
- * ```
350
- */
351
- /**
352
- * Resets the counter for a resource + identifier, granting full capacity again.
353
- *
354
- * **Behavior:**
355
- * - Deletes the rate limit key from Redis, resetting the counter to zero.
356
- * - After reset, the next request will be allowed (full capacity available).
357
- *
358
- * **Returns:**
359
- * - `true` if a counter existed and was removed.
360
- * - `false` if no counter existed (key already deleted).
361
- *
362
- * **Example:**
363
- * ```ts
364
- * // User upgraded to a premium plan, lift their limits
365
- * await limiter.reset('/api/export', 'user-7');
366
- * ```
367
- *
368
- * **Parameters:**
369
- * - `resource` - The rate-limited resource.
370
- * - `identifier` - The caller identity.
371
- * - `namespace` - Key prefix. Defaults to the limiter's configured namespace.
372
- *
373
- * @returns `true` if a counter existed and was removed.
374
- */
375
- async reset(resource, identifier, namespace = this.defaultNamespace) {
376
- const key = this.makeKey(resource, identifier, namespace);
377
- try {
378
- const deleted = await this.client.del(key);
379
- return deleted > 0;
380
- }
381
- catch (error) {
382
- this.logger.error('Rate limit reset failed', { key, error });
383
- return false;
384
- }
385
- }
386
- /**
387
- * Consumes one unit of capacity using the fixed-window algorithm.
388
- *
389
- * **Behavior:**
390
- * - Uses Redis `INCR` to increment a counter key.
391
- * - If the counter was `1` (first request in the window), sets a TTL via `EXPIRE`.
392
- * - The window resets at fixed boundaries determined by the TTL.
393
- * - Returns `allowed: true` as long as `count <= limit`.
394
- *
395
- * **Returns:**
396
- * - A {@link RateLimitResult} with the current window state.
397
- *
398
- * **Parameters:**
399
- * - `key` - The Redis key for this resource + identifier combination.
400
- * - `limit` - The maximum allowed requests within the window.
401
- * - `duration` - The TTL in seconds for the key (also the window length).
402
- *
403
- * @internal
404
- */
405
- async consumeFixed(key, limit, duration) {
406
- const now = Date.now();
407
- const count = await this.client.incr(key);
408
- if (count === 1) {
409
- await this.client.expire(key, duration);
410
- }
411
- const ttl = await this.client.ttl(key);
412
- const ttlSeconds = ttl > 0 ? ttl : duration;
413
- const allowed = count <= limit;
414
- return {
415
- allowed,
416
- limit,
417
- used: count,
418
- remaining: Math.max(0, limit - count),
419
- resetAt: now + ttlSeconds * 1000,
420
- retryAfter: allowed ? 0 : ttlSeconds,
421
- };
422
- }
423
- /**
424
- * Consumes one unit of capacity using the sliding-window algorithm.
425
- *
426
- * **Behavior:**
427
- * - Uses an atomic Lua script over a sorted set for a precise rolling window.
428
- * - Old entries outside the window are purged before counting.
429
- * - A unique member (timestamp + UUID) is added for each request.
430
- * - The `PEXPIRE` command ensures the key expires after the window duration.
431
- * - Returns `allowed: true` as long as the count of entries within the window is < limit.
432
- *
433
- * **The Lua script** (see {@link CONSUME_SCRIPT}) performs these operations atomically:
434
- * 1. Remove entries with scores older than `now - window`
435
- * 2. Count remaining entries (`ZCARD`)
436
- * 3. If count >= limit, return `allowed: false` with `retryAfter`
437
- * 4. Otherwise, add the new entry (`ZADD`) and return `allowed: true`
438
- *
439
- * **Returns:**
440
- * - A {@link RateLimitResult} with the current window state.
441
- *
442
- * **Parameters:**
443
- * - `key` - The Redis key for this resource + identifier combination.
444
- * - `limit` - The maximum allowed requests within the window.
445
- * - `duration` - The window length in seconds.
446
- *
447
- * @internal
448
- */
449
- async consumeSliding(key, limit, duration) {
450
- const now = Date.now();
451
- const member = `${now}:${randomUUID()}`;
452
- const result = (await this.client.raw.eval(CONSUME_SCRIPT, 1, key, now, duration * 1000, limit, member));
453
- const allowed = result[0] === 1;
454
- const used = result[1] ?? 0;
455
- const remaining = result[2] ?? 0;
456
- const retryAfter = result[3] ?? 0;
457
- return {
458
- allowed,
459
- limit,
460
- used,
461
- remaining: allowed ? remaining : 0,
462
- resetAt: now + retryAfter * 1000,
463
- retryAfter,
464
- };
465
- }
466
- /**
467
- * Peeks at the current limit using the fixed-window algorithm.
468
- *
469
- * **Behavior:**
470
- * - Reads the current counter value from Redis via `GET`.
471
- * - If the key does not exist, `used` is `0`.
472
- * - Returns `allowed: true` when `used < limit`.
473
- *
474
- * **Returns:**
475
- * - A {@link RateLimitResult} with the current window state.
476
- *
477
- * **Parameters:**
478
- * - `key` - The Redis key for this resource + identifier combination.
479
- * - `limit` - The maximum allowed requests within the window.
480
- * - `duration` - The TTL/window length in seconds.
481
- *
482
- * @internal
483
- */
484
- async checkFixed(key, limit, duration) {
485
- const now = Date.now();
486
- const raw = await this.client.get(key);
487
- const used = raw === null || raw === undefined ? 0 : Number(raw) || 0;
488
- const ttl = await this.client.ttl(key);
489
- const ttlSeconds = ttl > 0 ? ttl : duration;
490
- const allowed = used < limit;
491
- return {
492
- allowed,
493
- limit,
494
- used,
495
- remaining: Math.max(0, limit - used),
496
- resetAt: now + ttlSeconds * 1000,
497
- retryAfter: allowed ? 0 : ttlSeconds,
498
- };
499
- }
500
- /**
501
- * Peeks at the current limit using the sliding-window algorithm.
502
- *
503
- * **Behavior:**
504
- * - Uses an atomic Lua script (see {@link PEEK_SCRIPT}) to count entries within
505
- * the rolling window without consuming capacity.
506
- * - Old entries outside the window are purged before counting.
507
- * - Returns `allowed: true` when the count of entries within the window is < limit.
508
- *
509
- * **The Lua script** (see {@link PEEK_SCRIPT}) performs:
510
- * 1. Remove entries with scores older than `now - window`
511
- * 2. Count remaining entries (`ZCARD`)
512
- * 3. Return the count and optional `retryAfter`
513
- *
514
- * **Returns:**
515
- * - A {@link RateLimitResult} with the current window state.
516
- * `used` is the count of entries in the window; not incremented.
517
- *
518
- * **Parameters:**
519
- * - `key` - The Redis key for this resource + identifier combination.
520
- * - `limit` - The maximum allowed requests within the window.
521
- * - `duration` - The window length in seconds.
522
- *
523
- * @internal
524
- */
525
- async checkSliding(key, limit, duration) {
526
- const now = Date.now();
527
- const result = (await this.client.raw.eval(PEEK_SCRIPT, 1, key, now, duration * 1000, limit));
528
- const used = result[0] ?? 0;
529
- const retryAfter = result[1] ?? 0;
530
- return {
531
- allowed: used < limit,
532
- limit,
533
- used,
534
- remaining: Math.max(0, limit - used),
535
- resetAt: now + retryAfter * 1000,
536
- retryAfter,
537
- };
538
- }
539
- }
@@ -1,23 +0,0 @@
1
- export { RedisRevocationStore } from './revocation-store.js';
2
- export type { RedisRevocationStoreOptions } from './revocation-store.js';
3
- export { RevocationBatchError, RevocationError } from './session-errors.js';
4
- export { createSessionManager, SessionManager } from './session-manager.js';
5
- export type { SessionManagerOptions } from './session-manager.js';
6
- export { SessionService } from './session-service.js';
7
- export type { SessionServiceDeps } from './session-service.js';
8
- export { SessionRepository } from './session-repository.js';
9
- export type { SessionScriptRegistryOptions } from './session-scripts.js';
10
- export { SessionScriptRegistry, SCRIPT_NAMES } from './session-scripts.js';
11
- export type { ScriptName } from './session-scripts.js';
12
- export { SessionKeyStrategy, encodeUserId } from './session-keys.js';
13
- export { SessionTokenManager } from './session-token.js';
14
- export { StaticSessionKeyProvider, createRandomSessionKeyProvider, toKeyBuffer, } from './session-encryption.js';
15
- export { serializeSession, serializeEncryptedSession, deserializeSession, validateSessionRecord, envelopeKind, encryptedHeaderOf, } from './session-serializer.js';
16
- export { parseSessionConfig, redactSessionConfig, SessionConfigSchema, TTL, IDLE_TIMEOUT, TOUCH_INTERVAL, } from './session-config.js';
17
- export type { SessionConfig, SessionConfigInput, PartialSessionConfig, } from './session-config.js';
18
- export { SessionError, SessionNotFoundError, SessionExpiredError, SessionRevokedError, SessionInvalidError, SessionRotationError, SessionReplayError, SessionStorageError, SessionSerializationError, SessionConfigurationError, SessionConcurrencyError, CircuitBreakerOpenError, redactIdentifier, } from './session-errors.js';
19
- export type { SessionRecord, SessionCreateInput, SessionUpdatePatch, CreatedSession, RotatedSession, SessionValidationResult, SessionInvalidReason, TouchOutcome, SessionEnvelope, } from './session-types.js';
20
- export { SessionMetrics, type SessionMetricsAdapter } from './session-metrics.js';
21
- export { SessionCircuitBreaker } from './session-circuit-breaker.js';
22
- export { SessionHealthChecker } from './session-health.js';
23
- export { SessionCookieManager, type SerializeCookieOptions, type SerializedCookie, type SerializedCookieAttributes, } from './session-cookie.js';
@@ -1,16 +0,0 @@
1
- export { RedisRevocationStore } from './revocation-store.js';
2
- export { RevocationBatchError, RevocationError } from './session-errors.js';
3
- export { createSessionManager, SessionManager } from './session-manager.js';
4
- export { SessionService } from './session-service.js';
5
- export { SessionRepository } from './session-repository.js';
6
- export { SessionScriptRegistry, SCRIPT_NAMES } from './session-scripts.js';
7
- export { SessionKeyStrategy, encodeUserId } from './session-keys.js';
8
- export { SessionTokenManager } from './session-token.js';
9
- export { StaticSessionKeyProvider, createRandomSessionKeyProvider, toKeyBuffer, } from './session-encryption.js';
10
- export { serializeSession, serializeEncryptedSession, deserializeSession, validateSessionRecord, envelopeKind, encryptedHeaderOf, } from './session-serializer.js';
11
- export { parseSessionConfig, redactSessionConfig, SessionConfigSchema, TTL, IDLE_TIMEOUT, TOUCH_INTERVAL, } from './session-config.js';
12
- export { SessionError, SessionNotFoundError, SessionExpiredError, SessionRevokedError, SessionInvalidError, SessionRotationError, SessionReplayError, SessionStorageError, SessionSerializationError, SessionConfigurationError, SessionConcurrencyError, CircuitBreakerOpenError, redactIdentifier, } from './session-errors.js';
13
- export { SessionMetrics } from './session-metrics.js';
14
- export { SessionCircuitBreaker } from './session-circuit-breaker.js';
15
- export { SessionHealthChecker } from './session-health.js';
16
- export { SessionCookieManager, } from './session-cookie.js';