@aetherframework/middleware 1.0.2

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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/middleware/session
6
+ */
7
+
8
+ import crypto from "crypto";
9
+
10
+ //Ultimate optimization: Use the most primitive, zero object allocation for-loop for single-point cookie lookup, reducing GC overhead to absolute zero
11
+ function getCookieValue(cookieHeader, name) {
12
+ if (!cookieHeader) return undefined;
13
+ const target = name + "=";
14
+ const len = cookieHeader.length;
15
+ let pos = 0;
16
+ while (pos < len) {
17
+ pos = cookieHeader.indexOf(target, pos);
18
+ if (pos === -1) break;
19
+ // Ensure it's an independent cookie name, not a prefix of another
20
+ if (
21
+ pos === 0 ||
22
+ cookieHeader.charCodeAt(pos - 1) === 32 ||
23
+ cookieHeader.charCodeAt(pos - 1) === 59
24
+ ) {
25
+ pos += target.length;
26
+ let end = cookieHeader.indexOf(";", pos);
27
+ if (end === -1) end = len;
28
+ return cookieHeader.substring(pos, end).trim();
29
+ }
30
+ pos += 1;
31
+ }
32
+ return undefined;
33
+ }
34
+
35
+ //Abandon long UUID, use 16-byte high-performance hexadecimal ID, shorten cookie length, reduce network and compression middleware overhead
36
+ const genId = () => crypto.randomBytes(16).toString("hex");
37
+
38
+ class MemoryStore {
39
+ constructor() {
40
+ this.cache = new Map();
41
+ }
42
+ async get(id) {
43
+ const s = this.cache.get(id);
44
+ if (!s) return null;
45
+ if (Date.now() > s.exp) {
46
+ this.cache.delete(id);
47
+ return null;
48
+ }
49
+ return s.data;
50
+ }
51
+ async set(id, data, ttl) {
52
+ this.cache.set(id, { data, exp: Date.now() + ttl });
53
+ }
54
+ async delete(id) {
55
+ this.cache.delete(id);
56
+ }
57
+ prune() {
58
+ const now = Date.now();
59
+ for (const [k, v] of this.cache) if (now > v.exp) this.cache.delete(k);
60
+ }
61
+ }
62
+
63
+ export class SessionManager {
64
+ constructor(options = {}) {
65
+ const { store, ...restOptions } = options;
66
+ this.config = {
67
+ enabled: process.env.SESSION_ENABLED !== "false",
68
+ maxAge: parseInt(process.env.SESSION_MAX_AGE) || 86400000,
69
+ cookieName: process.env.SESSION_COOKIE_NAME || "aether_sid",
70
+ ...restOptions,
71
+ };
72
+ this.config.store =
73
+ store && typeof store === "object" ? store : new MemoryStore();
74
+ if (this.config.store instanceof MemoryStore) {
75
+ this.cleanup = setInterval(
76
+ () => this.config.store.prune(),
77
+ 60000,
78
+ ).unref();
79
+ }
80
+ }
81
+
82
+ middleware() {
83
+ if (!this.config.enabled)
84
+ return (ctx, next) => next && (next.next ? next.next() : next());
85
+
86
+ const { store, maxAge, cookieName } = this.config;
87
+ const cookieSuffix = `; HttpOnly; Secure; SameSite=Strict; Max-Age=${Math.floor(maxAge / 1000)}; Path=/`;
88
+
89
+ return async (ctx, next) => {
90
+ ctx.state ??= {};
91
+
92
+ const sid = getCookieValue(ctx.getHeader("cookie"), cookieName);
93
+ let sessionData = sid ? await store.get(sid) : null;
94
+
95
+ // 🚀 Strategy adjustment: If not obtained, first give an empty object, never write to storage early, never set cookie early
96
+ let isNew = false;
97
+ if (!sessionData) {
98
+ sessionData = {};
99
+ isNew = true;
100
+ }
101
+
102
+ const sessionState = { id: sid, data: sessionData, dirty: false };
103
+
104
+ ctx.session = {
105
+ get: (key) => sessionState.data[key],
106
+ set: (key, val) => {
107
+ sessionState.data[key] = val;
108
+ sessionState.dirty = true;
109
+ },
110
+ delete: (key) => {
111
+ delete sessionState.data[key];
112
+ sessionState.dirty = true;
113
+ },
114
+ clear: () => {
115
+ sessionState.data = {};
116
+ sessionState.dirty = true;
117
+ },
118
+ destroy: async () => {
119
+ if (sessionState.id) await store.delete(sessionState.id);
120
+ sessionState.id = null;
121
+ sessionState.data = {};
122
+ sessionState.dirty = false;
123
+ ctx.setHeader(
124
+ "Set-Cookie",
125
+ `${cookieName}=; HttpOnly; Secure; SameSite=Strict; Max-Age=0; Path=/`,
126
+ );
127
+ },
128
+ regenerate: async () => {
129
+ if (sessionState.id) await store.delete(sessionState.id);
130
+ sessionState.id = genId();
131
+ await store.set(sessionState.id, sessionState.data, maxAge);
132
+ ctx.setHeader(
133
+ "Set-Cookie",
134
+ `${cookieName}=${sessionState.id}${cookieSuffix}`,
135
+ );
136
+ sessionState.dirty = false;
137
+ isNew = false;
138
+ },
139
+ };
140
+
141
+ //Use the most reliable and well-isolated try...finally to ensure pipeline smoothness, while strictly limiting persistence logic to the "actually modified" checkpoint
142
+ try {
143
+ if (next) {
144
+ next.next ? await next.next() : await next();
145
+ }
146
+ } finally {
147
+ if (sessionState.dirty) {
148
+ // If it's a new session and the route has written data, only now generate the ID and issue the cookie
149
+ if (isNew || !sessionState.id) {
150
+ sessionState.id = genId();
151
+ ctx.setHeader(
152
+ "Set-Cookie",
153
+ `${cookieName}=${sessionState.id}${cookieSuffix}`,
154
+ );
155
+ }
156
+ await store.set(sessionState.id, sessionState.data, maxAge);
157
+ }
158
+ }
159
+ };
160
+ }
161
+
162
+ destroy() {
163
+ if (this.cleanup) clearInterval(this.cleanup);
164
+ }
165
+ }
166
+
167
+ export default SessionManager;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/utils/atomic-ops
6
+ */
7
+
8
+
9
+ class AtomicCounter {
10
+ constructor(initialValue = 0) {
11
+ this.value = initialValue;
12
+ }
13
+
14
+ /**
15
+ * Increment counter
16
+ * @param {number} delta - Amount to increment
17
+ * @returns {number} - New value
18
+ */
19
+ increment(delta = 1) {
20
+ this.value += delta;
21
+ return this.value;
22
+ }
23
+
24
+ /**
25
+ * Decrement counter
26
+ * @param {number} delta - Amount to decrement
27
+ * @returns {number} - New value
28
+ */
29
+ decrement(delta = 1) {
30
+ this.value -= delta;
31
+ return this.value;
32
+ }
33
+
34
+ /**
35
+ * Get current value
36
+ * @returns {number} - Current value
37
+ */
38
+ get() {
39
+ return this.value;
40
+ }
41
+
42
+ /**
43
+ * Set value
44
+ * @param {number} value - New value
45
+ */
46
+ set(value) {
47
+ this.value = value;
48
+ }
49
+
50
+ /**
51
+ * Compare and swap
52
+ * @param {number} expected - Expected value
53
+ * @param {number} newValue - New value
54
+ * @returns {boolean} - Whether swap succeeded
55
+ */
56
+ compareAndSwap(expected, newValue) {
57
+ if (this.value === expected) {
58
+ this.value = newValue;
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * AtomicFlag - Thread-safe boolean flag
67
+ */
68
+ class AtomicFlag {
69
+ constructor(initialValue = false) {
70
+ this.value = initialValue;
71
+ }
72
+
73
+ /**
74
+ * Set flag to true
75
+ * @returns {boolean} - Previous value
76
+ */
77
+ set() {
78
+ const prev = this.value;
79
+ this.value = true;
80
+ return prev;
81
+ }
82
+
83
+ /**
84
+ * Set flag to false
85
+ * @returns {boolean} - Previous value
86
+ */
87
+ clear() {
88
+ const prev = this.value;
89
+ this.value = false;
90
+ return prev;
91
+ }
92
+
93
+ /**
94
+ * Toggle flag
95
+ * @returns {boolean} - New value
96
+ */
97
+ toggle() {
98
+ this.value = !this.value;
99
+ return this.value;
100
+ }
101
+
102
+ /**
103
+ * Get current value
104
+ * @returns {boolean} - Current value
105
+ */
106
+ get() {
107
+ return this.value;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Factory functions
113
+ */
114
+ function createAtomicCounter(initialValue = 0) {
115
+ return new AtomicCounter(initialValue);
116
+ }
117
+
118
+ function createAtomicFlag(initialValue = false) {
119
+ return new AtomicFlag(initialValue);
120
+ }
121
+
122
+ export default {
123
+ createAtomicCounter,
124
+ createAtomicFlag,
125
+ AtomicCounter,
126
+ AtomicFlag
127
+ };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/utils/evn-loader
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ /**
11
+ * Parse .env file content
12
+ * @param {string} content - .env file content
13
+ * @returns {Object} - Parsed environment variables
14
+ */
15
+ function parseEnvContent(content) {
16
+ const env = {};
17
+ const lines = content.split('\n');
18
+
19
+ for (const line of lines) {
20
+ // Skip comments and empty lines
21
+ const trimmedLine = line.trim();
22
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
23
+ continue;
24
+ }
25
+
26
+ // Split key=value
27
+ const equalIndex = trimmedLine.indexOf('=');
28
+ if (equalIndex === -1) {
29
+ continue;
30
+ }
31
+
32
+ const key = trimmedLine.substring(0, equalIndex).trim();
33
+ let value = trimmedLine.substring(equalIndex + 1).trim();
34
+
35
+ // Remove quotes if present
36
+ if ((value.startsWith('"') && value.endsWith('"')) ||
37
+ (value.startsWith("'") && value.endsWith("'"))) {
38
+ value = value.substring(1, value.length - 1);
39
+ }
40
+
41
+ // Set environment variable
42
+ env[key] = value;
43
+ }
44
+
45
+ return env;
46
+ }
47
+
48
+ /**
49
+ * Load environment variables from .env file
50
+ * @param {string} envPath - Path to .env file
51
+ * @param {boolean} override - Whether to override existing env vars
52
+ * @returns {Object} - Loaded environment variables
53
+ */
54
+ function loadEnv(envPath = '.env', override = false) {
55
+ const absolutePath = path.resolve(process.cwd(), envPath);
56
+
57
+ if (!fs.existsSync(absolutePath)) {
58
+ console.warn(`AetherJS: .env file not found at ${absolutePath}`);
59
+ return {};
60
+ }
61
+
62
+ try {
63
+ const content = fs.readFileSync(absolutePath, 'utf8');
64
+ const env = parseEnvContent(content);
65
+
66
+ // Set environment variables
67
+ for (const [key, value] of Object.entries(env)) {
68
+ if (override || process.env[key] === undefined) {
69
+ process.env[key] = value;
70
+ }
71
+ }
72
+
73
+ return env;
74
+ } catch (error) {
75
+ console.error(`AetherJS: Error loading .env file: ${error.message}`);
76
+ return {};
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Watch .env file for changes and reload
82
+ * @param {string} envPath - Path to .env file
83
+ * @param {Function} callback - Callback when env changes
84
+ * @returns {Object} - Watcher object with close method
85
+ */
86
+ function watchEnv(envPath = '.env', callback) {
87
+ const absolutePath = path.resolve(process.cwd(), envPath);
88
+
89
+ if (!fs.existsSync(absolutePath)) {
90
+ console.warn(`AetherJS: .env file not found at ${absolutePath}`);
91
+ return { close: () => {} };
92
+ }
93
+
94
+ let lastContent = fs.readFileSync(absolutePath, 'utf8');
95
+
96
+ const watcher = fs.watch(absolutePath, (eventType) => {
97
+ if (eventType === 'change') {
98
+ try {
99
+ const newContent = fs.readFileSync(absolutePath, 'utf8');
100
+ if (newContent !== lastContent) {
101
+ lastContent = newContent;
102
+ const newEnv = parseEnvContent(newContent);
103
+
104
+ // Update process.env
105
+ for (const [key, value] of Object.entries(newEnv)) {
106
+ process.env[key] = value;
107
+ }
108
+
109
+ if (callback) {
110
+ callback(newEnv);
111
+ }
112
+ }
113
+ } catch (error) {
114
+ console.error(`AetherJS: Error reloading .env file: ${error.message}`);
115
+ }
116
+ }
117
+ });
118
+
119
+ return {
120
+ close: () => watcher.close()
121
+ };
122
+ }
123
+ export default {
124
+ loadEnv,
125
+ watchEnv,
126
+ parseEnvContent
127
+ };
128
+
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/utils/memory-pool
6
+ */
7
+ class MemoryPool {
8
+ constructor(options = {}) {
9
+ this.bufferSize = options.bufferSize || 8192; // 8KB default
10
+ this.poolSize = options.poolSize || 100;
11
+ this.buffers = [];
12
+ this.available = [];
13
+ this.stats = {
14
+ allocated: 0,
15
+ reused: 0,
16
+ created: 0
17
+ };
18
+
19
+ // Initialize pool
20
+ for (let i = 0; i < this.poolSize; i++) {
21
+ const buffer = Buffer.allocUnsafe(this.bufferSize);
22
+ this.buffers.push(buffer);
23
+ this.available.push(i);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Get a buffer from the pool
29
+ * @returns {Buffer} - Buffer from pool
30
+ */
31
+ get() {
32
+ if (this.available.length > 0) {
33
+ const index = this.available.pop();
34
+ this.stats.reused++;
35
+ return this.buffers[index];
36
+ }
37
+
38
+ // Pool exhausted, create new buffer
39
+ const buffer = Buffer.allocUnsafe(this.bufferSize);
40
+ this.buffers.push(buffer);
41
+ this.stats.created++;
42
+ this.stats.allocated++;
43
+ return buffer;
44
+ }
45
+
46
+ /**
47
+ * Return a buffer to the pool
48
+ * @param {Buffer} buffer - Buffer to return
49
+ */
50
+ release(buffer) {
51
+ const index = this.buffers.indexOf(buffer);
52
+ if (index !== -1 && !this.available.includes(index)) {
53
+ // Clear buffer content for security
54
+ buffer.fill(0);
55
+ this.available.push(index);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get pool statistics
61
+ * @returns {Object} - Pool stats
62
+ */
63
+ getStats() {
64
+ return {
65
+ ...this.stats,
66
+ totalBuffers: this.buffers.length,
67
+ availableBuffers: this.available.length,
68
+ utilization: ((this.buffers.length - this.available.length) / this.buffers.length * 100).toFixed(2) + '%'
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Destroy pool and free memory
74
+ */
75
+ destroy() {
76
+ this.buffers = [];
77
+ this.available = [];
78
+ this.stats = { allocated: 0, reused: 0, created: 0 };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Factory function to create memory pool instances
84
+ * @param {Object} options - Pool configuration
85
+ * @returns {MemoryPool} - Memory pool instance
86
+ */
87
+ function createMemoryPool(options = {}) {
88
+ return new MemoryPool(options);
89
+ }
90
+
91
+
92
+
93
+ export default createMemoryPool;