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