vibe-gx 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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(
@@ -408,61 +416,77 @@ app.plugin(adapt(compression()));
408
416
 
409
417
  Built-in protections:
410
418
 
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 | ✅ |
419
+ | Feature | Status |
420
+ | :--------------------------------------- | :----: |
421
+ | **File upload protection** (opt-in only) | ✅ |
422
+ | Path traversal protection | ✅ |
423
+ | File type validation | ✅ |
424
+ | Body size limits (1MB JSON, 10MB files) | ✅ |
425
+ | Error sanitization (production mode) | ✅ |
426
+ | Safe filename generation | ✅ |
427
+ | Port validation | ✅ |
419
428
 
420
- Set `NODE_ENV=production` for secure error handling (stack traces hidden).
429
+ ### File Upload Security
421
430
 
422
- >>>>>>> cpp-optimization
423
- ---
431
+ Routes **reject multipart uploads by default** unless `media` is explicitly configured:
424
432
 
425
- ### Interceptors (Middleware)
433
+ ```javascript
434
+ // ❌ This will reject file uploads with 400 Bad Request
435
+ app.post("/api/data", (req) => ({ data: req.body }));
426
436
 
427
- Interceptors run before your handler. Return `false` to stop execution.
437
+ // This accepts file uploads (explicit opt-in)
438
+ app.post(
439
+ "/upload",
440
+ {
441
+ media: {
442
+ dest: "uploads",
443
+ maxSize: 5 * 1024 * 1024,
444
+ allowedTypes: ["image/*", "application/pdf"],
445
+ },
446
+ },
447
+ handler,
448
+ );
449
+ ```
428
450
 
429
- #### Single Interceptor
451
+ This prevents attackers from uploading malicious files to unintended routes.
430
452
 
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
- };
453
+ Set `NODE_ENV=production` for secure error handling (stack traces hidden).
440
454
 
441
- app.get("/protected", { intercept: authCheck }, (req) => {
442
- return { user: req.user };
443
- });
444
- ```
455
+ ---
456
+
457
+ ## ⚡ Schema-Based Serialization
445
458
 
446
- #### Multiple Interceptors
459
+ **Optional** performance boost: Pre-compile JSON serializers for 2-3x faster responses.
447
460
 
448
461
  ```javascript
449
462
  app.get(
450
- "/admin",
463
+ "/users/:id",
451
464
  {
452
- intercept: [authCheck, adminCheck, rateLimiter],
465
+ schema: {
466
+ response: {
467
+ type: "object",
468
+ properties: {
469
+ id: { type: "number" },
470
+ name: { type: "string" },
471
+ email: { type: "string" },
472
+ active: { type: "boolean" },
473
+ },
474
+ },
475
+ },
476
+ },
477
+ async (req) => {
478
+ const user = await db.getUser(req.params.id);
479
+ return user; // Uses pre-compiled serializer (2-3x faster than JSON.stringify)
453
480
  },
454
- handler,
455
481
  );
456
482
  ```
457
483
 
458
- #### Global Interceptors
484
+ **Benefits:**
459
485
 
460
- ```javascript
461
- // Applies to ALL routes
462
- app.plugin((req, res) => {
463
- console.log(`${req.method} ${req.url}`);
464
- });
465
- ```
486
+ - ✅ 2-3x faster JSON serialization
487
+ - No `Object.keys()` enumeration
488
+ - Zero runtime type checking
489
+ - ✅ Completely optional (routes work without schemas)
466
490
 
467
491
  ---
468
492
 
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.0.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();