@aetherframework/middleware 1.0.5 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aetherframework/middleware",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Ultra-performance, framework-agnostic middleware system for Aether Framework",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -61,9 +61,10 @@ function strToBuffer(str) {
61
61
  * @returns {string} Base64 string
62
62
  */
63
63
  function arrayBufferToBase64(buffer) {
64
+ // FIXED: Use ES Module compatible approach
64
65
  if (typeof Buffer !== "undefined") {
65
- // Node.js environment
66
- const Buffer = require("buffer").Buffer;
66
+ // Node.js environment - use global Buffer if available
67
+ // Note: In ES Modules, Buffer is available globally in Node.js
67
68
  return Buffer.from(buffer).toString("base64");
68
69
  } else {
69
70
  // Browser environment
@@ -82,9 +83,9 @@ function arrayBufferToBase64(buffer) {
82
83
  * @returns {Uint8Array} ArrayBuffer representation
83
84
  */
84
85
  function base64ToArrayBuffer(base64) {
86
+ // FIXED: Use ES Module compatible approach
85
87
  if (typeof Buffer !== "undefined") {
86
- // Node.js environment
87
- const Buffer = require("buffer").Buffer;
88
+ // Node.js environment - use global Buffer if available
88
89
  return new Uint8Array(Buffer.from(base64, "base64"));
89
90
  } else {
90
91
  // Browser environment
@@ -200,7 +201,7 @@ async function generateHmacSignature(algorithm, key, data) {
200
201
  const signature = await crypto.subtle.sign(algo.name, cryptoKey, dataBuf);
201
202
  return new Uint8Array(signature);
202
203
  } else if (typeof require !== "undefined") {
203
- // Node.js environment with crypto module
204
+ // Node.js environment with crypto module (CommonJS)
204
205
  const crypto = require("crypto");
205
206
  const hmac = crypto.createHmac(algo.hash.toLowerCase().replace("-", ""), key);
206
207
  hmac.update(data);
@@ -238,7 +239,7 @@ async function verifyHmacSignature(algorithm, key, data, signature) {
238
239
  );
239
240
  return await crypto.subtle.verify(algo.name, cryptoKey, signature, dataBuf);
240
241
  } else if (typeof require !== "undefined") {
241
- // Node.js environment with crypto module
242
+ // Node.js environment with crypto module (CommonJS)
242
243
  const crypto = require("crypto");
243
244
  const hmac = crypto.createHmac(algo.hash.toLowerCase().replace("-", ""), key);
244
245
  hmac.update(data);
@@ -252,7 +253,7 @@ async function verifyHmacSignature(algorithm, key, data, signature) {
252
253
 
253
254
  let result = 0;
254
255
  for (let i = 0; i < expected.length; i++) {
255
- result |= expected[i]^ provided[i];
256
+ result |= expected[i] ^ provided[i];
256
257
  }
257
258
  return result === 0;
258
259
  } else {
@@ -306,7 +307,7 @@ async function generateAsymmetricSignature(algorithm, key, data) {
306
307
  );
307
308
  return new Uint8Array(signature);
308
309
  } else if (typeof require !== "undefined") {
309
- // Node.js environment with crypto module
310
+ // Node.js environment with crypto module (CommonJS)
310
311
  const crypto = require("crypto");
311
312
  const sign = crypto.createSign(algo.hash.replace("SHA-", "RSA-SHA"));
312
313
  sign.update(data);
@@ -369,7 +370,7 @@ async function verifyAsymmetricSignature(algorithm, key, data, signature) {
369
370
  dataBuf
370
371
  );
371
372
  } else if (typeof require !== "undefined") {
372
- // Node.js environment with crypto module
373
+ // Node.js environment with crypto module (CommonJS)
373
374
  const crypto = require("crypto");
374
375
  const verify = crypto.createVerify(algo.hash.replace("SHA-", "RSA-SHA"));
375
376
  verify.update(data);
@@ -485,8 +486,8 @@ async function jwtSign(payload, key, options = {}) {
485
486
  } else if (typeof expiresIn === "string") {
486
487
  const match = expiresIn.match(/^(\d+)([smhd])$/);
487
488
  if (match) {
488
- const value = parseInt(match, 10);
489
- const unit = match;
489
+ const value = parseInt(match[1], 10);
490
+ const unit = match[2];
490
491
  switch (unit) {
491
492
  case "s": seconds = value; break;
492
493
  case "m": seconds = value * 60; break;
@@ -632,9 +633,9 @@ function jwtDecode(token, options = {}) {
632
633
  try {
633
634
  const payload = JSON.parse(base64UrlDecode(payloadEncoded));
634
635
  return options.complete ? {
635
- header: JSON.parse(base64UrlDecode(parts)),
636
+ header: JSON.parse(base64UrlDecode(parts[0])),
636
637
  payload,
637
- signature: parts
638
+ signature: parts[2]
638
639
  } : payload;
639
640
  } catch (e) {
640
641
  throw new Error("Invalid token payload");
@@ -673,7 +674,7 @@ function createJwtMiddleware(options = {}) {
673
674
  publicKey: envConfig.publicKey,
674
675
  algorithms: parseAlgorithms(envConfig.algorithms),
675
676
  algorithm: envConfig.algorithms
676
- ? parseAlgorithms(envConfig.algorithms)
677
+ ? parseAlgorithms(envConfig.algorithms)[0]
677
678
  : "HS256",
678
679
  audience: envConfig.audience,
679
680
  issuer: envConfig.issuer,
@@ -800,7 +801,7 @@ function createJwtMiddleware(options = {}) {
800
801
  throw new Error("Invalid token format");
801
802
  }
802
803
 
803
- const header = JSON.parse(base64UrlDecode(parts));
804
+ const header = JSON.parse(base64UrlDecode(parts[0]));
804
805
  const algorithm = header.alg;
805
806
  const key = getVerificationKey(algorithm);
806
807
 
@@ -900,7 +901,7 @@ createJwtMiddleware.verify = async function (token, options = {}) {
900
901
  throw new Error("Invalid token format");
901
902
  }
902
903
 
903
- const header = JSON.parse(base64UrlDecode(parts));
904
+ const header = JSON.parse(base64UrlDecode(parts[0]));
904
905
  const algorithm = header.alg;
905
906
  const algo = ALGORITHMS[algorithm];
906
907
 
@@ -5,17 +5,29 @@
5
5
  */
6
6
 
7
7
  import crypto from "crypto";
8
- import redis from "redis";
8
+ import { createRequire } from "module";
9
9
 
10
- // [V8-OPT] Zero allocation cookie lookup
10
+ /**
11
+ * [V8-OPT] Zero allocation cookie lookup.
12
+ * Parses the cookie header string manually to avoid the overhead of splitting
13
+ * and creating intermediate arrays/objects, ensuring high performance.
14
+ *
15
+ * @param {string|undefined} cookieHeader - The raw 'Cookie' header from the request.
16
+ * @param {string} name - The name of the cookie to extract.
17
+ * @returns {string|undefined} The value of the cookie, or undefined if not found.
18
+ */
11
19
  function getCookieValue(cookieHeader, name) {
12
20
  if (!cookieHeader) return undefined;
21
+
13
22
  const target = name + "=";
14
23
  const len = cookieHeader.length;
15
24
  let pos = 0;
25
+
16
26
  while (pos < len) {
17
27
  pos = cookieHeader.indexOf(target, pos);
18
28
  if (pos === -1) break;
29
+
30
+ // Ensure we match the exact cookie name (preceded by start of string, space, or semicolon)
19
31
  if (pos === 0 || cookieHeader.charCodeAt(pos - 1) === 32 || cookieHeader.charCodeAt(pos - 1) === 59) {
20
32
  pos += target.length;
21
33
  let end = cookieHeader.indexOf(";", pos);
@@ -24,29 +36,120 @@ function getCookieValue(cookieHeader, name) {
24
36
  }
25
37
  pos += 1;
26
38
  }
39
+
27
40
  return undefined;
28
41
  }
29
42
 
43
+ /**
44
+ * Generates a cryptographically secure random session ID.
45
+ * @returns {string} A 32-character hex string.
46
+ */
30
47
  const genId = () => crypto.randomBytes(16).toString("hex");
31
48
 
32
- // ... (MemoryStore and RedisStore remain exactly the same as your original code) ...
49
+ /**
50
+ * In-memory session store.
51
+ * Used as the default fallback when Redis is disabled or unavailable.
52
+ */
33
53
  class MemoryStore {
34
- constructor() { this.cache = new Map(); }
35
- async get(id) { const s = this.cache.get(id); if (!s) return null; if (Date.now() > s.exp) { this.cache.delete(id); return null; } return s.data; }
36
- async set(id, data, ttl) { this.cache.set(id, { data, exp: Date.now() + ttl }); }
37
- async delete(id) { this.cache.delete(id); }
38
- prune() { const now = Date.now(); for (const [k, v] of this.cache) if (now > v.exp) this.cache.delete(k); }
54
+ constructor() {
55
+ /** @type {Map<string, {data: any, exp: number}>} */
56
+ this.cache = new Map();
57
+ }
58
+
59
+ /**
60
+ * Retrieves session data if it exists and hasn't expired.
61
+ * @param {string} id - Session ID.
62
+ * @returns {Promise<any|null>} Session data or null.
63
+ */
64
+ async get(id) {
65
+ const s = this.cache.get(id);
66
+ if (!s) return null;
67
+ if (Date.now() > s.exp) {
68
+ this.cache.delete(id);
69
+ return null;
70
+ }
71
+ return s.data;
72
+ }
73
+
74
+ /**
75
+ * Saves session data with an expiration timestamp.
76
+ * @param {string} id - Session ID.
77
+ * @param {any} data - Session payload.
78
+ * @param {number} ttl - Time to live in milliseconds.
79
+ */
80
+ async set(id, data, ttl) {
81
+ this.cache.set(id, { data, exp: Date.now() + ttl });
82
+ }
83
+
84
+ /**
85
+ * Deletes a specific session.
86
+ * @param {string} id - Session ID.
87
+ */
88
+ async delete(id) {
89
+ this.cache.delete(id);
90
+ }
91
+
92
+ /**
93
+ * Cleans up expired sessions to prevent memory leaks.
94
+ */
95
+ prune() {
96
+ const now = Date.now();
97
+ for (const [k, v] of this.cache) {
98
+ if (now > v.exp) this.cache.delete(k);
99
+ }
100
+ }
39
101
  }
40
102
 
103
+ /**
104
+ * Redis-backed session store for distributed/production environments.
105
+ */
41
106
  class RedisStore {
42
- constructor(client) { this.client = client; this.prefix = "sess:"; }
43
- async get(id) { try { const data = await this.client.get(`${this.prefix}${id}`); return data ? JSON.parse(data) : null; } catch (err) { return null; } }
44
- async set(id, data, ttl) { try { await this.client.setEx(`${this.prefix}${id}`, Math.floor(ttl / 1000), JSON.stringify(data)); } catch (err) {} }
45
- async delete(id) { try { await this.client.del(`${this.prefix}${id}`); } catch (err) {} }
107
+ /**
108
+ * @param {import('redis').RedisClientType} client - Connected Redis client instance.
109
+ */
110
+ constructor(client) {
111
+ this.client = client;
112
+ this.prefix = "sess:";
113
+ }
114
+
115
+ async get(id) {
116
+ try {
117
+ const data = await this.client.get(`${this.prefix}${id}`);
118
+ return data ? JSON.parse(data) : null;
119
+ } catch (err) {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ async set(id, data, ttl) {
125
+ try {
126
+ // Redis SETEX expects TTL in seconds
127
+ await this.client.setEx(`${this.prefix}${id}`, Math.floor(ttl / 1000), JSON.stringify(data));
128
+ } catch (err) {
129
+ console.error("[RedisStore] Set error:", err);
130
+ }
131
+ }
132
+
133
+ async delete(id) {
134
+ try {
135
+ await this.client.del(`${this.prefix}${id}`);
136
+ } catch (err) {
137
+ console.error("[RedisStore] Delete error:", err);
138
+ }
139
+ }
140
+
141
+ // Redis handles expiration natively via TTL, so manual pruning is not needed.
46
142
  prune() {}
47
143
  }
48
144
 
49
- // [V8-OPT] Safe boolean parser
145
+ /**
146
+ * [V8-OPT] Safe boolean parser for environment variables.
147
+ * Handles various truthy/falsy string representations safely.
148
+ *
149
+ * @param {string} key - Environment variable name.
150
+ * @param {boolean} defaultValue - Fallback value if undefined.
151
+ * @returns {boolean} Parsed boolean value.
152
+ */
50
153
  function isEnvEnabled(key, defaultValue = false) {
51
154
  const val = process.env[key];
52
155
  if (val === undefined || val === null) return defaultValue;
@@ -55,14 +158,49 @@ function isEnvEnabled(key, defaultValue = false) {
55
158
  return ['true', '1', 'yes', 'on'].includes(val.toLowerCase().trim());
56
159
  }
57
160
 
161
+ /**
162
+ * Safely attempts to load the 'redis' package synchronously.
163
+ * Prevents the application from crashing if the package is not installed
164
+ * and Redis is not being used.
165
+ *
166
+ * @returns {typeof import('redis') | null} The redis module or null if unavailable.
167
+ */
168
+ function loadRedisModule() {
169
+ try {
170
+ // Attempt to get import.meta.url safely to avoid SyntaxError in pure CommonJS environments
171
+ // @ts-ignore
172
+ const metaUrl = typeof import.meta !== "undefined" && import.meta !== null ? import.meta.url : undefined;
173
+
174
+ if (metaUrl) {
175
+ // Node.js ESM environment: use createRequire to load CommonJS modules synchronously
176
+ const require = createRequire(metaUrl);
177
+ return require("redis");
178
+ } else {
179
+ // Node.js CommonJS environment: use global require
180
+ // @ts-ignore
181
+ return typeof require === "function" ? require("redis") : null;
182
+ }
183
+ } catch (err) {
184
+ // Module not found or environment restriction
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Core Session Manager.
191
+ * Handles configuration, store initialization, and middleware generation.
192
+ */
58
193
  export class SessionManager {
194
+ /**
195
+ * @param {object} options - Configuration overrides.
196
+ */
59
197
  constructor(options = {}) {
60
198
  const { store, ...restOptions } = options;
61
199
 
62
200
  this.config = {
63
- // [FIX] Use strict boolean parsing
201
+ // [FIX] Use strict boolean parsing for environment variables
64
202
  enabled: isEnvEnabled('SESSION_ENABLED', false),
65
- maxAge: parseInt(process.env.SESSION_MAX_AGE) || 86400000,
203
+ maxAge: parseInt(process.env.SESSION_MAX_AGE) || 86400000, // Default: 24 hours
66
204
  cookieName: process.env.SESSION_COOKIE_NAME || "aether_sid",
67
205
  cookieDomain: process.env.SESSION_COOKIE_DOMAIN || null,
68
206
  cookiePath: process.env.SESSION_COOKIE_PATH || "/",
@@ -75,37 +213,54 @@ export class SessionManager {
75
213
  const redisHost = process.env.REDIS_HOST;
76
214
  const redisPort = process.env.REDIS_PORT;
77
215
 
216
+ // Initialize Store based on environment and configuration
78
217
  if (redisEnabled && redisHost && redisPort) {
79
- try {
80
- const redisClient = redis.createClient({
81
- socket: { host: redisHost, port: parseInt(redisPort) },
82
- database: parseInt(process.env.REDIS_DB) || 0,
83
- password: process.env.REDIS_PASSWORD || undefined,
84
- });
85
- redisClient.connect().catch((err) => console.error("[SessionManager] Redis connect error:", err));
86
- this.config.store = new RedisStore(redisClient);
87
- } catch (err) {
218
+ // Lazily load the redis package only if Redis is actually enabled
219
+ const redis = loadRedisModule();
220
+
221
+ if (!redis) {
222
+ console.warn("[SessionManager] REDIS_ENABLED is true, but the 'redis' package is not installed. Falling back to MemoryStore.");
88
223
  this.config.store = store || new MemoryStore();
224
+ } else {
225
+ try {
226
+ const redisClient = redis.createClient({
227
+ socket: { host: redisHost, port: parseInt(redisPort) },
228
+ database: parseInt(process.env.REDIS_DB) || 0,
229
+ password: process.env.REDIS_PASSWORD || undefined,
230
+ });
231
+
232
+ // Connect asynchronously; errors are caught and logged
233
+ redisClient.connect().catch((err) => console.error("[SessionManager] Redis connect error:", err));
234
+ this.config.store = new RedisStore(redisClient);
235
+ } catch (err) {
236
+ console.error("[SessionManager] Failed to initialize Redis client:", err);
237
+ this.config.store = store || new MemoryStore();
238
+ }
89
239
  }
90
240
  } else {
241
+ // Redis disabled or missing config, use provided store or default to Memory
91
242
  this.config.store = store || new MemoryStore();
92
243
  }
93
244
 
245
+ // Set up periodic cleanup for MemoryStore to prevent memory leaks
94
246
  if (this.config.store instanceof MemoryStore) {
95
247
  this.cleanup = setInterval(() => this.config.store.prune(), 60000).unref();
96
248
  }
97
249
  }
98
250
 
251
+ /**
252
+ * Generates the session middleware function.
253
+ * @returns {function} Express/Koa/Aether compatible middleware function.
254
+ */
99
255
  middleware() {
100
256
  const { enabled, store, maxAge, cookieName, cookieDomain, cookiePath, cookieSecure, cookieSameSite } = this.config;
101
257
 
102
- // [FIX] If disabled, actively clear the browser's residual cookie and bypass
258
+ // [FIX] If sessions are disabled, actively clear any residual browser cookies and bypass
103
259
  if (!enabled) {
104
260
  return async (ctx, next) => {
105
- // Check if browser sent the residual cookie
106
261
  const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
107
262
  if (sid) {
108
- // Actively destroy the cookie in the browser by setting Max-Age=0
263
+ // Destroy the cookie in the browser by setting Max-Age=0
109
264
  ctx.setHeader(
110
265
  "Set-Cookie",
111
266
  `${cookieName}=; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=${cookieSameSite}; Max-Age=0; Path=${cookiePath}${cookieDomain ? `; Domain=${cookieDomain}` : ""}`
@@ -115,44 +270,80 @@ export class SessionManager {
115
270
  };
116
271
  }
117
272
 
273
+ // Pre-compute the static parts of the Set-Cookie header for performance
118
274
  const cookieSuffixParts = [
119
- "HttpOnly", cookieSecure ? "Secure" : "", `SameSite=${cookieSameSite}`,
120
- `Max-Age=${Math.floor(maxAge / 1000)}`, `Path=${cookiePath}`, cookieDomain ? `Domain=${cookieDomain}` : ""
275
+ "HttpOnly",
276
+ cookieSecure ? "Secure" : "",
277
+ `SameSite=${cookieSameSite}`,
278
+ `Max-Age=${Math.floor(maxAge / 1000)}`,
279
+ `Path=${cookiePath}`,
280
+ cookieDomain ? `Domain=${cookieDomain}` : ""
121
281
  ].filter(Boolean).join("; ");
122
282
 
123
283
  return async (ctx, next) => {
124
284
  ctx.state ??= {};
285
+
286
+ // 1. Extract Session ID from cookie
125
287
  const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
288
+
289
+ // 2. Load existing session data from store
126
290
  let sessionData = sid ? await store.get(sid) : null;
127
291
  let isNew = false;
128
- if (!sessionData) { sessionData = {}; isNew = true; }
292
+
293
+ if (!sessionData) {
294
+ sessionData = {};
295
+ isNew = true;
296
+ }
129
297
 
298
+ // 3. Create internal state tracker
130
299
  const sessionState = { id: sid, data: sessionData, dirty: false };
131
300
 
301
+ // 4. Expose public API to the context
132
302
  ctx.session = {
133
303
  get: (key) => sessionState.data[key],
134
- set: (key, val) => { sessionState.data[key] = val; sessionState.dirty = true; },
135
- delete: (key) => { delete sessionState.data[key]; sessionState.dirty = true; },
136
- clear: () => { sessionState.data = {}; sessionState.dirty = true; },
304
+
305
+ set: (key, val) => {
306
+ sessionState.data[key] = val;
307
+ sessionState.dirty = true;
308
+ },
309
+
310
+ delete: (key) => {
311
+ delete sessionState.data[key];
312
+ sessionState.dirty = true;
313
+ },
314
+
315
+ clear: () => {
316
+ sessionState.data = {};
317
+ sessionState.dirty = true;
318
+ },
319
+
137
320
  destroy: async () => {
138
321
  if (sessionState.id) await store.delete(sessionState.id);
139
- sessionState.id = null; sessionState.data = {}; sessionState.dirty = false;
322
+ sessionState.id = null;
323
+ sessionState.data = {};
324
+ sessionState.dirty = false;
325
+ // Clear browser cookie
140
326
  ctx.setHeader("Set-Cookie", `${cookieName}=; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=${cookieSameSite}; Max-Age=0; Path=${cookiePath}${cookieDomain ? `; Domain=${cookieDomain}` : ""}`);
141
327
  },
328
+
142
329
  regenerate: async () => {
143
330
  if (sessionState.id) await store.delete(sessionState.id);
144
331
  sessionState.id = genId();
145
332
  await store.set(sessionState.id, sessionState.data, maxAge);
146
333
  ctx.setHeader("Set-Cookie", `${cookieName}=${sessionState.id}; ${cookieSuffixParts}`);
147
- sessionState.dirty = false; isNew = false;
334
+ sessionState.dirty = false;
335
+ isNew = false;
148
336
  },
337
+
149
338
  getId: () => sessionState.id,
150
339
  getAllData: () => ({ ...sessionState.data }),
151
340
  };
152
341
 
342
+ // 5. Execute downstream middleware and handle persistence
153
343
  try {
154
344
  if (typeof next === "function") await next();
155
345
  } finally {
346
+ // Only save to store if data was modified
156
347
  if (sessionState.dirty) {
157
348
  if (isNew || !sessionState.id) {
158
349
  sessionState.id = genId();
@@ -164,7 +355,13 @@ export class SessionManager {
164
355
  };
165
356
  }
166
357
 
167
- destroy() { if (this.cleanup) clearInterval(this.cleanup); }
358
+ /**
359
+ * Cleans up resources (e.g., stops the MemoryStore pruning interval).
360
+ * Should be called when shutting down the application.
361
+ */
362
+ destroy() {
363
+ if (this.cleanup) clearInterval(this.cleanup);
364
+ }
168
365
  }
169
366
 
170
367
  export default SessionManager;