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