bro-framework 2.0.1 → 2.1.0

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
@@ -182,6 +182,13 @@ async function bootstrap() {
182
182
  console.error("");
183
183
  process.exit(1);
184
184
  }
185
+ globalConfig.envData = envResult.data;
186
+ }
187
+
188
+ if (process.env.NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key'].includes(globalConfig.jwtSecret)) {
189
+ console.error(`\n ✗ CRITICAL SECURITY ERROR: You are running in production with a default JWT secret!`);
190
+ console.error(` Please set auth.jwtSecret in bro.config.js or via JWT_SECRET environment variable.`);
191
+ process.exit(1);
185
192
  }
186
193
 
187
194
  if (!fs.existsSync(routesDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bro-framework",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "The No-BS Backend Framework for Node.js",
5
5
  "repository": {
6
6
  "type": "git",
package/src/auth.js CHANGED
@@ -1,31 +1,23 @@
1
1
  import jwt from 'jsonwebtoken';
2
2
 
3
- let secret = 'bro_default_secret_key';
4
-
5
- /**
6
- * Update the secret used for signing and verifying JWTs.
7
- * @param {string} newSecret
8
- */
9
- export function setJwtSecret(newSecret) {
10
- secret = newSecret;
11
- }
12
-
13
3
  /**
14
4
  * Signs a JWT payload.
15
5
  * @param {Object} payload - The data to embed in the token.
6
+ * @param {string} secret - The JWT secret key.
16
7
  * @param {jwt.SignOptions} [options] - jsonwebtoken sign options.
17
8
  * @returns {string} The signed JWT token.
18
9
  */
19
- export function signJwt(payload, options = { expiresIn: '1d' }) {
10
+ export function signJwt(payload, secret, options = { expiresIn: '1d' }) {
20
11
  return jwt.sign(payload, secret, options);
21
12
  }
22
13
 
23
14
  /**
24
15
  * Verifies and decodes a JWT token.
25
16
  * @param {string} token - The JWT token to verify.
17
+ * @param {string} secret - The JWT secret key.
26
18
  * @returns {{ valid: boolean, payload?: any, error?: string }}
27
19
  */
28
- export function verifyJwt(token) {
20
+ export function verifyJwt(token, secret) {
29
21
  try {
30
22
  const payload = jwt.verify(token, secret);
31
23
  return { valid: true, payload };
package/src/router.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 'crypto';
4
5
  import { zodToJsonSchema } from 'zod-to-json-schema';
5
6
 
6
7
  /**
@@ -48,10 +49,11 @@ export function parseRouteFile(filePath, routesDir) {
48
49
  if (routePath === '/.') routePath = '';
49
50
 
50
51
  if (namePart !== 'index') {
51
- const formattedName = namePart.replace(/\[(.*?)\]/g, ':$1');
52
- routePath += `/${formattedName}`;
52
+ routePath += `/${namePart}`;
53
53
  }
54
54
 
55
+ routePath = routePath.replace(/\[(.*?)\]/g, ':$1');
56
+
55
57
  if (routePath === '') routePath = '/';
56
58
 
57
59
  return { routePath, method };
@@ -68,6 +70,7 @@ export function parseRouteFile(filePath, routesDir) {
68
70
  export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
69
71
  const files = scanDir(routesDir);
70
72
  const loadedRoutes = [];
73
+ const routeModules = [];
71
74
 
72
75
  for (const file of files) {
73
76
  const routeInfo = parseRouteFile(file, routesDir);
@@ -77,68 +80,76 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
77
80
 
78
81
  if (typeof app[method] !== 'function') continue;
79
82
 
80
- try {
81
- const moduleUrl = pathToFileURL(file).href + '?update=' + Date.now();
82
- const module = await import(moduleUrl);
83
- const config = module.default;
84
-
85
- if (!config) continue;
83
+ const moduleUrl = pathToFileURL(file).href + '?update=' + crypto.randomUUID();
84
+ const module = await import(moduleUrl);
85
+ const config = module.default;
86
+
87
+ if (!config) continue;
88
+
89
+ routeModules.push({ file, routePath, method, config });
90
+ }
91
+
92
+ for (const { file, routePath, method, config } of routeModules) {
93
+ const handler = createHandler(config);
94
+ app[method](routePath, handler);
95
+
96
+ if (openApiSpec) {
97
+ const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
98
+ if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
86
99
 
87
- const handler = createHandler(config);
88
- app[method](routePath, handler);
100
+ const operation = {
101
+ summary: config.summary || `${method.toUpperCase()} ${routePath}`,
102
+ responses: { '200': { description: 'Successful response' } }
103
+ };
89
104
 
90
- if (openApiSpec) {
91
- const openApiPath = routePath.replace(/:([a-zA-Z0-9_]+)/g, '{$1}');
92
- if (!openApiSpec.paths[openApiPath]) openApiSpec.paths[openApiPath] = {};
93
-
94
- const operation = {
95
- summary: config.summary || `${method.toUpperCase()} ${routePath}`,
96
- responses: { '200': { description: 'Successful response' } }
105
+ const bodySchema = config.schema?.body || config.body;
106
+ const paramsSchema = config.schema?.params || config.params;
107
+ const querySchema = config.schema?.query || config.query;
108
+
109
+ if (bodySchema) {
110
+ operation.requestBody = {
111
+ content: { 'application/json': { schema: zodToJsonSchema(bodySchema) } }
97
112
  };
98
-
99
- if (config.body) {
100
- operation.requestBody = {
101
- content: { 'application/json': { schema: zodToJsonSchema(config.body) } }
102
- };
103
- }
104
-
105
- if (config.params) {
106
- operation.parameters = operation.parameters || [];
107
- const pSchema = zodToJsonSchema(config.params);
108
- if (pSchema.properties) {
109
- for (const [key, schema] of Object.entries(pSchema.properties)) {
110
- operation.parameters.push({ name: key, in: 'path', required: true, schema });
111
- }
113
+ }
114
+
115
+ if (paramsSchema) {
116
+ operation.parameters = operation.parameters || [];
117
+ const pSchema = zodToJsonSchema(paramsSchema);
118
+ if (pSchema.properties) {
119
+ for (const [key, schema] of Object.entries(pSchema.properties)) {
120
+ operation.parameters.push({ name: key, in: 'path', required: true, schema });
112
121
  }
113
122
  }
123
+ }
114
124
 
115
- if (config.query) {
116
- operation.parameters = operation.parameters || [];
117
- const qSchema = zodToJsonSchema(config.query);
118
- if (qSchema.properties) {
119
- for (const [key, schema] of Object.entries(qSchema.properties)) {
120
- operation.parameters.push({
121
- name: key,
122
- in: 'query',
123
- required: qSchema.required?.includes(key),
124
- schema
125
- });
126
- }
125
+ if (querySchema) {
126
+ operation.parameters = operation.parameters || [];
127
+ const qSchema = zodToJsonSchema(querySchema);
128
+ if (qSchema.properties) {
129
+ for (const [key, schema] of Object.entries(qSchema.properties)) {
130
+ operation.parameters.push({
131
+ name: key,
132
+ in: 'query',
133
+ required: qSchema.required?.includes(key),
134
+ schema
135
+ });
127
136
  }
128
137
  }
129
-
130
- openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
131
138
  }
132
139
 
133
- loadedRoutes.push({
134
- method: method.toUpperCase(),
135
- path: routePath,
136
- auth: !!config.auth
137
- });
140
+ // Auto-inject security definition if auth is true
141
+ if (config.auth) {
142
+ operation.security = [{ bearerAuth: [] }];
143
+ }
138
144
 
139
- } catch (err) {
140
- console.error(`[bro.js] Failed to load route ${file}:`, err);
145
+ openApiSpec.paths[openApiPath][method.toLowerCase()] = operation;
141
146
  }
147
+
148
+ loadedRoutes.push({
149
+ method: method.toUpperCase(),
150
+ path: routePath,
151
+ auth: !!config.auth
152
+ });
142
153
  }
143
154
 
144
155
  return loadedRoutes;
package/src/sdk.js CHANGED
@@ -44,7 +44,10 @@ async function request(method, path, data) {
44
44
  options.body = JSON.stringify(data);
45
45
  } else if (data && ['GET', 'DELETE'].includes(method.toUpperCase())) {
46
46
  const params = new URLSearchParams(data);
47
- path += '?' + params.toString();
47
+ const qs = params.toString();
48
+ if (qs) {
49
+ path += '?' + qs;
50
+ }
48
51
  }
49
52
 
50
53
  const url = CONFIG.baseURL + path;
@@ -115,10 +118,10 @@ function generateApiObject(endpoints) {
115
118
 
116
119
  for (const [key, val] of Object.entries(node)) {
117
120
  if (val._isParam) {
118
- result += `${indent}${key}: (${key}) => ({\n`;
121
+ result += `${indent}"${key}": (${key}) => ({\n`;
119
122
 
120
123
  for (const [m, p] of Object.entries(val._methods)) {
121
- const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${$1}');
124
+ const templatedPath = p.replace(/:([a-zA-Z0-9_]+)/g, '${encodeURIComponent($1)}');
122
125
  result += `${indent} ${m}: (data) => request('${m}', \`${templatedPath}\`, data),\n`;
123
126
  }
124
127
 
@@ -129,7 +132,7 @@ function generateApiObject(endpoints) {
129
132
 
130
133
  result += `${indent}}),\n`;
131
134
  } else {
132
- result += `${indent}${key}: {\n`;
135
+ result += `${indent}"${key}": {\n`;
133
136
  for (const [m, p] of Object.entries(val._methods)) {
134
137
  result += `${indent} ${m}: (data) => request('${m}', '${p}', data),\n`;
135
138
  }
package/src/server.js CHANGED
@@ -5,11 +5,9 @@ import { Server } from 'socket.io';
5
5
  import rateLimit from 'express-rate-limit';
6
6
  import multer from 'multer';
7
7
  import { apiReference } from '@scalar/express-api-reference';
8
- import { verifyJwt, signJwt, setJwtSecret } from './auth.js';
8
+ import { verifyJwt, signJwt } from './auth.js';
9
9
  import { loadRoutes } from './router.js';
10
10
 
11
- const upload = multer();
12
-
13
11
  /**
14
12
  * Creates and configures the core Express server.
15
13
  * @param {Object} globalConfig - User's bro.config.js configurations.
@@ -23,7 +21,10 @@ export async function createServer(globalConfig, routesDir, db) {
23
21
 
24
22
  const corsConfig = globalConfig.server?.cors !== undefined ? globalConfig.server.cors : true;
25
23
 
26
- app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
24
+ if (corsConfig !== false) {
25
+ app.use(cors(typeof corsConfig === 'object' ? corsConfig : {}));
26
+ }
27
+
27
28
  app.use(express.json());
28
29
 
29
30
  if (globalConfig.rateLimit) {
@@ -36,10 +37,6 @@ export async function createServer(globalConfig, routesDir, db) {
36
37
  await globalConfig.sockets(io, db);
37
38
  }
38
39
 
39
- if (globalConfig.jwtSecret) {
40
- setJwtSecret(globalConfig.jwtSecret);
41
- }
42
-
43
40
  const createHandler = (routeConfig) => {
44
41
  const middlewares = [];
45
42
 
@@ -48,12 +45,23 @@ export async function createServer(globalConfig, routesDir, db) {
48
45
  }
49
46
 
50
47
  if (routeConfig.upload) {
51
- middlewares.push(upload.any());
48
+ const routeMulterConfig = {
49
+ limits: globalConfig.upload?.limits || {
50
+ fileSize: 10 * 1024 * 1024,
51
+ files: 5,
52
+ fields: 20
53
+ }
54
+ };
55
+ if (typeof routeConfig.upload === 'object' && routeConfig.upload.limits) {
56
+ routeMulterConfig.limits = { ...routeMulterConfig.limits, ...routeConfig.upload.limits };
57
+ }
58
+ middlewares.push(multer(routeMulterConfig).any());
52
59
  }
53
60
 
54
61
  middlewares.push(async (req, res) => {
55
62
  try {
56
63
  const ctx = {
64
+ env: globalConfig.envData || process.env,
57
65
  db,
58
66
  io,
59
67
  body: req.body,
@@ -61,7 +69,7 @@ export async function createServer(globalConfig, routesDir, db) {
61
69
  query: req.query,
62
70
  files: req.files || req.file,
63
71
  user: null,
64
- jwt: { sign: signJwt },
72
+ jwt: { sign: (payload, opts) => signJwt(payload, globalConfig.jwtSecret, opts || { expiresIn: globalConfig.auth?.expiresIn || '1d' }) },
65
73
  error: (status, message) => {
66
74
  const err = new Error(message);
67
75
  err.status = status;
@@ -76,7 +84,7 @@ export async function createServer(globalConfig, routesDir, db) {
76
84
  }
77
85
 
78
86
  const token = authHeader.split(' ')[1];
79
- const authResult = verifyJwt(token);
87
+ const authResult = verifyJwt(token, globalConfig.jwtSecret);
80
88
 
81
89
  if (!authResult.valid) {
82
90
  return res.status(401).json({ error: 'Unauthorized', details: authResult.error });
@@ -143,6 +151,21 @@ export async function createServer(globalConfig, routesDir, db) {
143
151
  let openApiSpec = {
144
152
  openapi: '3.0.0',
145
153
  info: { title: 'bro.js API', version: '1.0.0' },
154
+ components: {
155
+ securitySchemes: {
156
+ bearerAuth: {
157
+ type: 'http',
158
+ scheme: 'bearer',
159
+ bearerFormat: 'JWT'
160
+ }
161
+ },
162
+ responses: {
163
+ BadRequest: { description: 'Bad Request', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
164
+ Unauthorized: { description: 'Unauthorized', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
165
+ NotFound: { description: 'Not Found', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } },
166
+ ServerError: { description: 'Internal Server Error', content: { 'application/json': { schema: { type: 'object', properties: { error: { type: 'string' } } } } } }
167
+ }
168
+ },
146
169
  paths: {}
147
170
  };
148
171
 
@@ -174,6 +197,15 @@ export async function createServer(globalConfig, routesDir, db) {
174
197
  routeStack(req, res, next);
175
198
  });
176
199
 
200
+ app.use((req, res) => {
201
+ res.status(404).json({ error: 'Not Found' });
202
+ });
203
+
204
+ app.use((err, req, res, next) => {
205
+ console.error(`[bro.js] Uncaught Error:`, err);
206
+ res.status(err.status || 500).json({ error: err.message || 'Internal Server Error' });
207
+ });
208
+
177
209
  const reload = async () => {
178
210
  const newRouter = express.Router();
179
211
  openApiSpec.paths = {};
package/src/tasks.js CHANGED
@@ -3,29 +3,39 @@ import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
4
  import cron from 'node-cron';
5
5
  import { colors } from './logger.js';
6
+ import { scanDir } from './router.js';
7
+
8
+ let taskHandles = [];
9
+
10
+ export function stopTasks() {
11
+ taskHandles.forEach(t => t.stop());
12
+ taskHandles = [];
13
+ }
6
14
 
7
15
  export async function scanTasks(ctx) {
8
16
  const tasksDir = path.join(process.cwd(), 'tasks');
9
17
  if (!fs.existsSync(tasksDir)) return;
10
18
 
11
- const files = fs.readdirSync(tasksDir).filter(f => f.endsWith('.js'));
19
+ stopTasks();
20
+
21
+ const files = scanDir(tasksDir);
12
22
  if (files.length === 0) return;
13
23
 
14
24
  let count = 0;
15
25
  for (const file of files) {
16
- const filePath = path.join(tasksDir, file);
17
26
  try {
18
- const moduleUrl = pathToFileURL(filePath).href;
27
+ const moduleUrl = pathToFileURL(file).href;
19
28
  const taskModule = await import(moduleUrl);
20
29
 
21
30
  if (taskModule.cron && typeof taskModule.handler === 'function') {
22
- cron.schedule(taskModule.cron, async () => {
31
+ const task = cron.schedule(taskModule.cron, async () => {
23
32
  try {
24
33
  await taskModule.handler(ctx);
25
34
  } catch (err) {
26
35
  console.error(`\n ${colors.red}❌ Task Error (${file}):${colors.reset}`, err);
27
36
  }
28
37
  });
38
+ taskHandles.push(task);
29
39
  count++;
30
40
  }
31
41
  } catch (err) {