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 +66 -42
- 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 +27 -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(
|
|
@@ -408,61 +416,77 @@ app.plugin(adapt(compression()));
|
|
|
408
416
|
|
|
409
417
|
Built-in protections:
|
|
410
418
|
|
|
411
|
-
| Feature
|
|
412
|
-
|
|
|
413
|
-
|
|
|
414
|
-
|
|
|
415
|
-
|
|
|
416
|
-
|
|
|
417
|
-
|
|
|
418
|
-
|
|
|
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
|
-
|
|
429
|
+
### File Upload Security
|
|
421
430
|
|
|
422
|
-
|
|
423
|
-
---
|
|
431
|
+
Routes **reject multipart uploads by default** unless `media` is explicitly configured:
|
|
424
432
|
|
|
425
|
-
|
|
433
|
+
```javascript
|
|
434
|
+
// ❌ This will reject file uploads with 400 Bad Request
|
|
435
|
+
app.post("/api/data", (req) => ({ data: req.body }));
|
|
426
436
|
|
|
427
|
-
|
|
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
|
-
|
|
451
|
+
This prevents attackers from uploading malicious files to unintended routes.
|
|
430
452
|
|
|
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
|
-
};
|
|
453
|
+
Set `NODE_ENV=production` for secure error handling (stack traces hidden).
|
|
440
454
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
```
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## ⚡ Schema-Based Serialization
|
|
445
458
|
|
|
446
|
-
|
|
459
|
+
**Optional** performance boost: Pre-compile JSON serializers for 2-3x faster responses.
|
|
447
460
|
|
|
448
461
|
```javascript
|
|
449
462
|
app.get(
|
|
450
|
-
"/
|
|
463
|
+
"/users/:id",
|
|
451
464
|
{
|
|
452
|
-
|
|
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
|
-
|
|
484
|
+
**Benefits:**
|
|
459
485
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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": "
|
|
4
|
-
"description": "A lightweight, high-performance Node.js web framework
|
|
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
|
-
|
|
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();
|