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 +103 -43
- package/package.json +7 -18
- package/utils/core/compile-serializer.js +171 -0
- package/utils/core/parser.js +16 -2
- package/utils/core/response.js +139 -137
- package/utils/core/server.js +80 -18
- package/utils/native.js +6 -80
- package/vibe.js +28 -8
- package/binding.gyp +0 -36
- package/native/json_stringify.cc +0 -241
- package/native/json_stringify.h +0 -32
- package/native/url_parser.cc +0 -178
- package/native/url_parser.h +0 -34
- package/native/vibe_native.cc +0 -46
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
|
|
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
|
|
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
|
|
412
|
-
|
|
|
413
|
-
|
|
|
414
|
-
|
|
|
415
|
-
|
|
|
416
|
-
|
|
|
417
|
-
|
|
|
418
|
-
|
|
|
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
|
-
|
|
465
|
+
### File Upload Security
|
|
421
466
|
|
|
422
|
-
|
|
423
|
-
---
|
|
467
|
+
Routes **reject multipart uploads by default** unless `media` is explicitly configured:
|
|
424
468
|
|
|
425
|
-
|
|
469
|
+
```javascript
|
|
470
|
+
// ❌ This will reject file uploads with 400 Bad Request
|
|
471
|
+
app.post("/api/data", (req) => ({ data: req.body }));
|
|
426
472
|
|
|
427
|
-
|
|
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
|
-
|
|
487
|
+
This prevents attackers from uploading malicious files to unintended routes.
|
|
430
488
|
|
|
431
|
-
|
|
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
|
-
|
|
442
|
-
return { user: req.user };
|
|
443
|
-
});
|
|
444
|
-
```
|
|
491
|
+
---
|
|
445
492
|
|
|
446
|
-
|
|
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
|
-
"/
|
|
499
|
+
"/users/:id",
|
|
451
500
|
{
|
|
452
|
-
|
|
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
|
-
|
|
520
|
+
**Benefits:**
|
|
459
521
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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": "
|
|
4
|
-
"description": "A lightweight, high-performance Node.js web framework
|
|
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
|
-
|
|
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;
|
package/utils/core/parser.js
CHANGED
|
@@ -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
|
|
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
|
|
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();
|