@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 +9 -0
- package/dist/guards.d.ts +21 -7
- package/dist/guards.js +27 -6
- package/dist/jwt.js +32 -37
- package/dist/session/index.d.ts +11 -0
- package/dist/session/index.js +9 -0
- package/dist/session/store.d.ts +118 -0
- package/dist/session/store.js +136 -0
- package/dist/session/types.d.ts +204 -0
- package/dist/session/types.js +27 -0
- package/dist/session.d.ts +6 -289
- package/dist/session.js +4 -149
- package/dist/types.d.ts +27 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
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
|
|
51
|
-
*
|
|
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
|
-
*
|
|
59
|
-
*
|
|
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
|
|
35
|
-
*
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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 <
|
|
145
|
+
if (accessSeconds < TOKEN_BOUNDS.access.min) {
|
|
151
146
|
throw new AuthError(`Access token expiry (${accessExpiry} = ${accessSeconds}s) is below minimum of ` +
|
|
152
|
-
`${
|
|
147
|
+
`${TOKEN_BOUNDS.access.min}s (1 minute). Very short tokens cause excessive refreshes.`, 400, 'INVALID_TOKEN_EXPIRY');
|
|
153
148
|
}
|
|
154
|
-
if (accessSeconds >
|
|
149
|
+
if (accessSeconds > TOKEN_BOUNDS.access.max) {
|
|
155
150
|
throw new AuthError(`Access token expiry (${accessExpiry} = ${accessSeconds}s) exceeds maximum of ` +
|
|
156
|
-
`${
|
|
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 <
|
|
154
|
+
if (refreshSeconds < TOKEN_BOUNDS.refresh.min) {
|
|
160
155
|
throw new AuthError(`Refresh token expiry (${refreshExpiry} = ${refreshSeconds}s) is below minimum of ` +
|
|
161
|
-
`${
|
|
156
|
+
`${TOKEN_BOUNDS.refresh.min}s (1 hour). Very short refresh tokens impact usability.`, 400, 'INVALID_TOKEN_EXPIRY');
|
|
162
157
|
}
|
|
163
|
-
if (refreshSeconds >
|
|
158
|
+
if (refreshSeconds > TOKEN_BOUNDS.refresh.max) {
|
|
164
159
|
throw new AuthError(`Refresh token expiry (${refreshExpiry} = ${refreshSeconds}s) exceeds maximum of ` +
|
|
165
|
-
`${
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
65
|
-
"@veloxts/router": "0.6.
|
|
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/
|
|
90
|
-
"@veloxts/
|
|
89
|
+
"@veloxts/testing": "0.6.88",
|
|
90
|
+
"@veloxts/validation": "0.6.88"
|
|
91
91
|
},
|
|
92
92
|
"keywords": [
|
|
93
93
|
"velox",
|