vibe-gx 4.1.2 → 4.2.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 +74 -14
- package/package.json +1 -1
- package/utils/core/logger.js +41 -26
- package/utils/core/parser.js +13 -7
- package/utils/core/response.js +6 -1
- package/utils/core/server.js +38 -9
- package/utils/helpers/cors.js +124 -0
- package/utils/scaling/rate-limit.js +113 -0
- package/vibe.d.ts +170 -18
- package/vibe.js +49 -32
package/README.md
CHANGED
|
@@ -50,6 +50,8 @@ npm install vibe-gx
|
|
|
50
50
|
| 🎨 **Decorators** | Extend app, request, and response |
|
|
51
51
|
| ⚡ **Cluster Mode** | Built-in multi-process scaling |
|
|
52
52
|
| 💾 **LRU Cache** | Built-in response caching with ETag |
|
|
53
|
+
| 🛡️ **Rate Limiting** | Built-in sliding window rate limiter — no dependencies |
|
|
54
|
+
| 🌐 **CORS** | Built-in CORS with preflight handling — no dependencies |
|
|
53
55
|
| 🔗 **Connection Pool** | Generic pool for databases |
|
|
54
56
|
| 📂 **File Uploads** | Multipart uploads with size/type validation |
|
|
55
57
|
| 🌊 **Streaming** | Large file uploads without buffering |
|
|
@@ -474,15 +476,61 @@ Use any Express middleware with the adapter:
|
|
|
474
476
|
|
|
475
477
|
```javascript
|
|
476
478
|
import vibe, { adapt } from "vibe-gx";
|
|
477
|
-
import cors from "cors";
|
|
478
479
|
import helmet from "helmet";
|
|
479
480
|
import compression from "compression";
|
|
480
481
|
|
|
481
|
-
app.plugin(adapt(cors()));
|
|
482
482
|
app.plugin(adapt(helmet()));
|
|
483
483
|
app.plugin(adapt(compression()));
|
|
484
484
|
```
|
|
485
485
|
|
|
486
|
+
> **Note:** Vibe ships with a native `cors()` helper. No need to adapt the `cors` npm package.
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## 🛡️ Rate Limiting
|
|
491
|
+
|
|
492
|
+
Built-in sliding window rate limiter — no external dependencies:
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
import vibe, { rateLimit } from "vibe-gx";
|
|
496
|
+
|
|
497
|
+
const app = vibe();
|
|
498
|
+
|
|
499
|
+
// Global limit: 100 requests per minute per IP
|
|
500
|
+
app.plugin(rateLimit({ max: 100, window: 60_000 }));
|
|
501
|
+
|
|
502
|
+
// Per-route: tight limit on login (brute force protection)
|
|
503
|
+
app.post(
|
|
504
|
+
"/auth/login",
|
|
505
|
+
{ intercept: rateLimit({ max: 5, window: 60_000 }) },
|
|
506
|
+
handler,
|
|
507
|
+
);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Sets `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers automatically.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 🌐 CORS
|
|
515
|
+
|
|
516
|
+
Built-in CORS with automatic preflight handling — no external dependencies:
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
import vibe, { cors } from "vibe-gx";
|
|
520
|
+
|
|
521
|
+
const app = vibe();
|
|
522
|
+
|
|
523
|
+
app.plugin(
|
|
524
|
+
cors({
|
|
525
|
+
origin: "https://myapp.com",
|
|
526
|
+
credentials: true,
|
|
527
|
+
maxAge: 86_400, // cache preflight for 24 hours
|
|
528
|
+
}),
|
|
529
|
+
);
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Supports wildcard, single origin, array of origins, and dynamic origin functions.
|
|
533
|
+
|
|
486
534
|
---
|
|
487
535
|
|
|
488
536
|
## 🔒 Security
|
|
@@ -602,18 +650,18 @@ app.post(
|
|
|
602
650
|
|
|
603
651
|
### Request (`req`)
|
|
604
652
|
|
|
605
|
-
| Property | Description
|
|
606
|
-
| :------------ |
|
|
607
|
-
| `req.id` |
|
|
608
|
-
| `req.log` |
|
|
609
|
-
| `req.params` | Route parameters (`:id`)
|
|
610
|
-
| `req.query` | Query string (`?page=1`)
|
|
611
|
-
| `req.body` | Parsed JSON/form body
|
|
612
|
-
| `req.files` | Uploaded files (multipart)
|
|
613
|
-
| `req.ip` |
|
|
614
|
-
| `req.method` | HTTP method
|
|
615
|
-
| `req.url` | Request URL
|
|
616
|
-
| `req.headers` | Request headers
|
|
653
|
+
| Property | Description |
|
|
654
|
+
| :------------ | :------------------------------------------------------- |
|
|
655
|
+
| `req.id` | Lazy UUID — generated only on first access |
|
|
656
|
+
| `req.log` | Lazy context-bound logger — created only on first access |
|
|
657
|
+
| `req.params` | Route parameters (`:id`) |
|
|
658
|
+
| `req.query` | Query string (`?page=1`) |
|
|
659
|
+
| `req.body` | Parsed JSON/form body |
|
|
660
|
+
| `req.files` | Uploaded files (multipart) |
|
|
661
|
+
| `req.ip` | Real client IP — proxy-aware (`x-forwarded-for` first) |
|
|
662
|
+
| `req.method` | HTTP method |
|
|
663
|
+
| `req.url` | Request URL (pathname only, query stripped) |
|
|
664
|
+
| `req.headers` | Request headers |
|
|
617
665
|
|
|
618
666
|
### Response (`res`)
|
|
619
667
|
|
|
@@ -665,6 +713,18 @@ app.post(
|
|
|
665
713
|
| `pool.close()` | Close pool |
|
|
666
714
|
| `pool.stats` | Get pool statistics |
|
|
667
715
|
|
|
716
|
+
### Rate Limit Utilities
|
|
717
|
+
|
|
718
|
+
| Function | Description |
|
|
719
|
+
| :---------------- | :----------------------------------- |
|
|
720
|
+
| `rateLimit(opts)` | Create a sliding window rate limiter |
|
|
721
|
+
|
|
722
|
+
### CORS Utilities
|
|
723
|
+
|
|
724
|
+
| Function | Description |
|
|
725
|
+
| :------------ | :------------------------ |
|
|
726
|
+
| `cors(opts?)` | Create a CORS interceptor |
|
|
727
|
+
|
|
668
728
|
---
|
|
669
729
|
|
|
670
730
|
## 📊 Benchmarks
|
package/package.json
CHANGED
package/utils/core/logger.js
CHANGED
|
@@ -9,6 +9,7 @@ const LOG_LEVELS = {
|
|
|
9
9
|
warn: 40,
|
|
10
10
|
error: 50,
|
|
11
11
|
fatal: 60,
|
|
12
|
+
silent: 100, // Higher than all levels — suppresses all output (logger: false)
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
const LEVEL_NAMES = {
|
|
@@ -136,45 +137,59 @@ export class Logger {
|
|
|
136
137
|
_printPretty(log) {
|
|
137
138
|
const time = new Date(log.time).toLocaleTimeString();
|
|
138
139
|
const lvlName = LEVEL_NAMES[log.level] || "INFO";
|
|
139
|
-
let prefixC = color.cyan;
|
|
140
|
-
if (log.level >= 50) prefixC = color.red;
|
|
141
|
-
else if (log.level === 40) prefixC = color.yellow;
|
|
142
|
-
else if (log.level <= 20) prefixC = color.dim;
|
|
143
|
-
|
|
144
|
-
const prefix = prefixC(`[VIBE ${lvlName} ${time}]`);
|
|
145
|
-
let context = "";
|
|
146
|
-
if (log.reqId) {
|
|
147
|
-
context = `\x1b[90m[${log.reqId}]\x1b[0m `;
|
|
148
|
-
}
|
|
149
140
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
const isError = log.level >= 50;
|
|
142
|
+
const isWarn = log.level === 40;
|
|
143
|
+
const isDebug = log.level <= 20;
|
|
144
|
+
|
|
145
|
+
// Build context tag (reqId)
|
|
146
|
+
const context = log.reqId ? `[${log.reqId}] ` : "";
|
|
154
147
|
|
|
148
|
+
// Build message content
|
|
149
|
+
let content = log.msg || "";
|
|
155
150
|
if (log.err && log.err.stack) {
|
|
156
|
-
content += "\n" +
|
|
151
|
+
content += "\n" + log.err.stack;
|
|
157
152
|
}
|
|
158
153
|
|
|
159
|
-
//
|
|
154
|
+
// Build metadata string (skip standard keys)
|
|
160
155
|
const skipKeys = [
|
|
161
|
-
"level",
|
|
162
|
-
"time",
|
|
163
|
-
"pid",
|
|
164
|
-
"hostname",
|
|
165
|
-
"reqId",
|
|
166
|
-
"msg",
|
|
167
|
-
"err",
|
|
168
|
-
"color",
|
|
156
|
+
"level", "time", "pid", "hostname", "reqId", "msg", "err", "color",
|
|
169
157
|
];
|
|
170
158
|
let metaStr = "";
|
|
171
159
|
for (const key of Object.keys(log)) {
|
|
172
160
|
if (!skipKeys.includes(key)) {
|
|
173
|
-
metaStr += `
|
|
161
|
+
metaStr += ` ${key}=${JSON.stringify(log[key])}`;
|
|
174
162
|
}
|
|
175
163
|
}
|
|
176
164
|
|
|
177
|
-
|
|
165
|
+
const rawPrefix = `[VIBE ${lvlName} ${time}]`;
|
|
166
|
+
|
|
167
|
+
if (isError) {
|
|
168
|
+
// Entire line is red — prefix, context, message, stack, metadata
|
|
169
|
+
const fullLine = `${rawPrefix} ${context}${content}${metaStr}`;
|
|
170
|
+
this.stream.write(color.red(fullLine) + "\n");
|
|
171
|
+
} else if (isWarn) {
|
|
172
|
+
// Yellow prefix, bright content
|
|
173
|
+
const coloredContent = log.color && color[log.color]
|
|
174
|
+
? color[log.color](content)
|
|
175
|
+
: color.bright(content);
|
|
176
|
+
this.stream.write(
|
|
177
|
+
color.yellow(rawPrefix) + " " + context + coloredContent +
|
|
178
|
+
(metaStr ? color.dim(metaStr) : "") + "\n",
|
|
179
|
+
);
|
|
180
|
+
} else if (isDebug) {
|
|
181
|
+
// Dim entire line for trace/debug
|
|
182
|
+
this.stream.write(color.dim(`${rawPrefix} ${context}${content}${metaStr}`) + "\n");
|
|
183
|
+
} else {
|
|
184
|
+
// Info — green prefix + bright content (matches [VIBE LOG] style)
|
|
185
|
+
const coloredContent = log.color && color[log.color]
|
|
186
|
+
? color[log.color](content)
|
|
187
|
+
: color.bright(content);
|
|
188
|
+
this.stream.write(
|
|
189
|
+
color.green(rawPrefix) + " " + context + coloredContent +
|
|
190
|
+
(metaStr ? color.dim(metaStr) : "") + "\n",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
178
193
|
}
|
|
179
194
|
}
|
|
180
195
|
|
package/utils/core/parser.js
CHANGED
|
@@ -103,7 +103,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
|
|
|
103
103
|
},
|
|
104
104
|
});
|
|
105
105
|
} catch (err) {
|
|
106
|
-
|
|
106
|
+
options.logger?.error(err, "[VIBE] Busboy init failed");
|
|
107
107
|
return resolve();
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -152,7 +152,10 @@ function parseMultipart(req, res, media, options, resolve, reject) {
|
|
|
152
152
|
media.public &&
|
|
153
153
|
!dest.startsWith(path.resolve(options.publicFolder || ""))
|
|
154
154
|
) {
|
|
155
|
-
|
|
155
|
+
options.logger?.warn(
|
|
156
|
+
{ dest, publicFolder: options.publicFolder },
|
|
157
|
+
"[VIBE] Attempted upload outside public folder, skipping",
|
|
158
|
+
);
|
|
156
159
|
pendingWrites--;
|
|
157
160
|
checkComplete();
|
|
158
161
|
return file.resume();
|
|
@@ -161,7 +164,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
|
|
|
161
164
|
try {
|
|
162
165
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
163
166
|
} catch (err) {
|
|
164
|
-
|
|
167
|
+
options.logger?.error(err, "[VIBE] Failed to create upload folder");
|
|
165
168
|
pendingWrites--;
|
|
166
169
|
checkComplete();
|
|
167
170
|
return file.resume();
|
|
@@ -200,14 +203,14 @@ function parseMultipart(req, res, media, options, resolve, reject) {
|
|
|
200
203
|
});
|
|
201
204
|
|
|
202
205
|
file.on("error", (err) => {
|
|
203
|
-
|
|
206
|
+
options.logger?.error(err, "[VIBE] File stream error");
|
|
204
207
|
writeStream.end();
|
|
205
208
|
pendingWrites--;
|
|
206
209
|
checkComplete();
|
|
207
210
|
});
|
|
208
211
|
|
|
209
212
|
writeStream.on("error", (err) => {
|
|
210
|
-
|
|
213
|
+
options.logger?.error(err, "[VIBE] Write stream error");
|
|
211
214
|
file.resume();
|
|
212
215
|
pendingWrites--;
|
|
213
216
|
checkComplete();
|
|
@@ -231,7 +234,7 @@ function parseMultipart(req, res, media, options, resolve, reject) {
|
|
|
231
234
|
});
|
|
232
235
|
|
|
233
236
|
bb.on("error", (err) => {
|
|
234
|
-
|
|
237
|
+
options.logger?.error(err, "[VIBE] Busboy error");
|
|
235
238
|
req.unpipe(bb);
|
|
236
239
|
reject(err);
|
|
237
240
|
});
|
|
@@ -266,7 +269,10 @@ function parseJson(req, res, media, options, resolve, reject) {
|
|
|
266
269
|
req.on("data", (chunk) => {
|
|
267
270
|
body += chunk;
|
|
268
271
|
if (body.length > limit) {
|
|
269
|
-
|
|
272
|
+
options.logger?.warn(
|
|
273
|
+
{ limit, received: body.length },
|
|
274
|
+
"[VIBE] JSON payload too large, destroying connection",
|
|
275
|
+
);
|
|
270
276
|
req.destroy();
|
|
271
277
|
}
|
|
272
278
|
});
|
package/utils/core/response.js
CHANGED
|
@@ -243,7 +243,12 @@ const vibeResponseMethods = {
|
|
|
243
243
|
* @param {Error} error
|
|
244
244
|
*/
|
|
245
245
|
serverError(error) {
|
|
246
|
-
|
|
246
|
+
const logger = this._vibeOptions?.logger;
|
|
247
|
+
if (logger) {
|
|
248
|
+
logger.error(error, "[VIBE] Internal server error");
|
|
249
|
+
} else {
|
|
250
|
+
console.error(error);
|
|
251
|
+
}
|
|
247
252
|
this.writeHead(500, JSON_CT);
|
|
248
253
|
this.end(RESPONSES.serverError);
|
|
249
254
|
},
|
package/utils/core/server.js
CHANGED
|
@@ -90,15 +90,35 @@ async function server(options, port, host, callback) {
|
|
|
90
90
|
|
|
91
91
|
// Main request handler - ULTRA OPTIMIZED
|
|
92
92
|
function reqListener(req, res) {
|
|
93
|
-
req.id
|
|
94
|
-
|
|
93
|
+
// Lazy req.id and req.log — UUID and child logger are only created
|
|
94
|
+
// on first access. Routes that don't log or need an ID pay zero cost.
|
|
95
|
+
let _reqId = null;
|
|
96
|
+
let _reqLog = null;
|
|
97
|
+
Object.defineProperty(req, "id", {
|
|
98
|
+
get() {
|
|
99
|
+
if (_reqId === null) _reqId = crypto.randomUUID();
|
|
100
|
+
return _reqId;
|
|
101
|
+
},
|
|
102
|
+
configurable: true,
|
|
103
|
+
});
|
|
104
|
+
Object.defineProperty(req, "log", {
|
|
105
|
+
get() {
|
|
106
|
+
if (_reqLog === null)
|
|
107
|
+
_reqLog = options.logger.child({ reqId: req.id });
|
|
108
|
+
return _reqLog;
|
|
109
|
+
},
|
|
110
|
+
configurable: true,
|
|
111
|
+
});
|
|
95
112
|
|
|
96
113
|
if (options.loggerConfig && options.loggerConfig.lifecycle) {
|
|
97
114
|
req.startTime = Date.now();
|
|
98
115
|
|
|
99
116
|
// Determine sender IP early for logging
|
|
100
117
|
const sender =
|
|
101
|
-
req.
|
|
118
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
119
|
+
req.headers["x-real-ip"] ||
|
|
120
|
+
req.socket.remoteAddress ||
|
|
121
|
+
"unknown";
|
|
102
122
|
|
|
103
123
|
req.log.info(
|
|
104
124
|
{ type: "req", url: req.url, method: req.method, sender },
|
|
@@ -129,6 +149,12 @@ async function server(options, port, host, callback) {
|
|
|
129
149
|
|
|
130
150
|
req.url = pathname;
|
|
131
151
|
|
|
152
|
+
// Resolve real client IP once — proxy-aware, available on ALL paths
|
|
153
|
+
req.ip =
|
|
154
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
155
|
+
req.headers["x-real-ip"] ||
|
|
156
|
+
req.socket.remoteAddress;
|
|
157
|
+
|
|
132
158
|
// Stamp response with options ref (ONLY per-request cost for response methods)
|
|
133
159
|
res._vibeOptions = options;
|
|
134
160
|
|
|
@@ -215,10 +241,7 @@ async function server(options, port, host, callback) {
|
|
|
215
241
|
if (!(await runIntercept(interceptors, req, res))) return;
|
|
216
242
|
}
|
|
217
243
|
|
|
218
|
-
// Lazy IP
|
|
219
|
-
if (!req.ip) {
|
|
220
|
-
req.ip = req.socket.remoteAddress || req.headers["x-forwarded-for"];
|
|
221
|
-
}
|
|
244
|
+
// Lazy IP already resolved in reqListener — no-op needed here
|
|
222
245
|
|
|
223
246
|
// Route matching - FAST PATH first
|
|
224
247
|
const routeKey = req.method + pathname;
|
|
@@ -296,8 +319,14 @@ async function server(options, port, host, callback) {
|
|
|
296
319
|
getNetworkIP(mainHost, port);
|
|
297
320
|
|
|
298
321
|
const strategy = useTrieMatching ? "Trie (O(log n))" : "Linear (O(n))";
|
|
299
|
-
|
|
300
|
-
|
|
322
|
+
options.logger.info(
|
|
323
|
+
{
|
|
324
|
+
strategy,
|
|
325
|
+
routeCount: options.routeCount,
|
|
326
|
+
staticRoutes: staticRoutes.size,
|
|
327
|
+
trieThreshold: options.trieThreshold,
|
|
328
|
+
},
|
|
329
|
+
"[VIBE] Route matching strategy initialized",
|
|
301
330
|
);
|
|
302
331
|
|
|
303
332
|
if (callback) callback();
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in CORS (Cross-Origin Resource Sharing) helper for Vibe.
|
|
3
|
+
*
|
|
4
|
+
* Returns an interceptor that handles preflight OPTIONS requests and sets
|
|
5
|
+
* the appropriate Access-Control-* headers on every response.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import vibe, { cors } from "vibe-gx";
|
|
9
|
+
* const app = vibe();
|
|
10
|
+
*
|
|
11
|
+
* app.plugin(cors({ origin: "https://myapp.com", credentials: true }));
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Pre-allocated headers for preflight responses
|
|
15
|
+
const PREFLIGHT_HEADERS = { "content-length": "0" };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} CorsOptions
|
|
19
|
+
* @property {string | string[] | ((origin: string) => boolean)} [origin="*"]
|
|
20
|
+
* Allowed origin(s). Can be a string, array of strings, or a function
|
|
21
|
+
* that returns true/false for a given origin.
|
|
22
|
+
* @property {string[]} [methods] Allowed HTTP methods. Default: common methods
|
|
23
|
+
* @property {string[]} [allowedHeaders] Allowed request headers
|
|
24
|
+
* @property {string[]} [exposedHeaders] Headers exposed to the browser
|
|
25
|
+
* @property {boolean} [credentials=false] Allow cookies / auth headers
|
|
26
|
+
* @property {number} [maxAge] Preflight cache duration in seconds
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a CORS interceptor.
|
|
31
|
+
*
|
|
32
|
+
* - Handles OPTIONS preflight requests automatically (responds 204 and stops).
|
|
33
|
+
* - Sets Access-Control-* headers on every request.
|
|
34
|
+
* - Supports wildcard, single origin, array of origins, or dynamic function.
|
|
35
|
+
*
|
|
36
|
+
* @param {CorsOptions} [opts={}]
|
|
37
|
+
* @returns {import("../vibe.js").Interceptor}
|
|
38
|
+
*/
|
|
39
|
+
export function cors(opts = {}) {
|
|
40
|
+
const {
|
|
41
|
+
origin = "*",
|
|
42
|
+
methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
|
|
43
|
+
allowedHeaders = ["Content-Type", "Authorization"],
|
|
44
|
+
exposedHeaders = [],
|
|
45
|
+
credentials = false,
|
|
46
|
+
maxAge,
|
|
47
|
+
} = opts;
|
|
48
|
+
|
|
49
|
+
// Pre-compute static header values where possible
|
|
50
|
+
const methodsStr = methods.join(", ");
|
|
51
|
+
const allowedHeadersStr = allowedHeaders.join(", ");
|
|
52
|
+
const exposedHeadersStr = exposedHeaders.length
|
|
53
|
+
? exposedHeaders.join(", ")
|
|
54
|
+
: null;
|
|
55
|
+
const maxAgeStr = maxAge != null ? String(maxAge) : null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the Access-Control-Allow-Origin value for a given request origin.
|
|
59
|
+
* @param {string} requestOrigin
|
|
60
|
+
* @returns {string | null} The value to set, or null to omit the header
|
|
61
|
+
*/
|
|
62
|
+
function resolveOrigin(requestOrigin) {
|
|
63
|
+
if (origin === "*") return "*";
|
|
64
|
+
|
|
65
|
+
if (typeof origin === "function") {
|
|
66
|
+
return origin(requestOrigin) ? requestOrigin : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(origin)) {
|
|
70
|
+
return origin.includes(requestOrigin) ? requestOrigin : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Single string
|
|
74
|
+
return origin === requestOrigin ? requestOrigin : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The interceptor returned to app.plugin().
|
|
79
|
+
* @param {import("../vibe.js").VibeRequest} req
|
|
80
|
+
* @param {import("../vibe.js").VibeResponse} res
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
return function corsInterceptor(req, res) {
|
|
84
|
+
const requestOrigin = req.headers["origin"];
|
|
85
|
+
|
|
86
|
+
// No Origin header — not a cross-origin request, skip CORS headers
|
|
87
|
+
if (!requestOrigin) return true;
|
|
88
|
+
|
|
89
|
+
const allowedOrigin = resolveOrigin(requestOrigin);
|
|
90
|
+
|
|
91
|
+
if (allowedOrigin) {
|
|
92
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
93
|
+
|
|
94
|
+
// If not wildcard, tell proxies/CDNs the response varies by Origin
|
|
95
|
+
if (allowedOrigin !== "*") {
|
|
96
|
+
res.setHeader("Vary", "Origin");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (credentials) {
|
|
101
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (exposedHeadersStr) {
|
|
105
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeadersStr);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle preflight (OPTIONS) — respond immediately and stop
|
|
109
|
+
if (req.method === "OPTIONS") {
|
|
110
|
+
res.setHeader("Access-Control-Allow-Methods", methodsStr);
|
|
111
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeadersStr);
|
|
112
|
+
|
|
113
|
+
if (maxAgeStr) {
|
|
114
|
+
res.setHeader("Access-Control-Max-Age", maxAgeStr);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
res.writeHead(204, PREFLIGHT_HEADERS);
|
|
118
|
+
res.end();
|
|
119
|
+
return false; // Stop further processing
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in sliding window rate limiter for Vibe.
|
|
3
|
+
*
|
|
4
|
+
* Works as both a global plugin and a per-route interceptor.
|
|
5
|
+
* Uses an in-memory Map with automatic TTL-based cleanup.
|
|
6
|
+
* No external dependencies.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Global — all routes
|
|
10
|
+
* import { rateLimit } from "vibe-gx";
|
|
11
|
+
* app.plugin(rateLimit({ max: 100, window: 60_000 }));
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // Per-route — tight limit on login
|
|
15
|
+
* app.post("/auth/login", { intercept: rateLimit({ max: 5, window: 60_000 }) }, handler);
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} RateLimitOptions
|
|
20
|
+
* @property {number} max - Maximum number of requests allowed per window
|
|
21
|
+
* @property {number} [window=60000] - Window duration in milliseconds. Default: 60s
|
|
22
|
+
* @property {(req: import("../vibe.js").VibeRequest) => string} [keyBy] - Custom key function. Default: req.ip
|
|
23
|
+
* @property {string} [message] - Custom error message. Default: "Too Many Requests"
|
|
24
|
+
* @property {number} [statusCode=429] - HTTP status code when limit exceeded. Default: 429
|
|
25
|
+
* @property {(req: import("../vibe.js").VibeRequest) => boolean} [skip] - Return true to bypass the limiter for a request
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Pre-allocated 429 response headers
|
|
29
|
+
const RATE_LIMIT_HEADERS = { "content-type": "text/plain" };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a rate limiter interceptor using a sliding window algorithm.
|
|
33
|
+
*
|
|
34
|
+
* Each unique key (default: IP address) gets a counter and a window start time.
|
|
35
|
+
* When the window expires the counter resets. When the counter exceeds `max`
|
|
36
|
+
* the request is rejected with 429 and a `Retry-After` header.
|
|
37
|
+
*
|
|
38
|
+
* @param {RateLimitOptions} opts
|
|
39
|
+
* @returns {import("../vibe.js").Interceptor}
|
|
40
|
+
*/
|
|
41
|
+
export function rateLimit(opts = {}) {
|
|
42
|
+
const max = opts.max;
|
|
43
|
+
|
|
44
|
+
if (!max || typeof max !== "number" || max < 1) {
|
|
45
|
+
throw new Error("[vibe] rateLimit: `max` must be a positive number");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const windowMs = opts.window ?? 60_000;
|
|
49
|
+
const message = opts.message ?? "Too Many Requests";
|
|
50
|
+
const statusCode = opts.statusCode ?? 429;
|
|
51
|
+
const keyBy = opts.keyBy ?? ((req) => req.ip ?? "unknown");
|
|
52
|
+
const skip = opts.skip ?? null;
|
|
53
|
+
|
|
54
|
+
// In-memory store: key → { count, windowStart }
|
|
55
|
+
// Entries are cleaned up automatically when the window expires
|
|
56
|
+
const store = new Map();
|
|
57
|
+
|
|
58
|
+
// Periodic cleanup to prevent unbounded memory growth.
|
|
59
|
+
// Runs every `windowMs` and removes all expired entries.
|
|
60
|
+
const cleanupInterval = setInterval(() => {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
for (const [key, entry] of store) {
|
|
63
|
+
if (now - entry.windowStart >= windowMs) {
|
|
64
|
+
store.delete(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, windowMs);
|
|
68
|
+
|
|
69
|
+
// Don't keep the process alive just for cleanup
|
|
70
|
+
if (cleanupInterval.unref) cleanupInterval.unref();
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The interceptor function returned to app.plugin() or route intercept.
|
|
74
|
+
* @param {import("../vibe.js").VibeRequest} req
|
|
75
|
+
* @param {import("../vibe.js").VibeResponse} res
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
return function rateLimitInterceptor(req, res) {
|
|
79
|
+
// Allow bypassing for certain requests (e.g. internal health checks)
|
|
80
|
+
if (skip && skip(req)) return true;
|
|
81
|
+
|
|
82
|
+
const key = keyBy(req);
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
|
|
85
|
+
let entry = store.get(key);
|
|
86
|
+
|
|
87
|
+
// No entry yet or window has expired — start a fresh window
|
|
88
|
+
if (!entry || now - entry.windowStart >= windowMs) {
|
|
89
|
+
entry = { count: 1, windowStart: now };
|
|
90
|
+
store.set(key, entry);
|
|
91
|
+
} else {
|
|
92
|
+
entry.count++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const remaining = Math.max(0, max - entry.count);
|
|
96
|
+
const resetInMs = windowMs - (now - entry.windowStart);
|
|
97
|
+
const resetInSeconds = Math.ceil(resetInMs / 1000);
|
|
98
|
+
|
|
99
|
+
// Always set informational headers
|
|
100
|
+
res.setHeader("X-RateLimit-Limit", max);
|
|
101
|
+
res.setHeader("X-RateLimit-Remaining", remaining);
|
|
102
|
+
res.setHeader("X-RateLimit-Reset", Math.ceil((entry.windowStart + windowMs) / 1000));
|
|
103
|
+
|
|
104
|
+
if (entry.count > max) {
|
|
105
|
+
res.setHeader("Retry-After", resetInSeconds);
|
|
106
|
+
res.writeHead(statusCode, RATE_LIMIT_HEADERS);
|
|
107
|
+
res.end(message);
|
|
108
|
+
return false; // Stop request processing
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
113
|
+
}
|
package/vibe.d.ts
CHANGED
|
@@ -219,8 +219,6 @@ export interface LoggerAPI {
|
|
|
219
219
|
export interface VibeConfig {
|
|
220
220
|
/** Configuration for the native Vibe terminal logger */
|
|
221
221
|
logger?: LoggerConfig | boolean;
|
|
222
|
-
/** Enable automatic process restarting on crash. Spawns an internal cluster manager. Default: false */
|
|
223
|
-
autoRestart?: boolean;
|
|
224
222
|
}
|
|
225
223
|
|
|
226
224
|
// ==========================================
|
|
@@ -239,10 +237,8 @@ export interface VibeRequest extends IncomingMessage {
|
|
|
239
237
|
body: Record<string, any>;
|
|
240
238
|
/** Uploaded files array (if multipart/form-data) */
|
|
241
239
|
files?: UploadedFile[];
|
|
242
|
-
/**
|
|
240
|
+
/** Real client IP — first entry from x-forwarded-for, x-real-ip, or socket address */
|
|
243
241
|
ip?: string;
|
|
244
|
-
/** Detailed client IP info */
|
|
245
|
-
fullIp?: string;
|
|
246
242
|
/** Automatically generated UUID for the request lifecycle */
|
|
247
243
|
id: string;
|
|
248
244
|
/** Context-bound logger automatically stamped with the req.id constraint */
|
|
@@ -304,9 +300,58 @@ export type Interceptor = (
|
|
|
304
300
|
res: VibeResponse,
|
|
305
301
|
) => boolean | void | Promise<boolean | void>;
|
|
306
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Scoped app interface passed to register() plugin callbacks.
|
|
305
|
+
* A subset of VibeApp — excludes server-level methods that make no sense
|
|
306
|
+
* inside an encapsulated plugin (listen, logRoutes, setPublicFolder, include).
|
|
307
|
+
*/
|
|
308
|
+
export interface ScopedVibeApp {
|
|
309
|
+
get: RouteRegistrar;
|
|
310
|
+
post: RouteRegistrar;
|
|
311
|
+
put: RouteRegistrar;
|
|
312
|
+
del: RouteRegistrar;
|
|
313
|
+
patch: RouteRegistrar;
|
|
314
|
+
head: RouteRegistrar;
|
|
315
|
+
|
|
316
|
+
/** Register a global interceptor within this plugin scope */
|
|
317
|
+
plugin: (interceptor: Interceptor) => void;
|
|
318
|
+
|
|
319
|
+
/** Register a nested plugin */
|
|
320
|
+
register: (fn: PluginCallback, opts?: RegisterOptions) => Promise<void>;
|
|
321
|
+
|
|
322
|
+
/** Decorate the app with a custom property */
|
|
323
|
+
decorate: (name: string, value: any) => void;
|
|
324
|
+
|
|
325
|
+
/** Decorate request objects with a custom property */
|
|
326
|
+
decorateRequest: (name: string, value: any) => void;
|
|
327
|
+
|
|
328
|
+
/** Decorate response objects with a custom property */
|
|
329
|
+
decorateReply: (name: string, value: any) => void;
|
|
330
|
+
|
|
331
|
+
/** Override the error handler for this plugin scope */
|
|
332
|
+
setErrorHandler: (
|
|
333
|
+
fn: (error: Error, req: VibeRequest, res: VibeResponse) => void,
|
|
334
|
+
) => void;
|
|
335
|
+
|
|
336
|
+
/** Structured logger — app.log.info(), .warn(), .error() etc. */
|
|
337
|
+
log: LoggerAPI;
|
|
338
|
+
|
|
339
|
+
/** Alias for log */
|
|
340
|
+
logger: LoggerAPI;
|
|
341
|
+
|
|
342
|
+
/** Legacy colorized string logger */
|
|
343
|
+
logLegacy: (
|
|
344
|
+
value: any,
|
|
345
|
+
typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
|
|
346
|
+
) => void;
|
|
347
|
+
|
|
348
|
+
/** Any decorators registered via decorate() are available as direct properties */
|
|
349
|
+
[key: string]: any;
|
|
350
|
+
}
|
|
351
|
+
|
|
307
352
|
/** Plugin callback function (Fastify-style) */
|
|
308
353
|
export type PluginCallback = (
|
|
309
|
-
app:
|
|
354
|
+
app: ScopedVibeApp,
|
|
310
355
|
opts: RegisterOptions,
|
|
311
356
|
) => void | Promise<void>;
|
|
312
357
|
|
|
@@ -393,11 +438,23 @@ export interface RouterAPI {
|
|
|
393
438
|
head: RouteRegistrar;
|
|
394
439
|
|
|
395
440
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
* @
|
|
441
|
+
* Pino/Fastify-compatible structured logger.
|
|
442
|
+
* Available inside both `app.register()` plugins and `app.include()` sub-routers.
|
|
443
|
+
* @example
|
|
444
|
+
* api.log.info("Route registered");
|
|
445
|
+
* api.log.warn({ userId: 1 }, "Slow query");
|
|
446
|
+
*/
|
|
447
|
+
log: LoggerAPI;
|
|
448
|
+
|
|
449
|
+
/** Alias for `log` — consistent with `app.logger` */
|
|
450
|
+
logger: LoggerAPI;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Legacy colorized string logger (plain terminal output).
|
|
454
|
+
* @param value The message to log
|
|
455
|
+
* @param typeOrColor Optional color name (e.g. 'green', 'red')
|
|
399
456
|
*/
|
|
400
|
-
|
|
457
|
+
logLegacy: (
|
|
401
458
|
value: any,
|
|
402
459
|
typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
|
|
403
460
|
) => void;
|
|
@@ -489,6 +546,17 @@ export interface VibeApp extends RouterAPI {
|
|
|
489
546
|
|
|
490
547
|
/** Alias for `app.log` */
|
|
491
548
|
logger: LoggerAPI;
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Legacy colorized string logger (plain terminal output).
|
|
552
|
+
* Bypasses the Pino JSON interface — for simple dev-time messages.
|
|
553
|
+
* @param value The message to log
|
|
554
|
+
* @param typeOrColor Optional color name (e.g. 'green', 'red')
|
|
555
|
+
*/
|
|
556
|
+
logLegacy: (
|
|
557
|
+
value: any,
|
|
558
|
+
typeOrColor?: ColorName | "info" | "error" | "warn" | "req",
|
|
559
|
+
) => void;
|
|
492
560
|
}
|
|
493
561
|
|
|
494
562
|
/**
|
|
@@ -648,6 +716,98 @@ export function parseJsonStream(
|
|
|
648
716
|
onError?: (err: Error) => void,
|
|
649
717
|
): void;
|
|
650
718
|
|
|
719
|
+
// ==========================================
|
|
720
|
+
// Rate Limiting
|
|
721
|
+
// ==========================================
|
|
722
|
+
|
|
723
|
+
export interface RateLimitOptions {
|
|
724
|
+
/** Maximum number of requests allowed per window */
|
|
725
|
+
max: number;
|
|
726
|
+
/** Window duration in milliseconds. Default: 60000 (1 minute) */
|
|
727
|
+
window?: number;
|
|
728
|
+
/**
|
|
729
|
+
* Custom function to derive the rate limit key from the request.
|
|
730
|
+
* Defaults to req.ip.
|
|
731
|
+
* @example keyBy: (req) => req.headers["authorization"] // limit per token
|
|
732
|
+
*/
|
|
733
|
+
keyBy?: (req: VibeRequest) => string;
|
|
734
|
+
/** Custom message sent when limit is exceeded. Default: "Too Many Requests" */
|
|
735
|
+
message?: string;
|
|
736
|
+
/** HTTP status code when limit is exceeded. Default: 429 */
|
|
737
|
+
statusCode?: number;
|
|
738
|
+
/**
|
|
739
|
+
* Function to skip rate limiting for certain requests.
|
|
740
|
+
* Return true to bypass the limiter.
|
|
741
|
+
* @example skip: (req) => req.ip === "127.0.0.1"
|
|
742
|
+
*/
|
|
743
|
+
skip?: (req: VibeRequest) => boolean;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Creates a sliding window rate limiter interceptor.
|
|
748
|
+
*
|
|
749
|
+
* Works as a global plugin or a per-route interceptor.
|
|
750
|
+
* Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.
|
|
751
|
+
* Returns 429 with a Retry-After header when the limit is exceeded.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* // Global rate limit
|
|
755
|
+
* import { rateLimit } from "vibe-gx";
|
|
756
|
+
* app.plugin(rateLimit({ max: 100, window: 60_000 }));
|
|
757
|
+
*
|
|
758
|
+
* @example
|
|
759
|
+
* // Per-route (tight limit on login)
|
|
760
|
+
* app.post("/auth/login", { intercept: rateLimit({ max: 5, window: 60_000 }) }, handler);
|
|
761
|
+
*/
|
|
762
|
+
export function rateLimit(options: RateLimitOptions): Interceptor;
|
|
763
|
+
|
|
764
|
+
// ==========================================
|
|
765
|
+
// CORS
|
|
766
|
+
// ==========================================
|
|
767
|
+
|
|
768
|
+
export interface CorsOptions {
|
|
769
|
+
/**
|
|
770
|
+
* Allowed origin(s). Can be:
|
|
771
|
+
* - `"*"` to allow all origins
|
|
772
|
+
* - A single origin string e.g. `"https://myapp.com"`
|
|
773
|
+
* - An array of allowed origins
|
|
774
|
+
* - A function `(origin: string) => boolean` for dynamic allow/deny
|
|
775
|
+
* Default: `"*"`
|
|
776
|
+
*/
|
|
777
|
+
origin?: string | string[] | ((origin: string) => boolean);
|
|
778
|
+
/** Allowed HTTP methods. Default: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS */
|
|
779
|
+
methods?: string[];
|
|
780
|
+
/** Headers the browser is allowed to send. Default: Content-Type, Authorization */
|
|
781
|
+
allowedHeaders?: string[];
|
|
782
|
+
/** Headers exposed to the browser in the response */
|
|
783
|
+
exposedHeaders?: string[];
|
|
784
|
+
/** Allow cookies and Authorization headers. Default: false */
|
|
785
|
+
credentials?: boolean;
|
|
786
|
+
/** Seconds to cache the preflight response. Reduces OPTIONS calls from the browser */
|
|
787
|
+
maxAge?: number;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Creates a CORS interceptor.
|
|
792
|
+
*
|
|
793
|
+
* Handles OPTIONS preflight requests automatically and sets
|
|
794
|
+
* Access-Control-* headers on every cross-origin response.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* import { cors } from "vibe-gx";
|
|
798
|
+
*
|
|
799
|
+
* // Allow all origins
|
|
800
|
+
* app.plugin(cors());
|
|
801
|
+
*
|
|
802
|
+
* // Specific origin with credentials
|
|
803
|
+
* app.plugin(cors({ origin: "https://myapp.com", credentials: true }));
|
|
804
|
+
*
|
|
805
|
+
* // Multiple origins
|
|
806
|
+
* app.plugin(cors({ origin: ["https://myapp.com", "https://admin.myapp.com"] }));
|
|
807
|
+
*/
|
|
808
|
+
export function cors(options?: CorsOptions): Interceptor;
|
|
809
|
+
|
|
810
|
+
|
|
651
811
|
// ==========================================
|
|
652
812
|
// Express Middleware Adapter
|
|
653
813
|
// ==========================================
|
|
@@ -669,11 +829,3 @@ export function adapt(
|
|
|
669
829
|
mw: (req: any, res: any, next: (err?: any) => void) => void,
|
|
670
830
|
): Interceptor;
|
|
671
831
|
|
|
672
|
-
/**
|
|
673
|
-
* Adapt multiple Express middlewares at once.
|
|
674
|
-
* @param middlewares - Express middleware functions
|
|
675
|
-
* @returns Array of Vibe-compatible interceptors
|
|
676
|
-
*/
|
|
677
|
-
export function adaptAll(
|
|
678
|
-
...middlewares: Array<(req: any, res: any, next: (err?: any) => void) => void>
|
|
679
|
-
): Interceptor[];
|
package/vibe.js
CHANGED
|
@@ -37,7 +37,6 @@ function pathToRegex(path) {
|
|
|
37
37
|
* size: number
|
|
38
38
|
* }>,
|
|
39
39
|
* ip?: string,
|
|
40
|
-
* fullIp?: string
|
|
41
40
|
* }} VibeRequest
|
|
42
41
|
*/
|
|
43
42
|
|
|
@@ -134,9 +133,12 @@ function pathToRegex(path) {
|
|
|
134
133
|
* Initializes a Vibe application instance.
|
|
135
134
|
* @param {Object} [config={}]
|
|
136
135
|
* @param {Object|boolean} [config.logger] - Logger configuration
|
|
137
|
-
* @param {boolean} [config.autoRestart] - Restart server automatically on crash
|
|
138
136
|
* @returns {VibeApp}
|
|
139
137
|
*/
|
|
138
|
+
// Guard to ensure process-level listeners are only registered once
|
|
139
|
+
// across multiple vibe() instances (e.g. in test environments)
|
|
140
|
+
let _processListenersInstalled = false;
|
|
141
|
+
|
|
140
142
|
const vibe = (config = {}) => {
|
|
141
143
|
// Route trie for O(log n) matching (used when routes > threshold)
|
|
142
144
|
const trie = new RouteTrie();
|
|
@@ -145,7 +147,7 @@ const vibe = (config = {}) => {
|
|
|
145
147
|
const routes = [];
|
|
146
148
|
|
|
147
149
|
// Threshold for switching between linear and trie matching
|
|
148
|
-
const TRIE_THRESHOLD =
|
|
150
|
+
const TRIE_THRESHOLD = 40;
|
|
149
151
|
|
|
150
152
|
// Static routes Map for O(1) lookup (routes without params)
|
|
151
153
|
const staticRoutes = new Map();
|
|
@@ -175,21 +177,24 @@ const vibe = (config = {}) => {
|
|
|
175
177
|
errorHandler: handleError,
|
|
176
178
|
};
|
|
177
179
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// cluster manager will restart it.
|
|
183
|
-
setTimeout(() => process.exit(1), 100);
|
|
184
|
-
});
|
|
180
|
+
// Register global process listeners once only (prevents listener leak
|
|
181
|
+
// when vibe() is called multiple times e.g. in tests)
|
|
182
|
+
if (!_processListenersInstalled) {
|
|
183
|
+
_processListenersInstalled = true;
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
185
|
+
process.on("uncaughtException", (err) => {
|
|
186
|
+
appLogger.fatal(err, "Uncaught Exception crashed the server");
|
|
187
|
+
setTimeout(() => process.exit(1), 100);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
process.on("unhandledRejection", (reason) => {
|
|
191
|
+
appLogger.fatal(
|
|
192
|
+
{ err: reason },
|
|
193
|
+
"Unhandled Promise Rejection crashed the server",
|
|
194
|
+
);
|
|
195
|
+
setTimeout(() => process.exit(1), 100);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
193
198
|
|
|
194
199
|
// Register default landing route
|
|
195
200
|
const defaultRoute = {
|
|
@@ -395,13 +400,7 @@ const vibe = (config = {}) => {
|
|
|
395
400
|
host = undefined;
|
|
396
401
|
}
|
|
397
402
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (config.autoRestart) {
|
|
401
|
-
clusterize(startServer, { workers: 1, restart: true });
|
|
402
|
-
} else {
|
|
403
|
-
startServer();
|
|
404
|
-
}
|
|
403
|
+
server(options, Number(port), host, callback);
|
|
405
404
|
}
|
|
406
405
|
|
|
407
406
|
/**
|
|
@@ -430,21 +429,34 @@ const vibe = (config = {}) => {
|
|
|
430
429
|
decorateRequest,
|
|
431
430
|
decorateReply,
|
|
432
431
|
register,
|
|
433
|
-
log,
|
|
432
|
+
log: appLogger, // Structured logger (api.log.info / warn / error etc.)
|
|
433
|
+
logger: appLogger, // Alias — consistent with root app.logger
|
|
434
|
+
logLegacy: log, // Legacy colorized string logger (api.logLegacy(msg, color))
|
|
435
|
+
setErrorHandler: (fn) => { options.errorHandler = fn; },
|
|
434
436
|
// Expose decorators
|
|
435
437
|
...options.decorators,
|
|
436
438
|
};
|
|
437
439
|
|
|
438
|
-
// Execute plugin
|
|
440
|
+
// Execute plugin — invoke fn() synchronously first so all route
|
|
441
|
+
// registrations that happen synchronously inside the plugin use the
|
|
442
|
+
// correct prefix. Restore currentPrefix IMMEDIATELY after the call
|
|
443
|
+
// (before any await) so that concurrent un-awaited register() calls
|
|
444
|
+
// from the caller cannot inherit this plugin's prefix.
|
|
445
|
+
let result;
|
|
439
446
|
try {
|
|
440
|
-
|
|
441
|
-
if (result && result.then) {
|
|
442
|
-
await result;
|
|
443
|
-
}
|
|
447
|
+
result = fn(scopedApp, opts);
|
|
444
448
|
} finally {
|
|
445
|
-
// Restore prefix
|
|
449
|
+
// Restore prefix synchronously — this is the critical fix.
|
|
450
|
+
// If fn() is async its routes should already be registered
|
|
451
|
+
// synchronously at the top of the function; the await below is
|
|
452
|
+
// only needed to propagate rejections.
|
|
446
453
|
currentPrefix = previousPrefix;
|
|
447
454
|
}
|
|
455
|
+
|
|
456
|
+
// Await async plugins for error propagation only (prefix already restored)
|
|
457
|
+
if (result && typeof result.then === "function") {
|
|
458
|
+
await result;
|
|
459
|
+
}
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
/**
|
|
@@ -476,7 +488,9 @@ const vibe = (config = {}) => {
|
|
|
476
488
|
del: wrap("DELETE"),
|
|
477
489
|
patch: wrap("PATCH"),
|
|
478
490
|
head: wrap("HEAD"),
|
|
479
|
-
log,
|
|
491
|
+
log: appLogger, // Structured logger — consistent with app.log
|
|
492
|
+
logger: appLogger, // Alias
|
|
493
|
+
logLegacy: log, // Legacy colorized string logger
|
|
480
494
|
plugin,
|
|
481
495
|
};
|
|
482
496
|
}
|
|
@@ -630,3 +644,6 @@ export {
|
|
|
630
644
|
export { LRUCache, cacheMiddleware } from "./utils/scaling/cache.js";
|
|
631
645
|
export { Pool, createPool } from "./utils/scaling/pool.js";
|
|
632
646
|
export { parseJsonStream } from "./utils/core/parser.js";
|
|
647
|
+
export { rateLimit } from "./utils/scaling/rate-limit.js";
|
|
648
|
+
export { cors } from "./utils/helpers/cors.js";
|
|
649
|
+
|