@kyro-cms/core 0.1.4 → 0.1.5

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 (77) hide show
  1. package/dist/bootstrap-BDTTUGY2.js +4 -0
  2. package/dist/{bootstrap-Q2TWUQF3.js.map → bootstrap-BDTTUGY2.js.map} +1 -1
  3. package/dist/bootstrap-X6TP3NKX.cjs +29 -0
  4. package/dist/{bootstrap-2WJK6PG7.cjs.map → bootstrap-X6TP3NKX.cjs.map} +1 -1
  5. package/dist/chunk-5BLDMQED.cjs +18 -0
  6. package/dist/{chunk-Q7SFCCGT.cjs.map → chunk-5BLDMQED.cjs.map} +1 -1
  7. package/dist/{chunk-U4CHJTWX.cjs → chunk-7G6EVYCU.cjs} +5 -5
  8. package/dist/{chunk-U4CHJTWX.cjs.map → chunk-7G6EVYCU.cjs.map} +1 -1
  9. package/dist/chunk-A3RQWHKD.cjs +263 -0
  10. package/dist/chunk-A3RQWHKD.cjs.map +1 -0
  11. package/dist/{chunk-V67YXRBT.js → chunk-C74MQIRL.js} +517 -203
  12. package/dist/chunk-C74MQIRL.js.map +1 -0
  13. package/dist/{chunk-XLMVCGXA.js → chunk-LRTZJJPD.js} +3 -3
  14. package/dist/{chunk-XLMVCGXA.js.map → chunk-LRTZJJPD.js.map} +1 -1
  15. package/dist/{chunk-I4BORBXT.cjs → chunk-MHS6CPO5.cjs} +517 -204
  16. package/dist/chunk-MHS6CPO5.cjs.map +1 -0
  17. package/dist/chunk-NSBPE2FW.js +15 -0
  18. package/dist/{chunk-PZ5AY32C.js.map → chunk-NSBPE2FW.js.map} +1 -1
  19. package/dist/{chunk-M4JFHQ5J.js → chunk-QUJ4OLSC.js} +3 -3
  20. package/dist/{chunk-M4JFHQ5J.js.map → chunk-QUJ4OLSC.js.map} +1 -1
  21. package/dist/{chunk-5AOILNGY.cjs → chunk-TZFJMPCH.cjs} +4 -4
  22. package/dist/{chunk-5AOILNGY.cjs.map → chunk-TZFJMPCH.cjs.map} +1 -1
  23. package/dist/chunk-VMSRTAH7.js +256 -0
  24. package/dist/chunk-VMSRTAH7.js.map +1 -0
  25. package/dist/{chunk-KA3UOIFC.js → chunk-XTZSUDSI.js} +3 -3
  26. package/dist/{chunk-KA3UOIFC.js.map → chunk-XTZSUDSI.js.map} +1 -1
  27. package/dist/{chunk-KWTKEBHM.cjs → chunk-YD7Y25W7.cjs} +19 -19
  28. package/dist/{chunk-KWTKEBHM.cjs.map → chunk-YD7Y25W7.cjs.map} +1 -1
  29. package/dist/cli/index.cjs +5 -5
  30. package/dist/cli/index.js +5 -5
  31. package/dist/database-7CJOXEZR.js +5 -0
  32. package/dist/{database-37KXWUER.js.map → database-7CJOXEZR.js.map} +1 -1
  33. package/dist/database-QOIV44GT.cjs +22 -0
  34. package/dist/{database-LJKD3HE4.cjs.map → database-QOIV44GT.cjs.map} +1 -1
  35. package/dist/drizzle/index.cjs +8 -8
  36. package/dist/drizzle/index.d.cts +1 -1
  37. package/dist/drizzle/index.d.ts +1 -1
  38. package/dist/drizzle/index.js +4 -4
  39. package/dist/graphql/index.cjs +1 -1
  40. package/dist/graphql/index.js +1 -1
  41. package/dist/{index-CzkEHKqu.d.cts → index-BMySjW6o.d.cts} +6 -0
  42. package/dist/{index-BVFlb7uU.d.ts → index-CMUNCIWQ.d.ts} +6 -0
  43. package/dist/index.cjs +727 -346
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +229 -62
  46. package/dist/index.d.ts +229 -62
  47. package/dist/index.js +706 -331
  48. package/dist/index.js.map +1 -1
  49. package/dist/mongodb/index.cjs +1 -1
  50. package/dist/mongodb/index.js +1 -1
  51. package/dist/postgres-auth-adapter-REJFUMP7.js +5 -0
  52. package/dist/{postgres-auth-adapter-LTDUGBMB.js.map → postgres-auth-adapter-REJFUMP7.js.map} +1 -1
  53. package/dist/postgres-auth-adapter-VK6GY7LX.cjs +14 -0
  54. package/dist/{postgres-auth-adapter-CYZAVPPP.cjs.map → postgres-auth-adapter-VK6GY7LX.cjs.map} +1 -1
  55. package/dist/redis-adapter-4YDY4LWE.js +4 -0
  56. package/dist/redis-adapter-4YDY4LWE.js.map +1 -0
  57. package/dist/redis-adapter-LBLNKGNS.cjs +13 -0
  58. package/dist/redis-adapter-LBLNKGNS.cjs.map +1 -0
  59. package/dist/rest/index.cjs +1 -1
  60. package/dist/rest/index.js +1 -1
  61. package/dist/templates/index.cjs +1 -1
  62. package/dist/templates/index.js +1 -1
  63. package/dist/trpc/index.cjs +1 -1
  64. package/dist/trpc/index.js +1 -1
  65. package/dist/ws/index.cjs +1 -1
  66. package/dist/ws/index.js +1 -1
  67. package/package.json +2 -2
  68. package/dist/bootstrap-2WJK6PG7.cjs +0 -29
  69. package/dist/bootstrap-Q2TWUQF3.js +0 -4
  70. package/dist/chunk-I4BORBXT.cjs.map +0 -1
  71. package/dist/chunk-PZ5AY32C.js +0 -9
  72. package/dist/chunk-Q7SFCCGT.cjs +0 -11
  73. package/dist/chunk-V67YXRBT.js.map +0 -1
  74. package/dist/database-37KXWUER.js +0 -5
  75. package/dist/database-LJKD3HE4.cjs +0 -22
  76. package/dist/postgres-auth-adapter-CYZAVPPP.cjs +0 -14
  77. package/dist/postgres-auth-adapter-LTDUGBMB.js +0 -5
@@ -1,55 +1,270 @@
1
- import Redis2 from 'ioredis';
2
1
  import bcrypt from 'bcryptjs';
3
2
  import { randomBytes } from 'crypto';
4
3
  import nodemailer from 'nodemailer';
5
4
 
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;
5
+ // src/auth/sqlite-adapter.ts
6
+ var DEFAULT_BUSY_TIMEOUT = 5e3;
7
+ var DEFAULT_WAL_CHECKPOINT = 1e3;
8
+ var DEFAULT_CACHE_SIZE = -64e3;
9
+ var DEFAULT_MMAP_SIZE = 268435456;
10
+ var SQLiteAuthAdapter = class {
11
+ db = null;
12
+ path;
13
+ saltRounds;
14
+ externalDb;
15
+ busyTimeout;
16
+ walAutoCheckpoint;
17
+ cacheSize;
18
+ mmapSize;
19
+ preparedStatements = /* @__PURE__ */ new Map();
15
20
  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;
21
+ this.path = options.path || "./data/auth.db";
22
+ this.saltRounds = options.saltRounds || 12;
23
+ this.externalDb = !!options.db;
24
+ this.busyTimeout = options.busyTimeout ?? DEFAULT_BUSY_TIMEOUT;
25
+ this.walAutoCheckpoint = options.walAutoCheckpoint ?? DEFAULT_WAL_CHECKPOINT;
26
+ this.cacheSize = options.cacheSize ?? DEFAULT_CACHE_SIZE;
27
+ this.mmapSize = options.mmapSize ?? DEFAULT_MMAP_SIZE;
28
+ if (options.db) {
29
+ this.db = options.db;
30
+ }
26
31
  }
27
32
  async connect() {
28
- await this.redis.connect();
33
+ if (this.db) return;
34
+ const Database = (await import('better-sqlite3')).default;
35
+ this.db = new Database(this.path, {
36
+ timeout: this.busyTimeout
37
+ });
38
+ this.db.pragma("journal_mode = WAL");
39
+ this.db.pragma("synchronous = NORMAL");
40
+ this.db.pragma("cache_size = " + this.cacheSize);
41
+ this.db.pragma("mmap_size = " + this.mmapSize);
42
+ this.db.pragma("wal_autocheckpoint = " + this.walAutoCheckpoint);
43
+ this.db.pragma("foreign_keys = ON");
44
+ this.db.pragma("temp_store = MEMORY");
45
+ this.ensureTables();
46
+ this.prepareStatements();
29
47
  }
30
48
  async disconnect() {
31
- await this.redis.quit();
49
+ if (this.db && !this.externalDb) {
50
+ this.db.pragma("wal_checkpoint(TRUNCATE)");
51
+ this.db.close();
52
+ this.db = null;
53
+ this.preparedStatements.clear();
54
+ }
55
+ }
56
+ ensureTables() {
57
+ if (!this.db) return;
58
+ this.db.exec(`
59
+ CREATE TABLE IF NOT EXISTS kyro_users (
60
+ id TEXT PRIMARY KEY,
61
+ email TEXT UNIQUE NOT NULL,
62
+ password_hash TEXT NOT NULL,
63
+ role TEXT NOT NULL DEFAULT 'customer',
64
+ tenant_id TEXT,
65
+ email_verified INTEGER DEFAULT 0,
66
+ locked INTEGER DEFAULT 0,
67
+ last_login TEXT,
68
+ failed_login_attempts INTEGER DEFAULT 0,
69
+ locked_until TEXT,
70
+ created_at TEXT NOT NULL,
71
+ updated_at TEXT NOT NULL
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS kyro_sessions (
75
+ id TEXT PRIMARY KEY,
76
+ user_id TEXT NOT NULL,
77
+ token TEXT NOT NULL,
78
+ refresh_token TEXT,
79
+ expires_at TEXT NOT NULL,
80
+ created_at TEXT NOT NULL,
81
+ ip_address TEXT,
82
+ user_agent TEXT,
83
+ FOREIGN KEY (user_id) REFERENCES kyro_users(id) ON DELETE CASCADE
84
+ );
85
+
86
+ CREATE TABLE IF NOT EXISTS kyro_password_history (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ user_id TEXT NOT NULL,
89
+ password_hash TEXT NOT NULL,
90
+ created_at TEXT NOT NULL,
91
+ FOREIGN KEY (user_id) REFERENCES kyro_users(id) ON DELETE CASCADE
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS kyro_rate_limits (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ key TEXT NOT NULL,
97
+ window_start INTEGER NOT NULL,
98
+ count INTEGER NOT NULL DEFAULT 1,
99
+ UNIQUE(key, window_start)
100
+ );
101
+
102
+ CREATE TABLE IF NOT EXISTS kyro_lockouts (
103
+ user_id TEXT PRIMARY KEY,
104
+ attempts INTEGER NOT NULL DEFAULT 0,
105
+ last_attempt INTEGER,
106
+ locked_at INTEGER,
107
+ locked_until INTEGER
108
+ );
109
+
110
+ CREATE TABLE IF NOT EXISTS kyro_audit_logs (
111
+ id TEXT PRIMARY KEY,
112
+ timestamp TEXT NOT NULL,
113
+ action TEXT NOT NULL,
114
+ user_id TEXT,
115
+ user_email TEXT,
116
+ role TEXT,
117
+ resource TEXT NOT NULL,
118
+ resource_id TEXT,
119
+ ip_address TEXT,
120
+ user_agent TEXT,
121
+ success INTEGER NOT NULL,
122
+ error TEXT,
123
+ metadata TEXT,
124
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
125
+ );
126
+
127
+ CREATE INDEX IF NOT EXISTS idx_kyro_users_email ON kyro_users(email);
128
+ CREATE INDEX IF NOT EXISTS idx_kyro_sessions_user_id ON kyro_sessions(user_id);
129
+ CREATE INDEX IF NOT EXISTS idx_kyro_sessions_token ON kyro_sessions(token);
130
+ CREATE INDEX IF NOT EXISTS idx_kyro_sessions_refresh_token ON kyro_sessions(refresh_token);
131
+ CREATE INDEX IF NOT EXISTS idx_kyro_sessions_expires ON kyro_sessions(expires_at);
132
+ CREATE INDEX IF NOT EXISTS idx_kyro_password_history_user_id ON kyro_password_history(user_id);
133
+ CREATE INDEX IF NOT EXISTS idx_kyro_rate_limits_key ON kyro_rate_limits(key);
134
+ CREATE INDEX IF NOT EXISTS idx_kyro_rate_limits_window ON kyro_rate_limits(window_start);
135
+ CREATE INDEX IF NOT EXISTS idx_kyro_lockouts_locked_until ON kyro_lockouts(locked_until);
136
+ CREATE INDEX IF NOT EXISTS idx_kyro_audit_logs_timestamp ON kyro_audit_logs(timestamp);
137
+ CREATE INDEX IF NOT EXISTS idx_kyro_audit_logs_action ON kyro_audit_logs(action);
138
+ CREATE INDEX IF NOT EXISTS idx_kyro_audit_logs_user_id ON kyro_audit_logs(user_id);
139
+ CREATE INDEX IF NOT EXISTS idx_kyro_audit_logs_resource ON kyro_audit_logs(resource);
140
+ `);
32
141
  }
33
- userKey(userId) {
34
- return `${this.prefix}users:${userId}`;
142
+ prepareStatements() {
143
+ if (!this.db) return;
144
+ this.preparedStatements.set(
145
+ "findUserByEmail",
146
+ this.db.prepare("SELECT * FROM kyro_users WHERE email = ?")
147
+ );
148
+ this.preparedStatements.set(
149
+ "findUserById",
150
+ this.db.prepare("SELECT * FROM kyro_users WHERE id = ?")
151
+ );
152
+ this.preparedStatements.set(
153
+ "findSessionByToken",
154
+ this.db.prepare("SELECT * FROM kyro_sessions WHERE token = ?")
155
+ );
156
+ this.preparedStatements.set(
157
+ "findSessionByRefreshToken",
158
+ this.db.prepare("SELECT * FROM kyro_sessions WHERE refresh_token = ?")
159
+ );
160
+ this.preparedStatements.set(
161
+ "deleteSession",
162
+ this.db.prepare("DELETE FROM kyro_sessions WHERE id = ? OR token = ?")
163
+ );
164
+ this.preparedStatements.set(
165
+ "deleteUserSessions",
166
+ this.db.prepare("DELETE FROM kyro_sessions WHERE user_id = ?")
167
+ );
168
+ this.preparedStatements.set(
169
+ "countUsers",
170
+ this.db.prepare("SELECT COUNT(*) as count FROM kyro_users")
171
+ );
172
+ this.preparedStatements.set(
173
+ "deleteUser",
174
+ this.db.prepare("DELETE FROM kyro_users WHERE id = ?")
175
+ );
176
+ this.preparedStatements.set(
177
+ "getPasswordHistory",
178
+ this.db.prepare(
179
+ "SELECT password_hash FROM kyro_password_history WHERE user_id = ? ORDER BY created_at DESC LIMIT ?"
180
+ )
181
+ );
182
+ this.preparedStatements.set(
183
+ "addPasswordHistory",
184
+ this.db.prepare(
185
+ "INSERT INTO kyro_password_history (user_id, password_hash, created_at) VALUES (?, ?, ?)"
186
+ )
187
+ );
188
+ this.preparedStatements.set(
189
+ "trimPasswordHistory",
190
+ this.db.prepare(
191
+ `DELETE FROM kyro_password_history WHERE id IN (
192
+ SELECT id FROM kyro_password_history WHERE user_id = ? ORDER BY created_at DESC LIMIT -1 OFFSET 5
193
+ )`
194
+ )
195
+ );
196
+ this.preparedStatements.set(
197
+ "deleteExpiredSessions",
198
+ this.db.prepare("DELETE FROM kyro_sessions WHERE expires_at < ?")
199
+ );
200
+ this.preparedStatements.set(
201
+ "cleanupOldAuditLogs",
202
+ this.db.prepare("DELETE FROM kyro_audit_logs WHERE timestamp < ?")
203
+ );
204
+ this.preparedStatements.set(
205
+ "cleanupExpiredLockouts",
206
+ this.db.prepare(
207
+ "UPDATE kyro_lockouts SET attempts = 0, locked_at = NULL, locked_until = NULL WHERE locked_until < ?"
208
+ )
209
+ );
210
+ this.preparedStatements.set(
211
+ "getLockout",
212
+ this.db.prepare("SELECT * FROM kyro_lockouts WHERE user_id = ?")
213
+ );
214
+ this.preparedStatements.set(
215
+ "upsertLockout",
216
+ this.db.prepare(`
217
+ INSERT INTO kyro_lockouts (user_id, attempts, last_attempt, locked_at, locked_until)
218
+ VALUES (?, ?, ?, ?, ?)
219
+ ON CONFLICT(user_id) DO UPDATE SET
220
+ attempts = excluded.attempts,
221
+ last_attempt = excluded.last_attempt,
222
+ locked_at = excluded.locked_at,
223
+ locked_until = excluded.locked_until
224
+ `)
225
+ );
226
+ this.preparedStatements.set(
227
+ "resetLockout",
228
+ this.db.prepare(
229
+ "UPDATE kyro_lockouts SET attempts = 0, locked_at = NULL, locked_until = NULL WHERE user_id = ?"
230
+ )
231
+ );
35
232
  }
36
- sessionKey(sessionId) {
37
- return `${this.prefix}sessions:${sessionId}`;
233
+ stmt(name) {
234
+ const stmt = this.preparedStatements.get(name);
235
+ if (!stmt) throw new Error(`Prepared statement not found: ${name}`);
236
+ return stmt;
38
237
  }
39
- refreshKey(token) {
40
- return `${this.prefix}refresh:${token}`;
238
+ async cleanupExpiredSessions() {
239
+ if (!this.db) throw new Error("Not connected");
240
+ const result = this.stmt("deleteExpiredSessions").run(
241
+ (/* @__PURE__ */ new Date()).toISOString()
242
+ );
243
+ return result.changes;
41
244
  }
42
- userByEmailKey(email) {
43
- return `${this.prefix}users:email:${email.toLowerCase()}`;
245
+ async cleanupOldAuditLogs(retentionDays = 30) {
246
+ if (!this.db) throw new Error("Not connected");
247
+ const cutoff = new Date(
248
+ Date.now() - retentionDays * 24 * 60 * 60 * 1e3
249
+ ).toISOString();
250
+ const result = this.stmt("cleanupOldAuditLogs").run(cutoff);
251
+ return result.changes;
44
252
  }
45
- passwordHistoryKey(userId) {
46
- return `${this.prefix}users:${userId}:password_history`;
253
+ async getStats() {
254
+ if (!this.db) throw new Error("Not connected");
255
+ const userCount = this.stmt("countUsers").get().count;
256
+ const activeSessionCount = this.db.prepare(
257
+ "SELECT COUNT(*) as count FROM kyro_sessions WHERE expires_at > ?"
258
+ ).get((/* @__PURE__ */ new Date()).toISOString()).count;
259
+ const auditLogCount = this.db.prepare("SELECT COUNT(*) as count FROM kyro_audit_logs").get().count;
260
+ return { userCount, activeSessionCount, auditLogCount };
47
261
  }
48
262
  async createUser(data) {
49
- const userId = randomBytes(16).toString("hex");
263
+ if (!this.db) throw new Error("Not connected");
264
+ const id = randomBytes(16).toString("hex");
50
265
  const now = (/* @__PURE__ */ new Date()).toISOString();
51
266
  const user = {
52
- id: userId,
267
+ id,
53
268
  email: data.email.toLowerCase(),
54
269
  passwordHash: data.passwordHash,
55
270
  role: data.role || "customer",
@@ -57,197 +272,317 @@ var RedisAuthAdapter = class {
57
272
  createdAt: now,
58
273
  updatedAt: now
59
274
  };
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();
275
+ this.db.prepare(
276
+ `INSERT INTO kyro_users (id, email, password_hash, role, tenant_id, created_at, updated_at)
277
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
278
+ ).run(
279
+ id,
280
+ user.email,
281
+ user.passwordHash,
282
+ user.role,
283
+ user.tenantId,
284
+ now,
285
+ now
286
+ );
64
287
  return user;
65
288
  }
66
289
  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);
290
+ if (!this.db) throw new Error("Not connected");
291
+ const row = this.stmt("findUserByEmail").get(email.toLowerCase());
292
+ if (!row) return null;
293
+ return this.rowToUser(row);
72
294
  }
73
295
  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);
296
+ if (!this.db) throw new Error("Not connected");
297
+ const row = this.stmt("findUserById").get(userId);
298
+ if (!row) return null;
299
+ return this.rowToUser(row);
77
300
  }
78
301
  async updateUser(userId, data) {
302
+ if (!this.db) throw new Error("Not connected");
79
303
  const existing = await this.findUserById(userId);
80
304
  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();
305
+ const updates = [];
306
+ const values = [];
307
+ if (data.email !== void 0) {
308
+ updates.push("email = ?");
309
+ values.push(data.email.toLowerCase());
92
310
  }
93
- await this.redis.hset(this.userKey(userId), this.userToHash(updated));
94
- return updated;
311
+ if (data.passwordHash !== void 0) {
312
+ updates.push("password_hash = ?");
313
+ values.push(data.passwordHash);
314
+ }
315
+ if (data.role !== void 0) {
316
+ updates.push("role = ?");
317
+ values.push(data.role);
318
+ }
319
+ if (data.tenantId !== void 0) {
320
+ updates.push("tenant_id = ?");
321
+ values.push(data.tenantId);
322
+ }
323
+ if (data.emailVerified !== void 0) {
324
+ updates.push("email_verified = ?");
325
+ values.push(data.emailVerified ? 1 : 0);
326
+ }
327
+ if (data.locked !== void 0) {
328
+ updates.push("locked = ?");
329
+ values.push(data.locked ? 1 : 0);
330
+ }
331
+ if (data.lastLogin !== void 0) {
332
+ updates.push("last_login = ?");
333
+ values.push(data.lastLogin);
334
+ }
335
+ if (data.failedLoginAttempts !== void 0) {
336
+ updates.push("failed_login_attempts = ?");
337
+ values.push(data.failedLoginAttempts);
338
+ }
339
+ updates.push("updated_at = ?");
340
+ values.push((/* @__PURE__ */ new Date()).toISOString());
341
+ values.push(userId);
342
+ this.db.prepare(`UPDATE kyro_users SET ${updates.join(", ")} WHERE id = ?`).run(...values);
343
+ return this.findUserById(userId);
95
344
  }
96
345
  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;
346
+ if (!this.db) throw new Error("Not connected");
347
+ const result = this.stmt("deleteUser").run(userId);
348
+ return result.changes > 0;
105
349
  }
106
350
  async hashPassword(password) {
107
- return bcrypt.hash(password, 12);
351
+ return bcrypt.hash(password, this.saltRounds);
108
352
  }
109
353
  async verifyPassword(password, hash) {
110
354
  return bcrypt.compare(password, hash);
111
355
  }
112
356
  async createSession(userId, data = {}) {
113
- const sessionId = randomBytes(32).toString("hex");
357
+ if (!this.db) throw new Error("Not connected");
358
+ const id = randomBytes(32).toString("hex");
114
359
  const token = randomBytes(32).toString("base64url");
115
360
  const refreshToken = randomBytes(32).toString("base64url");
116
361
  const now = /* @__PURE__ */ new Date();
362
+ const expiresAt = new Date(now.getTime() + 864e5).toISOString();
117
363
  const session = {
118
- id: sessionId,
364
+ id,
119
365
  userId,
120
366
  token,
121
367
  refreshToken,
122
- expiresAt: new Date(
123
- now.getTime() + this.tokenExpiration * 1e3
124
- ).toISOString(),
368
+ expiresAt,
125
369
  createdAt: now.toISOString(),
126
370
  ipAddress: data.ipAddress,
127
371
  userAgent: data.userAgent
128
372
  };
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
373
+ this.db.prepare(
374
+ `INSERT INTO kyro_sessions (id, user_id, token, refresh_token, expires_at, created_at, ip_address, user_agent)
375
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
376
+ ).run(
377
+ session.id,
378
+ session.userId,
379
+ session.token,
380
+ session.refreshToken,
381
+ session.expiresAt,
382
+ session.createdAt,
383
+ session.ipAddress,
384
+ session.userAgent
135
385
  );
136
- await pipeline.exec();
137
386
  return session;
138
387
  }
139
388
  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);
389
+ if (!this.db) throw new Error("Not connected");
390
+ const row = this.stmt("findSessionByToken").get(token);
391
+ if (!row) return null;
392
+ return this.rowToSession(row);
393
+ }
394
+ async findSessionByRefreshToken(refreshToken) {
395
+ if (!this.db) throw new Error("Not connected");
396
+ const row = this.stmt("findSessionByRefreshToken").get(refreshToken);
397
+ if (!row) return null;
398
+ return this.rowToSession(row);
143
399
  }
144
400
  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;
401
+ if (!this.db) throw new Error("Not connected");
402
+ const result = this.stmt("deleteSession").run(sessionId, sessionId);
403
+ return result.changes > 0;
154
404
  }
155
405
  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;
406
+ if (!this.db) throw new Error("Not connected");
407
+ const result = this.stmt("deleteUserSessions").run(userId);
408
+ return result.changes;
409
+ }
410
+ async hasAnyUsers() {
411
+ if (!this.db) throw new Error("Not connected");
412
+ const row = this.stmt("countUsers").get();
413
+ return row.count > 0;
178
414
  }
179
415
  async addPasswordToHistory(userId, passwordHash) {
180
- await this.redis.lpush(this.passwordHistoryKey(userId), passwordHash);
181
- await this.redis.ltrim(this.passwordHistoryKey(userId), 0, 4);
416
+ if (!this.db) throw new Error("Not connected");
417
+ this.stmt("addPasswordHistory").run(
418
+ userId,
419
+ passwordHash,
420
+ (/* @__PURE__ */ new Date()).toISOString()
421
+ );
422
+ this.stmt("trimPasswordHistory").run(userId);
182
423
  }
183
424
  async getPasswordHistory(userId, count = 5) {
184
- return this.redis.lrange(this.passwordHistoryKey(userId), 0, count - 1);
425
+ if (!this.db) throw new Error("Not connected");
426
+ const rows = this.stmt("getPasswordHistory").all(userId, count);
427
+ return rows.map((r) => r.password_hash);
185
428
  }
186
429
  async isPasswordInHistory(password, userId, historyCount = 5) {
187
430
  const history = await this.getPasswordHistory(userId, historyCount);
188
431
  for (const hash of history) {
189
- if (await this.verifyPassword(password, hash)) {
432
+ if (await bcrypt.compare(password, hash)) {
190
433
  return true;
191
434
  }
192
435
  }
193
436
  return false;
194
437
  }
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
438
+ async recordFailedAttempt(userId) {
439
+ if (!this.db) throw new Error("Not connected");
440
+ const now = Date.now();
441
+ const lockout = this.stmt("getLockout").get(userId);
442
+ const attempts = (lockout?.attempts || 0) + 1;
443
+ const lockedUntil = attempts >= 5 ? now + 15 * 60 * 1e3 : lockout?.locked_until || null;
444
+ this.stmt("upsertLockout").run(
445
+ userId,
446
+ attempts,
447
+ now,
448
+ lockedUntil !== null ? now : null,
449
+ lockedUntil
450
+ );
451
+ }
452
+ async resetAttempts(userId) {
453
+ if (!this.db) throw new Error("Not connected");
454
+ this.stmt("resetLockout").run(userId);
455
+ }
456
+ async checkLockout(userId) {
457
+ if (!this.db) throw new Error("Not connected");
458
+ this.stmt("cleanupExpiredLockouts").run(Date.now());
459
+ const lockout = this.stmt("getLockout").get(userId);
460
+ if (!lockout) {
461
+ return {
462
+ locked: false,
463
+ attemptsRemaining: 5,
464
+ totalAttempts: 0
465
+ };
466
+ }
467
+ if (lockout.locked_until !== null && lockout.locked_until > Date.now()) {
468
+ return {
469
+ locked: true,
470
+ attemptsRemaining: 0,
471
+ lockedUntil: new Date(lockout.locked_until),
472
+ totalAttempts: lockout.attempts
473
+ };
474
+ }
475
+ return {
476
+ locked: false,
477
+ attemptsRemaining: Math.max(0, 5 - lockout.attempts),
478
+ totalAttempts: lockout.attempts
203
479
  };
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) {
480
+ }
481
+ async logAudit(data) {
482
+ if (!this.db) throw new Error("Not connected");
483
+ const id = randomBytes(16).toString("hex");
484
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
485
+ this.db.prepare(
486
+ `INSERT INTO kyro_audit_logs (
487
+ id, timestamp, action, user_id, user_email, role, resource, resource_id,
488
+ ip_address, user_agent, success, error, metadata, created_at
489
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
490
+ ).run(
491
+ id,
492
+ timestamp,
493
+ data.action,
494
+ data.userId || null,
495
+ data.userEmail || null,
496
+ data.role || null,
497
+ data.resource,
498
+ data.resourceId || null,
499
+ data.ipAddress || null,
500
+ data.userAgent || null,
501
+ data.success ? 1 : 0,
502
+ data.error || null,
503
+ data.metadata ? JSON.stringify(data.metadata) : null,
504
+ (/* @__PURE__ */ new Date()).toISOString()
505
+ );
506
+ return id;
507
+ }
508
+ async queryAuditLogs(options = {}) {
509
+ if (!this.db) throw new Error("Not connected");
510
+ const conditions = [];
511
+ const params = [];
512
+ if (options.action) {
513
+ conditions.push("action = ?");
514
+ params.push(options.action);
515
+ }
516
+ if (options.userId) {
517
+ conditions.push("user_id = ?");
518
+ params.push(options.userId);
519
+ }
520
+ if (options.resource) {
521
+ conditions.push("resource = ?");
522
+ params.push(options.resource);
523
+ }
524
+ if (options.success !== void 0) {
525
+ conditions.push("success = ?");
526
+ params.push(options.success ? 1 : 0);
527
+ }
528
+ if (options.startDate) {
529
+ conditions.push("timestamp >= ?");
530
+ params.push(options.startDate.toISOString());
531
+ }
532
+ if (options.endDate) {
533
+ conditions.push("timestamp <= ?");
534
+ params.push(options.endDate.toISOString());
535
+ }
536
+ const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
537
+ const limit = options.limit || 50;
538
+ const offset = options.offset || 0;
539
+ const totalResult = this.db.prepare(`SELECT COUNT(*) as count FROM kyro_audit_logs ${where}`).get(...params);
540
+ const rows = this.db.prepare(
541
+ `SELECT * FROM kyro_audit_logs ${where} ORDER BY timestamp DESC LIMIT ? OFFSET ?`
542
+ ).all(...params, limit, offset);
214
543
  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
544
+ total: totalResult.count,
545
+ logs: rows.map((row) => ({
546
+ id: row.id,
547
+ timestamp: new Date(row.timestamp),
548
+ action: row.action,
549
+ userId: row.user_id || void 0,
550
+ userEmail: row.user_email || void 0,
551
+ resource: row.resource,
552
+ resourceId: row.resource_id || void 0,
553
+ ipAddress: row.ip_address || void 0,
554
+ userAgent: row.user_agent || void 0,
555
+ success: row.success === 1,
556
+ error: row.error || void 0,
557
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0
558
+ }))
226
559
  };
227
560
  }
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
561
+ rowToUser(row) {
562
+ return {
563
+ id: row.id,
564
+ email: row.email,
565
+ passwordHash: row.password_hash,
566
+ role: row.role,
567
+ tenantId: row.tenant_id,
568
+ emailVerified: row.email_verified === 1,
569
+ locked: row.locked === 1,
570
+ lastLogin: row.last_login,
571
+ failedLoginAttempts: row.failed_login_attempts || 0,
572
+ createdAt: row.created_at,
573
+ updatedAt: row.updated_at
235
574
  };
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
575
  }
241
- hashToSession(hash) {
576
+ rowToSession(row) {
242
577
  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
578
+ id: row.id,
579
+ userId: row.user_id,
580
+ token: row.token,
581
+ refreshToken: row.refresh_token,
582
+ expiresAt: row.expires_at,
583
+ createdAt: row.created_at,
584
+ ipAddress: row.ip_address,
585
+ userAgent: row.user_agent
251
586
  };
252
587
  }
253
588
  };
@@ -743,10 +1078,6 @@ var PasswordPolicy = class {
743
1078
  // src/auth/bootstrap.ts
744
1079
  async function bootstrapAdmin(config) {
745
1080
  const {
746
- redisUrl,
747
- redisHost,
748
- redisPort,
749
- redisPassword,
750
1081
  adminEmail,
751
1082
  adminPassword,
752
1083
  adminRole = "super_admin",
@@ -754,34 +1085,21 @@ async function bootstrapAdmin(config) {
754
1085
  emailConfig,
755
1086
  sendWelcomeEmail = false
756
1087
  } = 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
- }
1088
+ const authAdapter = config.authAdapter || new SQLiteAuthAdapter({
1089
+ path: config.authDbPath || "./data/auth.db"
1090
+ });
768
1091
  try {
769
- await redis.connect();
1092
+ await authAdapter.connect?.();
770
1093
  } catch (error) {
771
1094
  return {
772
1095
  success: false,
773
- error: "Failed to connect to Redis"
1096
+ error: "Failed to connect to auth storage"
774
1097
  };
775
1098
  }
776
- const authAdapter = new RedisAuthAdapter({
777
- url: redisUrl,
778
- host: redisHost,
779
- port: redisPort,
780
- password: redisPassword
781
- });
782
1099
  const passwordPolicy = new PasswordPolicy();
783
1100
  const passwordValidation = passwordPolicy.validate(adminPassword);
784
1101
  if (!passwordValidation.valid) {
1102
+ await authAdapter.disconnect?.();
785
1103
  return {
786
1104
  success: false,
787
1105
  error: `Invalid password: ${passwordValidation.errors.join(", ")}`
@@ -789,6 +1107,7 @@ async function bootstrapAdmin(config) {
789
1107
  }
790
1108
  const existingUser = await authAdapter.findUserByEmail(adminEmail);
791
1109
  if (existingUser) {
1110
+ await authAdapter.disconnect?.();
792
1111
  return {
793
1112
  success: false,
794
1113
  error: "Admin user already exists"
@@ -811,23 +1130,21 @@ async function bootstrapAdmin(config) {
811
1130
  ...welcomeTemplate
812
1131
  });
813
1132
  }
1133
+ await authAdapter.disconnect?.();
814
1134
  return {
815
1135
  success: true,
816
1136
  user
817
1137
  };
818
1138
  } catch (error) {
1139
+ await authAdapter.disconnect?.();
819
1140
  return {
820
1141
  success: false,
821
1142
  error: error instanceof Error ? error.message : "Failed to create admin user"
822
1143
  };
823
- } finally {
824
- await redis.quit();
825
1144
  }
826
1145
  }
827
- async function checkBootstrapRequired(redis, adminEmail) {
828
- const existingUser = await redis.get(
829
- `kyro:auth:users:email:${adminEmail.toLowerCase()}`
830
- );
1146
+ async function checkBootstrapRequired(authAdapter, adminEmail) {
1147
+ const existingUser = await authAdapter.findUserByEmail(adminEmail);
831
1148
  return !existingUser;
832
1149
  }
833
1150
  function getBootstrapFromEnv() {
@@ -837,10 +1154,7 @@ function getBootstrapFromEnv() {
837
1154
  return null;
838
1155
  }
839
1156
  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,
1157
+ authDbPath: process.env.KYRO_AUTH_DB_PATH || "./data/auth.db",
844
1158
  adminEmail: email,
845
1159
  adminPassword: password,
846
1160
  adminRole: process.env.KYRO_ADMIN_ROLE || "super_admin",
@@ -894,6 +1208,6 @@ async function bootstrapWithRetry(config, maxRetries = 3, retryDelayMs = 2e3) {
894
1208
  };
895
1209
  }
896
1210
 
897
- export { EmailTransport, PasswordPolicy, RedisAuthAdapter, autoBootstrap, bootstrapAdmin, bootstrapWithRetry, checkBootstrapRequired, getBootstrapFromEnv };
898
- //# sourceMappingURL=chunk-V67YXRBT.js.map
899
- //# sourceMappingURL=chunk-V67YXRBT.js.map
1211
+ export { EmailTransport, PasswordPolicy, SQLiteAuthAdapter, autoBootstrap, bootstrapAdmin, bootstrapWithRetry, checkBootstrapRequired, getBootstrapFromEnv };
1212
+ //# sourceMappingURL=chunk-C74MQIRL.js.map
1213
+ //# sourceMappingURL=chunk-C74MQIRL.js.map