@veloxts/auth 0.6.67 → 0.6.69
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 +56 -0
- package/dist/guards-narrowing.d.ts +123 -0
- package/dist/guards-narrowing.js +80 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/token-store.d.ts +105 -0
- package/dist/token-store.js +159 -0
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# @veloxts/auth
|
|
2
2
|
|
|
3
|
+
## 0.6.69
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- implement user feedback improvements across packages
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
Addresses 9 user feedback items to improve DX, reduce boilerplate, and eliminate template duplications.
|
|
12
|
+
|
|
13
|
+
### Phase 1: Validation Helpers (`@veloxts/validation`)
|
|
14
|
+
|
|
15
|
+
- Add `prismaDecimal()`, `prismaDecimalNullable()`, `prismaDecimalOptional()` for Prisma Decimal → number conversion
|
|
16
|
+
- Add `dateToIso`, `dateToIsoNullable`, `dateToIsoOptional` aliases for consistency
|
|
17
|
+
|
|
18
|
+
### Phase 2: Template Deduplication (`@veloxts/auth`)
|
|
19
|
+
|
|
20
|
+
- Export `createEnhancedTokenStore()` with token revocation and refresh token reuse detection
|
|
21
|
+
- Export `parseUserRoles()` and `DEFAULT_ALLOWED_ROLES`
|
|
22
|
+
- Fix memory leak: track pending timeouts for proper cleanup on `destroy()`
|
|
23
|
+
- Update templates to import from `@veloxts/auth` instead of duplicating code
|
|
24
|
+
- Fix jwtManager singleton pattern in templates
|
|
25
|
+
|
|
26
|
+
### Phase 3: Router Helpers (`@veloxts/router`)
|
|
27
|
+
|
|
28
|
+
- Add `createRouter()` returning `{ collections, router }` for DRY setup
|
|
29
|
+
- Add `toRouter()` for router-only use cases
|
|
30
|
+
- Update all router templates to use `createRouter()`
|
|
31
|
+
|
|
32
|
+
### Phase 4: Guard Type Narrowing - Experimental (`@veloxts/auth`, `@veloxts/router`)
|
|
33
|
+
|
|
34
|
+
- Add `NarrowingGuard` interface with phantom `_narrows` type
|
|
35
|
+
- Add `authenticatedNarrow` and `hasRoleNarrow()` guards
|
|
36
|
+
- Add `guardNarrow()` method to `ProcedureBuilder` for context narrowing
|
|
37
|
+
- Enables `ctx.user` to be non-null after guard passes
|
|
38
|
+
|
|
39
|
+
### Phase 5: Documentation (`@veloxts/router`)
|
|
40
|
+
|
|
41
|
+
- Document `.rest()` override patterns
|
|
42
|
+
- Document `createRouter()` helper usage
|
|
43
|
+
- Document `guardNarrow()` experimental API
|
|
44
|
+
- Add schema browser-safety patterns for RSC apps
|
|
45
|
+
|
|
46
|
+
- Updated dependencies
|
|
47
|
+
- @veloxts/core@0.6.69
|
|
48
|
+
- @veloxts/router@0.6.69
|
|
49
|
+
|
|
50
|
+
## 0.6.68
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- ci: add Claude code review and security review workflows, add GitHub release workflow, remove npm publish job
|
|
55
|
+
- Updated dependencies
|
|
56
|
+
- @veloxts/core@0.6.68
|
|
57
|
+
- @veloxts/router@0.6.68
|
|
58
|
+
|
|
3
59
|
## 0.6.67
|
|
4
60
|
|
|
5
61
|
### Patch Changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing Guards (Experimental)
|
|
3
|
+
*
|
|
4
|
+
* These guards provide TypeScript type narrowing after they pass.
|
|
5
|
+
* When using `guardNarrow(authenticatedNarrow)`, the context type
|
|
6
|
+
* is narrowed to guarantee `ctx.user` is non-null.
|
|
7
|
+
*
|
|
8
|
+
* EXPERIMENTAL: This API may change. The current recommended approach
|
|
9
|
+
* is to use middleware for context type extension.
|
|
10
|
+
*
|
|
11
|
+
* @module auth/guards-narrowing
|
|
12
|
+
*/
|
|
13
|
+
import type { AuthContext, GuardFunction, User } from './types.js';
|
|
14
|
+
/**
|
|
15
|
+
* A guard that narrows the context type after passing.
|
|
16
|
+
*
|
|
17
|
+
* The `_narrows` phantom type indicates what the guard guarantees
|
|
18
|
+
* about the context after it passes.
|
|
19
|
+
*
|
|
20
|
+
* @template TRequired - Context properties required to run the guard
|
|
21
|
+
* @template TGuaranteed - Context properties guaranteed after guard passes
|
|
22
|
+
*/
|
|
23
|
+
export interface NarrowingGuard<TRequired, TGuaranteed> {
|
|
24
|
+
/** Guard name for error messages */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Guard check function (matches GuardFunction signature) */
|
|
27
|
+
check: GuardFunction<TRequired>;
|
|
28
|
+
/** Custom error message */
|
|
29
|
+
message?: string;
|
|
30
|
+
/** HTTP status code for guard failures */
|
|
31
|
+
statusCode?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Phantom type declaring what the guard guarantees.
|
|
34
|
+
* Used by ProcedureBuilder.guardNarrow() for type narrowing.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
readonly _narrows: TGuaranteed;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Context type with a guaranteed authenticated user.
|
|
41
|
+
*
|
|
42
|
+
* After `authenticatedNarrow` passes, the context is narrowed to this type.
|
|
43
|
+
*/
|
|
44
|
+
export interface AuthenticatedContext {
|
|
45
|
+
auth: AuthContext & {
|
|
46
|
+
isAuthenticated: true;
|
|
47
|
+
};
|
|
48
|
+
user: User;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Context type with a guaranteed user having specific roles.
|
|
52
|
+
*/
|
|
53
|
+
export interface RoleNarrowedContext {
|
|
54
|
+
user: User & {
|
|
55
|
+
roles: string[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Authenticated guard with type narrowing.
|
|
60
|
+
*
|
|
61
|
+
* When used with `guardNarrow()`, narrows `ctx.user` from `User | undefined`
|
|
62
|
+
* to `User`, eliminating the need for null checks in the handler.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { authenticatedNarrow } from '@veloxts/auth';
|
|
67
|
+
*
|
|
68
|
+
* // With guardNarrow (experimental):
|
|
69
|
+
* procedure()
|
|
70
|
+
* .guardNarrow(authenticatedNarrow)
|
|
71
|
+
* .query(({ ctx }) => {
|
|
72
|
+
* // ctx.user is typed as User (non-null)
|
|
73
|
+
* return { email: ctx.user.email };
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Current recommended alternative using middleware:
|
|
77
|
+
* procedure()
|
|
78
|
+
* .guard(authenticated)
|
|
79
|
+
* .use(async ({ ctx, next }) => {
|
|
80
|
+
* if (!ctx.user) throw new Error('Unreachable');
|
|
81
|
+
* return next({ ctx: { user: ctx.user } });
|
|
82
|
+
* })
|
|
83
|
+
* .query(({ ctx }) => {
|
|
84
|
+
* // ctx.user is non-null via middleware
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare const authenticatedNarrow: NarrowingGuard<{
|
|
89
|
+
auth?: AuthContext;
|
|
90
|
+
}, AuthenticatedContext>;
|
|
91
|
+
/**
|
|
92
|
+
* Creates a role-checking guard with type narrowing.
|
|
93
|
+
*
|
|
94
|
+
* Narrows `ctx.user` to guarantee non-null with roles array.
|
|
95
|
+
*
|
|
96
|
+
* @param roles - Required role(s)
|
|
97
|
+
* @returns NarrowingGuard that guarantees user with roles
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* import { hasRoleNarrow } from '@veloxts/auth';
|
|
102
|
+
*
|
|
103
|
+
* procedure()
|
|
104
|
+
* .guardNarrow(hasRoleNarrow('admin'))
|
|
105
|
+
* .mutation(({ ctx }) => {
|
|
106
|
+
* // ctx.user is typed as User (non-null)
|
|
107
|
+
* // ctx.user.roles is string[]
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export declare function hasRoleNarrow(roles: string | string[]): NarrowingGuard<{
|
|
112
|
+
user?: User;
|
|
113
|
+
}, RoleNarrowedContext>;
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the narrowed context type from a NarrowingGuard.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* type Ctx = InferNarrowedContext<typeof authenticatedNarrow>;
|
|
120
|
+
* // Ctx = AuthenticatedContext = { auth: AuthContext & { isAuthenticated: true }; user: User }
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export type InferNarrowedContext<T> = T extends NarrowingGuard<unknown, infer U> ? U : never;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing Guards (Experimental)
|
|
3
|
+
*
|
|
4
|
+
* These guards provide TypeScript type narrowing after they pass.
|
|
5
|
+
* When using `guardNarrow(authenticatedNarrow)`, the context type
|
|
6
|
+
* is narrowed to guarantee `ctx.user` is non-null.
|
|
7
|
+
*
|
|
8
|
+
* EXPERIMENTAL: This API may change. The current recommended approach
|
|
9
|
+
* is to use middleware for context type extension.
|
|
10
|
+
*
|
|
11
|
+
* @module auth/guards-narrowing
|
|
12
|
+
*/
|
|
13
|
+
import { authenticated, hasRole as hasRoleBase } from './guards.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Narrowing Guards
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/**
|
|
18
|
+
* Authenticated guard with type narrowing.
|
|
19
|
+
*
|
|
20
|
+
* When used with `guardNarrow()`, narrows `ctx.user` from `User | undefined`
|
|
21
|
+
* to `User`, eliminating the need for null checks in the handler.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { authenticatedNarrow } from '@veloxts/auth';
|
|
26
|
+
*
|
|
27
|
+
* // With guardNarrow (experimental):
|
|
28
|
+
* procedure()
|
|
29
|
+
* .guardNarrow(authenticatedNarrow)
|
|
30
|
+
* .query(({ ctx }) => {
|
|
31
|
+
* // ctx.user is typed as User (non-null)
|
|
32
|
+
* return { email: ctx.user.email };
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Current recommended alternative using middleware:
|
|
36
|
+
* procedure()
|
|
37
|
+
* .guard(authenticated)
|
|
38
|
+
* .use(async ({ ctx, next }) => {
|
|
39
|
+
* if (!ctx.user) throw new Error('Unreachable');
|
|
40
|
+
* return next({ ctx: { user: ctx.user } });
|
|
41
|
+
* })
|
|
42
|
+
* .query(({ ctx }) => {
|
|
43
|
+
* // ctx.user is non-null via middleware
|
|
44
|
+
* });
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const authenticatedNarrow = {
|
|
48
|
+
...authenticated,
|
|
49
|
+
// Phantom type: value is never used at runtime, only carries type info.
|
|
50
|
+
// The `undefined as unknown as T` pattern is standard for phantom types.
|
|
51
|
+
_narrows: undefined,
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Creates a role-checking guard with type narrowing.
|
|
55
|
+
*
|
|
56
|
+
* Narrows `ctx.user` to guarantee non-null with roles array.
|
|
57
|
+
*
|
|
58
|
+
* @param roles - Required role(s)
|
|
59
|
+
* @returns NarrowingGuard that guarantees user with roles
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* import { hasRoleNarrow } from '@veloxts/auth';
|
|
64
|
+
*
|
|
65
|
+
* procedure()
|
|
66
|
+
* .guardNarrow(hasRoleNarrow('admin'))
|
|
67
|
+
* .mutation(({ ctx }) => {
|
|
68
|
+
* // ctx.user is typed as User (non-null)
|
|
69
|
+
* // ctx.user.roles is string[]
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function hasRoleNarrow(roles) {
|
|
74
|
+
const baseGuard = hasRoleBase(roles);
|
|
75
|
+
return {
|
|
76
|
+
...baseGuard,
|
|
77
|
+
// Phantom type: carries type info for guardNarrow() context narrowing
|
|
78
|
+
_narrows: undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ LegacySessionConfig, PolicyAction, PolicyDefinition, RateLimitConfig, TokenPair,
|
|
|
16
16
|
export { AuthError } from './types.js';
|
|
17
17
|
export type { TokenStore } from './jwt.js';
|
|
18
18
|
export { createInMemoryTokenStore, generateTokenId, isValidTimespan, JwtManager, jwtManager, parseTimeToSeconds, validateTokenExpiration, } from './jwt.js';
|
|
19
|
+
export type { EnhancedTokenStore, EnhancedTokenStoreOptions } from './token-store.js';
|
|
20
|
+
export { createEnhancedTokenStore, DEFAULT_ALLOWED_ROLES, parseUserRoles, } from './token-store.js';
|
|
21
|
+
export type { AuthenticatedContext, InferNarrowedContext, NarrowingGuard, RoleNarrowedContext, } from './guards-narrowing.js';
|
|
22
|
+
export { authenticatedNarrow, hasRoleNarrow } from './guards-narrowing.js';
|
|
19
23
|
export { hashPassword, PasswordHasher, passwordHasher, verifyPassword, } from './hash.js';
|
|
20
24
|
export { allOf, anyOf, authenticated, defineGuard, emailVerified, executeGuard, executeGuards, guard, hasAnyPermission, hasPermission, hasRole, not, userCan, } from './guards.js';
|
|
21
25
|
export { authorize, can, cannot, clearPolicies, createAdminOnlyPolicy, createOwnerOrAdminPolicy, createPolicyBuilder, createReadOnlyPolicy, definePolicy, getPolicy, registerPolicy, } from './policies.js';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
export { AUTH_VERSION } from './plugin.js';
|
|
14
14
|
export { AuthError } from './types.js';
|
|
15
15
|
export { createInMemoryTokenStore, generateTokenId, isValidTimespan, JwtManager, jwtManager, parseTimeToSeconds, validateTokenExpiration, } from './jwt.js';
|
|
16
|
+
export { createEnhancedTokenStore, DEFAULT_ALLOWED_ROLES, parseUserRoles, } from './token-store.js';
|
|
17
|
+
export { authenticatedNarrow, hasRoleNarrow } from './guards-narrowing.js';
|
|
16
18
|
// ============================================================================
|
|
17
19
|
// Password Hashing
|
|
18
20
|
// ============================================================================
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Token Store for JWT Revocation
|
|
3
|
+
*
|
|
4
|
+
* Provides an enhanced in-memory token store with:
|
|
5
|
+
* - Token revocation with expiry
|
|
6
|
+
* - Refresh token reuse detection
|
|
7
|
+
* - Automatic cleanup of expired entries
|
|
8
|
+
*
|
|
9
|
+
* @module auth/token-store
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Enhanced token store interface
|
|
13
|
+
*
|
|
14
|
+
* Extends basic token revocation with refresh token reuse detection
|
|
15
|
+
* and automatic cleanup capabilities.
|
|
16
|
+
*/
|
|
17
|
+
export interface EnhancedTokenStore {
|
|
18
|
+
/** Revoke a token with optional expiry time */
|
|
19
|
+
revoke(jti: string, expiresInMs?: number): void;
|
|
20
|
+
/** Check if token is revoked */
|
|
21
|
+
isRevoked(jti: string): boolean;
|
|
22
|
+
/** Mark refresh token as used (for reuse detection) */
|
|
23
|
+
markRefreshTokenUsed(jti: string, userId: string): void;
|
|
24
|
+
/** Check if refresh token was already used, returns userId if reused */
|
|
25
|
+
isRefreshTokenUsed(jti: string): string | undefined;
|
|
26
|
+
/** Revoke all tokens for a user (placeholder for production implementation) */
|
|
27
|
+
revokeAllUserTokens(userId: string): void;
|
|
28
|
+
/** Clear all entries (useful for testing) */
|
|
29
|
+
clear(): void;
|
|
30
|
+
/** Stop cleanup interval and release resources */
|
|
31
|
+
destroy(): void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for creating an enhanced token store
|
|
35
|
+
*/
|
|
36
|
+
export interface EnhancedTokenStoreOptions {
|
|
37
|
+
/** Interval for cleanup of expired entries (default: 5 minutes) */
|
|
38
|
+
cleanupIntervalMs?: number;
|
|
39
|
+
/** Default expiry for revoked tokens (default: 7 days) */
|
|
40
|
+
defaultExpiryMs?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates an enhanced in-memory token store
|
|
44
|
+
*
|
|
45
|
+
* Provides token revocation with expiry tracking and refresh token
|
|
46
|
+
* reuse detection for security.
|
|
47
|
+
*
|
|
48
|
+
* **WARNING: NOT suitable for production!**
|
|
49
|
+
* Use Redis or database-backed store for:
|
|
50
|
+
* - Persistence across server restarts
|
|
51
|
+
* - Horizontal scaling (multiple server instances)
|
|
52
|
+
* - Proper token revocation across deployments
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { createEnhancedTokenStore } from '@veloxts/auth';
|
|
57
|
+
*
|
|
58
|
+
* // Create store with defaults
|
|
59
|
+
* const tokenStore = createEnhancedTokenStore();
|
|
60
|
+
*
|
|
61
|
+
* // Revoke token on logout
|
|
62
|
+
* tokenStore.revoke(accessTokenJti);
|
|
63
|
+
*
|
|
64
|
+
* // Detect refresh token reuse (security measure)
|
|
65
|
+
* const previousUser = tokenStore.isRefreshTokenUsed(refreshJti);
|
|
66
|
+
* if (previousUser) {
|
|
67
|
+
* // Potential token theft - revoke all user tokens
|
|
68
|
+
* tokenStore.revokeAllUserTokens(previousUser);
|
|
69
|
+
* throw new SecurityError('Token reuse detected');
|
|
70
|
+
* }
|
|
71
|
+
* tokenStore.markRefreshTokenUsed(refreshJti, userId);
|
|
72
|
+
*
|
|
73
|
+
* // Clean up on shutdown
|
|
74
|
+
* process.on('SIGTERM', () => tokenStore.destroy());
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function createEnhancedTokenStore(options?: EnhancedTokenStoreOptions): EnhancedTokenStore;
|
|
78
|
+
/**
|
|
79
|
+
* Default allowed roles for role parsing
|
|
80
|
+
*/
|
|
81
|
+
export declare const DEFAULT_ALLOWED_ROLES: readonly ["user", "admin", "moderator", "editor"];
|
|
82
|
+
/**
|
|
83
|
+
* Parses JSON-encoded roles string to array
|
|
84
|
+
*
|
|
85
|
+
* Safely parses a JSON array of role strings, filtering to only allowed roles.
|
|
86
|
+
* Returns default ['user'] role if parsing fails or no valid roles found.
|
|
87
|
+
*
|
|
88
|
+
* @param rolesJson - JSON string of roles (e.g., '["admin", "user"]')
|
|
89
|
+
* @param allowedRoles - Optional list of valid roles (defaults to DEFAULT_ALLOWED_ROLES)
|
|
90
|
+
* @returns Array of valid roles, defaults to ['user'] if parsing fails
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { parseUserRoles } from '@veloxts/auth';
|
|
95
|
+
*
|
|
96
|
+
* const roles = parseUserRoles(user.roles);
|
|
97
|
+
* // Input: '["admin", "user"]' -> Output: ['admin', 'user']
|
|
98
|
+
* // Input: null -> Output: ['user']
|
|
99
|
+
* // Input: 'invalid' -> Output: ['user']
|
|
100
|
+
*
|
|
101
|
+
* // With custom allowed roles
|
|
102
|
+
* const roles = parseUserRoles(user.roles, ['admin', 'superadmin']);
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function parseUserRoles(rolesJson: string | null, allowedRoles?: readonly string[]): string[];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Token Store for JWT Revocation
|
|
3
|
+
*
|
|
4
|
+
* Provides an enhanced in-memory token store with:
|
|
5
|
+
* - Token revocation with expiry
|
|
6
|
+
* - Refresh token reuse detection
|
|
7
|
+
* - Automatic cleanup of expired entries
|
|
8
|
+
*
|
|
9
|
+
* @module auth/token-store
|
|
10
|
+
*/
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Implementation
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Creates an enhanced in-memory token store
|
|
16
|
+
*
|
|
17
|
+
* Provides token revocation with expiry tracking and refresh token
|
|
18
|
+
* reuse detection for security.
|
|
19
|
+
*
|
|
20
|
+
* **WARNING: NOT suitable for production!**
|
|
21
|
+
* Use Redis or database-backed store for:
|
|
22
|
+
* - Persistence across server restarts
|
|
23
|
+
* - Horizontal scaling (multiple server instances)
|
|
24
|
+
* - Proper token revocation across deployments
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { createEnhancedTokenStore } from '@veloxts/auth';
|
|
29
|
+
*
|
|
30
|
+
* // Create store with defaults
|
|
31
|
+
* const tokenStore = createEnhancedTokenStore();
|
|
32
|
+
*
|
|
33
|
+
* // Revoke token on logout
|
|
34
|
+
* tokenStore.revoke(accessTokenJti);
|
|
35
|
+
*
|
|
36
|
+
* // Detect refresh token reuse (security measure)
|
|
37
|
+
* const previousUser = tokenStore.isRefreshTokenUsed(refreshJti);
|
|
38
|
+
* if (previousUser) {
|
|
39
|
+
* // Potential token theft - revoke all user tokens
|
|
40
|
+
* tokenStore.revokeAllUserTokens(previousUser);
|
|
41
|
+
* throw new SecurityError('Token reuse detected');
|
|
42
|
+
* }
|
|
43
|
+
* tokenStore.markRefreshTokenUsed(refreshJti, userId);
|
|
44
|
+
*
|
|
45
|
+
* // Clean up on shutdown
|
|
46
|
+
* process.on('SIGTERM', () => tokenStore.destroy());
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function createEnhancedTokenStore(options) {
|
|
50
|
+
const { cleanupIntervalMs = 5 * 60 * 1000, defaultExpiryMs = 7 * 24 * 60 * 60 * 1000 } = options ?? {};
|
|
51
|
+
const revokedTokens = new Map();
|
|
52
|
+
const usedRefreshTokens = new Map();
|
|
53
|
+
// Track pending timeouts to prevent memory leaks on destroy()
|
|
54
|
+
const pendingTimeouts = new Set();
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
for (const [jti, expiry] of revokedTokens.entries()) {
|
|
58
|
+
if (now > expiry) {
|
|
59
|
+
revokedTokens.delete(jti);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const cleanupInterval = setInterval(cleanup, cleanupIntervalMs);
|
|
64
|
+
return {
|
|
65
|
+
revoke(jti, expiresInMs = defaultExpiryMs) {
|
|
66
|
+
revokedTokens.set(jti, Date.now() + expiresInMs);
|
|
67
|
+
},
|
|
68
|
+
isRevoked(jti) {
|
|
69
|
+
const expiry = revokedTokens.get(jti);
|
|
70
|
+
if (!expiry)
|
|
71
|
+
return false;
|
|
72
|
+
if (Date.now() > expiry) {
|
|
73
|
+
revokedTokens.delete(jti);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
markRefreshTokenUsed(jti, userId) {
|
|
79
|
+
usedRefreshTokens.set(jti, userId);
|
|
80
|
+
// Auto-expire after default expiry, track timeout for cleanup
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
usedRefreshTokens.delete(jti);
|
|
83
|
+
pendingTimeouts.delete(timeout);
|
|
84
|
+
}, defaultExpiryMs);
|
|
85
|
+
pendingTimeouts.add(timeout);
|
|
86
|
+
},
|
|
87
|
+
isRefreshTokenUsed(jti) {
|
|
88
|
+
return usedRefreshTokens.get(jti);
|
|
89
|
+
},
|
|
90
|
+
revokeAllUserTokens(userId) {
|
|
91
|
+
// Placeholder - in production, implement proper user->token mapping
|
|
92
|
+
console.warn(`[Security] Token reuse detected for user ${userId}. ` +
|
|
93
|
+
'All tokens should be revoked. Implement proper user->token mapping for production.');
|
|
94
|
+
},
|
|
95
|
+
clear() {
|
|
96
|
+
// Clear pending timeouts since we're clearing the tokens they reference
|
|
97
|
+
for (const timeout of pendingTimeouts) {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
}
|
|
100
|
+
pendingTimeouts.clear();
|
|
101
|
+
revokedTokens.clear();
|
|
102
|
+
usedRefreshTokens.clear();
|
|
103
|
+
},
|
|
104
|
+
destroy() {
|
|
105
|
+
clearInterval(cleanupInterval);
|
|
106
|
+
// Clear all pending timeouts to prevent memory leaks
|
|
107
|
+
for (const timeout of pendingTimeouts) {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
}
|
|
110
|
+
pendingTimeouts.clear();
|
|
111
|
+
revokedTokens.clear();
|
|
112
|
+
usedRefreshTokens.clear();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Default allowed roles for role parsing
|
|
118
|
+
*/
|
|
119
|
+
export const DEFAULT_ALLOWED_ROLES = ['user', 'admin', 'moderator', 'editor'];
|
|
120
|
+
/**
|
|
121
|
+
* Parses JSON-encoded roles string to array
|
|
122
|
+
*
|
|
123
|
+
* Safely parses a JSON array of role strings, filtering to only allowed roles.
|
|
124
|
+
* Returns default ['user'] role if parsing fails or no valid roles found.
|
|
125
|
+
*
|
|
126
|
+
* @param rolesJson - JSON string of roles (e.g., '["admin", "user"]')
|
|
127
|
+
* @param allowedRoles - Optional list of valid roles (defaults to DEFAULT_ALLOWED_ROLES)
|
|
128
|
+
* @returns Array of valid roles, defaults to ['user'] if parsing fails
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* import { parseUserRoles } from '@veloxts/auth';
|
|
133
|
+
*
|
|
134
|
+
* const roles = parseUserRoles(user.roles);
|
|
135
|
+
* // Input: '["admin", "user"]' -> Output: ['admin', 'user']
|
|
136
|
+
* // Input: null -> Output: ['user']
|
|
137
|
+
* // Input: 'invalid' -> Output: ['user']
|
|
138
|
+
*
|
|
139
|
+
* // With custom allowed roles
|
|
140
|
+
* const roles = parseUserRoles(user.roles, ['admin', 'superadmin']);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export function parseUserRoles(rolesJson, allowedRoles = DEFAULT_ALLOWED_ROLES) {
|
|
144
|
+
if (!rolesJson)
|
|
145
|
+
return ['user'];
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(rolesJson);
|
|
148
|
+
if (!Array.isArray(parsed)) {
|
|
149
|
+
return ['user'];
|
|
150
|
+
}
|
|
151
|
+
const validRoles = parsed
|
|
152
|
+
.filter((role) => typeof role === 'string')
|
|
153
|
+
.filter((role) => allowedRoles.includes(role));
|
|
154
|
+
return validRoles.length > 0 ? validRoles : ['user'];
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return ['user'];
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veloxts/auth",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.69",
|
|
4
4
|
"description": "Authentication and authorization system for VeloxTS framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@fastify/cookie": "11.0.2",
|
|
59
59
|
"fastify": "5.6.2",
|
|
60
|
-
"@veloxts/
|
|
61
|
-
"@veloxts/
|
|
60
|
+
"@veloxts/core": "0.6.69",
|
|
61
|
+
"@veloxts/router": "0.6.69"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"argon2": ">=0.30.0",
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
"fastify-plugin": "5.1.0",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
84
|
"vitest": "4.0.16",
|
|
85
|
-
"@veloxts/testing": "0.6.
|
|
86
|
-
"@veloxts/validation": "0.6.
|
|
85
|
+
"@veloxts/testing": "0.6.69",
|
|
86
|
+
"@veloxts/validation": "0.6.69"
|
|
87
87
|
},
|
|
88
88
|
"keywords": [
|
|
89
89
|
"velox",
|