bro-framework 3.0.0 → 3.0.2
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 +96 -11
- package/package.json +1 -1
- package/src/dashboard/dashboard.css +286 -0
- package/src/dashboard/dashboard.html +196 -0
- package/src/dashboard.js +49 -41
- package/src/engine.js +6 -0
- package/src/index.d.ts +38 -21
- package/src/next.d.ts +42 -1
package/bin/bro.js
CHANGED
|
@@ -32,7 +32,7 @@ async function bootstrap() {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), '
|
|
35
|
+
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), 'routes');
|
|
36
36
|
const localeDir = globalConfig.locale?.directory || path.resolve(process.cwd(), 'locale');
|
|
37
37
|
const tasksDir = globalConfig.tasksDir || path.resolve(process.cwd(), 'tasks');
|
|
38
38
|
const port = globalConfig.server?.port || process.env.PORT || 3000;
|
|
@@ -55,7 +55,7 @@ async function bootstrap() {
|
|
|
55
55
|
|
|
56
56
|
if (process.argv.includes('--ui')) {
|
|
57
57
|
import('../src/dashboard.js').then(({ startDashboard }) => {
|
|
58
|
-
startDashboard(globalConfig, currentRoutes);
|
|
58
|
+
startDashboard(globalConfig, () => currentRoutes);
|
|
59
59
|
}).catch(err => console.error('[bro.js] Error starting dashboard:', err));
|
|
60
60
|
}
|
|
61
61
|
} else if (command === 'start') {
|
|
@@ -138,15 +138,92 @@ async function bootstrap() {
|
|
|
138
138
|
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
139
139
|
const envPath = path.resolve(process.cwd(), '.env.example');
|
|
140
140
|
if (!fs.existsSync(configPath)) {
|
|
141
|
-
fs.writeFileSync(configPath, `
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
fs.writeFileSync(configPath, `import { defineConfig } from 'bro-framework';
|
|
142
|
+
|
|
143
|
+
export default defineConfig({
|
|
144
|
+
// Server Settings
|
|
145
|
+
server: {
|
|
146
|
+
port: 5000,
|
|
147
|
+
cors: process.env.NODE_ENV === 'production' ? ['https://yourdomain.com'] : true, // Set to true to allow all, or pass a CORS options object
|
|
148
|
+
helmet: true // Enable security headers
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// Authentication Settings
|
|
152
|
+
auth: {
|
|
153
|
+
jwtSecret: process.env.JWT_SECRET || 'dev_secret_please_change', // Must be at least 32 characters in production
|
|
154
|
+
expiresIn: '7d',
|
|
155
|
+
//apiKey: process.env.API_KEY || ['dev_key_1', 'dev_key_2'] // Supports array for zero-downtime rotation
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Trust reverse proxy IP headers (Nginx/Cloudflare)
|
|
159
|
+
trustProxy: true,
|
|
160
|
+
|
|
161
|
+
// Observability & Telemetry
|
|
162
|
+
observability: {
|
|
163
|
+
// Output structured JSON logs with request IDs and execution timing (ideal for CloudWatch/Datadog)
|
|
164
|
+
// Options: 'json' | 'pretty' (default: 'pretty' in dev, 'json' in production)
|
|
165
|
+
logging: 'json',
|
|
166
|
+
|
|
167
|
+
// Enable OpenTelemetry W3C trace propagation and HTTP span generation
|
|
168
|
+
openTelemetry: true
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Optional file-based API translations
|
|
172
|
+
// Add locale/en.js, locale/fr.js, etc.
|
|
173
|
+
locale: {
|
|
174
|
+
defaultLocale: 'en'
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// API Documentation (Scalar UI)
|
|
178
|
+
docs: process.env.NODE_ENV !== 'production', // Set to false to disable completely, or true to force in prod
|
|
179
|
+
|
|
180
|
+
// Rate Limiting
|
|
181
|
+
rateLimit: {
|
|
182
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
183
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Redis Configuration (Auto-scales WebSockets, distributed caches & rate-limiting)
|
|
187
|
+
redisUrl: process.env.REDIS_URL, // e.g., 'redis://localhost:6379'
|
|
188
|
+
|
|
189
|
+
// WebSockets Setup
|
|
190
|
+
sockets: async (io, db) => {
|
|
191
|
+
io.on('connection', (socket) => {
|
|
192
|
+
console.log('Client connected:', socket.id);
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Database Context Injection
|
|
197
|
+
// This instance will be injected into every route's ctx.db (if defined)
|
|
198
|
+
db: async () => {
|
|
199
|
+
// If you use a database, set up your connection here
|
|
200
|
+
// and return the connection instance or an object of your models.
|
|
201
|
+
// Could be MongoDB, MySQL, etc. (your choice)
|
|
202
|
+
// --- MONGOOSE EXAMPLE ---
|
|
203
|
+
// import mongoose from 'mongoose';
|
|
204
|
+
|
|
205
|
+
// await mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/bro_database');
|
|
206
|
+
// console.log("Connected to MongoDB");
|
|
207
|
+
|
|
208
|
+
// You can return mongoose itself, or an object of your models
|
|
209
|
+
// to access them instantly in your routes without importing them!
|
|
210
|
+
// Example: return { User, Post };
|
|
211
|
+
|
|
212
|
+
// return mongoose.connection;
|
|
213
|
+
// --------------------------
|
|
214
|
+
return null;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Graceful Teardown Hook
|
|
218
|
+
onShutdown: async (db) => {
|
|
219
|
+
// Close application-owned database resources gracefully here
|
|
220
|
+
}
|
|
221
|
+
});
|
|
145
222
|
`);
|
|
146
223
|
console.log('[bro.js] Created bro.config.js');
|
|
147
224
|
}
|
|
148
225
|
if (!fs.existsSync(envPath)) {
|
|
149
|
-
fs.writeFileSync(envPath, 'JWT_SECRET
|
|
226
|
+
fs.writeFileSync(envPath, 'JWT_SECRET=your_jwt_secret_here\nNODE_ENV=development\nREDIS_URL=redis://localhost:6379\n');
|
|
150
227
|
console.log('[bro.js] Created .env.example');
|
|
151
228
|
}
|
|
152
229
|
} else if (command === 'doctor') {
|
|
@@ -160,7 +237,7 @@ async function bootstrap() {
|
|
|
160
237
|
if (config.server?.cors === true) console.error('✗ Permissive CORS is enabled (cors: true). Use an array of allowed origins.');
|
|
161
238
|
else console.log('✓ CORS is strict.');
|
|
162
239
|
|
|
163
|
-
if (['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here'].includes(config.auth?.jwtSecret)) {
|
|
240
|
+
if (['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here', ''].includes(config.auth?.jwtSecret?.trim())) {
|
|
164
241
|
console.error('✗ Hardcoded insecure JWT secret detected.');
|
|
165
242
|
} else {
|
|
166
243
|
console.log('✓ Secrets look ok.');
|
|
@@ -168,9 +245,17 @@ async function bootstrap() {
|
|
|
168
245
|
});
|
|
169
246
|
}
|
|
170
247
|
} else if (command === 'sdk' || command === 'client' || command === 'generate-client') {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
248
|
+
const sdkOutPath = process.argv[3] || './client.ts';
|
|
249
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
250
|
+
let globalConfig = {};
|
|
251
|
+
if (fs.existsSync(configPath)) {
|
|
252
|
+
try {
|
|
253
|
+
const configModule = await import(configPath);
|
|
254
|
+
globalConfig = configModule.default || configModule;
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
}
|
|
257
|
+
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), 'routes');
|
|
258
|
+
generateSDK(routesDir, sdkOutPath).then(() => {
|
|
174
259
|
console.log(`\x1b[32m✓ SDK successfully generated at ${sdkOutPath}\x1b[0m`);
|
|
175
260
|
}).catch(err => {
|
|
176
261
|
console.error('\x1b[31m✗ Failed to generate SDK:\x1b[0m', err);
|
package/package.json
CHANGED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/* Minimalist High-Contrast Monochrome Design */
|
|
2
|
+
:root {
|
|
3
|
+
--bg-color: #000000;
|
|
4
|
+
--bg-card: #09090b;
|
|
5
|
+
--bg-card-hover: #121214;
|
|
6
|
+
--border-color: #27272a;
|
|
7
|
+
--text-primary: #fafafa;
|
|
8
|
+
--text-secondary: #a1a1aa;
|
|
9
|
+
--text-muted: #71717a;
|
|
10
|
+
--accent-color: #ffffff;
|
|
11
|
+
--success-color: #10b981;
|
|
12
|
+
--error-color: #ef4444;
|
|
13
|
+
--warning-color: #f59e0b;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
* {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
margin: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
24
|
+
background-color: var(--bg-color);
|
|
25
|
+
color: var(--text-primary);
|
|
26
|
+
line-height: 1.5;
|
|
27
|
+
-webkit-font-smoothing: antialiased;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
header {
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
align-items: center;
|
|
34
|
+
padding: 1rem 2rem;
|
|
35
|
+
border-bottom: 1px solid var(--border-color);
|
|
36
|
+
background-color: var(--bg-card);
|
|
37
|
+
position: sticky;
|
|
38
|
+
top: 0;
|
|
39
|
+
z-index: 10;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.header-title {
|
|
43
|
+
font-size: 1.25rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 0.5rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.status-dot {
|
|
51
|
+
width: 8px;
|
|
52
|
+
height: 8px;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
background-color: var(--success-color);
|
|
55
|
+
box-shadow: 0 0 8px var(--success-color);
|
|
56
|
+
animation: pulse 2s infinite;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@keyframes pulse {
|
|
60
|
+
0% { opacity: 1; }
|
|
61
|
+
50% { opacity: 0.5; }
|
|
62
|
+
100% { opacity: 1; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.header-controls {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
gap: 1rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.port-label {
|
|
72
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
73
|
+
color: var(--text-secondary);
|
|
74
|
+
font-size: 0.875rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.btn {
|
|
78
|
+
background: var(--bg-card);
|
|
79
|
+
border: 1px solid var(--border-color);
|
|
80
|
+
color: var(--text-primary);
|
|
81
|
+
padding: 0.5rem 1rem;
|
|
82
|
+
border-radius: 6px;
|
|
83
|
+
font-size: 0.875rem;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
transition: all 0.2s;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.btn:hover {
|
|
89
|
+
background: var(--bg-card-hover);
|
|
90
|
+
border-color: var(--text-secondary);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main {
|
|
94
|
+
max-width: 1200px;
|
|
95
|
+
margin: 0 auto;
|
|
96
|
+
padding: 2rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Grid layout for metrics */
|
|
100
|
+
.metrics-grid {
|
|
101
|
+
display: grid;
|
|
102
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
103
|
+
gap: 1.5rem;
|
|
104
|
+
margin-bottom: 2rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.card {
|
|
108
|
+
background: var(--bg-card);
|
|
109
|
+
border: 1px solid var(--border-color);
|
|
110
|
+
border-radius: 8px;
|
|
111
|
+
padding: 1.5rem;
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.card-title {
|
|
117
|
+
font-size: 0.875rem;
|
|
118
|
+
color: var(--text-secondary);
|
|
119
|
+
text-transform: uppercase;
|
|
120
|
+
letter-spacing: 0.05em;
|
|
121
|
+
margin-bottom: 0.5rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.card-value {
|
|
125
|
+
font-size: 1.5rem;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.card-subtitle {
|
|
130
|
+
font-size: 0.875rem;
|
|
131
|
+
color: var(--text-muted);
|
|
132
|
+
margin-top: 0.5rem;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Search bar */
|
|
136
|
+
.search-container {
|
|
137
|
+
margin-bottom: 1.5rem;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.search-input {
|
|
141
|
+
width: 100%;
|
|
142
|
+
padding: 0.75rem 1rem;
|
|
143
|
+
background: var(--bg-card);
|
|
144
|
+
border: 1px solid var(--border-color);
|
|
145
|
+
color: var(--text-primary);
|
|
146
|
+
border-radius: 8px;
|
|
147
|
+
font-size: 1rem;
|
|
148
|
+
transition: border-color 0.2s;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.search-input:focus {
|
|
152
|
+
outline: none;
|
|
153
|
+
border-color: var(--accent-color);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Routes Table */
|
|
157
|
+
.routes-list {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
gap: 0.5rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.route-item {
|
|
164
|
+
background: var(--bg-card);
|
|
165
|
+
border: 1px solid var(--border-color);
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
padding: 1rem;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
gap: 1rem;
|
|
171
|
+
transition: background 0.2s;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.route-item:hover {
|
|
175
|
+
background: var(--bg-card-hover);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.route-header {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 1rem;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.method-pill {
|
|
186
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
187
|
+
font-size: 0.75rem;
|
|
188
|
+
font-weight: 700;
|
|
189
|
+
padding: 0.25rem 0.5rem;
|
|
190
|
+
border-radius: 4px;
|
|
191
|
+
border: 1px solid var(--border-color);
|
|
192
|
+
width: 60px;
|
|
193
|
+
text-align: center;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.method-GET { color: #38bdf8; border-color: #0369a1; background: rgba(3, 105, 161, 0.1); }
|
|
197
|
+
.method-POST { color: #10b981; border-color: #047857; background: rgba(4, 120, 87, 0.1); }
|
|
198
|
+
.method-PUT { color: #f59e0b; border-color: #b45309; background: rgba(180, 83, 9, 0.1); }
|
|
199
|
+
.method-PATCH { color: #f59e0b; border-color: #b45309; background: rgba(180, 83, 9, 0.1); }
|
|
200
|
+
.method-DELETE { color: #ef4444; border-color: #b91c1c; background: rgba(185, 28, 28, 0.1); }
|
|
201
|
+
.method-ANY { color: #a1a1aa; border-color: #52525b; background: rgba(82, 82, 91, 0.1); }
|
|
202
|
+
|
|
203
|
+
.route-path {
|
|
204
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
205
|
+
font-size: 0.875rem;
|
|
206
|
+
flex-grow: 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.badges {
|
|
210
|
+
display: flex;
|
|
211
|
+
gap: 0.5rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.badge {
|
|
215
|
+
font-size: 0.75rem;
|
|
216
|
+
padding: 0.1rem 0.4rem;
|
|
217
|
+
border-radius: 4px;
|
|
218
|
+
background: #18181b;
|
|
219
|
+
border: 1px solid var(--border-color);
|
|
220
|
+
color: var(--text-secondary);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.badge.active {
|
|
224
|
+
color: var(--text-primary);
|
|
225
|
+
border-color: var(--text-secondary);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.route-details {
|
|
229
|
+
display: none;
|
|
230
|
+
border-top: 1px dashed var(--border-color);
|
|
231
|
+
padding-top: 1rem;
|
|
232
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
233
|
+
font-size: 0.875rem;
|
|
234
|
+
color: var(--text-secondary);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.route-details.expanded {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 0.5rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.detail-row {
|
|
244
|
+
display: flex;
|
|
245
|
+
gap: 1rem;
|
|
246
|
+
}
|
|
247
|
+
.detail-label {
|
|
248
|
+
color: var(--text-muted);
|
|
249
|
+
width: 80px;
|
|
250
|
+
}
|
|
251
|
+
.detail-value {
|
|
252
|
+
color: var(--text-primary);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* Config Grid */
|
|
256
|
+
.config-container {
|
|
257
|
+
margin-top: 3rem;
|
|
258
|
+
border-top: 1px solid var(--border-color);
|
|
259
|
+
padding-top: 2rem;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.config-container h2 {
|
|
263
|
+
font-size: 1.25rem;
|
|
264
|
+
margin-bottom: 1rem;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.config-grid {
|
|
268
|
+
display: grid;
|
|
269
|
+
grid-template-columns: max-content 1fr;
|
|
270
|
+
gap: 0.75rem 2rem;
|
|
271
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
272
|
+
font-size: 0.875rem;
|
|
273
|
+
background: var(--bg-card);
|
|
274
|
+
border: 1px solid var(--border-color);
|
|
275
|
+
padding: 1.5rem;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
overflow-x: auto;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.config-key {
|
|
281
|
+
color: var(--text-secondary);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.config-val {
|
|
285
|
+
color: var(--text-primary);
|
|
286
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>bro.js | Dev Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/dashboard.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<div class="header-title">
|
|
12
|
+
<div class="status-dot"></div>
|
|
13
|
+
bro.js <span style="color: var(--text-muted); font-size: 1rem;">v3.0.0</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="header-controls">
|
|
16
|
+
<span class="port-label" id="portLabel"></span>
|
|
17
|
+
<button class="btn" id="refreshBtn">Refresh</button>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<main>
|
|
22
|
+
<div class="metrics-grid">
|
|
23
|
+
<div class="card">
|
|
24
|
+
<div class="card-title">Active Routes</div>
|
|
25
|
+
<div class="card-value" id="routesCount">-</div>
|
|
26
|
+
<div class="card-subtitle">Endpoints loaded in memory</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="card">
|
|
29
|
+
<div class="card-title">Security State</div>
|
|
30
|
+
<div class="card-value" id="securityState">Strict</div>
|
|
31
|
+
<div class="card-subtitle" id="securitySubtitle">CORS / Trust Proxy</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="card">
|
|
34
|
+
<div class="card-title">Environment</div>
|
|
35
|
+
<div class="card-value" id="envState">Node.js</div>
|
|
36
|
+
<div class="card-subtitle" id="envSubtitle">Version</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="search-container">
|
|
41
|
+
<input type="text" id="searchInput" class="search-input" placeholder="Search routes by path or method (e.g. GET /users)...">
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="routes-list" id="routesList">
|
|
45
|
+
<!-- Routes injected here -->
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="config-container">
|
|
49
|
+
<h2>Configuration Inspector</h2>
|
|
50
|
+
<div class="config-grid" id="configGrid">
|
|
51
|
+
<!-- Config injected here -->
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</main>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
let allRoutes = [];
|
|
58
|
+
|
|
59
|
+
async function fetchState() {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/api/state');
|
|
62
|
+
if (!res.ok) throw new Error('Failed to fetch state');
|
|
63
|
+
const data = await res.json();
|
|
64
|
+
|
|
65
|
+
// Update port
|
|
66
|
+
document.getElementById('portLabel').textContent = window.location.origin;
|
|
67
|
+
|
|
68
|
+
// Update Routes Count
|
|
69
|
+
document.getElementById('routesCount').textContent = data.routes.length;
|
|
70
|
+
|
|
71
|
+
// Update Env
|
|
72
|
+
document.getElementById('envState').textContent = data.env.platform;
|
|
73
|
+
document.getElementById('envSubtitle').textContent = data.env.nodeVersion;
|
|
74
|
+
|
|
75
|
+
// Update Security
|
|
76
|
+
const isStrict = data.config.server?.cors === false || data.config.server?.cors === undefined;
|
|
77
|
+
document.getElementById('securityState').textContent = isStrict ? 'Strict' : 'Permissive';
|
|
78
|
+
document.getElementById('securitySubtitle').textContent = `Proxy: ${data.config.trustProxy ? 'Trusted' : 'Untrusted'}`;
|
|
79
|
+
|
|
80
|
+
allRoutes = data.routes;
|
|
81
|
+
renderRoutes(allRoutes);
|
|
82
|
+
renderConfig(data.config);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function copyToClipboard(text) {
|
|
89
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
90
|
+
// Could show a toast here
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function toggleRoute(index) {
|
|
95
|
+
const el = document.getElementById(`details-${index}`);
|
|
96
|
+
if (el) {
|
|
97
|
+
el.classList.toggle('expanded');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function renderRoutes(routes) {
|
|
102
|
+
const container = document.getElementById('routesList');
|
|
103
|
+
container.innerHTML = '';
|
|
104
|
+
|
|
105
|
+
if (routes.length === 0) {
|
|
106
|
+
container.innerHTML = '<div style="color: var(--text-muted); padding: 1rem;">No routes found.</div>';
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
routes.forEach((r, idx) => {
|
|
111
|
+
const method = r.method.toUpperCase();
|
|
112
|
+
const path = r.path;
|
|
113
|
+
|
|
114
|
+
const item = document.createElement('div');
|
|
115
|
+
item.className = 'route-item';
|
|
116
|
+
|
|
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>
|
|
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>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="route-details" id="details-\${idx}">
|
|
128
|
+
<div class="detail-row">
|
|
129
|
+
<div class="detail-label">Body</div>
|
|
130
|
+
<div class="detail-value">\${r.hasBodySchema ? 'Schema Validated' : 'Any'}</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="detail-row">
|
|
133
|
+
<div class="detail-label">Query</div>
|
|
134
|
+
<div class="detail-value">\${r.hasQuerySchema ? 'Schema Validated' : 'Any'}</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="detail-row">
|
|
137
|
+
<div class="detail-label">Params</div>
|
|
138
|
+
<div class="detail-value">\${r.hasParamsSchema ? 'Schema Validated' : 'Any'}</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="detail-row">
|
|
141
|
+
<div class="detail-label">Response</div>
|
|
142
|
+
<div class="detail-value">\${r.hasResponseSchema ? 'Schema Validated' : 'Any'}</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
container.appendChild(item);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderConfig(config, prefix = '') {
|
|
152
|
+
const container = document.getElementById('configGrid');
|
|
153
|
+
if (prefix === '') container.innerHTML = '';
|
|
154
|
+
|
|
155
|
+
for (const [key, val] of Object.entries(config)) {
|
|
156
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
157
|
+
|
|
158
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
|
159
|
+
renderConfig(val, fullKey);
|
|
160
|
+
} else {
|
|
161
|
+
let displayVal = val;
|
|
162
|
+
if (typeof val === 'string') displayVal = `"${val}"`;
|
|
163
|
+
if (Array.isArray(val)) displayVal = `[${val.join(', ')}]`;
|
|
164
|
+
if (val === undefined) displayVal = 'undefined';
|
|
165
|
+
if (val === null) displayVal = 'null';
|
|
166
|
+
|
|
167
|
+
const keyEl = document.createElement('div');
|
|
168
|
+
keyEl.className = 'config-key';
|
|
169
|
+
keyEl.textContent = fullKey;
|
|
170
|
+
|
|
171
|
+
const valEl = document.createElement('div');
|
|
172
|
+
valEl.className = 'config-val';
|
|
173
|
+
valEl.textContent = displayVal;
|
|
174
|
+
|
|
175
|
+
container.appendChild(keyEl);
|
|
176
|
+
container.appendChild(valEl);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.getElementById('searchInput').addEventListener('input', (e) => {
|
|
182
|
+
const query = e.target.value.toLowerCase();
|
|
183
|
+
const filtered = allRoutes.filter(r =>
|
|
184
|
+
r.path.toLowerCase().includes(query) ||
|
|
185
|
+
r.method.toLowerCase().includes(query)
|
|
186
|
+
);
|
|
187
|
+
renderRoutes(filtered);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
document.getElementById('refreshBtn').addEventListener('click', fetchState);
|
|
191
|
+
|
|
192
|
+
// Initial load
|
|
193
|
+
fetchState();
|
|
194
|
+
</script>
|
|
195
|
+
</body>
|
|
196
|
+
</html>
|
package/src/dashboard.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import url from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Local Developer Dashboard Server
|
|
7
10
|
* Serves a dashboard UI that visualizes routes, telemetry, and configuration.
|
|
8
11
|
*/
|
|
9
|
-
export function startDashboard(globalConfig,
|
|
12
|
+
export function startDashboard(globalConfig, routeRegistryOrGetter) {
|
|
10
13
|
const host = '127.0.0.1'; // Strict local binding
|
|
11
|
-
const
|
|
14
|
+
const basePort = globalConfig.server?.port || globalConfig.port || process.env.PORT || 3000;
|
|
15
|
+
const port = Number(basePort) + 1;
|
|
12
16
|
|
|
13
|
-
const server = http.createServer((req, res) => {
|
|
17
|
+
const server = http.createServer(async (req, res) => {
|
|
14
18
|
// Restrict access to localhost strictly
|
|
15
19
|
if (req.socket.remoteAddress !== '127.0.0.1' && req.socket.remoteAddress !== '::1') {
|
|
16
20
|
res.writeHead(403);
|
|
@@ -19,52 +23,56 @@ export function startDashboard(globalConfig, routes) {
|
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
if (req.url === '/') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
`);
|
|
26
|
+
try {
|
|
27
|
+
const html = await fs.promises.readFile(path.join(__dirname, 'dashboard', 'dashboard.html'), 'utf-8');
|
|
28
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
29
|
+
res.end(html);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
res.writeHead(500);
|
|
32
|
+
res.end('Error loading dashboard UI');
|
|
33
|
+
}
|
|
34
|
+
} else if (req.url === '/dashboard.css') {
|
|
35
|
+
try {
|
|
36
|
+
const css = await fs.promises.readFile(path.join(__dirname, 'dashboard', 'dashboard.css'), 'utf-8');
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
38
|
+
res.end(css);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
res.writeHead(500);
|
|
41
|
+
res.end('Error loading dashboard CSS');
|
|
42
|
+
}
|
|
57
43
|
} else if (req.url === '/api/state') {
|
|
58
44
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
59
45
|
|
|
60
46
|
// Strict Redaction of Secrets
|
|
61
|
-
const redactedConfig = JSON.parse(JSON.stringify(globalConfig));
|
|
47
|
+
const redactedConfig = JSON.parse(JSON.stringify(globalConfig, (k, v) => typeof v === 'function' ? '[Function]' : v));
|
|
62
48
|
if (redactedConfig.auth?.jwtSecret) redactedConfig.auth.jwtSecret = '***REDACTED***';
|
|
63
49
|
if (redactedConfig.db) redactedConfig.db = '[Database Instance]';
|
|
50
|
+
if (redactedConfig.redisUrl) redactedConfig.redisUrl = '***REDACTED***';
|
|
51
|
+
|
|
52
|
+
// Dynamic live state resolution
|
|
53
|
+
const routes = typeof routeRegistryOrGetter === 'function'
|
|
54
|
+
? routeRegistryOrGetter()
|
|
55
|
+
: (routeRegistryOrGetter.getRoutes ? routeRegistryOrGetter.getRoutes() : routeRegistryOrGetter);
|
|
56
|
+
|
|
57
|
+
const normalizedRoutes = (routes || []).map(r => ({
|
|
58
|
+
method: r.method || 'GET',
|
|
59
|
+
path: r.path || r.routePath || r.url || '/',
|
|
60
|
+
auth: r.auth ? (typeof r.auth === 'string' ? r.auth : 'required') : 'public',
|
|
61
|
+
rateLimit: r.rateLimit ? `${r.rateLimit.max} req / ${r.rateLimit.windowMs / 1000}s` : 'none',
|
|
62
|
+
cache: r.cache ? `${r.cache}s` : 'none',
|
|
63
|
+
hasBodySchema: Boolean(r.body || r.schema?.body),
|
|
64
|
+
hasQuerySchema: Boolean(r.query || r.schema?.query),
|
|
65
|
+
hasParamsSchema: Boolean(r.params || r.schema?.params),
|
|
66
|
+
hasResponseSchema: Boolean(r.response || r.schema?.response)
|
|
67
|
+
}));
|
|
64
68
|
|
|
65
69
|
res.end(JSON.stringify({
|
|
66
70
|
config: redactedConfig,
|
|
67
|
-
routes:
|
|
71
|
+
routes: normalizedRoutes,
|
|
72
|
+
env: {
|
|
73
|
+
platform: process.platform,
|
|
74
|
+
nodeVersion: process.version
|
|
75
|
+
}
|
|
68
76
|
}));
|
|
69
77
|
} else {
|
|
70
78
|
res.writeHead(404);
|
|
@@ -73,7 +81,7 @@ export function startDashboard(globalConfig, routes) {
|
|
|
73
81
|
});
|
|
74
82
|
|
|
75
83
|
server.listen(port, host, () => {
|
|
76
|
-
console.log(`[bro.js]
|
|
84
|
+
console.log(`[bro.js] Dev Dashboard running locally at http://${host}:${port}`);
|
|
77
85
|
});
|
|
78
86
|
|
|
79
87
|
return server;
|
package/src/engine.js
CHANGED
|
@@ -92,6 +92,12 @@ export async function executeRequest(routeConfig, requestData, globalConfig, ctx
|
|
|
92
92
|
|
|
93
93
|
try {
|
|
94
94
|
const ctx = {
|
|
95
|
+
req: requestData.originalUrl,
|
|
96
|
+
method: requestData.method,
|
|
97
|
+
ip: requestData.ip,
|
|
98
|
+
headers: requestData.headers,
|
|
99
|
+
locale: requestData.locale,
|
|
100
|
+
requestId: reqId,
|
|
95
101
|
...ctxExtras,
|
|
96
102
|
env: globalConfig.envData || process.env,
|
|
97
103
|
body: requestData.body,
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
1
|
+
/// <reference types="node" />
|
|
2
2
|
import { z, ZodTypeAny } from 'zod';
|
|
3
3
|
|
|
4
4
|
type InferZod<T> = T extends ZodTypeAny ? z.infer<T> : any;
|
|
@@ -15,12 +15,12 @@ export interface UploadedFile {
|
|
|
15
15
|
buffer?: Buffer;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface AppContext<Env = any, Db = any, User = any> {
|
|
19
|
-
env: Env;
|
|
20
|
-
db: Db;
|
|
21
|
-
user: User;
|
|
22
|
-
}
|
|
23
|
-
|
|
18
|
+
export interface AppContext<Env = any, Db = any, User = any> {
|
|
19
|
+
env: Env;
|
|
20
|
+
db: Db;
|
|
21
|
+
user: User;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
24
|
export interface BroContext<Body = any, Params = any, Query = any, App extends AppContext = AppContext> {
|
|
25
25
|
env?: App['env'];
|
|
26
26
|
jwt?: { sign: (payload: any, options?: any) => string };
|
|
@@ -36,10 +36,11 @@ export interface BroContext<Body = any, Params = any, Query = any, App extends A
|
|
|
36
36
|
t: (key: string, values?: Record<string, unknown>) => string;
|
|
37
37
|
error?: any;
|
|
38
38
|
redis?: any;
|
|
39
|
+
requestId?: string;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface RouteConfig<Body = any, Params = any, Query = any, Response = any, App extends AppContext = AppContext> {
|
|
42
|
-
auth?: boolean | string[] | 'api-key';
|
|
43
|
+
auth?: boolean | string[] | 'api-key';
|
|
43
44
|
operationId?: string;
|
|
44
45
|
upload?: boolean | { limits?: any, fields?: { name: string, maxCount?: number }[], single?: string, array?: string, fileFilter?: any, storage?: any };
|
|
45
46
|
body?: Body;
|
|
@@ -66,24 +67,40 @@ export function loadLocale(directory: string, options?: { defaultLocale?: string
|
|
|
66
67
|
translate: (locale: string, key: string, values?: Record<string, unknown>) => string;
|
|
67
68
|
}>;
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
export interface BroPlugin {
|
|
71
|
-
name: string;
|
|
72
|
-
version: string;
|
|
73
|
-
order?: number;
|
|
74
|
-
onInit?: (globalConfig: BroConfig, app: any) => void | Promise<void>;
|
|
75
|
-
onContext?: (ctx: BroContext) => any | Promise<any>;
|
|
76
|
-
onRequest?: (req: any, res: any) => void | Promise<void>;
|
|
77
|
-
onError?: (err: any, req: any, res: any) => void | Promise<void>;
|
|
78
|
-
onShutdown?: () => void | Promise<void>;
|
|
79
|
-
}
|
|
80
|
-
export interface BroConfig {
|
|
81
|
-
|
|
70
|
+
|
|
71
|
+
export interface BroPlugin {
|
|
72
|
+
name: string;
|
|
73
|
+
version: string;
|
|
74
|
+
order?: number;
|
|
75
|
+
onInit?: (globalConfig: BroConfig, app: any) => void | Promise<void>;
|
|
76
|
+
onContext?: (ctx: BroContext) => any | Promise<any>;
|
|
77
|
+
onRequest?: (req: any, res: any) => void | Promise<void>;
|
|
78
|
+
onError?: (err: any, req: any, res: any) => void | Promise<void>;
|
|
79
|
+
onShutdown?: () => void | Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
export interface BroConfig {
|
|
82
|
+
routesDir?: string;
|
|
83
|
+
tasksDir?: string;
|
|
84
|
+
logger?: { level?: string; [key: string]: any };
|
|
85
|
+
plugins?: BroPlugin[];
|
|
86
|
+
fixtures?: Record<string, any>;
|
|
87
|
+
stores?: Record<string, any>;
|
|
88
|
+
health?: boolean | { dbCheck?: (db: any) => Promise<any> };
|
|
89
|
+
locales?: Record<string, any>;
|
|
90
|
+
defaultLocale?: string;
|
|
91
|
+
envData?: any;
|
|
92
|
+
redis?: any;
|
|
93
|
+
port?: number;
|
|
94
|
+
jwtSecret?: string;
|
|
95
|
+
trustProxy?: boolean;
|
|
96
|
+
validateResponse?: boolean | 'strict' | 'warn';
|
|
82
97
|
env?: ZodTypeAny;
|
|
83
98
|
server?: {
|
|
84
99
|
port?: number;
|
|
85
100
|
cors?: boolean | object;
|
|
86
101
|
helmet?: boolean | object;
|
|
102
|
+
timeoutMs?: number;
|
|
103
|
+
headersTimeoutMs?: number;
|
|
87
104
|
};
|
|
88
105
|
locale?: {
|
|
89
106
|
directory?: string;
|
package/src/next.d.ts
CHANGED
|
@@ -12,9 +12,33 @@ export interface UploadedFile {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface NextBroGlobalConfig<TEnv = any, TDb = any, TUser = any> {
|
|
15
|
-
|
|
15
|
+
routesDir?: string;
|
|
16
|
+
tasksDir?: string;
|
|
17
|
+
logger?: { level?: string; [key: string]: any };
|
|
18
|
+
plugins?: any[];
|
|
19
|
+
fixtures?: Record<string, any>;
|
|
20
|
+
stores?: Record<string, any>;
|
|
21
|
+
health?: boolean | { dbCheck?: (db: any) => Promise<any> };
|
|
16
22
|
locales?: Record<string, any>;
|
|
17
23
|
defaultLocale?: string;
|
|
24
|
+
envData?: any;
|
|
25
|
+
redis?: any;
|
|
26
|
+
port?: number;
|
|
27
|
+
jwtSecret?: string;
|
|
28
|
+
trustProxy?: boolean;
|
|
29
|
+
validateResponse?: boolean | 'strict' | 'warn';
|
|
30
|
+
env?: ZodTypeAny;
|
|
31
|
+
server?: {
|
|
32
|
+
port?: number;
|
|
33
|
+
cors?: boolean | object;
|
|
34
|
+
helmet?: boolean | object;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
headersTimeoutMs?: number;
|
|
37
|
+
};
|
|
38
|
+
locale?: {
|
|
39
|
+
directory?: string;
|
|
40
|
+
defaultLocale?: string;
|
|
41
|
+
};
|
|
18
42
|
redisUrl?: string;
|
|
19
43
|
rateLimit?: { windowMs: number; max: number; };
|
|
20
44
|
auth?: {
|
|
@@ -22,7 +46,18 @@ export interface NextBroGlobalConfig<TEnv = any, TDb = any, TUser = any> {
|
|
|
22
46
|
apiKey?: string | string[];
|
|
23
47
|
expiresIn?: string | number;
|
|
24
48
|
};
|
|
49
|
+
docs?: boolean | { auth?: { user: string; pass: string } };
|
|
50
|
+
upload?: {
|
|
51
|
+
limits?: {
|
|
52
|
+
fileSize?: number;
|
|
53
|
+
files?: number;
|
|
54
|
+
fields?: number;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
25
58
|
db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
|
|
59
|
+
sockets?: (io: any, db: any) => Promise<void> | void;
|
|
60
|
+
onShutdown?: (db: any) => Promise<void> | void;
|
|
26
61
|
}
|
|
27
62
|
|
|
28
63
|
export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TEnv = any, TDb = any, TUser = any> {
|
|
@@ -37,6 +72,11 @@ export interface NextRouteContext<TBody = any, TQuery = any, TParams = any, TEnv
|
|
|
37
72
|
file?: UploadedFile;
|
|
38
73
|
files?: Record<string, UploadedFile[]>;
|
|
39
74
|
locale: string;
|
|
75
|
+
method: string;
|
|
76
|
+
ip: string;
|
|
77
|
+
headers: Record<string, string>;
|
|
78
|
+
requestId: string;
|
|
79
|
+
logger: any;
|
|
40
80
|
t: (key: string, values?: any) => string;
|
|
41
81
|
user?: TUser;
|
|
42
82
|
jwt: { sign: (payload: any, opts?: any) => string };
|
|
@@ -52,6 +92,7 @@ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TEnv
|
|
|
52
92
|
rateLimit?: { windowMs: number; max: number; } | false;
|
|
53
93
|
response?: ZodTypeAny;
|
|
54
94
|
summary?: string;
|
|
95
|
+
operationId?: string;
|
|
55
96
|
upload?: any;
|
|
56
97
|
handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TEnv, TDb, TUser>) => Promise<any> | any;
|
|
57
98
|
}
|