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.
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/cache.d.ts +298 -0
- package/dist/cache.js +606 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.js +958 -0
- package/dist/cluster-slot.d.ts +4 -0
- package/dist/cluster-slot.js +31 -0
- package/dist/cluster.d.ts +79 -0
- package/dist/cluster.js +156 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +63 -0
- package/dist/health.d.ts +39 -0
- package/dist/health.js +106 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +44 -0
- package/dist/lock.d.ts +215 -0
- package/dist/lock.js +385 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +40 -0
- package/dist/pubsub.d.ts +171 -0
- package/dist/pubsub.js +285 -0
- package/dist/ratelimiter.d.ts +162 -0
- package/dist/ratelimiter.js +289 -0
- package/dist/session/index.d.ts +23 -0
- package/dist/session/index.js +16 -0
- package/dist/session/revocation-store.d.ts +171 -0
- package/dist/session/revocation-store.js +310 -0
- package/dist/session/scripts/cleanup-index.lua +21 -0
- package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
- package/dist/session/scripts/conditional-update.lua +63 -0
- package/dist/session/scripts/create.lua +68 -0
- package/dist/session/scripts/delete-by-user.lua +29 -0
- package/dist/session/scripts/delete.lua +15 -0
- package/dist/session/scripts/enforce-limit.lua +38 -0
- package/dist/session/scripts/revoke.lua +61 -0
- package/dist/session/scripts/rotate-encrypted.lua +107 -0
- package/dist/session/scripts/rotate.lua +119 -0
- package/dist/session/scripts/touch-encrypted.lua +89 -0
- package/dist/session/scripts/touch.lua +72 -0
- package/dist/session/scripts/validate.lua +90 -0
- package/dist/session/session-circuit-breaker.d.ts +42 -0
- package/dist/session/session-circuit-breaker.js +129 -0
- package/dist/session/session-config.d.ts +335 -0
- package/dist/session/session-config.js +162 -0
- package/dist/session/session-cookie.d.ts +72 -0
- package/dist/session/session-cookie.js +101 -0
- package/dist/session/session-encryption.d.ts +87 -0
- package/dist/session/session-encryption.js +139 -0
- package/dist/session/session-errors.d.ts +85 -0
- package/dist/session/session-errors.js +145 -0
- package/dist/session/session-health.d.ts +38 -0
- package/dist/session/session-health.js +60 -0
- package/dist/session/session-keys.d.ts +51 -0
- package/dist/session/session-keys.js +113 -0
- package/dist/session/session-manager.d.ts +59 -0
- package/dist/session/session-manager.js +94 -0
- package/dist/session/session-metrics.d.ts +33 -0
- package/dist/session/session-metrics.js +112 -0
- package/dist/session/session-repository.d.ts +161 -0
- package/dist/session/session-repository.js +683 -0
- package/dist/session/session-scripts.d.ts +36 -0
- package/dist/session/session-scripts.js +130 -0
- package/dist/session/session-serializer.d.ts +42 -0
- package/dist/session/session-serializer.js +248 -0
- package/dist/session/session-service.d.ts +104 -0
- package/dist/session/session-service.js +611 -0
- package/dist/session/session-token.d.ts +38 -0
- package/dist/session/session-token.js +86 -0
- package/dist/session/session-types.d.ts +253 -0
- package/dist/session/session-types.js +16 -0
- package/dist/types.d.ts +782 -0
- package/dist/types.js +140 -0
- package/package.json +97 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Default absolute session lifetime: 7 days. */
|
|
3
|
+
export declare const TTL: number;
|
|
4
|
+
/** Default idle timeout: 24 hours. */
|
|
5
|
+
export declare const IDLE_TIMEOUT: number;
|
|
6
|
+
/** Default touch throttle interval: 5 minutes. */
|
|
7
|
+
export declare const TOUCH_INTERVAL: number;
|
|
8
|
+
export declare const SessionStatusSchema: z.ZodEnum<{
|
|
9
|
+
active: "active";
|
|
10
|
+
consumed: "consumed";
|
|
11
|
+
revoked: "revoked";
|
|
12
|
+
}>;
|
|
13
|
+
/** How strictly session binding metadata (IP/UA/device) is enforced. */
|
|
14
|
+
export declare const SessionBindingPolicySchema: z.ZodEnum<{
|
|
15
|
+
advisory: "advisory";
|
|
16
|
+
disabled: "disabled";
|
|
17
|
+
strict: "strict";
|
|
18
|
+
}>;
|
|
19
|
+
/** Optional fail-closed circuit breaker around session operations. */
|
|
20
|
+
export interface SessionCircuitBreakerConfig {
|
|
21
|
+
/** Enable the fail-closed circuit breaker. Default: false. */
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
/** Consecutive failures needed to open the circuit. */
|
|
24
|
+
failureThreshold: number;
|
|
25
|
+
/** Milliseconds the circuit stays open before half-open probes. */
|
|
26
|
+
resetTimeoutMs: number;
|
|
27
|
+
/** Maximum concurrent probe requests while half-open. */
|
|
28
|
+
halfOpenMaxRequests: number;
|
|
29
|
+
}
|
|
30
|
+
export declare const SessionCircuitBreakerConfigSchema: z.ZodPrefault<z.ZodObject<{
|
|
31
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
failureThreshold: z.ZodDefault<z.ZodNumber>;
|
|
33
|
+
resetTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
34
|
+
halfOpenMaxRequests: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
/** Optional AES-256-GCM encryption of session data at rest. */
|
|
37
|
+
export interface SessionEncryptionConfig {
|
|
38
|
+
/**
|
|
39
|
+
* Enable AES-256-GCM encryption at rest. Default: false.
|
|
40
|
+
*
|
|
41
|
+
* Evaluate first whether transport TLS, ACLs, private networking and
|
|
42
|
+
* infrastructure controls already cover your threat model. Encryption
|
|
43
|
+
* protects session data against a compromised Redis instance or its
|
|
44
|
+
* disk; it does NOT protect against a compromised application process.
|
|
45
|
+
*/
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Re-encrypt with the current key version on touch/update (lazy key
|
|
49
|
+
* rotation). Default: true.
|
|
50
|
+
*/
|
|
51
|
+
reEncryptOnWrite: boolean;
|
|
52
|
+
}
|
|
53
|
+
export declare const SessionEncryptionConfigSchema: z.ZodObject<{
|
|
54
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
55
|
+
reEncryptOnWrite: z.ZodDefault<z.ZodBoolean>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
/** Metrics collection for session operations. */
|
|
58
|
+
export interface SessionMetricsConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Collect internal session metrics through the injected metrics adapter.
|
|
61
|
+
* When no adapter is provided, metrics are a safe no-op regardless.
|
|
62
|
+
*/
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare const SessionMetricsConfigSchema: z.ZodObject<{
|
|
66
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
/** Health check thresholds for the session dependency. */
|
|
69
|
+
export interface SessionHealthConfig {
|
|
70
|
+
/** PING latency above this (ms) marks the dependency degraded. */
|
|
71
|
+
latencyThresholdMs: number;
|
|
72
|
+
/** Recent operation error rate above this marks the dependency degraded. */
|
|
73
|
+
errorRateThreshold: number;
|
|
74
|
+
/** Number of recent operations sampled for the error rate. */
|
|
75
|
+
errorWindowSize: number;
|
|
76
|
+
}
|
|
77
|
+
export declare const SessionHealthConfigSchema: z.ZodObject<{
|
|
78
|
+
latencyThresholdMs: z.ZodDefault<z.ZodNumber>;
|
|
79
|
+
errorRateThreshold: z.ZodDefault<z.ZodNumber>;
|
|
80
|
+
errorWindowSize: z.ZodDefault<z.ZodNumber>;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
/** Cookie defaults for the framework-independent cookie manager. */
|
|
83
|
+
export interface SessionCookieConfig {
|
|
84
|
+
/** Cookie name. Default: 'sid'. */
|
|
85
|
+
name: string;
|
|
86
|
+
/** Cookie Path attribute. Default: '/'. */
|
|
87
|
+
path: string;
|
|
88
|
+
/** Cookie Domain attribute (empty = host-only cookie). */
|
|
89
|
+
domain?: string;
|
|
90
|
+
/** HttpOnly attribute. Default: true. */
|
|
91
|
+
httpOnly: boolean;
|
|
92
|
+
/** Secure attribute. Default: true. */
|
|
93
|
+
secure: boolean;
|
|
94
|
+
/** SameSite attribute. Default: 'lax'. */
|
|
95
|
+
sameSite: 'strict' | 'lax' | 'none';
|
|
96
|
+
/** Max-Age in seconds (falls back to the session TTL when unset). */
|
|
97
|
+
maxAge?: number;
|
|
98
|
+
}
|
|
99
|
+
export declare const SessionCookieConfigSchema: z.ZodObject<{
|
|
100
|
+
name: z.ZodDefault<z.ZodString>;
|
|
101
|
+
path: z.ZodDefault<z.ZodString>;
|
|
102
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
103
|
+
httpOnly: z.ZodDefault<z.ZodBoolean>;
|
|
104
|
+
secure: z.ZodDefault<z.ZodBoolean>;
|
|
105
|
+
sameSite: z.ZodDefault<z.ZodEnum<{
|
|
106
|
+
lax: "lax";
|
|
107
|
+
none: "none";
|
|
108
|
+
strict: "strict";
|
|
109
|
+
}>>;
|
|
110
|
+
maxAge: z.ZodOptional<z.ZodNumber>;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
/** Limits protecting Redis memory, Lua and pipelines. */
|
|
113
|
+
export interface SessionLimitsConfig {
|
|
114
|
+
/** Maximum serialized metadata size in bytes (reject larger writes). */
|
|
115
|
+
maxMetadataSize: number;
|
|
116
|
+
/** Maximum sessions fetched per list page. */
|
|
117
|
+
maxListPageSize: number;
|
|
118
|
+
/** Maximum session keys touched by one Lua script invocation. */
|
|
119
|
+
maxBatchSize: number;
|
|
120
|
+
/** Maximum concurrent cross-slot pipelines (revokeAll, jti cleanup). */
|
|
121
|
+
maxFanOutConcurrency: number;
|
|
122
|
+
/** Maximum sessions evicted by a single enforce-limit script call. */
|
|
123
|
+
maxEvictionsPerCall: number;
|
|
124
|
+
/** Maximum session deletions per user-request path (revokeAll/destroy-all). */
|
|
125
|
+
maxSessionsPerUserHardCap: number;
|
|
126
|
+
}
|
|
127
|
+
export declare const SessionLimitsConfigSchema: z.ZodObject<{
|
|
128
|
+
maxMetadataSize: z.ZodDefault<z.ZodNumber>;
|
|
129
|
+
maxListPageSize: z.ZodDefault<z.ZodNumber>;
|
|
130
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
131
|
+
maxFanOutConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
132
|
+
maxEvictionsPerCall: z.ZodDefault<z.ZodNumber>;
|
|
133
|
+
maxSessionsPerUserHardCap: z.ZodDefault<z.ZodNumber>;
|
|
134
|
+
}, z.core.$strip>;
|
|
135
|
+
/** Full parsed session configuration (defaults applied by the schema). */
|
|
136
|
+
export interface SessionConfig {
|
|
137
|
+
/**
|
|
138
|
+
* Master switch. Sessions are NOT enabled implicitly; an application
|
|
139
|
+
* must explicitly opt in. Default: false.
|
|
140
|
+
*/
|
|
141
|
+
enabled: boolean;
|
|
142
|
+
/** Key namespace for all session keys. Default: 'authcore'. */
|
|
143
|
+
namespace: string;
|
|
144
|
+
/**
|
|
145
|
+
* Raw session token entropy in bytes (32 = 256 bits). Minimum 16
|
|
146
|
+
* (128 bits). Default: 32.
|
|
147
|
+
*/
|
|
148
|
+
tokenBytes: number;
|
|
149
|
+
/**
|
|
150
|
+
* Absolute session lifetime in seconds (the hard maximum). Redis TTL is
|
|
151
|
+
* derived from this boundary; touch/rolling NEVER extends past it.
|
|
152
|
+
* Default: 7 days.
|
|
153
|
+
*/
|
|
154
|
+
ttl: number;
|
|
155
|
+
/**
|
|
156
|
+
* Idle timeout in seconds. When null, sessions never expire through
|
|
157
|
+
* inactivity. Default: 1 day.
|
|
158
|
+
*/
|
|
159
|
+
idleTimeout: number | null;
|
|
160
|
+
/**
|
|
161
|
+
* Rolling sessions: valid activity extends the idle boundary (never the
|
|
162
|
+
* absolute boundary). Only meaningful when idleTimeout is set.
|
|
163
|
+
* Default: true.
|
|
164
|
+
*/
|
|
165
|
+
rolling: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Touch throttling in seconds: touch() performs no write when the last
|
|
168
|
+
* activity is more recent than this interval. Default: 300.
|
|
169
|
+
*/
|
|
170
|
+
touchInterval: number;
|
|
171
|
+
/**
|
|
172
|
+
* Maximum concurrent sessions per user. 0 disables the limit.
|
|
173
|
+
* Default: 20.
|
|
174
|
+
*
|
|
175
|
+
* Enforcement is atomic per create (same-slot Lua): the create that
|
|
176
|
+
* pushes the count over the limit evicts the oldest excess sessions in
|
|
177
|
+
* the same script, so concurrent logins cannot both observe spare
|
|
178
|
+
* capacity. For extremely large per-user session counts, eviction is
|
|
179
|
+
* bounded per script call and converges over subsequent creates.
|
|
180
|
+
*/
|
|
181
|
+
maxSessionsPerUser: number;
|
|
182
|
+
/** Store the device id on creation (advisory binding). Default: false. */
|
|
183
|
+
storeDeviceId: boolean;
|
|
184
|
+
/** Store the IP address on creation (advisory binding). Default: false. */
|
|
185
|
+
storeIpAddress: boolean;
|
|
186
|
+
/** Store the user agent on creation (advisory binding). Default: false. */
|
|
187
|
+
storeUserAgent: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Session binding policy. 'disabled' (default) ignores binding fields;
|
|
190
|
+
* 'advisory' reports mismatches on validation; 'strict' rejects with
|
|
191
|
+
* reason 'binding_mismatch'. IP addresses change (NAT, mobile), user
|
|
192
|
+
* agents are spoofable, device ids may be absent — do not enable strict
|
|
193
|
+
* binding lightly.
|
|
194
|
+
*/
|
|
195
|
+
bindingPolicy: 'disabled' | 'advisory' | 'strict';
|
|
196
|
+
/**
|
|
197
|
+
* Security versioning. When enabled, validate() requires the session's
|
|
198
|
+
* securityVersion to match the current version stored at
|
|
199
|
+
* `{ns}:security-version:{userId}`. Use setSecurityVersion(userId, v)
|
|
200
|
+
* after password changes / MFA resets to invalidate all older sessions.
|
|
201
|
+
*/
|
|
202
|
+
securityVersion: {
|
|
203
|
+
enabled: boolean;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Optional global JTI -> userId lookup index.
|
|
207
|
+
*
|
|
208
|
+
* Default: false. Prefer passing userId to validate()/get()/rotate() —
|
|
209
|
+
* the authentication layer already knows it, and the index adds a write,
|
|
210
|
+
* a read, a second consistency boundary and a second key family. The
|
|
211
|
+
* index is NEVER authoritative: the session record is. It has its own
|
|
212
|
+
* TTL, self-heals on read, and a missing entry is not proof of absence
|
|
213
|
+
* (see docs/architecture for exact semantics).
|
|
214
|
+
*/
|
|
215
|
+
jtiIndex: {
|
|
216
|
+
enabled: boolean;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* Check the revocation store during validate(). Off by default:
|
|
220
|
+
* in-record revocation (status revoked/consumed) already covers rotation
|
|
221
|
+
* reuse and session-level revoke; the revocation store is for external
|
|
222
|
+
* JTI revocations (e.g. JWT jti denylists) and adds a second read.
|
|
223
|
+
*/
|
|
224
|
+
checkRevocationStore: boolean;
|
|
225
|
+
/** Optional AES-256-GCM encryption at rest. Default: disabled. */
|
|
226
|
+
encryption: SessionEncryptionConfig;
|
|
227
|
+
/** Optional fail-closed circuit breaker. Default: disabled. */
|
|
228
|
+
circuitBreaker: SessionCircuitBreakerConfig;
|
|
229
|
+
/** Metrics collection. Default: enabled (no-op without an adapter). */
|
|
230
|
+
metrics: SessionMetricsConfig;
|
|
231
|
+
/** Health check thresholds. */
|
|
232
|
+
health: SessionHealthConfig;
|
|
233
|
+
/** Cookie defaults for the framework-independent cookie manager. */
|
|
234
|
+
cookie: SessionCookieConfig;
|
|
235
|
+
/** Operational limits (memory, Lua, pipeline bounds). */
|
|
236
|
+
limits: SessionLimitsConfig;
|
|
237
|
+
/**
|
|
238
|
+
* Idempotent creation: when SessionCreateInput.idempotencyKey is set,
|
|
239
|
+
* store a short-lived claim so retries return the original session.
|
|
240
|
+
* Default: false.
|
|
241
|
+
*/
|
|
242
|
+
enableCreateIdempotency: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Retain a short-lived consumed tombstone after rotation instead of
|
|
245
|
+
* deleting the old record, enabling replay detection. The tombstone is
|
|
246
|
+
* bounded by the remaining absolute lifetime. Default: true.
|
|
247
|
+
*/
|
|
248
|
+
retainConsumedTombstones: boolean;
|
|
249
|
+
}
|
|
250
|
+
export declare const SessionConfigSchema: z.ZodObject<{
|
|
251
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
252
|
+
namespace: z.ZodDefault<z.ZodString>;
|
|
253
|
+
tokenBytes: z.ZodDefault<z.ZodNumber>;
|
|
254
|
+
ttl: z.ZodDefault<z.ZodNumber>;
|
|
255
|
+
idleTimeout: z.ZodDefault<z.ZodNullable<z.ZodNumber>>;
|
|
256
|
+
rolling: z.ZodDefault<z.ZodBoolean>;
|
|
257
|
+
touchInterval: z.ZodDefault<z.ZodNumber>;
|
|
258
|
+
maxSessionsPerUser: z.ZodDefault<z.ZodNumber>;
|
|
259
|
+
storeDeviceId: z.ZodDefault<z.ZodBoolean>;
|
|
260
|
+
storeIpAddress: z.ZodDefault<z.ZodBoolean>;
|
|
261
|
+
storeUserAgent: z.ZodDefault<z.ZodBoolean>;
|
|
262
|
+
bindingPolicy: z.ZodDefault<z.ZodEnum<{
|
|
263
|
+
advisory: "advisory";
|
|
264
|
+
disabled: "disabled";
|
|
265
|
+
strict: "strict";
|
|
266
|
+
}>>;
|
|
267
|
+
securityVersion: z.ZodPrefault<z.ZodObject<{
|
|
268
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
269
|
+
}, z.core.$strip>>;
|
|
270
|
+
jtiIndex: z.ZodPrefault<z.ZodObject<{
|
|
271
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
272
|
+
}, z.core.$strip>>;
|
|
273
|
+
checkRevocationStore: z.ZodDefault<z.ZodBoolean>;
|
|
274
|
+
encryption: z.ZodPrefault<z.ZodObject<{
|
|
275
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
276
|
+
reEncryptOnWrite: z.ZodDefault<z.ZodBoolean>;
|
|
277
|
+
}, z.core.$strip>>;
|
|
278
|
+
circuitBreaker: z.ZodPrefault<z.ZodObject<{
|
|
279
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
280
|
+
failureThreshold: z.ZodDefault<z.ZodNumber>;
|
|
281
|
+
resetTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
282
|
+
halfOpenMaxRequests: z.ZodDefault<z.ZodNumber>;
|
|
283
|
+
}, z.core.$strip>>;
|
|
284
|
+
metrics: z.ZodPrefault<z.ZodObject<{
|
|
285
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
286
|
+
}, z.core.$strip>>;
|
|
287
|
+
health: z.ZodPrefault<z.ZodObject<{
|
|
288
|
+
latencyThresholdMs: z.ZodDefault<z.ZodNumber>;
|
|
289
|
+
errorRateThreshold: z.ZodDefault<z.ZodNumber>;
|
|
290
|
+
errorWindowSize: z.ZodDefault<z.ZodNumber>;
|
|
291
|
+
}, z.core.$strip>>;
|
|
292
|
+
cookie: z.ZodPrefault<z.ZodObject<{
|
|
293
|
+
name: z.ZodDefault<z.ZodString>;
|
|
294
|
+
path: z.ZodDefault<z.ZodString>;
|
|
295
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
296
|
+
httpOnly: z.ZodDefault<z.ZodBoolean>;
|
|
297
|
+
secure: z.ZodDefault<z.ZodBoolean>;
|
|
298
|
+
sameSite: z.ZodDefault<z.ZodEnum<{
|
|
299
|
+
lax: "lax";
|
|
300
|
+
none: "none";
|
|
301
|
+
strict: "strict";
|
|
302
|
+
}>>;
|
|
303
|
+
maxAge: z.ZodOptional<z.ZodNumber>;
|
|
304
|
+
}, z.core.$strip>>;
|
|
305
|
+
limits: z.ZodPrefault<z.ZodObject<{
|
|
306
|
+
maxMetadataSize: z.ZodDefault<z.ZodNumber>;
|
|
307
|
+
maxListPageSize: z.ZodDefault<z.ZodNumber>;
|
|
308
|
+
maxBatchSize: z.ZodDefault<z.ZodNumber>;
|
|
309
|
+
maxFanOutConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
310
|
+
maxEvictionsPerCall: z.ZodDefault<z.ZodNumber>;
|
|
311
|
+
maxSessionsPerUserHardCap: z.ZodDefault<z.ZodNumber>;
|
|
312
|
+
}, z.core.$strip>>;
|
|
313
|
+
enableCreateIdempotency: z.ZodDefault<z.ZodBoolean>;
|
|
314
|
+
retainConsumedTombstones: z.ZodDefault<z.ZodBoolean>;
|
|
315
|
+
}, z.core.$strip>;
|
|
316
|
+
/** Recursively makes every config field optional (matches the schema's input shape). */
|
|
317
|
+
export type DeepPartial<T> = {
|
|
318
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
319
|
+
};
|
|
320
|
+
/** Raw unparsed config input (all fields optional, nested included). */
|
|
321
|
+
export type SessionConfigInput = DeepPartial<SessionConfig>;
|
|
322
|
+
/** Raw unparsed config (partial, defaults applied). */
|
|
323
|
+
export type PartialSessionConfig = Partial<SessionConfigInput>;
|
|
324
|
+
/**
|
|
325
|
+
* Parses and validates session configuration.
|
|
326
|
+
*
|
|
327
|
+
* @throws {SessionConfigurationError} with a safe message on invalid config.
|
|
328
|
+
*/
|
|
329
|
+
export declare function parseSessionConfig(input?: PartialSessionConfig): SessionConfig;
|
|
330
|
+
/**
|
|
331
|
+
* Returns a redacted copy of the config suitable for logging.
|
|
332
|
+
* Strips nothing by default (no secrets are allowed in config), but the
|
|
333
|
+
* serializer is explicit so future secret-bearing fields cannot leak.
|
|
334
|
+
*/
|
|
335
|
+
export declare function redactSessionConfig(config: SessionConfig): Record<string, unknown>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SessionConfigurationError } from './session-errors.js';
|
|
3
|
+
/* -------------------------------------------------------------------------- */
|
|
4
|
+
/* Session configuration. */
|
|
5
|
+
/* */
|
|
6
|
+
/* The public types below (SessionConfig + nested configs) are documented */
|
|
7
|
+
/* interfaces: they are what editors show in intellisense. The Zod schemas */
|
|
8
|
+
/* are the runtime validator; a compile-time shape guard (see bottom) keeps */
|
|
9
|
+
/* the interfaces and schemas in sync. */
|
|
10
|
+
/* */
|
|
11
|
+
/* Secrets (encryption keys, Redis credentials) NEVER live in this config. */
|
|
12
|
+
/* Encryption keys are injected via a SessionKeyProvider at construction. */
|
|
13
|
+
/* -------------------------------------------------------------------------- */
|
|
14
|
+
/** Default absolute session lifetime: 7 days. */
|
|
15
|
+
export const TTL = 7 * 24 * 60 * 60;
|
|
16
|
+
/** Default idle timeout: 24 hours. */
|
|
17
|
+
export const IDLE_TIMEOUT = 24 * 60 * 60;
|
|
18
|
+
/** Default touch throttle interval: 5 minutes. */
|
|
19
|
+
export const TOUCH_INTERVAL = 5 * 60;
|
|
20
|
+
export const SessionStatusSchema = z.enum(['active', 'consumed', 'revoked']);
|
|
21
|
+
/** How strictly session binding metadata (IP/UA/device) is enforced. */
|
|
22
|
+
export const SessionBindingPolicySchema = z.enum(['disabled', 'advisory', 'strict']);
|
|
23
|
+
export const SessionCircuitBreakerConfigSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
enabled: z.boolean().default(false),
|
|
26
|
+
failureThreshold: z.number().int().min(1).default(10),
|
|
27
|
+
resetTimeoutMs: z.number().int().min(1000).default(30_000),
|
|
28
|
+
halfOpenMaxRequests: z.number().int().min(1).default(5),
|
|
29
|
+
})
|
|
30
|
+
.prefault({});
|
|
31
|
+
export const SessionEncryptionConfigSchema = z.object({
|
|
32
|
+
enabled: z.boolean().default(false),
|
|
33
|
+
reEncryptOnWrite: z.boolean().default(true),
|
|
34
|
+
});
|
|
35
|
+
export const SessionMetricsConfigSchema = z.object({
|
|
36
|
+
enabled: z.boolean().default(true),
|
|
37
|
+
});
|
|
38
|
+
export const SessionHealthConfigSchema = z.object({
|
|
39
|
+
latencyThresholdMs: z.number().int().min(1).default(200),
|
|
40
|
+
errorRateThreshold: z.number().min(0).max(1).default(0.1),
|
|
41
|
+
errorWindowSize: z.number().int().min(1).default(100),
|
|
42
|
+
});
|
|
43
|
+
export const SessionCookieConfigSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
name: z.string().min(1).max(128).default('sid'),
|
|
46
|
+
path: z.string().min(1).default('/'),
|
|
47
|
+
domain: z.string().optional(),
|
|
48
|
+
httpOnly: z.boolean().default(true),
|
|
49
|
+
secure: z.boolean().default(true),
|
|
50
|
+
sameSite: z.enum(['strict', 'lax', 'none']).default('lax'),
|
|
51
|
+
maxAge: z.number().int().min(1).optional(),
|
|
52
|
+
})
|
|
53
|
+
.superRefine((data, ctx) => {
|
|
54
|
+
// SameSite=None is rejected by browsers unless Secure is set.
|
|
55
|
+
if (data.sameSite === 'none' && !data.secure) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
message: 'SameSite=None requires secure: true',
|
|
59
|
+
path: ['sameSite'],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
export const SessionLimitsConfigSchema = z.object({
|
|
64
|
+
maxMetadataSize: z.number().int().min(0).default(4096),
|
|
65
|
+
maxListPageSize: z.number().int().min(1).default(100),
|
|
66
|
+
maxBatchSize: z.number().int().min(1).max(500).default(100),
|
|
67
|
+
maxFanOutConcurrency: z.number().int().min(1).max(64).default(8),
|
|
68
|
+
maxEvictionsPerCall: z.number().int().min(1).max(5000).default(1000),
|
|
69
|
+
maxSessionsPerUserHardCap: z.number().int().min(0).default(10_000),
|
|
70
|
+
});
|
|
71
|
+
export const SessionConfigSchema = z
|
|
72
|
+
.object({
|
|
73
|
+
enabled: z.boolean().default(false),
|
|
74
|
+
namespace: z.string().min(1).max(64).default('authcore'),
|
|
75
|
+
tokenBytes: z.number().int().min(16).max(64).default(32),
|
|
76
|
+
ttl: z.number().int().min(1).default(TTL),
|
|
77
|
+
idleTimeout: z.number().int().min(1).nullable().default(IDLE_TIMEOUT),
|
|
78
|
+
rolling: z.boolean().default(true),
|
|
79
|
+
touchInterval: z.number().int().min(0).default(TOUCH_INTERVAL),
|
|
80
|
+
maxSessionsPerUser: z.number().int().min(0).default(20),
|
|
81
|
+
storeDeviceId: z.boolean().default(false),
|
|
82
|
+
storeIpAddress: z.boolean().default(false),
|
|
83
|
+
storeUserAgent: z.boolean().default(false),
|
|
84
|
+
bindingPolicy: SessionBindingPolicySchema.default('disabled'),
|
|
85
|
+
securityVersion: z
|
|
86
|
+
.object({
|
|
87
|
+
enabled: z.boolean().default(false),
|
|
88
|
+
})
|
|
89
|
+
.prefault({}),
|
|
90
|
+
jtiIndex: z
|
|
91
|
+
.object({
|
|
92
|
+
enabled: z.boolean().default(false),
|
|
93
|
+
})
|
|
94
|
+
.prefault({}),
|
|
95
|
+
checkRevocationStore: z.boolean().default(false),
|
|
96
|
+
encryption: SessionEncryptionConfigSchema.prefault({}),
|
|
97
|
+
circuitBreaker: SessionCircuitBreakerConfigSchema,
|
|
98
|
+
metrics: SessionMetricsConfigSchema.prefault({}),
|
|
99
|
+
health: SessionHealthConfigSchema.prefault({}),
|
|
100
|
+
cookie: SessionCookieConfigSchema.prefault({}),
|
|
101
|
+
limits: SessionLimitsConfigSchema.prefault({}),
|
|
102
|
+
enableCreateIdempotency: z.boolean().default(false),
|
|
103
|
+
retainConsumedTombstones: z.boolean().default(true),
|
|
104
|
+
})
|
|
105
|
+
.superRefine((data, ctx) => {
|
|
106
|
+
if (data.idleTimeout !== null && data.idleTimeout > data.ttl) {
|
|
107
|
+
ctx.addIssue({
|
|
108
|
+
code: z.ZodIssueCode.custom,
|
|
109
|
+
message: 'idleTimeout must not exceed ttl (absolute lifetime)',
|
|
110
|
+
path: ['idleTimeout'],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (data.touchInterval > data.ttl) {
|
|
114
|
+
ctx.addIssue({
|
|
115
|
+
code: z.ZodIssueCode.custom,
|
|
116
|
+
message: 'touchInterval must not exceed ttl',
|
|
117
|
+
path: ['touchInterval'],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* Parses and validates session configuration.
|
|
123
|
+
*
|
|
124
|
+
* @throws {SessionConfigurationError} with a safe message on invalid config.
|
|
125
|
+
*/
|
|
126
|
+
export function parseSessionConfig(input = {}) {
|
|
127
|
+
try {
|
|
128
|
+
return SessionConfigSchema.parse(input);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof z.ZodError) {
|
|
132
|
+
const first = error.issues[0];
|
|
133
|
+
const where = first?.path.length ? ` at "${first.path.join('.')}"` : '';
|
|
134
|
+
throw new SessionConfigurationError(`Invalid session configuration${where}: ${first?.message ?? 'unknown error'}`);
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns a redacted copy of the config suitable for logging.
|
|
141
|
+
* Strips nothing by default (no secrets are allowed in config), but the
|
|
142
|
+
* serializer is explicit so future secret-bearing fields cannot leak.
|
|
143
|
+
*/
|
|
144
|
+
export function redactSessionConfig(config) {
|
|
145
|
+
return {
|
|
146
|
+
enabled: config.enabled,
|
|
147
|
+
namespace: config.namespace,
|
|
148
|
+
tokenBytes: config.tokenBytes,
|
|
149
|
+
ttl: config.ttl,
|
|
150
|
+
idleTimeout: config.idleTimeout,
|
|
151
|
+
rolling: config.rolling,
|
|
152
|
+
touchInterval: config.touchInterval,
|
|
153
|
+
maxSessionsPerUser: config.maxSessionsPerUser,
|
|
154
|
+
bindingPolicy: config.bindingPolicy,
|
|
155
|
+
securityVersion: config.securityVersion.enabled,
|
|
156
|
+
jtiIndex: config.jtiIndex.enabled,
|
|
157
|
+
checkRevocationStore: config.checkRevocationStore,
|
|
158
|
+
encryption: { enabled: config.encryption.enabled },
|
|
159
|
+
circuitBreaker: { enabled: config.circuitBreaker.enabled },
|
|
160
|
+
cookie: { name: config.cookie.name, secure: config.cookie.secure },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SessionCookieConfig } from './session-config.js';
|
|
2
|
+
export interface SerializeCookieOptions {
|
|
3
|
+
/** Overrides the Max-Age (seconds). Defaults to config.maxAge. */
|
|
4
|
+
maxAge?: number;
|
|
5
|
+
/** Overrides the Path attribute. */
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Structured Set-Cookie attributes, mirroring the serialized header. */
|
|
9
|
+
export interface SerializedCookieAttributes {
|
|
10
|
+
path: string;
|
|
11
|
+
domain?: string;
|
|
12
|
+
httpOnly: boolean;
|
|
13
|
+
secure: boolean;
|
|
14
|
+
sameSite: 'strict' | 'lax' | 'none';
|
|
15
|
+
maxAge?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A serialized cookie: the `Set-Cookie` header string plus the same
|
|
19
|
+
* attributes as structured fields for frameworks that need the pieces
|
|
20
|
+
* (or for tests asserting the exact shape).
|
|
21
|
+
*/
|
|
22
|
+
export interface SerializedCookie {
|
|
23
|
+
/** The full `Set-Cookie` header value. */
|
|
24
|
+
header: string;
|
|
25
|
+
/** The cookie name. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** The cookie value — the raw session token. */
|
|
28
|
+
value: string;
|
|
29
|
+
/** Structured attributes mirroring the header. */
|
|
30
|
+
attributes: SerializedCookieAttributes;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Builds Set-Cookie values and reads Cookie headers for session tokens.
|
|
34
|
+
*/
|
|
35
|
+
export declare class SessionCookieManager {
|
|
36
|
+
private readonly config;
|
|
37
|
+
constructor(config: SessionCookieConfig);
|
|
38
|
+
/** The configured cookie name. */
|
|
39
|
+
get name(): string;
|
|
40
|
+
/**
|
|
41
|
+
* Builds the `Set-Cookie` header value for a freshly created session.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const header = cookies.serialize(token, { maxAge: ttlSeconds });
|
|
46
|
+
* res.setHeader('Set-Cookie', header);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
serialize(token: string, options?: SerializeCookieOptions): string;
|
|
50
|
+
/**
|
|
51
|
+
* Like {@link serialize}, but returns the header string together with the
|
|
52
|
+
* structured cookie object.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const cookie = cookies.serializeWithAttributes(token, { maxAge: ttlSeconds });
|
|
57
|
+
* res.setHeader('Set-Cookie', cookie.header);
|
|
58
|
+
* console.log(cookie.attributes.sameSite);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
serializeWithAttributes(token: string, options?: SerializeCookieOptions): SerializedCookie;
|
|
62
|
+
private buildHeader;
|
|
63
|
+
/**
|
|
64
|
+
* Builds the `Set-Cookie` header value that expires the cookie immediately.
|
|
65
|
+
*/
|
|
66
|
+
clear(options?: SerializeCookieOptions): string;
|
|
67
|
+
/**
|
|
68
|
+
* Extracts the session token from a `Cookie` request header, or null when
|
|
69
|
+
* the cookie is absent or its value is empty.
|
|
70
|
+
*/
|
|
71
|
+
parse(header: string | null | undefined): string | null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds Set-Cookie values and reads Cookie headers for session tokens.
|
|
3
|
+
*/
|
|
4
|
+
export class SessionCookieManager {
|
|
5
|
+
config;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
/** The configured cookie name. */
|
|
10
|
+
get name() {
|
|
11
|
+
return this.config.name;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Builds the `Set-Cookie` header value for a freshly created session.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const header = cookies.serialize(token, { maxAge: ttlSeconds });
|
|
19
|
+
* res.setHeader('Set-Cookie', header);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
serialize(token, options = {}) {
|
|
23
|
+
return this.serializeWithAttributes(token, options).header;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Like {@link serialize}, but returns the header string together with the
|
|
27
|
+
* structured cookie object.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const cookie = cookies.serializeWithAttributes(token, { maxAge: ttlSeconds });
|
|
32
|
+
* res.setHeader('Set-Cookie', cookie.header);
|
|
33
|
+
* console.log(cookie.attributes.sameSite);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
serializeWithAttributes(token, options = {}) {
|
|
37
|
+
const path = options.path ?? this.config.path;
|
|
38
|
+
const maxAge = options.maxAge ?? this.config.maxAge;
|
|
39
|
+
const header = this.buildHeader(token, path, maxAge);
|
|
40
|
+
const attributes = {
|
|
41
|
+
path,
|
|
42
|
+
httpOnly: this.config.httpOnly,
|
|
43
|
+
secure: this.config.secure,
|
|
44
|
+
sameSite: this.config.sameSite,
|
|
45
|
+
};
|
|
46
|
+
if (this.config.domain)
|
|
47
|
+
attributes.domain = this.config.domain;
|
|
48
|
+
if (maxAge !== undefined)
|
|
49
|
+
attributes.maxAge = Math.max(0, Math.floor(maxAge));
|
|
50
|
+
return { header, name: this.config.name, value: token, attributes };
|
|
51
|
+
}
|
|
52
|
+
buildHeader(token, path, maxAge) {
|
|
53
|
+
const parts = [`${this.config.name}=${token}`];
|
|
54
|
+
parts.push(`Path=${path}`);
|
|
55
|
+
if (this.config.domain) {
|
|
56
|
+
parts.push(`Domain=${this.config.domain}`);
|
|
57
|
+
}
|
|
58
|
+
if (this.config.httpOnly)
|
|
59
|
+
parts.push('HttpOnly');
|
|
60
|
+
if (this.config.secure)
|
|
61
|
+
parts.push('Secure');
|
|
62
|
+
parts.push(`SameSite=${capitalize(this.config.sameSite)}`);
|
|
63
|
+
if (maxAge !== undefined) {
|
|
64
|
+
parts.push(`Max-Age=${Math.max(0, Math.floor(maxAge))}`);
|
|
65
|
+
}
|
|
66
|
+
return parts.join('; ');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Builds the `Set-Cookie` header value that expires the cookie immediately.
|
|
70
|
+
*/
|
|
71
|
+
clear(options = {}) {
|
|
72
|
+
const parts = [`${this.config.name}=`];
|
|
73
|
+
parts.push(`Path=${options.path ?? this.config.path}`);
|
|
74
|
+
if (this.config.domain) {
|
|
75
|
+
parts.push(`Domain=${this.config.domain}`);
|
|
76
|
+
}
|
|
77
|
+
parts.push('Max-Age=0');
|
|
78
|
+
parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');
|
|
79
|
+
return parts.join('; ');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Extracts the session token from a `Cookie` request header, or null when
|
|
83
|
+
* the cookie is absent or its value is empty.
|
|
84
|
+
*/
|
|
85
|
+
parse(header) {
|
|
86
|
+
if (!header)
|
|
87
|
+
return null;
|
|
88
|
+
const prefix = `${this.config.name}=`;
|
|
89
|
+
for (const part of header.split(';')) {
|
|
90
|
+
const trimmed = part.trim();
|
|
91
|
+
if (trimmed.startsWith(prefix)) {
|
|
92
|
+
const value = trimmed.slice(prefix.length);
|
|
93
|
+
return value.length > 0 ? value : null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function capitalize(value) {
|
|
100
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
101
|
+
}
|