@fuzionx/framework 0.1.21 β†’ 0.1.23

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.
@@ -1,14 +1,6 @@
1
1
  import { Application } from '@fuzionx/framework';
2
- import webRoutes from './routes/web.js';
3
- import apiRoutes from './routes/api.js';
4
2
 
5
3
  const app = new Application({ configPath: './fuzionx.yaml' });
6
4
 
7
- app.routes(webRoutes);
8
- app.routes(apiRoutes);
9
-
10
5
  await app.boot();
11
-
12
- app.listen(49080, () => {
13
- console.log('πŸš€ FuzionX running on http://localhost:49080');
14
- });
6
+ await app.listen();
@@ -8,6 +8,6 @@ export default class HomeController extends Controller {
8
8
 
9
9
  /** ν™ˆ νŽ˜μ΄μ§€ */
10
10
  async index(ctx) {
11
- ctx.render('pages/home');
11
+ ctx.render('home');
12
12
  }
13
13
  }
@@ -1,13 +1,38 @@
1
1
  # FuzionX Configuration
2
2
  bridge:
3
3
  port: 49080
4
- workers: 4
4
+ workers: 0
5
5
  worker_timeout: 30
6
6
 
7
+ cors:
8
+ enabled: false
9
+ origins:
10
+ - "*"
11
+
7
12
  rate_limit:
8
13
  enabled: true
9
14
  per_ip: 1000
10
15
 
16
+ session:
17
+ enabled: false
18
+ store: memory
19
+ ttl: 3600
20
+ cookie_name: fuzionx.sid
21
+
22
+ websocket:
23
+ enabled: false
24
+ path: /ws
25
+ check_interval: 60
26
+ timeout: 60
27
+
28
+ static:
29
+ - url: /public
30
+ path: ./public
31
+
32
+ logging:
33
+ level: info
34
+ intercept_console: true
35
+
11
36
  database:
12
37
  default: main
13
38
  connections:
@@ -18,15 +43,18 @@ database:
18
43
  app:
19
44
  name: '{{name}}'
20
45
  environment: development
46
+
21
47
  auth:
22
- secret: 'change-me-in-production'
48
+ secret: '${JWT_SECRET:change-me-in-production}'
23
49
  accessTtl: '15m'
50
+
24
51
  i18n:
25
52
  default_locale: 'ko'
26
53
  fallback: 'en'
54
+
27
55
  docs:
28
56
  enabled: true
29
57
  path: '/docs'
30
58
 
31
- themes:
32
- default: 'default'
59
+ themes:
60
+ default: 'default'
@@ -1,5 +1,5 @@
1
+ import HomeController from '../controllers/HomeController.js';
2
+
1
3
  export default (r) => {
2
- r.get('/', (ctx) => {
3
- ctx.render('home');
4
- });
4
+ r.get('/', HomeController.index);
5
5
  };
@@ -1,7 +1,7 @@
1
1
  import { Middleware } from '@fuzionx/framework';
2
2
 
3
3
  export default class {{Name}}Middleware extends Middleware {
4
- static name = '{{nameLower}}';
4
+ static alias = '{{nameLower}}';
5
5
 
6
6
  async handle(ctx, next) {
7
7
  // TODO: implement
@@ -245,6 +245,18 @@ export default class Application {
245
245
  async boot() {
246
246
  if (this._booted) return;
247
247
 
248
+ // 0. Bridge μ΄ˆκΈ°ν™” β€” boot 둜직 전에 bridge 확보
249
+ // Helper(crypto/hash/media/file/i18n/logger/view)κ°€ boot 쀑 bridge ν•„μš”
250
+ if (FuzionXApp && !this._coreApp) {
251
+ this._coreApp = new FuzionXApp({
252
+ config: this.configPath
253
+ ? path.resolve(this.baseDir, this.configPath)
254
+ : undefined,
255
+ });
256
+ this._bridge = this._coreApp._bridge;
257
+ this._propagateBridge();
258
+ }
259
+
248
260
  // 1. .env λ‘œλ“œ (17-config.md)
249
261
  this.config.loadEnv(this.baseDir);
250
262
  // .env λ‘œλ“œ ν›„ μΊμ‹œ 클리어 (μƒˆλ‘œμš΄ ν™˜κ²½λ³€μˆ˜ 반영)
@@ -307,6 +319,21 @@ export default class Application {
307
319
  await this.emit('booted');
308
320
  }
309
321
 
322
+ /**
323
+ * Bridge μ°Έμ‘°λ₯Ό λͺ¨λ“  Helper μΈμŠ€ν„΄μŠ€μ— μ „νŒŒ
324
+ * @private
325
+ */
326
+ _propagateBridge() {
327
+ if (!this._bridge) return;
328
+ if (this._view) this._view._bridge = this._bridge;
329
+ if (this.logger) this.logger._bridge = this._bridge;
330
+ if (this.crypto) this.crypto._bridge = this._bridge;
331
+ if (this.hash) this.hash._bridge = this._bridge;
332
+ if (this.media) this.media._bridge = this._bridge;
333
+ if (this.file) this.file._bridge = this._bridge;
334
+ if (this.i18n) this.i18n._bridge = this._bridge;
335
+ }
336
+
310
337
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
311
338
  // μ„œλ²„ μ‹œμž‘ β€” Bridge μ—°κ²°
312
339
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -331,29 +358,14 @@ export default class Application {
331
358
 
332
359
  await this.emit('ready');
333
360
 
334
- // Bridge 연동
335
- if (FuzionXApp) {
336
- if (!this._coreApp) {
337
- this._coreApp = new FuzionXApp({
338
- config: this.configPath
339
- ? path.resolve(this.baseDir, this.configPath)
340
- : undefined,
341
- port,
342
- });
343
- // FuzionXApp 생성 ν›„ Bridge μ „νŒŒ β€” boot() μ‹œμ μ—λŠ” nullμ΄μ—ˆμŒ
344
- this._bridge = this._coreApp._bridge;
345
- if (this._bridge) {
346
- if (this._view) this._view._bridge = this._bridge;
347
- if (this.logger) this.logger._bridge = this._bridge;
348
- if (this.crypto) this.crypto._bridge = this._bridge;
349
- if (this.hash) this.hash._bridge = this._bridge;
350
- if (this.media) this.media._bridge = this._bridge;
351
- if (this.file) this.file._bridge = this._bridge;
352
- if (this.i18n) this.i18n._bridge = this._bridge;
353
- }
354
- // WS proxy μ—°κ²° (WsHandlerμ—μ„œ this.app.ws μ‚¬μš©)
355
- this.ws = this._coreApp.ws;
356
- }
361
+ // Bridge 연동 β€” _coreApp은 boot()μ—μ„œ 이미 생성됨
362
+ if (this._coreApp) {
363
+ // boot()μ—μ„œ port 없이 μƒμ„±λμœΌλ―€λ‘œ port μ„€μ •
364
+ this._coreApp._port = port;
365
+
366
+ // WS proxy μ—°κ²° (WsHandlerμ—μ„œ this.app.ws μ‚¬μš©)
367
+ if (!this.ws) this.ws = this._coreApp.ws;
368
+
357
369
  this._registerBridgeRoutes(this._coreApp);
358
370
 
359
371
  // WsHandler β†’ Bridge WS 이벀트 μ—°κ²°
@@ -527,14 +539,15 @@ export default class Application {
527
539
  const eventMap = HandlerClass.buildEventMap();
528
540
  const wsNs = coreApp.ws(namespace);
529
541
 
542
+ // 싱글톀 μΈμŠ€ν„΄μŠ€ β€” μ—°κ²°/λ©”μ‹œμ§€/ν•΄μ œμ—μ„œ 동일 μΈμŠ€ν„΄μŠ€ μ‚¬μš©
543
+ const inst = new HandlerClass(this);
544
+
530
545
  wsNs.on('connect', (socket) => {
531
- const inst = new HandlerClass(this);
532
546
  console.log(`[WS] connect: ${namespace} sid=${socket.sessionId}`);
533
547
  inst.onConnect(socket);
534
548
  });
535
549
 
536
550
  wsNs.on('message', (socket, rawMessage) => {
537
- const inst = new HandlerClass(this);
538
551
  let parsed;
539
552
  try { parsed = typeof rawMessage === 'string' ? JSON.parse(rawMessage) : rawMessage; }
540
553
  catch { parsed = { type: 'message', data: rawMessage }; }
@@ -556,7 +569,6 @@ export default class Application {
556
569
  });
557
570
 
558
571
  wsNs.on('disconnect', (socket) => {
559
- const inst = new HandlerClass(this);
560
572
  console.log(`[WS] disconnect: ${namespace} sid=${socket.sessionId}`);
561
573
  inst.onDisconnect(socket);
562
574
  });
@@ -605,7 +617,7 @@ export default class Application {
605
617
  }, this);
606
618
 
607
619
  // Framework β†’ async 체인 μ‹€ν–‰ ν›„ 직접 sendAsyncResponse
608
- const promise = this._executeChain(middlewareFns, route, ctx, res);
620
+ const promise = this._executeChain(middlewareFns, route, ctx);
609
621
 
610
622
  promise.then(() => {
611
623
  const response = ctx.toResponse();
@@ -646,7 +658,7 @@ export default class Application {
646
658
  * 미듀웨어 체인 + ν•Έλ“€λŸ¬ μ‹€ν–‰
647
659
  * @private
648
660
  */
649
- async _executeChain(middlewareFns, route, ctx, res) {
661
+ async _executeChain(middlewareFns, route, ctx) {
650
662
  let idx = 0;
651
663
 
652
664
  const next = async () => {
@@ -677,18 +689,7 @@ export default class Application {
677
689
  }
678
690
  if (!handled) this._errorHandler.handle(err, ctx);
679
691
  }
680
-
681
- // Context β†’ Bridge res λ³€ν™˜ (이쀑 응닡 λ°©μ§€ β€” Core resλŠ” _sent μ‚¬μš©)
682
- if (!res._sent) {
683
- const response = ctx.toResponse();
684
- res.status(response.status);
685
- if (response.headers) {
686
- for (const [k, v] of Object.entries(response.headers)) {
687
- res.header(k, v);
688
- }
689
- }
690
- res.send(response.body);
691
- }
692
+ // 응닡은 _createBridgeHandler의 .then()μ—μ„œ sendAsyncResponse둜 전솑
692
693
  }
693
694
 
694
695
  /**
@@ -106,8 +106,13 @@ export default class AutoLoader {
106
106
  */
107
107
  static registerController(ControllerClass) {
108
108
  const proto = ControllerClass.prototype;
109
+ // Base ν”„λ‘œν† νƒ€μž… λ©”μ„œλ“œ μŠ€ν‚΅ (Controller.register와 동일 둜직)
110
+ const baseNames = new Set(Object.getOwnPropertyNames(
111
+ Object.getPrototypeOf(proto) // Base.prototype (Controller extends Base)
112
+ ));
109
113
  for (const method of Object.getOwnPropertyNames(proto)) {
110
114
  if (method === 'constructor') continue;
115
+ if (baseNames.has(method)) continue;
111
116
  if (typeof proto[method] !== 'function') continue;
112
117
  // static property둜 __handler__ λ””μŠ€ν¬λ¦½ν„° 등둝
113
118
  ControllerClass[method] = {
@@ -140,7 +145,7 @@ export default class AutoLoader {
140
145
  const mod = await import(file);
141
146
  const MwClass = mod.default;
142
147
  if (!MwClass) continue;
143
- const mwName = MwClass.name || extractName(file, 'Middleware').toLowerCase();
148
+ const mwName = MwClass.alias || extractName(file, 'Middleware').toLowerCase();
144
149
  this.app._middlewareRegistry.set(mwName, MwClass);
145
150
  }
146
151
  }
@@ -38,7 +38,8 @@ export default class Config {
38
38
 
39
39
  try {
40
40
  const content = readFileSync(configPath, 'utf-8');
41
- const parsed = Config.parseYaml(content);
41
+ const raw = Config.parseYaml(content);
42
+ const parsed = Config.resolveEnvVars(raw);
42
43
 
43
44
  // _raw에 병합 (κΈ°μ‘΄ opts.config μš°μ„ )
44
45
  for (const [key, value] of Object.entries(parsed)) {
@@ -175,9 +175,14 @@ export default class Context {
175
175
  return this;
176
176
  }
177
177
 
178
- send(text) {
179
- this._body = String(text);
180
- this._headers['Content-Type'] = 'text/html; charset=utf-8';
178
+ send(data) {
179
+ if (typeof data === 'object' && data !== null) {
180
+ return this.json(data);
181
+ }
182
+ this._body = String(data);
183
+ if (!this._headers['Content-Type']) {
184
+ this._headers['Content-Type'] = 'text/html; charset=utf-8';
185
+ }
181
186
  this._sent = true;
182
187
  return this;
183
188
  }
@@ -139,7 +139,23 @@ export default class ConnectionManager {
139
139
  case 'mongo': {
140
140
  const mongoose = tryRequire('mongoose');
141
141
  if (!mongoose) return this._createStub('mongo', config);
142
- return { type: 'mongo', mongoose, config };
142
+ // Lazy connect β€” promise μΊμ‹±μœΌλ‘œ 이쀑 μ—°κ²° λ°©μ§€
143
+ const conn = { type: 'mongo', mongoose, config, _connected: false, _connectPromise: null };
144
+ conn.connect = () => {
145
+ if (conn._connected) return Promise.resolve();
146
+ if (conn._connectPromise) return conn._connectPromise;
147
+ const uri = config.uri || config.url
148
+ || `mongodb://${config.host || '127.0.0.1'}:${config.port || 27017}/${config.database || ''}`;
149
+ conn._connectPromise = mongoose.connect(uri, config.options || {}).then(() => {
150
+ conn._connected = true;
151
+ });
152
+ return conn._connectPromise;
153
+ };
154
+ // μ¦‰μ‹œ μ—°κ²° μ‹œμž‘ (await 없이 β€” λ°±κ·ΈλΌμš΄λ“œ)
155
+ conn.connect().catch(err => {
156
+ console.error(`[ConnectionManager] MongoDB μ—°κ²° μ‹€νŒ¨: ${err.message}`);
157
+ });
158
+ return conn;
143
159
  }
144
160
 
145
161
  default:
@@ -94,9 +94,10 @@ export default class SqlModel extends Model {
94
94
  }
95
95
 
96
96
  if (conn.type === 'sqlite' && conn.db) {
97
- const columns = Object.keys(insertData);
97
+ const columns = Object.keys(insertData).map(c => SqlQueryBuilder._sanitizeName(c));
98
98
  const placeholders = columns.map(() => '?').join(', ');
99
- const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) VALUES (${placeholders})`;
99
+ const safeTable = SqlQueryBuilder._sanitizeName(this.table);
100
+ const sql = `INSERT INTO ${safeTable} (${columns.join(', ')}) VALUES (${placeholders})`;
100
101
  const result = conn.db.prepare(sql).run(...Object.values(insertData));
101
102
  const id = result.lastInsertRowid;
102
103
  return new this({ ...insertData, [this.primaryKey]: Number(id) });
@@ -207,9 +208,11 @@ export default class SqlModel extends Model {
207
208
  const conn = this.constructor.getConnection();
208
209
 
209
210
  if (conn.type === 'sqlite' && conn.db) {
210
- const columns = Object.keys(data);
211
+ const columns = Object.keys(data).map(c => SqlQueryBuilder._sanitizeName(c));
212
+ const pk = SqlQueryBuilder._sanitizeName(this.constructor.primaryKey);
213
+ const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
211
214
  const sets = columns.map(c => `${c} = ?`).join(', ');
212
- const sql = `UPDATE ${this.constructor.table} SET ${sets} WHERE ${pk} = ?`;
215
+ const sql = `UPDATE ${safeTable} SET ${sets} WHERE ${pk} = ?`;
213
216
  conn.db.prepare(sql).run(...Object.values(data), id);
214
217
  } else if (conn.type === 'knex' && conn.db) {
215
218
  await conn.db(this.constructor.table).where(pk, id).update(data);
@@ -244,7 +247,9 @@ export default class SqlModel extends Model {
244
247
  const conn = this.constructor.getConnection();
245
248
 
246
249
  if (conn.type === 'sqlite' && conn.db) {
247
- conn.db.prepare(`DELETE FROM ${this.constructor.table} WHERE ${pk} = ?`).run(id);
250
+ const safeTable = SqlQueryBuilder._sanitizeName(this.constructor.table);
251
+ const safePk = SqlQueryBuilder._sanitizeName(pk);
252
+ conn.db.prepare(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`).run(id);
248
253
  } else if (conn.type === 'knex' && conn.db) {
249
254
  await conn.db(this.constructor.table).where(pk, id).delete();
250
255
  }
@@ -7,12 +7,26 @@
7
7
  * @see docs/framework/02-database-orm.md
8
8
  */
9
9
  import QueryBuilder from './QueryBuilder.js';
10
+ import Pagination from './Pagination.js';
10
11
 
11
12
  export default class SqlQueryBuilder extends QueryBuilder {
12
13
  constructor(model) {
13
14
  super(model);
14
15
  }
15
16
 
17
+ /**
18
+ * SQL μ‹λ³„μž(컬럼λͺ…/ν…Œμ΄λΈ”λͺ…) μœ νš¨μ„± 검증 β€” SQL Injection λ°©μ§€
19
+ * @param {string} name
20
+ * @returns {string}
21
+ * @private
22
+ */
23
+ static _sanitizeName(name) {
24
+ if (typeof name !== 'string' || !/^[\w.]+$/.test(name)) {
25
+ throw new Error(`Invalid SQL identifier: '${name}'`);
26
+ }
27
+ return name;
28
+ }
29
+
16
30
  /**
17
31
  * κ²°κ³Ό 쑰회 β€” SQL SELECT μ‹€ν–‰
18
32
  * @returns {Promise<Array<import('./Model.js').default>>}
@@ -117,7 +131,8 @@ export default class SqlQueryBuilder extends QueryBuilder {
117
131
 
118
132
  if (conn.type === 'sqlite' && conn.db) {
119
133
  const { whereClause, whereParams } = this._buildWhereSql();
120
- const sql = `DELETE FROM ${this._model.table}${whereClause}`;
134
+ const safeTable = SqlQueryBuilder._sanitizeName(this._model.table);
135
+ const sql = `DELETE FROM ${safeTable}${whereClause}`;
121
136
  const result = conn.db.prepare(sql).run(...whereParams);
122
137
  return result.changes;
123
138
  }
@@ -141,13 +156,7 @@ export default class SqlQueryBuilder extends QueryBuilder {
141
156
  this._limit = perPage;
142
157
  this._offset = (page - 1) * perPage;
143
158
  const data = await this.get();
144
- return {
145
- data,
146
- page,
147
- perPage,
148
- total,
149
- lastPage: Math.ceil(total / perPage),
150
- };
159
+ return new Pagination(data, total, page, perPage);
151
160
  }
152
161
 
153
162
  // ━━━━━━━━━━ SQLite μ‹€ν–‰ ━━━━━━━━━━
@@ -161,13 +170,15 @@ export default class SqlQueryBuilder extends QueryBuilder {
161
170
 
162
171
  const selectColumns = mode === 'count'
163
172
  ? 'COUNT(*) as cnt'
164
- : (this._selects ? this._selects.join(', ') : '*');
173
+ : (this._selects ? this._selects.map(c => SqlQueryBuilder._sanitizeName(c)).join(', ') : '*');
174
+
175
+ const safeTable = SqlQueryBuilder._sanitizeName(this._model.table);
165
176
 
166
- let sql = `SELECT ${selectColumns} FROM ${this._model.table}${whereClause}`;
177
+ let sql = `SELECT ${selectColumns} FROM ${safeTable}${whereClause}`;
167
178
 
168
179
  // ORDER BY
169
180
  if (this._orders.length > 0 && mode !== 'count') {
170
- const orderParts = this._orders.map(o => `${o.column} ${o.direction.toUpperCase()}`);
181
+ const orderParts = this._orders.map(o => `${SqlQueryBuilder._sanitizeName(o.column)} ${o.direction.toUpperCase()}`);
171
182
  sql += ` ORDER BY ${orderParts.join(', ')}`;
172
183
  }
173
184
 
@@ -204,20 +215,21 @@ export default class SqlQueryBuilder extends QueryBuilder {
204
215
  }
205
216
 
206
217
  for (const w of this._wheres) {
218
+ const safeKey = SqlQueryBuilder._sanitizeName(w.key);
207
219
  const prefix = parts.length > 0
208
220
  ? (w.type === 'or' ? ' OR ' : ' AND ')
209
221
  : '';
210
222
 
211
223
  if (w.op === 'IN') {
212
224
  const placeholders = w.value.map(() => '?').join(', ');
213
- parts.push(`${prefix}${w.key} IN (${placeholders})`);
225
+ parts.push(`${prefix}${safeKey} IN (${placeholders})`);
214
226
  params.push(...w.value);
215
227
  } else if (w.op === 'IS NULL') {
216
- parts.push(`${prefix}${w.key} IS NULL`);
228
+ parts.push(`${prefix}${safeKey} IS NULL`);
217
229
  } else if (w.op === 'IS NOT NULL') {
218
- parts.push(`${prefix}${w.key} IS NOT NULL`);
230
+ parts.push(`${prefix}${safeKey} IS NOT NULL`);
219
231
  } else {
220
- parts.push(`${prefix}${w.key} ${w.op} ?`);
232
+ parts.push(`${prefix}${safeKey} ${w.op} ?`);
221
233
  params.push(w.value);
222
234
  }
223
235
  }
@@ -233,8 +245,8 @@ export default class SqlQueryBuilder extends QueryBuilder {
233
245
  _buildUpdateSql(data) {
234
246
  const { whereClause, whereParams } = this._buildWhereSql();
235
247
  const columns = Object.keys(data);
236
- const sets = columns.map(c => `${c} = ?`).join(', ');
237
- const sql = `UPDATE ${this._model.table} SET ${sets}${whereClause}`;
248
+ const sets = columns.map(c => `${SqlQueryBuilder._sanitizeName(c)} = ?`).join(', ');
249
+ const sql = `UPDATE ${SqlQueryBuilder._sanitizeName(this._model.table)} SET ${sets}${whereClause}`;
238
250
  const params = [...Object.values(data), ...whereParams];
239
251
  return { sql, params };
240
252
  }
@@ -8,7 +8,7 @@ import Base from '../core/Base.js';
8
8
 
9
9
  export default class Middleware extends Base {
10
10
  /** @type {string} 미듀웨어 등둝 이름 (λΌμš°νŠΈμ—μ„œ μ°Έμ‘°) */
11
- static name = '';
11
+ static alias = '';
12
12
 
13
13
  /**
14
14
  * 미듀웨어 ν•Έλ“€λŸ¬ (μ„œλΈŒν΄λž˜μŠ€μ—μ„œ μ˜€λ²„λΌμ΄λ“œ)
@@ -4,6 +4,7 @@
4
4
  * @see docs/framework/12-middleware.md
5
5
  * @see docs/framework/14-authentication.md
6
6
  */
7
+ import { createHmac, timingSafeEqual } from 'node:crypto';
7
8
 
8
9
  /**
9
10
  * JSON/Form body νŒŒμ‹± 미듀웨어
@@ -200,7 +201,6 @@ function decodeJwtPayload(token, secret) {
200
201
  const [headerB64, payloadB64, signatureB64] = parts;
201
202
 
202
203
  // ── HMAC-SHA256 μ„œλͺ… 검증 ──
203
- const { createHmac, timingSafeEqual } = require('node:crypto');
204
204
  const signingInput = `${headerB64}.${payloadB64}`;
205
205
  const expectedSig = createHmac('sha256', secret)
206
206
  .update(signingInput)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "description": "Full-stack MVC framework built on @fuzionx/core β€” Controller, Service, Model, Middleware, DI, EventBus",
6
6
  "main": "index.js",
@@ -34,7 +34,7 @@
34
34
  "url": "https://github.com/saytohenry/fuzionx"
35
35
  },
36
36
  "dependencies": {
37
- "@fuzionx/core": "^0.1.21",
37
+ "@fuzionx/core": "^0.1.23",
38
38
  "better-sqlite3": "^12.8.0",
39
39
  "knex": "^3.2.5",
40
40
  "mongoose": "^9.3.2",