create-velox-app 0.6.31 → 0.6.51

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/GUIDE.md +230 -0
  3. package/dist/cli.js +1 -0
  4. package/dist/index.js +14 -4
  5. package/dist/templates/auth.js +10 -0
  6. package/dist/templates/index.js +30 -1
  7. package/dist/templates/placeholders.js +0 -3
  8. package/dist/templates/rsc-auth.d.ts +12 -0
  9. package/dist/templates/rsc-auth.js +208 -0
  10. package/dist/templates/rsc.js +40 -1
  11. package/dist/templates/shared/css-generator.d.ts +26 -0
  12. package/dist/templates/shared/css-generator.js +553 -0
  13. package/dist/templates/shared/index.d.ts +3 -0
  14. package/dist/templates/shared/index.js +3 -0
  15. package/dist/templates/shared/rsc-styles.d.ts +54 -0
  16. package/dist/templates/shared/rsc-styles.js +68 -0
  17. package/dist/templates/shared/theme.d.ts +133 -0
  18. package/dist/templates/shared/theme.js +141 -0
  19. package/dist/templates/spa.js +10 -0
  20. package/dist/templates/trpc.js +10 -0
  21. package/dist/templates/types.d.ts +2 -1
  22. package/dist/templates/types.js +6 -0
  23. package/package.json +6 -3
  24. package/src/templates/source/api/config/database.ts +13 -32
  25. package/src/templates/source/api/docker-compose.yml +21 -0
  26. package/src/templates/source/root/CLAUDE.auth.md +6 -0
  27. package/src/templates/source/root/CLAUDE.default.md +6 -0
  28. package/src/templates/source/rsc/CLAUDE.md +56 -2
  29. package/src/templates/source/rsc/app/actions/posts.ts +1 -1
  30. package/src/templates/source/rsc/app/actions/users.ts +111 -20
  31. package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
  32. package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
  33. package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
  34. package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
  35. package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
  36. package/src/templates/source/rsc/docker-compose.yml +21 -0
  37. package/src/templates/source/rsc/package.json +3 -3
  38. package/src/templates/source/rsc/src/api/database.ts +13 -32
  39. package/src/templates/source/rsc/src/api/handler.ts +1 -1
  40. package/src/templates/source/rsc/src/entry.client.tsx +65 -18
  41. package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
  42. package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
  43. package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
  44. package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
  45. package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
  46. package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
  47. package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
  48. package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
  49. package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
  50. package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
  51. package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
  52. package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
  53. package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
  54. package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
  55. package/src/templates/source/rsc-auth/app.config.ts +12 -0
  56. package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
  57. package/src/templates/source/rsc-auth/env.example +11 -0
  58. package/src/templates/source/rsc-auth/gitignore +34 -0
  59. package/src/templates/source/rsc-auth/package.json +44 -0
  60. package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
  61. package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
  62. package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
  63. package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
  64. package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
  65. package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
  66. package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
  67. package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
  68. package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
  69. package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
  70. package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
  71. package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
  72. package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
  73. package/src/templates/source/rsc-auth/tsconfig.json +24 -0
  74. package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Auth Procedures
3
+ *
4
+ * Authentication procedures for user registration, login, and token management.
5
+ *
6
+ * REST Endpoints:
7
+ * - POST /auth/register - Create new account
8
+ * - POST /auth/login - Authenticate and get tokens
9
+ * - POST /auth/refresh - Refresh access token
10
+ * - POST /auth/logout - Revoke current token
11
+ * - GET /auth/me - Get current user (protected)
12
+ */
13
+
14
+ import {
15
+ AuthError,
16
+ authenticated,
17
+ createAuthRateLimiter,
18
+ hashPassword,
19
+ jwtManager,
20
+ verifyPassword,
21
+ } from '@veloxts/auth';
22
+ import { procedure, procedures } from '@veloxts/router';
23
+
24
+ import {
25
+ LoginInput,
26
+ LogoutResponse,
27
+ RefreshInput,
28
+ RegisterInput,
29
+ TokenResponse,
30
+ UserResponse,
31
+ } from '../schemas/auth.js';
32
+ import { getJwtSecrets, parseUserRoles, tokenStore } from '../utils/auth.js';
33
+
34
+ // ============================================================================
35
+ // Rate Limiter
36
+ // ============================================================================
37
+
38
+ const rateLimiter = createAuthRateLimiter({
39
+ login: {
40
+ maxAttempts: 5,
41
+ windowMs: 15 * 60 * 1000,
42
+ lockoutDurationMs: 15 * 60 * 1000,
43
+ progressiveBackoff: true,
44
+ },
45
+ register: {
46
+ maxAttempts: 3,
47
+ windowMs: 60 * 60 * 1000,
48
+ lockoutDurationMs: 60 * 60 * 1000,
49
+ },
50
+ refresh: {
51
+ maxAttempts: 10,
52
+ windowMs: 60 * 1000,
53
+ lockoutDurationMs: 60 * 1000,
54
+ },
55
+ });
56
+
57
+ // ============================================================================
58
+ // Password Blacklist (runtime-only, not in type chain)
59
+ // ============================================================================
60
+
61
+ const COMMON_PASSWORDS = new Set([
62
+ 'password',
63
+ 'password123',
64
+ '12345678',
65
+ '123456789',
66
+ 'qwerty123',
67
+ 'letmein',
68
+ 'welcome',
69
+ 'admin123',
70
+ ]);
71
+
72
+ // Enhanced password validation with common password check
73
+ const EnhancedRegisterInput = RegisterInput.extend({
74
+ password: RegisterInput.shape.password
75
+ .refine((pwd) => /[a-z]/.test(pwd), 'Password must contain at least one lowercase letter')
76
+ .refine((pwd) => /[A-Z]/.test(pwd), 'Password must contain at least one uppercase letter')
77
+ .refine((pwd) => /[0-9]/.test(pwd), 'Password must contain at least one number')
78
+ .refine(
79
+ (pwd) => !COMMON_PASSWORDS.has(pwd.toLowerCase()),
80
+ 'Password is too common. Please choose a stronger password.'
81
+ ),
82
+ });
83
+
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
+ // Dummy hash for timing attack prevention
100
+ const DUMMY_HASH = '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.uy7dPSSXB5G6Uy';
101
+
102
+ // ============================================================================
103
+ // Auth Procedures
104
+ // ============================================================================
105
+
106
+ export const authProcedures = procedures('auth', {
107
+ createAccount: procedure()
108
+ .rest({ method: 'POST', path: '/auth/register' })
109
+ .use(rateLimiter.register())
110
+ .input(EnhancedRegisterInput)
111
+ .output(TokenResponse)
112
+ .mutation(async ({ input, ctx }) => {
113
+ const normalizedEmail = input.email.toLowerCase().trim();
114
+
115
+ const existing = await ctx.db.user.findUnique({
116
+ where: { email: normalizedEmail },
117
+ });
118
+
119
+ if (existing) {
120
+ throw new AuthError(
121
+ 'Registration failed. If this email is not already registered, please try again.',
122
+ 400,
123
+ 'REGISTRATION_FAILED'
124
+ );
125
+ }
126
+
127
+ const hashedPassword = await hashPassword(input.password);
128
+
129
+ const user = await ctx.db.user.create({
130
+ data: {
131
+ name: input.name.trim(),
132
+ email: normalizedEmail,
133
+ password: hashedPassword,
134
+ roles: JSON.stringify(['user']),
135
+ },
136
+ });
137
+
138
+ return jwt.createTokenPair({
139
+ id: user.id,
140
+ email: user.email,
141
+ roles: ['user'],
142
+ });
143
+ }),
144
+
145
+ createSession: procedure()
146
+ .rest({ method: 'POST', path: '/auth/login' })
147
+ .use(
148
+ rateLimiter.login((ctx) => {
149
+ const input = ctx.input as { email?: string } | undefined;
150
+ return input?.email?.toLowerCase() ?? '';
151
+ })
152
+ )
153
+ .input(LoginInput)
154
+ .output(TokenResponse)
155
+ .mutation(async ({ input, ctx }) => {
156
+ const normalizedEmail = input.email.toLowerCase().trim();
157
+
158
+ const user = await ctx.db.user.findUnique({
159
+ where: { email: normalizedEmail },
160
+ });
161
+
162
+ const hashToVerify = user?.password || DUMMY_HASH;
163
+ const isValid = await verifyPassword(input.password, hashToVerify);
164
+
165
+ if (!user || !user.password || !isValid) {
166
+ throw new AuthError('Invalid email or password', 401, 'INVALID_CREDENTIALS');
167
+ }
168
+
169
+ const roles = parseUserRoles(user.roles);
170
+
171
+ return jwt.createTokenPair({
172
+ id: user.id,
173
+ email: user.email,
174
+ roles,
175
+ });
176
+ }),
177
+
178
+ createRefresh: procedure()
179
+ .rest({ method: 'POST', path: '/auth/refresh' })
180
+ .use(rateLimiter.refresh())
181
+ .input(RefreshInput)
182
+ .output(TokenResponse)
183
+ .mutation(async ({ input, ctx }) => {
184
+ try {
185
+ const payload = jwt.verifyToken(input.refreshToken);
186
+
187
+ if (payload.type !== 'refresh') {
188
+ throw new AuthError('Invalid token type', 401, 'INVALID_TOKEN_TYPE');
189
+ }
190
+
191
+ if (payload.jti && tokenStore.isRevoked(payload.jti)) {
192
+ throw new AuthError('Token has been revoked', 401, 'TOKEN_REVOKED');
193
+ }
194
+
195
+ if (payload.jti) {
196
+ const previousUserId = tokenStore.isRefreshTokenUsed(payload.jti);
197
+ if (previousUserId) {
198
+ tokenStore.revokeAllUserTokens(previousUserId);
199
+ throw new AuthError(
200
+ 'Security alert: Refresh token reuse detected.',
201
+ 401,
202
+ 'TOKEN_REUSE_DETECTED'
203
+ );
204
+ }
205
+ tokenStore.markRefreshTokenUsed(payload.jti, payload.sub);
206
+ }
207
+
208
+ const user = await ctx.db.user.findUnique({
209
+ where: { id: payload.sub },
210
+ });
211
+
212
+ if (!user) {
213
+ throw new AuthError('User not found', 401, 'USER_NOT_FOUND');
214
+ }
215
+
216
+ return jwt.createTokenPair({
217
+ id: user.id,
218
+ email: user.email,
219
+ roles: parseUserRoles(user.roles),
220
+ });
221
+ } catch (error) {
222
+ if (error instanceof AuthError) throw error;
223
+ throw new AuthError('Invalid refresh token', 401, 'INVALID_REFRESH_TOKEN');
224
+ }
225
+ }),
226
+
227
+ deleteSession: procedure()
228
+ .rest({ method: 'POST', path: '/auth/logout' })
229
+ .guard(authenticated)
230
+ .output(LogoutResponse)
231
+ .mutation(async ({ ctx }) => {
232
+ const tokenId = ctx.auth?.token?.jti;
233
+
234
+ if (tokenId) {
235
+ tokenStore.revoke(tokenId, 15 * 60 * 1000);
236
+ }
237
+
238
+ return {
239
+ success: true,
240
+ message: 'Successfully logged out',
241
+ };
242
+ }),
243
+
244
+ getMe: procedure()
245
+ .rest({ method: 'GET', path: '/auth/me' })
246
+ .guard(authenticated)
247
+ .output(UserResponse)
248
+ .query(async ({ ctx }) => {
249
+ const user = ctx.user;
250
+
251
+ if (!user) {
252
+ throw new AuthError('Not authenticated', 401, 'NOT_AUTHENTICATED');
253
+ }
254
+
255
+ return {
256
+ id: user.id,
257
+ name: (user.name as string) || '',
258
+ email: user.email,
259
+ roles: Array.isArray(user.roles) ? user.roles : ['user'],
260
+ };
261
+ }),
262
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Health Check Procedures
3
+ *
4
+ * API endpoints for application health monitoring.
5
+ */
6
+
7
+ import { procedure, procedures } from '@veloxts/router';
8
+
9
+ import { db } from '../database.js';
10
+
11
+ export const healthProcedures = procedures('health', {
12
+ /**
13
+ * Basic health check
14
+ * GET /api/health
15
+ */
16
+ getHealth: procedure()
17
+ .rest({ method: 'GET', path: '/health' })
18
+ .query(() => ({
19
+ status: 'healthy',
20
+ timestamp: new Date().toISOString(),
21
+ uptime: process.uptime(),
22
+ })),
23
+
24
+ /**
25
+ * Readiness check (includes database)
26
+ * GET /api/health/ready
27
+ */
28
+ getReady: procedure()
29
+ .rest({ method: 'GET', path: '/health/ready' })
30
+ .query(async () => {
31
+ try {
32
+ // Test database connection
33
+ await db.$queryRaw`SELECT 1`;
34
+
35
+ return {
36
+ status: 'ready',
37
+ database: 'connected',
38
+ timestamp: new Date().toISOString(),
39
+ };
40
+ } catch {
41
+ return {
42
+ status: 'not_ready',
43
+ database: 'disconnected',
44
+ timestamp: new Date().toISOString(),
45
+ };
46
+ }
47
+ }),
48
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * User Procedures
3
+ *
4
+ * CRUD operations for user management.
5
+ * Uses direct db import for proper PrismaClient typing.
6
+ */
7
+
8
+ import { procedure, procedures } from '@veloxts/router';
9
+ import { z } from 'zod';
10
+
11
+ import { db } from '../database.js';
12
+ import { CreateUserSchema, UpdateUserSchema, UserSchema } from '../schemas/user.js';
13
+
14
+ export const userProcedures = procedures('users', {
15
+ /**
16
+ * List all users
17
+ * GET /api/users
18
+ */
19
+ listUsers: procedure()
20
+ .output(z.array(UserSchema))
21
+ .query(async () => {
22
+ return db.user.findMany({
23
+ orderBy: { createdAt: 'desc' },
24
+ });
25
+ }),
26
+
27
+ /**
28
+ * Get a single user by ID
29
+ * GET /api/users/:id
30
+ */
31
+ getUser: procedure()
32
+ .input(z.object({ id: z.string().uuid() }))
33
+ .output(UserSchema)
34
+ .query(async ({ input }) => {
35
+ const user = await db.user.findUnique({
36
+ where: { id: input.id },
37
+ });
38
+
39
+ if (!user) {
40
+ throw Object.assign(new Error('User not found'), { statusCode: 404 });
41
+ }
42
+
43
+ return user;
44
+ }),
45
+
46
+ /**
47
+ * Create a new user
48
+ * POST /api/users
49
+ */
50
+ createUser: procedure()
51
+ .input(CreateUserSchema)
52
+ .output(UserSchema)
53
+ .mutation(async ({ input }) => {
54
+ return db.user.create({
55
+ data: input,
56
+ });
57
+ }),
58
+
59
+ /**
60
+ * Update an existing user
61
+ * PUT /api/users/:id
62
+ */
63
+ updateUser: procedure()
64
+ .input(UpdateUserSchema.extend({ id: z.string().uuid() }))
65
+ .output(UserSchema)
66
+ .mutation(async ({ input }) => {
67
+ const { id, ...data } = input;
68
+ return db.user.update({
69
+ where: { id },
70
+ data,
71
+ });
72
+ }),
73
+
74
+ /**
75
+ * Delete a user
76
+ * DELETE /api/users/:id
77
+ */
78
+ deleteUser: procedure()
79
+ .input(z.object({ id: z.string().uuid() }))
80
+ .output(z.object({ success: z.boolean() }))
81
+ .mutation(async ({ input }) => {
82
+ await db.user.delete({
83
+ where: { id: input.id },
84
+ });
85
+ return { success: true };
86
+ }),
87
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Auth Schemas
3
+ *
4
+ * BROWSER-SAFE: This file imports ONLY from 'zod'.
5
+ * Never import from @veloxts/* packages here.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+
10
+ // ============================================================================
11
+ // Password Schema (for validation display)
12
+ // ============================================================================
13
+
14
+ export const PasswordSchema = z
15
+ .string()
16
+ .min(12, 'Password must be at least 12 characters')
17
+ .max(128, 'Password must not exceed 128 characters');
18
+
19
+ // ============================================================================
20
+ // Email Schema
21
+ // ============================================================================
22
+
23
+ export const EmailSchema = z
24
+ .string()
25
+ .email('Invalid email address')
26
+ .transform((email) => email.toLowerCase().trim());
27
+
28
+ // ============================================================================
29
+ // Input Schemas
30
+ // ============================================================================
31
+
32
+ export const RegisterInput = z.object({
33
+ name: z.string().min(2).max(100).trim(),
34
+ email: EmailSchema,
35
+ password: PasswordSchema,
36
+ });
37
+
38
+ export const LoginInput = z.object({
39
+ email: EmailSchema,
40
+ password: z.string().min(1),
41
+ });
42
+
43
+ export const RefreshInput = z.object({
44
+ refreshToken: z.string(),
45
+ });
46
+
47
+ // ============================================================================
48
+ // Response Schemas
49
+ // ============================================================================
50
+
51
+ export const TokenResponse = z.object({
52
+ accessToken: z.string(),
53
+ refreshToken: z.string(),
54
+ expiresIn: z.number(),
55
+ tokenType: z.literal('Bearer'),
56
+ });
57
+
58
+ export const UserResponse = z.object({
59
+ id: z.string(),
60
+ name: z.string(),
61
+ email: z.string(),
62
+ roles: z.array(z.string()),
63
+ });
64
+
65
+ export const LogoutResponse = z.object({
66
+ success: z.boolean(),
67
+ message: z.string(),
68
+ });
69
+
70
+ // ============================================================================
71
+ // Type Exports
72
+ // ============================================================================
73
+
74
+ export type RegisterData = z.infer<typeof RegisterInput>;
75
+ export type LoginData = z.infer<typeof LoginInput>;
76
+ export type RefreshData = z.infer<typeof RefreshInput>;
77
+ export type TokenResponseData = z.infer<typeof TokenResponse>;
78
+ export type UserResponseData = z.infer<typeof UserResponse>;
79
+ export type LogoutResponseData = z.infer<typeof LogoutResponse>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * User Schemas
3
+ *
4
+ * Zod schemas for user validation.
5
+ */
6
+
7
+ import { z } from 'zod';
8
+
9
+ /**
10
+ * User entity schema
11
+ */
12
+ export const UserSchema = z.object({
13
+ id: z.string().uuid(),
14
+ name: z.string(),
15
+ email: z.string().email(),
16
+ createdAt: z.date(),
17
+ updatedAt: z.date(),
18
+ });
19
+
20
+ /**
21
+ * Create user input schema
22
+ */
23
+ export const CreateUserSchema = z.object({
24
+ name: z.string().min(1, 'Name is required'),
25
+ email: z.string().email('Invalid email address'),
26
+ });
27
+
28
+ /**
29
+ * Update user input schema (all fields optional)
30
+ */
31
+ export const UpdateUserSchema = z.object({
32
+ name: z.string().min(1).optional(),
33
+ email: z.string().email().optional(),
34
+ });
35
+
36
+ export type User = z.infer<typeof UserSchema>;
37
+ export type CreateUser = z.infer<typeof CreateUserSchema>;
38
+ export type UpdateUser = z.infer<typeof UpdateUserSchema>;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Auth Utilities
3
+ *
4
+ * Shared utilities for authentication that don't require database access.
5
+ * These are safe to import from procedures without pulling in server-only code.
6
+ */
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);
19
+
20
+ if (!Array.isArray(parsed)) {
21
+ return ['user'];
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
+ }
33
+
34
+ // ============================================================================
35
+ // Token Revocation Store
36
+ // ============================================================================
37
+
38
+ /**
39
+ * In-memory token revocation store.
40
+ *
41
+ * PRODUCTION NOTE: Replace with Redis or database-backed store for:
42
+ * - Persistence across server restarts
43
+ * - Horizontal scaling (multiple server instances)
44
+ */
45
+ class InMemoryTokenStore {
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();
102
+
103
+ // ============================================================================
104
+ // JWT Configuration Helper
105
+ // ============================================================================
106
+
107
+ /**
108
+ * Gets required JWT secrets from environment variables.
109
+ * Throws a clear error in production if secrets are not configured.
110
+ */
111
+ export function getJwtSecrets(): { jwtSecret: string; refreshSecret: string } {
112
+ const jwtSecret = process.env.JWT_SECRET;
113
+ const refreshSecret = process.env.JWT_REFRESH_SECRET;
114
+
115
+ const isDevelopment = process.env.NODE_ENV !== 'production';
116
+
117
+ if (!jwtSecret || !refreshSecret) {
118
+ if (isDevelopment) {
119
+ console.warn(
120
+ '\n' +
121
+ '='.repeat(70) +
122
+ '\n' +
123
+ ' WARNING: JWT secrets not configured!\n' +
124
+ ' Using temporary development secrets. DO NOT USE IN PRODUCTION!\n' +
125
+ '\n' +
126
+ ' To configure secrets, add to .env:\n' +
127
+ ' JWT_SECRET=<generate with: openssl rand -base64 64>\n' +
128
+ ' JWT_REFRESH_SECRET=<generate with: openssl rand -base64 64>\n' +
129
+ '='.repeat(70) +
130
+ '\n'
131
+ );
132
+ return {
133
+ jwtSecret:
134
+ jwtSecret || `dev-only-jwt-secret-${Math.random().toString(36).substring(2).repeat(4)}`,
135
+ refreshSecret:
136
+ refreshSecret ||
137
+ `dev-only-refresh-secret-${Math.random().toString(36).substring(2).repeat(4)}`,
138
+ };
139
+ }
140
+
141
+ throw new Error(
142
+ '\n' +
143
+ 'CRITICAL: JWT secrets are required but not configured.\n' +
144
+ '\n' +
145
+ 'Required environment variables:\n' +
146
+ ' - JWT_SECRET: Secret for signing access tokens (64+ characters)\n' +
147
+ ' - JWT_REFRESH_SECRET: Secret for signing refresh tokens (64+ characters)\n' +
148
+ '\n' +
149
+ 'Generate secure secrets with:\n' +
150
+ ' openssl rand -base64 64\n' +
151
+ '\n' +
152
+ 'Add them to your environment or .env file before starting the server.\n'
153
+ );
154
+ }
155
+
156
+ return { jwtSecret, refreshSecret };
157
+ }