bro-framework 2.4.4 → 3.0.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/README.md +44 -0
- package/bin/bro.js +194 -292
- package/package.json +138 -103
- package/src/dashboard.js +80 -0
- package/src/database.js +105 -0
- package/src/edge.js +244 -0
- package/src/engine.js +245 -0
- package/src/index.d.ts +31 -11
- package/src/index.js +10 -0
- package/src/logger.js +47 -0
- package/src/next.d.ts +9 -9
- package/src/next.js +91 -129
- package/src/observability.js +81 -0
- package/src/plugins.js +135 -0
- package/src/policy-auth.js +88 -0
- package/src/router.js +1 -1
- package/src/sdk.js +229 -169
- package/src/server.js +181 -170
- package/src/studio.js +162 -0
- package/src/task-engine.js +88 -0
- package/src/testing.js +142 -0
- package/src/uploads.js +129 -0
package/package.json
CHANGED
|
@@ -1,103 +1,138 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "bro-framework",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "The No-BS Backend Framework for Node.js",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/medyass1ne/bro.js"
|
|
8
|
-
},
|
|
9
|
-
"homepage": "https://brojs.yessindevs.me",
|
|
10
|
-
"author": "Yessindevs",
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"main": "src/index.js",
|
|
13
|
-
"types": "src/index.d.ts",
|
|
14
|
-
"type": "module",
|
|
15
|
-
"exports": {
|
|
16
|
-
".": {
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "bro-framework",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "The No-BS Backend Framework for Node.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/medyass1ne/bro.js"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://brojs.yessindevs.me",
|
|
10
|
+
"author": "Yessindevs",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "src/index.js",
|
|
13
|
+
"types": "src/index.d.ts",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"node": {
|
|
18
|
+
"types": "./src/index.d.ts",
|
|
19
|
+
"import": "./src/index.js",
|
|
20
|
+
"require": "./src/index.js"
|
|
21
|
+
},
|
|
22
|
+
"edge": {
|
|
23
|
+
"types": "./src/next.d.ts",
|
|
24
|
+
"import": "./src/edge.js"
|
|
25
|
+
},
|
|
26
|
+
"workerd": {
|
|
27
|
+
"types": "./src/next.d.ts",
|
|
28
|
+
"import": "./src/edge.js"
|
|
29
|
+
},
|
|
30
|
+
"default": {
|
|
31
|
+
"types": "./src/index.d.ts",
|
|
32
|
+
"import": "./src/index.js",
|
|
33
|
+
"require": "./src/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./next": {
|
|
37
|
+
"types": "./src/next.d.ts",
|
|
38
|
+
"import": "./src/next.js",
|
|
39
|
+
"require": "./src/next.js"
|
|
40
|
+
},
|
|
41
|
+
"./engine": {
|
|
42
|
+
"import": "./src/engine.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json",
|
|
45
|
+
"./edge": {
|
|
46
|
+
"types": "./src/next.d.ts",
|
|
47
|
+
"import": "./src/edge.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"bin": {
|
|
51
|
+
"bro": "./bin/bro.js"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"src",
|
|
55
|
+
"bin"
|
|
56
|
+
],
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@scalar/express-api-reference": "^0.10.18",
|
|
59
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
60
|
+
"chokidar": "^5.0.0",
|
|
61
|
+
"cors": "^2.8.6",
|
|
62
|
+
"dotenv": "^16.4.5",
|
|
63
|
+
"express": "^4.21.1",
|
|
64
|
+
"express-rate-limit": "^8.7.0",
|
|
65
|
+
"helmet": "^8.3.0",
|
|
66
|
+
"jsonwebtoken": "^9.0.2",
|
|
67
|
+
"multer": "^2.3.0",
|
|
68
|
+
"node-cron": "^4.6.0",
|
|
69
|
+
"redis": "^6.2.1",
|
|
70
|
+
"socket.io": "^4.8.3",
|
|
71
|
+
"tsx": "^4.23.13",
|
|
72
|
+
"zod": "^3.23.8",
|
|
73
|
+
"zod-to-json-schema": "^3.25.2"
|
|
74
|
+
},
|
|
75
|
+
"keywords": [
|
|
76
|
+
"bro",
|
|
77
|
+
"brojs",
|
|
78
|
+
"bro.js",
|
|
79
|
+
"bro-framework",
|
|
80
|
+
"framework",
|
|
81
|
+
"backend",
|
|
82
|
+
"api",
|
|
83
|
+
"server",
|
|
84
|
+
"node",
|
|
85
|
+
"nodejs",
|
|
86
|
+
"express",
|
|
87
|
+
"rest",
|
|
88
|
+
"zero-config",
|
|
89
|
+
"zero-boilerplate",
|
|
90
|
+
"file-based-routing",
|
|
91
|
+
"auto-routing",
|
|
92
|
+
"zod",
|
|
93
|
+
"validation",
|
|
94
|
+
"jwt",
|
|
95
|
+
"authentication",
|
|
96
|
+
"auth",
|
|
97
|
+
"websocket",
|
|
98
|
+
"websockets",
|
|
99
|
+
"socket.io",
|
|
100
|
+
"realtime",
|
|
101
|
+
"openapi",
|
|
102
|
+
"swagger",
|
|
103
|
+
"scalar",
|
|
104
|
+
"docs",
|
|
105
|
+
"auto-docs",
|
|
106
|
+
"sdk",
|
|
107
|
+
"sdk-generator",
|
|
108
|
+
"rpc",
|
|
109
|
+
"trpc-alternative",
|
|
110
|
+
"express-alternative",
|
|
111
|
+
"cron",
|
|
112
|
+
"scheduler",
|
|
113
|
+
"background-tasks",
|
|
114
|
+
"file-upload",
|
|
115
|
+
"rate-limit",
|
|
116
|
+
"esm",
|
|
117
|
+
"ai-friendly",
|
|
118
|
+
"developer-experience",
|
|
119
|
+
"dx"
|
|
120
|
+
],
|
|
121
|
+
"devDependencies": {
|
|
122
|
+
"@types/node": "^26.6.2",
|
|
123
|
+
"ioredis-mock": "^8.13.1",
|
|
124
|
+
"next": "^16.3.5",
|
|
125
|
+
"typescript": "^7.0.2"
|
|
126
|
+
},
|
|
127
|
+
"engines": {
|
|
128
|
+
"node": ">=18.0.0"
|
|
129
|
+
},
|
|
130
|
+
"peerDependencies": {
|
|
131
|
+
"next": ">=13.0.0"
|
|
132
|
+
},
|
|
133
|
+
"peerDependenciesMeta": {
|
|
134
|
+
"next": {
|
|
135
|
+
"optional": true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/dashboard.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Local Developer Dashboard Server
|
|
7
|
+
* Serves a dashboard UI that visualizes routes, telemetry, and configuration.
|
|
8
|
+
*/
|
|
9
|
+
export function startDashboard(globalConfig, routes) {
|
|
10
|
+
const host = '127.0.0.1'; // Strict local binding
|
|
11
|
+
const port = (globalConfig.port || 5000) + 1;
|
|
12
|
+
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
// Restrict access to localhost strictly
|
|
15
|
+
if (req.socket.remoteAddress !== '127.0.0.1' && req.socket.remoteAddress !== '::1') {
|
|
16
|
+
res.writeHead(403);
|
|
17
|
+
res.end('Forbidden: Dashboard only available on localhost');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (req.url === '/') {
|
|
22
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
23
|
+
res.end(`
|
|
24
|
+
<!DOCTYPE html>
|
|
25
|
+
<html>
|
|
26
|
+
<head>
|
|
27
|
+
<title>bro.js Dev Dashboard</title>
|
|
28
|
+
<style>
|
|
29
|
+
body { font-family: system-ui, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
|
|
30
|
+
h1 { color: #38bdf8; }
|
|
31
|
+
.card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; }
|
|
32
|
+
pre { background: #0f172a; padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<h1>bro.js Dev Dashboard</h1>
|
|
37
|
+
|
|
38
|
+
<div class="card">
|
|
39
|
+
<h2>Configuration (Redacted)</h2>
|
|
40
|
+
<pre id="config"></pre>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="card">
|
|
44
|
+
<h2>Active Routes</h2>
|
|
45
|
+
<pre id="routes"></pre>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<script>
|
|
49
|
+
fetch('/api/state').then(r => r.json()).then(data => {
|
|
50
|
+
document.getElementById('config').textContent = JSON.stringify(data.config, null, 2);
|
|
51
|
+
document.getElementById('routes').textContent = JSON.stringify(data.routes, null, 2);
|
|
52
|
+
});
|
|
53
|
+
</script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
`);
|
|
57
|
+
} else if (req.url === '/api/state') {
|
|
58
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
59
|
+
|
|
60
|
+
// Strict Redaction of Secrets
|
|
61
|
+
const redactedConfig = JSON.parse(JSON.stringify(globalConfig));
|
|
62
|
+
if (redactedConfig.auth?.jwtSecret) redactedConfig.auth.jwtSecret = '***REDACTED***';
|
|
63
|
+
if (redactedConfig.db) redactedConfig.db = '[Database Instance]';
|
|
64
|
+
|
|
65
|
+
res.end(JSON.stringify({
|
|
66
|
+
config: redactedConfig,
|
|
67
|
+
routes: routes.map(r => ({ method: r.method, path: r.routePath, auth: r.auth }))
|
|
68
|
+
}));
|
|
69
|
+
} else {
|
|
70
|
+
res.writeHead(404);
|
|
71
|
+
res.end('Not found');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
server.listen(port, host, () => {
|
|
76
|
+
console.log(`[bro.js] 🛠️ Dev Dashboard running locally at http://${host}:${port}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return server;
|
|
80
|
+
}
|
package/src/database.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Database Adapter Contract for bro.js
|
|
3
|
+
* Standardizes lifecycle, health checks, and transactions across different DB providers.
|
|
4
|
+
*/
|
|
5
|
+
export class BaseDatabaseAdapter {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
this.pool = null;
|
|
9
|
+
this.isConnected = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Initializes the connection pool.
|
|
14
|
+
* Expected to set this.pool and this.isConnected = true
|
|
15
|
+
*/
|
|
16
|
+
async connect() {
|
|
17
|
+
throw new Error('connect() must be implemented by the database adapter');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if the database is ready for queries.
|
|
22
|
+
* @returns {Promise<boolean>}
|
|
23
|
+
*/
|
|
24
|
+
async isReady() {
|
|
25
|
+
return this.isConnected;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gracefully shuts down the connection pool with a deadline.
|
|
30
|
+
* @param {number} deadlineMs Timeout in milliseconds
|
|
31
|
+
*/
|
|
32
|
+
async shutdown(deadlineMs = 5000) {
|
|
33
|
+
if (!this.isConnected) return;
|
|
34
|
+
|
|
35
|
+
return Promise.race([
|
|
36
|
+
this._performShutdown(),
|
|
37
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Database shutdown timeout exceeded')), deadlineMs))
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async _performShutdown() {
|
|
42
|
+
throw new Error('_performShutdown() must be implemented by the database adapter');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Executes a callback within a managed transaction.
|
|
47
|
+
* @param {Function} callback (trx) => Promise<any>
|
|
48
|
+
*/
|
|
49
|
+
async healthCheck() {
|
|
50
|
+
return await this.isReady();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async transaction(callback) {
|
|
54
|
+
throw new Error('transaction() must be implemented by the database adapter');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Example Adapter for PostgreSQL (pg)
|
|
60
|
+
*/
|
|
61
|
+
export class PostgresAdapter extends BaseDatabaseAdapter {
|
|
62
|
+
async connect() {
|
|
63
|
+
let pg;
|
|
64
|
+
try {
|
|
65
|
+
pg = await import('pg');
|
|
66
|
+
} catch (e) {
|
|
67
|
+
throw new Error('Please install pg to use PostgresAdapter');
|
|
68
|
+
}
|
|
69
|
+
this.pool = new (pg.default?.Pool || pg.Pool)(this.config);
|
|
70
|
+
await this.pool.query('SELECT 1');
|
|
71
|
+
this.isConnected = true;
|
|
72
|
+
return this.pool;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async _performShutdown() {
|
|
76
|
+
if (this.pool) await this.pool.end();
|
|
77
|
+
this.isConnected = false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async isReady() {
|
|
81
|
+
if (!this.isConnected || !this.pool) return false;
|
|
82
|
+
try {
|
|
83
|
+
await this.pool.query('SELECT 1');
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async transaction(callback) {
|
|
91
|
+
if (!this.isConnected || !this.pool) throw new Error('DB not connected');
|
|
92
|
+
const client = await this.pool.connect();
|
|
93
|
+
try {
|
|
94
|
+
await client.query('BEGIN');
|
|
95
|
+
const result = await callback(client);
|
|
96
|
+
await client.query('COMMIT');
|
|
97
|
+
return result;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
await client.query('ROLLBACK');
|
|
100
|
+
throw e;
|
|
101
|
+
} finally {
|
|
102
|
+
client.release();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/edge.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { verifyJwt, signJwt } from './auth.js';
|
|
3
|
+
import { executeRequest, RouteRegistry, resolveIdentity } from './engine.js';
|
|
4
|
+
|
|
5
|
+
export async function hashIdentity(identity) {
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const data = encoder.encode(String(identity));
|
|
8
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
9
|
+
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
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)}`; }
|
|
13
|
+
|
|
14
|
+
export { z };
|
|
15
|
+
|
|
16
|
+
export function createBro(globalConfig = {}) {
|
|
17
|
+
const globalLogger = { info: console.log, debug: console.debug, warn: console.warn, error: console.error };
|
|
18
|
+
|
|
19
|
+
let isInitialized = false;
|
|
20
|
+
let initPromise = null;
|
|
21
|
+
|
|
22
|
+
let globalDb = null;
|
|
23
|
+
let globalLocale = null;
|
|
24
|
+
const routeRegistry = new RouteRegistry();
|
|
25
|
+
|
|
26
|
+
const __broRedis = null;
|
|
27
|
+
const __broMemoryCache = new Map();
|
|
28
|
+
const __broMemoryRateLimit = new Map();
|
|
29
|
+
|
|
30
|
+
async function ensureInitialized() {
|
|
31
|
+
if (isInitialized) return;
|
|
32
|
+
if (initPromise) return initPromise;
|
|
33
|
+
|
|
34
|
+
initPromise = (async () => {
|
|
35
|
+
try {
|
|
36
|
+
if ((typeof process !== 'undefined' ? process.env : {}).NODE_ENV === 'production' && ['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here'].includes(globalConfig.auth?.jwtSecret)) {
|
|
37
|
+
throw new Error('CRITICAL SECURITY ERROR: You are running in production with a default JWT secret!');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (globalConfig.env) {
|
|
41
|
+
try {
|
|
42
|
+
globalConfig.env.parse((typeof process !== 'undefined' ? process.env : {}));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error('[bro.js/next] Environment Validation Error:', err);
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof globalConfig.db === 'function') {
|
|
50
|
+
globalDb = await globalConfig.db();
|
|
51
|
+
} else if (globalConfig.db && typeof globalConfig.db.init === 'function') {
|
|
52
|
+
globalDb = await globalConfig.db.init();
|
|
53
|
+
} else if (globalConfig.db) {
|
|
54
|
+
globalDb = globalConfig.db;
|
|
55
|
+
if (globalDb instanceof Promise) globalDb = await globalDb;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Redis is not supported natively in edge.js. Provide via globalConfig.redis client.
|
|
59
|
+
|
|
60
|
+
isInitialized = true;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('[bro.js/next] Initialization Error:', err);
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
|
|
67
|
+
return initPromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const resolveLocale = (headers) => {
|
|
71
|
+
const acceptLanguage = headers['accept-language'] || '';
|
|
72
|
+
const preferredLanguages = acceptLanguage
|
|
73
|
+
.split(',')
|
|
74
|
+
.map(lang => lang.split(';')[0].trim().toLowerCase())
|
|
75
|
+
.filter(lang => lang);
|
|
76
|
+
|
|
77
|
+
const configuredLocales = Object.keys(globalConfig.locales || {});
|
|
78
|
+
|
|
79
|
+
for (const lang of preferredLanguages) {
|
|
80
|
+
if (configuredLocales.includes(lang)) {
|
|
81
|
+
return lang;
|
|
82
|
+
}
|
|
83
|
+
const baseLang = lang.split('-')[0];
|
|
84
|
+
if (configuredLocales.includes(baseLang)) {
|
|
85
|
+
return baseLang;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return globalConfig.defaultLocale || 'en';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const translate = (locale, key, values = {}) => {
|
|
93
|
+
let messages = globalConfig.locales?.[locale] || globalConfig.locales?.[globalConfig.defaultLocale || 'en'];
|
|
94
|
+
if (!messages) return key;
|
|
95
|
+
|
|
96
|
+
if (messages.default && typeof messages.default === 'object') {
|
|
97
|
+
messages = messages.default;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const message = key.split('.').reduce((acc, part) => acc && acc[part], messages);
|
|
101
|
+
if (!message || typeof message !== 'string') return key;
|
|
102
|
+
|
|
103
|
+
return message.replace(/\{(\w+)\}/g, (_, name) => {
|
|
104
|
+
return values[name] !== undefined ? String(values[name]) : `{${name}}`;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const errorHelper = (status, message) => {
|
|
109
|
+
const err = new Error(message);
|
|
110
|
+
err.status = status;
|
|
111
|
+
throw err;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function defineRoute(config) {
|
|
115
|
+
// Register route for OpenAPI in Next environments
|
|
116
|
+
routeRegistry.register(config);
|
|
117
|
+
|
|
118
|
+
return async function (req, context) {
|
|
119
|
+
try {
|
|
120
|
+
await ensureInitialized();
|
|
121
|
+
|
|
122
|
+
const resolvedLocale = resolveLocale(Object.fromEntries(req.headers.entries()));
|
|
123
|
+
const url = new URL(req.url);
|
|
124
|
+
|
|
125
|
+
let rawBody = {};
|
|
126
|
+
const parsedFiles = {};
|
|
127
|
+
let totalFiles = 0;
|
|
128
|
+
|
|
129
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
130
|
+
const contentType = req.headers.get('content-type') || '';
|
|
131
|
+
|
|
132
|
+
if (contentType.includes('multipart/form-data')) {
|
|
133
|
+
try {
|
|
134
|
+
const formData = await req.formData();
|
|
135
|
+
for (const [key, value] of formData.entries()) {
|
|
136
|
+
if (value instanceof File || value instanceof Blob) {
|
|
137
|
+
if (!parsedFiles[key]) parsedFiles[key] = [];
|
|
138
|
+
parsedFiles[key].push(value);
|
|
139
|
+
totalFiles++;
|
|
140
|
+
} else {
|
|
141
|
+
rawBody[key] = value;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {}
|
|
145
|
+
} else {
|
|
146
|
+
try {
|
|
147
|
+
rawBody = await req.json();
|
|
148
|
+
} catch (err) {}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let ctxFile = undefined;
|
|
153
|
+
let ctxFiles = undefined;
|
|
154
|
+
if (totalFiles === 1) {
|
|
155
|
+
const keys = Object.keys(parsedFiles);
|
|
156
|
+
ctxFile = parsedFiles[keys[0]][0];
|
|
157
|
+
ctxFiles = parsedFiles;
|
|
158
|
+
} else if (totalFiles > 1) {
|
|
159
|
+
ctxFiles = parsedFiles;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const requestData = {
|
|
163
|
+
method: req.method,
|
|
164
|
+
originalUrl: url.pathname + url.search,
|
|
165
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
166
|
+
body: rawBody,
|
|
167
|
+
query: Object.fromEntries(url.searchParams.entries()),
|
|
168
|
+
params: context?.params ? await context.params : {},
|
|
169
|
+
files: ctxFiles || ctxFile,
|
|
170
|
+
ip: req.headers.get('x-forwarded-for') || 'ip',
|
|
171
|
+
locale: resolvedLocale
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let redisFallback = __broRedis;
|
|
175
|
+
if (!redisFallback) {
|
|
176
|
+
redisFallback = {
|
|
177
|
+
async get(key) {
|
|
178
|
+
const cached = __broMemoryCache.get(key);
|
|
179
|
+
if (cached && Date.now() < cached.expires) return JSON.stringify(cached.data);
|
|
180
|
+
return null;
|
|
181
|
+
},
|
|
182
|
+
async setEx(key, ex, val) {
|
|
183
|
+
__broMemoryCache.set(key, { data: JSON.parse(val), expires: Date.now() + (ex * 1000) });
|
|
184
|
+
},
|
|
185
|
+
async del(key) { __broMemoryCache.delete(key); },
|
|
186
|
+
async incr(key) {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
let record = __broMemoryRateLimit.get(key);
|
|
189
|
+
if (!record || now > record.expires) record = { count: 0, expires: now + 60000 };
|
|
190
|
+
record.count++;
|
|
191
|
+
__broMemoryRateLimit.set(key, record);
|
|
192
|
+
return record.count;
|
|
193
|
+
},
|
|
194
|
+
async expire(key, ex) {
|
|
195
|
+
let record = __broMemoryRateLimit.get(key);
|
|
196
|
+
if (record) {
|
|
197
|
+
record.expires = Date.now() + (ex * 1000);
|
|
198
|
+
__broMemoryRateLimit.set(key, record);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ctxExtras = {
|
|
205
|
+
verifyJwt,
|
|
206
|
+
generateRateLimitKey,
|
|
207
|
+
generateCacheKey,
|
|
208
|
+
generateId: () => crypto.randomUUID(),
|
|
209
|
+
req, // Native NextRequest
|
|
210
|
+
db: globalDb,
|
|
211
|
+
redis: redisFallback,
|
|
212
|
+
io: { emit: () => console.warn('[bro.js/next] WebSockets require standard bro.js server.') },
|
|
213
|
+
t: (key, values) => translate(resolvedLocale, key, values),
|
|
214
|
+
logger: globalLogger,
|
|
215
|
+
jwt: { sign: (payload, opts) => signJwt(payload, globalConfig?.auth?.jwtSecret || (typeof process !== 'undefined' ? process.env : {}).JWT_SECRET, Object.assign({ expiresIn: globalConfig?.auth?.expiresIn || '1d' }, opts || {})) },
|
|
216
|
+
error: errorHelper
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const response = await executeRequest(config, requestData, globalConfig, ctxExtras);
|
|
220
|
+
|
|
221
|
+
return Response.json(response.body, {
|
|
222
|
+
status: response.status,
|
|
223
|
+
headers: response.headers
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[bro.js/next] Unhandled Error:', error);
|
|
228
|
+
return Response.json(
|
|
229
|
+
{
|
|
230
|
+
type: 'https://brojs.dev/errors/internal_server_error',
|
|
231
|
+
title: 'Internal Server Error',
|
|
232
|
+
status: 500,
|
|
233
|
+
instance: req.url,
|
|
234
|
+
requestId: req.headers.get('x-request-id') || 'unknown',
|
|
235
|
+
detail: error.message || 'An unexpected error occurred'
|
|
236
|
+
},
|
|
237
|
+
{ status: 500, headers: { 'Content-Type': 'application/problem+json' } }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { defineRoute, z };
|
|
244
|
+
}
|