fullstx 1.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 +86 -0
- package/bin/create.js +68 -0
- package/bin/generate.js +78 -0
- package/coverage/clover.xml +1356 -0
- package/coverage/coverage-final.json +30 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/fullstx/index.html +146 -0
- package/coverage/lcov-report/fullstx/index.js.html +379 -0
- package/coverage/lcov-report/fullstx/lib/application.js.html +1840 -0
- package/coverage/lcov-report/fullstx/lib/browser-sync.js.html +199 -0
- package/coverage/lcov-report/fullstx/lib/database.js.html +535 -0
- package/coverage/lcov-report/fullstx/lib/errors.js.html +154 -0
- package/coverage/lcov-report/fullstx/lib/features/hooks.js.html +151 -0
- package/coverage/lcov-report/fullstx/lib/features/index.html +176 -0
- package/coverage/lcov-report/fullstx/lib/features/jwt.js.html +214 -0
- package/coverage/lcov-report/fullstx/lib/features/plugins.js.html +172 -0
- package/coverage/lcov-report/fullstx/lib/features/routing.js.html +211 -0
- package/coverage/lcov-report/fullstx/lib/features/utils.js.html +1216 -0
- package/coverage/lcov-report/fullstx/lib/index.html +236 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/apiKey.js.html +127 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/authorize.js.html +115 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/basicAuth.js.html +148 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/csp.js.html +124 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/csrf.js.html +178 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/htmx.js.html +157 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/i18n.js.html +175 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/index.html +281 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/rateLimit.js.html +169 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/redisCache.js.html +154 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/requirePermission.js.html +118 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/routeLocalization.js.html +118 -0
- package/coverage/lcov-report/fullstx/lib/middlewares/session.js.html +133 -0
- package/coverage/lcov-report/fullstx/lib/middlewares.js.html +340 -0
- package/coverage/lcov-report/fullstx/lib/radix-router.js.html +418 -0
- package/coverage/lcov-report/fullstx/lib/request.js.html +328 -0
- package/coverage/lcov-report/fullstx/lib/response.js.html +610 -0
- package/coverage/lcov-report/fullstx/lib/websocket.js.html +166 -0
- package/coverage/lcov-report/fullstx/router.js.html +226 -0
- package/coverage/lcov-report/fullstx/server.js.html +664 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +2753 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +238 -0
- package/dist/index.mjs.map +1 -0
- package/index.d.ts +115 -0
- package/index.js +98 -0
- package/lib/application.js +585 -0
- package/lib/browser-sync.js +38 -0
- package/lib/database.js +150 -0
- package/lib/errors.js +23 -0
- package/lib/features/hooks.js +22 -0
- package/lib/features/jwt.js +43 -0
- package/lib/features/plugins.js +29 -0
- package/lib/features/routing.js +42 -0
- package/lib/features/utils.js +377 -0
- package/lib/middlewares/apiKey.js +14 -0
- package/lib/middlewares/authorize.js +10 -0
- package/lib/middlewares/basicAuth.js +21 -0
- package/lib/middlewares/csp.js +13 -0
- package/lib/middlewares/csrf.js +31 -0
- package/lib/middlewares/htmx.js +24 -0
- package/lib/middlewares/i18n.js +30 -0
- package/lib/middlewares/rateLimit.js +28 -0
- package/lib/middlewares/redisCache.js +23 -0
- package/lib/middlewares/requirePermission.js +11 -0
- package/lib/middlewares/routeLocalization.js +11 -0
- package/lib/middlewares/session.js +16 -0
- package/lib/middlewares.js +85 -0
- package/lib/radix-router.js +111 -0
- package/lib/request.js +81 -0
- package/lib/response.js +175 -0
- package/lib/websocket.js +27 -0
- package/package.json +61 -0
- package/router.js +47 -0
- package/server.js +193 -0
- package/session.js +109 -0
package/lib/response.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const RESPONSE_POOL = [];
|
|
4
|
+
const POOL_SIZE = 1000;
|
|
5
|
+
|
|
6
|
+
class FullstxResponse {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.raw = null;
|
|
9
|
+
this.req = null;
|
|
10
|
+
this.route = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
init(rawRes, req) {
|
|
14
|
+
this.raw = rawRes;
|
|
15
|
+
this.req = req;
|
|
16
|
+
this.route = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get statusCode() {
|
|
20
|
+
return this.raw.statusCode;
|
|
21
|
+
}
|
|
22
|
+
set statusCode(val) {
|
|
23
|
+
this.raw.statusCode = val;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get headersSent() {
|
|
27
|
+
return this.raw.headersSent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setHeader(name, value) {
|
|
31
|
+
this.raw.setHeader(name, value);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getHeader(name) {
|
|
36
|
+
return this.raw.getHeader(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removeHeader(name) {
|
|
40
|
+
this.raw.removeHeader(name);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeHead(statusCode, headers) {
|
|
45
|
+
this.raw.writeHead(statusCode, headers);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
write(chunk, encoding, callback) {
|
|
50
|
+
return this.raw.write(chunk, encoding, callback);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
end(data, encoding, callback) {
|
|
54
|
+
return this.raw.end(data, encoding, callback);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
on(event, listener) {
|
|
58
|
+
this.raw.on(event, listener);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
status(code) {
|
|
63
|
+
this.raw.statusCode = code;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
json(data, statusCode = 200) {
|
|
68
|
+
this.raw.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
69
|
+
if (this.route && this.route.serializer) {
|
|
70
|
+
this.raw.end(this.route.serializer(data));
|
|
71
|
+
} else {
|
|
72
|
+
this.raw.end(JSON.stringify(data));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
html(htmlStr, statusCode = 200) {
|
|
77
|
+
this.raw.writeHead(statusCode, { 'Content-Type': 'text/html' });
|
|
78
|
+
this.raw.end(htmlStr);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
render(viewPath, data = {}) {
|
|
82
|
+
const ejs = require('ejs');
|
|
83
|
+
const fullPath = path.isAbsolute(viewPath) ? viewPath : path.join(process.cwd(), 'views', viewPath);
|
|
84
|
+
ejs.renderFile(fullPath, data, (err, str) => {
|
|
85
|
+
if (err) {
|
|
86
|
+
this.raw.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
87
|
+
this.raw.end('Template rendering error: ' + err.message);
|
|
88
|
+
} else {
|
|
89
|
+
this.html(str);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
sse(data, event = 'message') {
|
|
95
|
+
if (!this.raw.headersSent) {
|
|
96
|
+
this.raw.writeHead(200, {
|
|
97
|
+
'Content-Type': 'text/event-stream',
|
|
98
|
+
'Cache-Control': 'no-cache',
|
|
99
|
+
Connection: 'keep-alive'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
this.raw.write(`event: ${event}\n`);
|
|
103
|
+
this.raw.write(`data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
redirect(url, statusCode = 302) {
|
|
107
|
+
this.raw.writeHead(statusCode, { Location: url });
|
|
108
|
+
this.raw.end();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
header(key, value) {
|
|
112
|
+
this.raw.setHeader(key, value);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
cookie(name, value, options = {}) {
|
|
117
|
+
let cookieString = `${name}=${encodeURIComponent(value)}`;
|
|
118
|
+
if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;
|
|
119
|
+
if (options.domain) cookieString += `; Domain=${options.domain}`;
|
|
120
|
+
if (options.path) cookieString += `; Path=${options.path}`;
|
|
121
|
+
if (options.expires) cookieString += `; Expires=${options.expires.toUTCString()}`;
|
|
122
|
+
if (options.httpOnly) cookieString += `; HttpOnly`;
|
|
123
|
+
if (options.secure) cookieString += `; Secure`;
|
|
124
|
+
if (options.sameSite) cookieString += `; SameSite=${options.sameSite}`;
|
|
125
|
+
|
|
126
|
+
let existing = this.raw.getHeader('Set-Cookie');
|
|
127
|
+
if (!existing) {
|
|
128
|
+
existing = [];
|
|
129
|
+
} else if (!Array.isArray(existing)) {
|
|
130
|
+
existing = [existing];
|
|
131
|
+
}
|
|
132
|
+
existing.push(cookieString);
|
|
133
|
+
this.raw.setHeader('Set-Cookie', existing);
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
send(body) {
|
|
138
|
+
if (typeof body === 'object' && body !== null && !Buffer.isBuffer(body)) {
|
|
139
|
+
return this.json(body);
|
|
140
|
+
}
|
|
141
|
+
if (typeof body === 'string') {
|
|
142
|
+
if (!this.raw.getHeader('Content-Type')) {
|
|
143
|
+
this.raw.setHeader('Content-Type', 'text/html');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this.raw.end(body);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getResponse(rawRes, req) {
|
|
151
|
+
if (RESPONSE_POOL.length > 0) {
|
|
152
|
+
const res = RESPONSE_POOL.pop();
|
|
153
|
+
res.init(rawRes, req);
|
|
154
|
+
return res;
|
|
155
|
+
}
|
|
156
|
+
const res = new FullstxResponse();
|
|
157
|
+
res.init(rawRes, req);
|
|
158
|
+
return res;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function releaseResponse(res) {
|
|
162
|
+
res.raw = null;
|
|
163
|
+
res.req = null;
|
|
164
|
+
res.route = null;
|
|
165
|
+
|
|
166
|
+
if (RESPONSE_POOL.length < POOL_SIZE) {
|
|
167
|
+
RESPONSE_POOL.push(res);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
FullstxResponse,
|
|
173
|
+
getResponse,
|
|
174
|
+
releaseResponse
|
|
175
|
+
};
|
package/lib/websocket.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports = function setupWebsocket(opts) {
|
|
2
|
+
let wsRedisPub = null;
|
|
3
|
+
let wsRedisSub = null;
|
|
4
|
+
|
|
5
|
+
if (opts.ws && opts.ws.redisUrl) {
|
|
6
|
+
try {
|
|
7
|
+
const { createClient } = require('redis');
|
|
8
|
+
wsRedisPub = createClient({ url: opts.ws.redisUrl });
|
|
9
|
+
wsRedisSub = createClient({ url: opts.ws.redisUrl });
|
|
10
|
+
|
|
11
|
+
wsRedisPub.on('error', (e) => console.error('[fullstx] WS Redis Pub Error:', e));
|
|
12
|
+
wsRedisSub.on('error', (e) => console.error('[fullstx] WS Redis Sub Error:', e));
|
|
13
|
+
|
|
14
|
+
wsRedisPub.connect().catch(() => {});
|
|
15
|
+
wsRedisSub
|
|
16
|
+
.connect()
|
|
17
|
+
.then(() => {
|
|
18
|
+
console.log('[fullstx] WebSocket Redis Pub/Sub Adapter enabled.');
|
|
19
|
+
})
|
|
20
|
+
.catch(() => {});
|
|
21
|
+
} catch {
|
|
22
|
+
console.error('[fullstx] Failed to load redis for WS Pub/Sub');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return { wsRedisPub, wsRedisSub };
|
|
27
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fullstx",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A high-performance, custom backend framework with built-in hot-reload.",
|
|
5
|
+
"author": "fullstx developer",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"bin": {
|
|
18
|
+
"create-fullstx": "./bin/create.js",
|
|
19
|
+
"fullstx-generate": "./bin/generate.js"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup",
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"test:watch": "jest --watch"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@eslint/js": "^10.0.1",
|
|
28
|
+
"autocannon": "^8.0.0",
|
|
29
|
+
"eslint": "^10.6.0",
|
|
30
|
+
"eslint-config-prettier": "^10.1.8",
|
|
31
|
+
"eslint-plugin-no-comments": "^1.2.1",
|
|
32
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
33
|
+
"globals": "^17.7.0",
|
|
34
|
+
"jest": "^29.7.0",
|
|
35
|
+
"prettier": "^3.9.3",
|
|
36
|
+
"semantic-release": "^25.0.5",
|
|
37
|
+
"supertest": "^7.2.2",
|
|
38
|
+
"tsup": "^8.5.1",
|
|
39
|
+
"typescript": "^6.0.3"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@faker-js/faker": "^10.5.0",
|
|
43
|
+
"@sentry/node": "^10.62.0",
|
|
44
|
+
"browser-sync": "^3.0.4",
|
|
45
|
+
"bullmq": "^5.79.2",
|
|
46
|
+
"compression": "^1.8.1",
|
|
47
|
+
"dotenv": "^17.4.2",
|
|
48
|
+
"ejs": "^6.0.1",
|
|
49
|
+
"fast-json-stringify": "^7.0.0",
|
|
50
|
+
"jsonwebtoken": "^9.0.3",
|
|
51
|
+
"knex": "^3.3.0",
|
|
52
|
+
"multer": "^2.2.0",
|
|
53
|
+
"nodemailer": "^9.0.1",
|
|
54
|
+
"redis": "^6.0.1",
|
|
55
|
+
"sanitize-html": "^2.17.5",
|
|
56
|
+
"sqlite3": "^6.0.1",
|
|
57
|
+
"swagger-ui-dist": "^5.32.8",
|
|
58
|
+
"ws": "^8.21.0",
|
|
59
|
+
"zod": "^4.4.3"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/router.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const RadixRouter = require('./lib/radix-router');
|
|
2
|
+
|
|
3
|
+
const radix = new RadixRouter();
|
|
4
|
+
const middlewares = [];
|
|
5
|
+
|
|
6
|
+
function insertRoute(method, path, options, handler) {
|
|
7
|
+
if (typeof options === 'function') {
|
|
8
|
+
handler = options;
|
|
9
|
+
options = {};
|
|
10
|
+
}
|
|
11
|
+
radix.insert(method, path, { handler, ...options });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function get(path, options, handler) {
|
|
15
|
+
insertRoute('GET', path, options, handler);
|
|
16
|
+
}
|
|
17
|
+
function post(path, options, handler) {
|
|
18
|
+
insertRoute('POST', path, options, handler);
|
|
19
|
+
}
|
|
20
|
+
function put(path, options, handler) {
|
|
21
|
+
insertRoute('PUT', path, options, handler);
|
|
22
|
+
}
|
|
23
|
+
function patch(path, options, handler) {
|
|
24
|
+
insertRoute('PATCH', path, options, handler);
|
|
25
|
+
}
|
|
26
|
+
function del(path, options, handler) {
|
|
27
|
+
insertRoute('DELETE', path, options, handler);
|
|
28
|
+
}
|
|
29
|
+
function use(fn) {
|
|
30
|
+
middlewares.push(fn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function lookup(method, path) {
|
|
34
|
+
return radix.lookup(method, path);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
middlewares,
|
|
39
|
+
get,
|
|
40
|
+
post,
|
|
41
|
+
put,
|
|
42
|
+
patch,
|
|
43
|
+
del,
|
|
44
|
+
delete: del,
|
|
45
|
+
use,
|
|
46
|
+
lookup
|
|
47
|
+
};
|
package/server.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getRequest, releaseRequest } = require('./lib/request');
|
|
5
|
+
const { getResponse, releaseResponse } = require('./lib/response');
|
|
6
|
+
|
|
7
|
+
const MIME_TYPES = {
|
|
8
|
+
'.html': 'text/html',
|
|
9
|
+
'.css': 'text/css',
|
|
10
|
+
'.js': 'application/javascript',
|
|
11
|
+
'.json': 'application/json',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpeg',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.ico': 'image/x-icon'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function sanitizePayload(obj) {
|
|
19
|
+
if (obj === null || typeof obj !== 'object') return obj;
|
|
20
|
+
if (Array.isArray(obj)) return obj.map(sanitizePayload);
|
|
21
|
+
const proto = Object.getPrototypeOf(obj);
|
|
22
|
+
if (proto !== null && proto !== Object.prototype) return obj;
|
|
23
|
+
const sanitized = {};
|
|
24
|
+
for (const key in obj) {
|
|
25
|
+
if (key === '__proto__' || key === 'constructor') continue;
|
|
26
|
+
sanitized[key] = sanitizePayload(obj[key]);
|
|
27
|
+
}
|
|
28
|
+
return sanitized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class RequestContext {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.req = null;
|
|
34
|
+
this.res = null;
|
|
35
|
+
this.middlewares = null;
|
|
36
|
+
this.router = null;
|
|
37
|
+
this.staticDir = null;
|
|
38
|
+
this.i = 0;
|
|
39
|
+
this.next = this.next.bind(this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
init(req, res, middlewares, router, staticDir) {
|
|
43
|
+
this.req = req;
|
|
44
|
+
this.res = res;
|
|
45
|
+
this.middlewares = middlewares;
|
|
46
|
+
this.router = router;
|
|
47
|
+
this.staticDir = staticDir;
|
|
48
|
+
this.i = 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
parseBody(callback) {
|
|
52
|
+
if (this.req.method === 'GET' || this.req.method === 'HEAD') {
|
|
53
|
+
this.req.body = {};
|
|
54
|
+
return callback();
|
|
55
|
+
}
|
|
56
|
+
let raw = '';
|
|
57
|
+
this.req.on('data', (chunk) => {
|
|
58
|
+
raw += chunk;
|
|
59
|
+
});
|
|
60
|
+
this.req.on('end', () => {
|
|
61
|
+
try {
|
|
62
|
+
const ct = (this.req.headers['content-type'] || '').split(';')[0].trim();
|
|
63
|
+
if (ct === 'application/x-www-form-urlencoded') {
|
|
64
|
+
const params = new URLSearchParams(raw);
|
|
65
|
+
const obj = {};
|
|
66
|
+
for (const [k, v] of params.entries()) obj[k] = v;
|
|
67
|
+
this.req.body = sanitizePayload(obj);
|
|
68
|
+
} else {
|
|
69
|
+
this.req.body = raw ? sanitizePayload(JSON.parse(raw)) : {};
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
this.req.body = raw || {};
|
|
73
|
+
}
|
|
74
|
+
callback();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
serveStatic(callback) {
|
|
79
|
+
if (!this.staticDir) return callback();
|
|
80
|
+
|
|
81
|
+
let filePath = this.req.url === '/' ? '/index.html' : this.req.url;
|
|
82
|
+
filePath = path.join(this.staticDir, filePath);
|
|
83
|
+
|
|
84
|
+
if (!filePath.startsWith(this.staticDir)) {
|
|
85
|
+
return callback();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fs.readFile(filePath, (err, content) => {
|
|
89
|
+
if (err) return callback();
|
|
90
|
+
const ext = path.extname(filePath);
|
|
91
|
+
const contentType = MIME_TYPES[ext] || 'text/plain';
|
|
92
|
+
this.res.writeHead(200, { 'Content-Type': contentType });
|
|
93
|
+
this.res.end(content);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
handleError(err) {
|
|
98
|
+
if (global.sentryCapture) global.sentryCapture(err);
|
|
99
|
+
console.error('[fullstx] Error:', err);
|
|
100
|
+
if (!this.res.headersSent) {
|
|
101
|
+
this.res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
102
|
+
this.res.end(JSON.stringify({ error: 'Internal Server Error' }));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
next(err) {
|
|
107
|
+
if (err) {
|
|
108
|
+
return this.handleError(err);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.i < this.middlewares.length) {
|
|
112
|
+
try {
|
|
113
|
+
const mw = this.middlewares[this.i++];
|
|
114
|
+
const result = mw(this.req, this.res, this.next);
|
|
115
|
+
if (result instanceof Promise) {
|
|
116
|
+
result.catch((e) => this.next(e));
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
this.next(e);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
const route = this.router.lookup(this.req.method, this.req.pathname);
|
|
123
|
+
if (route && route.handler) {
|
|
124
|
+
this.res.route = route.routeConfig;
|
|
125
|
+
this.req.params = Object.assign(this.req.params || {}, route.params);
|
|
126
|
+
try {
|
|
127
|
+
const result = route.handler(this.req, this.res);
|
|
128
|
+
if (result instanceof Promise) {
|
|
129
|
+
result.catch((e) => this.handleError(e));
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
this.handleError(e);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
this.serveStatic(() => {
|
|
136
|
+
if (!this.res.headersSent) {
|
|
137
|
+
this.res.writeHead(404);
|
|
138
|
+
this.res.end('404 Not Found');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const CONTEXT_POOL = [];
|
|
147
|
+
function getContext(req, res, middlewares, router, staticDir) {
|
|
148
|
+
if (CONTEXT_POOL.length > 0) {
|
|
149
|
+
const ctx = CONTEXT_POOL.pop();
|
|
150
|
+
ctx.init(req, res, middlewares, router, staticDir);
|
|
151
|
+
return ctx;
|
|
152
|
+
}
|
|
153
|
+
const ctx = new RequestContext();
|
|
154
|
+
ctx.init(req, res, middlewares, router, staticDir);
|
|
155
|
+
return ctx;
|
|
156
|
+
}
|
|
157
|
+
function releaseContext(ctx) {
|
|
158
|
+
ctx.req = null;
|
|
159
|
+
ctx.res = null;
|
|
160
|
+
ctx.middlewares = null;
|
|
161
|
+
ctx.router = null;
|
|
162
|
+
if (CONTEXT_POOL.length < 1000) CONTEXT_POOL.push(ctx);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function createServer(router, middlewares, options = {}) {
|
|
166
|
+
const staticDir = options.staticDir ? path.resolve(options.staticDir) : null;
|
|
167
|
+
|
|
168
|
+
const server = http.createServer((rawReq, rawRes) => {
|
|
169
|
+
const reqWrapper = getRequest(rawReq);
|
|
170
|
+
const resWrapper = getResponse(rawRes, reqWrapper);
|
|
171
|
+
const ctx = getContext(reqWrapper, resWrapper, middlewares, router, staticDir);
|
|
172
|
+
|
|
173
|
+
let released = false;
|
|
174
|
+
const releaseAll = () => {
|
|
175
|
+
if (released) return;
|
|
176
|
+
released = true;
|
|
177
|
+
setImmediate(() => {
|
|
178
|
+
releaseRequest(reqWrapper);
|
|
179
|
+
releaseResponse(resWrapper);
|
|
180
|
+
releaseContext(ctx);
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
rawRes.on('finish', releaseAll);
|
|
185
|
+
rawRes.on('close', releaseAll);
|
|
186
|
+
|
|
187
|
+
ctx.parseBody(ctx.next);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return server;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = createServer;
|
package/session.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class MemoryStore {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.sessions = new Map();
|
|
6
|
+
}
|
|
7
|
+
async get(sid) {
|
|
8
|
+
return this.sessions.get(sid);
|
|
9
|
+
}
|
|
10
|
+
async set(sid, data, ttl) {
|
|
11
|
+
this.sessions.set(sid, data);
|
|
12
|
+
|
|
13
|
+
setTimeout(() => this.sessions.delete(sid), ttl * 1000).unref();
|
|
14
|
+
}
|
|
15
|
+
async destroy(sid) {
|
|
16
|
+
this.sessions.delete(sid);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class RedisStore {
|
|
21
|
+
constructor(redisClient) {
|
|
22
|
+
this.client = redisClient;
|
|
23
|
+
}
|
|
24
|
+
async get(sid) {
|
|
25
|
+
const data = await this.client.get(`session:${sid}`);
|
|
26
|
+
return data ? JSON.parse(data) : null;
|
|
27
|
+
}
|
|
28
|
+
async set(sid, data, ttl) {
|
|
29
|
+
await this.client.set(`session:${sid}`, JSON.stringify(data), {
|
|
30
|
+
EX: ttl
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async destroy(sid) {
|
|
34
|
+
await this.client.del(`session:${sid}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function generateSessionId() {
|
|
39
|
+
return crypto.randomBytes(32).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = function createSessionMiddleware(opts) {
|
|
43
|
+
if (!opts || !opts.session) return null;
|
|
44
|
+
|
|
45
|
+
const sessionOpts = Object.assign(
|
|
46
|
+
{
|
|
47
|
+
secret: 'fullstx-default-secret',
|
|
48
|
+
store: 'memory',
|
|
49
|
+
redisUrl: 'redis://localhost:6379',
|
|
50
|
+
cookieName: 'fullstx_sid',
|
|
51
|
+
ttl: 86400
|
|
52
|
+
},
|
|
53
|
+
opts.session
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
let store;
|
|
57
|
+
if (sessionOpts.store === 'redis') {
|
|
58
|
+
try {
|
|
59
|
+
const { createClient } = require('redis');
|
|
60
|
+
const client = createClient({ url: sessionOpts.redisUrl });
|
|
61
|
+
client.on('error', (err) => console.error('[fullstx] Session Redis Error:', err));
|
|
62
|
+
client.connect().catch((e) => console.error('[fullstx] Session Redis Connect Error:', e));
|
|
63
|
+
store = new RedisStore(client);
|
|
64
|
+
console.log('[fullstx] Session Store: Redis');
|
|
65
|
+
} catch {
|
|
66
|
+
console.warn('[fullstx] Failed to load redis for session, falling back to memory store.');
|
|
67
|
+
store = new MemoryStore();
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
store = new MemoryStore();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return async function sessionMiddleware(req, res, next) {
|
|
74
|
+
const sid = req.cookies && req.cookies[sessionOpts.cookieName];
|
|
75
|
+
let sessionData = null;
|
|
76
|
+
|
|
77
|
+
if (sid) {
|
|
78
|
+
sessionData = await store.get(sid);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const currentSid = sid || generateSessionId();
|
|
82
|
+
|
|
83
|
+
if (!sessionData) {
|
|
84
|
+
sessionData = {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
req.session = sessionData;
|
|
88
|
+
req.sessionID = currentSid;
|
|
89
|
+
|
|
90
|
+
if (!sid && res.cookie) {
|
|
91
|
+
res.cookie(sessionOpts.cookieName, currentSid, {
|
|
92
|
+
maxAge: sessionOpts.ttl,
|
|
93
|
+
httpOnly: true,
|
|
94
|
+
path: '/'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const originalEnd = res.end;
|
|
99
|
+
res.end = function (chunk, encoding, callback) {
|
|
100
|
+
store.set(currentSid, req.session, sessionOpts.ttl).catch((err) => {
|
|
101
|
+
console.error('[fullstx] Session Save Error:', err);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
originalEnd.call(res, chunk, encoding, callback);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
next();
|
|
108
|
+
};
|
|
109
|
+
};
|