@techiev2/vajra 1.4.2 → 1.4.3

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 CHANGED
@@ -22,7 +22,13 @@ Like the Vajra, this server delivers maximum power in minimal form.
22
22
 
23
23
  ## Changelog
24
24
 
25
- ### 1.4.1 (Current)
25
+ ### 1.4.3 (2026-01-02)
26
+ - Adds guardrails for unsafe operations with template paths.
27
+
28
+ ### 1.4.2 (2026-01-02)
29
+ - Fixes bug in parsing params that dropped file extensions.
30
+
31
+ ### 1.4.1 (2026-01-01)
26
32
  - Added support to handle drift in system time after signing
27
33
 
28
34
  ### 1.4.0 (2025-12-31)
package/README.md CHANGED
@@ -22,7 +22,13 @@ Like the Vajra, this server delivers maximum power in minimal form.
22
22
 
23
23
  ## Changelog
24
24
 
25
- ### 1.4.1 (Current)
25
+ ### 1.4.3 (2026-01-02)
26
+ - Adds guardrails for unsafe operations with template paths.
27
+
28
+ ### 1.4.2 (2026-01-02)
29
+ - Fixes bug in parsing params that dropped file extensions.
30
+
31
+ ### 1.4.1 (2026-01-01)
26
32
  - Added support to handle drift in system time after signing
27
33
 
28
34
  ### 1.4.0 (2025-12-31)
package/_ ADDED
@@ -0,0 +1,5 @@
1
+ const MIMES={html: 'text/html',js: 'application/javascript',css: 'text/css',json: 'application/json',png: 'image/png',jpg: 'image/jpeg',jpeg: 'image/jpeg',gif: 'image/gif',svg: 'image/svg+xml'}
2
+
3
+ res.sendFile = (filePath) => new Promise((resolve) => {
4
+ createReadStream(filePath).on('open', () => { res.setHeader('Content-Type', MIMES[filePath.split('.').pop()?.toLowerCase()] || 'application/octet-stream'); !res.getHeader('Accept-Ranges') && res.setHeader('Accept-Ranges', 'bytes'); }).on('error', (err) => { (err.code === 'ENOENT' ? default_404(req, res, `${filePath.split('/').slice(-1)} not found.`) : default_500(req, res)); resolve() }).pipe(res).on('finish', resolve).on('error', resolve);
5
+ });
package/_README.md ADDED
@@ -0,0 +1,226 @@
1
+ # Vajra ⚡
2
+
3
+ ![Vajra Thunderbolt](assets/vajra.jpg) <!-- or use one of the above hosted URLs if you like -->
4
+
5
+
6
+ **Ultra-minimal, zero-dependency Node.js HTTP server**
7
+ Routing · Middleware · Multipart parsing · HTML templating
8
+ All in **111 lines** of pure JavaScript
9
+
10
+ ## Name Origin
11
+
12
+ Vajra draws from the Rigvedic thunderbolt weapon of Indra — crafted from the bones of Sage Dadhichi, symbolizing unbreakable strength through selfless sacrifice.
13
+
14
+ Like the Vajra, this server delivers maximum power in minimal form.
15
+
16
+
17
+ [![npm version](https://img.shields.io/npm/v/@techiev2/vajra.svg?style=flat-square)](https://www.npmjs.com/package/@techiev2/vajra)
18
+ [![npm downloads](https://img.shields.io/npm/dm/vajra.svg?style=flat-square)](https://www.npmjs.com/package/@techiev2/vajra)
19
+ [![Node.js version](https://img.shields.io/node/v/@techiev2/vajra.svg?style=flat-square)](https://nodejs.org)
20
+ [![License](https://img.shields.io/npm/l/@techiev2/vajra.svg?style=flat-square)](LICENSE)
21
+
22
+
23
+ ## Changelog
24
+
25
+ ### 1.4.3 (2026-01-02)
26
+ - Adds guardrails for unsafe operations with template paths.
27
+
28
+ ### 1.4.2 (2026-01-02)
29
+ - Fixes bug in parsing params that dropped file extensions.
30
+
31
+ ### 1.4.1 (2026-01-01)
32
+ - Added support to handle drift in system time after signing
33
+
34
+ ### 1.4.0 (2025-12-31)
35
+ - Added full HS256 JWT support (`@techiev2/vajra/libs/auth/jwt.js`)
36
+ - Ultra-minimal, zero-dependency implementation
37
+ - Key and header caching for maximum performance
38
+ - Robust base64url handling
39
+ - Numeric exp validation and expiration checks
40
+
41
+ ### 1.3.0 (2025-12-30)
42
+ - Performance improvements to routing in bare routes
43
+
44
+ ### 1.2.0 (2025-12-30)
45
+ - Adds cookie support
46
+
47
+ ### 1.0.0 (2025-12-25)
48
+ - Initial release
49
+
50
+ ## Features
51
+
52
+ - Zero external dependencies
53
+ - Built-in routing with named parameters (`:id`)
54
+ - Asynchronous batched logging for performance
55
+ - Global middleware support with `next()` chaining
56
+ - JSON, urlencoded, and **multipart/form-data** body parsing
57
+ - Fast HTML templating with loops, nested objects, and simple array headers
58
+ - Helper methods: `res.json()`, `res.html()`, `res.status()`, `res.writeMessage()`
59
+ - Payload size limiting with 413 responses
60
+ - Sensible defaults for 404/405/500
61
+
62
+ ## Performance (Apple M4, Node 20+)
63
+
64
+ | Test Case | Vajra | Express + Multer | Notes |
65
+ |------------------------------------------------|----------------|------------------|---------------------------|
66
+ | 1MB Multipart Upload (wrk -t16 -c600) | **~94–98k req/s** | ~72k req/s | +30% faster |
67
+ | Idle RSS | ~52–53 MB | ~44 MB | Zero deps vs extra packages |
68
+ | Peak RSS under load | ~228 MB | ~209 MB | Full buffering trade-off |
69
+ | Code size (source) | **111 lines** | ~2k+ lines | Hand-crafted minimalism |
70
+
71
+ ## Performance Benchmarks (wrk)
72
+
73
+ ![wrk multipart benchmarks on M4 and VPS](assets/wrk-benchmarks.png)
74
+
75
+ ## Installation
76
+
77
+ ```bash
78
+ npm install vajra
79
+ ```
80
+
81
+
82
+ ## Quick Start
83
+ ```JavaScript
84
+ import Vajra from '../index.js';
85
+ import { encode } from 'node:querystring';
86
+
87
+ async function getUsers(query = {}) {
88
+ return (await fetch(`https://jsonplaceholder.typicode.com/users?${encode(query)}`)).json()
89
+ }
90
+
91
+ const { get, post, use, start, setProperty, log } = Vajra.create();
92
+
93
+ setProperty({ viewsRoot: `${import.meta.url}/views` })
94
+ // Or as a key-value pair
95
+ // setProperty('viewsRoot', `${import.meta.url}/views`)
96
+
97
+ use((req, res, next) => {
98
+ // Vajra provides an async batched logger to provide a balance between 100% log coverage and performance.
99
+ // If you prefer blocking immediate logs, you can switch to console.log
100
+ // or any other library of your choice.
101
+ log(`${req.method} ${req.url}`);
102
+ next();
103
+ });
104
+
105
+ get('/', (req, res) => {
106
+ res.writeMessage('Hello from Vajra ⚡');
107
+ });
108
+
109
+ post('/upload', (req, res) => {
110
+ res.json({ received: true, filesCount: req.files.length, files: req.files, body: req.body });
111
+ });
112
+
113
+ start({ port: 4002 }, () => {
114
+ console.log('Ready at http://localhost:4002');
115
+ });
116
+
117
+ get('/api/users', async ({ query }, res) => {
118
+ const users = await getUsers(query)
119
+ return res.json({ users })
120
+ })
121
+
122
+ get('/web/users', async ({ query }, res) => {
123
+ const users = await getUsers(query)
124
+ const headers = Object.keys(users[0])
125
+ return res.html(`users.html`, { users, headers })
126
+ })
127
+ ```
128
+
129
+ ## HTML Templating
130
+ ```JavaScript
131
+ import Vajra from '../index.js';
132
+ const { get, post, use, start, setProperty } = Vajra.create();
133
+
134
+ get('/users', (req, res) => {
135
+ const data = {
136
+ users: [
137
+ { id: 1, name: 'Alice' },
138
+ { id: 2, name: 'Bob' }
139
+ ],
140
+ headers: ['ID', 'Name']
141
+ };
142
+
143
+ // If no view root is set, .html() expects the absolute path.
144
+ res.html('views/users.html', data);
145
+ });
146
+ ```
147
+
148
+ #### views/users.html
149
+ ```html
150
+ <table>
151
+ <thead>
152
+ {{# headers }}
153
+ <th>{{ header@ }}</th>
154
+ {{/ headers }}
155
+ </thead>
156
+ <tbody>
157
+ {{# users }}
158
+ <tr>
159
+ <td>{{ id }}</td>
160
+ <td>{{ name }}</td>
161
+ </tr>
162
+ {{/ users }}
163
+ </tbody>
164
+ </table>
165
+ ```
166
+
167
+ Supports:
168
+
169
+ - Loops ({{# array }} ... {{/ array }})
170
+ - Dot notation ({{ user.name }})
171
+ - Special header shorthand ({{ header@ }} for simple arrays)
172
+
173
+
174
+ ## Configuration
175
+ ```JavaScript
176
+ const app = vajra.create({
177
+ maxFileSize: 10 // in MB (default: 2)
178
+ });
179
+
180
+ // Set view root path
181
+ app.setProperty('viewRoot', './views');
182
+ ```
183
+
184
+
185
+ ## API
186
+
187
+ - `get/post/put/patch/delete/head/options(path, handler)`
188
+ - `use(middleware)`
189
+ - `start({ port, host }, callback?)`
190
+ - `setProperty(key, value)` or `setProperty({ key: value })`
191
+ - `log(message)`
192
+
193
+
194
+ #### Response helpers:
195
+
196
+ `res.status(code)`
197
+ `res.json(data)`
198
+ `res.writeMessage(text)`
199
+ `res.html(pathOrString, data)`
200
+
201
+
202
+ ## Philosophy
203
+
204
+ Vajra is built on the principle that minimalism can maximise outcomes.
205
+
206
+ Everything you need for real internal tools, admin panels, APIs, and prototypes — without the bloat.
207
+
208
+ No dependencies.
209
+ No build step.
210
+ Just copy `index.js` and go.
211
+
212
+
213
+ ## Benchmarks & Memory
214
+ Run under extreme multipart load (wrk -t16 -c600 -d30s 1MB payloads):
215
+
216
+ Throughput: ~95k req/s
217
+ Idle RSS: ~52 MB
218
+ Peak under load: ~228 MB (drops back on idle)
219
+
220
+ ## License
221
+ MIT
222
+
223
+ ## Credits
224
+ Hand-crafted by [[Sriram Velamur](https://linkedin.com/in/techiev2)/[@techiev2](https://x.com/techiev2)]
225
+
226
+ Inspired by the desire for a truly tiny, powerful, and dependency-free Node server.
package/_index.js ADDED
@@ -0,0 +1,110 @@
1
+ import { createServer } from 'node:http'
2
+ import { readFile, access } from 'node:fs/promises'
3
+ import { resolve, isAbsolute } from 'path'
4
+
5
+ const BLOCK_MATCHER=/{{\s*#\s*(?<grpStart>\w+)\s*}}\s*(?<block>.*?)\s*{{\s*\/\s*(?<grpEnd>\w+)\s*}}/gmsi; const INNER_BLOCK_MATCHER = /{\s*(.*?)\s*}/gmsi
6
+ const LOOP_MATCHER=/({\s*\w+@\s*})/gmis; const ESCAPE = { regex: /[&<>"']/g, map: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' }}
7
+
8
+ function default_404({ url, method, isPossibleJSON }, res) {
9
+ const message = `Route ${url} not found for method ${method}.`
10
+ res.status(404); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
11
+ }
12
+ function default_405({ url, method, isPossibleJSON }, res) {
13
+ const message = `Method ${method} not allowed by route ${url}.`
14
+ res.status(405); return isPossibleJSON ? res.json({ message }) : res.writeMessage(message)
15
+ }
16
+ function default_500({ url, method }, res, error) {
17
+ process.env.DEBUG && console.log({ error })
18
+ res.status(500).writeMessage(process.env.DEBUG ? `${error.stack}` : `Server error.\nRoute: ${url}\nMethod: ${method}\nTimestamp: ${new Date().getTime()}\n`)
19
+ }
20
+ function default_413(res) {
21
+ res.status(413).writeMessage('Payload Too Large')
22
+ }
23
+
24
+ const MAX_MB = 2; const MAX_FILE_SIZE = MAX_MB * 1024 * 1024
25
+
26
+ export default class Vajra {
27
+ static #app; static #routes = {}; static #middlewares = []; static #straightRoutes = {}; static #MAX_FILE_SIZE; static #onCreate; static #props = {}
28
+ static create({ maxFileSize } = { maxFileSize: 2 }) {
29
+ Vajra.#app = createServer()
30
+ const _queue = []; const LOG_QUEUE_SIZE = 100; const logOut = () => { if (_queue.length) { process.stdout.write(`${_queue.join('').trim()}\n`) }; _queue.length = 0 };
31
+ const flushAndShutDown = () => { logOut(); Vajra.#app.close(() => { process.exit(0); }); }; 'SIGINT_SIGTERM_SIGABRT'.split('_').map((evt) => process.on(evt, flushAndShutDown));
32
+ process.on('exit', logOut); function log(message) { _queue.push(`${message}\n`); if (_queue.length >= LOG_QUEUE_SIZE) { logOut(); _queue.length = 0; } }
33
+ Vajra.#MAX_FILE_SIZE = !+MAX_FILE_SIZE ? +maxFileSize * 1024 * 1024 : MAX_FILE_SIZE
34
+ Vajra.#app.on('request', async (req, res) => {
35
+ if (+(req.headers['Content-Length'] || req.headers['content-length']) > Vajra.#MAX_FILE_SIZE) { return default_413(res) }
36
+ res.sent = false; res.status = (/**@type{code} Number*/ code) => { if (!+code || +code < 100 || +code > 599) { throw new Error(`Invalid status code ${code}`) }; res.statusCode = code; res.statusSet = true; return res }
37
+ res.json = data => {
38
+ if (res.sent) return res; if (!res.statusSet) res.statusCode = 200
39
+ const response = JSON.stringify(data); res.sent = true; res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.from(response).byteLength); res.write(response); res.end(); return res
40
+ }
41
+ res.writeMessage = (message = '') => {
42
+ if (res.sent) { return res }; if (!message) { res.status(500); message = 'Server error'; }
43
+ res.sent = true; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', Buffer.from(message).byteLength); res.write(message); res.end()
44
+ return res
45
+ }
46
+ function sanitize(value) { if (value == null) return ''; return String(value).replace(ESCAPE.regex, m => ESCAPE.map[m]); }
47
+ res.html = async (templatePath, data = {}) => {
48
+ templatePath = resolve(templatePath); const appRoot = resolve(process.cwd()); const root = this.#props.viewRoot ? resolve(this.#props.viewRoot) : appRoot;
49
+ const _templatePath = resolve(`${root}/${templatePath}`)
50
+ if (templatePath.includes('..') || !_templatePath.startsWith(appRoot) || !root.startsWith(appRoot)) { return default_500(req, res, new Error("Invalid template path")) }
51
+ if (res.sent) { return res }; let content; templatePath = `${root}/${_templatePath}`
52
+ try { await access(templatePath); content = (await readFile(templatePath)).toString() } catch (_) { content = templatePath }
53
+ content.matchAll(BLOCK_MATCHER).forEach((match) => { if (!match?.groups || (match.groups.grpStart !== match.groups.grpEnd)) { return }; const data_ = (data[match.groups.grpStart] || []); if (match.groups.block.indexOf('@') !== -1) { content = content.replace(match[2], data_.map((key) => match.groups.block.replace(LOOP_MATCHER, sanitize(key))).join('')); return }
54
+ if (!data_.length) { content = content.replace(match.groups.block, '') } else { data_.forEach((dataItem) => { match.groups.block.matchAll(INNER_BLOCK_MATCHER).forEach((_match) => { const parts = _match[1].split('.').slice(1); let value = dataItem; parts.forEach((part) => (value = value ? value[part] : value)); content = content.replace(_match[0], sanitize(value)) }) }) }
55
+ }); content = content.replace(/{{\s*# .*?\s*}}/gmsi, '').replace(/{{\s*\/.*?}}/gmsi, '')
56
+ if (!res.statusSet) res.statusCode = 200; res.sent = true; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.from(content).byteLength); res.write(content); res.end(); return res
57
+ }
58
+ req._headers = { ...req.headers }; req.headers = Object.fromEntries(req.rawHeaders.map((e, i) => i % 2 ? false : [e, req.rawHeaders[i + 1]]).filter(Boolean)); req.isPossibleJSON = req._headers['content-type'] === 'application/json'; req.params = {}
59
+ res.cookie = (k, v, options) => {
60
+ let { expires, path, maxAge, domain, secure, httpOnly, sameSite } = typeof options === 'object' ? options : typeof v === 'object' ? v : {}; const cookieOpts = [];!isNaN(+maxAge) && cookieOpts.push(`Max-Age=${Math.floor(maxAge)}`); !isNaN(+expires) && cookieOpts.push(`Expires=${new Date(expires).toUTCString()}`); expires instanceof Date && cookieOpts.push(`Expires=${expires.toUTCString()}`)
61
+ path && cookieOpts.push(`Path=${path}`); domain && cookieOpts.push(`Domain=${domain}`); !!secure && cookieOpts.push(`Secure`); !!httpOnly && cookieOpts.push(`HttpOnly`); sameSite = sameSite && (sameSite === true ? 'Strict' : typeof sameSite === 'string' ? sameSite.charAt(0).toUpperCase() + sameSite.slice(1).toLowerCase() : ''); !['Strict', 'Lax', 'None'].includes(sameSite) ? sameSite = 'Strict' : sameSite = sameSite; cookieOpts.push(`SameSite=${sameSite}`)
62
+ res.setHeader('Set-Cookie', (typeof k === 'object') ? Object.entries(k).map(([k_, v_]) => `${k_}=${encodeURIComponent(v_)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`) : [...(res.getHeader('Set-Cookie') || []).filter(Boolean), `${k}=${encodeURIComponent(v)}${cookieOpts.length ? `; ${cookieOpts.join('; ')}` : ''}`])
63
+ }
64
+ let url = `http://${req.headers.host || req.headers.host}/${req.url}`; req.query = Object.fromEntries(new URL(url).searchParams)
65
+ if (req.method === 'GET' || req.method === 'HEAD') { return runMiddlwares() }
66
+ async function runMiddlwares() { let idx = 0; const next = async () => { if (idx >= Vajra.#middlewares.length) { return setImmediate(handleRoute) } const fn = Vajra.#middlewares[idx]; idx++; try { await fn(req, res, next); } catch (err) { return default_500({ url: req.url, method: req.method }, res, err); } }; await next(); }
67
+ setImmediate(() => {
68
+ req.body = {}; req.rawData = ''; req.formData = {}; let dataSize = 0
69
+ req.on('data', (chunk) => { dataSize += chunk.length; if (dataSize > Vajra.#MAX_FILE_SIZE) { return default_413(res) }; req.rawData+=chunk })
70
+ const formDataMatcher = /Content-Disposition: form-data; name=['"](?<name>[^"']+)['"]\s+(?<value>.*?)$/smi;
71
+ let boundaryMatch = (req.headers['Content-Type'] || '').match(/boundary=(.*)/); const boundary = boundaryMatch ? '--' + boundaryMatch[1] : null; const fileDataMatcher = /^Content-Disposition:.*?name=["'](?<field>[^"']+)["'].*?filename=["'](?<fileName>[^"']+)["'].*?Content-Type:\s*(?<contentType>[^\r\n]*)\r?\n\r?\n(?<content>[\s\S]*)$/ims
72
+ req.on('end', async () => {
73
+ req.files = []; if (boundary) { req.rawData.split(boundary).filter(Boolean).map((line) => {
74
+ let key, value; if (line.includes('filename')) { req.files.push(fileDataMatcher.exec(line)?.groups || {}); return }
75
+ [key, value] = Object.values(line.match(formDataMatcher)?.groups || {}); (key && value) && Object.assign(req.formData, { [key]: value }); return
76
+ })
77
+ }
78
+ if (Object.keys(req.formData).length) { req.body = req.formData } else {
79
+ try { req.body = JSON.parse(req.rawData); req.isPossibleJSON = true } catch (_) { req.body = Object.fromEntries(req.rawData.split('&').map((pair) => pair.split('='))) }
80
+ }; setImmediate(runMiddlwares)
81
+ })
82
+ req.cookies = Object.fromEntries((req.headers.Cookie || req.headers.cookie || '').split(/;\s*/).map((k) => k.split('=')).map(([k, v]) => [k.trim(), decodeURIComponent(v).trim()]))
83
+ })
84
+ async function handleRoute() {
85
+ let _url = req.url.split('?')[0]; if (_url.endsWith('/')) _url = _url.split('/').slice(0, -1).join('/')
86
+ let match_; const directHandler = (Vajra.#straightRoutes[_url] || Vajra.#straightRoutes[`${_url}/`] || {})[req.method.toLowerCase()]
87
+ if (directHandler) { try { await directHandler(req, res); if (!res.sent && !res.writableEnded) res.end() } catch (error) { return default_500(req, res, error) }; return }
88
+ Object.entries(Vajra.#routes).map(([route, handler]) => {
89
+ if (match_) { return }; const match = new RegExp(route).exec(req.url); if (!!match && handler[req.method.toLowerCase()]) { match_ = handler; Object.assign(req.params, match.groups); return }
90
+ })
91
+ if (!match_) { return default_404(req, res) }
92
+ if (!match_[req.method.toLowerCase()]) { return default_405(req, res) }
93
+ try { await match_[req.method.toLowerCase()](req, res); if (!res.sent && !res.writableEnded) res.end() } catch (error) { return default_500(req, res, error) }
94
+ }
95
+ })
96
+ function setProperty(k, v) { Object.assign(Vajra.#props, typeof k == 'object' ? k: { k: v }); return defaults }
97
+ function start({ port, host = '127.0.0.1' }, cb) { Vajra.#app.listen(port, () => { console.log(`App listening at http://${host}:${port}`); if (typeof cb === 'function') { cb() } }); return defaults }
98
+ function register(method, path, handler) {
99
+ const paramMatcher = /.*?(?<param>\:[a-zA-Z]{1,})/g; let pathMatcherStr = path
100
+ path.matchAll(paramMatcher).forEach(match => pathMatcherStr = pathMatcherStr.replace(match.groups.param, `{0,1}(?<${match.groups.param.slice(1)}>[\\w|\.]+)`))
101
+ if (path !== '/' && pathMatcherStr.endsWith('/')) { pathMatcherStr = pathMatcherStr.replace(/(\/)$/, '/?') }
102
+ if (!paramMatcher.exec(path)?.groups) { Vajra.#straightRoutes[pathMatcherStr] = Object.assign(Vajra.#straightRoutes[pathMatcherStr] || {}, { [method]: handler }); return }
103
+ Vajra.#routes[pathMatcherStr] = Object.assign(Vajra.#routes[pathMatcherStr] || {}, {[method]: handler}); return defaults
104
+ }
105
+ function use(fn) {
106
+ if (typeof fn !== "function") { throw new Error(`${fn} is not a function. Can't use as middleware`) }; Vajra.#middlewares.push(fn); return defaults
107
+ }
108
+ const defaults = Object.freeze(Object.assign({}, { use, setProperty, start, log }, Object.fromEntries('get__post__put__patch__delete__head__options'.split('__').map((method) => [method, (...args) => register(method, ...args)])))); return Object.assign({}, { start }, defaults)
109
+ }
110
+ }
package/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createServer } from 'node:http'
2
2
  import { readFile, access } from 'node:fs/promises'
3
+ import { resolve, isAbsolute } from 'path'
3
4
 
4
5
  const BLOCK_MATCHER=/{{\s*#\s*(?<grpStart>\w+)\s*}}\s*(?<block>.*?)\s*{{\s*\/\s*(?<grpEnd>\w+)\s*}}/gmsi; const INNER_BLOCK_MATCHER = /{\s*(.*?)\s*}/gmsi
5
- const LOOP_MATCHER=/({\s*\w+@\s*})/gmis
6
+ const LOOP_MATCHER=/({\s*\w+@\s*})/gmis; const ESCAPE = { regex: /[&<>"']/g, map: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' }}
6
7
 
7
8
  function default_404({ url, method, isPossibleJSON }, res) {
8
9
  const message = `Route ${url} not found for method ${method}.`
@@ -32,14 +33,9 @@ export default class Vajra {
32
33
  Vajra.#MAX_FILE_SIZE = !+MAX_FILE_SIZE ? +maxFileSize * 1024 * 1024 : MAX_FILE_SIZE
33
34
  Vajra.#app.on('request', async (req, res) => {
34
35
  if (+(req.headers['Content-Length'] || req.headers['content-length']) > Vajra.#MAX_FILE_SIZE) { return default_413(res) }
35
- res.sent = false;
36
- res.status = (/**@type{code} Number*/ code) => {
37
- if (!+code || +code < 100 || +code > 599) { throw new Error(`Invalid status code ${code}`) }
38
- res.statusCode = code; res.statusSet = true; return res
39
- }
36
+ res.sent = false; res.status = (/**@type{code} Number*/ code) => { if (!+code || +code < 100 || +code > 599) { throw new Error(`Invalid status code ${code}`) }; res.statusCode = code; res.statusSet = true; return res }
40
37
  res.json = data => {
41
- if (res.sent) return res
42
- if (!res.statusSet) res.statusCode = 200
38
+ if (res.sent) return res; if (!res.statusSet) res.statusCode = 200
43
39
  const response = JSON.stringify(data); res.sent = true; res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Length', Buffer.from(response).byteLength); res.write(response); res.end(); return res
44
40
  }
45
41
  res.writeMessage = (message = '') => {
@@ -47,17 +43,17 @@ export default class Vajra {
47
43
  res.sent = true; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Length', Buffer.from(message).byteLength); res.write(message); res.end()
48
44
  return res
49
45
  }
46
+ function sanitize(value) { if (value == null) return ''; return String(value).replace(ESCAPE.regex, m => ESCAPE.map[m]); }
50
47
  res.html = async (templatePath, data = {}) => {
51
- if (res.sent) { return res }; let content;
52
- if (this.#props.viewRoot) templatePath = `${this.#props.viewRoot}/${templatePath}`
48
+ templatePath = resolve(templatePath); const appRoot = resolve(process.cwd()); const root = this.#props.viewRoot ? resolve(this.#props.viewRoot) : appRoot;
49
+ const _templatePath = resolve(`${root}/${templatePath}`)
50
+ if (templatePath.includes('..') || !_templatePath.startsWith(appRoot) || !root.startsWith(appRoot)) { return default_500(req, res, new Error("Invalid template path")) }
51
+ if (res.sent) { return res }; let content; templatePath = `${root}/${_templatePath}`
53
52
  try { await access(templatePath); content = (await readFile(templatePath)).toString() } catch (_) { content = templatePath }
54
- content.matchAll(BLOCK_MATCHER).forEach((match) => {
55
- if (!match?.groups || (match.groups.grpStart !== match.groups.grpEnd)) { return }
56
- const data_ = (data[match.groups.grpStart] || []); if (match.groups.block.indexOf('@') !== -1) { content = content.replace(match[2], data_.map((key) => match.groups.block.replace(LOOP_MATCHER, key)).join('')); return }
57
- data_.forEach((dataItem) => { match.groups.block.matchAll(INNER_BLOCK_MATCHER).forEach((_match) => { const parts = _match[1].split('.').slice(1); let value = dataItem; parts.forEach((part) => (value = value ? value[part] : value)) ; content = content.replace(_match[0], value) }) })
53
+ content.matchAll(BLOCK_MATCHER).forEach((match) => { if (!match?.groups || (match.groups.grpStart !== match.groups.grpEnd)) { return }; const data_ = (data[match.groups.grpStart] || []); if (match.groups.block.indexOf('@') !== -1) { content = content.replace(match[2], data_.map((key) => match.groups.block.replace(LOOP_MATCHER, sanitize(key))).join('')); return }
54
+ if (!data_.length) { content = content.replace(match.groups.block, '') } else { data_.forEach((dataItem) => { match.groups.block.matchAll(INNER_BLOCK_MATCHER).forEach((_match) => { const parts = _match[1].split('.').slice(1); let value = dataItem; parts.forEach((part) => (value = value ? value[part] : value)); content = content.replace(_match[0], sanitize(value)) }) }) }
58
55
  }); content = content.replace(/{{\s*# .*?\s*}}/gmsi, '').replace(/{{\s*\/.*?}}/gmsi, '')
59
- if (!res.statusSet) res.statusCode = 200
60
- res.sent = true; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.from(content).byteLength); res.write(content); res.end(); return res
56
+ if (!res.statusSet) res.statusCode = 200; res.sent = true; res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.from(content).byteLength); res.write(content); res.end(); return res
61
57
  }
62
58
  req._headers = { ...req.headers }; req.headers = Object.fromEntries(req.rawHeaders.map((e, i) => i % 2 ? false : [e, req.rawHeaders[i + 1]]).filter(Boolean)); req.isPossibleJSON = req._headers['content-type'] === 'application/json'; req.params = {}
63
59
  res.cookie = (k, v, options) => {
@@ -67,10 +63,7 @@ export default class Vajra {
67
63
  }
68
64
  let url = `http://${req.headers.host || req.headers.host}/${req.url}`; req.query = Object.fromEntries(new URL(url).searchParams)
69
65
  if (req.method === 'GET' || req.method === 'HEAD') { return runMiddlwares() }
70
- async function runMiddlwares() {
71
- let idx = 0; const next = async () => { if (idx >= Vajra.#middlewares.length) { return setImmediate(handleRoute) } const fn = Vajra.#middlewares[idx]; idx++; try { await fn(req, res, next); } catch (err) { return default_500({ url: req.url, method: req.method }, res, err); } };
72
- await next();
73
- }
66
+ async function runMiddlwares() { let idx = 0; const next = async () => { if (idx >= Vajra.#middlewares.length) { return setImmediate(handleRoute) } const fn = Vajra.#middlewares[idx]; idx++; try { await fn(req, res, next); } catch (err) { return default_500({ url: req.url, method: req.method }, res, err); } }; await next(); }
74
67
  setImmediate(() => {
75
68
  req.body = {}; req.rawData = ''; req.formData = {}; let dataSize = 0
76
69
  req.on('data', (chunk) => { dataSize += chunk.length; if (dataSize > Vajra.#MAX_FILE_SIZE) { return default_413(res) }; req.rawData+=chunk })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techiev2/vajra",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Blazing-fast, zero-dependency Node.js server with routing, middleware, multipart uploads, and templating. 111 lines · ~95k req/s · ~52 MB idle.",
5
5
  "keywords": [
6
6
  "http-server",
@@ -38,6 +38,6 @@
38
38
  "test": "tests"
39
39
  },
40
40
  "scripts": {
41
- "test": "node --watch tests/tests.js"
41
+ "test": "node tests/tests.js"
42
42
  }
43
43
  }