@kyro-cms/core 0.1.4 → 0.1.6

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