@kyro-cms/core 0.1.1 → 0.1.2

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/README.md +196 -109
  2. package/dist/bootstrap-2WJK6PG7.cjs +29 -0
  3. package/dist/bootstrap-2WJK6PG7.cjs.map +1 -0
  4. package/dist/bootstrap-Q2TWUQF3.js +4 -0
  5. package/dist/bootstrap-Q2TWUQF3.js.map +1 -0
  6. package/dist/chunk-3QX6KG2S.js +2125 -0
  7. package/dist/chunk-3QX6KG2S.js.map +1 -0
  8. package/dist/chunk-5AOILNGY.cjs +212 -0
  9. package/dist/chunk-5AOILNGY.cjs.map +1 -0
  10. package/dist/{chunk-DKSMFC3L.js → chunk-EINVJPFM.js} +2 -2
  11. package/dist/{chunk-DKSMFC3L.js.map → chunk-EINVJPFM.js.map} +1 -1
  12. package/dist/chunk-F5B64H5S.cjs +2149 -0
  13. package/dist/chunk-F5B64H5S.cjs.map +1 -0
  14. package/dist/chunk-I4BORBXT.cjs +914 -0
  15. package/dist/chunk-I4BORBXT.cjs.map +1 -0
  16. package/dist/chunk-KA3UOIFC.js +206 -0
  17. package/dist/chunk-KA3UOIFC.js.map +1 -0
  18. package/dist/chunk-KWTKEBHM.cjs +176 -0
  19. package/dist/chunk-KWTKEBHM.cjs.map +1 -0
  20. package/dist/chunk-M4JFHQ5J.js +170 -0
  21. package/dist/chunk-M4JFHQ5J.js.map +1 -0
  22. package/dist/chunk-PZ5AY32C.js +9 -0
  23. package/dist/chunk-PZ5AY32C.js.map +1 -0
  24. package/dist/chunk-Q7SFCCGT.cjs +11 -0
  25. package/dist/chunk-Q7SFCCGT.cjs.map +1 -0
  26. package/dist/chunk-U4CHJTWX.cjs +94 -0
  27. package/dist/chunk-U4CHJTWX.cjs.map +1 -0
  28. package/dist/{chunk-3Q3FS5J4.cjs → chunk-V3B25QOK.cjs} +2 -2
  29. package/dist/{chunk-3Q3FS5J4.cjs.map → chunk-V3B25QOK.cjs.map} +1 -1
  30. package/dist/chunk-V67YXRBT.js +899 -0
  31. package/dist/chunk-V67YXRBT.js.map +1 -0
  32. package/dist/chunk-XLMVCGXA.js +86 -0
  33. package/dist/chunk-XLMVCGXA.js.map +1 -0
  34. package/dist/cli/index.cjs +106 -14
  35. package/dist/cli/index.cjs.map +1 -1
  36. package/dist/cli/index.js +106 -14
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/database-37KXWUER.js +5 -0
  39. package/dist/database-37KXWUER.js.map +1 -0
  40. package/dist/database-LJKD3HE4.cjs +22 -0
  41. package/dist/database-LJKD3HE4.cjs.map +1 -0
  42. package/dist/drizzle/index.cjs +25 -5
  43. package/dist/drizzle/index.d.cts +5 -49
  44. package/dist/drizzle/index.d.ts +5 -49
  45. package/dist/drizzle/index.js +5 -1
  46. package/dist/graphql/index.cjs +1 -0
  47. package/dist/graphql/index.js +1 -0
  48. package/dist/index-BVFlb7uU.d.ts +192 -0
  49. package/dist/index-CzkEHKqu.d.cts +192 -0
  50. package/dist/index.cjs +1203 -23
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +382 -68
  53. package/dist/index.d.ts +382 -68
  54. package/dist/index.js +1110 -20
  55. package/dist/index.js.map +1 -1
  56. package/dist/mongodb/index.cjs +1 -0
  57. package/dist/mongodb/index.js +1 -0
  58. package/dist/postgres-auth-adapter-CYZAVPPP.cjs +14 -0
  59. package/dist/postgres-auth-adapter-CYZAVPPP.cjs.map +1 -0
  60. package/dist/postgres-auth-adapter-LTDUGBMB.js +5 -0
  61. package/dist/postgres-auth-adapter-LTDUGBMB.js.map +1 -0
  62. package/dist/rest/index.cjs +1 -0
  63. package/dist/rest/index.js +1 -0
  64. package/dist/templates/index.cjs +94 -536
  65. package/dist/templates/index.cjs.map +1 -1
  66. package/dist/templates/index.d.cts +45 -1
  67. package/dist/templates/index.d.ts +45 -1
  68. package/dist/templates/index.js +2 -535
  69. package/dist/templates/index.js.map +1 -1
  70. package/dist/trpc/index.cjs +1 -0
  71. package/dist/trpc/index.js +1 -0
  72. package/dist/ws/index.cjs +1 -0
  73. package/dist/ws/index.js +1 -0
  74. package/package.json +23 -8
@@ -0,0 +1,899 @@
1
+ import Redis2 from 'ioredis';
2
+ import bcrypt from 'bcryptjs';
3
+ import { randomBytes } from 'crypto';
4
+ import nodemailer from 'nodemailer';
5
+
6
+ // src/auth/bootstrap.ts
7
+ var DEFAULT_PREFIX = "kyro:auth:";
8
+ var DEFAULT_TOKEN_EXPIRATION = 86400;
9
+ var DEFAULT_REFRESH_EXPIRATION = 604800;
10
+ var RedisAuthAdapter = class {
11
+ redis;
12
+ prefix;
13
+ tokenExpiration;
14
+ refreshExpiration;
15
+ constructor(options = {}) {
16
+ const url = options.url || `redis://${options.host || "localhost"}:${options.port || 6379}`;
17
+ this.redis = new Redis2(url, {
18
+ password: options.password,
19
+ db: options.db,
20
+ lazyConnect: true,
21
+ tls: options.tls ? {} : void 0
22
+ });
23
+ this.prefix = options.keyPrefix || DEFAULT_PREFIX;
24
+ this.tokenExpiration = options.tokenExpiration || DEFAULT_TOKEN_EXPIRATION;
25
+ this.refreshExpiration = options.refreshTokenExpiration || DEFAULT_REFRESH_EXPIRATION;
26
+ }
27
+ async connect() {
28
+ await this.redis.connect();
29
+ }
30
+ async disconnect() {
31
+ await this.redis.quit();
32
+ }
33
+ userKey(userId) {
34
+ return `${this.prefix}users:${userId}`;
35
+ }
36
+ sessionKey(sessionId) {
37
+ return `${this.prefix}sessions:${sessionId}`;
38
+ }
39
+ refreshKey(token) {
40
+ return `${this.prefix}refresh:${token}`;
41
+ }
42
+ userByEmailKey(email) {
43
+ return `${this.prefix}users:email:${email.toLowerCase()}`;
44
+ }
45
+ passwordHistoryKey(userId) {
46
+ return `${this.prefix}users:${userId}:password_history`;
47
+ }
48
+ async createUser(data) {
49
+ const userId = randomBytes(16).toString("hex");
50
+ const now = (/* @__PURE__ */ new Date()).toISOString();
51
+ const user = {
52
+ id: userId,
53
+ email: data.email.toLowerCase(),
54
+ passwordHash: data.passwordHash,
55
+ role: data.role || "customer",
56
+ tenantId: data.tenantId,
57
+ createdAt: now,
58
+ updatedAt: now
59
+ };
60
+ const pipeline = this.redis.pipeline();
61
+ pipeline.hset(this.userKey(userId), this.userToHash(user));
62
+ pipeline.set(this.userByEmailKey(data.email), userId);
63
+ await pipeline.exec();
64
+ return user;
65
+ }
66
+ async findUserByEmail(email) {
67
+ const userId = await this.redis.get(
68
+ this.userByEmailKey(email.toLowerCase())
69
+ );
70
+ if (!userId) return null;
71
+ return this.findUserById(userId);
72
+ }
73
+ async findUserById(userId) {
74
+ const data = await this.redis.hgetall(this.userKey(userId));
75
+ if (!data || Object.keys(data).length === 0) return null;
76
+ return this.hashToUser(data);
77
+ }
78
+ async updateUser(userId, data) {
79
+ const existing = await this.findUserById(userId);
80
+ if (!existing) return null;
81
+ const updated = {
82
+ ...existing,
83
+ ...data,
84
+ id: userId,
85
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
86
+ };
87
+ if (data.email && data.email !== existing.email) {
88
+ const pipeline = this.redis.pipeline();
89
+ pipeline.del(this.userByEmailKey(existing.email));
90
+ pipeline.set(this.userByEmailKey(data.email), userId);
91
+ await pipeline.exec();
92
+ }
93
+ await this.redis.hset(this.userKey(userId), this.userToHash(updated));
94
+ return updated;
95
+ }
96
+ async deleteUser(userId) {
97
+ const user = await this.findUserById(userId);
98
+ if (!user) return false;
99
+ const pipeline = this.redis.pipeline();
100
+ pipeline.del(this.userKey(userId));
101
+ pipeline.del(this.userByEmailKey(user.email));
102
+ pipeline.del(this.passwordHistoryKey(userId));
103
+ await pipeline.exec();
104
+ return true;
105
+ }
106
+ async hashPassword(password) {
107
+ return bcrypt.hash(password, 12);
108
+ }
109
+ async verifyPassword(password, hash) {
110
+ return bcrypt.compare(password, hash);
111
+ }
112
+ async createSession(userId, data = {}) {
113
+ const sessionId = randomBytes(32).toString("hex");
114
+ const token = randomBytes(32).toString("base64url");
115
+ const refreshToken = randomBytes(32).toString("base64url");
116
+ const now = /* @__PURE__ */ new Date();
117
+ const session = {
118
+ id: sessionId,
119
+ userId,
120
+ token,
121
+ refreshToken,
122
+ expiresAt: new Date(
123
+ now.getTime() + this.tokenExpiration * 1e3
124
+ ).toISOString(),
125
+ createdAt: now.toISOString(),
126
+ ipAddress: data.ipAddress,
127
+ userAgent: data.userAgent
128
+ };
129
+ const pipeline = this.redis.pipeline();
130
+ pipeline.hset(this.sessionKey(sessionId), this.sessionToHash(session));
131
+ pipeline.setex(
132
+ this.refreshKey(refreshToken),
133
+ this.refreshExpiration,
134
+ sessionId
135
+ );
136
+ await pipeline.exec();
137
+ return session;
138
+ }
139
+ async findSessionByToken(token) {
140
+ const data = await this.redis.hgetall(this.sessionKey(token));
141
+ if (!data || Object.keys(data).length === 0) return null;
142
+ return this.hashToSession(data);
143
+ }
144
+ async deleteSession(sessionId) {
145
+ const session = await this.redis.hgetall(this.sessionKey(sessionId));
146
+ if (!session || Object.keys(session).length === 0) return false;
147
+ const pipeline = this.redis.pipeline();
148
+ pipeline.del(this.sessionKey(sessionId));
149
+ if (session.refreshToken) {
150
+ pipeline.del(this.refreshKey(session.refreshToken));
151
+ }
152
+ await pipeline.exec();
153
+ return true;
154
+ }
155
+ async deleteUserSessions(userId) {
156
+ const pattern = `${this.prefix}sessions:*`;
157
+ let cursor = "0";
158
+ let deleted = 0;
159
+ do {
160
+ const [nextCursor, keys] = await this.redis.scan(
161
+ cursor,
162
+ "MATCH",
163
+ pattern,
164
+ "COUNT",
165
+ 100
166
+ );
167
+ cursor = nextCursor;
168
+ for (const key of keys) {
169
+ const sessionData = await this.redis.hgetall(key);
170
+ if (sessionData.userId === userId) {
171
+ const sessionId = key.replace(`${this.prefix}sessions:`, "");
172
+ await this.deleteSession(sessionId);
173
+ deleted++;
174
+ }
175
+ }
176
+ } while (cursor !== "0");
177
+ return deleted;
178
+ }
179
+ async addPasswordToHistory(userId, passwordHash) {
180
+ await this.redis.lpush(this.passwordHistoryKey(userId), passwordHash);
181
+ await this.redis.ltrim(this.passwordHistoryKey(userId), 0, 4);
182
+ }
183
+ async getPasswordHistory(userId, count = 5) {
184
+ return this.redis.lrange(this.passwordHistoryKey(userId), 0, count - 1);
185
+ }
186
+ async isPasswordInHistory(password, userId, historyCount = 5) {
187
+ const history = await this.getPasswordHistory(userId, historyCount);
188
+ for (const hash of history) {
189
+ if (await this.verifyPassword(password, hash)) {
190
+ return true;
191
+ }
192
+ }
193
+ return false;
194
+ }
195
+ userToHash(user) {
196
+ const hash = {
197
+ id: user.id,
198
+ email: user.email,
199
+ passwordHash: user.passwordHash || "",
200
+ role: user.role,
201
+ createdAt: user.createdAt,
202
+ updatedAt: user.updatedAt
203
+ };
204
+ if (user.tenantId) hash.tenantId = user.tenantId;
205
+ if (user.emailVerified !== void 0)
206
+ hash.emailVerified = String(user.emailVerified);
207
+ if (user.locked !== void 0) hash.locked = String(user.locked);
208
+ if (user.lastLogin) hash.lastLogin = user.lastLogin;
209
+ if (user.failedLoginAttempts !== void 0)
210
+ hash.failedLoginAttempts = String(user.failedLoginAttempts);
211
+ return hash;
212
+ }
213
+ hashToUser(hash) {
214
+ return {
215
+ id: hash.id,
216
+ email: hash.email,
217
+ passwordHash: hash.passwordHash,
218
+ role: hash.role,
219
+ tenantId: hash.tenantId,
220
+ createdAt: hash.createdAt,
221
+ updatedAt: hash.updatedAt,
222
+ emailVerified: hash.emailVerified === "true",
223
+ locked: hash.locked === "true",
224
+ lastLogin: hash.lastLogin,
225
+ failedLoginAttempts: hash.failedLoginAttempts ? parseInt(hash.failedLoginAttempts, 10) : 0
226
+ };
227
+ }
228
+ sessionToHash(session) {
229
+ const hash = {
230
+ id: session.id,
231
+ userId: session.userId,
232
+ token: session.token,
233
+ expiresAt: session.expiresAt,
234
+ createdAt: session.createdAt
235
+ };
236
+ if (session.refreshToken) hash.refreshToken = session.refreshToken;
237
+ if (session.ipAddress) hash.ipAddress = session.ipAddress;
238
+ if (session.userAgent) hash.userAgent = session.userAgent;
239
+ return hash;
240
+ }
241
+ hashToSession(hash) {
242
+ return {
243
+ id: hash.id,
244
+ userId: hash.userId,
245
+ token: hash.token,
246
+ refreshToken: hash.refreshToken,
247
+ expiresAt: hash.expiresAt,
248
+ createdAt: hash.createdAt,
249
+ ipAddress: hash.ipAddress,
250
+ userAgent: hash.userAgent
251
+ };
252
+ }
253
+ };
254
+ var defaultTemplates = {
255
+ verifyEmail: (link, userName = "User") => ({
256
+ subject: "Verify your email address",
257
+ html: `
258
+ <!DOCTYPE html>
259
+ <html>
260
+ <head>
261
+ <meta charset="utf-8">
262
+ <meta name="viewport" content="width=device-width, initial-scale=1">
263
+ <title>Verify Email</title>
264
+ <style>
265
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
266
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
267
+ .button { display: inline-block; padding: 12px 24px; background: #0b1222; color: white; text-decoration: none; border-radius: 6px; font-weight: 600; }
268
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <div class="container">
273
+ <h1>Welcome, ${userName}!</h1>
274
+ <p>Please verify your email address by clicking the button below:</p>
275
+ <p style="text-align: center; margin: 30px 0;">
276
+ <a href="${link}" class="button">Verify Email</a>
277
+ </p>
278
+ <p>Or copy and paste this link into your browser:</p>
279
+ <p style="word-break: break-all; color: #666;">${link}</p>
280
+ <p>This link will expire in 24 hours.</p>
281
+ <div class="footer">
282
+ <p>If you didn't create an account, you can safely ignore this email.</p>
283
+ </div>
284
+ </div>
285
+ </body>
286
+ </html>
287
+ `,
288
+ text: `Welcome ${userName}!
289
+
290
+ Please verify your email by clicking this link: ${link}
291
+
292
+ This link will expire in 24 hours.
293
+
294
+ If you didn't create an account, you can safely ignore this email.`
295
+ }),
296
+ resetPassword: (link, userName = "User") => ({
297
+ subject: "Reset your password",
298
+ html: `
299
+ <!DOCTYPE html>
300
+ <html>
301
+ <head>
302
+ <meta charset="utf-8">
303
+ <meta name="viewport" content="width=device-width, initial-scale=1">
304
+ <title>Reset Password</title>
305
+ <style>
306
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
307
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
308
+ .button { display: inline-block; padding: 12px 24px; background: #dc2626; color: white; text-decoration: none; border-radius: 6px; font-weight: 600; }
309
+ .warning { background: #fef3c7; border: 1px solid #f59e0b; padding: 12px; border-radius: 6px; margin: 20px 0; }
310
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
311
+ </style>
312
+ </head>
313
+ <body>
314
+ <div class="container">
315
+ <h1>Password Reset Request</h1>
316
+ <p>Hello ${userName},</p>
317
+ <p>We received a request to reset your password. Click the button below to create a new password:</p>
318
+ <p style="text-align: center; margin: 30px 0;">
319
+ <a href="${link}" class="button">Reset Password</a>
320
+ </p>
321
+ <p>Or copy and paste this link into your browser:</p>
322
+ <p style="word-break: break-all; color: #666;">${link}</p>
323
+ <div class="warning">
324
+ <strong>\u26A0\uFE0F Important:</strong> This link will expire in 1 hour. If you didn't request a password reset, please ignore this email or contact support if you have concerns.
325
+ </div>
326
+ <div class="footer">
327
+ <p>For security reasons, please don't share this email with anyone.</p>
328
+ </div>
329
+ </div>
330
+ </body>
331
+ </html>
332
+ `,
333
+ text: `Password Reset Request
334
+
335
+ Hello ${userName},
336
+
337
+ We received a request to reset your password. Click this link to create a new password: ${link}
338
+
339
+ This link will expire in 1 hour.
340
+
341
+ If you didn't request a password reset, please ignore this email.`
342
+ }),
343
+ welcome: (userName = "User") => ({
344
+ subject: "Welcome to Kyro CMS",
345
+ html: `
346
+ <!DOCTYPE html>
347
+ <html>
348
+ <head>
349
+ <meta charset="utf-8">
350
+ <meta name="viewport" content="width=device-width, initial-scale=1">
351
+ <title>Welcome</title>
352
+ <style>
353
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
354
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
355
+ .button { display: inline-block; padding: 12px 24px; background: #0b1222; color: white; text-decoration: none; border-radius: 6px; font-weight: 600; }
356
+ </style>
357
+ </head>
358
+ <body>
359
+ <div class="container">
360
+ <h1>Welcome to Kyro CMS, ${userName}!</h1>
361
+ <p>Your account has been created successfully.</p>
362
+ <p>You can now:</p>
363
+ <ul>
364
+ <li>Manage your content collections</li>
365
+ <li>Upload and organize media</li>
366
+ <li>Configure settings</li>
367
+ <li>And much more...</li>
368
+ </ul>
369
+ <p style="text-align: center; margin: 30px 0;">
370
+ <a href="#" class="button">Get Started</a>
371
+ </p>
372
+ <p>If you have any questions, feel free to reach out to our support team.</p>
373
+ </div>
374
+ </body>
375
+ </html>
376
+ `,
377
+ text: `Welcome to Kyro CMS, ${userName}!
378
+
379
+ Your account has been created successfully.
380
+
381
+ You can now:
382
+ - Manage your content collections
383
+ - Upload and organize media
384
+ - Configure settings
385
+ - And much more...
386
+
387
+ Get started by logging into your dashboard.`
388
+ }),
389
+ accountLocked: (attempts, duration, userName = "User") => ({
390
+ subject: "Account Security Alert - Account Locked",
391
+ html: `
392
+ <!DOCTYPE html>
393
+ <html>
394
+ <head>
395
+ <meta charset="utf-8">
396
+ <meta name="viewport" content="width=device-width, initial-scale=1">
397
+ <title>Account Locked</title>
398
+ <style>
399
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
400
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
401
+ .alert { background: #fef2f2; border: 1px solid #ef4444; padding: 16px; border-radius: 8px; margin: 20px 0; }
402
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <div class="container">
407
+ <h1>Account Security Alert</h1>
408
+ <p>Hello ${userName},</p>
409
+ <div class="alert">
410
+ <p><strong>\u26A0\uFE0F Your account has been temporarily locked due to multiple failed login attempts.</strong></p>
411
+ <p>Failed attempts: ${attempts}</p>
412
+ <p>Lockout duration: ${Math.round(duration / 6e4)} minutes</p>
413
+ </div>
414
+ <p>Your account will automatically unlock after the lockout period expires.</p>
415
+ <p>If this wasn't you, we recommend:</p>
416
+ <ul>
417
+ <li>Using a strong, unique password</li>
418
+ <li>Enabling two-factor authentication (coming soon)</li>
419
+ <li>Reviewing your recent account activity</li>
420
+ </ul>
421
+ <div class="footer">
422
+ <p>If you need immediate assistance, please contact support.</p>
423
+ </div>
424
+ </div>
425
+ </body>
426
+ </html>
427
+ `,
428
+ text: `Account Security Alert
429
+
430
+ Hello ${userName},
431
+
432
+ Your account has been temporarily locked due to multiple failed login attempts (${attempts}).
433
+
434
+ Lockout duration: ${Math.round(duration / 6e4)} minutes
435
+
436
+ Your account will automatically unlock after this period.
437
+
438
+ If this wasn't you, we recommend using a strong, unique password.`
439
+ }),
440
+ passwordChanged: (userName = "User") => ({
441
+ subject: "Your password has been changed",
442
+ html: `
443
+ <!DOCTYPE html>
444
+ <html>
445
+ <head>
446
+ <meta charset="utf-8">
447
+ <meta name="viewport" content="width=device-width, initial-scale=1">
448
+ <title>Password Changed</title>
449
+ <style>
450
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
451
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
452
+ .info { background: #f0fdf4; border: 1px solid #22c55e; padding: 12px; border-radius: 6px; margin: 20px 0; }
453
+ </style>
454
+ </head>
455
+ <body>
456
+ <div class="container">
457
+ <h1>Password Changed</h1>
458
+ <p>Hello ${userName},</p>
459
+ <div class="info">
460
+ <p>Your password was recently changed.</p>
461
+ </div>
462
+ <p>If you did this, you can safely ignore this email.</p>
463
+ <p><strong>If you didn't change your password</strong>, please contact our support team immediately as your account may have been compromised.</p>
464
+ </div>
465
+ </body>
466
+ </html>
467
+ `,
468
+ text: `Password Changed
469
+
470
+ Hello ${userName},
471
+
472
+ Your password was recently changed.
473
+
474
+ If you did this, you can safely ignore this email.
475
+
476
+ If you didn't change your password, please contact support immediately.`
477
+ }),
478
+ newLogin: (location, time, userName = "User") => ({
479
+ subject: "New login to your account",
480
+ html: `
481
+ <!DOCTYPE html>
482
+ <html>
483
+ <head>
484
+ <meta charset="utf-8">
485
+ <meta name="viewport" content="width=device-width, initial-scale=1">
486
+ <title>New Login</title>
487
+ <style>
488
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
489
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
490
+ .info-box { background: #f8fafc; border: 1px solid #e2e8f0; padding: 16px; border-radius: 8px; margin: 20px 0; }
491
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
492
+ </style>
493
+ </head>
494
+ <body>
495
+ <div class="container">
496
+ <h1>New Login Detected</h1>
497
+ <p>Hello ${userName},</p>
498
+ <p>We detected a new login to your account:</p>
499
+ <div class="info-box">
500
+ <p><strong>Location:</strong> ${location}</p>
501
+ <p><strong>Time:</strong> ${time}</p>
502
+ </div>
503
+ <p><strong>If this was you</strong>, no action is needed.</p>
504
+ <p><strong>If this wasn't you</strong>, your account may be compromised. Please:</p>
505
+ <ol>
506
+ <li>Change your password immediately</li>
507
+ <li>Review your recent account activity</li>
508
+ <li>Contact support if needed</li>
509
+ </ol>
510
+ <div class="footer">
511
+ <p>This is an automated security notification.</p>
512
+ </div>
513
+ </div>
514
+ </body>
515
+ </html>
516
+ `,
517
+ text: `New Login Detected
518
+
519
+ Hello ${userName},
520
+
521
+ We detected a new login to your account:
522
+
523
+ Location: ${location}
524
+ Time: ${time}
525
+
526
+ If this wasn't you, please change your password immediately and contact support.`
527
+ })
528
+ };
529
+ var EmailTransport = class _EmailTransport {
530
+ transporter;
531
+ from;
532
+ fromName;
533
+ templates;
534
+ constructor(config, templates) {
535
+ this.transporter = nodemailer.createTransport({
536
+ host: config.host,
537
+ port: config.port,
538
+ secure: config.secure,
539
+ auth: config.auth
540
+ });
541
+ this.from = config.from;
542
+ this.fromName = config.fromName || "Kyro CMS";
543
+ this.templates = { ...defaultTemplates, ...templates };
544
+ }
545
+ async send(options) {
546
+ return this.transporter.sendMail({
547
+ from: `"${this.fromName}" <${this.from}>`,
548
+ to: Array.isArray(options.to) ? options.to.join(", ") : options.to,
549
+ subject: options.subject,
550
+ html: options.html,
551
+ text: options.text
552
+ });
553
+ }
554
+ getTemplates() {
555
+ return this.templates;
556
+ }
557
+ async verifyConnection() {
558
+ try {
559
+ await this.transporter.verify();
560
+ return true;
561
+ } catch {
562
+ return false;
563
+ }
564
+ }
565
+ static fromEnv() {
566
+ const host = process.env.SMTP_HOST;
567
+ const port = parseInt(process.env.SMTP_PORT || "587", 10);
568
+ const secure = process.env.SMTP_SECURE === "true";
569
+ const user = process.env.SMTP_USER;
570
+ const pass = process.env.SMTP_PASS;
571
+ const from = process.env.SMTP_FROM || process.env.DEFAULT_FROM || "noreply@example.com";
572
+ const fromName = process.env.SMTP_FROM_NAME || "Kyro CMS";
573
+ if (!host || !user || !pass) {
574
+ return null;
575
+ }
576
+ return new _EmailTransport({
577
+ host,
578
+ port,
579
+ secure,
580
+ auth: { user, pass },
581
+ from,
582
+ fromName
583
+ });
584
+ }
585
+ };
586
+
587
+ // src/auth/security/password-policy.ts
588
+ var DEFAULT_PASSWORD_POLICY = {
589
+ minLength: 12,
590
+ requireUppercase: true,
591
+ requireLowercase: true,
592
+ requireNumbers: true,
593
+ requireSpecialChars: true,
594
+ preventReuse: 5,
595
+ maxLength: 128
596
+ };
597
+ var PasswordPolicy = class {
598
+ config;
599
+ constructor(config = {}) {
600
+ this.config = { ...DEFAULT_PASSWORD_POLICY, ...config };
601
+ }
602
+ validate(password) {
603
+ const errors = [];
604
+ if (this.config.maxLength && password.length > this.config.maxLength) {
605
+ errors.push(
606
+ `Password must not exceed ${this.config.maxLength} characters`
607
+ );
608
+ }
609
+ if (password.length < this.config.minLength) {
610
+ errors.push(
611
+ `Password must be at least ${this.config.minLength} characters`
612
+ );
613
+ }
614
+ if (this.config.requireUppercase && !/[A-Z]/.test(password)) {
615
+ errors.push("Password must contain at least one uppercase letter");
616
+ }
617
+ if (this.config.requireLowercase && !/[a-z]/.test(password)) {
618
+ errors.push("Password must contain at least one lowercase letter");
619
+ }
620
+ if (this.config.requireNumbers && !/[0-9]/.test(password)) {
621
+ errors.push("Password must contain at least one number");
622
+ }
623
+ if (this.config.requireSpecialChars && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
624
+ errors.push("Password must contain at least one special character");
625
+ }
626
+ const commonPasswords = [
627
+ "password",
628
+ "123456",
629
+ "12345678",
630
+ "qwerty",
631
+ "abc123",
632
+ "monkey",
633
+ "1234567",
634
+ "letmein",
635
+ "trustno1",
636
+ "dragon",
637
+ "baseball",
638
+ "iloveyou",
639
+ "master",
640
+ "sunshine",
641
+ "ashley",
642
+ "football",
643
+ "password1",
644
+ "shadow",
645
+ "123123",
646
+ "654321"
647
+ ];
648
+ if (commonPasswords.includes(password.toLowerCase())) {
649
+ errors.push(
650
+ "This password is too common. Please choose a more secure password"
651
+ );
652
+ }
653
+ if (/^[a-zA-Z]+$/.test(password) || /^[0-9]+$/.test(password)) {
654
+ errors.push(
655
+ "Password must contain a mix of letters, numbers, and/or special characters"
656
+ );
657
+ }
658
+ if (/(.)\1{2,}/.test(password)) {
659
+ errors.push(
660
+ "Password must not contain more than 2 consecutive identical characters"
661
+ );
662
+ }
663
+ if (/^(012|123|234|345|456|567|678|789|890|098|987|876|765|654|543|432|321|210)+$/i.test(
664
+ password
665
+ )) {
666
+ errors.push("Password must not contain sequential numbers or letters");
667
+ }
668
+ return {
669
+ valid: errors.length === 0,
670
+ errors
671
+ };
672
+ }
673
+ async checkReuse(passwordHash, history, verifyFn) {
674
+ return {
675
+ valid: true,
676
+ errors: []
677
+ };
678
+ }
679
+ async isInHistory(password, history, verifyFn) {
680
+ for (const hash of history) {
681
+ if (await verifyFn(password, hash)) {
682
+ return true;
683
+ }
684
+ }
685
+ return false;
686
+ }
687
+ generatePassword(length = 16) {
688
+ const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
689
+ const lowercase = "abcdefghijklmnopqrstuvwxyz";
690
+ const numbers = "0123456789";
691
+ const special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
692
+ let password = "";
693
+ password += uppercase[Math.floor(Math.random() * uppercase.length)];
694
+ password += lowercase[Math.floor(Math.random() * lowercase.length)];
695
+ password += numbers[Math.floor(Math.random() * numbers.length)];
696
+ password += special[Math.floor(Math.random() * special.length)];
697
+ const allChars = uppercase + lowercase + numbers + special;
698
+ for (let i = password.length; i < length; i++) {
699
+ password += allChars[Math.floor(Math.random() * allChars.length)];
700
+ }
701
+ return password.split("").sort(() => Math.random() - 0.5).join("");
702
+ }
703
+ getStrength(password) {
704
+ let score = 0;
705
+ const feedback = [];
706
+ if (password.length >= 8) score += 1;
707
+ if (password.length >= 12) score += 1;
708
+ if (password.length >= 16) score += 1;
709
+ if (/[a-z]/.test(password)) score += 1;
710
+ if (/[A-Z]/.test(password)) score += 1;
711
+ if (/[0-9]/.test(password)) score += 1;
712
+ if (/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) score += 1;
713
+ if (password.length > 8) score += 1;
714
+ if (password.length > 12) score += 1;
715
+ const uniqueChars = new Set(password).size;
716
+ if (uniqueChars > 6) score += 1;
717
+ if (uniqueChars > 10) score += 1;
718
+ let label;
719
+ if (score <= 3) {
720
+ label = "Weak";
721
+ feedback.push("Add more characters");
722
+ feedback.push("Include uppercase and lowercase letters");
723
+ } else if (score <= 5) {
724
+ label = "Fair";
725
+ feedback.push("Add special characters");
726
+ feedback.push("Consider making it longer");
727
+ } else if (score <= 7) {
728
+ label = "Good";
729
+ feedback.push("Consider making it longer for extra security");
730
+ } else {
731
+ label = "Strong";
732
+ }
733
+ return { score, label, feedback };
734
+ }
735
+ setConfig(config) {
736
+ this.config = { ...this.config, ...config };
737
+ }
738
+ getConfig() {
739
+ return { ...this.config };
740
+ }
741
+ };
742
+
743
+ // src/auth/bootstrap.ts
744
+ async function bootstrapAdmin(config) {
745
+ const {
746
+ redisUrl,
747
+ redisHost,
748
+ redisPort,
749
+ redisPassword,
750
+ adminEmail,
751
+ adminPassword,
752
+ adminRole = "super_admin",
753
+ tenantId,
754
+ emailConfig,
755
+ sendWelcomeEmail = false
756
+ } = config;
757
+ let redis;
758
+ if (redisUrl) {
759
+ redis = new Redis2(redisUrl);
760
+ } else {
761
+ redis = new Redis2({
762
+ host: redisHost || "localhost",
763
+ port: redisPort || 6379,
764
+ password: redisPassword,
765
+ lazyConnect: true
766
+ });
767
+ }
768
+ try {
769
+ await redis.connect();
770
+ } catch (error) {
771
+ return {
772
+ success: false,
773
+ error: "Failed to connect to Redis"
774
+ };
775
+ }
776
+ const authAdapter = new RedisAuthAdapter({
777
+ url: redisUrl,
778
+ host: redisHost,
779
+ port: redisPort,
780
+ password: redisPassword
781
+ });
782
+ const passwordPolicy = new PasswordPolicy();
783
+ const passwordValidation = passwordPolicy.validate(adminPassword);
784
+ if (!passwordValidation.valid) {
785
+ return {
786
+ success: false,
787
+ error: `Invalid password: ${passwordValidation.errors.join(", ")}`
788
+ };
789
+ }
790
+ const existingUser = await authAdapter.findUserByEmail(adminEmail);
791
+ if (existingUser) {
792
+ return {
793
+ success: false,
794
+ error: "Admin user already exists"
795
+ };
796
+ }
797
+ try {
798
+ const passwordHash = await authAdapter.hashPassword(adminPassword);
799
+ const user = await authAdapter.createUser({
800
+ email: adminEmail,
801
+ passwordHash,
802
+ role: adminRole || "admin",
803
+ tenantId
804
+ });
805
+ if (sendWelcomeEmail && emailConfig) {
806
+ const emailTransport = new EmailTransport(emailConfig);
807
+ const templates = emailTransport.getTemplates();
808
+ const welcomeTemplate = templates.welcome(adminEmail.split("@")[0]);
809
+ await emailTransport.send({
810
+ to: adminEmail,
811
+ ...welcomeTemplate
812
+ });
813
+ }
814
+ return {
815
+ success: true,
816
+ user
817
+ };
818
+ } catch (error) {
819
+ return {
820
+ success: false,
821
+ error: error instanceof Error ? error.message : "Failed to create admin user"
822
+ };
823
+ } finally {
824
+ await redis.quit();
825
+ }
826
+ }
827
+ async function checkBootstrapRequired(redis, adminEmail) {
828
+ const existingUser = await redis.get(
829
+ `kyro:auth:users:email:${adminEmail.toLowerCase()}`
830
+ );
831
+ return !existingUser;
832
+ }
833
+ function getBootstrapFromEnv() {
834
+ const email = process.env.KYRO_ADMIN_EMAIL;
835
+ const password = process.env.KYRO_ADMIN_PASSWORD;
836
+ if (!email || !password) {
837
+ return null;
838
+ }
839
+ return {
840
+ redisUrl: process.env.REDIS_URL,
841
+ redisHost: process.env.REDIS_HOST,
842
+ redisPort: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT, 10) : void 0,
843
+ redisPassword: process.env.REDIS_PASSWORD,
844
+ adminEmail: email,
845
+ adminPassword: password,
846
+ adminRole: process.env.KYRO_ADMIN_ROLE || "super_admin",
847
+ tenantId: process.env.KYRO_ADMIN_TENANT_ID,
848
+ emailConfig: process.env.SMTP_HOST ? {
849
+ host: process.env.SMTP_HOST,
850
+ port: parseInt(process.env.SMTP_PORT || "587", 10),
851
+ secure: process.env.SMTP_SECURE === "true",
852
+ auth: {
853
+ user: process.env.SMTP_USER || "",
854
+ pass: process.env.SMTP_PASS || ""
855
+ },
856
+ from: process.env.SMTP_FROM || "noreply@example.com",
857
+ fromName: process.env.SMTP_FROM_NAME
858
+ } : void 0,
859
+ sendWelcomeEmail: process.env.KYRO_ADMIN_SEND_WELCOME === "true"
860
+ };
861
+ }
862
+ async function autoBootstrap() {
863
+ const config = getBootstrapFromEnv();
864
+ if (!config) {
865
+ return null;
866
+ }
867
+ console.log("Auto-bootstrapping admin user...");
868
+ const result = await bootstrapAdmin(config);
869
+ if (result.success) {
870
+ console.log(`Admin user created: ${config.adminEmail}`);
871
+ } else {
872
+ console.error(`Bootstrap failed: ${result.error}`);
873
+ }
874
+ return result;
875
+ }
876
+ async function bootstrapWithRetry(config, maxRetries = 3, retryDelayMs = 2e3) {
877
+ let lastError = "";
878
+ for (let i = 0; i < maxRetries; i++) {
879
+ const result = await bootstrapAdmin(config);
880
+ if (result.success) {
881
+ return result;
882
+ }
883
+ lastError = result.error || "Unknown error";
884
+ if (lastError.includes("already exists")) {
885
+ return result;
886
+ }
887
+ if (i < maxRetries - 1) {
888
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
889
+ }
890
+ }
891
+ return {
892
+ success: false,
893
+ error: `Failed after ${maxRetries} retries: ${lastError}`
894
+ };
895
+ }
896
+
897
+ export { EmailTransport, PasswordPolicy, RedisAuthAdapter, autoBootstrap, bootstrapAdmin, bootstrapWithRetry, checkBootstrapRequired, getBootstrapFromEnv };
898
+ //# sourceMappingURL=chunk-V67YXRBT.js.map
899
+ //# sourceMappingURL=chunk-V67YXRBT.js.map