@veloxts/auth 0.6.83 → 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 +18 -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/guards.d.ts +71 -1
- package/dist/guards.js +120 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -3
- package/dist/rate-limit.js +85 -57
- package/dist/testing.d.ts +22 -0
- package/dist/testing.js +25 -0
- package/package.json +9 -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/guards.d.ts
CHANGED
|
@@ -18,13 +18,83 @@ import type { AuthContext, GuardDefinition, GuardFunction, User } from './types.
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function defineGuard<TContext = unknown>(definition: GuardDefinition<TContext>): GuardDefinition<TContext>;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Fluent guard builder for progressive configuration
|
|
22
22
|
*
|
|
23
|
+
* Allows building guards step-by-step with method chaining.
|
|
24
|
+
* The builder is compatible with GuardLike, so it can be used
|
|
25
|
+
* directly with `.guard()` on procedures.
|
|
26
|
+
*
|
|
27
|
+
* **Note**: This builder uses mutable internal state. Each method
|
|
28
|
+
* modifies the builder and returns the same instance. See
|
|
29
|
+
* `createGuardBuilder` for usage patterns and caveats.
|
|
30
|
+
*/
|
|
31
|
+
export interface GuardBuilder<TContext> {
|
|
32
|
+
/** Guard name for error messages (read-only, set via named()) */
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/** Guard check function (read-only) */
|
|
35
|
+
readonly check: GuardFunction<TContext>;
|
|
36
|
+
/** Custom error message (read-only, set via msg()) */
|
|
37
|
+
readonly message: string | undefined;
|
|
38
|
+
/** HTTP status code on failure (read-only, set via status()) */
|
|
39
|
+
readonly statusCode: number;
|
|
40
|
+
/** Set a descriptive name (used in error messages and debugging) */
|
|
41
|
+
named(name: string): GuardBuilder<TContext>;
|
|
42
|
+
/** Set custom error message shown when guard fails */
|
|
43
|
+
msg(message: string): GuardBuilder<TContext>;
|
|
44
|
+
/** Set HTTP status code returned when guard fails (default: 403) */
|
|
45
|
+
status(code: number): GuardBuilder<TContext>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resets the guard counter to zero.
|
|
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.
|
|
52
|
+
*
|
|
53
|
+
* @internal
|
|
23
54
|
* @example
|
|
24
55
|
* ```typescript
|
|
56
|
+
* import { _resetGuardCounter } from '@veloxts/auth';
|
|
57
|
+
*
|
|
58
|
+
* beforeEach(() => {
|
|
59
|
+
* _resetGuardCounter();
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function _resetGuardCounter(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Creates a guard with simplified syntax
|
|
66
|
+
*
|
|
67
|
+
* Supports three usage patterns with progressive disclosure:
|
|
68
|
+
*
|
|
69
|
+
* @example Simple check function (returns builder for configuration)
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const isVerified = guard((ctx) => ctx.user?.emailVerified === true)
|
|
72
|
+
* .msg('Email verification required');
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Check with message (most common - auto-generates name)
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const isVerified = guard(
|
|
78
|
+
* (ctx) => ctx.user?.emailVerified === true,
|
|
79
|
+
* 'Email verification required'
|
|
80
|
+
* );
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Named guard (explicit name for debugging)
|
|
84
|
+
* ```typescript
|
|
25
85
|
* const isActive = guard('isActive', (ctx) => ctx.user?.status === 'active');
|
|
26
86
|
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example Full fluent configuration
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const isPremium = guard((ctx) => ctx.user?.subscription === 'premium')
|
|
91
|
+
* .named('isPremium')
|
|
92
|
+
* .msg('Premium subscription required')
|
|
93
|
+
* .status(402);
|
|
94
|
+
* ```
|
|
27
95
|
*/
|
|
96
|
+
export declare function guard<TContext = unknown>(check: GuardFunction<TContext>): GuardBuilder<TContext>;
|
|
97
|
+
export declare function guard<TContext = unknown>(check: GuardFunction<TContext>, message: string): GuardDefinition<TContext>;
|
|
28
98
|
export declare function guard<TContext = unknown>(name: string, check: GuardFunction<TContext>): GuardDefinition<TContext>;
|
|
29
99
|
/**
|
|
30
100
|
* Guard that requires authentication
|
package/dist/guards.js
CHANGED
|
@@ -24,15 +24,131 @@ export function defineGuard(definition) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* Counter for generating unique guard names when not provided
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
let guardCounter = 0;
|
|
31
|
+
/**
|
|
32
|
+
* Resets the guard counter to zero.
|
|
33
|
+
*
|
|
34
|
+
* This is intended for testing purposes only to ensure deterministic
|
|
35
|
+
* guard naming across test runs. Should not be used in production code.
|
|
28
36
|
*
|
|
37
|
+
* @internal
|
|
29
38
|
* @example
|
|
30
39
|
* ```typescript
|
|
31
|
-
*
|
|
40
|
+
* import { _resetGuardCounter } from '@veloxts/auth';
|
|
41
|
+
*
|
|
42
|
+
* beforeEach(() => {
|
|
43
|
+
* _resetGuardCounter();
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function _resetGuardCounter() {
|
|
48
|
+
guardCounter = 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Attempts to infer a meaningful name from a guard check function
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
function inferGuardName(check) {
|
|
55
|
+
// Try to use function name if it exists and isn't generic
|
|
56
|
+
if (check.name && check.name !== 'check' && check.name !== 'anonymous') {
|
|
57
|
+
return check.name;
|
|
58
|
+
}
|
|
59
|
+
// Fall back to generated name
|
|
60
|
+
return `guard_${++guardCounter}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates a guard builder instance.
|
|
64
|
+
*
|
|
65
|
+
* The builder uses **mutable internal state** with method chaining that returns
|
|
66
|
+
* the same instance. This is intentional for performance and API simplicity:
|
|
67
|
+
*
|
|
68
|
+
* ```typescript
|
|
69
|
+
* // Each method mutates the builder and returns `this`
|
|
70
|
+
* const myGuard = guard((ctx) => ctx.user?.active)
|
|
71
|
+
* .named('isActive') // Mutates name, returns same builder
|
|
72
|
+
* .msg('User inactive') // Mutates message, returns same builder
|
|
73
|
+
* .status(403); // Mutates statusCode, returns same builder
|
|
74
|
+
*
|
|
75
|
+
* // The builder IS the guard definition (implements GuardLike)
|
|
76
|
+
* // No need to call .build() - use directly with .guard()
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* **Important**: Because the builder mutates, avoid patterns like:
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // DON'T do this - both variables reference the same mutable builder
|
|
82
|
+
* const base = guard((ctx) => ctx.user != null);
|
|
83
|
+
* const withMsg = base.msg('Auth required');
|
|
84
|
+
* const withOtherMsg = base.msg('Login needed'); // Overwrites previous msg!
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* If you need variations, create separate guards:
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const authRequired = guard((ctx) => ctx.user != null, 'Auth required');
|
|
90
|
+
* const loginNeeded = guard((ctx) => ctx.user != null, 'Login needed');
|
|
32
91
|
* ```
|
|
92
|
+
*
|
|
93
|
+
* @internal
|
|
33
94
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
95
|
+
function createGuardBuilder(check, initialName) {
|
|
96
|
+
// Mutable internal state - intentionally not exposed directly
|
|
97
|
+
let guardName = initialName;
|
|
98
|
+
let guardMessage;
|
|
99
|
+
let guardStatusCode = 403;
|
|
100
|
+
const builder = {
|
|
101
|
+
// GuardLike compatible properties (getters for current values)
|
|
102
|
+
get name() {
|
|
103
|
+
return guardName;
|
|
104
|
+
},
|
|
105
|
+
get check() {
|
|
106
|
+
return check;
|
|
107
|
+
},
|
|
108
|
+
get message() {
|
|
109
|
+
return guardMessage;
|
|
110
|
+
},
|
|
111
|
+
get statusCode() {
|
|
112
|
+
return guardStatusCode;
|
|
113
|
+
},
|
|
114
|
+
// Builder methods (return self for chaining)
|
|
115
|
+
named(name) {
|
|
116
|
+
guardName = name;
|
|
117
|
+
return builder;
|
|
118
|
+
},
|
|
119
|
+
msg(message) {
|
|
120
|
+
guardMessage = message;
|
|
121
|
+
return builder;
|
|
122
|
+
},
|
|
123
|
+
status(code) {
|
|
124
|
+
guardStatusCode = code;
|
|
125
|
+
return builder;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return builder;
|
|
129
|
+
}
|
|
130
|
+
// Implementation
|
|
131
|
+
export function guard(nameOrCheck, checkOrMessage) {
|
|
132
|
+
// Overload 3: Legacy (name, check)
|
|
133
|
+
if (typeof nameOrCheck === 'string' && typeof checkOrMessage === 'function') {
|
|
134
|
+
return defineGuard({ name: nameOrCheck, check: checkOrMessage });
|
|
135
|
+
}
|
|
136
|
+
// Overloads 1 & 2: (check) or (check, message)
|
|
137
|
+
if (typeof nameOrCheck === 'function') {
|
|
138
|
+
const check = nameOrCheck;
|
|
139
|
+
const message = typeof checkOrMessage === 'string' ? checkOrMessage : undefined;
|
|
140
|
+
if (message !== undefined) {
|
|
141
|
+
// Overload 2: Simple form with message - return completed guard
|
|
142
|
+
return defineGuard({
|
|
143
|
+
name: inferGuardName(check),
|
|
144
|
+
check,
|
|
145
|
+
message,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Overload 1: Return builder for fluent configuration
|
|
149
|
+
return createGuardBuilder(check, inferGuardName(check));
|
|
150
|
+
}
|
|
151
|
+
throw new Error('Invalid guard arguments: expected (check), (check, message), or (name, check)');
|
|
36
152
|
}
|
|
37
153
|
// ============================================================================
|
|
38
154
|
// Built-in Guards
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { createEnhancedTokenStore, DEFAULT_ALLOWED_ROLES, parseUserRoles, } from
|
|
|
18
18
|
export type { AuthenticatedContext, InferNarrowedContext, NarrowingGuard, RoleNarrowedContext, } from './guards-narrowing.js';
|
|
19
19
|
export { authenticatedNarrow, hasRoleNarrow } from './guards-narrowing.js';
|
|
20
20
|
export { hashPassword, PasswordHasher, passwordHasher, verifyPassword, } from './hash.js';
|
|
21
|
+
export type { GuardBuilder } from './guards.js';
|
|
21
22
|
export { allOf, anyOf, authenticated, defineGuard, emailVerified, executeGuard, executeGuards, guard, hasAnyPermission, hasPermission, hasRole, not, userCan, } from './guards.js';
|
|
22
23
|
export { authorize, can, cannot, clearPolicies, createAdminOnlyPolicy, createOwnerOrAdminPolicy, createPolicyBuilder, createReadOnlyPolicy, definePolicy, getPolicy, registerPolicy, } from './policies.js';
|
|
23
24
|
export { authMiddleware, clearRateLimitStore, rateLimitMiddleware, } from './middleware.js';
|
package/dist/index.js
CHANGED
|
@@ -23,9 +23,6 @@ export { authenticatedNarrow, hasRoleNarrow } from './guards-narrowing.js';
|
|
|
23
23
|
// Password Hashing
|
|
24
24
|
// ============================================================================
|
|
25
25
|
export { hashPassword, PasswordHasher, passwordHasher, verifyPassword, } from './hash.js';
|
|
26
|
-
// ============================================================================
|
|
27
|
-
// Guards
|
|
28
|
-
// ============================================================================
|
|
29
26
|
export {
|
|
30
27
|
// Combinators
|
|
31
28
|
allOf, anyOf,
|
|
@@ -35,6 +32,7 @@ authenticated,
|
|
|
35
32
|
defineGuard, emailVerified,
|
|
36
33
|
// Execution
|
|
37
34
|
executeGuard, executeGuards, guard, hasAnyPermission, hasPermission, hasRole, not, userCan, } from './guards.js';
|
|
35
|
+
// NOTE: _resetGuardCounter is available via '@veloxts/auth/testing' for test isolation
|
|
38
36
|
// ============================================================================
|
|
39
37
|
// Policies
|
|
40
38
|
// ============================================================================
|
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
|
*/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @veloxts/auth/testing - Internal testing utilities
|
|
3
|
+
*
|
|
4
|
+
* This module exports utilities intended for testing purposes only.
|
|
5
|
+
* These are NOT part of the public API and may change without notice.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { _resetGuardCounter } from '@veloxts/auth/testing';
|
|
10
|
+
*
|
|
11
|
+
* beforeEach(() => {
|
|
12
|
+
* _resetGuardCounter();
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
* @module @veloxts/auth/testing
|
|
18
|
+
*/
|
|
19
|
+
export { _resetGuardCounter } from './guards.js';
|
|
20
|
+
export { clearRateLimitStore } from './middleware.js';
|
|
21
|
+
export { clearPolicies } from './policies.js';
|
|
22
|
+
export { clearAuthRateLimitStore, stopAuthRateLimitCleanup } from './rate-limit.js';
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @veloxts/auth/testing - Internal testing utilities
|
|
3
|
+
*
|
|
4
|
+
* This module exports utilities intended for testing purposes only.
|
|
5
|
+
* These are NOT part of the public API and may change without notice.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { _resetGuardCounter } from '@veloxts/auth/testing';
|
|
10
|
+
*
|
|
11
|
+
* beforeEach(() => {
|
|
12
|
+
* _resetGuardCounter();
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
* @module @veloxts/auth/testing
|
|
18
|
+
*/
|
|
19
|
+
// Guard testing utilities
|
|
20
|
+
export { _resetGuardCounter } from './guards.js';
|
|
21
|
+
// Rate limit store clearing (for test isolation)
|
|
22
|
+
export { clearRateLimitStore } from './middleware.js';
|
|
23
|
+
// Policy registry clearing (for test isolation)
|
|
24
|
+
export { clearPolicies } from './policies.js';
|
|
25
|
+
export { clearAuthRateLimitStore, stopAuthRateLimitCleanup } from './rate-limit.js';
|
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",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./testing": {
|
|
14
|
+
"types": "./dist/testing.d.ts",
|
|
15
|
+
"import": "./dist/testing.js"
|
|
16
|
+
},
|
|
13
17
|
"./adapters": {
|
|
14
18
|
"types": "./dist/adapters/index.d.ts",
|
|
15
19
|
"import": "./dist/adapters/index.js"
|
|
@@ -57,8 +61,8 @@
|
|
|
57
61
|
"dependencies": {
|
|
58
62
|
"@fastify/cookie": "11.0.2",
|
|
59
63
|
"fastify": "5.6.2",
|
|
60
|
-
"@veloxts/core": "0.6.
|
|
61
|
-
"@veloxts/router": "0.6.
|
|
64
|
+
"@veloxts/core": "0.6.85",
|
|
65
|
+
"@veloxts/router": "0.6.85"
|
|
62
66
|
},
|
|
63
67
|
"peerDependencies": {
|
|
64
68
|
"argon2": ">=0.30.0",
|
|
@@ -82,8 +86,8 @@
|
|
|
82
86
|
"fastify-plugin": "5.1.0",
|
|
83
87
|
"typescript": "5.9.3",
|
|
84
88
|
"vitest": "4.0.16",
|
|
85
|
-
"@veloxts/validation": "0.6.
|
|
86
|
-
"@veloxts/testing": "0.6.
|
|
89
|
+
"@veloxts/validation": "0.6.85",
|
|
90
|
+
"@veloxts/testing": "0.6.85"
|
|
87
91
|
},
|
|
88
92
|
"keywords": [
|
|
89
93
|
"velox",
|