bro-framework 3.0.3 → 3.0.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.
- package/package.json +1 -1
- package/src/edge.js +2 -2
- package/src/engine.js +9 -1
- package/src/next.js +2 -2
- package/src/server.js +159 -159
package/package.json
CHANGED
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}:${
|
|
12
|
-
export function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); return `bro:cache:${method}:${originalUrl}:${locale}:${
|
|
11
|
+
export async function generateRateLimitKey(req, config, prefix = 'route') { const identity = resolveIdentity(req, config); const hash = await hashIdentity(identity); return `bro:rate_limit:${prefix}:${req.originalUrl || req.url}:${hash}`; }
|
|
12
|
+
export async function generateCacheKey(req, config, locale) { const identity = resolveIdentity(req, config); const hash = await hashIdentity(identity); return `bro:cache:${req.method}:${req.originalUrl || req.url}:${locale}:${hash}`; }
|
|
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';
|
|
@@ -116,7 +118,7 @@ export async function executeRequest(routeConfig, requestData, globalConfig, ctx
|
|
|
116
118
|
const redisClient = ctxExtras.redis;
|
|
117
119
|
|
|
118
120
|
if (activeRateLimit && redisClient) {
|
|
119
|
-
const rlKey = generateRateLimitKey ? await generateRateLimitKey(requestData, globalConfig) : `bro:rl:${requestData.originalUrl}:${requestData.ip}`;
|
|
121
|
+
const rlKey = ctxExtras.generateRateLimitKey ? await ctxExtras.generateRateLimitKey(requestData, globalConfig) : `bro:rl:${requestData.originalUrl}:${requestData.ip}`;
|
|
120
122
|
try {
|
|
121
123
|
const current = await redisClient.incr(rlKey);
|
|
122
124
|
const windowSeconds = Math.floor(activeRateLimit.windowMs / 1000);
|
|
@@ -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/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: `
|
|
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
|
};
|