@veloxts/auth 0.6.84 → 0.6.85
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/adapter.d.ts +35 -17
- package/dist/adapter.js +33 -17
- package/dist/adapters/auth0.d.ts +316 -0
- package/dist/adapters/auth0.js +539 -0
- package/dist/adapters/clerk.d.ts +281 -0
- package/dist/adapters/clerk.js +314 -0
- package/dist/adapters/index.d.ts +46 -0
- package/dist/adapters/index.js +44 -0
- package/dist/adapters/utils.d.ts +31 -0
- package/dist/adapters/utils.js +49 -0
- package/dist/rate-limit.js +85 -57
- package/package.json +5 -5
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for auth adapters
|
|
3
|
+
*
|
|
4
|
+
* @module auth/adapters/utils
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract Bearer token from Authorization header
|
|
9
|
+
*
|
|
10
|
+
* @param headerValue - Authorization header value
|
|
11
|
+
* @returns Token string or null if not a Bearer token
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const token = extractBearerToken('Bearer eyJhbGci...');
|
|
16
|
+
* // Returns: 'eyJhbGci...'
|
|
17
|
+
*
|
|
18
|
+
* const invalid = extractBearerToken('Basic abc123');
|
|
19
|
+
* // Returns: null
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractBearerToken(headerValue: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Validates that a string value is non-empty
|
|
25
|
+
*
|
|
26
|
+
* @param value - Value to check
|
|
27
|
+
* @param fieldName - Field name for error message
|
|
28
|
+
* @returns The trimmed value if valid
|
|
29
|
+
* @throws Error if value is empty or whitespace-only
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateNonEmptyString(value: string | undefined, fieldName: string): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for auth adapters
|
|
3
|
+
*
|
|
4
|
+
* @module auth/adapters/utils
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Token Extraction
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Extract Bearer token from Authorization header
|
|
12
|
+
*
|
|
13
|
+
* @param headerValue - Authorization header value
|
|
14
|
+
* @returns Token string or null if not a Bearer token
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const token = extractBearerToken('Bearer eyJhbGci...');
|
|
19
|
+
* // Returns: 'eyJhbGci...'
|
|
20
|
+
*
|
|
21
|
+
* const invalid = extractBearerToken('Basic abc123');
|
|
22
|
+
* // Returns: null
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function extractBearerToken(headerValue) {
|
|
26
|
+
const parts = headerValue.split(' ');
|
|
27
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
// Trim whitespace from token to handle malformed headers
|
|
31
|
+
return parts[1].trim();
|
|
32
|
+
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Validation Helpers
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Validates that a string value is non-empty
|
|
38
|
+
*
|
|
39
|
+
* @param value - Value to check
|
|
40
|
+
* @param fieldName - Field name for error message
|
|
41
|
+
* @returns The trimmed value if valid
|
|
42
|
+
* @throws Error if value is empty or whitespace-only
|
|
43
|
+
*/
|
|
44
|
+
export function validateNonEmptyString(value, fieldName) {
|
|
45
|
+
if (!value || value.trim() === '') {
|
|
46
|
+
throw new Error(`${fieldName} is required and cannot be empty`);
|
|
47
|
+
}
|
|
48
|
+
return value.trim();
|
|
49
|
+
}
|
package/dist/rate-limit.js
CHANGED
|
@@ -70,6 +70,70 @@ export function clearAuthRateLimitStore() {
|
|
|
70
70
|
// Start cleanup on module load
|
|
71
71
|
startCleanup();
|
|
72
72
|
// ============================================================================
|
|
73
|
+
// Configuration Helpers
|
|
74
|
+
// ============================================================================
|
|
75
|
+
/** Time constants for readability */
|
|
76
|
+
const FIFTEEN_MINUTES_MS = 15 * 60 * 1000;
|
|
77
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
78
|
+
const ONE_MINUTE_MS = 60 * 1000;
|
|
79
|
+
/** IP-only key generator */
|
|
80
|
+
const ipOnlyKeyGenerator = (ctx) => ctx.request.ip ?? 'unknown';
|
|
81
|
+
/**
|
|
82
|
+
* Default key generator combining IP and identifier
|
|
83
|
+
*/
|
|
84
|
+
function defaultKeyGenerator(ctx, identifier) {
|
|
85
|
+
const ip = ctx.request.ip ?? 'unknown';
|
|
86
|
+
return identifier ? `${ip}:${identifier.toLowerCase()}` : ip;
|
|
87
|
+
}
|
|
88
|
+
const OPERATION_DEFAULTS = {
|
|
89
|
+
login: {
|
|
90
|
+
maxAttempts: 5,
|
|
91
|
+
windowMs: FIFTEEN_MINUTES_MS,
|
|
92
|
+
lockoutDurationMs: FIFTEEN_MINUTES_MS,
|
|
93
|
+
keyGenerator: defaultKeyGenerator,
|
|
94
|
+
message: 'Too many login attempts. Please try again later.',
|
|
95
|
+
progressiveBackoff: true,
|
|
96
|
+
},
|
|
97
|
+
register: {
|
|
98
|
+
maxAttempts: 3,
|
|
99
|
+
windowMs: ONE_HOUR_MS,
|
|
100
|
+
lockoutDurationMs: ONE_HOUR_MS,
|
|
101
|
+
keyGenerator: ipOnlyKeyGenerator,
|
|
102
|
+
message: 'Too many registration attempts. Please try again later.',
|
|
103
|
+
progressiveBackoff: false,
|
|
104
|
+
},
|
|
105
|
+
passwordReset: {
|
|
106
|
+
maxAttempts: 3,
|
|
107
|
+
windowMs: ONE_HOUR_MS,
|
|
108
|
+
lockoutDurationMs: ONE_HOUR_MS,
|
|
109
|
+
keyGenerator: ipOnlyKeyGenerator,
|
|
110
|
+
message: 'Too many password reset attempts. Please try again later.',
|
|
111
|
+
progressiveBackoff: false,
|
|
112
|
+
},
|
|
113
|
+
refresh: {
|
|
114
|
+
maxAttempts: 10,
|
|
115
|
+
windowMs: ONE_MINUTE_MS,
|
|
116
|
+
lockoutDurationMs: ONE_MINUTE_MS,
|
|
117
|
+
keyGenerator: ipOnlyKeyGenerator,
|
|
118
|
+
message: 'Too many token refresh attempts. Please try again later.',
|
|
119
|
+
progressiveBackoff: false,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Build a complete rate limit config by merging user config with defaults
|
|
124
|
+
*/
|
|
125
|
+
function buildRateLimitConfig(userConfig, operation) {
|
|
126
|
+
const defaults = OPERATION_DEFAULTS[operation];
|
|
127
|
+
return {
|
|
128
|
+
maxAttempts: userConfig?.maxAttempts ?? defaults.maxAttempts,
|
|
129
|
+
windowMs: userConfig?.windowMs ?? defaults.windowMs,
|
|
130
|
+
lockoutDurationMs: userConfig?.lockoutDurationMs ?? defaults.lockoutDurationMs,
|
|
131
|
+
keyGenerator: userConfig?.keyGenerator ?? defaults.keyGenerator,
|
|
132
|
+
message: userConfig?.message ?? defaults.message,
|
|
133
|
+
progressiveBackoff: userConfig?.progressiveBackoff ?? defaults.progressiveBackoff,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ============================================================================
|
|
73
137
|
// Auth Rate Limiter
|
|
74
138
|
// ============================================================================
|
|
75
139
|
/**
|
|
@@ -96,38 +160,17 @@ startCleanup();
|
|
|
96
160
|
* ```
|
|
97
161
|
*/
|
|
98
162
|
export function createAuthRateLimiter(config = {}) {
|
|
99
|
-
//
|
|
100
|
-
const loginConfig =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
windowMs: config.register?.windowMs ?? 60 * 60 * 1000, // 1 hour
|
|
111
|
-
lockoutDurationMs: config.register?.lockoutDurationMs ?? 60 * 60 * 1000,
|
|
112
|
-
keyGenerator: config.register?.keyGenerator ?? ((ctx) => ctx.request.ip ?? 'unknown'),
|
|
113
|
-
message: config.register?.message ?? 'Too many registration attempts. Please try again later.',
|
|
114
|
-
progressiveBackoff: config.register?.progressiveBackoff ?? false,
|
|
115
|
-
};
|
|
116
|
-
const passwordResetConfig = {
|
|
117
|
-
maxAttempts: config.passwordReset?.maxAttempts ?? 3,
|
|
118
|
-
windowMs: config.passwordReset?.windowMs ?? 60 * 60 * 1000, // 1 hour
|
|
119
|
-
lockoutDurationMs: config.passwordReset?.lockoutDurationMs ?? 60 * 60 * 1000,
|
|
120
|
-
keyGenerator: config.passwordReset?.keyGenerator ?? ((ctx) => ctx.request.ip ?? 'unknown'),
|
|
121
|
-
message: config.passwordReset?.message ?? 'Too many password reset attempts. Please try again later.',
|
|
122
|
-
progressiveBackoff: config.passwordReset?.progressiveBackoff ?? false,
|
|
123
|
-
};
|
|
124
|
-
const refreshConfig = {
|
|
125
|
-
maxAttempts: config.refresh?.maxAttempts ?? 10,
|
|
126
|
-
windowMs: config.refresh?.windowMs ?? 60 * 1000, // 1 minute
|
|
127
|
-
lockoutDurationMs: config.refresh?.lockoutDurationMs ?? 60 * 1000,
|
|
128
|
-
keyGenerator: config.refresh?.keyGenerator ?? ((ctx) => ctx.request.ip ?? 'unknown'),
|
|
129
|
-
message: config.refresh?.message ?? 'Too many token refresh attempts. Please try again later.',
|
|
130
|
-
progressiveBackoff: config.refresh?.progressiveBackoff ?? false,
|
|
163
|
+
// Build configurations using helper
|
|
164
|
+
const loginConfig = buildRateLimitConfig(config.login, 'login');
|
|
165
|
+
const registerConfig = buildRateLimitConfig(config.register, 'register');
|
|
166
|
+
const passwordResetConfig = buildRateLimitConfig(config.passwordReset, 'passwordReset');
|
|
167
|
+
const refreshConfig = buildRateLimitConfig(config.refresh, 'refresh');
|
|
168
|
+
// Config lookup for operations
|
|
169
|
+
const configs = {
|
|
170
|
+
login: loginConfig,
|
|
171
|
+
register: registerConfig,
|
|
172
|
+
'password-reset': passwordResetConfig,
|
|
173
|
+
refresh: refreshConfig,
|
|
131
174
|
};
|
|
132
175
|
return {
|
|
133
176
|
/**
|
|
@@ -178,18 +221,14 @@ export function createAuthRateLimiter(config = {}) {
|
|
|
178
221
|
*/
|
|
179
222
|
recordFailure: (key, operation) => {
|
|
180
223
|
const fullKey = `auth:${operation}:${key}`;
|
|
181
|
-
const
|
|
182
|
-
? loginConfig
|
|
183
|
-
: operation === 'register'
|
|
184
|
-
? registerConfig
|
|
185
|
-
: passwordResetConfig;
|
|
224
|
+
const operationConfig = configs[operation];
|
|
186
225
|
const now = Date.now();
|
|
187
226
|
const entry = authRateLimitStore.get(fullKey);
|
|
188
227
|
if (!entry || entry.windowResetAt <= now) {
|
|
189
228
|
// Start new window
|
|
190
229
|
authRateLimitStore.set(fullKey, {
|
|
191
230
|
attempts: 1,
|
|
192
|
-
windowResetAt: now +
|
|
231
|
+
windowResetAt: now + operationConfig.windowMs,
|
|
193
232
|
lockoutUntil: null,
|
|
194
233
|
lockoutCount: entry?.lockoutCount ?? 0,
|
|
195
234
|
});
|
|
@@ -198,9 +237,11 @@ export function createAuthRateLimiter(config = {}) {
|
|
|
198
237
|
// Increment in current window
|
|
199
238
|
entry.attempts++;
|
|
200
239
|
// Check if lockout should trigger
|
|
201
|
-
if (entry.attempts >=
|
|
202
|
-
const lockoutMultiplier =
|
|
203
|
-
|
|
240
|
+
if (entry.attempts >= operationConfig.maxAttempts) {
|
|
241
|
+
const lockoutMultiplier = operationConfig.progressiveBackoff
|
|
242
|
+
? 2 ** entry.lockoutCount
|
|
243
|
+
: 1;
|
|
244
|
+
entry.lockoutUntil = now + operationConfig.lockoutDurationMs * lockoutMultiplier;
|
|
204
245
|
entry.lockoutCount++;
|
|
205
246
|
}
|
|
206
247
|
}
|
|
@@ -231,31 +272,18 @@ export function createAuthRateLimiter(config = {}) {
|
|
|
231
272
|
*/
|
|
232
273
|
getRemainingAttempts: (key, operation) => {
|
|
233
274
|
const fullKey = `auth:${operation}:${key}`;
|
|
234
|
-
const
|
|
235
|
-
? loginConfig
|
|
236
|
-
: operation === 'register'
|
|
237
|
-
? registerConfig
|
|
238
|
-
: operation === 'refresh'
|
|
239
|
-
? refreshConfig
|
|
240
|
-
: passwordResetConfig;
|
|
275
|
+
const operationConfig = configs[operation];
|
|
241
276
|
const entry = authRateLimitStore.get(fullKey);
|
|
242
277
|
if (!entry || entry.windowResetAt <= Date.now()) {
|
|
243
|
-
return
|
|
278
|
+
return operationConfig.maxAttempts;
|
|
244
279
|
}
|
|
245
|
-
return Math.max(0,
|
|
280
|
+
return Math.max(0, operationConfig.maxAttempts - entry.attempts);
|
|
246
281
|
},
|
|
247
282
|
};
|
|
248
283
|
}
|
|
249
284
|
// ============================================================================
|
|
250
285
|
// Internal Helpers
|
|
251
286
|
// ============================================================================
|
|
252
|
-
/**
|
|
253
|
-
* Default key generator combining IP and identifier
|
|
254
|
-
*/
|
|
255
|
-
function defaultKeyGenerator(ctx, identifier) {
|
|
256
|
-
const ip = ctx.request.ip ?? 'unknown';
|
|
257
|
-
return identifier ? `${ip}:${identifier.toLowerCase()}` : ip;
|
|
258
|
-
}
|
|
259
287
|
/**
|
|
260
288
|
* Creates the actual rate limit middleware
|
|
261
289
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/auth",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.85",
|
|
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/
|
|
65
|
-
"@veloxts/
|
|
64
|
+
"@veloxts/core": "0.6.85",
|
|
65
|
+
"@veloxts/router": "0.6.85"
|
|
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/validation": "0.6.85",
|
|
90
|
+
"@veloxts/testing": "0.6.85"
|
|
91
91
|
},
|
|
92
92
|
"keywords": [
|
|
93
93
|
"velox",
|