ioredis-toolkit 0.0.1

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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +645 -0
  3. package/dist/cache.d.ts +298 -0
  4. package/dist/cache.js +606 -0
  5. package/dist/client.d.ts +177 -0
  6. package/dist/client.js +958 -0
  7. package/dist/cluster-slot.d.ts +4 -0
  8. package/dist/cluster-slot.js +31 -0
  9. package/dist/cluster.d.ts +79 -0
  10. package/dist/cluster.js +156 -0
  11. package/dist/errors.d.ts +30 -0
  12. package/dist/errors.js +63 -0
  13. package/dist/health.d.ts +39 -0
  14. package/dist/health.js +106 -0
  15. package/dist/index.d.ts +51 -0
  16. package/dist/index.js +44 -0
  17. package/dist/lock.d.ts +215 -0
  18. package/dist/lock.js +385 -0
  19. package/dist/logger.d.ts +12 -0
  20. package/dist/logger.js +40 -0
  21. package/dist/pubsub.d.ts +171 -0
  22. package/dist/pubsub.js +285 -0
  23. package/dist/ratelimiter.d.ts +162 -0
  24. package/dist/ratelimiter.js +289 -0
  25. package/dist/session/index.d.ts +23 -0
  26. package/dist/session/index.js +16 -0
  27. package/dist/session/revocation-store.d.ts +171 -0
  28. package/dist/session/revocation-store.js +310 -0
  29. package/dist/session/scripts/cleanup-index.lua +21 -0
  30. package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
  31. package/dist/session/scripts/conditional-update.lua +63 -0
  32. package/dist/session/scripts/create.lua +68 -0
  33. package/dist/session/scripts/delete-by-user.lua +29 -0
  34. package/dist/session/scripts/delete.lua +15 -0
  35. package/dist/session/scripts/enforce-limit.lua +38 -0
  36. package/dist/session/scripts/revoke.lua +61 -0
  37. package/dist/session/scripts/rotate-encrypted.lua +107 -0
  38. package/dist/session/scripts/rotate.lua +119 -0
  39. package/dist/session/scripts/touch-encrypted.lua +89 -0
  40. package/dist/session/scripts/touch.lua +72 -0
  41. package/dist/session/scripts/validate.lua +90 -0
  42. package/dist/session/session-circuit-breaker.d.ts +42 -0
  43. package/dist/session/session-circuit-breaker.js +129 -0
  44. package/dist/session/session-config.d.ts +335 -0
  45. package/dist/session/session-config.js +162 -0
  46. package/dist/session/session-cookie.d.ts +72 -0
  47. package/dist/session/session-cookie.js +101 -0
  48. package/dist/session/session-encryption.d.ts +87 -0
  49. package/dist/session/session-encryption.js +139 -0
  50. package/dist/session/session-errors.d.ts +85 -0
  51. package/dist/session/session-errors.js +145 -0
  52. package/dist/session/session-health.d.ts +38 -0
  53. package/dist/session/session-health.js +60 -0
  54. package/dist/session/session-keys.d.ts +51 -0
  55. package/dist/session/session-keys.js +113 -0
  56. package/dist/session/session-manager.d.ts +59 -0
  57. package/dist/session/session-manager.js +94 -0
  58. package/dist/session/session-metrics.d.ts +33 -0
  59. package/dist/session/session-metrics.js +112 -0
  60. package/dist/session/session-repository.d.ts +161 -0
  61. package/dist/session/session-repository.js +683 -0
  62. package/dist/session/session-scripts.d.ts +36 -0
  63. package/dist/session/session-scripts.js +130 -0
  64. package/dist/session/session-serializer.d.ts +42 -0
  65. package/dist/session/session-serializer.js +248 -0
  66. package/dist/session/session-service.d.ts +104 -0
  67. package/dist/session/session-service.js +611 -0
  68. package/dist/session/session-token.d.ts +38 -0
  69. package/dist/session/session-token.js +86 -0
  70. package/dist/session/session-types.d.ts +253 -0
  71. package/dist/session/session-types.js +16 -0
  72. package/dist/types.d.ts +782 -0
  73. package/dist/types.js +140 -0
  74. package/package.json +97 -0
@@ -0,0 +1,289 @@
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
+ function failOpenResult(limit, duration) {
45
+ return {
46
+ allowed: true,
47
+ limit,
48
+ used: 0,
49
+ remaining: limit,
50
+ resetAt: Date.now() + duration * 1000,
51
+ retryAfter: 0,
52
+ };
53
+ }
54
+ /**
55
+ * Generic Redis-backed rate limiter that works for any resource: routes, API
56
+ * endpoints, users, IPs, databases, email sending, etc.
57
+ *
58
+ * Keys are namespaced as `ratelimit:{namespace}:{resource}:{identifier}` so each
59
+ * resource + identifier combination is tracked independently. Supports fixed-window
60
+ * (`INCR`/`EXPIRE`) and sliding-window (atomic Lua over a sorted set) algorithms.
61
+ * Fails open when Redis is unavailable.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const limiter = new RateLimiter(client, { limit: 100, duration: 60 });
66
+ *
67
+ * const result = await limiter.consume('/api/login', 'ip-10.0.0.1');
68
+ * if (!result.allowed) {
69
+ * throw new Error(`Slow down, retry in ${result.retryAfter}s`);
70
+ * }
71
+ * ```
72
+ */
73
+ export class RateLimiter {
74
+ client;
75
+ logger;
76
+ defaultLimit;
77
+ defaultDuration;
78
+ defaultAlgorithm;
79
+ defaultNamespace;
80
+ /**
81
+ * Creates a rate limiter bound to a Redis client.
82
+ *
83
+ * @param client - The underlying {@link RedisClientWrapper}.
84
+ * @param options - Defaults applied when a call does not override them:
85
+ * `limit` (default `100`), `duration` in seconds (default `60`),
86
+ * `algorithm` (default `'sliding'`), `namespace` (default `'ratelimit'`).
87
+ * @param logger - Optional pino-compatible logger; defaults to `console`.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const limiter = new RateLimiter(client, { limit: 10, duration: 1, algorithm: 'fixed' });
92
+ * ```
93
+ */
94
+ constructor(client, options = {}, logger = defaultLogger) {
95
+ this.client = client;
96
+ this.logger = logger.child({ component: 'RateLimiter' });
97
+ this.defaultLimit = options.limit ?? 100;
98
+ this.defaultDuration = options.duration ?? 60;
99
+ this.defaultAlgorithm = options.algorithm ?? 'sliding';
100
+ this.defaultNamespace = options.namespace ?? 'ratelimit';
101
+ }
102
+ /**
103
+ * Builds the Redis key for a resource + identifier combination.
104
+ *
105
+ * @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
106
+ * a resource name `'email:send'`.
107
+ * @param identifier - The caller identity, e.g. an IP, user id or API key.
108
+ * @param namespace - Key prefix (defaults to the limiter's namespace).
109
+ * @returns The full key, e.g. `'ratelimit:/api/login:ip-10.0.0.1'`.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * limiter.makeKey('/api/login', 'ip-10.0.0.1');
114
+ * // 'ratelimit:/api/login:ip-10.0.0.1'
115
+ * ```
116
+ */
117
+ makeKey(resource, identifier, namespace = this.defaultNamespace) {
118
+ return `${namespace}:${resource}:${identifier}`;
119
+ }
120
+ /**
121
+ * Consumes one unit of capacity for a resource + identifier and returns the
122
+ * resulting limit state.
123
+ *
124
+ * When the limit is reached the request is not recorded and `allowed` is
125
+ * `false` with `retryAfter` (seconds) and `resetAt` (epoch ms) hints.
126
+ * Fails open (allows the request) if Redis errors.
127
+ *
128
+ * @param resource - The rate-limited resource, e.g. a route `'/api/login'` or
129
+ * a resource name `'db:write'`.
130
+ * @param identifier - The caller identity, e.g. an IP, user id or API key.
131
+ * @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
132
+ * and `namespace`.
133
+ * @returns The limit state: `allowed`, `limit`, `used`, `remaining`,
134
+ * `resetAt` (epoch ms), `retryAfter` (seconds).
135
+ *
136
+ * @example
137
+ * ```ts
138
+ * const result = await limiter.consume('/api/orders', 'user-7', { limit: 5, duration: 60 });
139
+ * if (!result.allowed) {
140
+ * res.setHeader('Retry-After', String(result.retryAfter));
141
+ * return res.status(429).json({ error: 'Too many requests' });
142
+ * }
143
+ * ```
144
+ */
145
+ async consume(resource, identifier, options = {}) {
146
+ const limit = options.limit ?? this.defaultLimit;
147
+ const duration = options.duration ?? this.defaultDuration;
148
+ const algorithm = options.algorithm ?? this.defaultAlgorithm;
149
+ const namespace = options.namespace ?? this.defaultNamespace;
150
+ const key = this.makeKey(resource, identifier, namespace);
151
+ try {
152
+ if (algorithm === 'fixed') {
153
+ return await this.consumeFixed(key, limit, duration);
154
+ }
155
+ return await this.consumeSliding(key, limit, duration);
156
+ }
157
+ catch (error) {
158
+ this.logger.error('Rate limit consume failed, failing open', { key, error });
159
+ return failOpenResult(limit, duration);
160
+ }
161
+ }
162
+ /**
163
+ * Peeks at the current limit state without consuming capacity.
164
+ *
165
+ * Useful for pre-flight checks (e.g. showing "limit reached" in a UI before
166
+ * the actual request). Also fails open on Redis errors.
167
+ *
168
+ * @param resource - The rate-limited resource.
169
+ * @param identifier - The caller identity.
170
+ * @param options - Per-call overrides for `limit`, `duration`, `algorithm`,
171
+ * and `namespace`.
172
+ * @returns The current limit state; `used` is not incremented.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * const state = await limiter.check('/api/search', 'user-1');
177
+ * if (state.remaining === 0) {
178
+ * // disable the search button
179
+ * }
180
+ * ```
181
+ */
182
+ async check(resource, identifier, options = {}) {
183
+ const limit = options.limit ?? this.defaultLimit;
184
+ const duration = options.duration ?? this.defaultDuration;
185
+ const algorithm = options.algorithm ?? this.defaultAlgorithm;
186
+ const namespace = options.namespace ?? this.defaultNamespace;
187
+ const key = this.makeKey(resource, identifier, namespace);
188
+ try {
189
+ if (algorithm === 'fixed') {
190
+ return await this.checkFixed(key, limit, duration);
191
+ }
192
+ return await this.checkSliding(key, limit, duration);
193
+ }
194
+ catch (error) {
195
+ this.logger.error('Rate limit check failed, failing open', { key, error });
196
+ return failOpenResult(limit, duration);
197
+ }
198
+ }
199
+ /**
200
+ * Resets the counter for a resource + identifier, granting full capacity again.
201
+ *
202
+ * @param resource - The rate-limited resource.
203
+ * @param identifier - The caller identity.
204
+ * @param namespace - Key prefix (defaults to the limiter's namespace).
205
+ * @returns `true` if a counter existed and was removed.
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * // user upgraded to a premium plan, lift their limits
210
+ * await limiter.reset('/api/export', 'user-7');
211
+ * ```
212
+ */
213
+ async reset(resource, identifier, namespace = this.defaultNamespace) {
214
+ const key = this.makeKey(resource, identifier, namespace);
215
+ try {
216
+ const deleted = await this.client.del(key);
217
+ return deleted > 0;
218
+ }
219
+ catch (error) {
220
+ this.logger.error('Rate limit reset failed', { key, error });
221
+ return false;
222
+ }
223
+ }
224
+ async consumeFixed(key, limit, duration) {
225
+ const now = Date.now();
226
+ const count = await this.client.incr(key);
227
+ if (count === 1) {
228
+ await this.client.expire(key, duration);
229
+ }
230
+ const ttl = await this.client.ttl(key);
231
+ const ttlSeconds = ttl > 0 ? ttl : duration;
232
+ const allowed = count <= limit;
233
+ return {
234
+ allowed,
235
+ limit,
236
+ used: count,
237
+ remaining: Math.max(0, limit - count),
238
+ resetAt: now + ttlSeconds * 1000,
239
+ retryAfter: allowed ? 0 : ttlSeconds,
240
+ };
241
+ }
242
+ async consumeSliding(key, limit, duration) {
243
+ const now = Date.now();
244
+ const member = `${now}:${randomUUID()}`;
245
+ const result = (await this.client.raw.eval(CONSUME_SCRIPT, 1, key, now, duration * 1000, limit, member));
246
+ const allowed = result[0] === 1;
247
+ const used = result[1] ?? 0;
248
+ const remaining = result[2] ?? 0;
249
+ const retryAfter = result[3] ?? 0;
250
+ return {
251
+ allowed,
252
+ limit,
253
+ used,
254
+ remaining: allowed ? remaining : 0,
255
+ resetAt: now + retryAfter * 1000,
256
+ retryAfter,
257
+ };
258
+ }
259
+ async checkFixed(key, limit, duration) {
260
+ const now = Date.now();
261
+ const raw = await this.client.get(key);
262
+ const used = raw === null || raw === undefined ? 0 : Number(raw) || 0;
263
+ const ttl = await this.client.ttl(key);
264
+ const ttlSeconds = ttl > 0 ? ttl : duration;
265
+ const allowed = used < limit;
266
+ return {
267
+ allowed,
268
+ limit,
269
+ used,
270
+ remaining: Math.max(0, limit - used),
271
+ resetAt: now + ttlSeconds * 1000,
272
+ retryAfter: allowed ? 0 : ttlSeconds,
273
+ };
274
+ }
275
+ async checkSliding(key, limit, duration) {
276
+ const now = Date.now();
277
+ const result = (await this.client.raw.eval(PEEK_SCRIPT, 1, key, now, duration * 1000, limit));
278
+ const used = result[0] ?? 0;
279
+ const retryAfter = result[1] ?? 0;
280
+ return {
281
+ allowed: used < limit,
282
+ limit,
283
+ used,
284
+ remaining: Math.max(0, limit - used),
285
+ resetAt: now + retryAfter * 1000,
286
+ retryAfter,
287
+ };
288
+ }
289
+ }
@@ -0,0 +1,23 @@
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';
@@ -0,0 +1,16 @@
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';
@@ -0,0 +1,171 @@
1
+ import type { RevocationRecord, RevocationStore } from './session-types.js';
2
+ import type { RedisClientWrapper } from '../client.js';
3
+ export interface RedisRevocationStoreOptions {
4
+ /**
5
+ * Redis client.
6
+ *
7
+ * Compatible with:
8
+ * - ioredis standalone
9
+ * - ioredis Sentinel
10
+ * - ioredis Cluster
11
+ */
12
+ client: RedisClientWrapper;
13
+ /**
14
+ * Key prefix, so multiple apps can share a Redis instance safely.
15
+ * Default: `authcore:revoked:`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * new RedisRevocationStore({ client, keyPrefix: 'auth:revoked:' });
20
+ * ```
21
+ */
22
+ keyPrefix?: string;
23
+ }
24
+ /**
25
+ * Redis-backed revocation store. Each revoked jti is stored as
26
+ * `{prefix}{jti} -> reason`, with the Redis key TTL itself set to the
27
+ * token's remaining lifetime — expired entries are reclaimed automatically
28
+ * by Redis, no sweep job required.
29
+ *
30
+ * Every operation here is a single-key command, so this store works
31
+ * identically on standalone, Sentinel, and Cluster with no hash tags
32
+ * required (unlike the session store, there's no multi-key atomicity
33
+ * requirement to satisfy).
34
+ *
35
+ * Batched operations (`revokeMany`, `isRevokedMany`) group their commands
36
+ * by hash slot and issue one pipeline per slot, so they never trigger
37
+ * `CROSSSLOT` errors on Redis Cluster. Pipeline failures are surfaced via
38
+ * {@link RevocationBatchError} instead of being silently swallowed —
39
+ * a missed revocation is a security bug.
40
+ *
41
+ * Validation fails fast and typed: invalid records throw
42
+ * {@link RevocationError} before any network call, and reads fail closed
43
+ * (an infra error is never treated as "not revoked").
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const revocations = new RedisRevocationStore({ client });
48
+ *
49
+ * await revocations.revoke({
50
+ * jti: 'a1b2c3d4',
51
+ * reason: 'password-change',
52
+ * expiresAt: Math.floor(Date.now() / 1000) + 3600,
53
+ * });
54
+ *
55
+ * if (await revocations.isRevoked('a1b2c3d4')) {
56
+ * // token was rotated or revoked - reject it
57
+ * }
58
+ * ```
59
+ */
60
+ export declare class RedisRevocationStore implements RevocationStore {
61
+ private readonly client;
62
+ private readonly keyPrefix;
63
+ /**
64
+ * Creates a Redis-backed revocation store.
65
+ *
66
+ * @param options - Client connection and key-prefix configuration.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const store = new RedisRevocationStore({
71
+ * client,
72
+ * keyPrefix: 'myapp:revoked:',
73
+ * });
74
+ * ```
75
+ */
76
+ constructor(options: RedisRevocationStoreOptions);
77
+ /**
78
+ * Marks a jti as revoked for the remainder of its lifetime.
79
+ *
80
+ * Stores `{prefix}{jti} -> reason` with a Redis TTL equal to the
81
+ * record's remaining lifetime (`expiresAt - now`), so the entry is
82
+ * garbage-collected automatically once the original token would have
83
+ * expired anyway. Overwriting an existing entry extends/refreshes its
84
+ * TTL to the new expiry.
85
+ *
86
+ * @param record - The revocation entry (`jti`, `expiresAt`, optional
87
+ * `reason`). `expiresAt` must be a finite Unix-seconds timestamp in
88
+ * the future.
89
+ * @throws {RevocationError} when `record.expiresAt` is missing, not a
90
+ * finite number, or not in the future (fails fast instead of sending
91
+ * an invalid `EX` to Redis).
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * await revocations.revoke({
96
+ * jti: 'a1b2c3d4',
97
+ * reason: 'logout',
98
+ * expiresAt: Math.floor(Date.now() / 1000) + 86400,
99
+ * });
100
+ * ```
101
+ */
102
+ revoke(record: RevocationRecord): Promise<void>;
103
+ /**
104
+ * Revokes many jtis in one batched call.
105
+ *
106
+ * All records are validated up front — an invalid `expiresAt` fails
107
+ * before any network call is issued, rather than partway through a
108
+ * batch. Commands are grouped by hash slot (one pipeline per slot) so
109
+ * the batch stays Cluster-safe, and every pipeline result is inspected:
110
+ * any failed command throws {@link RevocationBatchError} listing the
111
+ * affected jtis, because a silently-missed revocation is a security bug.
112
+ *
113
+ * @param records - The revocation entries to create/refresh.
114
+ * @throws {RevocationError} when any record is invalid (validation is
115
+ * all-or-nothing, before any network call).
116
+ * @throws {RevocationBatchError} when one or more pipeline commands
117
+ * fail; carries the exact jtis that were not revoked.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * await revocations.revokeMany([
122
+ * { jti: 'a1', reason: 'logout-all', expiresAt: expiry },
123
+ * { jti: 'b2', reason: 'logout-all', expiresAt: expiry },
124
+ * ]);
125
+ * ```
126
+ */
127
+ revokeMany(records: RevocationRecord[]): Promise<void>;
128
+ /**
129
+ * Checks whether a jti is currently revoked.
130
+ *
131
+ * Fail-closed: infrastructure errors are wrapped in a typed error and
132
+ * must NOT be treated as "not revoked".
133
+ *
134
+ * @param jti - The token/session id to check.
135
+ * @returns `true` when the jti has a live revocation entry.
136
+ * @throws {RevocationError} when the check itself fails (caller must
137
+ * treat the outcome as unknown).
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * if (await revocations.isRevoked(token.jti)) {
142
+ * return 401; // token was rotated away or explicitly revoked
143
+ * }
144
+ * ```
145
+ */
146
+ isRevoked(jti: string): Promise<boolean>;
147
+ /**
148
+ * Batched revocation check - one network round trip instead of N.
149
+ *
150
+ * Useful for validating a whole family of rotated tokens, or a batch
151
+ * of refresh attempts, at once. Commands are grouped by hash slot
152
+ * (one pipeline per slot) to stay Cluster-safe, and the check fails
153
+ * closed: if any command errors, {@link RevocationBatchError} is thrown
154
+ * rather than silently treating the jti as "not revoked".
155
+ *
156
+ * @param jtis - The token/session ids to check.
157
+ * @returns A `Set` containing exactly the revoked jtis.
158
+ * @throws {RevocationBatchError} when a pipeline command fails -
159
+ * the caller must treat the outcome as unknown, not as "valid".
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * const revoked = await revocations.isRevokedMany(['a1', 'b2', 'c3']);
164
+ * if (revoked.has('b2')) {
165
+ * // b2 must not be accepted
166
+ * }
167
+ * ```
168
+ */
169
+ isRevokedMany(jtis: string[]): Promise<Set<string>>;
170
+ private key;
171
+ }