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.
Files changed (83) hide show
  1. package/README.md +86 -0
  2. package/bin/create.js +68 -0
  3. package/bin/generate.js +78 -0
  4. package/coverage/clover.xml +1356 -0
  5. package/coverage/coverage-final.json +30 -0
  6. package/coverage/lcov-report/base.css +224 -0
  7. package/coverage/lcov-report/block-navigation.js +87 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/fullstx/index.html +146 -0
  10. package/coverage/lcov-report/fullstx/index.js.html +379 -0
  11. package/coverage/lcov-report/fullstx/lib/application.js.html +1840 -0
  12. package/coverage/lcov-report/fullstx/lib/browser-sync.js.html +199 -0
  13. package/coverage/lcov-report/fullstx/lib/database.js.html +535 -0
  14. package/coverage/lcov-report/fullstx/lib/errors.js.html +154 -0
  15. package/coverage/lcov-report/fullstx/lib/features/hooks.js.html +151 -0
  16. package/coverage/lcov-report/fullstx/lib/features/index.html +176 -0
  17. package/coverage/lcov-report/fullstx/lib/features/jwt.js.html +214 -0
  18. package/coverage/lcov-report/fullstx/lib/features/plugins.js.html +172 -0
  19. package/coverage/lcov-report/fullstx/lib/features/routing.js.html +211 -0
  20. package/coverage/lcov-report/fullstx/lib/features/utils.js.html +1216 -0
  21. package/coverage/lcov-report/fullstx/lib/index.html +236 -0
  22. package/coverage/lcov-report/fullstx/lib/middlewares/apiKey.js.html +127 -0
  23. package/coverage/lcov-report/fullstx/lib/middlewares/authorize.js.html +115 -0
  24. package/coverage/lcov-report/fullstx/lib/middlewares/basicAuth.js.html +148 -0
  25. package/coverage/lcov-report/fullstx/lib/middlewares/csp.js.html +124 -0
  26. package/coverage/lcov-report/fullstx/lib/middlewares/csrf.js.html +178 -0
  27. package/coverage/lcov-report/fullstx/lib/middlewares/htmx.js.html +157 -0
  28. package/coverage/lcov-report/fullstx/lib/middlewares/i18n.js.html +175 -0
  29. package/coverage/lcov-report/fullstx/lib/middlewares/index.html +281 -0
  30. package/coverage/lcov-report/fullstx/lib/middlewares/rateLimit.js.html +169 -0
  31. package/coverage/lcov-report/fullstx/lib/middlewares/redisCache.js.html +154 -0
  32. package/coverage/lcov-report/fullstx/lib/middlewares/requirePermission.js.html +118 -0
  33. package/coverage/lcov-report/fullstx/lib/middlewares/routeLocalization.js.html +118 -0
  34. package/coverage/lcov-report/fullstx/lib/middlewares/session.js.html +133 -0
  35. package/coverage/lcov-report/fullstx/lib/middlewares.js.html +340 -0
  36. package/coverage/lcov-report/fullstx/lib/radix-router.js.html +418 -0
  37. package/coverage/lcov-report/fullstx/lib/request.js.html +328 -0
  38. package/coverage/lcov-report/fullstx/lib/response.js.html +610 -0
  39. package/coverage/lcov-report/fullstx/lib/websocket.js.html +166 -0
  40. package/coverage/lcov-report/fullstx/router.js.html +226 -0
  41. package/coverage/lcov-report/fullstx/server.js.html +664 -0
  42. package/coverage/lcov-report/index.html +161 -0
  43. package/coverage/lcov-report/prettify.css +1 -0
  44. package/coverage/lcov-report/prettify.js +2 -0
  45. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  46. package/coverage/lcov-report/sorter.js +210 -0
  47. package/coverage/lcov.info +2753 -0
  48. package/dist/index.js +238 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/index.mjs +238 -0
  51. package/dist/index.mjs.map +1 -0
  52. package/index.d.ts +115 -0
  53. package/index.js +98 -0
  54. package/lib/application.js +585 -0
  55. package/lib/browser-sync.js +38 -0
  56. package/lib/database.js +150 -0
  57. package/lib/errors.js +23 -0
  58. package/lib/features/hooks.js +22 -0
  59. package/lib/features/jwt.js +43 -0
  60. package/lib/features/plugins.js +29 -0
  61. package/lib/features/routing.js +42 -0
  62. package/lib/features/utils.js +377 -0
  63. package/lib/middlewares/apiKey.js +14 -0
  64. package/lib/middlewares/authorize.js +10 -0
  65. package/lib/middlewares/basicAuth.js +21 -0
  66. package/lib/middlewares/csp.js +13 -0
  67. package/lib/middlewares/csrf.js +31 -0
  68. package/lib/middlewares/htmx.js +24 -0
  69. package/lib/middlewares/i18n.js +30 -0
  70. package/lib/middlewares/rateLimit.js +28 -0
  71. package/lib/middlewares/redisCache.js +23 -0
  72. package/lib/middlewares/requirePermission.js +11 -0
  73. package/lib/middlewares/routeLocalization.js +11 -0
  74. package/lib/middlewares/session.js +16 -0
  75. package/lib/middlewares.js +85 -0
  76. package/lib/radix-router.js +111 -0
  77. package/lib/request.js +81 -0
  78. package/lib/response.js +175 -0
  79. package/lib/websocket.js +27 -0
  80. package/package.json +61 -0
  81. package/router.js +47 -0
  82. package/server.js +193 -0
  83. package/session.js +109 -0
@@ -0,0 +1,150 @@
1
+ module.exports = function setupDatabase(opts, router) {
2
+ let db = null;
3
+
4
+ if (opts.sqlite && !opts.db) {
5
+ opts.db = { type: 'sqlite', url: opts.sqlite };
6
+ }
7
+
8
+ if (!opts.db) return db;
9
+
10
+ const { type, url, options = {}, dbName } = opts.db;
11
+
12
+ if (type === 'sqlite') {
13
+ try {
14
+ const { DatabaseSync } = require('node:sqlite');
15
+ db = new DatabaseSync(url);
16
+ } catch (e) {
17
+ console.warn(`[!] Failed to load SQLite database: ${e.message}`);
18
+ console.warn(` If you are using Node < 22.5.0, node:sqlite might not be available.`);
19
+ }
20
+ } else if (type === 'postgres') {
21
+ try {
22
+ const { Pool } = require('pg');
23
+ db = new Pool(Object.assign({ connectionString: url }, options));
24
+ } catch (e) {
25
+ if (e.code === 'MODULE_NOT_FOUND') {
26
+ console.error('\n[x] Missing driver for PostgreSQL.');
27
+ console.error(' Please install it by running: npm install pg\n');
28
+ process.exit(1);
29
+ }
30
+ throw e;
31
+ }
32
+ } else if (type === 'mysql' || type === 'mariadb') {
33
+ try {
34
+ const mysql = require('mysql2/promise');
35
+ db = mysql.createPool(Object.assign({ uri: url }, options));
36
+ } catch (e) {
37
+ if (e.code === 'MODULE_NOT_FOUND') {
38
+ console.error(`\n[x] Missing driver for ${type}.`);
39
+ console.error(' Please install it by running: npm install mysql2\n');
40
+ process.exit(1);
41
+ }
42
+ throw e;
43
+ }
44
+ } else if (type === 'mongo') {
45
+ try {
46
+ const { MongoClient } = require('mongodb');
47
+ const client = new MongoClient(url, options);
48
+ client
49
+ .connect()
50
+ .then(() => {
51
+ console.log(`[fullstx] Connected to MongoDB (${dbName || 'default'})`);
52
+ })
53
+ .catch((err) => {
54
+ console.error(`[!] MongoDB connection error:`, err);
55
+ });
56
+ db = client.db(dbName);
57
+ } catch (e) {
58
+ if (e.code === 'MODULE_NOT_FOUND') {
59
+ console.error('\n[x] Missing driver for MongoDB.');
60
+ console.error(' Please install it by running: npm install mongodb\n');
61
+ process.exit(1);
62
+ }
63
+ throw e;
64
+ }
65
+ } else if (type === 'redis') {
66
+ try {
67
+ const { createClient } = require('redis');
68
+ db = createClient(Object.assign({ url }, options));
69
+ db.on('error', (err) => console.error('[!] Redis Client Error', err));
70
+ db.connect()
71
+ .then(() => {
72
+ console.log(`[fullstx] Connected to Redis`);
73
+ })
74
+ .catch((err) => {
75
+ console.error(`[!] Redis connection error:`, err);
76
+ });
77
+ } catch (e) {
78
+ if (e.code === 'MODULE_NOT_FOUND') {
79
+ console.error('\n[x] Missing driver for Redis.');
80
+ console.error(' Please install it by running: npm install redis\n');
81
+ process.exit(1);
82
+ }
83
+ throw e;
84
+ }
85
+ } else if (type === 'chroma') {
86
+ try {
87
+ const { ChromaClient } = require('chromadb');
88
+ db = new ChromaClient(Object.assign({ path: url }, options));
89
+ console.log(`[fullstx] Initialized ChromaDB client`);
90
+ } catch (e) {
91
+ if (e.code === 'MODULE_NOT_FOUND') {
92
+ console.error('\n[x] Missing driver for ChromaDB.');
93
+ console.error(' Please install it by running: npm install chromadb\n');
94
+ process.exit(1);
95
+ }
96
+ throw e;
97
+ }
98
+ } else if (type === 'firebase') {
99
+ try {
100
+ const admin = require('firebase-admin');
101
+ const app = admin.initializeApp(options);
102
+ db = app.firestore();
103
+ console.log(`[fullstx] Initialized Firebase Firestore`);
104
+ } catch (e) {
105
+ if (e.code === 'MODULE_NOT_FOUND') {
106
+ console.error('\n[x] Missing driver for Firebase.');
107
+ console.error(' Please install it by running: npm install firebase-admin\n');
108
+ process.exit(1);
109
+ }
110
+ throw e;
111
+ }
112
+ } else if (type === 'supabase') {
113
+ try {
114
+ const { createClient } = require('@supabase/supabase-js');
115
+ db = createClient(url, options.key, options);
116
+ console.log(`[fullstx] Initialized Supabase client`);
117
+ } catch (e) {
118
+ if (e.code === 'MODULE_NOT_FOUND') {
119
+ console.error('\n[x] Missing driver for Supabase.');
120
+ console.error(' Please install it by running: npm install @supabase/supabase-js\n');
121
+ process.exit(1);
122
+ }
123
+ throw e;
124
+ }
125
+ } else if (type === 'elasticsearch' || type === 'elastic') {
126
+ try {
127
+ const { Client } = require('@elastic/elasticsearch');
128
+ db = new Client(Object.assign({ node: url }, options));
129
+ console.log(`[fullstx] Initialized Elasticsearch client`);
130
+ } catch (e) {
131
+ if (e.code === 'MODULE_NOT_FOUND') {
132
+ console.error('\n[x] Missing driver for Elasticsearch.');
133
+ console.error(' Please install it by running: npm install @elastic/elasticsearch\n');
134
+ process.exit(1);
135
+ }
136
+ throw e;
137
+ }
138
+ } else {
139
+ console.warn(`[!] Unsupported database type: ${type}`);
140
+ }
141
+
142
+ if (db) {
143
+ router.use((req, res, next) => {
144
+ req.db = db;
145
+ next();
146
+ });
147
+ }
148
+
149
+ return db;
150
+ };
package/lib/errors.js ADDED
@@ -0,0 +1,23 @@
1
+ class FullstxError extends Error {
2
+ constructor(code, message) {
3
+ super(message);
4
+ this.code = code;
5
+ this.name = 'FullstxError';
6
+ }
7
+ }
8
+
9
+ const Errors = {
10
+ ROUTE_NOT_FOUND: (path) => new FullstxError('FSTX_ERR_ROUTE_NOT_FOUND', `Route not found: ${path}`),
11
+ PLUGIN_INVALID: (msg) =>
12
+ new FullstxError(
13
+ 'FSTX_ERR_PLUGIN_INVALID',
14
+ msg || 'Invalid plugin format. Must be a function or an object with a register method.'
15
+ ),
16
+ DB_NOT_CONFIGURED: () => new FullstxError('FSTX_ERR_DB_NOT_CONFIGURED', 'Database is not configured.'),
17
+ DEPENDENCY_MISSING: (pkg, feature) =>
18
+ new FullstxError('FSTX_ERR_DEPENDENCY_MISSING', `Please install '${pkg}' to use ${feature}.`),
19
+ INTERNAL_SERVER_ERROR: (msg) => new FullstxError('FSTX_ERR_INTERNAL', msg || 'Internal Server Error'),
20
+ INVALID_CONFIGURATION: (msg) => new FullstxError('FSTX_ERR_CONFIG', msg || 'Invalid configuration provided.')
21
+ };
22
+
23
+ module.exports = { FullstxError, Errors };
@@ -0,0 +1,22 @@
1
+ module.exports = function setupHooks(appInstance) {
2
+ appInstance.hooks = {
3
+ onRequest: [],
4
+ preValidation: [],
5
+ preHandler: [],
6
+ onResponse: []
7
+ };
8
+
9
+ appInstance.addHook = function (name, handler) {
10
+ if (this.hooks[name]) {
11
+ this.hooks[name].push(handler);
12
+ }
13
+ return this;
14
+ };
15
+
16
+ appInstance.executeHooks = async function (name, req, res) {
17
+ if (!this.hooks[name]) return;
18
+ for (const hook of this.hooks[name]) {
19
+ await hook(req, res);
20
+ }
21
+ };
22
+ };
@@ -0,0 +1,43 @@
1
+ const jwt = require('jsonwebtoken');
2
+
3
+ module.exports = function setupJwt(appInstance, opts) {
4
+ appInstance.jwt = {
5
+ sign(payload, expiresIn = '1h') {
6
+ return jwt.sign(payload, opts.jwtSecret, { expiresIn });
7
+ },
8
+ verify(token) {
9
+ try {
10
+ return jwt.verify(token, opts.jwtSecret);
11
+ } catch {
12
+ return null;
13
+ }
14
+ },
15
+ middleware(req, res, next) {
16
+ const authHeader = req.headers.authorization;
17
+ let token = null;
18
+ if (authHeader && authHeader.startsWith('Bearer ')) {
19
+ token = authHeader.split(' ')[1];
20
+ } else if (req.cookies && req.cookies.token) {
21
+ token = decodeURIComponent(req.cookies.token);
22
+ }
23
+ if (!token) {
24
+ if (res.json) {
25
+ return res.json({ error: 'Unauthorized: No token provided' }, 401);
26
+ }
27
+ res.writeHead(401, { 'Content-Type': 'application/json' });
28
+ return res.end(JSON.stringify({ error: 'Unauthorized: No token provided' }));
29
+ }
30
+ try {
31
+ const decoded = jwt.verify(token, opts.jwtSecret);
32
+ req.user = decoded;
33
+ next();
34
+ } catch {
35
+ if (res.json) {
36
+ return res.json({ error: 'Unauthorized: Invalid token' }, 401);
37
+ }
38
+ res.writeHead(401, { 'Content-Type': 'application/json' });
39
+ return res.end(JSON.stringify({ error: 'Unauthorized: Invalid token' }));
40
+ }
41
+ }
42
+ };
43
+ };
@@ -0,0 +1,29 @@
1
+ module.exports = function setupPlugins(appInstance) {
2
+ appInstance.plugins = new Set();
3
+
4
+ appInstance.registerPlugin = function (plugin, options = {}) {
5
+ if (this.plugins.has(plugin)) return this;
6
+ this.plugins.add(plugin);
7
+
8
+ const pluginMeta = Symbol.for('plugin-meta');
9
+ let pluginContext = this;
10
+
11
+ const isGlobal = plugin[pluginMeta] && plugin[pluginMeta].global === true;
12
+ if (!isGlobal) {
13
+ pluginContext = Object.create(this);
14
+ }
15
+
16
+ try {
17
+ if (typeof plugin === 'function') {
18
+ plugin(pluginContext, options);
19
+ } else if (typeof plugin === 'object' && typeof plugin.register === 'function') {
20
+ plugin.register(pluginContext, options);
21
+ } else {
22
+ throw new Error('Invalid plugin format. Must be a function or an object with a register method.');
23
+ }
24
+ } catch (err) {
25
+ console.error(`[fullstx] Error registering plugin:`, err);
26
+ }
27
+ return this;
28
+ };
29
+ };
@@ -0,0 +1,42 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ module.exports = function setupRouting(appInstance, router) {
5
+ appInstance.autoload = function (directory, prefix = '') {
6
+ const walk = (dir, currentPath = '') => {
7
+ const files = fs.readdirSync(dir);
8
+ for (const file of files) {
9
+ const fullPath = path.join(dir, file);
10
+ const stat = fs.statSync(fullPath);
11
+ if (stat.isDirectory()) {
12
+ walk(fullPath, `${currentPath}/${file}`);
13
+ } else if (file.endsWith('.js')) {
14
+ let routePath = `${currentPath}/${file.replace('.js', '')}`;
15
+ routePath = routePath.replace(/\[([^\]]+)\]/g, ':$1');
16
+ if (routePath.endsWith('/index')) routePath = routePath.replace('/index', '');
17
+ if (routePath === '') routePath = '/';
18
+
19
+ let finalPath = prefix + routePath;
20
+ if (finalPath.length > 1 && finalPath.endsWith('/')) {
21
+ finalPath = finalPath.slice(0, -1);
22
+ }
23
+
24
+ const routeModule = require(fullPath);
25
+
26
+ if (typeof routeModule === 'function') {
27
+ router.get(finalPath, routeModule);
28
+ } else if (typeof routeModule === 'object') {
29
+ if (routeModule.get) router.get(finalPath, routeModule.get);
30
+ if (routeModule.post) router.post(finalPath, routeModule.post);
31
+ if (routeModule.put) router.put(finalPath, routeModule.put);
32
+ if (routeModule.delete) router.delete(finalPath, routeModule.delete);
33
+ }
34
+ console.log(`[fullstx] Autoloaded route: ${finalPath}`);
35
+ }
36
+ }
37
+ };
38
+ if (fs.existsSync(directory)) walk(directory);
39
+ else console.warn(`[fullstx] Autoload directory not found: ${directory}`);
40
+ return this;
41
+ };
42
+ };
@@ -0,0 +1,377 @@
1
+ module.exports = function setupUtils(app) {
2
+ const opts = app.opts;
3
+ const router = app.router;
4
+ const createServer = require('../../server');
5
+ let Sentry = app.sentry;
6
+ let faker = app.faker;
7
+ let sanitizeHtml = app.sanitizeHtml;
8
+ let wsRedisPub = app.wsRedisPub;
9
+
10
+ [
11
+ 'apiKey',
12
+ 'basicAuth',
13
+ 'authorize',
14
+ 'requirePermission',
15
+ 'rateLimit',
16
+ 'session',
17
+ 'csrf',
18
+ 'i18n',
19
+ 'redisCache',
20
+ 'csp',
21
+ 'htmx',
22
+ 'routeLocalization'
23
+ ].forEach((name) => {
24
+ Object.defineProperty(app, name, {
25
+ get() {
26
+ return require('../middlewares/' + name);
27
+ }
28
+ });
29
+ });
30
+
31
+ Object.assign(app, {
32
+ dbPoolStats() {
33
+ if (!app.db) return { error: 'Database is not configured' };
34
+ if (opts.db && opts.db.type === 'postgres') {
35
+ return {
36
+ totalCount: app.db.totalCount,
37
+ idleCount: app.db.idleCount,
38
+ waitingCount: app.db.waitingCount
39
+ };
40
+ }
41
+ if (opts.db && (opts.db.type === 'mysql' || opts.db.type === 'mariadb')) {
42
+ const pool = app.db.pool || app.db;
43
+
44
+ const internalPool = pool.pool || pool;
45
+ if (internalPool && internalPool._allConnections) {
46
+ return {
47
+ totalCount: internalPool._allConnections.length,
48
+ idleCount: internalPool._freeConnections.length,
49
+ waitingCount: internalPool._connectionQueue ? internalPool._connectionQueue.length : 0
50
+ };
51
+ }
52
+ }
53
+ return { message: 'Pool stats not supported for this database type' };
54
+ },
55
+
56
+ sanitize(html, options) {
57
+ if (!sanitizeHtml) throw new Error('Please install sanitize-html to use sanitize()');
58
+ return sanitizeHtml(html, options);
59
+ },
60
+
61
+ metaTags(tags = {}) {
62
+ let html = '';
63
+ for (const [key, value] of Object.entries(tags)) {
64
+ if (!value) continue;
65
+ if (key === 'title') {
66
+ html += `<title>${value}</title>\n`;
67
+ } else if (key.startsWith('og:') || key.startsWith('twitter:')) {
68
+ html += `<meta property="${key}" content="${value}">\n`;
69
+ } else {
70
+ html += `<meta name="${key}" content="${value}">\n`;
71
+ }
72
+ }
73
+ return html;
74
+ },
75
+
76
+ sentry: Sentry,
77
+
78
+ faker,
79
+
80
+ wsBroadcast(data) {
81
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
82
+ if (wsRedisPub && wsRedisPub.isOpen) {
83
+ const channel = opts.ws.pubSubChannel || 'fullstx_ws_pubsub';
84
+ wsRedisPub.publish(channel, payload).catch(console.error);
85
+ } else if (this.wss) {
86
+ this.wss.clients.forEach((client) => {
87
+ if (client.readyState === 1) client.send(payload);
88
+ });
89
+ }
90
+ },
91
+
92
+ factory(count, generatorFn) {
93
+ if (!faker) throw new Error('Please install @faker-js/faker to use factory()');
94
+ const items = [];
95
+ for (let i = 0; i < count; i++) {
96
+ items.push(generatorFn(faker, i));
97
+ }
98
+ return items;
99
+ },
100
+
101
+ zod: require('zod'),
102
+
103
+ container: {
104
+ services: new Map(),
105
+ register(name, instance) {
106
+ this.services.set(name, instance);
107
+ },
108
+ get(name) {
109
+ if (!this.services.has(name)) {
110
+ throw new Error(`Service ${name} not found in IoC container`);
111
+ }
112
+ return this.services.get(name);
113
+ }
114
+ },
115
+
116
+ cache: new Map(),
117
+
118
+ upload(options = { dest: 'uploads/' }) {
119
+ const multer = require('multer');
120
+ return multer(options);
121
+ },
122
+
123
+ knex(config) {
124
+ const knexLib = require('knex');
125
+ return knexLib(config);
126
+ },
127
+
128
+ queue(name, connection) {
129
+ const { Queue } = require('bullmq');
130
+ return new Queue(name, { connection });
131
+ },
132
+
133
+ worker(name, processor, connection) {
134
+ const { Worker } = require('bullmq');
135
+ return new Worker(name, processor, { connection });
136
+ },
137
+
138
+ swagger(docPath = '/api-docs', swaggerDoc = {}) {
139
+ const swaggerUiDistPath = require('swagger-ui-dist').getAbsoluteFSPath();
140
+ const fs = require('fs');
141
+ const path = require('path');
142
+
143
+ router.get(docPath + '/swagger.json', (req, res) => {
144
+ if (res.json) return res.json(swaggerDoc);
145
+ res.writeHead(200, { 'Content-Type': 'application/json' });
146
+ res.end(JSON.stringify(swaggerDoc));
147
+ });
148
+
149
+ router.use((req, res, next) => {
150
+ if (req.url.startsWith(docPath + '/assets/')) {
151
+ const filePath = req.url.replace(docPath + '/assets/', '');
152
+ const absPath = path.join(swaggerUiDistPath, filePath);
153
+
154
+ if (absPath.startsWith(swaggerUiDistPath) && fs.existsSync(absPath)) {
155
+ const content = fs.readFileSync(absPath);
156
+ const ext = path.extname(absPath);
157
+ const mimes = { '.css': 'text/css', '.js': 'application/javascript', '.png': 'image/png' };
158
+ res.writeHead(200, { 'Content-Type': mimes[ext] || 'text/plain' });
159
+ return res.end(content);
160
+ }
161
+ }
162
+ next();
163
+ });
164
+
165
+ router.get(docPath, (req, res) => {
166
+ const html = `
167
+ <!DOCTYPE html>
168
+ <html lang="en">
169
+ <head>
170
+ <meta charset="UTF-8">
171
+ <title>Swagger UI</title>
172
+ <link rel="stylesheet" type="text/css" href="${docPath}/assets/swagger-ui.css" />
173
+ <link rel="stylesheet" type="text/css" href="${docPath}/assets/index.css" />
174
+ <link rel="icon" type="image/png" href="${docPath}/assets/favicon-32x32.png" />
175
+ </head>
176
+ <body>
177
+ <div id="swagger-ui"></div>
178
+ <script src="${docPath}/assets/swagger-ui-bundle.js"></script>
179
+ <script src="${docPath}/assets/swagger-ui-standalone-preset.js"></script>
180
+ <script>
181
+ window.onload = function() {
182
+ window.ui = SwaggerUIBundle({
183
+ url: "${docPath}/swagger.json",
184
+ dom_id: '#swagger-ui',
185
+ deepLinking: true,
186
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
187
+ plugins: [SwaggerUIBundle.plugins.DownloadUrl],
188
+ layout: "StandaloneLayout"
189
+ });
190
+ };
191
+ </script>
192
+ </body>
193
+ </html>`;
194
+ if (res.html) return res.html(html);
195
+ res.writeHead(200, { 'Content-Type': 'text/html' });
196
+ res.end(html);
197
+ });
198
+ },
199
+
200
+ mailer(config) {
201
+ const nodemailer = require('nodemailer');
202
+ return nodemailer.createTransport(config);
203
+ },
204
+
205
+ cdn(url, cdnBaseUrl = opts.cdn) {
206
+ if (!cdnBaseUrl) return url;
207
+ const base = cdnBaseUrl.endsWith('/') ? cdnBaseUrl.slice(0, -1) : cdnBaseUrl;
208
+ const path = url.startsWith('/') ? url : '/' + url;
209
+ return base + path;
210
+ },
211
+
212
+ sitemap(urls = [], hostname = 'http://localhost') {
213
+ return (req, res) => {
214
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
215
+ xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
216
+ for (const url of urls) {
217
+ const loc = typeof url === 'string' ? { url } : url;
218
+ xml += ' <url>\n';
219
+ xml += ` <loc>${hostname}${loc.url.startsWith('/') ? loc.url : '/' + loc.url}</loc>\n`;
220
+ if (loc.lastmod) xml += ` <lastmod>${loc.lastmod}</lastmod>\n`;
221
+ if (loc.changefreq) xml += ` <changefreq>${loc.changefreq}</changefreq>\n`;
222
+ if (loc.priority) xml += ` <priority>${loc.priority}</priority>\n`;
223
+ xml += ' </url>\n';
224
+ }
225
+ xml += '</urlset>';
226
+ res.writeHead(200, { 'Content-Type': 'application/xml' });
227
+ res.end(xml);
228
+ };
229
+ },
230
+
231
+ featureFlags: new Map(),
232
+ setFeature(name, isEnabled) {
233
+ this.featureFlags.set(name, isEnabled);
234
+ },
235
+ isFeatureEnabled(name) {
236
+ return !!this.featureFlags.get(name);
237
+ },
238
+ requireFeature(name) {
239
+ return (req, res, next) => {
240
+ if (!this.isFeatureEnabled(name)) {
241
+ if (res.json) return res.json({ error: `Feature '${name}' is disabled or not found.` }, 404);
242
+ res.writeHead(404, { 'Content-Type': 'application/json' });
243
+ return res.end(JSON.stringify({ error: `Feature '${name}' is disabled or not found.` }));
244
+ }
245
+ next();
246
+ };
247
+ },
248
+
249
+ async loadTest(options = {}) {
250
+ let autocannon;
251
+ try {
252
+ autocannon = require('autocannon');
253
+ } catch {
254
+ console.error("[!] Please install 'autocannon' to use loadTest()");
255
+ return;
256
+ }
257
+ const config = Object.assign(
258
+ {
259
+ url: 'http://localhost:3000',
260
+ connections: 10,
261
+ pipelining: 1,
262
+ duration: 10
263
+ },
264
+ options
265
+ );
266
+ console.log(`[fullstx] Starting load test on ${config.url} for ${config.duration}s...`);
267
+ const result = await autocannon(config);
268
+ console.log(autocannon.printResult(result));
269
+ return result;
270
+ },
271
+
272
+ edgeCache({ maxAge = 60, swr = 300 } = {}) {
273
+ return (req, res, next) => {
274
+ if (req.method === 'GET') {
275
+ res.setHeader('Cache-Control', `public, max-age=${maxAge}, stale-while-revalidate=${swr}`);
276
+ }
277
+ next();
278
+ };
279
+ },
280
+
281
+ queueDashboard(queues = [], endpoint = '/admin/queues') {
282
+ router.get(endpoint, async (req, res) => {
283
+ const stats = [];
284
+ for (const q of queues) {
285
+ try {
286
+ const [waiting, active, completed, failed, delayed] = await Promise.all([
287
+ q.getWaitingCount(),
288
+ q.getActiveCount(),
289
+ q.getCompletedCount(),
290
+ q.getFailedCount(),
291
+ q.getDelayedCount()
292
+ ]);
293
+ stats.push({ name: q.name, waiting, active, completed, failed, delayed });
294
+ } catch {
295
+ stats.push({ name: q.name, error: 'Could not fetch stats' });
296
+ }
297
+ }
298
+ if (res.json) return res.json({ queues: stats });
299
+ res.writeHead(200, { 'Content-Type': 'application/json' });
300
+ res.end(JSON.stringify({ queues: stats }));
301
+ });
302
+ },
303
+
304
+ exportHandler() {
305
+ const server = createServer(router.routes, router.middlewares, { staticDir: opts.staticDir });
306
+ return (req, res) => {
307
+ server.emit('request', req, res);
308
+ };
309
+ },
310
+
311
+ graphql(endpoint = '/graphql', schema, rootValue) {
312
+ let createHandler;
313
+ try {
314
+ createHandler = require('graphql-http/lib/use/http').createHandler;
315
+ } catch {
316
+ console.error("[!] Please install 'graphql' and 'graphql-http' to use graphql()");
317
+ return;
318
+ }
319
+ const handler = createHandler({ schema, rootValue });
320
+ router.use((req, res, next) => {
321
+ if (req.url.startsWith(endpoint) && (req.method === 'GET' || req.method === 'POST')) {
322
+ return handler(req, res);
323
+ }
324
+ next();
325
+ });
326
+ },
327
+
328
+ twoFactor() {
329
+ let otplib, qrcode;
330
+ try {
331
+ otplib = require('otplib');
332
+ qrcode = require('qrcode');
333
+ } catch {
334
+ console.error("[!] Please install 'otplib' and 'qrcode' to use twoFactor()");
335
+ return {};
336
+ }
337
+ return {
338
+ generateSecret(user = 'user', service = 'fullstx') {
339
+ const secret = otplib.authenticator.generateSecret();
340
+ const otpauth = otplib.authenticator.keyuri(user, service, secret);
341
+ return { secret, otpauth };
342
+ },
343
+ async generateQRCode(otpauth) {
344
+ return await qrcode.toDataURL(otpauth);
345
+ },
346
+ verify(token, secret) {
347
+ return otplib.authenticator.check(token, secret);
348
+ }
349
+ };
350
+ },
351
+
352
+ grpc() {
353
+ let grpcJs, protoLoader;
354
+ try {
355
+ grpcJs = require('@grpc/grpc-js');
356
+ protoLoader = require('@grpc/proto-loader');
357
+ } catch {
358
+ console.error("[!] Please install '@grpc/grpc-js' and '@grpc/proto-loader' to use gRPC.");
359
+ return {};
360
+ }
361
+ return {
362
+ loadProto(
363
+ protoPath,
364
+ options = { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true }
365
+ ) {
366
+ const packageDefinition = protoLoader.loadSync(protoPath, options);
367
+ return grpcJs.loadPackageDefinition(packageDefinition);
368
+ },
369
+ createServer() {
370
+ return new grpcJs.Server();
371
+ },
372
+ ServerCredentials: grpcJs.ServerCredentials,
373
+ credentials: grpcJs.credentials
374
+ };
375
+ }
376
+ });
377
+ };