@techiev2/vajra 1.3.1 → 1.4.1

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
@@ -14,15 +14,38 @@ Vajra draws from the Rigvedic thunderbolt weapon of Indra — crafted from the b
14
14
  Like the Vajra, this server delivers maximum power in minimal form.
15
15
 
16
16
 
17
- [![npm version](https://img.shields.io/npm/v/vajra.svg?style=flat-square)](https://www.npmjs.com/package/vajra)
18
- [![npm downloads](https://img.shields.io/npm/dm/vajra.svg?style=flat-square)](https://www.npmjs.com/package/vajra)
19
- [![Node.js version](https://img.shields.io/node/v/vajra.svg?style=flat-square)](https://nodejs.org)
20
- [![License](https://img.shields.io/npm/l/vajra.svg?style=flat-square)](LICENSE)
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.1 (Current)
26
+ - Added support to handle drift in system time after signing
27
+
28
+ ### 1.4.0 (2025-12-31)
29
+ - Added full HS256 JWT support (`@techiev2/vajra/libs/auth/jwt.js`)
30
+ - Ultra-minimal, zero-dependency implementation
31
+ - Key and header caching for maximum performance
32
+ - Robust base64url handling
33
+ - Numeric exp validation and expiration checks
34
+
35
+ ### 1.3.0 (2025-12-30)
36
+ - Performance improvements to routing in bare routes
37
+
38
+ ### 1.2.0 (2025-12-30)
39
+ - Adds cookie support
40
+
41
+ ### 1.0.0 (2025-12-25)
42
+ - Initial release
21
43
 
22
44
  ## Features
23
45
 
24
46
  - Zero external dependencies
25
47
  - Built-in routing with named parameters (`:id`)
48
+ - Asynchronous batched logging for performance
26
49
  - Global middleware support with `next()` chaining
27
50
  - JSON, urlencoded, and **multipart/form-data** body parsing
28
51
  - Fast HTML templating with loops, nested objects, and simple array headers
@@ -59,14 +82,17 @@ async function getUsers(query = {}) {
59
82
  return (await fetch(`https://jsonplaceholder.typicode.com/users?${encode(query)}`)).json()
60
83
  }
61
84
 
62
- const { get, post, use, start, setProperty } = Vajra.create();
85
+ const { get, post, use, start, setProperty, log } = Vajra.create();
63
86
 
64
87
  setProperty({ viewsRoot: `${import.meta.url}/views` })
65
88
  // Or as a key-value pair
66
89
  // setProperty('viewsRoot', `${import.meta.url}/views`)
67
90
 
68
91
  use((req, res, next) => {
69
- console.log(`${req.method} ${req.url}`);
92
+ // Vajra provides an async batched logger to provide a balance between 100% log coverage and performance.
93
+ // If you prefer blocking immediate logs, you can switch to console.log
94
+ // or any other library of your choice.
95
+ log(`${req.method} ${req.url}`);
70
96
  next();
71
97
  });
72
98
 
@@ -156,6 +182,7 @@ app.setProperty('viewRoot', './views');
156
182
  - `use(middleware)`
157
183
  - `start({ port, host }, callback?)`
158
184
  - `setProperty(key, value)` or `setProperty({ key: value })`
185
+ - `log(message)`
159
186
 
160
187
 
161
188
  #### Response helpers:
package/README.md CHANGED
@@ -19,6 +19,28 @@ Like the Vajra, this server delivers maximum power in minimal form.
19
19
  [![Node.js version](https://img.shields.io/node/v/@techiev2/vajra.svg?style=flat-square)](https://nodejs.org)
20
20
  [![License](https://img.shields.io/npm/l/@techiev2/vajra.svg?style=flat-square)](LICENSE)
21
21
 
22
+
23
+ ## Changelog
24
+
25
+ ### 1.4.1 (Current)
26
+ - Added support to handle drift in system time after signing
27
+
28
+ ### 1.4.0 (2025-12-31)
29
+ - Added full HS256 JWT support (`@techiev2/vajra/libs/auth/jwt.js`)
30
+ - Ultra-minimal, zero-dependency implementation
31
+ - Key and header caching for maximum performance
32
+ - Robust base64url handling
33
+ - Numeric exp validation and expiration checks
34
+
35
+ ### 1.3.0 (2025-12-30)
36
+ - Performance improvements to routing in bare routes
37
+
38
+ ### 1.2.0 (2025-12-30)
39
+ - Adds cookie support
40
+
41
+ ### 1.0.0 (2025-12-25)
42
+ - Initial release
43
+
22
44
  ## Features
23
45
 
24
46
  - Zero external dependencies
@@ -0,0 +1,25 @@
1
+ import { subtle } from 'node:crypto';const hasBuffer = typeof Buffer !== 'undefined';
2
+ const DATA = { encoder: new TextEncoder(), decoder: new TextDecoder(), keys: {}, headers: { sign: { alg: 'HS256', typ: 'JWT' }, verify: { name: 'HMAC', hash: 'SHA-256' } } };
3
+ const ENCODED_HEADERS = {}
4
+ function encode(buf) { if (hasBuffer) return Buffer.from(buf).toString('base64url'); let s = ''; for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]); return btoa(s).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); }
5
+ function decode(str) { if (hasBuffer) return Buffer.from(str, 'base64url'); str = str.replace(/-/g,'+').replace(/_/g,'/'); while (str.length % 4) str += '='; const bin = atob(str); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out;}
6
+ async function populateKeyCache(secret) {
7
+ let { sign: signKey, verify: verifyKey } = DATA.keys[`${secret}`] || {};
8
+ if (!signKey || !verifyKey) { [signKey, verifyKey] = await Promise.all([subtle.importKey('raw', DATA.encoder.encode(secret), DATA.headers.verify, false, ['sign']), subtle.importKey('raw', DATA.encoder.encode(secret), DATA.headers.verify, false, ['verify'])]); Object.assign(DATA.keys, { [secret]: { sign: signKey, verify: verifyKey } }) }
9
+ return { signKey, verifyKey }
10
+ }
11
+ export async function sign(payload, secret, options = {}) {
12
+ if (!payload || typeof payload !== 'object') throw new Error("Payload to sign must be an object"); if (typeof secret !== 'string') throw new Error("Secret must be a string")
13
+ const { signKey } = await populateKeyCache(secret); const { alg = 'HS256' } = options;
14
+ ENCODED_HEADERS[alg] = ENCODED_HEADERS[alg] || DATA.encoder.encode({ ...DATA.headers.sign, alg }); const encodedPayload = encode(DATA.encoder.encode(JSON.stringify({ ...payload, _gen_ts: Math.floor(Date.now() / 1000) })))
15
+ const encodedSignature = encode(await subtle.sign('HMAC', signKey, DATA.encoder.encode(`${ENCODED_HEADERS[alg]}.${encodedPayload}`))); return `${ENCODED_HEADERS[alg]}.${encodedPayload}.${encodedSignature}`;
16
+ }
17
+ export async function verify(token, secret) {
18
+ if (!token || typeof token != 'string') throw new Error("Token must be a string"); if (!secret || typeof secret !== "string") throw new Error("Secret must be a string")
19
+ const [encodedHeader, encodedPayload, encodedSignature, ..._] = token.split('.'); if (!encodedPayload || !encodedSignature || _.length) throw new Error('Invalid token format');
20
+ const { verifyKey } = await populateKeyCache(secret); const valid = await subtle.verify('HMAC', verifyKey, decode(encodedSignature), DATA.encoder.encode(`${encodedHeader}.${encodedPayload}`));
21
+ if (!valid) throw new Error('Invalid signature'); const payload = JSON.parse(DATA.decoder.decode(decode(encodedPayload)));
22
+ const genTs = payload._gen_ts; const now = Math.floor(Date.now() / 1000); const maxBackwardDrift = 300;
23
+ if (typeof genTs === 'number' && (now + maxBackwardDrift < genTs)) { throw new Error('System clock appears to have moved backward — token rejected'); }
24
+ delete payload._gen_ts; if (payload.exp && isNaN(+payload.exp)) throw new Error("Expiry must be numeric."); if (payload.exp && Date.now() >= payload.exp * 1000) { throw new Error('Token expired'); } return payload;
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techiev2/vajra",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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",
package/tests/tests.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import assert from 'node:assert';
2
2
  import { encode } from 'node:querystring';
3
+ import { afterEach, beforeEach, suite, test } from 'node:test';
4
+ import { randomBytes, randomUUID } from 'node:crypto';
5
+
3
6
  import pkg from '../package.json' with {type: 'json'}
4
- import { suite, test } from 'node:test';
7
+ import { sign, verify } from '../libs/auth/jwt.js';
5
8
 
6
9
  async function getResponse(url, method = 'GET', body) {
7
10
  return (await fetch(url, !!body ?
@@ -14,6 +17,7 @@ async function getJSON(url, method = 'GET', body) {
14
17
  return (await getResponse(url, method, body)).json()
15
18
  }
16
19
 
20
+
17
21
  suite('Test HTTP API at port 4002', () => {
18
22
  const BASE_URL = 'http://localhost:4002'
19
23
  suite('Test HTTP GET', () => {
@@ -59,3 +63,240 @@ suite('Test HTTP API at port 4002', () => {
59
63
  })
60
64
  })
61
65
  })
66
+
67
+ suite('Tests for library functions', () => {
68
+ const encoder = new TextEncoder(); const signHeaders = { alg: 'HS256', typ: 'JWT' }; let timeOffsetMs = 0;
69
+ // const originalDateNow = Date.now;
70
+ // Date.now = function () { return originalDateNow() + timeOffsetMs; };
71
+ function setupMockTimer(timing) {
72
+ const originalNow = globalThis.Date.now
73
+ let timeDelta = timing
74
+ globalThis.Date.now = () => { return originalNow() + (timeDelta || 0) }
75
+ }
76
+ async function withMockTimer(timeDelta, fn) {
77
+ setupMockTimer(timeDelta)
78
+ await fn()
79
+ }
80
+ test('Verify that JWT helper returns the right token', async () => {
81
+ const secret = randomBytes(16).toString('hex')
82
+ const data = {
83
+ now: new Date().getTime(),
84
+ id: randomUUID()
85
+ }
86
+ const token = await sign(data, secret)
87
+ assert.strictEqual(!!token, true)
88
+ const verified = await verify(token, secret)
89
+ assert.strict.deepEqual(data, verified)
90
+ })
91
+
92
+ test('Verify that JWT helper throws an expired token error with exp set to gen time', async () => {
93
+ const secret = randomBytes(16).toString('hex')
94
+ let exp = Date.now() / 1000
95
+ const data = {
96
+ now: new Date().getTime(),
97
+ exp,
98
+ id: randomUUID()
99
+ }
100
+ const token = await sign(data, secret)
101
+ assert.strictEqual(!!token, true)
102
+ assert.rejects(verify.bind(null, token, secret), Error);
103
+ })
104
+
105
+ test('rejects malformed token formats', async () => {
106
+ const secret = 's';
107
+ await assert.rejects(verify('', secret),);
108
+ await assert.rejects(verify('a', secret),);
109
+ await assert.rejects(verify('a.b', secret),);
110
+ await assert.rejects(verify('a.b.c.d', secret),);
111
+ await assert.rejects(verify('a..c', secret),);
112
+ await assert.rejects(verify('a.b.', secret),);
113
+ });
114
+
115
+ test('rejects tampered signature', async () => {
116
+ const token = await sign({a:1}, 'secret');
117
+ const tampered = token.slice(0, -1) + (token.at(-1) === 'A' ? 'B' : 'A');
118
+ await assert.rejects(verify(tampered, 'secret'), /Invalid signature/);
119
+ });
120
+
121
+ test('rejects tampered payload', async () => {
122
+ const token = await sign({a:1}, 'secret');
123
+ const parts = token.split('.');
124
+ parts[1] = parts[1].slice(0, -1) + 'X';
125
+ await assert.rejects(verify(parts.join('.'), 'secret'), /Invalid signature/);
126
+ });
127
+
128
+ test('allows token without exp claim', async () => {
129
+ const secret = 's';
130
+ const payload = { sub: '123', iat: Math.floor(Date.now() / 1000) };
131
+ const token = await sign(payload, secret);
132
+ const verified = await verify(token, secret);
133
+ assert.deepStrictEqual(verified, payload);
134
+ });
135
+
136
+ test('rejects token with future exp after clock advances', async () => {
137
+ const secret = 's';
138
+ const past = Math.floor(Date.now() / 1000) - 3600;
139
+ const payload = { exp: past };
140
+ const token = await sign(payload, secret);
141
+ await assert.rejects(verify(token, secret), /Token expired/);
142
+ });
143
+
144
+ test('handles large payload without truncation', async () => {
145
+ const secret = 's';
146
+ const large = { data: 'x'.repeat(5000), arr: Array(100).fill(42) };
147
+ const token = await sign(large, secret);
148
+ const verified = await verify(token, secret);
149
+ assert.deepStrictEqual(verified, large);
150
+ });
151
+
152
+ test('rejects invalid JSON in payload segment', async () => {
153
+ const header = encode(encoder.encode(JSON.stringify(signHeaders)));
154
+ const badPayload = encode(encoder.encode('{"broken'));
155
+ const fakeSig = 'AAAA';
156
+ const badToken = `${header}.${badPayload}.${fakeSig}`;
157
+ await assert.rejects(verify(badToken, 'secret'), /Invalid signature/);
158
+ });
159
+
160
+ test('rejects non-object payload on sign', async () => {
161
+ await assert.rejects(sign('string', 'secret'));
162
+ await assert.rejects(sign(null, 'secret'));
163
+ await assert.rejects(sign(123, 'secret'));
164
+ });
165
+
166
+ // Add to existing suite
167
+ test('rejects non-numeric exp claim', async () => {
168
+ const secret = 's';
169
+ const payload = { exp: 'invalid', sub: '123' };
170
+ const token = await sign(payload, secret);
171
+ await assert.rejects(verify(token, secret), /Expiry must be numeric/);
172
+ });
173
+
174
+ test('handles clock skew edge case', async () => {
175
+ const secret = 's';
176
+ const now = Math.floor(Date.now() / 1000);
177
+ const payload = { exp: now + 1 }; // Expires in 1s
178
+ const token = await sign(payload, secret);
179
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
180
+ await assert.rejects(verify(token, secret), /Token expired/);
181
+ });
182
+
183
+ test('rejects malformed base64url segments', async () => {
184
+ const secret = 's';
185
+ const header = encode(encoder.encode(JSON.stringify(signHeaders)));
186
+ const validPayload = encode(encoder.encode('{"sub":"123"}'));
187
+ const badSegment = '!!invalid@@';
188
+ await assert.rejects(verify(`${header}.${badSegment}.AAAA`, secret), /Invalid signature/);
189
+ await assert.rejects(verify(`${header}.${validPayload}.!!invalid@@`, secret), /Invalid signature/);
190
+ });
191
+
192
+ test('handles empty payload object', async () => {
193
+ const secret = 's';
194
+ const payload = {};
195
+ const token = await sign(payload, secret);
196
+ const verified = await verify(token, secret);
197
+ assert.deepStrictEqual(verified, payload);
198
+ });
199
+
200
+ test('rejects very weak secret', async () => {
201
+ const secret = 'a'; // Short but still works (crypto allows it)
202
+ const payload = { sub: '123' };
203
+ const token = await sign(payload, secret);
204
+ const verified = await verify(token, secret);
205
+ assert.deepStrictEqual(verified, payload);
206
+ });
207
+
208
+ test('clock drift: expired token should NOT become valid when clock moves backward', async () => {
209
+ const secret = randomBytes(16).toString('hex');
210
+ const issueTime = Date.now();
211
+ const payload = { sub: 'test-user', exp: Math.floor((issueTime + 5000) / 1000) }; // Set expiry of 5 seconds
212
+ const token = await sign(payload, secret);
213
+ setupMockTimer(10000)
214
+ await assert.rejects(verify(token, secret), /Token expired/);
215
+ setupMockTimer(-3000000)
216
+ await assert.rejects(verify(token, secret), /System clock appears to have moved backward/);
217
+ });
218
+
219
+ test('clock drift: small backward drift does not falsely reject', async () => {
220
+ const secret = randomBytes(16).toString('hex');
221
+ const issueTime = Date.now();
222
+ const payload = { sub: 'test', exp: Math.floor((issueTime + 3600000) / 1000) };
223
+ const token = await sign(payload, secret);
224
+ setupMockTimer(-120000)
225
+ const verified = await verify(token, secret);
226
+ assert.strictEqual(verified.sub, 'test');
227
+ });
228
+
229
+ test('clock drift: small backward drift within leeway should be tolerated', async () => {
230
+ const secret = randomBytes(16).toString('hex');
231
+ const issueTime = Date.now();
232
+ const payload = { exp: Math.floor((issueTime + 5000) / 1000) };
233
+ const token = await sign(payload, secret);
234
+ setupMockTimer(-30000)
235
+ assert.deepStrictEqual(await verify(token, secret), payload);
236
+ });
237
+
238
+ test('clock drift: large backward jump should reject even non-expired token', async () => {
239
+ const secret = randomBytes(16).toString('hex');
240
+ const issueTime = Date.now();
241
+ const payload = { sub: 'drift-test', exp: Math.floor((issueTime + 3600000) / 1000) };
242
+ const token = await sign(payload, secret);
243
+ setupMockTimer(1000)
244
+ const verified = await verify(token, secret);
245
+ assert.strictEqual(verified.sub, 'drift-test');
246
+ setupMockTimer(-8640000);
247
+ await assert.rejects(
248
+ verify(token, secret),
249
+ /System clock appears to have moved backward — token rejected/
250
+ );
251
+ });
252
+
253
+ test('clock drift: forward jump should not falsely expire valid token', async () => {
254
+ const secret = randomBytes(16).toString('hex');
255
+ const issueTime = Date.now();
256
+ const payload = { exp: Math.floor((issueTime + 3600000) / 1000) }; // 1 hour valid
257
+ const token = await sign(payload, secret);
258
+ // Jump forward 30 minutes — should still be valid
259
+ setupMockTimer(1800000)
260
+ const verified = await verify(token, secret);
261
+ assert.ok(verified);
262
+ });
263
+
264
+ test('clock drift: large rollback rejects all tokens regardless of individual exp', async () => {
265
+ const secret = randomBytes(16).toString('hex');
266
+ const baseTime = Date.now();
267
+ const payloads = [
268
+ { id: 'expired', exp: Math.floor((baseTime + 5000) / 1000) },
269
+ { id: 'valid', exp: Math.floor((baseTime + 3600000) / 1000) }
270
+ ];
271
+ const [expiredToken, validToken] = await Promise.all([
272
+ sign(payloads[0], secret),
273
+ sign(payloads[1], secret)
274
+ ]);
275
+ await withMockTimer(-720000, async () => {
276
+ await Promise.all([
277
+ assert.rejects(verify(expiredToken, secret), /System clock appears to have moved backward/),
278
+ assert.rejects(verify(validToken, secret), /System clock appears to have moved backward/)
279
+ ])
280
+ })
281
+ })
282
+
283
+ test('clock drift: large rollback rejects all tokens regardless of individual exp', async () => {
284
+ const secret = randomBytes(16).toString('hex');
285
+ const baseTime = Date.now();
286
+ const payloads = [
287
+ { id: 'expired', exp: Math.floor((baseTime + 5000) / 1000) },
288
+ { id: 'valid', exp: Math.floor((baseTime + 3600000) / 1000) }
289
+ ];
290
+ const [expiredToken, validToken] = await Promise.all([
291
+ sign(payloads[0], secret),
292
+ sign(payloads[1], secret)
293
+ ]);
294
+ await withMockTimer(-1000000, async () => {
295
+ await Promise.all([
296
+ assert.rejects(verify(expiredToken, secret), /System clock appears to have moved backward/),
297
+ assert.rejects(verify(validToken, secret), /System clock appears to have moved backward/)
298
+ ])
299
+ })
300
+ });
301
+
302
+ })
package/video.mp4 DELETED
Binary file