@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 +1 -1
- package/src/middleware/jwt.js +17 -16
- package/src/middleware/session.js +233 -36
package/package.json
CHANGED
package/src/middleware/jwt.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
8
|
+
import { createRequire } from "module";
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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() {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
-
//
|
|
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",
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
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;
|
|
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
|
-
|
|
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;
|