kempo-server 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/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import http from 'http';
2
+ import router from './router.js';
3
+ import getFlags from './getFlags.js';
4
+
5
+ const flags = getFlags(process.argv.slice(2), {
6
+ port: 3000,
7
+ logging: 2,
8
+ root: './',
9
+ scan: false
10
+ }, {
11
+ p: 'port',
12
+ l: 'logging',
13
+ r: 'root',
14
+ s: 'scan'
15
+ });
16
+
17
+ if(typeof(flags.logging) === 'string'){
18
+ switch(flags.logging.toLowerCase()) {
19
+ case 'silent':
20
+ flags.logging = 0;
21
+ break;
22
+ case 'minimal':
23
+ flags.logging = 1;
24
+ break;
25
+ case 'verbose':
26
+ flags.logging = 3;
27
+ break;
28
+ case 'debug':
29
+ flags.logging = 4;
30
+ break;
31
+ default:
32
+ flags.logging = 2;
33
+ break;
34
+ }
35
+ }
36
+
37
+ const log = (message, level = 2) => {
38
+ if(level <= flags.logging){
39
+ console.log(message);
40
+ }
41
+ }
42
+
43
+ const server = http.createServer(await router(flags, log));
44
+ server.listen(flags.port);
45
+ log(`Server started at: http://localhost:${flags.port}`);
46
+
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "kempo-server",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "description": "",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "start": "node index.js -r docs"
9
+ },
10
+ "author": "",
11
+ "license": "ISC",
12
+ "devDependencies": {
13
+ "essentialcss": "^2.0.1"
14
+ }
15
+ }
@@ -0,0 +1,87 @@
1
+ import { URL } from 'url';
2
+
3
+ /**
4
+ * Creates an enhanced request object with Express-like functionality
5
+ * @param {IncomingMessage} request - The original Node.js request object
6
+ * @param {Object} params - Route parameters from dynamic routes
7
+ * @returns {Object} Enhanced request object
8
+ */
9
+ export function createRequestWrapper(request, params = {}) {
10
+ // Parse URL to extract query parameters
11
+ const url = new URL(request.url, `http://${request.headers.host || 'localhost'}`);
12
+ const query = Object.fromEntries(url.searchParams);
13
+
14
+ // Create the enhanced request object
15
+ const enhancedRequest = {
16
+ // Original request properties and methods
17
+ ...request,
18
+ method: request.method,
19
+ url: request.url,
20
+ headers: request.headers,
21
+
22
+ // Enhanced properties
23
+ params,
24
+ query,
25
+ path: url.pathname,
26
+
27
+ // Body parsing methods
28
+ async body() {
29
+ return new Promise((resolve, reject) => {
30
+ let body = '';
31
+
32
+ request.on('data', chunk => {
33
+ body += chunk.toString();
34
+ });
35
+
36
+ request.on('end', () => {
37
+ resolve(body);
38
+ });
39
+
40
+ request.on('error', reject);
41
+ });
42
+ },
43
+
44
+ async json() {
45
+ try {
46
+ const body = await this.body();
47
+ return JSON.parse(body);
48
+ } catch (error) {
49
+ throw new Error('Invalid JSON in request body');
50
+ }
51
+ },
52
+
53
+ async text() {
54
+ return this.body();
55
+ },
56
+
57
+ async buffer() {
58
+ return new Promise((resolve, reject) => {
59
+ const chunks = [];
60
+
61
+ request.on('data', chunk => {
62
+ chunks.push(chunk);
63
+ });
64
+
65
+ request.on('end', () => {
66
+ resolve(Buffer.concat(chunks));
67
+ });
68
+
69
+ request.on('error', reject);
70
+ });
71
+ },
72
+
73
+ // Utility methods
74
+ get(headerName) {
75
+ return request.headers[headerName.toLowerCase()];
76
+ },
77
+
78
+ is(type) {
79
+ const contentType = this.get('content-type') || '';
80
+ return contentType.includes(type);
81
+ }
82
+ };
83
+
84
+ return enhancedRequest;
85
+ }
86
+
87
+ export default createRequestWrapper;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Creates an enhanced response object with Express-like functionality
3
+ * @param {ServerResponse} response - The original Node.js response object
4
+ * @returns {Object} Enhanced response object
5
+ */
6
+ export function createResponseWrapper(response) {
7
+ // Track if response has been sent
8
+ let sent = false;
9
+
10
+ // Create the enhanced response object
11
+ const enhancedResponse = {
12
+ // Original response properties and methods
13
+ ...response,
14
+
15
+ // Status code management
16
+ status(code) {
17
+ if (sent) {
18
+ throw new Error('Cannot set status after response has been sent');
19
+ }
20
+ response.statusCode = code;
21
+ return enhancedResponse; // Allow chaining
22
+ },
23
+
24
+ // Header management
25
+ set(field, value) {
26
+ if (sent) {
27
+ throw new Error('Cannot set headers after response has been sent');
28
+ }
29
+ if (typeof field === 'object') {
30
+ // Set multiple headers: res.set({ 'Content-Type': 'text/html', 'X-Custom': 'value' })
31
+ Object.entries(field).forEach(([key, val]) => {
32
+ response.setHeader(key, val);
33
+ });
34
+ } else {
35
+ // Set single header: res.set('Content-Type', 'text/html')
36
+ response.setHeader(field, value);
37
+ }
38
+ return enhancedResponse; // Allow chaining
39
+ },
40
+
41
+ get(field) {
42
+ return response.getHeader(field);
43
+ },
44
+
45
+ // Content type helpers
46
+ type(contentType) {
47
+ if (sent) {
48
+ throw new Error('Cannot set content type after response has been sent');
49
+ }
50
+
51
+ // Handle common shortcuts
52
+ const typeMap = {
53
+ 'html': 'text/html',
54
+ 'json': 'application/json',
55
+ 'xml': 'application/xml',
56
+ 'text': 'text/plain',
57
+ 'css': 'text/css',
58
+ 'js': 'application/javascript'
59
+ };
60
+
61
+ const mimeType = typeMap[contentType] || contentType;
62
+ response.setHeader('Content-Type', mimeType);
63
+ return enhancedResponse; // Allow chaining
64
+ },
65
+
66
+ // JSON response
67
+ json(obj) {
68
+ if (sent) {
69
+ throw new Error('Cannot send response after it has already been sent');
70
+ }
71
+
72
+ sent = true;
73
+ response.setHeader('Content-Type', 'application/json');
74
+
75
+ try {
76
+ const jsonString = JSON.stringify(obj);
77
+ response.end(jsonString);
78
+ } catch (error) {
79
+ throw new Error('Failed to stringify object to JSON');
80
+ }
81
+
82
+ return enhancedResponse;
83
+ },
84
+
85
+ // Text response
86
+ send(data) {
87
+ if (sent) {
88
+ throw new Error('Cannot send response after it has already been sent');
89
+ }
90
+
91
+ sent = true;
92
+
93
+ if (data === null || data === undefined) {
94
+ response.end();
95
+ return enhancedResponse;
96
+ }
97
+
98
+ // Handle different data types
99
+ if (typeof data === 'object') {
100
+ // If it's an object, send as JSON
101
+ response.setHeader('Content-Type', 'application/json');
102
+ response.end(JSON.stringify(data));
103
+ } else if (typeof data === 'string') {
104
+ // If Content-Type not set, default to text/html for strings
105
+ if (!response.getHeader('Content-Type')) {
106
+ response.setHeader('Content-Type', 'text/html');
107
+ }
108
+ response.end(data);
109
+ } else if (Buffer.isBuffer(data)) {
110
+ // Handle buffers
111
+ response.end(data);
112
+ } else {
113
+ // Convert to string
114
+ if (!response.getHeader('Content-Type')) {
115
+ response.setHeader('Content-Type', 'text/plain');
116
+ }
117
+ response.end(String(data));
118
+ }
119
+
120
+ return enhancedResponse;
121
+ },
122
+
123
+ // HTML response helper
124
+ html(htmlString) {
125
+ if (sent) {
126
+ throw new Error('Cannot send response after it has already been sent');
127
+ }
128
+
129
+ sent = true;
130
+ response.setHeader('Content-Type', 'text/html');
131
+ response.end(htmlString);
132
+ return enhancedResponse;
133
+ },
134
+
135
+ // Text response helper
136
+ text(textString) {
137
+ if (sent) {
138
+ throw new Error('Cannot send response after it has already been sent');
139
+ }
140
+
141
+ sent = true;
142
+ response.setHeader('Content-Type', 'text/plain');
143
+ response.end(String(textString));
144
+ return enhancedResponse;
145
+ },
146
+
147
+ // Redirect helper
148
+ redirect(url, statusCode = 302) {
149
+ if (sent) {
150
+ throw new Error('Cannot redirect after response has been sent');
151
+ }
152
+
153
+ sent = true;
154
+ response.statusCode = statusCode;
155
+ response.setHeader('Location', url);
156
+ response.end();
157
+ return enhancedResponse;
158
+ },
159
+
160
+ // Cookie management (basic)
161
+ cookie(name, value, options = {}) {
162
+ if (sent) {
163
+ throw new Error('Cannot set cookies after response has been sent');
164
+ }
165
+
166
+ let cookieString = `${name}=${encodeURIComponent(value)}`;
167
+
168
+ if (options.maxAge) {
169
+ cookieString += `; Max-Age=${options.maxAge}`;
170
+ }
171
+ if (options.domain) {
172
+ cookieString += `; Domain=${options.domain}`;
173
+ }
174
+ if (options.path) {
175
+ cookieString += `; Path=${options.path}`;
176
+ }
177
+ if (options.secure) {
178
+ cookieString += '; Secure';
179
+ }
180
+ if (options.httpOnly) {
181
+ cookieString += '; HttpOnly';
182
+ }
183
+ if (options.sameSite) {
184
+ cookieString += `; SameSite=${options.sameSite}`;
185
+ }
186
+
187
+ const existingCookies = response.getHeader('Set-Cookie') || [];
188
+ const cookies = Array.isArray(existingCookies) ? existingCookies : [existingCookies];
189
+ cookies.push(cookieString);
190
+
191
+ response.setHeader('Set-Cookie', cookies);
192
+ return enhancedResponse;
193
+ },
194
+
195
+ // Clear cookie helper
196
+ clearCookie(name, options = {}) {
197
+ return this.cookie(name, '', { ...options, maxAge: 0 });
198
+ }
199
+ };
200
+
201
+ return enhancedResponse;
202
+ }
203
+
204
+ export default createResponseWrapper;
package/router.js ADDED
@@ -0,0 +1,159 @@
1
+ import path from 'path';
2
+ import { readFile } from 'fs/promises';
3
+ import { pathToFileURL } from 'url';
4
+ import defaultConfig from './defaultConfig.js';
5
+ import getFiles from './getFiles.js';
6
+ import findFile from './findFile.js';
7
+ import serveFile from './serveFile.js';
8
+
9
+ export default async (flags, log) => {
10
+ log('Initializing router', 2);
11
+ const rootPath = path.join(process.cwd(), flags.root);
12
+ log(`Root path: ${rootPath}`, 2);
13
+
14
+ let config = defaultConfig;
15
+ try {
16
+ const configPath = path.join(rootPath, '.config.json');
17
+ log(`Loading config from: ${configPath}`, 2);
18
+ const configContent = await readFile(configPath, 'utf8');
19
+ const userConfig = JSON.parse(configContent);
20
+ config = {
21
+ ...defaultConfig,
22
+ ...userConfig
23
+ };
24
+ log('User config loaded and merged with defaults', 2);
25
+ } catch (e){
26
+ log('Using default config (no .config.json found)', 2);
27
+ }
28
+
29
+ /*
30
+ Inject mandatory disallowed patterns
31
+ */
32
+ const dis = new Set(config.disallowedRegex);
33
+ dis.add("^/\\..*");
34
+ dis.add("\\.config$");
35
+ dis.add("\\.git/");
36
+ config.disallowedRegex = [...dis];
37
+ log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`, 2);
38
+
39
+ let files = await getFiles(rootPath, config, log);
40
+ log(`Initial scan found ${files.length} files`, 1);
41
+
42
+ // Process custom routes - resolve paths and validate files exist
43
+ const customRoutes = new Map();
44
+ if (config.customRoutes && Object.keys(config.customRoutes).length > 0) {
45
+ log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 2);
46
+
47
+ for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
48
+ try {
49
+ // Resolve the file path relative to the current working directory
50
+ const resolvedPath = path.resolve(filePath);
51
+
52
+ // Check if the file exists (we'll do this async)
53
+ const { stat } = await import('fs/promises');
54
+ await stat(resolvedPath);
55
+
56
+ customRoutes.set(urlPath, resolvedPath);
57
+ log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
58
+ } catch (error) {
59
+ log(`Custom route error for ${urlPath} -> ${filePath}: ${error.message}`, 1);
60
+ }
61
+ }
62
+ }
63
+
64
+ // Track 404 attempts to avoid unnecessary rescans
65
+ const rescanAttempts = new Map(); // path -> attempt count
66
+ const dynamicNoRescanPaths = new Set(); // paths that have exceeded max attempts
67
+
68
+ // Helper function to check if a path should skip rescanning
69
+ const shouldSkipRescan = (requestPath) => {
70
+ // Check static config patterns
71
+ const matchesConfigPattern = config.noRescanPaths.some(pattern => {
72
+ const regex = new RegExp(pattern);
73
+ return regex.test(requestPath);
74
+ });
75
+
76
+ if (matchesConfigPattern) {
77
+ log(`Skipping rescan for configured pattern: ${requestPath}`, 3);
78
+ return true;
79
+ }
80
+
81
+ // Check dynamic blacklist
82
+ if (dynamicNoRescanPaths.has(requestPath)) {
83
+ log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`, 3);
84
+ return true;
85
+ }
86
+
87
+ return false;
88
+ };
89
+
90
+ // Helper function to track rescan attempts
91
+ const trackRescanAttempt = (requestPath) => {
92
+ const currentAttempts = rescanAttempts.get(requestPath) || 0;
93
+ const newAttempts = currentAttempts + 1;
94
+ rescanAttempts.set(requestPath, newAttempts);
95
+
96
+ if (newAttempts >= config.maxRescanAttempts) {
97
+ dynamicNoRescanPaths.add(requestPath);
98
+ log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`, 1);
99
+ }
100
+
101
+ log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`, 2);
102
+ return newAttempts;
103
+ };
104
+
105
+ return async (req, res) => {
106
+ const requestPath = req.url.split('?')[0];
107
+ log(`${req.method} ${requestPath}`, 0);
108
+
109
+ // Check custom routes first
110
+ if (customRoutes.has(requestPath)) {
111
+ const customFilePath = customRoutes.get(requestPath);
112
+ log(`Serving custom route: ${requestPath} -> ${customFilePath}`, 2);
113
+
114
+ try {
115
+ const fileContent = await readFile(customFilePath);
116
+ const fileExtension = path.extname(customFilePath).toLowerCase().slice(1);
117
+ const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
118
+
119
+ log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
120
+ res.writeHead(200, { 'Content-Type': mimeType });
121
+ res.end(fileContent);
122
+ return; // Successfully served custom route
123
+ } catch (error) {
124
+ log(`Error serving custom route ${requestPath}: ${error.message}`, 0);
125
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
126
+ res.end('Internal Server Error');
127
+ return;
128
+ }
129
+ }
130
+
131
+ // Try to serve the file normally
132
+ const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
133
+
134
+ // If not served and scan flag is enabled, try rescanning once (with blacklist check)
135
+ if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
136
+ trackRescanAttempt(requestPath);
137
+ log('File not found, rescanning directory...', 1);
138
+ files = await getFiles(rootPath, config, log);
139
+ log(`Rescan found ${files.length} files`, 2);
140
+
141
+ // Try to serve again after rescan
142
+ const reserved = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log);
143
+
144
+ if (!reserved) {
145
+ log(`404 - File not found after rescan: ${requestPath}`, 1);
146
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
147
+ res.end('Not Found');
148
+ }
149
+ } else if (!served) {
150
+ if (shouldSkipRescan(requestPath)) {
151
+ log(`404 - Skipped rescan for: ${requestPath}`, 2);
152
+ } else {
153
+ log(`404 - File not found: ${requestPath}`, 1);
154
+ }
155
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
156
+ res.end('Not Found');
157
+ }
158
+ }
159
+ }
package/serveFile.js ADDED
@@ -0,0 +1,71 @@
1
+ import path from 'path';
2
+ import { readFile } from 'fs/promises';
3
+ import { pathToFileURL } from 'url';
4
+ import findFile from './findFile.js';
5
+ import createRequestWrapper from './requestWrapper.js';
6
+ import createResponseWrapper from './responseWrapper.js';
7
+
8
+ export default async (files, rootPath, requestPath, method, config, req, res, log) => {
9
+ log(`Attempting to serve: ${requestPath}`, 3);
10
+ const [file, params] = await findFile(files, rootPath, requestPath, method, log);
11
+
12
+ if (!file) {
13
+ log(`No file found for: ${requestPath}`, 3);
14
+ return false; // Could not find file
15
+ }
16
+
17
+ const fileName = path.basename(file);
18
+ log(`Found file: ${file}`, 2);
19
+
20
+ // Check if this is a route file that should be executed as a module
21
+ if (config.routeFiles.includes(fileName)) {
22
+ log(`Executing route file: ${fileName}`, 2);
23
+ try {
24
+ // Load the file as a module
25
+ const fileUrl = pathToFileURL(file).href;
26
+ log(`Loading module from: ${fileUrl}`, 3);
27
+ const module = await import(fileUrl);
28
+
29
+ // Execute the default export function
30
+ if (typeof module.default === 'function') {
31
+ log(`Executing route function with params: ${JSON.stringify(params)}`, 3);
32
+
33
+ // Create enhanced request and response wrappers
34
+ const enhancedRequest = createRequestWrapper(req, params);
35
+ const enhancedResponse = createResponseWrapper(res);
36
+
37
+ await module.default(enhancedRequest, enhancedResponse);
38
+ log(`Route executed successfully: ${fileName}`, 2);
39
+ return true; // Successfully served
40
+ } else {
41
+ log(`Route file does not export a function: ${fileName}`, 0);
42
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
43
+ res.end('Route file does not export a function');
44
+ return true; // Handled (even though it's an error)
45
+ }
46
+ } catch (error) {
47
+ log(`Error loading route file ${fileName}: ${error.message}`, 0);
48
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
49
+ res.end('Internal Server Error');
50
+ return true; // Handled (even though it's an error)
51
+ }
52
+ } else {
53
+ // Serve the file content with appropriate MIME type
54
+ log(`Serving static file: ${fileName}`, 2);
55
+ try {
56
+ const fileContent = await readFile(file);
57
+ const fileExtension = path.extname(file).toLowerCase().slice(1);
58
+ const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
59
+
60
+ log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`, 2);
61
+ res.writeHead(200, { 'Content-Type': mimeType });
62
+ res.end(fileContent);
63
+ return true; // Successfully served
64
+ } catch (error) {
65
+ log(`Error reading file ${file}: ${error.message}`, 0);
66
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
67
+ res.end('Internal Server Error');
68
+ return true; // Handled (even though it's an error)
69
+ }
70
+ }
71
+ };