vibe-gx 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,9 +223,17 @@ Extend app, request, or response with custom properties:
223
223
  // App decorator - shared config
224
224
  app.decorate("config", { env: "production", version: "1.0.0" });
225
225
 
226
- // Access via app.decorators.config
226
+ // Access via app.decorators in main app
227
227
  app.get("/version", () => ({ version: app.decorators.config.version }));
228
228
 
229
+ // In plugins, decorators are spread directly (no .decorators)
230
+ app.register(
231
+ async (api) => {
232
+ api.get("/env", () => ({ env: api.config.env })); // Direct access
233
+ },
234
+ { prefix: "/api" },
235
+ );
236
+
229
237
  // Request decorator - add to all requests
230
238
  app.decorateRequest("timestamp", () => Date.now());
231
239
 
@@ -241,19 +249,19 @@ app.decorateReply("sendSuccess", function (data) {
241
249
 
242
250
  ## 📂 File Uploads
243
251
 
244
- Vibe supports multipart file uploads with built-in validation.
252
+ Vibe supports multipart file uploads with built-in validation and security.
253
+
254
+ > **🔒 Security**: File uploads are **disabled by default**. You must explicitly configure `media` options to accept uploads.
245
255
 
246
256
  ### Basic Upload
247
257
 
248
258
  ```javascript
249
259
  app.post("/upload", { media: { dest: "uploads" } }, (req) => {
250
- >>>>>>> cpp-optimization
251
260
  return { files: req.files, body: req.body };
252
261
  });
253
262
  ```
254
263
 
255
264
  ### Media Options
256
- >>>>>>> cpp-optimization
257
265
 
258
266
  ```javascript
259
267
  app.post(
@@ -261,7 +269,7 @@ app.post(
261
269
  {
262
270
  media: {
263
271
  dest: "uploads", // Subfolder destination
264
- public: true, // Save inside public folder (default: true)
272
+ public: true, // Save in public folder (default: true)
265
273
  maxSize: 5 * 1024 * 1024, // Max file size: 5MB
266
274
  allowedTypes: ["image/jpeg", "image/png", "image/*"], // Wildcards supported
267
275
  },
@@ -270,6 +278,42 @@ app.post(
270
278
  );
271
279
  ```
272
280
 
281
+ ### Public vs Private Uploads
282
+
283
+ **Public uploads** (web-accessible):
284
+
285
+ ```javascript
286
+ app.post(
287
+ "/upload/avatar",
288
+ {
289
+ media: {
290
+ public: true, // ✅ Files accessible via HTTP
291
+ dest: "avatars", // Saved to: public/avatars/
292
+ },
293
+ },
294
+ handler,
295
+ );
296
+
297
+ // Files accessible at: http://yourapp.com/avatars/filename.jpg
298
+ ```
299
+
300
+ **Private uploads** (server-only access):
301
+
302
+ ```javascript
303
+ app.post(
304
+ "/upload/documents",
305
+ {
306
+ media: {
307
+ public: false, // 🔒 Files NOT web-accessible
308
+ dest: "documents", // Saved to: private/documents/
309
+ },
310
+ },
311
+ handler,
312
+ );
313
+
314
+ // Files only accessible via your backend code (e.g., sendAbsoluteFile)
315
+ ```
316
+
273
317
  ### Uploaded File Object
274
318
 
275
319
  ```javascript
@@ -408,61 +452,77 @@ app.plugin(adapt(compression()));
408
452
 
409
453
  Built-in protections:
410
454
 
411
- | Feature | Status |
412
- | :-------------------------------------- | :----: |
413
- | Path traversal protection | ✅ |
414
- | File type validation | ✅ |
415
- | Body size limits (1MB JSON, 10MB files) | ✅ |
416
- | Error sanitization (production mode) | ✅ |
417
- | Safe filename generation | ✅ |
418
- | Port validation | ✅ |
455
+ | Feature | Status |
456
+ | :--------------------------------------- | :----: |
457
+ | **File upload protection** (opt-in only) | ✅ |
458
+ | Path traversal protection | ✅ |
459
+ | File type validation | ✅ |
460
+ | Body size limits (1MB JSON, 10MB files) | ✅ |
461
+ | Error sanitization (production mode) | ✅ |
462
+ | Safe filename generation | ✅ |
463
+ | Port validation | ✅ |
419
464
 
420
- Set `NODE_ENV=production` for secure error handling (stack traces hidden).
465
+ ### File Upload Security
421
466
 
422
- >>>>>>> cpp-optimization
423
- ---
467
+ Routes **reject multipart uploads by default** unless `media` is explicitly configured:
424
468
 
425
- ### Interceptors (Middleware)
469
+ ```javascript
470
+ // ❌ This will reject file uploads with 400 Bad Request
471
+ app.post("/api/data", (req) => ({ data: req.body }));
426
472
 
427
- Interceptors run before your handler. Return `false` to stop execution.
473
+ // This accepts file uploads (explicit opt-in)
474
+ app.post(
475
+ "/upload",
476
+ {
477
+ media: {
478
+ dest: "uploads",
479
+ maxSize: 5 * 1024 * 1024,
480
+ allowedTypes: ["image/*", "application/pdf"],
481
+ },
482
+ },
483
+ handler,
484
+ );
485
+ ```
428
486
 
429
- #### Single Interceptor
487
+ This prevents attackers from uploading malicious files to unintended routes.
430
488
 
431
- ```javascript
432
- const authCheck = (req, res) => {
433
- if (!req.headers.authorization) {
434
- res.unauthorized("Token required");
435
- return false;
436
- }
437
- req.user = { id: 1 };
438
- return true;
439
- };
489
+ Set `NODE_ENV=production` for secure error handling (stack traces hidden).
440
490
 
441
- app.get("/protected", { intercept: authCheck }, (req) => {
442
- return { user: req.user };
443
- });
444
- ```
491
+ ---
445
492
 
446
- #### Multiple Interceptors
493
+ ## Schema-Based Serialization
494
+
495
+ **Optional** performance boost: Pre-compile JSON serializers for 2-3x faster responses.
447
496
 
448
497
  ```javascript
449
498
  app.get(
450
- "/admin",
499
+ "/users/:id",
451
500
  {
452
- intercept: [authCheck, adminCheck, rateLimiter],
501
+ schema: {
502
+ response: {
503
+ type: "object",
504
+ properties: {
505
+ id: { type: "number" },
506
+ name: { type: "string" },
507
+ email: { type: "string" },
508
+ active: { type: "boolean" },
509
+ },
510
+ },
511
+ },
512
+ },
513
+ async (req) => {
514
+ const user = await db.getUser(req.params.id);
515
+ return user; // Uses pre-compiled serializer (2-3x faster than JSON.stringify)
453
516
  },
454
- handler,
455
517
  );
456
518
  ```
457
519
 
458
- #### Global Interceptors
520
+ **Benefits:**
459
521
 
460
- ```javascript
461
- // Applies to ALL routes
462
- app.plugin((req, res) => {
463
- console.log(`${req.method} ${req.url}`);
464
- });
465
- ```
522
+ - ✅ 2-3x faster JSON serialization
523
+ - No `Object.keys()` enumeration
524
+ - Zero runtime type checking
525
+ - ✅ Completely optional (routes work without schemas)
466
526
 
467
527
  ---
468
528
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vibe-gx",
3
- "version": "2.0.0",
4
- "description": "A lightweight, high-performance Node.js web framework with optional C++ optimizations.",
3
+ "version": "3.1.0",
4
+ "description": "A lightweight, high-performance Node.js web framework.",
5
5
  "type": "module",
6
6
  "main": "vibe.js",
7
7
  "types": "vibe.d.ts",
@@ -14,18 +14,13 @@
14
14
  "files": [
15
15
  "vibe.js",
16
16
  "vibe.d.ts",
17
- "utils/",
18
- "native/",
19
- "binding.gyp"
17
+ "utils/"
20
18
  ],
21
19
  "scripts": {
22
20
  "test": "node tests/unit.test.js && node tests/live.test.js",
23
21
  "test:all": "node tests/unit.test.js && node tests/live.test.js && node tests/scalability.test.js",
24
22
  "benchmark": "node tests/full-benchmark.js",
25
- "start": "node server.js",
26
- "build:native": "node-gyp rebuild",
27
- "install": "node-gyp rebuild || echo 'Native build failed, using JS fallback'",
28
- "clean": "node-gyp clean"
23
+ "start": "node server.js"
29
24
  },
30
25
  "keywords": [
31
26
  "node",
@@ -36,21 +31,15 @@
36
31
  "web",
37
32
  "backend",
38
33
  "fast",
39
- "native",
40
34
  "performance",
41
35
  "express-alternative"
42
36
  ],
43
37
  "author": "Nnamdi \"Joe\" Amaga",
44
38
  "license": "MIT",
45
39
  "dependencies": {
46
- "busboy": "^1.6.0",
47
- "node-addon-api": "^7.1.1"
48
- },
49
- "optionalDependencies": {
50
- "node-gyp": "^10.3.1"
40
+ "busboy": "^1.6.0"
51
41
  },
52
42
  "engines": {
53
43
  "node": ">=18"
54
- },
55
- "gypfile": true
56
- }
44
+ }
45
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Schema-based JSON serializer compiler.
3
+ *
4
+ * Pre-compiles a specialized stringify function from a JSON schema
5
+ * at route registration time. The generated function knows the exact
6
+ * object shape, avoiding Object.keys() enumeration and type-checking
7
+ * at request time.
8
+ *
9
+ * @module compile-serializer
10
+ */
11
+
12
+ /**
13
+ * Compiles a JSON schema into a specialized serializer function.
14
+ *
15
+ * @param {Object} schema - JSON schema (subset)
16
+ * @returns {(data: any) => string} Compiled serializer
17
+ */
18
+ export function compileSerializer(schema) {
19
+ if (!schema || !schema.type) {
20
+ return JSON.stringify;
21
+ }
22
+
23
+ const fn = compileType(schema);
24
+ return fn;
25
+ }
26
+
27
+ /**
28
+ * Escapes a string for JSON output.
29
+ * Handles: " \ \b \f \n \r \t and control chars
30
+ */
31
+ const escapeChar = {
32
+ '"': '\\"',
33
+ "\\": "\\\\",
34
+ "\b": "\\b",
35
+ "\f": "\\f",
36
+ "\n": "\\n",
37
+ "\r": "\\r",
38
+ "\t": "\\t",
39
+ };
40
+
41
+ function escapeString(str) {
42
+ let result = '"';
43
+ let last = 0;
44
+
45
+ for (let i = 0; i < str.length; i++) {
46
+ const code = str.charCodeAt(i);
47
+ // Check for characters that need escaping
48
+ if (code === 34 || code === 92 || code < 32) {
49
+ const char = str[i];
50
+ if (i > last) result += str.slice(last, i);
51
+ result += escapeChar[char] || "\\u" + code.toString(16).padStart(4, "0");
52
+ last = i + 1;
53
+ }
54
+ }
55
+
56
+ if (last === 0) return '"' + str + '"';
57
+ if (last < str.length) result += str.slice(last);
58
+ return result + '"';
59
+ }
60
+
61
+ /**
62
+ * Compiles a type-specific serializer from schema.
63
+ * Returns a function (value) => string.
64
+ */
65
+ function compileType(schema) {
66
+ switch (schema.type) {
67
+ case "object":
68
+ return compileObject(schema);
69
+ case "array":
70
+ return compileArray(schema);
71
+ case "string":
72
+ return escapeString;
73
+ case "number":
74
+ case "integer":
75
+ return serializeNumber;
76
+ case "boolean":
77
+ return serializeBoolean;
78
+ case "null":
79
+ return () => "null";
80
+ default:
81
+ return JSON.stringify;
82
+ }
83
+ }
84
+
85
+ function serializeNumber(v) {
86
+ if (v !== v || v === Infinity || v === -Infinity) return "null"; // NaN, Inf
87
+ return "" + v;
88
+ }
89
+
90
+ function serializeBoolean(v) {
91
+ return v ? "true" : "false";
92
+ }
93
+
94
+ /**
95
+ * Compiles an object serializer from schema.properties.
96
+ *
97
+ * Generates a function that directly accesses known properties
98
+ * by name, avoiding Object.keys() enumeration entirely.
99
+ */
100
+ function compileObject(schema) {
101
+ const props = schema.properties;
102
+ if (!props) return JSON.stringify;
103
+
104
+ const keys = Object.keys(props);
105
+ if (keys.length === 0) return () => "{}";
106
+
107
+ // Pre-compute property serializers and JSON key prefixes
108
+ const entries = keys.map((key, i) => {
109
+ const serializer = compileType(props[key]);
110
+ // Pre-stringify the key + colon (and comma for non-first)
111
+ const prefix = (i > 0 ? "," : "") + '"' + key + '":';
112
+ return { key, serializer, prefix };
113
+ });
114
+
115
+ // Check if all properties are simple strings — ultra-fast path
116
+ const allStrings = keys.every((k) => props[k].type === "string");
117
+
118
+ if (allStrings && keys.length <= 8) {
119
+ // Ultra-fast path for small all-string objects (most common API response)
120
+ return function serializeStringObject(obj) {
121
+ if (obj === null || obj === undefined) return "null";
122
+ let result = "{";
123
+ for (let i = 0; i < entries.length; i++) {
124
+ const e = entries[i];
125
+ const v = obj[e.key];
126
+ result += e.prefix;
127
+ result += v === undefined || v === null ? "null" : escapeString("" + v);
128
+ }
129
+ return result + "}";
130
+ };
131
+ }
132
+
133
+ // General path for mixed-type objects
134
+ return function serializeObject(obj) {
135
+ if (obj === null || obj === undefined) return "null";
136
+ let result = "{";
137
+ for (let i = 0; i < entries.length; i++) {
138
+ const e = entries[i];
139
+ const v = obj[e.key];
140
+ result += e.prefix;
141
+ if (v === undefined || v === null) {
142
+ result += "null";
143
+ } else {
144
+ result += e.serializer(v);
145
+ }
146
+ }
147
+ return result + "}";
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Compiles an array serializer from schema.items.
153
+ */
154
+ function compileArray(schema) {
155
+ const itemSerializer = schema.items
156
+ ? compileType(schema.items)
157
+ : JSON.stringify;
158
+
159
+ return function serializeArray(arr) {
160
+ if (!Array.isArray(arr)) return "null";
161
+ if (arr.length === 0) return "[]";
162
+
163
+ let result = "[" + itemSerializer(arr[0]);
164
+ for (let i = 1; i < arr.length; i++) {
165
+ result += "," + itemSerializer(arr[i]);
166
+ }
167
+ return result + "]";
168
+ };
169
+ }
170
+
171
+ export default compileSerializer;
@@ -32,6 +32,20 @@ export default function bodyParser(req, res, media = {}, options = {}) {
32
32
 
33
33
  /* ---------- Multipart / File Uploads ---------- */
34
34
  if (contentType.includes("multipart/form-data")) {
35
+ // SECURITY: Only allow file uploads if media config is explicitly set
36
+ // This prevents attackers from uploading files to routes that don't expect them
37
+ if (!media || Object.keys(media).length === 0) {
38
+ res.writeHead(400, { "content-type": "application/json" });
39
+ res.end(
40
+ JSON.stringify({
41
+ error: "Bad Request",
42
+ message: "File uploads not allowed on this route",
43
+ }),
44
+ );
45
+ return reject(
46
+ new Error("File upload attempted without media configuration"),
47
+ );
48
+ }
35
49
  parseMultipart(req, res, media, options, resolve, reject);
36
50
  return;
37
51
  }
@@ -235,11 +249,11 @@ function parseMultipart(req, res, media, options, resolve, reject) {
235
249
  */
236
250
  function parseJson(req, res, media, options, resolve, reject) {
237
251
  const limit = options.maxJsonSize || 1e6;
238
- const streamThreshold = media.streamThreshold || DEFAULT_STREAM_THRESHOLD;
252
+ const streamThreshold = media?.streamThreshold || DEFAULT_STREAM_THRESHOLD;
239
253
  const contentLength = parseInt(req.headers["content-length"] || "0", 10);
240
254
 
241
255
  // STREAMING MODE: For very large JSON, let handler process incrementally
242
- if (media.streaming && contentLength > streamThreshold) {
256
+ if (media?.streaming && contentLength > streamThreshold) {
243
257
  req.body = null; // Signal that body should be consumed via stream
244
258
  req.emit("jsonStream", req);
245
259
  resolve();