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,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
+ }