@veloxts/auth 0.6.87 → 0.6.88

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @veloxts/auth
2
2
 
3
+ ## 0.6.88
4
+
5
+ ### Patch Changes
6
+
7
+ - add ecosystem presets for environment-aware configuration
8
+ - Updated dependencies
9
+ - @veloxts/core@0.6.88
10
+ - @veloxts/router@0.6.88
11
+
3
12
  ## 0.6.87
4
13
 
5
14
  ### Patch Changes
package/dist/guards.d.ts CHANGED
@@ -47,16 +47,30 @@ export interface GuardBuilder<TContext> {
47
47
  /**
48
48
  * Resets the guard counter to zero.
49
49
  *
50
- * This is intended for testing purposes only to ensure deterministic
51
- * guard naming across test runs. Should not be used in production code.
50
+ * **IMPORTANT:** This function MUST be called in test setup (beforeEach/beforeAll)
51
+ * when tests depend on deterministic auto-generated guard names. Without this,
52
+ * tests may pass individually but fail when run together due to counter pollution.
53
+ *
54
+ * @internal - Exported for testing purposes only. Do not use in production code.
52
55
  *
53
- * @internal
54
56
  * @example
55
57
  * ```typescript
56
- * import { _resetGuardCounter } from '@veloxts/auth';
57
- *
58
- * beforeEach(() => {
59
- * _resetGuardCounter();
58
+ * import { guard, _resetGuardCounter } from '@veloxts/auth';
59
+ * import { describe, beforeEach, it, expect } from 'vitest';
60
+ *
61
+ * describe('Guard tests', () => {
62
+ * // Reset counter before each test for deterministic naming
63
+ * beforeEach(() => {
64
+ * _resetGuardCounter();
65
+ * });
66
+ *
67
+ * it('should generate sequential names', () => {
68
+ * const guard1 = guard((ctx) => true);
69
+ * const guard2 = guard((ctx) => false);
70
+ *
71
+ * expect(guard1.name).toBe('guard_1'); // Always 1, not affected by previous tests
72
+ * expect(guard2.name).toBe('guard_2');
73
+ * });
60
74
  * });
61
75
  * ```
62
76
  */
package/dist/guards.js CHANGED
@@ -25,22 +25,43 @@ export function defineGuard(definition) {
25
25
  }
26
26
  /**
27
27
  * Counter for generating unique guard names when not provided
28
+ *
29
+ * **IMPORTANT for testing:** This counter is module-level state that persists
30
+ * across test runs. If your tests create guards without explicit names and
31
+ * assert on the generated names (e.g., 'guard_1', 'guard_2'), you MUST call
32
+ * `_resetGuardCounter()` in your test setup (beforeEach) to ensure deterministic
33
+ * results.
34
+ *
28
35
  * @internal
29
36
  */
30
37
  let guardCounter = 0;
31
38
  /**
32
39
  * Resets the guard counter to zero.
33
40
  *
34
- * This is intended for testing purposes only to ensure deterministic
35
- * guard naming across test runs. Should not be used in production code.
41
+ * **IMPORTANT:** This function MUST be called in test setup (beforeEach/beforeAll)
42
+ * when tests depend on deterministic auto-generated guard names. Without this,
43
+ * tests may pass individually but fail when run together due to counter pollution.
44
+ *
45
+ * @internal - Exported for testing purposes only. Do not use in production code.
36
46
  *
37
- * @internal
38
47
  * @example
39
48
  * ```typescript
40
- * import { _resetGuardCounter } from '@veloxts/auth';
49
+ * import { guard, _resetGuardCounter } from '@veloxts/auth';
50
+ * import { describe, beforeEach, it, expect } from 'vitest';
51
+ *
52
+ * describe('Guard tests', () => {
53
+ * // Reset counter before each test for deterministic naming
54
+ * beforeEach(() => {
55
+ * _resetGuardCounter();
56
+ * });
57
+ *
58
+ * it('should generate sequential names', () => {
59
+ * const guard1 = guard((ctx) => true);
60
+ * const guard2 = guard((ctx) => false);
41
61
  *
42
- * beforeEach(() => {
43
- * _resetGuardCounter();
62
+ * expect(guard1.name).toBe('guard_1'); // Always 1, not affected by previous tests
63
+ * expect(guard2.name).toBe('guard_2');
64
+ * });
44
65
  * });
45
66
  * ```
46
67
  */
package/dist/jwt.js CHANGED
@@ -22,34 +22,29 @@ const MIN_SECRET_ENTROPY_CHARS = 16;
22
22
  // Token Expiration Bounds (Security Phase 3.1)
23
23
  // ============================================================================
24
24
  /**
25
- * Minimum access token expiry: 1 minute
26
- * Shorter tokens increase security but may impact UX
27
- */
28
- const MIN_ACCESS_TOKEN_SECONDS = 60;
29
- /**
30
- * Maximum access token expiry: 1 hour
31
- * Longer lived tokens are a security risk if stolen
32
- */
33
- const MAX_ACCESS_TOKEN_SECONDS = 60 * 60;
34
- /**
35
- * Minimum refresh token expiry: 1 hour
36
- * Too short reduces usability
37
- */
38
- const MIN_REFRESH_TOKEN_SECONDS = 60 * 60;
39
- /**
40
- * Maximum refresh token expiry: 30 days
41
- * Longer lived refresh tokens increase risk window
42
- */
43
- const MAX_REFRESH_TOKEN_SECONDS = 30 * 24 * 60 * 60;
44
- /**
45
- * Recommended maximum access token expiry: 15 minutes
46
- * Beyond this, consider shorter lived tokens with refresh
47
- */
48
- const RECOMMENDED_MAX_ACCESS_SECONDS = 15 * 60;
49
- /**
50
- * Recommended maximum refresh token expiry: 7 days
25
+ * Token expiration bounds for security validation
26
+ *
27
+ * Groups all token lifetime constraints into a single configuration object.
28
+ * Used by validateTokenExpiry() to enforce security policies.
51
29
  */
52
- const RECOMMENDED_MAX_REFRESH_SECONDS = 7 * 24 * 60 * 60;
30
+ const TOKEN_BOUNDS = {
31
+ access: {
32
+ /** Minimum: 1 minute - shorter tokens cause excessive refreshes */
33
+ min: 60,
34
+ /** Maximum: 1 hour - longer tokens are a security risk */
35
+ max: 60 * 60,
36
+ /** Recommended: 15 minutes - balance of security and UX */
37
+ recommended: 15 * 60,
38
+ },
39
+ refresh: {
40
+ /** Minimum: 1 hour - shorter tokens impact usability */
41
+ min: 60 * 60,
42
+ /** Maximum: 30 days - longer tokens increase attack window */
43
+ max: 30 * 24 * 60 * 60,
44
+ /** Recommended: 7 days - standard refresh cycle */
45
+ recommended: 7 * 24 * 60 * 60,
46
+ },
47
+ };
53
48
  /**
54
49
  * Reserved JWT claims that cannot be overridden via additionalClaims
55
50
  */
@@ -147,29 +142,29 @@ export function validateTokenExpiration(accessExpiry, refreshExpiry) {
147
142
  const accessSeconds = parseTimeToSeconds(accessExpiry);
148
143
  const refreshSeconds = parseTimeToSeconds(refreshExpiry);
149
144
  // Validate access token bounds
150
- if (accessSeconds < MIN_ACCESS_TOKEN_SECONDS) {
145
+ if (accessSeconds < TOKEN_BOUNDS.access.min) {
151
146
  throw new AuthError(`Access token expiry (${accessExpiry} = ${accessSeconds}s) is below minimum of ` +
152
- `${MIN_ACCESS_TOKEN_SECONDS}s (1 minute). Very short tokens cause excessive refreshes.`, 400, 'INVALID_TOKEN_EXPIRY');
147
+ `${TOKEN_BOUNDS.access.min}s (1 minute). Very short tokens cause excessive refreshes.`, 400, 'INVALID_TOKEN_EXPIRY');
153
148
  }
154
- if (accessSeconds > MAX_ACCESS_TOKEN_SECONDS) {
149
+ if (accessSeconds > TOKEN_BOUNDS.access.max) {
155
150
  throw new AuthError(`Access token expiry (${accessExpiry} = ${accessSeconds}s) exceeds maximum of ` +
156
- `${MAX_ACCESS_TOKEN_SECONDS}s (1 hour). Long-lived access tokens are a security risk.`, 400, 'INVALID_TOKEN_EXPIRY');
151
+ `${TOKEN_BOUNDS.access.max}s (1 hour). Long-lived access tokens are a security risk.`, 400, 'INVALID_TOKEN_EXPIRY');
157
152
  }
158
153
  // Validate refresh token bounds
159
- if (refreshSeconds < MIN_REFRESH_TOKEN_SECONDS) {
154
+ if (refreshSeconds < TOKEN_BOUNDS.refresh.min) {
160
155
  throw new AuthError(`Refresh token expiry (${refreshExpiry} = ${refreshSeconds}s) is below minimum of ` +
161
- `${MIN_REFRESH_TOKEN_SECONDS}s (1 hour). Very short refresh tokens impact usability.`, 400, 'INVALID_TOKEN_EXPIRY');
156
+ `${TOKEN_BOUNDS.refresh.min}s (1 hour). Very short refresh tokens impact usability.`, 400, 'INVALID_TOKEN_EXPIRY');
162
157
  }
163
- if (refreshSeconds > MAX_REFRESH_TOKEN_SECONDS) {
158
+ if (refreshSeconds > TOKEN_BOUNDS.refresh.max) {
164
159
  throw new AuthError(`Refresh token expiry (${refreshExpiry} = ${refreshSeconds}s) exceeds maximum of ` +
165
- `${MAX_REFRESH_TOKEN_SECONDS}s (30 days). Long-lived refresh tokens increase attack window.`, 400, 'INVALID_TOKEN_EXPIRY');
160
+ `${TOKEN_BOUNDS.refresh.max}s (30 days). Long-lived refresh tokens increase attack window.`, 400, 'INVALID_TOKEN_EXPIRY');
166
161
  }
167
162
  // Warn about exceeding recommended limits (non-fatal)
168
- if (accessSeconds > RECOMMENDED_MAX_ACCESS_SECONDS) {
163
+ if (accessSeconds > TOKEN_BOUNDS.access.recommended) {
169
164
  console.warn(`[Security] Access token expiry (${accessExpiry}) exceeds recommended maximum of 15 minutes. ` +
170
165
  'Consider using shorter-lived access tokens with refresh.');
171
166
  }
172
- if (refreshSeconds > RECOMMENDED_MAX_REFRESH_SECONDS) {
167
+ if (refreshSeconds > TOKEN_BOUNDS.refresh.recommended) {
173
168
  console.warn(`[Security] Refresh token expiry (${refreshExpiry}) exceeds recommended maximum of 7 days. ` +
174
169
  'Long-lived refresh tokens increase the window for token theft attacks.');
175
170
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Session module barrel export
3
+ *
4
+ * Re-exports all session types, stores, and utilities.
5
+ *
6
+ * @module session
7
+ */
8
+ export type { SessionStore } from './store.js';
9
+ export { inMemorySessionStore } from './store.js';
10
+ export type { Session, SessionAuthContext, SessionContext, SessionData, SessionMiddlewareOptions, StoredSession, } from './types.js';
11
+ export { DEFAULT_COOKIE_NAME, DEFAULT_SESSION_TTL, MIN_SECRET_LENGTH, SESSION_ID_BYTES, } from './types.js';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Session module barrel export
3
+ *
4
+ * Re-exports all session types, stores, and utilities.
5
+ *
6
+ * @module session
7
+ */
8
+ export { inMemorySessionStore } from './store.js';
9
+ export { DEFAULT_COOKIE_NAME, DEFAULT_SESSION_TTL, MIN_SECRET_LENGTH, SESSION_ID_BYTES, } from './types.js';
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Session storage backends
3
+ *
4
+ * Provides pluggable session storage interfaces and implementations.
5
+ *
6
+ * @module session/store
7
+ */
8
+ import type { StoredSession } from './types.js';
9
+ /**
10
+ * Pluggable session storage backend interface
11
+ *
12
+ * Implementations:
13
+ * - InMemorySessionStore (default, for development)
14
+ * - RedisSessionStore (production, distributed)
15
+ * - DatabaseSessionStore (production, audit trail)
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // Custom Redis implementation
20
+ * class RedisSessionStore implements SessionStore {
21
+ * constructor(private redis: Redis) {}
22
+ *
23
+ * async get(sessionId: string): Promise<StoredSession | null> {
24
+ * const data = await this.redis.get(`session:${sessionId}`);
25
+ * return data ? JSON.parse(data) : null;
26
+ * }
27
+ *
28
+ * async set(sessionId: string, session: StoredSession): Promise<void> {
29
+ * const ttl = Math.ceil((session.expiresAt - Date.now()) / 1000);
30
+ * await this.redis.setex(`session:${sessionId}`, ttl, JSON.stringify(session));
31
+ * }
32
+ *
33
+ * async delete(sessionId: string): Promise<void> {
34
+ * await this.redis.del(`session:${sessionId}`);
35
+ * }
36
+ *
37
+ * async touch(sessionId: string, expiresAt: number): Promise<void> {
38
+ * const session = await this.get(sessionId);
39
+ * if (session) {
40
+ * session.expiresAt = expiresAt;
41
+ * session.data._lastAccessedAt = Date.now();
42
+ * await this.set(sessionId, session);
43
+ * }
44
+ * }
45
+ *
46
+ * async clear(): Promise<void> {
47
+ * const keys = await this.redis.keys('session:*');
48
+ * if (keys.length > 0) {
49
+ * await this.redis.del(...keys);
50
+ * }
51
+ * }
52
+ * }
53
+ * ```
54
+ */
55
+ export interface SessionStore {
56
+ /**
57
+ * Retrieve a session by ID
58
+ * @param sessionId - The session ID to look up
59
+ * @returns The stored session or null if not found/expired
60
+ */
61
+ get(sessionId: string): Promise<StoredSession | null> | StoredSession | null;
62
+ /**
63
+ * Store or update a session
64
+ * @param sessionId - The session ID
65
+ * @param session - The session data to store
66
+ */
67
+ set(sessionId: string, session: StoredSession): Promise<void> | void;
68
+ /**
69
+ * Delete a session
70
+ * @param sessionId - The session ID to delete
71
+ */
72
+ delete(sessionId: string): Promise<void> | void;
73
+ /**
74
+ * Refresh session TTL without modifying data
75
+ * Used for sliding expiration
76
+ * @param sessionId - The session ID to touch
77
+ * @param expiresAt - New expiration timestamp (Unix ms)
78
+ */
79
+ touch(sessionId: string, expiresAt: number): Promise<void> | void;
80
+ /**
81
+ * Clear all sessions (useful for testing and maintenance)
82
+ */
83
+ clear(): Promise<void> | void;
84
+ /**
85
+ * Get all active session IDs for a user (optional)
86
+ * Useful for "logout from all devices" functionality
87
+ * @param userId - The user ID to look up
88
+ * @returns Array of session IDs for the user
89
+ */
90
+ getSessionsByUser?(userId: string): Promise<string[]> | string[];
91
+ /**
92
+ * Delete all sessions for a user (optional)
93
+ * Useful for "logout from all devices" functionality
94
+ * @param userId - The user ID whose sessions to delete
95
+ */
96
+ deleteSessionsByUser?(userId: string): Promise<void> | void;
97
+ }
98
+ /**
99
+ * In-memory session store for development and testing
100
+ *
101
+ * WARNING: NOT suitable for production!
102
+ * - Sessions are lost on server restart
103
+ * - Does not work across multiple server instances
104
+ * - No persistence mechanism
105
+ *
106
+ * For production, use Redis or database-backed storage.
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * const store = inMemorySessionStore();
111
+ *
112
+ * const manager = sessionManager({
113
+ * store,
114
+ * secret: process.env.SESSION_SECRET!,
115
+ * });
116
+ * ```
117
+ */
118
+ export declare function inMemorySessionStore(): SessionStore;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Session storage backends
3
+ *
4
+ * Provides pluggable session storage interfaces and implementations.
5
+ *
6
+ * @module session/store
7
+ */
8
+ // ============================================================================
9
+ // In-Memory Session Store
10
+ // ============================================================================
11
+ /**
12
+ * In-memory session store for development and testing
13
+ *
14
+ * WARNING: NOT suitable for production!
15
+ * - Sessions are lost on server restart
16
+ * - Does not work across multiple server instances
17
+ * - No persistence mechanism
18
+ *
19
+ * For production, use Redis or database-backed storage.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const store = inMemorySessionStore();
24
+ *
25
+ * const manager = sessionManager({
26
+ * store,
27
+ * secret: process.env.SESSION_SECRET!,
28
+ * });
29
+ * ```
30
+ */
31
+ export function inMemorySessionStore() {
32
+ const sessions = new Map();
33
+ const userSessions = new Map();
34
+ /**
35
+ * Clean up expired sessions
36
+ */
37
+ function cleanup() {
38
+ const now = Date.now();
39
+ for (const [id, session] of sessions) {
40
+ if (session.expiresAt <= now) {
41
+ // Remove from user index
42
+ const userId = session.data.userId;
43
+ if (userId) {
44
+ const userSessionSet = userSessions.get(userId);
45
+ if (userSessionSet) {
46
+ userSessionSet.delete(id);
47
+ if (userSessionSet.size === 0) {
48
+ userSessions.delete(userId);
49
+ }
50
+ }
51
+ }
52
+ sessions.delete(id);
53
+ }
54
+ }
55
+ }
56
+ // Run cleanup periodically (every 5 minutes)
57
+ const cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
58
+ // Don't prevent process exit
59
+ cleanupInterval.unref();
60
+ return {
61
+ get(sessionId) {
62
+ const session = sessions.get(sessionId);
63
+ if (!session) {
64
+ return null;
65
+ }
66
+ // Check expiration
67
+ if (session.expiresAt <= Date.now()) {
68
+ sessions.delete(sessionId);
69
+ return null;
70
+ }
71
+ return session;
72
+ },
73
+ set(sessionId, session) {
74
+ // Track user sessions for getSessionsByUser
75
+ const existingSession = sessions.get(sessionId);
76
+ const oldUserId = existingSession?.data.userId;
77
+ const newUserId = session.data.userId;
78
+ // Update user index if userId changed
79
+ if (oldUserId && oldUserId !== newUserId) {
80
+ const oldSet = userSessions.get(oldUserId);
81
+ if (oldSet) {
82
+ oldSet.delete(sessionId);
83
+ if (oldSet.size === 0) {
84
+ userSessions.delete(oldUserId);
85
+ }
86
+ }
87
+ }
88
+ if (newUserId) {
89
+ let userSet = userSessions.get(newUserId);
90
+ if (!userSet) {
91
+ userSet = new Set();
92
+ userSessions.set(newUserId, userSet);
93
+ }
94
+ userSet.add(sessionId);
95
+ }
96
+ sessions.set(sessionId, session);
97
+ },
98
+ delete(sessionId) {
99
+ const session = sessions.get(sessionId);
100
+ if (session?.data.userId) {
101
+ const userSet = userSessions.get(session.data.userId);
102
+ if (userSet) {
103
+ userSet.delete(sessionId);
104
+ if (userSet.size === 0) {
105
+ userSessions.delete(session.data.userId);
106
+ }
107
+ }
108
+ }
109
+ sessions.delete(sessionId);
110
+ },
111
+ touch(sessionId, expiresAt) {
112
+ const session = sessions.get(sessionId);
113
+ if (session) {
114
+ session.expiresAt = expiresAt;
115
+ session.data._lastAccessedAt = Date.now();
116
+ }
117
+ },
118
+ clear() {
119
+ sessions.clear();
120
+ userSessions.clear();
121
+ },
122
+ getSessionsByUser(userId) {
123
+ const userSet = userSessions.get(userId);
124
+ return userSet ? [...userSet] : [];
125
+ },
126
+ deleteSessionsByUser(userId) {
127
+ const userSet = userSessions.get(userId);
128
+ if (userSet) {
129
+ for (const sessionId of userSet) {
130
+ sessions.delete(sessionId);
131
+ }
132
+ userSessions.delete(userId);
133
+ }
134
+ },
135
+ };
136
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Session type definitions
3
+ *
4
+ * Core types for cookie-based session management.
5
+ *
6
+ * @module session/types
7
+ */
8
+ import type { User } from '../types.js';
9
+ /**
10
+ * Session ID entropy in bytes (32 bytes = 256 bits)
11
+ * Provides sufficient entropy to prevent brute-force attacks
12
+ */
13
+ export declare const SESSION_ID_BYTES = 32;
14
+ /**
15
+ * Minimum secret length for session ID signing (32 characters)
16
+ */
17
+ export declare const MIN_SECRET_LENGTH = 32;
18
+ /**
19
+ * Default session TTL (24 hours in seconds)
20
+ */
21
+ export declare const DEFAULT_SESSION_TTL = 86400;
22
+ /**
23
+ * Default cookie name
24
+ */
25
+ export declare const DEFAULT_COOKIE_NAME = "velox.session";
26
+ /**
27
+ * Base session data interface
28
+ *
29
+ * Applications should extend this via declaration merging:
30
+ * @example
31
+ * ```typescript
32
+ * declare module '@veloxts/auth' {
33
+ * interface SessionData {
34
+ * cart: CartItem[];
35
+ * preferences: UserPreferences;
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ export interface SessionData {
41
+ /** User ID if authenticated */
42
+ userId?: string;
43
+ /** User email if authenticated */
44
+ userEmail?: string;
45
+ /** Flash data - persists for one request only */
46
+ _flash?: Record<string, unknown>;
47
+ /** Previous flash data being read */
48
+ _flashOld?: Record<string, unknown>;
49
+ /** Session creation timestamp (Unix ms) */
50
+ _createdAt: number;
51
+ /** Last access timestamp (Unix ms) */
52
+ _lastAccessedAt: number;
53
+ /** Allow extension via declaration merging */
54
+ [key: string]: unknown;
55
+ }
56
+ /**
57
+ * Stored session entry in the session store
58
+ */
59
+ export interface StoredSession {
60
+ /** Session ID (signed) */
61
+ id: string;
62
+ /** Session data */
63
+ data: SessionData;
64
+ /** Expiration timestamp (Unix ms) */
65
+ expiresAt: number;
66
+ }
67
+ /**
68
+ * Session handle for accessing and modifying session data
69
+ */
70
+ export interface Session {
71
+ /** Session ID */
72
+ readonly id: string;
73
+ /** Whether this is a new session */
74
+ readonly isNew: boolean;
75
+ /** Whether the session has been modified */
76
+ readonly isModified: boolean;
77
+ /** Whether the session has been destroyed */
78
+ readonly isDestroyed: boolean;
79
+ /** Session data */
80
+ readonly data: SessionData;
81
+ /**
82
+ * Get a session value
83
+ */
84
+ get<K extends keyof SessionData>(key: K): SessionData[K];
85
+ /**
86
+ * Set a session value
87
+ */
88
+ set<K extends keyof SessionData>(key: K, value: SessionData[K]): void;
89
+ /**
90
+ * Delete a session value
91
+ */
92
+ delete<K extends keyof SessionData>(key: K): void;
93
+ /**
94
+ * Check if a key exists
95
+ */
96
+ has<K extends keyof SessionData>(key: K): boolean;
97
+ /**
98
+ * Set flash data (persists for one request only)
99
+ */
100
+ flash(key: string, value: unknown): void;
101
+ /**
102
+ * Get flash data (clears after read)
103
+ */
104
+ getFlash<T = unknown>(key: string): T | undefined;
105
+ /**
106
+ * Get all flash data
107
+ */
108
+ getAllFlash(): Record<string, unknown>;
109
+ /**
110
+ * Regenerate session ID (for security after privilege change)
111
+ * Preserves session data with new ID
112
+ */
113
+ regenerate(): Promise<void>;
114
+ /**
115
+ * Destroy the session completely
116
+ */
117
+ destroy(): Promise<void>;
118
+ /**
119
+ * Save session changes
120
+ * Called automatically by middleware, but can be called manually
121
+ */
122
+ save(): Promise<void>;
123
+ /**
124
+ * Reload session data from store
125
+ */
126
+ reload(): Promise<void>;
127
+ /**
128
+ * Log in a user to the session
129
+ *
130
+ * Regenerates the session ID to prevent session fixation attacks,
131
+ * then stores the user's ID and email in the session.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const login = procedure()
136
+ * .input(LoginSchema)
137
+ * .mutation(async ({ input, ctx }) => {
138
+ * const user = await verifyCredentials(input.email, input.password);
139
+ * await ctx.session.login(user);
140
+ * return { success: true };
141
+ * });
142
+ * ```
143
+ */
144
+ login(user: User): Promise<void>;
145
+ /**
146
+ * Log out the current user by destroying the session
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * const logout = procedure()
151
+ * .use(session.requireAuth())
152
+ * .mutation(async ({ ctx }) => {
153
+ * await ctx.session.logout();
154
+ * return { success: true };
155
+ * });
156
+ * ```
157
+ */
158
+ logout(): Promise<void>;
159
+ /**
160
+ * Check if the session is authenticated (has a logged-in user)
161
+ *
162
+ * @returns true if a user is logged in, false otherwise
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * if (ctx.session.check()) {
167
+ * // User is authenticated
168
+ * console.log('User ID:', ctx.session.get('userId'));
169
+ * }
170
+ * ```
171
+ */
172
+ check(): boolean;
173
+ }
174
+ /**
175
+ * Session context added to request context
176
+ */
177
+ export interface SessionContext {
178
+ /** Current session */
179
+ session: Session;
180
+ }
181
+ /**
182
+ * Extended context with session and optional user
183
+ */
184
+ export interface SessionAuthContext extends SessionContext {
185
+ /** Authenticated user (if logged in) */
186
+ user?: User;
187
+ /** Whether user is authenticated via session */
188
+ isAuthenticated: boolean;
189
+ }
190
+ /**
191
+ * Options for session middleware
192
+ */
193
+ export interface SessionMiddlewareOptions {
194
+ /**
195
+ * Create session lazily (only when data is set)
196
+ * @default false
197
+ */
198
+ lazy?: boolean;
199
+ /**
200
+ * Require authentication (session with userId)
201
+ * @default false
202
+ */
203
+ requireAuth?: boolean;
204
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Session type definitions
3
+ *
4
+ * Core types for cookie-based session management.
5
+ *
6
+ * @module session/types
7
+ */
8
+ // ============================================================================
9
+ // Constants
10
+ // ============================================================================
11
+ /**
12
+ * Session ID entropy in bytes (32 bytes = 256 bits)
13
+ * Provides sufficient entropy to prevent brute-force attacks
14
+ */
15
+ export const SESSION_ID_BYTES = 32;
16
+ /**
17
+ * Minimum secret length for session ID signing (32 characters)
18
+ */
19
+ export const MIN_SECRET_LENGTH = 32;
20
+ /**
21
+ * Default session TTL (24 hours in seconds)
22
+ */
23
+ export const DEFAULT_SESSION_TTL = 86400;
24
+ /**
25
+ * Default cookie name
26
+ */
27
+ export const DEFAULT_COOKIE_NAME = 'velox.session';
package/dist/session.d.ts CHANGED
@@ -12,157 +12,12 @@ import type { BaseContext } from '@veloxts/core';
12
12
  import type { MiddlewareFunction } from '@veloxts/router';
13
13
  import type { User } from './types.js';
14
14
  import { type FastifyReplyWithCookies, type FastifyRequestWithCookies } from './utils/cookie-support.js';
15
- /**
16
- * Base session data interface
17
- *
18
- * Applications should extend this via declaration merging:
19
- * @example
20
- * ```typescript
21
- * declare module '@veloxts/auth' {
22
- * interface SessionData {
23
- * cart: CartItem[];
24
- * preferences: UserPreferences;
25
- * }
26
- * }
27
- * ```
28
- */
29
- export interface SessionData {
30
- /** User ID if authenticated */
31
- userId?: string;
32
- /** User email if authenticated */
33
- userEmail?: string;
34
- /** Flash data - persists for one request only */
35
- _flash?: Record<string, unknown>;
36
- /** Previous flash data being read */
37
- _flashOld?: Record<string, unknown>;
38
- /** Session creation timestamp (Unix ms) */
39
- _createdAt: number;
40
- /** Last access timestamp (Unix ms) */
41
- _lastAccessedAt: number;
42
- /** Allow extension via declaration merging */
43
- [key: string]: unknown;
44
- }
45
- /**
46
- * Stored session entry in the session store
47
- */
48
- export interface StoredSession {
49
- /** Session ID (signed) */
50
- id: string;
51
- /** Session data */
52
- data: SessionData;
53
- /** Expiration timestamp (Unix ms) */
54
- expiresAt: number;
55
- }
56
- /**
57
- * Pluggable session storage backend interface
58
- *
59
- * Implementations:
60
- * - InMemorySessionStore (default, for development)
61
- * - RedisSessionStore (production, distributed)
62
- * - DatabaseSessionStore (production, audit trail)
63
- *
64
- * @example
65
- * ```typescript
66
- * // Custom Redis implementation
67
- * class RedisSessionStore implements SessionStore {
68
- * constructor(private redis: Redis) {}
69
- *
70
- * async get(sessionId: string): Promise<StoredSession | null> {
71
- * const data = await this.redis.get(`session:${sessionId}`);
72
- * return data ? JSON.parse(data) : null;
73
- * }
74
- *
75
- * async set(sessionId: string, session: StoredSession): Promise<void> {
76
- * const ttl = Math.ceil((session.expiresAt - Date.now()) / 1000);
77
- * await this.redis.setex(`session:${sessionId}`, ttl, JSON.stringify(session));
78
- * }
79
- *
80
- * async delete(sessionId: string): Promise<void> {
81
- * await this.redis.del(`session:${sessionId}`);
82
- * }
83
- *
84
- * async touch(sessionId: string, expiresAt: number): Promise<void> {
85
- * const session = await this.get(sessionId);
86
- * if (session) {
87
- * session.expiresAt = expiresAt;
88
- * session.data._lastAccessedAt = Date.now();
89
- * await this.set(sessionId, session);
90
- * }
91
- * }
92
- *
93
- * async clear(): Promise<void> {
94
- * const keys = await this.redis.keys('session:*');
95
- * if (keys.length > 0) {
96
- * await this.redis.del(...keys);
97
- * }
98
- * }
99
- * }
100
- * ```
101
- */
102
- export interface SessionStore {
103
- /**
104
- * Retrieve a session by ID
105
- * @param sessionId - The session ID to look up
106
- * @returns The stored session or null if not found/expired
107
- */
108
- get(sessionId: string): Promise<StoredSession | null> | StoredSession | null;
109
- /**
110
- * Store or update a session
111
- * @param sessionId - The session ID
112
- * @param session - The session data to store
113
- */
114
- set(sessionId: string, session: StoredSession): Promise<void> | void;
115
- /**
116
- * Delete a session
117
- * @param sessionId - The session ID to delete
118
- */
119
- delete(sessionId: string): Promise<void> | void;
120
- /**
121
- * Refresh session TTL without modifying data
122
- * Used for sliding expiration
123
- * @param sessionId - The session ID to touch
124
- * @param expiresAt - New expiration timestamp (Unix ms)
125
- */
126
- touch(sessionId: string, expiresAt: number): Promise<void> | void;
127
- /**
128
- * Clear all sessions (useful for testing and maintenance)
129
- */
130
- clear(): Promise<void> | void;
131
- /**
132
- * Get all active session IDs for a user (optional)
133
- * Useful for "logout from all devices" functionality
134
- * @param userId - The user ID to look up
135
- * @returns Array of session IDs for the user
136
- */
137
- getSessionsByUser?(userId: string): Promise<string[]> | string[];
138
- /**
139
- * Delete all sessions for a user (optional)
140
- * Useful for "logout from all devices" functionality
141
- * @param userId - The user ID whose sessions to delete
142
- */
143
- deleteSessionsByUser?(userId: string): Promise<void> | void;
144
- }
145
- /**
146
- * In-memory session store for development and testing
147
- *
148
- * WARNING: NOT suitable for production!
149
- * - Sessions are lost on server restart
150
- * - Does not work across multiple server instances
151
- * - No persistence mechanism
152
- *
153
- * For production, use Redis or database-backed storage.
154
- *
155
- * @example
156
- * ```typescript
157
- * const store = inMemorySessionStore();
158
- *
159
- * const manager = sessionManager({
160
- * store,
161
- * secret: process.env.SESSION_SECRET!,
162
- * });
163
- * ```
164
- */
165
- export declare function inMemorySessionStore(): SessionStore;
15
+ export type { SessionStore } from './session/store.js';
16
+ export { inMemorySessionStore } from './session/store.js';
17
+ export type { Session, SessionAuthContext, SessionContext, SessionData, SessionMiddlewareOptions, StoredSession, } from './session/types.js';
18
+ export { DEFAULT_COOKIE_NAME, DEFAULT_SESSION_TTL, MIN_SECRET_LENGTH, SESSION_ID_BYTES, } from './session/types.js';
19
+ import type { SessionStore } from './session/store.js';
20
+ import type { Session, SessionAuthContext, SessionContext, SessionMiddlewareOptions } from './session/types.js';
166
21
  /**
167
22
  * Cookie configuration for sessions
168
23
  */
@@ -247,113 +102,6 @@ export interface SessionConfig {
247
102
  */
248
103
  userLoader?: (userId: string) => Promise<User | null>;
249
104
  }
250
- /**
251
- * Session handle for accessing and modifying session data
252
- */
253
- export interface Session {
254
- /** Session ID */
255
- readonly id: string;
256
- /** Whether this is a new session */
257
- readonly isNew: boolean;
258
- /** Whether the session has been modified */
259
- readonly isModified: boolean;
260
- /** Whether the session has been destroyed */
261
- readonly isDestroyed: boolean;
262
- /** Session data */
263
- readonly data: SessionData;
264
- /**
265
- * Get a session value
266
- */
267
- get<K extends keyof SessionData>(key: K): SessionData[K];
268
- /**
269
- * Set a session value
270
- */
271
- set<K extends keyof SessionData>(key: K, value: SessionData[K]): void;
272
- /**
273
- * Delete a session value
274
- */
275
- delete<K extends keyof SessionData>(key: K): void;
276
- /**
277
- * Check if a key exists
278
- */
279
- has<K extends keyof SessionData>(key: K): boolean;
280
- /**
281
- * Set flash data (persists for one request only)
282
- */
283
- flash(key: string, value: unknown): void;
284
- /**
285
- * Get flash data (clears after read)
286
- */
287
- getFlash<T = unknown>(key: string): T | undefined;
288
- /**
289
- * Get all flash data
290
- */
291
- getAllFlash(): Record<string, unknown>;
292
- /**
293
- * Regenerate session ID (for security after privilege change)
294
- * Preserves session data with new ID
295
- */
296
- regenerate(): Promise<void>;
297
- /**
298
- * Destroy the session completely
299
- */
300
- destroy(): Promise<void>;
301
- /**
302
- * Save session changes
303
- * Called automatically by middleware, but can be called manually
304
- */
305
- save(): Promise<void>;
306
- /**
307
- * Reload session data from store
308
- */
309
- reload(): Promise<void>;
310
- /**
311
- * Log in a user to the session
312
- *
313
- * Regenerates the session ID to prevent session fixation attacks,
314
- * then stores the user's ID and email in the session.
315
- *
316
- * @example
317
- * ```typescript
318
- * const login = procedure()
319
- * .input(LoginSchema)
320
- * .mutation(async ({ input, ctx }) => {
321
- * const user = await verifyCredentials(input.email, input.password);
322
- * await ctx.session.login(user);
323
- * return { success: true };
324
- * });
325
- * ```
326
- */
327
- login(user: User): Promise<void>;
328
- /**
329
- * Log out the current user by destroying the session
330
- *
331
- * @example
332
- * ```typescript
333
- * const logout = procedure()
334
- * .use(session.requireAuth())
335
- * .mutation(async ({ ctx }) => {
336
- * await ctx.session.logout();
337
- * return { success: true };
338
- * });
339
- * ```
340
- */
341
- logout(): Promise<void>;
342
- /**
343
- * Check if the session is authenticated (has a logged-in user)
344
- *
345
- * @returns true if a user is logged in, false otherwise
346
- *
347
- * @example
348
- * ```typescript
349
- * if (ctx.session.check()) {
350
- * // User is authenticated
351
- * console.log('User ID:', ctx.session.get('userId'));
352
- * }
353
- * ```
354
- */
355
- check(): boolean;
356
- }
357
105
  /**
358
106
  * Session manager for creating and managing sessions
359
107
  */
@@ -408,22 +156,6 @@ export interface SessionManager {
408
156
  * ```
409
157
  */
410
158
  export declare function sessionManager(config: SessionConfig): SessionManager;
411
- /**
412
- * Session context added to request context
413
- */
414
- export interface SessionContext {
415
- /** Current session */
416
- session: Session;
417
- }
418
- /**
419
- * Extended context with session and optional user
420
- */
421
- export interface SessionAuthContext extends SessionContext {
422
- /** Authenticated user (if logged in) */
423
- user?: User;
424
- /** Whether user is authenticated via session */
425
- isAuthenticated: boolean;
426
- }
427
159
  declare module '@veloxts/core' {
428
160
  interface BaseContext {
429
161
  /** Session context - available when session middleware is used */
@@ -436,21 +168,6 @@ declare module 'fastify' {
436
168
  session?: Session;
437
169
  }
438
170
  }
439
- /**
440
- * Options for session middleware
441
- */
442
- export interface SessionMiddlewareOptions {
443
- /**
444
- * Create session lazily (only when data is set)
445
- * @default false
446
- */
447
- lazy?: boolean;
448
- /**
449
- * Require authentication (session with userId)
450
- * @default false
451
- */
452
- requireAuth?: boolean;
453
- }
454
171
  /**
455
172
  * Creates session middleware for procedures (succinct API)
456
173
  *
package/dist/session.js CHANGED
@@ -11,155 +11,10 @@
11
11
  import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
12
12
  import { AuthError } from './types.js';
13
13
  import { getValidatedCookieContext, } from './utils/cookie-support.js';
14
- // ============================================================================
15
- // Constants
16
- // ============================================================================
17
- /**
18
- * Session ID entropy in bytes (32 bytes = 256 bits)
19
- * Provides sufficient entropy to prevent brute-force attacks
20
- */
21
- const SESSION_ID_BYTES = 32;
22
- /**
23
- * Minimum secret length for session ID signing (32 characters)
24
- */
25
- const MIN_SECRET_LENGTH = 32;
26
- /**
27
- * Default session TTL (24 hours in seconds)
28
- */
29
- const DEFAULT_SESSION_TTL = 86400;
30
- /**
31
- * Default cookie name
32
- */
33
- const DEFAULT_COOKIE_NAME = 'velox.session';
34
- // ============================================================================
35
- // In-Memory Session Store
36
- // ============================================================================
37
- /**
38
- * In-memory session store for development and testing
39
- *
40
- * WARNING: NOT suitable for production!
41
- * - Sessions are lost on server restart
42
- * - Does not work across multiple server instances
43
- * - No persistence mechanism
44
- *
45
- * For production, use Redis or database-backed storage.
46
- *
47
- * @example
48
- * ```typescript
49
- * const store = inMemorySessionStore();
50
- *
51
- * const manager = sessionManager({
52
- * store,
53
- * secret: process.env.SESSION_SECRET!,
54
- * });
55
- * ```
56
- */
57
- export function inMemorySessionStore() {
58
- const sessions = new Map();
59
- const userSessions = new Map();
60
- /**
61
- * Clean up expired sessions
62
- */
63
- function cleanup() {
64
- const now = Date.now();
65
- for (const [id, session] of sessions) {
66
- if (session.expiresAt <= now) {
67
- // Remove from user index
68
- const userId = session.data.userId;
69
- if (userId) {
70
- const userSessionSet = userSessions.get(userId);
71
- if (userSessionSet) {
72
- userSessionSet.delete(id);
73
- if (userSessionSet.size === 0) {
74
- userSessions.delete(userId);
75
- }
76
- }
77
- }
78
- sessions.delete(id);
79
- }
80
- }
81
- }
82
- // Run cleanup periodically (every 5 minutes)
83
- const cleanupInterval = setInterval(cleanup, 5 * 60 * 1000);
84
- // Don't prevent process exit
85
- cleanupInterval.unref();
86
- return {
87
- get(sessionId) {
88
- const session = sessions.get(sessionId);
89
- if (!session) {
90
- return null;
91
- }
92
- // Check expiration
93
- if (session.expiresAt <= Date.now()) {
94
- sessions.delete(sessionId);
95
- return null;
96
- }
97
- return session;
98
- },
99
- set(sessionId, session) {
100
- // Track user sessions for getSessionsByUser
101
- const existingSession = sessions.get(sessionId);
102
- const oldUserId = existingSession?.data.userId;
103
- const newUserId = session.data.userId;
104
- // Update user index if userId changed
105
- if (oldUserId && oldUserId !== newUserId) {
106
- const oldSet = userSessions.get(oldUserId);
107
- if (oldSet) {
108
- oldSet.delete(sessionId);
109
- if (oldSet.size === 0) {
110
- userSessions.delete(oldUserId);
111
- }
112
- }
113
- }
114
- if (newUserId) {
115
- let userSet = userSessions.get(newUserId);
116
- if (!userSet) {
117
- userSet = new Set();
118
- userSessions.set(newUserId, userSet);
119
- }
120
- userSet.add(sessionId);
121
- }
122
- sessions.set(sessionId, session);
123
- },
124
- delete(sessionId) {
125
- const session = sessions.get(sessionId);
126
- if (session?.data.userId) {
127
- const userSet = userSessions.get(session.data.userId);
128
- if (userSet) {
129
- userSet.delete(sessionId);
130
- if (userSet.size === 0) {
131
- userSessions.delete(session.data.userId);
132
- }
133
- }
134
- }
135
- sessions.delete(sessionId);
136
- },
137
- touch(sessionId, expiresAt) {
138
- const session = sessions.get(sessionId);
139
- if (session) {
140
- session.expiresAt = expiresAt;
141
- session.data._lastAccessedAt = Date.now();
142
- }
143
- },
144
- clear() {
145
- sessions.clear();
146
- userSessions.clear();
147
- },
148
- getSessionsByUser(userId) {
149
- const userSet = userSessions.get(userId);
150
- return userSet ? [...userSet] : [];
151
- },
152
- deleteSessionsByUser(userId) {
153
- const userSet = userSessions.get(userId);
154
- if (userSet) {
155
- for (const sessionId of userSet) {
156
- sessions.delete(sessionId);
157
- }
158
- userSessions.delete(userId);
159
- }
160
- },
161
- };
162
- }
14
+ export { inMemorySessionStore } from './session/store.js';
15
+ export { DEFAULT_COOKIE_NAME, DEFAULT_SESSION_TTL, MIN_SECRET_LENGTH, SESSION_ID_BYTES, } from './session/types.js';
16
+ import { inMemorySessionStore } from './session/store.js';
17
+ import { DEFAULT_COOKIE_NAME, DEFAULT_SESSION_TTL, MIN_SECRET_LENGTH, SESSION_ID_BYTES, } from './session/types.js';
163
18
  // ============================================================================
164
19
  // Session ID Generation and Signing
165
20
  // ============================================================================
package/dist/types.d.ts CHANGED
@@ -232,7 +232,33 @@ export interface AuthConfig {
232
232
  /**
233
233
  * Token blacklist checker - check if token is revoked
234
234
  * Called on every authenticated request
235
- * @deprecated Use `tokenStore` with TokenStore interface instead
235
+ *
236
+ * @deprecated Since v0.5.0. Use `tokenStore` with TokenStore interface instead.
237
+ * This option will be removed in v1.0.0.
238
+ *
239
+ * **Migration guide:**
240
+ * ```typescript
241
+ * // Before (deprecated)
242
+ * authPlugin({
243
+ * jwt: { secret: '...' },
244
+ * isTokenRevoked: async (tokenId) => {
245
+ * return await redis.sismember('revoked_tokens', tokenId);
246
+ * },
247
+ * });
248
+ *
249
+ * // After (recommended)
250
+ * authPlugin({
251
+ * jwt: { secret: '...' },
252
+ * tokenStore: {
253
+ * isRevoked: async (tokenId) => {
254
+ * return await redis.sismember('revoked_tokens', tokenId);
255
+ * },
256
+ * revoke: async (tokenId) => {
257
+ * await redis.sadd('revoked_tokens', tokenId);
258
+ * },
259
+ * },
260
+ * });
261
+ * ```
236
262
  */
237
263
  isTokenRevoked?: (tokenId: string) => Promise<boolean>;
238
264
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veloxts/auth",
3
- "version": "0.6.87",
3
+ "version": "0.6.88",
4
4
  "description": "Authentication and authorization system for VeloxTS framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -61,8 +61,8 @@
61
61
  "dependencies": {
62
62
  "@fastify/cookie": "11.0.2",
63
63
  "fastify": "5.6.2",
64
- "@veloxts/core": "0.6.87",
65
- "@veloxts/router": "0.6.87"
64
+ "@veloxts/core": "0.6.88",
65
+ "@veloxts/router": "0.6.88"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "argon2": ">=0.30.0",
@@ -86,8 +86,8 @@
86
86
  "fastify-plugin": "5.1.0",
87
87
  "typescript": "5.9.3",
88
88
  "vitest": "4.0.16",
89
- "@veloxts/validation": "0.6.87",
90
- "@veloxts/testing": "0.6.87"
89
+ "@veloxts/testing": "0.6.88",
90
+ "@veloxts/validation": "0.6.88"
91
91
  },
92
92
  "keywords": [
93
93
  "velox",