bro-framework 3.0.2 → 3.0.4

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.
package/bin/bro.js CHANGED
@@ -45,7 +45,7 @@ async function bootstrap() {
45
45
  db = globalConfig.db;
46
46
  }
47
47
 
48
- const { app, server, routes, reload, reloadLocale, shutdown } = await createServer(globalConfig, routesDir, db);
48
+ const { app, server, routes, reload, reloadLocale, reloadTasks, shutdown } = await createServer(globalConfig, routesDir, db);
49
49
  let currentRoutes = routes;
50
50
 
51
51
  server.listen(port, () => {
@@ -93,7 +93,7 @@ async function bootstrap() {
93
93
  if (isLocaleFile) {
94
94
  await reloadLocale();
95
95
  } else if (isTaskFile) {
96
- // Tasks reload
96
+ await reloadTasks();
97
97
  } else {
98
98
  currentRoutes = await reload();
99
99
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,31 +115,31 @@
115
115
  item.className = 'route-item';
116
116
 
117
117
  item.innerHTML = `
118
- <div class="route-header" onclick="toggleRoute(\${idx})">
119
- <div class="method-pill method-\${method}">\${method}</div>
120
- <div class="route-path" title="Click to expand/collapse">\${path}</div>
118
+ <div class="route-header" onclick="toggleRoute(${idx})">
119
+ <div class="method-pill method-${method}">${method}</div>
120
+ <div class="route-path" title="Click to expand/collapse">${path}</div>
121
121
  <div class="badges">
122
- <div class="badge \${r.auth !== 'public' ? 'active' : ''}">AUTH: \${r.auth}</div>
123
- <div class="badge \${r.rateLimit !== 'none' ? 'active' : ''}">RL: \${r.rateLimit}</div>
124
- <div class="badge \${r.cache !== 'none' ? 'active' : ''}">CACHE: \${r.cache}</div>
122
+ <div class="badge ${r.auth !== 'public' ? 'active' : ''}">AUTH: ${r.auth}</div>
123
+ <div class="badge ${r.rateLimit !== 'none' ? 'active' : ''}">RL: ${r.rateLimit}</div>
124
+ <div class="badge ${r.cache !== 'none' ? 'active' : ''}">CACHE: ${r.cache}</div>
125
125
  </div>
126
126
  </div>
127
- <div class="route-details" id="details-\${idx}">
127
+ <div class="route-details" id="details-${idx}">
128
128
  <div class="detail-row">
129
129
  <div class="detail-label">Body</div>
130
- <div class="detail-value">\${r.hasBodySchema ? 'Schema Validated' : 'Any'}</div>
130
+ <div class="detail-value">${r.hasBodySchema ? 'Schema Validated' : 'Any'}</div>
131
131
  </div>
132
132
  <div class="detail-row">
133
133
  <div class="detail-label">Query</div>
134
- <div class="detail-value">\${r.hasQuerySchema ? 'Schema Validated' : 'Any'}</div>
134
+ <div class="detail-value">${r.hasQuerySchema ? 'Schema Validated' : 'Any'}</div>
135
135
  </div>
136
136
  <div class="detail-row">
137
137
  <div class="detail-label">Params</div>
138
- <div class="detail-value">\${r.hasParamsSchema ? 'Schema Validated' : 'Any'}</div>
138
+ <div class="detail-value">${r.hasParamsSchema ? 'Schema Validated' : 'Any'}</div>
139
139
  </div>
140
140
  <div class="detail-row">
141
141
  <div class="detail-label">Response</div>
142
- <div class="detail-value">\${r.hasResponseSchema ? 'Schema Validated' : 'Any'}</div>
142
+ <div class="detail-value">${r.hasResponseSchema ? 'Schema Validated' : 'Any'}</div>
143
143
  </div>
144
144
  </div>
145
145
  `;
package/src/edge.js CHANGED
@@ -8,8 +8,8 @@ export async function hashIdentity(identity) {
8
8
  const hash = await crypto.subtle.digest('SHA-256', data);
9
9
  return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
10
10
  }
11
- export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${originalUrl}:${hashIdentity(identity)}`; }
12
- export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${method}:${originalUrl}:${locale}:${hashIdentity(identity)}`; }
11
+ export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${req.originalUrl || req.url}:${hashIdentity(identity)}`; }
12
+ export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${req.method}:${req.originalUrl || req.url}:${locale}:${hashIdentity(identity)}`; }
13
13
 
14
14
  export { z };
15
15
 
package/src/engine.js CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ import crypto from 'node:crypto';
3
+
2
4
  export function resolveIdentity(req, config = {}) {
3
5
  const headers = req.headers || {};
4
6
  let ip = req.ip || (req.socket && req.socket.remoteAddress) || 'anonymous';
@@ -246,6 +248,12 @@ export async function executeRequest(routeConfig, requestData, globalConfig, ctx
246
248
  const status = err.status || 500;
247
249
  const title = status === 500 ? 'Internal Server Error' : err.message;
248
250
  const isProd = process.env.NODE_ENV === 'production';
251
+
252
+ if (status >= 500) {
253
+ console.error(`[bro.js] Execution Error in route:`);
254
+ console.error(err.stack || err);
255
+ }
256
+
249
257
  return { status, headers: errorHeaders, body: formatErrorEnvelope(status, isProd && status >= 500 ? 'Internal Server Error' : title, isProd && status >= 500 ? null : err.details, reqId, requestData.originalUrl), error: status >= 500 ? err : undefined };
250
258
  }
251
259
  }
package/src/locale.js CHANGED
@@ -81,8 +81,15 @@ export async function loadLocale(directory, options = {}) {
81
81
 
82
82
  const messages = {};
83
83
  for (const file of files) {
84
- const module = await import(`${pathToFileURL(path.join(directory, file)).href}?update=${crypto.randomUUID()}`);
85
- const catalog = module.default || module.messages || module;
84
+ const fullPath = path.join(directory, file);
85
+ let catalog;
86
+ if (file.endsWith('.json')) {
87
+ catalog = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
88
+ } else {
89
+ const module = await import(`${pathToFileURL(fullPath).href}?update=${crypto.randomUUID()}`);
90
+ catalog = module.default || module.messages || module;
91
+ }
92
+
86
93
  if (catalog && typeof catalog === 'object') {
87
94
  messages[localeFromFilename(file)] = catalog;
88
95
  }
package/src/next.js CHANGED
@@ -6,8 +6,8 @@ import { createClient } from 'redis';
6
6
  import { createLogger } from './logger.js';
7
7
  import { executeRequest, RouteRegistry, resolveIdentity } from './engine.js';
8
8
  export function hashIdentity(identity) { return crypto.createHash('sha256').update(String(identity)).digest('hex'); }
9
- export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${originalUrl}:${hashIdentity(identity)}`; }
10
- export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${method}:${originalUrl}:${locale}:${hashIdentity(identity)}`; }
9
+ export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${req.originalUrl}:${hashIdentity(identity)}`; }
10
+ export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${req.method}:${req.originalUrl}:${locale}:${hashIdentity(identity)}`; }
11
11
 
12
12
  export { z };
13
13
 
package/src/server.js CHANGED
@@ -12,13 +12,13 @@ import { createAdapter } from '@socket.io/redis-adapter';
12
12
  import { apiReference } from '@scalar/express-api-reference';
13
13
  import { verifyJwt, signJwt } from './auth.js';
14
14
  import { loadLocale } from './locale.js';
15
- import { loadRoutes } from './router.js';
16
- import { executeRequest, resolveIdentity, RouteRegistry } from './engine.js';
17
-
18
- export function hashIdentity(identity) { return crypto.createHash('sha256').update(String(identity)).digest('hex'); }
19
- export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${originalUrl}:${hashIdentity(identity)}`; }
20
- export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${method}:${originalUrl}:${locale}:${hashIdentity(identity)}`; }
21
- import { createLogger } from './logger.js';
15
+ import { loadRoutes } from './router.js';
16
+ import { executeRequest, resolveIdentity, RouteRegistry } from './engine.js';
17
+
18
+ export function hashIdentity(identity) { return crypto.createHash('sha256').update(String(identity)).digest('hex'); }
19
+ export function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); return `bro:rate_limit:${prefix}:${req.originalUrl || req.url}:${hashIdentity(identity)}`; }
20
+ export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${req.method}:${req.originalUrl || req.url}:${locale}:${hashIdentity(identity)}`; }
21
+ import { createLogger } from './logger.js';
22
22
  import { PluginManager } from './plugins.js';
23
23
  import { scanTasks } from './tasks.js';
24
24
 
@@ -29,33 +29,33 @@ import { scanTasks } from './tasks.js';
29
29
  * @param {any} db - Initialized database instance.
30
30
  * @returns {Promise<{ app: import('express').Application, server: http.Server, routes: Array, reload: Function, reloadLocale: Function, io: import('socket.io').Server, shutdown: Function }>}
31
31
  */
32
- export async function createServer(globalConfig, routesDir, db) {
33
- if (db && typeof db.isReady !== 'function') {
34
- throw new Error('[bro.js] CRITICAL: In v3.0.0, the \'db\' passed to createServer MUST extend BaseDatabaseAdapter and implement isReady(), transaction(), healthCheck(), and shutdown().');
35
- }
36
- const globalLogger = createLogger(globalConfig.logger || { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug' });
32
+ export async function createServer(globalConfig, routesDir, db) {
33
+ if (db && typeof db.isReady !== 'function') {
34
+ throw new Error('[bro.js] CRITICAL: In v3.0.0, the \'db\' passed to createServer MUST extend BaseDatabaseAdapter and implement isReady(), transaction(), healthCheck(), and shutdown().');
35
+ }
36
+ const globalLogger = createLogger(globalConfig.logger || { level: process.env.NODE_ENV === 'production' ? 'info' : 'debug' });
37
37
 
38
38
  if (process.env.NODE_ENV === 'production' && (!globalConfig.jwtSecret || globalConfig.jwtSecret.length < 32 || ['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here'].includes(globalConfig.jwtSecret))) {
39
39
  throw new Error('CRITICAL SECURITY ERROR: You are running in production without a secure JWT secret! Provide a secret of at least 32 characters.');
40
40
  }
41
41
 
42
- const routeRegistry = new RouteRegistry();
43
- const pluginManager = new PluginManager();
44
- if (Array.isArray(globalConfig.plugins)) {
45
- for (const plugin of globalConfig.plugins) {
46
- pluginManager.register(plugin);
47
- }
48
- }
49
-
50
- const app = express();
51
- await pluginManager.runOnInit(globalConfig, app);
52
- if (db) {
53
- if (!await db.isReady()) {
54
- throw new Error('[bro.js] Database adapter failed readiness check during boot.');
55
- }
42
+ const routeRegistry = new RouteRegistry();
43
+ const pluginManager = new PluginManager();
44
+ if (Array.isArray(globalConfig.plugins)) {
45
+ for (const plugin of globalConfig.plugins) {
46
+ pluginManager.register(plugin);
47
+ }
48
+ }
49
+
50
+ const app = express();
51
+ await pluginManager.runOnInit(globalConfig, app);
52
+ if (db) {
53
+ if (!await db.isReady()) {
54
+ throw new Error('[bro.js] Database adapter failed readiness check during boot.');
55
+ }
56
56
  }
57
- const server = http.createServer(app);
58
- server.requestTimeout = globalConfig.server?.timeoutMs || 30000;
57
+ const server = http.createServer(app);
58
+ server.requestTimeout = globalConfig.server?.timeoutMs || 30000;
59
59
  server.headersTimeout = globalConfig.server?.headersTimeoutMs || 35000;
60
60
  const localeDirectory = globalConfig.locale?.directory || path.join(process.cwd(), 'locale');
61
61
  let locale = await loadLocale(localeDirectory, globalConfig.locale);
@@ -75,14 +75,14 @@ export async function createServer(globalConfig, routesDir, db) {
75
75
  }));
76
76
  }
77
77
 
78
- const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : false;
79
- if (corsConfig === true) {
80
- if (process.env.NODE_ENV === 'production') {
81
- throw new Error('CRITICAL SECURITY ERROR: Permissive CORS (cors: true) is forbidden in production in v3.0.0. Provide an explicit origin allowlist.');
82
- }
83
- app.use(cors());
84
- } else if (corsConfig) {
85
- app.use(cors(corsConfig));
78
+ const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : false;
79
+ if (corsConfig === true) {
80
+ if (process.env.NODE_ENV === 'production') {
81
+ throw new Error('CRITICAL SECURITY ERROR: Permissive CORS (cors: true) is forbidden in production in v3.0.0. Provide an explicit origin allowlist.');
82
+ }
83
+ app.use(cors());
84
+ } else if (corsConfig) {
85
+ app.use(cors(corsConfig));
86
86
  }
87
87
 
88
88
  app.use((req, res, next) => {
@@ -91,36 +91,36 @@ export async function createServer(globalConfig, routesDir, db) {
91
91
  next();
92
92
  });
93
93
 
94
- const sendError = (res, status, message, details = null, req) => {
95
- let finalMessage = message;
96
- let finalDetails = details;
97
- if (status >= 500 && process.env.NODE_ENV === 'production') {
98
- finalMessage = 'Internal Server Error';
99
- finalDetails = null;
100
- }
101
- const codeMap = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 429: 'TOO_MANY_REQUESTS' };
102
- const code = codeMap[status] || (status >= 500 ? 'INTERNAL_ERROR' : 'ERROR');
103
-
104
- // v3.0.0 ALWAYS uses RFC 9457 Problem Details
105
- const payload = {
106
- type: `https://brojs.dev/errors/${code.toLowerCase()}`,
107
- title: finalMessage,
108
- status,
109
- instance: req.originalUrl || req.url,
110
- requestId: req.id
111
- };
112
- if (finalDetails) {
113
- if (typeof finalDetails === 'string') payload.detail = finalDetails;
114
- else payload.errors = finalDetails;
115
- }
116
- return res.status(status).type('application/problem+json').json(payload);
94
+ const sendError = (res, status, message, details = null, req) => {
95
+ let finalMessage = message;
96
+ let finalDetails = details;
97
+ if (status >= 500 && process.env.NODE_ENV === 'production') {
98
+ finalMessage = 'Internal Server Error';
99
+ finalDetails = null;
100
+ }
101
+ const codeMap = { 400: 'BAD_REQUEST', 401: 'UNAUTHORIZED', 403: 'FORBIDDEN', 404: 'NOT_FOUND', 429: 'TOO_MANY_REQUESTS' };
102
+ const code = codeMap[status] || (status >= 500 ? 'INTERNAL_ERROR' : 'ERROR');
103
+
104
+ // v3.0.0 ALWAYS uses RFC 9457 Problem Details
105
+ const payload = {
106
+ type: `errors/${code.toLowerCase()}`,
107
+ title: finalMessage,
108
+ status,
109
+ instance: req.originalUrl || req.url,
110
+ requestId: req.id
111
+ };
112
+ if (finalDetails) {
113
+ if (typeof finalDetails === 'string') payload.detail = finalDetails;
114
+ else payload.errors = finalDetails;
115
+ }
116
+ return res.status(status).type('application/problem+json').json(payload);
117
117
  };
118
118
 
119
119
  const formatZodError = (error) => error.issues.map(i => ({ path: i.path.join('.'), message: i.message, code: i.code }));
120
120
 
121
-
122
-
123
- app.use(pluginManager.getRequestMiddleware());
121
+
122
+
123
+ app.use(pluginManager.getRequestMiddleware());
124
124
  app.use(express.json());
125
125
 
126
126
  const safeConnect = async (client) => {
@@ -239,55 +239,55 @@ export async function createServer(globalConfig, routesDir, db) {
239
239
  }
240
240
  }
241
241
 
242
- middlewares.push(async (req, res) => {
243
- const requestData = {
244
- method: req.method,
245
- originalUrl: req.originalUrl,
246
- headers: req.headers,
247
- body: req.body,
248
- query: req.query,
249
- params: req.params,
250
- files: req.files || req.file,
251
- ip: req.ip,
252
- locale: locale.resolveLocale(req)
253
- };
254
-
255
- const ctxExtras = {
256
- verifyJwt,
257
- generateRateLimitKey,
258
- generateCacheKey,
259
- generateId: () => crypto.randomUUID(),
260
- req,
261
- res,
262
- db,
263
- redis: redisClient,
264
- io,
265
- pluginManager,
266
- t: (key, values) => locale.translate(requestData.locale, key, values),
267
- logger: globalLogger,
268
- jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
269
- error: (status, message) => {
270
- const err = new Error(message);
271
- err.status = status;
272
- throw err;
273
- },
274
- fixtures: globalConfig.fixtures || {},
275
- stores: globalConfig.stores || {}
276
- };
277
-
278
- const response = await executeRequest(routeConfig, requestData, globalConfig, ctxExtras);
279
-
280
- if (response.headers && !res.headersSent) {
281
- for (const [k, v] of Object.entries(response.headers)) {
282
- res.setHeader(k, v);
283
- }
284
- }
285
-
286
- if (!res.headersSent) {
287
- res.status(response.status).json(response.body);
288
- }
289
- });
290
-
242
+ middlewares.push(async (req, res) => {
243
+ const requestData = {
244
+ method: req.method,
245
+ originalUrl: req.originalUrl,
246
+ headers: req.headers,
247
+ body: req.body,
248
+ query: req.query,
249
+ params: req.params,
250
+ files: req.files || req.file,
251
+ ip: req.ip,
252
+ locale: locale.resolveLocale(req)
253
+ };
254
+
255
+ const ctxExtras = {
256
+ verifyJwt,
257
+ generateRateLimitKey,
258
+ generateCacheKey,
259
+ generateId: () => crypto.randomUUID(),
260
+ req,
261
+ res,
262
+ db,
263
+ redis: redisClient,
264
+ io,
265
+ pluginManager,
266
+ t: (key, values) => locale.translate(requestData.locale, key, values),
267
+ logger: globalLogger,
268
+ jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
269
+ error: (status, message) => {
270
+ const err = new Error(message);
271
+ err.status = status;
272
+ throw err;
273
+ },
274
+ fixtures: globalConfig.fixtures || {},
275
+ stores: globalConfig.stores || {}
276
+ };
277
+
278
+ const response = await executeRequest(routeConfig, requestData, globalConfig, ctxExtras);
279
+
280
+ if (response.headers && !res.headersSent) {
281
+ for (const [k, v] of Object.entries(response.headers)) {
282
+ res.setHeader(k, v);
283
+ }
284
+ }
285
+
286
+ if (!res.headersSent) {
287
+ res.status(response.status).json(response.body);
288
+ }
289
+ });
290
+
291
291
  return middlewares;
292
292
  };
293
293
 
@@ -339,52 +339,52 @@ export async function createServer(globalConfig, routesDir, db) {
339
339
  app.use('/docs', docsAuthMiddleware, apiReference({ spec: { url: '/docs/json' } }));
340
340
  }
341
341
 
342
- if (globalConfig.health) {
343
- app.get('/health/live', (req, res) => {
344
- res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
345
- });
346
-
347
- app.get('/health/ready', async (req, res) => {
348
- let isReady = true;
349
- const checks = {};
350
-
351
- const timeoutPromise = (ms, promise) => {
352
- return new Promise((resolve, reject) => {
353
- const timer = setTimeout(() => reject(new Error('timeout')), ms);
354
- promise.then(val => { clearTimeout(timer); resolve(val); }).catch(err => { clearTimeout(timer); reject(err); });
355
- });
356
- };
357
-
358
- if (redisClient) {
359
- try {
360
- await timeoutPromise(2000, redisClient.ping());
361
- checks.redis = 'up';
362
- } catch (err) {
363
- checks.redis = 'down';
364
- isReady = false;
365
- }
366
- }
367
-
368
- if (globalConfig.health === true || typeof globalConfig.health.dbCheck !== 'function') {
369
- if (db) checks.db = 'unknown (provide health.dbCheck)';
370
- } else if (db) {
371
- try {
372
- await timeoutPromise(2000, globalConfig.health.dbCheck(db));
373
- checks.db = 'up';
374
- } catch (err) {
375
- checks.db = 'down';
376
- isReady = false;
377
- }
378
- }
379
-
380
- res.status(isReady ? 200 : 503).json({
381
- status: isReady ? 'ready' : 'unavailable',
382
- checks,
383
- timestamp: new Date().toISOString()
384
- });
385
- });
386
- }
387
-
342
+ if (globalConfig.health) {
343
+ app.get('/health/live', (req, res) => {
344
+ res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
345
+ });
346
+
347
+ app.get('/health/ready', async (req, res) => {
348
+ let isReady = true;
349
+ const checks = {};
350
+
351
+ const timeoutPromise = (ms, promise) => {
352
+ return new Promise((resolve, reject) => {
353
+ const timer = setTimeout(() => reject(new Error('timeout')), ms);
354
+ promise.then(val => { clearTimeout(timer); resolve(val); }).catch(err => { clearTimeout(timer); reject(err); });
355
+ });
356
+ };
357
+
358
+ if (redisClient) {
359
+ try {
360
+ await timeoutPromise(2000, redisClient.ping());
361
+ checks.redis = 'up';
362
+ } catch (err) {
363
+ checks.redis = 'down';
364
+ isReady = false;
365
+ }
366
+ }
367
+
368
+ if (globalConfig.health === true || typeof globalConfig.health.dbCheck !== 'function') {
369
+ if (db) checks.db = 'unknown (provide health.dbCheck)';
370
+ } else if (db) {
371
+ try {
372
+ await timeoutPromise(2000, globalConfig.health.dbCheck(db));
373
+ checks.db = 'up';
374
+ } catch (err) {
375
+ checks.db = 'down';
376
+ isReady = false;
377
+ }
378
+ }
379
+
380
+ res.status(isReady ? 200 : 503).json({
381
+ status: isReady ? 'ready' : 'unavailable',
382
+ checks,
383
+ timestamp: new Date().toISOString()
384
+ });
385
+ });
386
+ }
387
+
388
388
  let routeStack = express.Router();
389
389
 
390
390
  app.use((req, res, next) => {
@@ -395,7 +395,7 @@ export async function createServer(globalConfig, routesDir, db) {
395
395
  sendError(res, 404, 'Not Found', null, req);
396
396
  });
397
397
 
398
- app.use(pluginManager.getErrorMiddleware());
398
+ app.use(pluginManager.getErrorMiddleware());
399
399
  app.use((err, req, res, next) => {
400
400
  console.error(`[bro.js] Uncaught Error:`, err);
401
401
  sendError(res, err.status || 500, err.message || 'Internal Server Error', null, req);
@@ -420,7 +420,7 @@ export async function createServer(globalConfig, routesDir, db) {
420
420
  let taskManager = await scanTasks({ db, io });
421
421
 
422
422
  const reloadTasks = async () => {
423
- if (taskManager) taskManager.stopAll();
423
+ if (taskManager) taskManager.stopAll();
424
424
  await pluginManager.runOnShutdown();
425
425
  taskManager = await scanTasks({ db, io });
426
426
  };
package/src/tasks.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
+ import crypto from 'node:crypto';
4
5
  import cron from 'node-cron';
5
6
  import { colors } from './logger.js';
6
7
  import { scanDir } from './router.js';
@@ -27,7 +28,7 @@ export async function scanTasks(ctx) {
27
28
  let count = 0;
28
29
  for (const file of files) {
29
30
  try {
30
- const moduleUrl = pathToFileURL(file).href;
31
+ const moduleUrl = `${pathToFileURL(file).href}?update=${crypto.randomUUID()}`;
31
32
  const taskModule = await import(moduleUrl);
32
33
 
33
34
  if (taskModule.cron && typeof taskModule.handler === 'function') {