zero-http 0.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -0
  3. package/documentation/controllers/cleanup.js +25 -0
  4. package/documentation/controllers/echo.js +14 -0
  5. package/documentation/controllers/headers.js +1 -0
  6. package/documentation/controllers/proxy.js +112 -0
  7. package/documentation/controllers/root.js +1 -0
  8. package/documentation/controllers/uploads.js +289 -0
  9. package/documentation/full-server.js +129 -0
  10. package/documentation/public/data/api.json +167 -0
  11. package/documentation/public/data/examples.json +62 -0
  12. package/documentation/public/data/options.json +13 -0
  13. package/documentation/public/index.html +414 -0
  14. package/documentation/public/prism-overrides.css +40 -0
  15. package/documentation/public/scripts/app.js +44 -0
  16. package/documentation/public/scripts/data-sections.js +300 -0
  17. package/documentation/public/scripts/helpers.js +166 -0
  18. package/documentation/public/scripts/playground.js +71 -0
  19. package/documentation/public/scripts/proxy.js +98 -0
  20. package/documentation/public/scripts/ui.js +210 -0
  21. package/documentation/public/scripts/uploads.js +459 -0
  22. package/documentation/public/styles.css +310 -0
  23. package/documentation/public/vendor/icons/fetch.svg +23 -0
  24. package/documentation/public/vendor/icons/plug.svg +27 -0
  25. package/documentation/public/vendor/icons/static.svg +35 -0
  26. package/documentation/public/vendor/icons/stream.svg +22 -0
  27. package/documentation/public/vendor/icons/zero.svg +21 -0
  28. package/documentation/public/vendor/prism-copy-to-clipboard.min.js +27 -0
  29. package/documentation/public/vendor/prism-javascript.min.js +1 -0
  30. package/documentation/public/vendor/prism-json.min.js +1 -0
  31. package/documentation/public/vendor/prism-okaidia.css +1 -0
  32. package/documentation/public/vendor/prism-toolbar.css +27 -0
  33. package/documentation/public/vendor/prism-toolbar.min.js +41 -0
  34. package/documentation/public/vendor/prism.min.js +1 -0
  35. package/index.js +43 -0
  36. package/lib/app.js +159 -0
  37. package/lib/body/index.js +14 -0
  38. package/lib/body/json.js +54 -0
  39. package/lib/body/multipart.js +310 -0
  40. package/lib/body/raw.js +40 -0
  41. package/lib/body/rawBuffer.js +74 -0
  42. package/lib/body/sendError.js +17 -0
  43. package/lib/body/text.js +43 -0
  44. package/lib/body/typeMatch.js +22 -0
  45. package/lib/body/urlencoded.js +166 -0
  46. package/lib/cors.js +72 -0
  47. package/lib/fetch.js +218 -0
  48. package/lib/logger.js +68 -0
  49. package/lib/rateLimit.js +64 -0
  50. package/lib/request.js +76 -0
  51. package/lib/response.js +165 -0
  52. package/lib/router.js +87 -0
  53. package/lib/static.js +196 -0
  54. package/package.json +44 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * In-memory rate-limiting middleware.
3
+ * Limits requests per IP address within a sliding window.
4
+ *
5
+ * @param {object} [opts]
6
+ * @param {number} [opts.windowMs=60000] - Time window in milliseconds.
7
+ * @param {number} [opts.max=100] - Maximum requests per window per IP.
8
+ * @param {string} [opts.message] - Custom error message.
9
+ * @param {number} [opts.statusCode=429] - HTTP status for rate-limited responses.
10
+ * @param {function} [opts.keyGenerator] - (req) => string; custom key extraction (default: req.ip).
11
+ * @returns {function} Middleware function.
12
+ */
13
+ function rateLimit(opts = {})
14
+ {
15
+ const windowMs = opts.windowMs || 60_000;
16
+ const max = opts.max || 100;
17
+ const statusCode = opts.statusCode || 429;
18
+ const message = opts.message || 'Too many requests, please try again later.';
19
+ const keyGenerator = typeof opts.keyGenerator === 'function' ? opts.keyGenerator : (req) => req.ip || 'unknown';
20
+
21
+ const hits = new Map(); // key → { count, resetTime }
22
+
23
+ // Periodic cleanup to prevent memory leaks
24
+ const cleanupInterval = setInterval(() =>
25
+ {
26
+ const now = Date.now();
27
+ for (const [key, entry] of hits)
28
+ {
29
+ if (now >= entry.resetTime) hits.delete(key);
30
+ }
31
+ }, windowMs);
32
+ if (cleanupInterval.unref) cleanupInterval.unref();
33
+
34
+ return (req, res, next) =>
35
+ {
36
+ const key = keyGenerator(req);
37
+ const now = Date.now();
38
+ let entry = hits.get(key);
39
+
40
+ if (!entry || now >= entry.resetTime)
41
+ {
42
+ entry = { count: 0, resetTime: now + windowMs };
43
+ hits.set(key, entry);
44
+ }
45
+
46
+ entry.count++;
47
+
48
+ // Set rate-limit headers
49
+ const remaining = Math.max(0, max - entry.count);
50
+ res.set('X-RateLimit-Limit', String(max));
51
+ res.set('X-RateLimit-Remaining', String(remaining));
52
+ res.set('X-RateLimit-Reset', String(Math.ceil(entry.resetTime / 1000)));
53
+
54
+ if (entry.count > max)
55
+ {
56
+ res.set('Retry-After', String(Math.ceil(windowMs / 1000)));
57
+ return res.status(statusCode).json({ error: message });
58
+ }
59
+
60
+ next();
61
+ };
62
+ }
63
+
64
+ module.exports = rateLimit;
package/lib/request.js ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @module request
3
+ * @description Lightweight wrapper around Node's `IncomingMessage`.
4
+ * Provides parsed query string, params, body, and convenience helpers.
5
+ */
6
+
7
+ /**
8
+ * Wrapped HTTP request.
9
+ *
10
+ * @property {import('http').IncomingMessage} raw - Original Node request.
11
+ * @property {string} method - HTTP method (e.g. 'GET').
12
+ * @property {string} url - Full request URL including query string.
13
+ * @property {object} headers - Lower-cased request headers.
14
+ * @property {object} query - Parsed query-string key/value pairs.
15
+ * @property {object} params - Route parameters populated by the router.
16
+ * @property {*} body - Request body (set by body-parsing middleware).
17
+ * @property {string|null} ip - Remote IP address.
18
+ */
19
+ class Request
20
+ {
21
+ /**
22
+ * @param {import('http').IncomingMessage} req - Raw Node incoming message.
23
+ */
24
+ constructor(req)
25
+ {
26
+ this.raw = req;
27
+ this.method = req.method;
28
+ this.url = req.url;
29
+ this.headers = req.headers;
30
+ this.query = this._parseQuery();
31
+ this.params = {};
32
+ this.body = null;
33
+ this.ip = req.socket ? req.socket.remoteAddress : null;
34
+ }
35
+
36
+ /**
37
+ * Parse the query string from `this.url` into a plain object.
38
+ *
39
+ * @private
40
+ * @returns {Object<string, string>} Parsed key-value pairs.
41
+ */
42
+ _parseQuery()
43
+ {
44
+ const idx = this.url.indexOf('?');
45
+ if (idx === -1) return {};
46
+ return Object.fromEntries(new URLSearchParams(this.url.slice(idx + 1)));
47
+ }
48
+
49
+ /**
50
+ * Get a specific request header (case-insensitive).
51
+ * @param {string} name
52
+ * @returns {string|undefined}
53
+ */
54
+ get(name)
55
+ {
56
+ return this.headers[name.toLowerCase()];
57
+ }
58
+
59
+ /**
60
+ * Check if the request Content-Type matches the given type.
61
+ * @param {string} type - e.g. 'json', 'html', 'application/json'
62
+ * @returns {boolean}
63
+ */
64
+ is(type)
65
+ {
66
+ const ct = this.headers['content-type'] || '';
67
+ if (type.indexOf('/') === -1)
68
+ {
69
+ // shorthand: 'json' → 'application/json', 'html' → 'text/html'
70
+ return ct.indexOf(type) !== -1;
71
+ }
72
+ return ct.indexOf(type) !== -1;
73
+ }
74
+ }
75
+
76
+ module.exports = Request;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @module response
3
+ * @description Lightweight wrapper around Node's `ServerResponse`.
4
+ * Provides chainable helpers for status, headers, and body output.
5
+ */
6
+
7
+ /**
8
+ * Wrapped HTTP response.
9
+ *
10
+ * @property {import('http').ServerResponse} raw - Original Node response.
11
+ */
12
+ class Response
13
+ {
14
+ /**
15
+ * @param {import('http').ServerResponse} res - Raw Node server response.
16
+ */
17
+ constructor(res)
18
+ {
19
+ this.raw = res;
20
+ /** @type {number} */
21
+ this._status = 200;
22
+ /** @type {Object<string, string>} */
23
+ this._headers = {};
24
+ /** @type {boolean} */
25
+ this._sent = false;
26
+ }
27
+
28
+ /**
29
+ * Set HTTP status code. Chainable.
30
+ *
31
+ * @param {number} code - HTTP status code (e.g. 200, 404).
32
+ * @returns {Response} `this` for chaining.
33
+ */
34
+ status(code) { this._status = code; return this; }
35
+
36
+ /**
37
+ * Set a response header. Chainable.
38
+ *
39
+ * @param {string} name - Header name.
40
+ * @param {string} value - Header value.
41
+ * @returns {Response} `this` for chaining.
42
+ */
43
+ set(name, value) { this._headers[name] = value; return this; }
44
+
45
+ /**
46
+ * Get a previously-set response header (case-insensitive).
47
+ *
48
+ * @param {string} name - Header name.
49
+ * @returns {string|undefined}
50
+ */
51
+ get(name)
52
+ {
53
+ const key = Object.keys(this._headers).find(k => k.toLowerCase() === name.toLowerCase());
54
+ return key ? this._headers[key] : undefined;
55
+ }
56
+
57
+ /**
58
+ * Set the Content-Type header.
59
+ * Accepts a shorthand alias (`'json'`, `'html'`, `'text'`, etc.) or
60
+ * a full MIME string. Chainable.
61
+ *
62
+ * @param {string} ct - MIME type or shorthand alias.
63
+ * @returns {Response} `this` for chaining.
64
+ */
65
+ type(ct)
66
+ {
67
+ const map = {
68
+ json: 'application/json',
69
+ html: 'text/html',
70
+ text: 'text/plain',
71
+ xml: 'application/xml',
72
+ form: 'application/x-www-form-urlencoded',
73
+ bin: 'application/octet-stream',
74
+ };
75
+ this._headers['Content-Type'] = map[ct] || ct;
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Send a response body and finalise the response.
81
+ * Auto-detects Content-Type (Buffer → octet-stream, string → text or
82
+ * HTML, object → JSON) when not explicitly set.
83
+ *
84
+ * @param {string|Buffer|object|null} body - Response payload.
85
+ */
86
+ send(body)
87
+ {
88
+ if (this._sent) return;
89
+ const res = this.raw;
90
+
91
+ Object.entries(this._headers).forEach(([k, v]) => res.setHeader(k, v));
92
+ res.statusCode = this._status;
93
+
94
+ if (body === undefined || body === null)
95
+ {
96
+ res.end();
97
+ this._sent = true;
98
+ return;
99
+ }
100
+
101
+ // Auto-detect Content-Type if not already set
102
+ const hasContentType = Object.keys(this._headers).some(k => k.toLowerCase() === 'content-type');
103
+
104
+ if (Buffer.isBuffer(body))
105
+ {
106
+ if (!hasContentType) res.setHeader('Content-Type', 'application/octet-stream');
107
+ res.end(body);
108
+ }
109
+ else if (typeof body === 'string')
110
+ {
111
+ if (!hasContentType)
112
+ {
113
+ // Heuristic: if it looks like HTML, set text/html
114
+ res.setHeader('Content-Type', body.trimStart().startsWith('<') ? 'text/html' : 'text/plain');
115
+ }
116
+ res.end(body);
117
+ }
118
+ else
119
+ {
120
+ // Object / array → JSON
121
+ if (!hasContentType) res.setHeader('Content-Type', 'application/json');
122
+ res.end(JSON.stringify(body));
123
+ }
124
+ this._sent = true;
125
+ }
126
+
127
+ /**
128
+ * Send a JSON response. Sets `Content-Type: application/json`.
129
+ *
130
+ * @param {*} obj - Value to serialise as JSON.
131
+ */
132
+ json(obj) { this.set('Content-Type', 'application/json'); return this.send(obj); }
133
+
134
+ /**
135
+ * Send a plain-text response. Sets `Content-Type: text/plain`.
136
+ *
137
+ * @param {string} str - Text payload.
138
+ */
139
+ text(str) { this.set('Content-Type', 'text/plain'); return this.send(String(str)); }
140
+
141
+ /**
142
+ * Send an HTML response. Sets `Content-Type: text/html`.
143
+ *
144
+ * @param {string} str - HTML payload.
145
+ */
146
+ html(str) { this.set('Content-Type', 'text/html'); return this.send(String(str)); }
147
+
148
+ /**
149
+ * Redirect to the given URL with an optional status code (default 302).
150
+ * @param {number|string} statusOrUrl - Status code or URL.
151
+ * @param {string} [url] - URL if first arg was status code.
152
+ */
153
+ redirect(statusOrUrl, url)
154
+ {
155
+ if (this._sent) return;
156
+ let code = 302;
157
+ let target = statusOrUrl;
158
+ if (typeof statusOrUrl === 'number') { code = statusOrUrl; target = url; }
159
+ this._status = code;
160
+ this.set('Location', target);
161
+ this.send('');
162
+ }
163
+ }
164
+
165
+ module.exports = Response;
package/lib/router.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @module router
3
+ * @description Simple pattern-matching router with named parameters,
4
+ * wildcard catch-alls, and sequential handler chains.
5
+ */
6
+
7
+ /**
8
+ * Convert a route path pattern into a RegExp and extract named parameter keys.
9
+ * Supports `:param` segments and trailing `*` wildcards.
10
+ *
11
+ * @param {string} path - Route pattern (e.g. '/users/:id', '/api/*').
12
+ * @returns {{ regex: RegExp, keys: string[] }} Compiled regex and ordered parameter names.
13
+ */
14
+ function pathToRegex(path)
15
+ {
16
+ // Wildcard catch-all: /api/*
17
+ if (path.endsWith('*'))
18
+ {
19
+ const prefix = path.slice(0, -1); // e.g. "/api/"
20
+ const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
+ return { regex: new RegExp('^' + escaped + '(.*)$'), keys: ['0'] };
22
+ }
23
+
24
+ const parts = path.split('/').filter(Boolean);
25
+ const keys = [];
26
+ const pattern = parts.map(p =>
27
+ {
28
+ if (p.startsWith(':')) { keys.push(p.slice(1)); return '([^/]+)'; }
29
+ return p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
30
+ }).join('/');
31
+ return { regex: new RegExp('^/' + pattern + '/?$'), keys };
32
+ }
33
+
34
+ class Router
35
+ {
36
+ /** Create a new Router with an empty route table. */
37
+ constructor() { this.routes = []; }
38
+
39
+ /**
40
+ * Register a route.
41
+ *
42
+ * @param {string} method - HTTP method (e.g. 'GET') or 'ALL' to match any.
43
+ * @param {string} path - Route pattern.
44
+ * @param {Function[]} handlers - One or more handler functions `(req, res, next) => void`.
45
+ */
46
+ add(method, path, handlers)
47
+ {
48
+ const { regex, keys } = pathToRegex(path);
49
+ this.routes.push({ method: method.toUpperCase(), path, regex, keys, handlers });
50
+ }
51
+
52
+ /**
53
+ * Match an incoming request against the route table and execute the first
54
+ * matching handler chain. Sends a 404 JSON response when no route matches.
55
+ *
56
+ * @param {import('./request')} req - Wrapped request.
57
+ * @param {import('./response')} res - Wrapped response.
58
+ */
59
+ handle(req, res)
60
+ {
61
+ const method = req.method.toUpperCase();
62
+ const url = req.url.split('?')[0];
63
+ for (const r of this.routes)
64
+ {
65
+ // ALL matches any method
66
+ if (r.method !== 'ALL' && r.method !== method) continue;
67
+ const m = url.match(r.regex);
68
+ if (!m) continue;
69
+ req.params = {};
70
+ r.keys.forEach((k, i) => req.params[k] = decodeURIComponent(m[i + 1] || ''));
71
+ // run handlers sequentially
72
+ let idx = 0;
73
+ const next = () =>
74
+ {
75
+ if (idx < r.handlers.length)
76
+ {
77
+ const h = r.handlers[idx++];
78
+ return h(req, res, next);
79
+ }
80
+ };
81
+ return next();
82
+ }
83
+ res.status(404).json({ error: 'Not Found' });
84
+ }
85
+ }
86
+
87
+ module.exports = Router;
package/lib/static.js ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @module static
3
+ * @description Static file-serving middleware with MIME detection, directory
4
+ * index files, extension fallbacks, dotfile policies, caching,
5
+ * and custom header hooks.
6
+ */
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ /**
11
+ * Extension → MIME-type lookup table.
12
+ * @type {Object<string, string>}
13
+ */
14
+ const MIME = {
15
+ // Text
16
+ '.html': 'text/html',
17
+ '.htm': 'text/html',
18
+ '.css': 'text/css',
19
+ '.txt': 'text/plain',
20
+ '.csv': 'text/csv',
21
+ '.xml': 'application/xml',
22
+ '.json': 'application/json',
23
+ '.jsonld': 'application/ld+json',
24
+
25
+ // JavaScript / WASM
26
+ '.js': 'application/javascript',
27
+ '.mjs': 'application/javascript',
28
+ '.wasm': 'application/wasm',
29
+
30
+ // Images
31
+ '.png': 'image/png',
32
+ '.jpg': 'image/jpeg',
33
+ '.jpeg': 'image/jpeg',
34
+ '.gif': 'image/gif',
35
+ '.webp': 'image/webp',
36
+ '.avif': 'image/avif',
37
+ '.svg': 'image/svg+xml',
38
+ '.ico': 'image/x-icon',
39
+ '.bmp': 'image/bmp',
40
+ '.tiff': 'image/tiff',
41
+ '.tif': 'image/tiff',
42
+
43
+ // Fonts
44
+ '.woff': 'font/woff',
45
+ '.woff2': 'font/woff2',
46
+ '.ttf': 'font/ttf',
47
+ '.otf': 'font/otf',
48
+ '.eot': 'application/vnd.ms-fontobject',
49
+
50
+ // Audio
51
+ '.mp3': 'audio/mpeg',
52
+ '.ogg': 'audio/ogg',
53
+ '.wav': 'audio/wav',
54
+ '.flac': 'audio/flac',
55
+ '.aac': 'audio/aac',
56
+ '.m4a': 'audio/mp4',
57
+
58
+ // Video
59
+ '.mp4': 'video/mp4',
60
+ '.webm': 'video/webm',
61
+ '.ogv': 'video/ogg',
62
+ '.avi': 'video/x-msvideo',
63
+ '.mov': 'video/quicktime',
64
+
65
+ // Documents / Archives
66
+ '.pdf': 'application/pdf',
67
+ '.zip': 'application/zip',
68
+ '.gz': 'application/gzip',
69
+ '.tar': 'application/x-tar',
70
+ '.7z': 'application/x-7z-compressed',
71
+
72
+ // Other
73
+ '.map': 'application/json',
74
+ '.yaml': 'text/yaml',
75
+ '.yml': 'text/yaml',
76
+ '.md': 'text/markdown',
77
+ '.sh': 'application/x-sh',
78
+ };
79
+
80
+ /**
81
+ * Stream a file to the raw Node response, setting Content-Type and
82
+ * Content-Length headers from the extension and optional `stat` result.
83
+ *
84
+ * @param {import('./response')} res - Wrapped response object.
85
+ * @param {string} filePath - Absolute path to the file.
86
+ * @param {import('fs').Stats} [stat] - Pre-fetched `fs.Stats` (for Content-Length).
87
+ */
88
+ function sendFile(res, filePath, stat)
89
+ {
90
+ const ext = path.extname(filePath).toLowerCase();
91
+ const ct = MIME[ext] || 'application/octet-stream';
92
+ const raw = res.raw;
93
+ try
94
+ {
95
+ raw.setHeader('Content-Type', ct);
96
+ if (stat && stat.size) raw.setHeader('Content-Length', stat.size);
97
+ }
98
+ catch (e) { /* best-effort */ }
99
+ const stream = fs.createReadStream(filePath);
100
+ stream.on('error', () => { try { raw.statusCode = 404; raw.end(); } catch (e) { } });
101
+ stream.pipe(raw);
102
+ }
103
+
104
+ /**
105
+ * Create a static-file-serving middleware.
106
+ *
107
+ * @param {string} root - Root directory to serve files from.
108
+ * @param {object} [options]
109
+ * @param {string|false} [options.index='index.html'] - Default file for directory requests, or `false` to disable.
110
+ * @param {number} [options.maxAge=0] - `Cache-Control` max-age in **milliseconds**.
111
+ * @param {string} [options.dotfiles='ignore'] - Dotfile policy: `'allow'` | `'deny'` | `'ignore'`.
112
+ * @param {string[]} [options.extensions] - Array of fallback extensions (e.g. `['html', 'htm']`).
113
+ * @param {Function} [options.setHeaders] - `(res, filePath) => void` hook to set custom headers.
114
+ * @returns {Function} Middleware `(req, res, next) => void`.
115
+ */
116
+ function serveStatic(root, options = {})
117
+ {
118
+ root = path.resolve(root);
119
+ const index = options.hasOwnProperty('index') ? options.index : 'index.html';
120
+ const maxAge = options.hasOwnProperty('maxAge') ? options.maxAge : 0;
121
+ const dotfiles = options.hasOwnProperty('dotfiles') ? options.dotfiles : 'ignore'; // allow|deny|ignore
122
+ const extensions = Array.isArray(options.extensions) ? options.extensions : null;
123
+ const setHeaders = typeof options.setHeaders === 'function' ? options.setHeaders : null;
124
+
125
+ function isDotfile(p)
126
+ {
127
+ return path.basename(p).startsWith('.');
128
+ }
129
+
130
+ function applyHeaders(res, filePath)
131
+ {
132
+ if (maxAge) try { res.raw.setHeader('Cache-Control', 'max-age=' + Math.floor(Number(maxAge) / 1000)); } catch (e) { }
133
+ if (setHeaders) try { setHeaders(res, filePath); } catch (e) { }
134
+ }
135
+
136
+ return (req, res, next) =>
137
+ {
138
+ if (req.method !== 'GET' && req.method !== 'HEAD') return next();
139
+ const urlPath = decodeURIComponent(req.url.split('?')[0]);
140
+ let file = path.join(root, urlPath);
141
+ if (!file.startsWith(root)) return res.status(403).json({ error: 'Forbidden' });
142
+
143
+ if (isDotfile(file) && dotfiles === 'deny') return res.status(403).json({ error: 'Forbidden' });
144
+
145
+ fs.stat(file, (err, st) =>
146
+ {
147
+ if (err)
148
+ {
149
+ // try extensions fallback
150
+ if (extensions && !urlPath.endsWith('/'))
151
+ {
152
+ (function tryExt(i)
153
+ {
154
+ if (i >= extensions.length) return next();
155
+ const ext = extensions[i].startsWith('.') ? extensions[i] : '.' + extensions[i];
156
+ const f = file + ext;
157
+ fs.stat(f, (e2, st2) =>
158
+ {
159
+ if (!e2 && st2 && st2.isFile())
160
+ {
161
+ if (isDotfile(f) && dotfiles === 'deny') return res.status(403).json({ error: 'Forbidden' });
162
+ applyHeaders(res, f);
163
+ return sendFile(res, f, st2);
164
+ }
165
+ tryExt(i + 1);
166
+ });
167
+ })(0);
168
+ return;
169
+ }
170
+ return next();
171
+ }
172
+
173
+ if (st.isDirectory())
174
+ {
175
+ if (!index) return next();
176
+ const idxFile = path.join(file, index);
177
+ fs.stat(idxFile, (err2, st2) =>
178
+ {
179
+ if (err2) return next();
180
+ if (isDotfile(idxFile) && dotfiles === 'deny') return res.status(403).json({ error: 'Forbidden' });
181
+ applyHeaders(res, idxFile);
182
+ sendFile(res, idxFile, st2);
183
+ });
184
+ }
185
+ else
186
+ {
187
+ if (isDotfile(file) && dotfiles === 'ignore') return next();
188
+ if (isDotfile(file) && dotfiles === 'deny') return res.status(403).json({ error: 'Forbidden' });
189
+ applyHeaders(res, file);
190
+ sendFile(res, file, st);
191
+ }
192
+ });
193
+ };
194
+ }
195
+
196
+ module.exports = serveStatic;
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "zero-http",
3
+ "version": "0.2.0",
4
+ "description": "Zero-dependency, minimal Express-like HTTP server and tiny fetch replacement",
5
+ "main": "index.js",
6
+ "files": [
7
+ "lib",
8
+ "documentation",
9
+ "index.js",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "start": "node documentation/full-server.js",
15
+ "docs": "node documentation/full-server.js",
16
+ "test": "node --test"
17
+ },
18
+ "keywords": [
19
+ "http",
20
+ "server",
21
+ "router",
22
+ "middleware",
23
+ "fetch",
24
+ "multipart",
25
+ "static",
26
+ "zero-http"
27
+ ],
28
+ "author": "Anthony Wiedman",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/tonywied17/zero-http-npm.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/tonywied17/zero-http-npm/issues"
36
+ },
37
+ "homepage": "https://zero-http.molex.cloud",
38
+ "engines": {
39
+ "node": ">=14.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }