create-velox-app 0.6.68 → 0.6.70
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 +79 -0
- package/package.json +1 -1
- package/src/templates/source/api/config/auth.ts +5 -152
- package/src/templates/source/api/procedures/auth.ts +1 -17
- package/src/templates/source/api/router.auth.ts +7 -10
- package/src/templates/source/api/router.default.ts +3 -9
- package/src/templates/source/api/router.trpc.ts +4 -7
- package/src/templates/source/api/utils/auth.ts +26 -82
- package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +1 -17
- package/src/templates/source/rsc-auth/src/api/utils/auth.ts +26 -82
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# create-velox-app
|
|
2
2
|
|
|
3
|
+
## 0.6.70
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ### feat(auth): Unified Adapter-Only Architecture
|
|
8
|
+
|
|
9
|
+
**New Features:**
|
|
10
|
+
|
|
11
|
+
- Add `JwtAdapter` implementing the `AuthAdapter` interface for unified JWT authentication
|
|
12
|
+
- Add `jwtAuth()` convenience function for direct adapter usage with optional built-in routes (`/api/auth/refresh`, `/api/auth/logout`)
|
|
13
|
+
- Add `AuthContext` discriminated union (`NativeAuthContext | AdapterAuthContext`) for type-safe auth mode handling
|
|
14
|
+
- Add double-registration protection to prevent conflicting auth system setups
|
|
15
|
+
- Add shared decoration utilities (`decorateAuth`, `setRequestAuth`, `checkDoubleRegistration`)
|
|
16
|
+
|
|
17
|
+
**Architecture Changes:**
|
|
18
|
+
|
|
19
|
+
- `authPlugin` now uses `JwtAdapter` internally - all authentication flows through the adapter pattern
|
|
20
|
+
- Single code path for authentication (no more dual native/adapter modes)
|
|
21
|
+
- `authContext.authMode` is now always `'adapter'` with `providerId='jwt'` when using `authPlugin`
|
|
22
|
+
|
|
23
|
+
**Breaking Changes:**
|
|
24
|
+
|
|
25
|
+
- Remove deprecated `LegacySessionConfig` interface (use `sessionMiddleware` instead)
|
|
26
|
+
- Remove deprecated `session` field from `AuthConfig`
|
|
27
|
+
- `User` interface no longer has index signature (extend via declaration merging)
|
|
28
|
+
|
|
29
|
+
**Type Safety Improvements:**
|
|
30
|
+
|
|
31
|
+
- `AuthContext` discriminated union enables exhaustive type narrowing based on `authMode`
|
|
32
|
+
- Export `NativeAuthContext` and `AdapterAuthContext` types for explicit typing
|
|
33
|
+
|
|
34
|
+
**Migration:**
|
|
35
|
+
|
|
36
|
+
- Existing `authPlugin` usage remains backward-compatible
|
|
37
|
+
- If checking `authContext.token`, use `authContext.session` instead (token stored in session for adapter mode)
|
|
38
|
+
|
|
39
|
+
## 0.6.69
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- implement user feedback improvements across packages
|
|
44
|
+
|
|
45
|
+
## Summary
|
|
46
|
+
|
|
47
|
+
Addresses 9 user feedback items to improve DX, reduce boilerplate, and eliminate template duplications.
|
|
48
|
+
|
|
49
|
+
### Phase 1: Validation Helpers (`@veloxts/validation`)
|
|
50
|
+
|
|
51
|
+
- Add `prismaDecimal()`, `prismaDecimalNullable()`, `prismaDecimalOptional()` for Prisma Decimal → number conversion
|
|
52
|
+
- Add `dateToIso`, `dateToIsoNullable`, `dateToIsoOptional` aliases for consistency
|
|
53
|
+
|
|
54
|
+
### Phase 2: Template Deduplication (`@veloxts/auth`)
|
|
55
|
+
|
|
56
|
+
- Export `createEnhancedTokenStore()` with token revocation and refresh token reuse detection
|
|
57
|
+
- Export `parseUserRoles()` and `DEFAULT_ALLOWED_ROLES`
|
|
58
|
+
- Fix memory leak: track pending timeouts for proper cleanup on `destroy()`
|
|
59
|
+
- Update templates to import from `@veloxts/auth` instead of duplicating code
|
|
60
|
+
- Fix jwtManager singleton pattern in templates
|
|
61
|
+
|
|
62
|
+
### Phase 3: Router Helpers (`@veloxts/router`)
|
|
63
|
+
|
|
64
|
+
- Add `createRouter()` returning `{ collections, router }` for DRY setup
|
|
65
|
+
- Add `toRouter()` for router-only use cases
|
|
66
|
+
- Update all router templates to use `createRouter()`
|
|
67
|
+
|
|
68
|
+
### Phase 4: Guard Type Narrowing - Experimental (`@veloxts/auth`, `@veloxts/router`)
|
|
69
|
+
|
|
70
|
+
- Add `NarrowingGuard` interface with phantom `_narrows` type
|
|
71
|
+
- Add `authenticatedNarrow` and `hasRoleNarrow()` guards
|
|
72
|
+
- Add `guardNarrow()` method to `ProcedureBuilder` for context narrowing
|
|
73
|
+
- Enables `ctx.user` to be non-null after guard passes
|
|
74
|
+
|
|
75
|
+
### Phase 5: Documentation (`@veloxts/router`)
|
|
76
|
+
|
|
77
|
+
- Document `.rest()` override patterns
|
|
78
|
+
- Document `createRouter()` helper usage
|
|
79
|
+
- Document `guardNarrow()` experimental API
|
|
80
|
+
- Add schema browser-safety patterns for RSC apps
|
|
81
|
+
|
|
3
82
|
## 0.6.68
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -9,159 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { AuthPluginOptions } from '@veloxts/velox';
|
|
11
11
|
|
|
12
|
+
import { getJwtSecrets, parseUserRoles, tokenStore } from '../utils/auth.js';
|
|
12
13
|
import { db } from './database.js';
|
|
13
14
|
|
|
14
|
-
// ============================================================================
|
|
15
|
-
// Environment Variable Validation
|
|
16
|
-
// ============================================================================
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Gets required JWT secrets from environment variables.
|
|
20
|
-
* Throws a clear error in production if secrets are not configured.
|
|
21
|
-
*/
|
|
22
|
-
function getRequiredSecrets(): { jwtSecret: string; refreshSecret: string } {
|
|
23
|
-
const jwtSecret = process.env.JWT_SECRET;
|
|
24
|
-
const refreshSecret = process.env.JWT_REFRESH_SECRET;
|
|
25
|
-
|
|
26
|
-
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
27
|
-
|
|
28
|
-
if (!jwtSecret || !refreshSecret) {
|
|
29
|
-
if (isDevelopment) {
|
|
30
|
-
console.warn(
|
|
31
|
-
'\n' +
|
|
32
|
-
'='.repeat(70) +
|
|
33
|
-
'\n' +
|
|
34
|
-
' WARNING: JWT secrets not configured!\n' +
|
|
35
|
-
' Using temporary development secrets. DO NOT USE IN PRODUCTION!\n' +
|
|
36
|
-
'\n' +
|
|
37
|
-
' To configure secrets, add to .env:\n' +
|
|
38
|
-
' JWT_SECRET=<generate with: openssl rand -base64 64>\n' +
|
|
39
|
-
' JWT_REFRESH_SECRET=<generate with: openssl rand -base64 64>\n' +
|
|
40
|
-
'='.repeat(70) +
|
|
41
|
-
'\n'
|
|
42
|
-
);
|
|
43
|
-
return {
|
|
44
|
-
jwtSecret:
|
|
45
|
-
jwtSecret || `dev-only-jwt-secret-${Math.random().toString(36).substring(2).repeat(4)}`,
|
|
46
|
-
refreshSecret:
|
|
47
|
-
refreshSecret ||
|
|
48
|
-
`dev-only-refresh-secret-${Math.random().toString(36).substring(2).repeat(4)}`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
throw new Error(
|
|
53
|
-
'\n' +
|
|
54
|
-
'CRITICAL: JWT secrets are required but not configured.\n' +
|
|
55
|
-
'\n' +
|
|
56
|
-
'Required environment variables:\n' +
|
|
57
|
-
' - JWT_SECRET: Secret for signing access tokens (64+ characters)\n' +
|
|
58
|
-
' - JWT_REFRESH_SECRET: Secret for signing refresh tokens (64+ characters)\n' +
|
|
59
|
-
'\n' +
|
|
60
|
-
'Generate secure secrets with:\n' +
|
|
61
|
-
' openssl rand -base64 64\n' +
|
|
62
|
-
'\n' +
|
|
63
|
-
'Add them to your environment or .env file before starting the server.\n'
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return { jwtSecret, refreshSecret };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ============================================================================
|
|
71
|
-
// Token Revocation Store
|
|
72
|
-
// ============================================================================
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* In-memory token revocation store.
|
|
76
|
-
*
|
|
77
|
-
* PRODUCTION NOTE: Replace with Redis or database-backed store for:
|
|
78
|
-
* - Persistence across server restarts
|
|
79
|
-
* - Horizontal scaling (multiple server instances)
|
|
80
|
-
*/
|
|
81
|
-
class InMemoryTokenStore {
|
|
82
|
-
private revokedTokens: Map<string, number> = new Map();
|
|
83
|
-
private usedRefreshTokens: Map<string, string> = new Map();
|
|
84
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
85
|
-
|
|
86
|
-
constructor() {
|
|
87
|
-
this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
revoke(jti: string, expiresInMs: number = 7 * 24 * 60 * 60 * 1000): void {
|
|
91
|
-
this.revokedTokens.set(jti, Date.now() + expiresInMs);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
isRevoked(jti: string): boolean {
|
|
95
|
-
const expiry = this.revokedTokens.get(jti);
|
|
96
|
-
if (!expiry) return false;
|
|
97
|
-
if (Date.now() > expiry) {
|
|
98
|
-
this.revokedTokens.delete(jti);
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
markRefreshTokenUsed(jti: string, userId: string): void {
|
|
105
|
-
this.usedRefreshTokens.set(jti, userId);
|
|
106
|
-
setTimeout(() => this.usedRefreshTokens.delete(jti), 7 * 24 * 60 * 60 * 1000);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
isRefreshTokenUsed(jti: string): string | undefined {
|
|
110
|
-
return this.usedRefreshTokens.get(jti);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
revokeAllUserTokens(userId: string): void {
|
|
114
|
-
console.warn(
|
|
115
|
-
`[Security] Token reuse detected for user ${userId}. ` +
|
|
116
|
-
'All tokens should be revoked. Implement proper user->token mapping for production.'
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private cleanup(): void {
|
|
121
|
-
const now = Date.now();
|
|
122
|
-
for (const [jti, expiry] of this.revokedTokens.entries()) {
|
|
123
|
-
if (now > expiry) {
|
|
124
|
-
this.revokedTokens.delete(jti);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
destroy(): void {
|
|
130
|
-
if (this.cleanupInterval) {
|
|
131
|
-
clearInterval(this.cleanupInterval);
|
|
132
|
-
this.cleanupInterval = null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export const tokenStore = new InMemoryTokenStore();
|
|
138
|
-
|
|
139
|
-
// ============================================================================
|
|
140
|
-
// Role Parsing
|
|
141
|
-
// ============================================================================
|
|
142
|
-
|
|
143
|
-
const ALLOWED_ROLES = ['user', 'admin', 'moderator', 'editor'] as const;
|
|
144
|
-
|
|
145
|
-
export function parseUserRoles(rolesJson: string | null): string[] {
|
|
146
|
-
if (!rolesJson) return ['user'];
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const parsed: unknown = JSON.parse(rolesJson);
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(parsed)) {
|
|
152
|
-
return ['user'];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const validRoles = parsed
|
|
156
|
-
.filter((role): role is string => typeof role === 'string')
|
|
157
|
-
.filter((role) => ALLOWED_ROLES.includes(role as (typeof ALLOWED_ROLES)[number]));
|
|
158
|
-
|
|
159
|
-
return validRoles.length > 0 ? validRoles : ['user'];
|
|
160
|
-
} catch {
|
|
161
|
-
return ['user'];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
15
|
// ============================================================================
|
|
166
16
|
// User Loader
|
|
167
17
|
// ============================================================================
|
|
@@ -186,7 +36,7 @@ async function userLoader(userId: string) {
|
|
|
186
36
|
// ============================================================================
|
|
187
37
|
|
|
188
38
|
export function createAuthConfig(): AuthPluginOptions {
|
|
189
|
-
const { jwtSecret, refreshSecret } =
|
|
39
|
+
const { jwtSecret, refreshSecret } = getJwtSecrets();
|
|
190
40
|
|
|
191
41
|
return {
|
|
192
42
|
jwt: {
|
|
@@ -207,3 +57,6 @@ export function createAuthConfig(): AuthPluginOptions {
|
|
|
207
57
|
}
|
|
208
58
|
|
|
209
59
|
export const authConfig = createAuthConfig();
|
|
60
|
+
|
|
61
|
+
// Re-export for convenience
|
|
62
|
+
export { parseUserRoles, tokenStore } from '../utils/auth.js';
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
authenticated,
|
|
17
17
|
createAuthRateLimiter,
|
|
18
18
|
hashPassword,
|
|
19
|
-
jwtManager,
|
|
20
19
|
procedure,
|
|
21
20
|
procedures,
|
|
22
21
|
verifyPassword,
|
|
@@ -30,7 +29,7 @@ import {
|
|
|
30
29
|
TokenResponse,
|
|
31
30
|
UserResponse,
|
|
32
31
|
} from '../schemas/auth.js';
|
|
33
|
-
import {
|
|
32
|
+
import { jwt, parseUserRoles, tokenStore } from '../utils/auth.js';
|
|
34
33
|
|
|
35
34
|
// ============================================================================
|
|
36
35
|
// Rate Limiter
|
|
@@ -82,21 +81,6 @@ const EnhancedRegisterInput = RegisterInput.extend({
|
|
|
82
81
|
),
|
|
83
82
|
});
|
|
84
83
|
|
|
85
|
-
// ============================================================================
|
|
86
|
-
// JWT Manager
|
|
87
|
-
// ============================================================================
|
|
88
|
-
|
|
89
|
-
const { jwtSecret, refreshSecret } = getJwtSecrets();
|
|
90
|
-
|
|
91
|
-
const jwt = jwtManager({
|
|
92
|
-
secret: jwtSecret,
|
|
93
|
-
refreshSecret: refreshSecret,
|
|
94
|
-
accessTokenExpiry: '15m',
|
|
95
|
-
refreshTokenExpiry: '7d',
|
|
96
|
-
issuer: 'velox-app',
|
|
97
|
-
audience: 'velox-app-client',
|
|
98
|
-
});
|
|
99
|
-
|
|
100
84
|
// Dummy hash for timing attack prevention
|
|
101
85
|
const DUMMY_HASH = '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.uy7dPSSXB5G6Uy';
|
|
102
86
|
|
|
@@ -9,21 +9,18 @@
|
|
|
9
9
|
* The server entry point (index.ts) handles environment setup.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { extractRoutes } from '@veloxts/velox';
|
|
12
|
+
import { createRouter, extractRoutes } from '@veloxts/velox';
|
|
13
13
|
|
|
14
14
|
import { authProcedures } from './procedures/auth.js';
|
|
15
15
|
import { healthProcedures } from './procedures/health.js';
|
|
16
16
|
import { userProcedures } from './procedures/users.js';
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
export const collections
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
health: healthProcedures,
|
|
25
|
-
users: userProcedures,
|
|
26
|
-
};
|
|
18
|
+
// Create router and collections from procedure definitions
|
|
19
|
+
export const { collections, router } = createRouter(
|
|
20
|
+
healthProcedures,
|
|
21
|
+
authProcedures,
|
|
22
|
+
userProcedures
|
|
23
|
+
);
|
|
27
24
|
|
|
28
25
|
export type AppRouter = typeof router;
|
|
29
26
|
|
|
@@ -9,19 +9,13 @@
|
|
|
9
9
|
* The server entry point (index.ts) handles environment setup.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { extractRoutes } from '@veloxts/velox';
|
|
12
|
+
import { createRouter, extractRoutes } from '@veloxts/velox';
|
|
13
13
|
|
|
14
14
|
import { healthProcedures } from './procedures/health.js';
|
|
15
15
|
import { userProcedures } from './procedures/users.js';
|
|
16
16
|
|
|
17
|
-
//
|
|
18
|
-
export const collections =
|
|
19
|
-
|
|
20
|
-
// Router definition for frontend type safety
|
|
21
|
-
export const router = {
|
|
22
|
-
health: healthProcedures,
|
|
23
|
-
users: userProcedures,
|
|
24
|
-
};
|
|
17
|
+
// Create router and collections from procedure definitions
|
|
18
|
+
export const { collections, router } = createRouter(healthProcedures, userProcedures);
|
|
25
19
|
|
|
26
20
|
export type AppRouter = typeof router;
|
|
27
21
|
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
* The server entry point (index.ts) handles environment setup.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { createRouter } from '@veloxts/velox';
|
|
13
|
+
|
|
12
14
|
import { healthProcedures } from './procedures/health.js';
|
|
13
15
|
import { userProcedures } from './procedures/users.js';
|
|
14
16
|
|
|
15
|
-
//
|
|
16
|
-
export const collections =
|
|
17
|
+
// Create router and collections from procedure definitions
|
|
18
|
+
export const { collections, router } = createRouter(healthProcedures, userProcedures);
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* AppRouter type for frontend type safety
|
|
@@ -29,9 +31,4 @@ export const collections = [healthProcedures, userProcedures];
|
|
|
29
31
|
* export const api = createVeloxHooks<AppRouter>();
|
|
30
32
|
* ```
|
|
31
33
|
*/
|
|
32
|
-
export const router = {
|
|
33
|
-
health: healthProcedures,
|
|
34
|
-
users: userProcedures,
|
|
35
|
-
};
|
|
36
|
-
|
|
37
34
|
export type AppRouter = typeof router;
|
|
@@ -5,34 +5,13 @@
|
|
|
5
5
|
* These are safe to import from procedures without pulling in server-only code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
// Role Parsing
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
const ALLOWED_ROLES = ['user', 'admin', 'moderator', 'editor'] as const;
|
|
13
|
-
|
|
14
|
-
export function parseUserRoles(rolesJson: string | null): string[] {
|
|
15
|
-
if (!rolesJson) return ['user'];
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const parsed: unknown = JSON.parse(rolesJson);
|
|
8
|
+
import { createEnhancedTokenStore, jwtManager } from '@veloxts/auth';
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const validRoles = parsed
|
|
25
|
-
.filter((role): role is string => typeof role === 'string')
|
|
26
|
-
.filter((role) => ALLOWED_ROLES.includes(role as (typeof ALLOWED_ROLES)[number]));
|
|
27
|
-
|
|
28
|
-
return validRoles.length > 0 ? validRoles : ['user'];
|
|
29
|
-
} catch {
|
|
30
|
-
return ['user'];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
10
|
+
// Re-export from @veloxts/auth for convenience
|
|
11
|
+
export { createEnhancedTokenStore, parseUserRoles } from '@veloxts/auth';
|
|
33
12
|
|
|
34
13
|
// ============================================================================
|
|
35
|
-
// Token
|
|
14
|
+
// Token Store
|
|
36
15
|
// ============================================================================
|
|
37
16
|
|
|
38
17
|
/**
|
|
@@ -42,63 +21,7 @@ export function parseUserRoles(rolesJson: string | null): string[] {
|
|
|
42
21
|
* - Persistence across server restarts
|
|
43
22
|
* - Horizontal scaling (multiple server instances)
|
|
44
23
|
*/
|
|
45
|
-
|
|
46
|
-
private revokedTokens: Map<string, number> = new Map();
|
|
47
|
-
private usedRefreshTokens: Map<string, string> = new Map();
|
|
48
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
49
|
-
|
|
50
|
-
constructor() {
|
|
51
|
-
this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
revoke(jti: string, expiresInMs: number = 7 * 24 * 60 * 60 * 1000): void {
|
|
55
|
-
this.revokedTokens.set(jti, Date.now() + expiresInMs);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
isRevoked(jti: string): boolean {
|
|
59
|
-
const expiry = this.revokedTokens.get(jti);
|
|
60
|
-
if (!expiry) return false;
|
|
61
|
-
if (Date.now() > expiry) {
|
|
62
|
-
this.revokedTokens.delete(jti);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
markRefreshTokenUsed(jti: string, userId: string): void {
|
|
69
|
-
this.usedRefreshTokens.set(jti, userId);
|
|
70
|
-
setTimeout(() => this.usedRefreshTokens.delete(jti), 7 * 24 * 60 * 60 * 1000);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
isRefreshTokenUsed(jti: string): string | undefined {
|
|
74
|
-
return this.usedRefreshTokens.get(jti);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
revokeAllUserTokens(userId: string): void {
|
|
78
|
-
console.warn(
|
|
79
|
-
`[Security] Token reuse detected for user ${userId}. ` +
|
|
80
|
-
'All tokens should be revoked. Implement proper user->token mapping for production.'
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private cleanup(): void {
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
for (const [jti, expiry] of this.revokedTokens.entries()) {
|
|
87
|
-
if (now > expiry) {
|
|
88
|
-
this.revokedTokens.delete(jti);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
destroy(): void {
|
|
94
|
-
if (this.cleanupInterval) {
|
|
95
|
-
clearInterval(this.cleanupInterval);
|
|
96
|
-
this.cleanupInterval = null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export const tokenStore = new InMemoryTokenStore();
|
|
24
|
+
export const tokenStore = createEnhancedTokenStore();
|
|
102
25
|
|
|
103
26
|
// ============================================================================
|
|
104
27
|
// JWT Configuration Helper
|
|
@@ -155,3 +78,24 @@ export function getJwtSecrets(): { jwtSecret: string; refreshSecret: string } {
|
|
|
155
78
|
|
|
156
79
|
return { jwtSecret, refreshSecret };
|
|
157
80
|
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// JWT Manager Singleton
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
const { jwtSecret, refreshSecret } = getJwtSecrets();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shared JWT manager instance for the application.
|
|
90
|
+
*
|
|
91
|
+
* Use this singleton instead of creating new jwtManager instances.
|
|
92
|
+
* Configured with environment variables via getJwtSecrets().
|
|
93
|
+
*/
|
|
94
|
+
export const jwt = jwtManager({
|
|
95
|
+
secret: jwtSecret,
|
|
96
|
+
refreshSecret: refreshSecret,
|
|
97
|
+
accessTokenExpiry: '15m',
|
|
98
|
+
refreshTokenExpiry: '7d',
|
|
99
|
+
issuer: 'velox-app',
|
|
100
|
+
audience: 'velox-app-client',
|
|
101
|
+
});
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
authenticated,
|
|
17
17
|
createAuthRateLimiter,
|
|
18
18
|
hashPassword,
|
|
19
|
-
jwtManager,
|
|
20
19
|
verifyPassword,
|
|
21
20
|
} from '@veloxts/auth';
|
|
22
21
|
import { procedure, procedures } from '@veloxts/router';
|
|
@@ -29,7 +28,7 @@ import {
|
|
|
29
28
|
TokenResponse,
|
|
30
29
|
UserResponse,
|
|
31
30
|
} from '../schemas/auth.js';
|
|
32
|
-
import {
|
|
31
|
+
import { jwt, parseUserRoles, tokenStore } from '../utils/auth.js';
|
|
33
32
|
|
|
34
33
|
// ============================================================================
|
|
35
34
|
// Rate Limiter
|
|
@@ -81,21 +80,6 @@ const EnhancedRegisterInput = RegisterInput.extend({
|
|
|
81
80
|
),
|
|
82
81
|
});
|
|
83
82
|
|
|
84
|
-
// ============================================================================
|
|
85
|
-
// JWT Manager
|
|
86
|
-
// ============================================================================
|
|
87
|
-
|
|
88
|
-
const { jwtSecret, refreshSecret } = getJwtSecrets();
|
|
89
|
-
|
|
90
|
-
const jwt = jwtManager({
|
|
91
|
-
secret: jwtSecret,
|
|
92
|
-
refreshSecret: refreshSecret,
|
|
93
|
-
accessTokenExpiry: '15m',
|
|
94
|
-
refreshTokenExpiry: '7d',
|
|
95
|
-
issuer: 'velox-app',
|
|
96
|
-
audience: 'velox-app-client',
|
|
97
|
-
});
|
|
98
|
-
|
|
99
83
|
// Dummy hash for timing attack prevention
|
|
100
84
|
const DUMMY_HASH = '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.uy7dPSSXB5G6Uy';
|
|
101
85
|
|
|
@@ -5,34 +5,13 @@
|
|
|
5
5
|
* These are safe to import from procedures without pulling in server-only code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
// Role Parsing
|
|
10
|
-
// ============================================================================
|
|
11
|
-
|
|
12
|
-
const ALLOWED_ROLES = ['user', 'admin', 'moderator', 'editor'] as const;
|
|
13
|
-
|
|
14
|
-
export function parseUserRoles(rolesJson: string | null): string[] {
|
|
15
|
-
if (!rolesJson) return ['user'];
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const parsed: unknown = JSON.parse(rolesJson);
|
|
8
|
+
import { createEnhancedTokenStore, jwtManager } from '@veloxts/auth';
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const validRoles = parsed
|
|
25
|
-
.filter((role): role is string => typeof role === 'string')
|
|
26
|
-
.filter((role) => ALLOWED_ROLES.includes(role as (typeof ALLOWED_ROLES)[number]));
|
|
27
|
-
|
|
28
|
-
return validRoles.length > 0 ? validRoles : ['user'];
|
|
29
|
-
} catch {
|
|
30
|
-
return ['user'];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
10
|
+
// Re-export from @veloxts/auth for convenience
|
|
11
|
+
export { createEnhancedTokenStore, parseUserRoles } from '@veloxts/auth';
|
|
33
12
|
|
|
34
13
|
// ============================================================================
|
|
35
|
-
// Token
|
|
14
|
+
// Token Store
|
|
36
15
|
// ============================================================================
|
|
37
16
|
|
|
38
17
|
/**
|
|
@@ -42,63 +21,7 @@ export function parseUserRoles(rolesJson: string | null): string[] {
|
|
|
42
21
|
* - Persistence across server restarts
|
|
43
22
|
* - Horizontal scaling (multiple server instances)
|
|
44
23
|
*/
|
|
45
|
-
|
|
46
|
-
private revokedTokens: Map<string, number> = new Map();
|
|
47
|
-
private usedRefreshTokens: Map<string, string> = new Map();
|
|
48
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
49
|
-
|
|
50
|
-
constructor() {
|
|
51
|
-
this.cleanupInterval = setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
revoke(jti: string, expiresInMs: number = 7 * 24 * 60 * 60 * 1000): void {
|
|
55
|
-
this.revokedTokens.set(jti, Date.now() + expiresInMs);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
isRevoked(jti: string): boolean {
|
|
59
|
-
const expiry = this.revokedTokens.get(jti);
|
|
60
|
-
if (!expiry) return false;
|
|
61
|
-
if (Date.now() > expiry) {
|
|
62
|
-
this.revokedTokens.delete(jti);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
markRefreshTokenUsed(jti: string, userId: string): void {
|
|
69
|
-
this.usedRefreshTokens.set(jti, userId);
|
|
70
|
-
setTimeout(() => this.usedRefreshTokens.delete(jti), 7 * 24 * 60 * 60 * 1000);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
isRefreshTokenUsed(jti: string): string | undefined {
|
|
74
|
-
return this.usedRefreshTokens.get(jti);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
revokeAllUserTokens(userId: string): void {
|
|
78
|
-
console.warn(
|
|
79
|
-
`[Security] Token reuse detected for user ${userId}. ` +
|
|
80
|
-
'All tokens should be revoked. Implement proper user->token mapping for production.'
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private cleanup(): void {
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
for (const [jti, expiry] of this.revokedTokens.entries()) {
|
|
87
|
-
if (now > expiry) {
|
|
88
|
-
this.revokedTokens.delete(jti);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
destroy(): void {
|
|
94
|
-
if (this.cleanupInterval) {
|
|
95
|
-
clearInterval(this.cleanupInterval);
|
|
96
|
-
this.cleanupInterval = null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export const tokenStore = new InMemoryTokenStore();
|
|
24
|
+
export const tokenStore = createEnhancedTokenStore();
|
|
102
25
|
|
|
103
26
|
// ============================================================================
|
|
104
27
|
// JWT Configuration Helper
|
|
@@ -155,3 +78,24 @@ export function getJwtSecrets(): { jwtSecret: string; refreshSecret: string } {
|
|
|
155
78
|
|
|
156
79
|
return { jwtSecret, refreshSecret };
|
|
157
80
|
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// JWT Manager Singleton
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
const { jwtSecret, refreshSecret } = getJwtSecrets();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Shared JWT manager instance for the application.
|
|
90
|
+
*
|
|
91
|
+
* Use this singleton instead of creating new jwtManager instances.
|
|
92
|
+
* Configured with environment variables via getJwtSecrets().
|
|
93
|
+
*/
|
|
94
|
+
export const jwt = jwtManager({
|
|
95
|
+
secret: jwtSecret,
|
|
96
|
+
refreshSecret: refreshSecret,
|
|
97
|
+
accessTokenExpiry: '15m',
|
|
98
|
+
refreshTokenExpiry: '7d',
|
|
99
|
+
issuer: 'velox-app',
|
|
100
|
+
audience: 'velox-app-client',
|
|
101
|
+
});
|