aetherframework-middleware 1.0.0

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,114 @@
1
+ // security.js - Security headers middleware for AetherJS
2
+ // High-performance implementation with pre-computed headers
3
+
4
+ /**
5
+ * Parse Permissions-Policy directives string
6
+ */
7
+ function parsePermissionsPolicy(directivesString) {
8
+ if (!directivesString || typeof directivesString !== "string") {
9
+ return null;
10
+ }
11
+ const directives = {};
12
+ const pairs = directivesString.split(/[,;]/);
13
+ for (const pair of pairs) {
14
+ const trimmed = pair.trim();
15
+ if (!trimmed) continue;
16
+ const equalIndex = trimmed.indexOf("=");
17
+ if (equalIndex !== -1) {
18
+ const feature = trimmed.substring(0, equalIndex).trim();
19
+ const value = trimmed.substring(equalIndex + 1).trim();
20
+ if (feature && value) directives[feature] = value;
21
+ }
22
+ }
23
+ return Object.keys(directives).length > 0 ? directives : null;
24
+ }
25
+
26
+ /**
27
+ * Create security headers middleware for AetherJS
28
+ * @param {Object} options - Security headers configuration
29
+ * @returns {Function} - Standard AetherJS middleware (ctx, next)
30
+ */
31
+ function createSecurityMiddleware(options = {}) {
32
+ const envConfig = {
33
+ hstsEnabled: process.env.SECURITY_HSTS_ENABLED,
34
+ hstsMaxAge: process.env.SECURITY_HSTS_MAX_AGE,
35
+ noSniffEnabled: process.env.SECURITY_NO_SNIFF,
36
+ xssFilterEnabled: process.env.SECURITY_XSS_FILTER,
37
+ frameguardAction: process.env.SECURITY_FRAMEGUARD_ACTION,
38
+ hidePoweredBy: process.env.SECURITY_HIDE_POWERED_BY,
39
+ referrerPolicy: process.env.SECURITY_REFERRER_POLICY,
40
+ };
41
+
42
+ const defaults = {
43
+ hsts: {
44
+ enabled: envConfig.hstsEnabled !== "false",
45
+ maxAge: envConfig.hstsMaxAge ? parseInt(envConfig.hstsMaxAge) : 31536000,
46
+ includeSubDomains: true,
47
+ preload: false,
48
+ },
49
+ noSniff: { enabled: envConfig.noSniffEnabled !== "false" },
50
+ xssFilter: { enabled: envConfig.xssFilterEnabled !== "false" },
51
+ frameguard: { enabled: true, action: envConfig.frameguardAction || "DENY" },
52
+ hidePoweredBy: envConfig.hidePoweredBy !== "false",
53
+ referrerPolicy: {
54
+ enabled: true,
55
+ value: envConfig.referrerPolicy || "strict-origin-when-cross-origin",
56
+ },
57
+ permissionsPolicy: {
58
+ enabled: true,
59
+ directives: { camera: "()", microphone: "()", geolocation: "()" },
60
+ },
61
+ };
62
+
63
+ // Deep merge simple version for performance
64
+ const config = { ...defaults, ...options };
65
+
66
+ // 🚀 性能优化:预计算所有 Header 字符串,避免在请求响应循环中构造字符串
67
+ const staticHeaders = [];
68
+
69
+ if (config.hsts.enabled) {
70
+ let val = `max-age=${config.hsts.maxAge}${config.hsts.includeSubDomains ? "; includeSubDomains" : ""}${config.hsts.preload ? "; preload" : ""}`;
71
+ staticHeaders.push(["Strict-Transport-Security", val]);
72
+ }
73
+ if (config.noSniff.enabled)
74
+ staticHeaders.push(["X-Content-Type-Options", "nosniff"]);
75
+ if (config.xssFilter.enabled)
76
+ staticHeaders.push(["X-XSS-Protection", "1; mode=block"]);
77
+ if (config.frameguard.enabled)
78
+ staticHeaders.push([
79
+ "X-Frame-Options",
80
+ config.frameguard.action.toUpperCase(),
81
+ ]);
82
+ if (config.referrerPolicy.enabled)
83
+ staticHeaders.push(["Referrer-Policy", config.referrerPolicy.value]);
84
+ if (config.permissionsPolicy.enabled) {
85
+ const p = Object.entries(config.permissionsPolicy.directives)
86
+ .map(([f, v]) => `${f}=${v}`)
87
+ .join(", ");
88
+ staticHeaders.push(["Permissions-Policy", p]);
89
+ }
90
+
91
+ /**
92
+ * 💡 修复后的核心中间件函数
93
+ * 使用 (context, next) 签名替代旧版的 (context, signal)
94
+ */
95
+ return async function securityMiddleware(context, next) {
96
+ console.log("Security middleware executing for URL:", context.url);
97
+ // 1. 批量写入预计算的 Header
98
+ for (let i = 0; i < staticHeaders.length; i++) {
99
+ context.setHeader(staticHeaders[i][0], staticHeaders[i][1]);
100
+ }
101
+
102
+ // 2. 移除敏感 Header
103
+ if (config.hidePoweredBy && context._response) {
104
+ context._response.removeHeader("X-Powered-By");
105
+ }
106
+
107
+ // 3. 💡 修复点:调用标准 next() 而不是 signal.next()
108
+ if (typeof next === "function") {
109
+ return next();
110
+ }
111
+ };
112
+ }
113
+
114
+ export default createSecurityMiddleware;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present, GameCuft & AetherJS Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aether/middleware/session
6
+ */
7
+
8
+ import crypto from "crypto";
9
+
10
+ // 🚀 极致优化:用最原始、无任何对象分配的 for 循环单点查找 Cookie,把 GC 开销降到绝对零度
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
+ // 确保是独立的 cookie 名,而不是别的前缀
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
+ // 🚀 放弃长 UUID,改用 16 字节的高性能十六进制 ID,缩短 Cookie 长度,减轻网络和压缩中间件开销
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
+ // 🚀 策略调整:如果没拿到,先给个空对象,绝不提前写库、不提前设 Cookie
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
+ // 🚀 用最稳妥且隔离良好的 try...finally 确保管道顺畅,同时将落盘逻辑卡死在“确实有改动”的关口
142
+ try {
143
+ if (next) {
144
+ next.next ? await next.next() : await next();
145
+ }
146
+ } finally {
147
+ if (sessionState.dirty) {
148
+ // 如果是新 session 且路由往里写了数据,在此刻才真正生成 ID 并下发 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,125 @@
1
+ // atomic-ops.js - Atomic operations for AetherJS
2
+ // Thread-safe state management using synchronous operations
3
+
4
+ /**
5
+ * AtomicCounter - Thread-safe counter
6
+ */
7
+ class AtomicCounter {
8
+ constructor(initialValue = 0) {
9
+ this.value = initialValue;
10
+ }
11
+
12
+ /**
13
+ * Increment counter
14
+ * @param {number} delta - Amount to increment
15
+ * @returns {number} - New value
16
+ */
17
+ increment(delta = 1) {
18
+ this.value += delta;
19
+ return this.value;
20
+ }
21
+
22
+ /**
23
+ * Decrement counter
24
+ * @param {number} delta - Amount to decrement
25
+ * @returns {number} - New value
26
+ */
27
+ decrement(delta = 1) {
28
+ this.value -= delta;
29
+ return this.value;
30
+ }
31
+
32
+ /**
33
+ * Get current value
34
+ * @returns {number} - Current value
35
+ */
36
+ get() {
37
+ return this.value;
38
+ }
39
+
40
+ /**
41
+ * Set value
42
+ * @param {number} value - New value
43
+ */
44
+ set(value) {
45
+ this.value = value;
46
+ }
47
+
48
+ /**
49
+ * Compare and swap
50
+ * @param {number} expected - Expected value
51
+ * @param {number} newValue - New value
52
+ * @returns {boolean} - Whether swap succeeded
53
+ */
54
+ compareAndSwap(expected, newValue) {
55
+ if (this.value === expected) {
56
+ this.value = newValue;
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * AtomicFlag - Thread-safe boolean flag
65
+ */
66
+ class AtomicFlag {
67
+ constructor(initialValue = false) {
68
+ this.value = initialValue;
69
+ }
70
+
71
+ /**
72
+ * Set flag to true
73
+ * @returns {boolean} - Previous value
74
+ */
75
+ set() {
76
+ const prev = this.value;
77
+ this.value = true;
78
+ return prev;
79
+ }
80
+
81
+ /**
82
+ * Set flag to false
83
+ * @returns {boolean} - Previous value
84
+ */
85
+ clear() {
86
+ const prev = this.value;
87
+ this.value = false;
88
+ return prev;
89
+ }
90
+
91
+ /**
92
+ * Toggle flag
93
+ * @returns {boolean} - New value
94
+ */
95
+ toggle() {
96
+ this.value = !this.value;
97
+ return this.value;
98
+ }
99
+
100
+ /**
101
+ * Get current value
102
+ * @returns {boolean} - Current value
103
+ */
104
+ get() {
105
+ return this.value;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Factory functions
111
+ */
112
+ function createAtomicCounter(initialValue = 0) {
113
+ return new AtomicCounter(initialValue);
114
+ }
115
+
116
+ function createAtomicFlag(initialValue = false) {
117
+ return new AtomicFlag(initialValue);
118
+ }
119
+
120
+ export default {
121
+ createAtomicCounter,
122
+ createAtomicFlag,
123
+ AtomicCounter,
124
+ AtomicFlag
125
+ };
@@ -0,0 +1,124 @@
1
+ // env-loader.js - Environment variable loader for AetherJS
2
+ // Supports .env file parsing and hot-reloading
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ /**
7
+ * Parse .env file content
8
+ * @param {string} content - .env file content
9
+ * @returns {Object} - Parsed environment variables
10
+ */
11
+ function parseEnvContent(content) {
12
+ const env = {};
13
+ const lines = content.split('\n');
14
+
15
+ for (const line of lines) {
16
+ // Skip comments and empty lines
17
+ const trimmedLine = line.trim();
18
+ if (!trimmedLine || trimmedLine.startsWith('#')) {
19
+ continue;
20
+ }
21
+
22
+ // Split key=value
23
+ const equalIndex = trimmedLine.indexOf('=');
24
+ if (equalIndex === -1) {
25
+ continue;
26
+ }
27
+
28
+ const key = trimmedLine.substring(0, equalIndex).trim();
29
+ let value = trimmedLine.substring(equalIndex + 1).trim();
30
+
31
+ // Remove quotes if present
32
+ if ((value.startsWith('"') && value.endsWith('"')) ||
33
+ (value.startsWith("'") && value.endsWith("'"))) {
34
+ value = value.substring(1, value.length - 1);
35
+ }
36
+
37
+ // Set environment variable
38
+ env[key] = value;
39
+ }
40
+
41
+ return env;
42
+ }
43
+
44
+ /**
45
+ * Load environment variables from .env file
46
+ * @param {string} envPath - Path to .env file
47
+ * @param {boolean} override - Whether to override existing env vars
48
+ * @returns {Object} - Loaded environment variables
49
+ */
50
+ function loadEnv(envPath = '.env', override = false) {
51
+ const absolutePath = path.resolve(process.cwd(), envPath);
52
+
53
+ if (!fs.existsSync(absolutePath)) {
54
+ console.warn(`AetherJS: .env file not found at ${absolutePath}`);
55
+ return {};
56
+ }
57
+
58
+ try {
59
+ const content = fs.readFileSync(absolutePath, 'utf8');
60
+ const env = parseEnvContent(content);
61
+
62
+ // Set environment variables
63
+ for (const [key, value] of Object.entries(env)) {
64
+ if (override || process.env[key] === undefined) {
65
+ process.env[key] = value;
66
+ }
67
+ }
68
+
69
+ return env;
70
+ } catch (error) {
71
+ console.error(`AetherJS: Error loading .env file: ${error.message}`);
72
+ return {};
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Watch .env file for changes and reload
78
+ * @param {string} envPath - Path to .env file
79
+ * @param {Function} callback - Callback when env changes
80
+ * @returns {Object} - Watcher object with close method
81
+ */
82
+ function watchEnv(envPath = '.env', callback) {
83
+ const absolutePath = path.resolve(process.cwd(), envPath);
84
+
85
+ if (!fs.existsSync(absolutePath)) {
86
+ console.warn(`AetherJS: .env file not found at ${absolutePath}`);
87
+ return { close: () => {} };
88
+ }
89
+
90
+ let lastContent = fs.readFileSync(absolutePath, 'utf8');
91
+
92
+ const watcher = fs.watch(absolutePath, (eventType) => {
93
+ if (eventType === 'change') {
94
+ try {
95
+ const newContent = fs.readFileSync(absolutePath, 'utf8');
96
+ if (newContent !== lastContent) {
97
+ lastContent = newContent;
98
+ const newEnv = parseEnvContent(newContent);
99
+
100
+ // Update process.env
101
+ for (const [key, value] of Object.entries(newEnv)) {
102
+ process.env[key] = value;
103
+ }
104
+
105
+ if (callback) {
106
+ callback(newEnv);
107
+ }
108
+ }
109
+ } catch (error) {
110
+ console.error(`AetherJS: Error reloading .env file: ${error.message}`);
111
+ }
112
+ }
113
+ });
114
+
115
+ return {
116
+ close: () => watcher.close()
117
+ };
118
+ }
119
+ export default {
120
+ loadEnv,
121
+ watchEnv,
122
+ parseEnvContent
123
+ };
124
+
@@ -0,0 +1,93 @@
1
+ // memory-pool.js - Memory pool management for AetherJS
2
+ // Zero-copy buffer management for high-performance operations
3
+
4
+ /**
5
+ * MemoryPool - Manages a pool of reusable buffers
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;