vibe-gx 4.1.2 → 4.1.4

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 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` | Auto-generated UUID logic |
608
- | `req.log` | Context-bound logger API |
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` | Client IP address |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "A lightweight, high-performance Node.js web framework.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
@@ -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 = crypto.randomUUID();
94
- req.log = options.logger.child({ reqId: req.id });
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.socket.remoteAddress || req.headers["x-forwarded-for"] || "unknown";
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;
@@ -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
  // ==========================================
@@ -648,6 +646,98 @@ export function parseJsonStream(
648
646
  onError?: (err: Error) => void,
649
647
  ): void;
650
648
 
649
+ // ==========================================
650
+ // Rate Limiting
651
+ // ==========================================
652
+
653
+ export interface RateLimitOptions {
654
+ /** Maximum number of requests allowed per window */
655
+ max: number;
656
+ /** Window duration in milliseconds. Default: 60000 (1 minute) */
657
+ window?: number;
658
+ /**
659
+ * Custom function to derive the rate limit key from the request.
660
+ * Defaults to req.ip.
661
+ * @example keyBy: (req) => req.headers["authorization"] // limit per token
662
+ */
663
+ keyBy?: (req: VibeRequest) => string;
664
+ /** Custom message sent when limit is exceeded. Default: "Too Many Requests" */
665
+ message?: string;
666
+ /** HTTP status code when limit is exceeded. Default: 429 */
667
+ statusCode?: number;
668
+ /**
669
+ * Function to skip rate limiting for certain requests.
670
+ * Return true to bypass the limiter.
671
+ * @example skip: (req) => req.ip === "127.0.0.1"
672
+ */
673
+ skip?: (req: VibeRequest) => boolean;
674
+ }
675
+
676
+ /**
677
+ * Creates a sliding window rate limiter interceptor.
678
+ *
679
+ * Works as a global plugin or a per-route interceptor.
680
+ * Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.
681
+ * Returns 429 with a Retry-After header when the limit is exceeded.
682
+ *
683
+ * @example
684
+ * // Global rate limit
685
+ * import { rateLimit } from "vibe-gx";
686
+ * app.plugin(rateLimit({ max: 100, window: 60_000 }));
687
+ *
688
+ * @example
689
+ * // Per-route (tight limit on login)
690
+ * app.post("/auth/login", { intercept: rateLimit({ max: 5, window: 60_000 }) }, handler);
691
+ */
692
+ export function rateLimit(options: RateLimitOptions): Interceptor;
693
+
694
+ // ==========================================
695
+ // CORS
696
+ // ==========================================
697
+
698
+ export interface CorsOptions {
699
+ /**
700
+ * Allowed origin(s). Can be:
701
+ * - `"*"` to allow all origins
702
+ * - A single origin string e.g. `"https://myapp.com"`
703
+ * - An array of allowed origins
704
+ * - A function `(origin: string) => boolean` for dynamic allow/deny
705
+ * Default: `"*"`
706
+ */
707
+ origin?: string | string[] | ((origin: string) => boolean);
708
+ /** Allowed HTTP methods. Default: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS */
709
+ methods?: string[];
710
+ /** Headers the browser is allowed to send. Default: Content-Type, Authorization */
711
+ allowedHeaders?: string[];
712
+ /** Headers exposed to the browser in the response */
713
+ exposedHeaders?: string[];
714
+ /** Allow cookies and Authorization headers. Default: false */
715
+ credentials?: boolean;
716
+ /** Seconds to cache the preflight response. Reduces OPTIONS calls from the browser */
717
+ maxAge?: number;
718
+ }
719
+
720
+ /**
721
+ * Creates a CORS interceptor.
722
+ *
723
+ * Handles OPTIONS preflight requests automatically and sets
724
+ * Access-Control-* headers on every cross-origin response.
725
+ *
726
+ * @example
727
+ * import { cors } from "vibe-gx";
728
+ *
729
+ * // Allow all origins
730
+ * app.plugin(cors());
731
+ *
732
+ * // Specific origin with credentials
733
+ * app.plugin(cors({ origin: "https://myapp.com", credentials: true }));
734
+ *
735
+ * // Multiple origins
736
+ * app.plugin(cors({ origin: ["https://myapp.com", "https://admin.myapp.com"] }));
737
+ */
738
+ export function cors(options?: CorsOptions): Interceptor;
739
+
740
+
651
741
  // ==========================================
652
742
  // Express Middleware Adapter
653
743
  // ==========================================
package/vibe.js CHANGED
@@ -134,9 +134,12 @@ function pathToRegex(path) {
134
134
  * Initializes a Vibe application instance.
135
135
  * @param {Object} [config={}]
136
136
  * @param {Object|boolean} [config.logger] - Logger configuration
137
- * @param {boolean} [config.autoRestart] - Restart server automatically on crash
138
137
  * @returns {VibeApp}
139
138
  */
139
+ // Guard to ensure process-level listeners are only registered once
140
+ // across multiple vibe() instances (e.g. in test environments)
141
+ let _processListenersInstalled = false;
142
+
140
143
  const vibe = (config = {}) => {
141
144
  // Route trie for O(log n) matching (used when routes > threshold)
142
145
  const trie = new RouteTrie();
@@ -145,7 +148,7 @@ const vibe = (config = {}) => {
145
148
  const routes = [];
146
149
 
147
150
  // Threshold for switching between linear and trie matching
148
- const TRIE_THRESHOLD = 50;
151
+ const TRIE_THRESHOLD = 40;
149
152
 
150
153
  // Static routes Map for O(1) lookup (routes without params)
151
154
  const staticRoutes = new Map();
@@ -175,21 +178,24 @@ const vibe = (config = {}) => {
175
178
  errorHandler: handleError,
176
179
  };
177
180
 
178
- // Add global uncaught exception handler to prevent silent deaths and log cleanly
179
- process.on("uncaughtException", (err) => {
180
- appLogger.fatal(err, "Uncaught Exception crashed the server");
181
- // Only exit here if we're not exiting gracefully anyway,
182
- // cluster manager will restart it.
183
- setTimeout(() => process.exit(1), 100);
184
- });
181
+ // Register global process listeners once only (prevents listener leak
182
+ // when vibe() is called multiple times e.g. in tests)
183
+ if (!_processListenersInstalled) {
184
+ _processListenersInstalled = true;
185
185
 
186
- process.on("unhandledRejection", (reason, promise) => {
187
- appLogger.fatal(
188
- { err: reason },
189
- "Unhandled Promise Rejection crashed the server",
190
- );
191
- setTimeout(() => process.exit(1), 100);
192
- });
186
+ process.on("uncaughtException", (err) => {
187
+ appLogger.fatal(err, "Uncaught Exception crashed the server");
188
+ setTimeout(() => process.exit(1), 100);
189
+ });
190
+
191
+ process.on("unhandledRejection", (reason) => {
192
+ appLogger.fatal(
193
+ { err: reason },
194
+ "Unhandled Promise Rejection crashed the server",
195
+ );
196
+ setTimeout(() => process.exit(1), 100);
197
+ });
198
+ }
193
199
 
194
200
  // Register default landing route
195
201
  const defaultRoute = {
@@ -395,13 +401,7 @@ const vibe = (config = {}) => {
395
401
  host = undefined;
396
402
  }
397
403
 
398
- const startServer = () => server(options, Number(port), host, callback);
399
-
400
- if (config.autoRestart) {
401
- clusterize(startServer, { workers: 1, restart: true });
402
- } else {
403
- startServer();
404
- }
404
+ server(options, Number(port), host, callback);
405
405
  }
406
406
 
407
407
  /**
@@ -435,16 +435,26 @@ const vibe = (config = {}) => {
435
435
  ...options.decorators,
436
436
  };
437
437
 
438
- // Execute plugin
438
+ // Execute plugin — invoke fn() synchronously first so all route
439
+ // registrations that happen synchronously inside the plugin use the
440
+ // correct prefix. Restore currentPrefix IMMEDIATELY after the call
441
+ // (before any await) so that concurrent un-awaited register() calls
442
+ // from the caller cannot inherit this plugin's prefix.
443
+ let result;
439
444
  try {
440
- const result = fn(scopedApp, opts);
441
- if (result && result.then) {
442
- await result;
443
- }
445
+ result = fn(scopedApp, opts);
444
446
  } finally {
445
- // Restore prefix
447
+ // Restore prefix synchronously — this is the critical fix.
448
+ // If fn() is async its routes should already be registered
449
+ // synchronously at the top of the function; the await below is
450
+ // only needed to propagate rejections.
446
451
  currentPrefix = previousPrefix;
447
452
  }
453
+
454
+ // Await async plugins for error propagation only (prefix already restored)
455
+ if (result && typeof result.then === "function") {
456
+ await result;
457
+ }
448
458
  }
449
459
 
450
460
  /**
@@ -630,3 +640,6 @@ export {
630
640
  export { LRUCache, cacheMiddleware } from "./utils/scaling/cache.js";
631
641
  export { Pool, createPool } from "./utils/scaling/pool.js";
632
642
  export { parseJsonStream } from "./utils/core/parser.js";
643
+ export { rateLimit } from "./utils/scaling/rate-limit.js";
644
+ export { cors } from "./utils/helpers/cors.js";
645
+