@veloxts/auth 0.3.2 → 0.3.4

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 (54) hide show
  1. package/README.md +755 -30
  2. package/dist/adapter.d.ts +710 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +581 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/adapters/better-auth.d.ts +271 -0
  7. package/dist/adapters/better-auth.d.ts.map +1 -0
  8. package/dist/adapters/better-auth.js +341 -0
  9. package/dist/adapters/better-auth.js.map +1 -0
  10. package/dist/adapters/index.d.ts +28 -0
  11. package/dist/adapters/index.d.ts.map +1 -0
  12. package/dist/adapters/index.js +28 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/csrf.d.ts +294 -0
  15. package/dist/csrf.d.ts.map +1 -0
  16. package/dist/csrf.js +396 -0
  17. package/dist/csrf.js.map +1 -0
  18. package/dist/guards.d.ts +139 -0
  19. package/dist/guards.d.ts.map +1 -0
  20. package/dist/guards.js +247 -0
  21. package/dist/guards.js.map +1 -0
  22. package/dist/hash.d.ts +85 -0
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/hash.js +220 -0
  25. package/dist/hash.js.map +1 -0
  26. package/dist/index.d.ts +25 -31
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +63 -31
  29. package/dist/index.js.map +1 -1
  30. package/dist/jwt.d.ts +128 -0
  31. package/dist/jwt.d.ts.map +1 -0
  32. package/dist/jwt.js +363 -0
  33. package/dist/jwt.js.map +1 -0
  34. package/dist/middleware.d.ts +87 -0
  35. package/dist/middleware.d.ts.map +1 -0
  36. package/dist/middleware.js +241 -0
  37. package/dist/middleware.js.map +1 -0
  38. package/dist/plugin.d.ts +107 -0
  39. package/dist/plugin.d.ts.map +1 -0
  40. package/dist/plugin.js +174 -0
  41. package/dist/plugin.js.map +1 -0
  42. package/dist/policies.d.ts +137 -0
  43. package/dist/policies.d.ts.map +1 -0
  44. package/dist/policies.js +240 -0
  45. package/dist/policies.js.map +1 -0
  46. package/dist/session.d.ts +494 -0
  47. package/dist/session.d.ts.map +1 -0
  48. package/dist/session.js +795 -0
  49. package/dist/session.js.map +1 -0
  50. package/dist/types.d.ts +251 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +33 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +38 -7
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Cookie-based Session Management for @veloxts/auth
3
+ *
4
+ * Provides server-side session storage with pluggable backends,
5
+ * secure session ID generation, and automatic session lifecycle management.
6
+ *
7
+ * Alternative to JWT authentication - users choose one or the other.
8
+ *
9
+ * @module auth/session
10
+ */
11
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
12
+ import { AuthError } from './types.js';
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+ /**
17
+ * Session ID entropy in bytes (32 bytes = 256 bits)
18
+ * Provides sufficient entropy to prevent brute-force attacks
19
+ */
20
+ const SESSION_ID_BYTES = 32;
21
+ /**
22
+ * Minimum secret length for session ID signing (32 characters)
23
+ */
24
+ const MIN_SECRET_LENGTH = 32;
25
+ /**
26
+ * Default session TTL (24 hours in seconds)
27
+ */
28
+ const DEFAULT_SESSION_TTL = 86400;
29
+ /**
30
+ * Default cookie name
31
+ */
32
+ const DEFAULT_COOKIE_NAME = 'velox.session';
33
+ // ============================================================================
34
+ // In-Memory Session Store
35
+ // ============================================================================
36
+ /**
37
+ * In-memory session store for development and testing
38
+ *
39
+ * WARNING: NOT suitable for production!
40
+ * - Sessions are lost on server restart
41
+ * - Does not work across multiple server instances
42
+ * - No persistence mechanism
43
+ *
44
+ * For production, use Redis or database-backed storage.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * const store = createInMemorySessionStore();
49
+ *
50
+ * const sessionManager = createSessionManager({
51
+ * store,
52
+ * secret: process.env.SESSION_SECRET!,
53
+ * });
54
+ * ```
55
+ */
56
+ export function createInMemorySessionStore() {
57
+ const sessions = new Map();
58
+ const userSessions = new Map();
59
+ /**
60
+ * Clean up expired sessions
61
+ */
62
+ function cleanup() {
63
+ const now = Date.now();
64
+ for (const [id, session] of sessions) {
65
+ if (session.expiresAt <= now) {
66
+ // Remove from user index
67
+ const userId = session.data.userId;
68
+ if (userId) {
69
+ const userSessionSet = userSessions.get(userId);
70
+ if (userSessionSet) {
71
+ userSessionSet.delete(id);
72
+ if (userSessionSet.size === 0) {
73
+ userSessions.delete(userId);
74
+ }
75
+ }
76
+ }
77
+ sessions.delete(id);
78
+ }
79
+ }
80
+ }
81
+ // Run cleanup periodically (every 5 minutes)
82
+ const cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
83
+ // Don't prevent process exit
84
+ cleanupInterval.unref();
85
+ return {
86
+ get(sessionId) {
87
+ const session = sessions.get(sessionId);
88
+ if (!session) {
89
+ return null;
90
+ }
91
+ // Check expiration
92
+ if (session.expiresAt <= Date.now()) {
93
+ sessions.delete(sessionId);
94
+ return null;
95
+ }
96
+ return session;
97
+ },
98
+ set(sessionId, session) {
99
+ // Track user sessions for getSessionsByUser
100
+ const existingSession = sessions.get(sessionId);
101
+ const oldUserId = existingSession?.data.userId;
102
+ const newUserId = session.data.userId;
103
+ // Update user index if userId changed
104
+ if (oldUserId && oldUserId !== newUserId) {
105
+ const oldSet = userSessions.get(oldUserId);
106
+ if (oldSet) {
107
+ oldSet.delete(sessionId);
108
+ if (oldSet.size === 0) {
109
+ userSessions.delete(oldUserId);
110
+ }
111
+ }
112
+ }
113
+ if (newUserId) {
114
+ let userSet = userSessions.get(newUserId);
115
+ if (!userSet) {
116
+ userSet = new Set();
117
+ userSessions.set(newUserId, userSet);
118
+ }
119
+ userSet.add(sessionId);
120
+ }
121
+ sessions.set(sessionId, session);
122
+ },
123
+ delete(sessionId) {
124
+ const session = sessions.get(sessionId);
125
+ if (session?.data.userId) {
126
+ const userSet = userSessions.get(session.data.userId);
127
+ if (userSet) {
128
+ userSet.delete(sessionId);
129
+ if (userSet.size === 0) {
130
+ userSessions.delete(session.data.userId);
131
+ }
132
+ }
133
+ }
134
+ sessions.delete(sessionId);
135
+ },
136
+ touch(sessionId, expiresAt) {
137
+ const session = sessions.get(sessionId);
138
+ if (session) {
139
+ session.expiresAt = expiresAt;
140
+ session.data._lastAccessedAt = Date.now();
141
+ }
142
+ },
143
+ clear() {
144
+ sessions.clear();
145
+ userSessions.clear();
146
+ },
147
+ getSessionsByUser(userId) {
148
+ const userSet = userSessions.get(userId);
149
+ return userSet ? [...userSet] : [];
150
+ },
151
+ deleteSessionsByUser(userId) {
152
+ const userSet = userSessions.get(userId);
153
+ if (userSet) {
154
+ for (const sessionId of userSet) {
155
+ sessions.delete(sessionId);
156
+ }
157
+ userSessions.delete(userId);
158
+ }
159
+ },
160
+ };
161
+ }
162
+ // ============================================================================
163
+ // Session ID Generation and Signing
164
+ // ============================================================================
165
+ /**
166
+ * Generate a cryptographically secure session ID
167
+ */
168
+ function generateSessionId() {
169
+ const bytes = randomBytes(SESSION_ID_BYTES);
170
+ // Use base64url encoding for URL-safe session IDs
171
+ return bytes.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
172
+ }
173
+ /**
174
+ * Sign a session ID with HMAC-SHA256
175
+ */
176
+ function signSessionId(sessionId, secret) {
177
+ const hmac = createHmac('sha256', secret);
178
+ hmac.update(sessionId);
179
+ const signature = hmac.digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
180
+ return `${sessionId}.${signature}`;
181
+ }
182
+ /**
183
+ * Verify and extract session ID from signed value
184
+ * Uses timing-safe comparison to prevent timing attacks
185
+ */
186
+ function verifySessionId(signedId, secret) {
187
+ const dotIndex = signedId.lastIndexOf('.');
188
+ if (dotIndex === -1) {
189
+ return null;
190
+ }
191
+ const sessionId = signedId.slice(0, dotIndex);
192
+ const signature = signedId.slice(dotIndex + 1);
193
+ // Recompute expected signature
194
+ const hmac = createHmac('sha256', secret);
195
+ hmac.update(sessionId);
196
+ const expectedSignature = hmac
197
+ .digest('base64')
198
+ .replace(/\+/g, '-')
199
+ .replace(/\//g, '_')
200
+ .replace(/=/g, '');
201
+ // Timing-safe comparison
202
+ const sigBuffer = Buffer.from(signature, 'utf8');
203
+ const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
204
+ if (sigBuffer.length !== expectedBuffer.length) {
205
+ return null;
206
+ }
207
+ if (!timingSafeEqual(sigBuffer, expectedBuffer)) {
208
+ return null;
209
+ }
210
+ return sessionId;
211
+ }
212
+ /**
213
+ * Validate session ID entropy (prevent weak session IDs)
214
+ */
215
+ function validateSessionIdEntropy(sessionId) {
216
+ // Session ID should be at least 32 bytes when decoded
217
+ // Our base64url encoding produces ~43 characters for 32 bytes
218
+ if (sessionId.length < 40) {
219
+ return false;
220
+ }
221
+ // Check for sufficient character variety (basic entropy check)
222
+ const uniqueChars = new Set(sessionId).size;
223
+ return uniqueChars >= 16;
224
+ }
225
+ // ============================================================================
226
+ // Session Manager Implementation
227
+ // ============================================================================
228
+ /**
229
+ * Creates a session manager
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const sessionManager = createSessionManager({
234
+ * secret: process.env.SESSION_SECRET!,
235
+ * cookie: {
236
+ * name: 'myapp.session',
237
+ * secure: true,
238
+ * sameSite: 'strict',
239
+ * },
240
+ * expiration: {
241
+ * ttl: 3600, // 1 hour
242
+ * sliding: true,
243
+ * },
244
+ * });
245
+ * ```
246
+ */
247
+ export function createSessionManager(config) {
248
+ // Validate secret
249
+ if (!config.secret || config.secret.length < MIN_SECRET_LENGTH) {
250
+ throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters. ` +
251
+ 'Generate with: openssl rand -base64 32');
252
+ }
253
+ // Validate secret entropy
254
+ const uniqueChars = new Set(config.secret).size;
255
+ if (uniqueChars < 16) {
256
+ throw new Error(`Session secret has insufficient entropy (only ${uniqueChars} unique characters). ` +
257
+ 'Use cryptographically random data with at least 16 unique characters.');
258
+ }
259
+ // Initialize store
260
+ const store = config.store ?? createInMemorySessionStore();
261
+ // Cookie configuration
262
+ const cookieName = config.cookie?.name ?? DEFAULT_COOKIE_NAME;
263
+ const cookiePath = config.cookie?.path ?? '/';
264
+ const cookieDomain = config.cookie?.domain;
265
+ const cookieSecure = config.cookie?.secure ?? process.env.NODE_ENV === 'production';
266
+ const cookieHttpOnly = config.cookie?.httpOnly ?? true;
267
+ const cookieSameSite = config.cookie?.sameSite ?? 'lax';
268
+ // Expiration configuration
269
+ const ttl = config.expiration?.ttl ?? DEFAULT_SESSION_TTL;
270
+ const sliding = config.expiration?.sliding ?? true;
271
+ const absoluteTimeout = config.expiration?.absoluteTimeout;
272
+ // Security validation: SameSite=none requires Secure
273
+ if (cookieSameSite === 'none' && !cookieSecure) {
274
+ throw new Error('Session cookie with SameSite=none requires Secure flag. ' +
275
+ 'Set cookie.secure: true or use a different SameSite policy.');
276
+ }
277
+ /**
278
+ * Set session cookie
279
+ */
280
+ function setCookie(reply, signedId) {
281
+ reply.cookie(cookieName, signedId, {
282
+ path: cookiePath,
283
+ domain: cookieDomain,
284
+ secure: cookieSecure,
285
+ httpOnly: cookieHttpOnly,
286
+ sameSite: cookieSameSite,
287
+ maxAge: ttl,
288
+ });
289
+ }
290
+ /**
291
+ * Clear session cookie
292
+ */
293
+ function clearCookie(reply) {
294
+ reply.clearCookie(cookieName, {
295
+ path: cookiePath,
296
+ domain: cookieDomain,
297
+ });
298
+ }
299
+ /**
300
+ * Create a Session handle
301
+ */
302
+ function createSessionHandle(sessionId, data, expiresAt, isNew, reply) {
303
+ let modified = isNew;
304
+ let destroyed = false;
305
+ let currentId = sessionId;
306
+ let currentData = { ...data };
307
+ let currentExpiresAt = expiresAt;
308
+ // Move flash data to old flash for reading
309
+ if (currentData._flash) {
310
+ currentData._flashOld = currentData._flash;
311
+ delete currentData._flash;
312
+ modified = true;
313
+ }
314
+ const session = {
315
+ get id() {
316
+ return currentId;
317
+ },
318
+ get isNew() {
319
+ return isNew;
320
+ },
321
+ get isModified() {
322
+ return modified;
323
+ },
324
+ get isDestroyed() {
325
+ return destroyed;
326
+ },
327
+ get data() {
328
+ return currentData;
329
+ },
330
+ get(key) {
331
+ return currentData[key];
332
+ },
333
+ set(key, value) {
334
+ if (destroyed) {
335
+ throw new AuthError('Cannot modify destroyed session', 400, 'SESSION_DESTROYED');
336
+ }
337
+ currentData[key] = value;
338
+ modified = true;
339
+ },
340
+ delete(key) {
341
+ if (destroyed) {
342
+ throw new AuthError('Cannot modify destroyed session', 400, 'SESSION_DESTROYED');
343
+ }
344
+ delete currentData[key];
345
+ modified = true;
346
+ },
347
+ has(key) {
348
+ return key in currentData;
349
+ },
350
+ flash(key, value) {
351
+ if (destroyed) {
352
+ throw new AuthError('Cannot modify destroyed session', 400, 'SESSION_DESTROYED');
353
+ }
354
+ if (!currentData._flash) {
355
+ currentData._flash = {};
356
+ }
357
+ currentData._flash[key] = value;
358
+ modified = true;
359
+ },
360
+ getFlash(key) {
361
+ const value = currentData._flashOld?.[key];
362
+ return value;
363
+ },
364
+ getAllFlash() {
365
+ return currentData._flashOld ?? {};
366
+ },
367
+ async regenerate() {
368
+ if (destroyed) {
369
+ throw new AuthError('Cannot regenerate destroyed session', 400, 'SESSION_DESTROYED');
370
+ }
371
+ // Delete old session
372
+ await store.delete(currentId);
373
+ // Generate new ID
374
+ const newSessionId = generateSessionId();
375
+ const newSignedId = signSessionId(newSessionId, config.secret);
376
+ // Update tracking
377
+ currentId = newSessionId;
378
+ currentData._lastAccessedAt = Date.now();
379
+ modified = true;
380
+ // Set new cookie
381
+ setCookie(reply, newSignedId);
382
+ // Save with new ID
383
+ await store.set(currentId, {
384
+ id: currentId,
385
+ data: currentData,
386
+ expiresAt: currentExpiresAt,
387
+ });
388
+ },
389
+ async destroy() {
390
+ await store.delete(currentId);
391
+ clearCookie(reply);
392
+ destroyed = true;
393
+ currentData = {};
394
+ },
395
+ async save() {
396
+ if (destroyed) {
397
+ return;
398
+ }
399
+ // Clear old flash data after it's been read
400
+ delete currentData._flashOld;
401
+ // Update last accessed time
402
+ currentData._lastAccessedAt = Date.now();
403
+ // Check absolute timeout
404
+ if (absoluteTimeout) {
405
+ const absoluteExpiresAt = currentData._createdAt + absoluteTimeout * 1000;
406
+ if (Date.now() >= absoluteExpiresAt) {
407
+ await session.destroy();
408
+ throw new AuthError('Session has reached absolute timeout', 401, 'SESSION_EXPIRED');
409
+ }
410
+ // Cap expiration at absolute timeout
411
+ currentExpiresAt = Math.min(currentExpiresAt, absoluteExpiresAt);
412
+ }
413
+ // Update sliding expiration
414
+ if (sliding) {
415
+ currentExpiresAt = Date.now() + ttl * 1000;
416
+ }
417
+ await store.set(currentId, {
418
+ id: currentId,
419
+ data: currentData,
420
+ expiresAt: currentExpiresAt,
421
+ });
422
+ // Refresh cookie with new expiration
423
+ const signedId = signSessionId(currentId, config.secret);
424
+ setCookie(reply, signedId);
425
+ modified = false;
426
+ },
427
+ async reload() {
428
+ if (destroyed) {
429
+ throw new AuthError('Cannot reload destroyed session', 400, 'SESSION_DESTROYED');
430
+ }
431
+ const stored = await store.get(currentId);
432
+ if (!stored) {
433
+ throw new AuthError('Session not found', 401, 'SESSION_NOT_FOUND');
434
+ }
435
+ currentData = { ...stored.data };
436
+ currentExpiresAt = stored.expiresAt;
437
+ modified = false;
438
+ },
439
+ };
440
+ return session;
441
+ }
442
+ return {
443
+ store,
444
+ createSession(reply) {
445
+ const sessionId = generateSessionId();
446
+ const signedId = signSessionId(sessionId, config.secret);
447
+ const now = Date.now();
448
+ const expiresAt = now + ttl * 1000;
449
+ const data = {
450
+ _createdAt: now,
451
+ _lastAccessedAt: now,
452
+ };
453
+ // Set cookie
454
+ setCookie(reply, signedId);
455
+ return createSessionHandle(sessionId, data, expiresAt, true, reply);
456
+ },
457
+ async loadSession(request) {
458
+ const signedId = request.cookies[cookieName];
459
+ if (!signedId) {
460
+ return null;
461
+ }
462
+ // Verify signature and extract session ID
463
+ const sessionId = verifySessionId(signedId, config.secret);
464
+ if (!sessionId) {
465
+ return null;
466
+ }
467
+ // Validate session ID entropy
468
+ if (!validateSessionIdEntropy(sessionId)) {
469
+ return null;
470
+ }
471
+ // Load from store
472
+ const stored = await store.get(sessionId);
473
+ if (!stored) {
474
+ return null;
475
+ }
476
+ // Check expiration
477
+ if (stored.expiresAt <= Date.now()) {
478
+ await store.delete(sessionId);
479
+ return null;
480
+ }
481
+ // We need reply for the session handle, but loadSession doesn't have it
482
+ // This is intentional - loadSession is for checking only
483
+ // Use getOrCreateSession for full session handling
484
+ return null;
485
+ },
486
+ async getOrCreateSession(request, reply) {
487
+ const signedId = request.cookies[cookieName];
488
+ if (signedId) {
489
+ // Verify signature
490
+ const sessionId = verifySessionId(signedId, config.secret);
491
+ if (sessionId && validateSessionIdEntropy(sessionId)) {
492
+ // Load from store
493
+ const stored = await store.get(sessionId);
494
+ if (stored && stored.expiresAt > Date.now()) {
495
+ // Check absolute timeout before returning
496
+ if (absoluteTimeout) {
497
+ const absoluteExpiresAt = stored.data._createdAt + absoluteTimeout * 1000;
498
+ if (Date.now() >= absoluteExpiresAt) {
499
+ await store.delete(sessionId);
500
+ clearCookie(reply);
501
+ // Create new session
502
+ return this.createSession(reply);
503
+ }
504
+ }
505
+ return createSessionHandle(sessionId, stored.data, stored.expiresAt, false, reply);
506
+ }
507
+ }
508
+ }
509
+ // Create new session
510
+ return this.createSession(reply);
511
+ },
512
+ async destroySession(sessionId) {
513
+ await store.delete(sessionId);
514
+ },
515
+ async destroyUserSessions(userId) {
516
+ if (store.deleteSessionsByUser) {
517
+ await store.deleteSessionsByUser(userId);
518
+ }
519
+ else if (store.getSessionsByUser) {
520
+ const sessions = await store.getSessionsByUser(userId);
521
+ for (const sessionId of sessions) {
522
+ await store.delete(sessionId);
523
+ }
524
+ }
525
+ // If store doesn't support user session tracking, silently skip
526
+ },
527
+ clearCookie,
528
+ };
529
+ }
530
+ // ============================================================================
531
+ // Session Middleware Factory
532
+ // ============================================================================
533
+ /**
534
+ * Creates session middleware for procedures
535
+ *
536
+ * @example
537
+ * ```typescript
538
+ * const session = createSessionMiddleware({
539
+ * secret: process.env.SESSION_SECRET!,
540
+ * cookie: { secure: true },
541
+ * });
542
+ *
543
+ * // Use in procedures
544
+ * const getCart = procedure()
545
+ * .use(session.middleware())
546
+ * .query(async ({ ctx }) => {
547
+ * return ctx.session.get('cart') ?? [];
548
+ * });
549
+ *
550
+ * // Require authentication
551
+ * const getProfile = procedure()
552
+ * .use(session.requireAuth())
553
+ * .query(async ({ ctx }) => {
554
+ * return ctx.user;
555
+ * });
556
+ * ```
557
+ */
558
+ export function createSessionMiddleware(config) {
559
+ const manager = createSessionManager(config);
560
+ /**
561
+ * Base session middleware
562
+ */
563
+ function middleware(options = {}) {
564
+ return async ({ ctx, next }) => {
565
+ const request = ctx.request;
566
+ const reply = ctx.reply;
567
+ let session;
568
+ if (options.lazy) {
569
+ // Lazy mode: only create session if accessed
570
+ // This requires a proxy to detect access
571
+ let realSession = null;
572
+ const lazySession = new Proxy({}, {
573
+ get(_target, prop) {
574
+ if (!realSession) {
575
+ // Create session on first access
576
+ // Note: This is synchronous, so we can't use getOrCreateSession
577
+ // For lazy mode, we create a new session
578
+ realSession = manager.createSession(reply);
579
+ }
580
+ const value = realSession[prop];
581
+ if (typeof value === 'function') {
582
+ return value.bind(realSession);
583
+ }
584
+ return value;
585
+ },
586
+ });
587
+ session = lazySession;
588
+ }
589
+ else {
590
+ session = await manager.getOrCreateSession(request, reply);
591
+ }
592
+ // Attach to request for hooks
593
+ request.session = session;
594
+ try {
595
+ const result = await next({
596
+ ctx: {
597
+ ...ctx,
598
+ session,
599
+ },
600
+ });
601
+ // Auto-save session if modified
602
+ if (session.isModified && !session.isDestroyed) {
603
+ await session.save();
604
+ }
605
+ return result;
606
+ }
607
+ catch (error) {
608
+ // Still try to save session on error
609
+ if (session.isModified && !session.isDestroyed) {
610
+ try {
611
+ await session.save();
612
+ }
613
+ catch {
614
+ // Ignore save errors during error handling
615
+ }
616
+ }
617
+ throw error;
618
+ }
619
+ };
620
+ }
621
+ /**
622
+ * Middleware that requires authentication
623
+ */
624
+ function requireAuth() {
625
+ return async ({ ctx, next }) => {
626
+ const request = ctx.request;
627
+ const reply = ctx.reply;
628
+ const session = await manager.getOrCreateSession(request, reply);
629
+ // Check if session has userId
630
+ const userId = session.get('userId');
631
+ if (!userId) {
632
+ throw new AuthError('Authentication required', 401, 'SESSION_UNAUTHORIZED');
633
+ }
634
+ // Load user if userLoader provided
635
+ let user;
636
+ if (config.userLoader) {
637
+ const loadedUser = await config.userLoader(userId);
638
+ if (!loadedUser) {
639
+ // User no longer exists - destroy session
640
+ await session.destroy();
641
+ throw new AuthError('User not found', 401, 'USER_NOT_FOUND');
642
+ }
643
+ user = loadedUser;
644
+ }
645
+ else {
646
+ // Create minimal user from session
647
+ user = {
648
+ id: userId,
649
+ email: session.get('userEmail') ?? '',
650
+ };
651
+ }
652
+ request.session = session;
653
+ try {
654
+ const result = await next({
655
+ ctx: {
656
+ ...ctx,
657
+ session,
658
+ user,
659
+ isAuthenticated: true,
660
+ },
661
+ });
662
+ if (session.isModified && !session.isDestroyed) {
663
+ await session.save();
664
+ }
665
+ return result;
666
+ }
667
+ catch (error) {
668
+ if (session.isModified && !session.isDestroyed) {
669
+ try {
670
+ await session.save();
671
+ }
672
+ catch {
673
+ // Ignore
674
+ }
675
+ }
676
+ throw error;
677
+ }
678
+ };
679
+ }
680
+ /**
681
+ * Middleware for optional authentication
682
+ */
683
+ function optionalAuth() {
684
+ return async ({ ctx, next }) => {
685
+ const request = ctx.request;
686
+ const reply = ctx.reply;
687
+ const session = await manager.getOrCreateSession(request, reply);
688
+ const userId = session.get('userId');
689
+ let user;
690
+ let isAuthenticated = false;
691
+ if (userId) {
692
+ if (config.userLoader) {
693
+ const loadedUser = await config.userLoader(userId);
694
+ if (loadedUser) {
695
+ user = loadedUser;
696
+ isAuthenticated = true;
697
+ }
698
+ }
699
+ else {
700
+ user = {
701
+ id: userId,
702
+ email: session.get('userEmail') ?? '',
703
+ };
704
+ isAuthenticated = true;
705
+ }
706
+ }
707
+ request.session = session;
708
+ try {
709
+ const result = await next({
710
+ ctx: {
711
+ ...ctx,
712
+ session,
713
+ user,
714
+ isAuthenticated,
715
+ },
716
+ });
717
+ if (session.isModified && !session.isDestroyed) {
718
+ await session.save();
719
+ }
720
+ return result;
721
+ }
722
+ catch (error) {
723
+ if (session.isModified && !session.isDestroyed) {
724
+ try {
725
+ await session.save();
726
+ }
727
+ catch {
728
+ // Ignore
729
+ }
730
+ }
731
+ throw error;
732
+ }
733
+ };
734
+ }
735
+ return {
736
+ /** Session manager instance */
737
+ manager,
738
+ /** Base session middleware */
739
+ middleware,
740
+ /** Authentication required middleware */
741
+ requireAuth,
742
+ /** Optional authentication middleware */
743
+ optionalAuth,
744
+ };
745
+ }
746
+ // ============================================================================
747
+ // Session Helper Functions
748
+ // ============================================================================
749
+ /**
750
+ * Login helper - sets user in session and regenerates ID
751
+ *
752
+ * @example
753
+ * ```typescript
754
+ * const login = procedure()
755
+ * .use(session.middleware())
756
+ * .input(LoginSchema)
757
+ * .mutation(async ({ input, ctx }) => {
758
+ * const user = await verifyCredentials(input.email, input.password);
759
+ * await loginSession(ctx.session, user);
760
+ * return { success: true };
761
+ * });
762
+ * ```
763
+ */
764
+ export async function loginSession(session, user) {
765
+ // Regenerate session ID to prevent session fixation
766
+ await session.regenerate();
767
+ // Store user info in session
768
+ session.set('userId', user.id);
769
+ session.set('userEmail', user.email);
770
+ // Save immediately
771
+ await session.save();
772
+ }
773
+ /**
774
+ * Logout helper - destroys session
775
+ *
776
+ * @example
777
+ * ```typescript
778
+ * const logout = procedure()
779
+ * .use(session.requireAuth())
780
+ * .mutation(async ({ ctx }) => {
781
+ * await logoutSession(ctx.session);
782
+ * return { success: true };
783
+ * });
784
+ * ```
785
+ */
786
+ export async function logoutSession(session) {
787
+ await session.destroy();
788
+ }
789
+ /**
790
+ * Check if session is authenticated
791
+ */
792
+ export function isSessionAuthenticated(session) {
793
+ return !!session.get('userId');
794
+ }
795
+ //# sourceMappingURL=session.js.map