@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,227 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/middleware/middleware/params.js
6
+ */
7
+
8
+ function createParamsMiddleware(options = {}) {
9
+ const defaults = {
10
+ // Parameter parsing configuration
11
+ parseNumbers: true, // Auto-convert numbers
12
+ parseBooleans: true, // Auto-convert booleans
13
+ parseArrays: true, // Auto-convert arrays
14
+ trimStrings: true, // Auto-trim strings
15
+
16
+ // Validation configuration
17
+ validation: {
18
+ enabled: true,
19
+ onError: (context, errors) => {
20
+ context.setStatus(400).json({
21
+ error: "Validation Error",
22
+ message: "Invalid request parameters",
23
+ details: errors
24
+ });
25
+ }
26
+ }
27
+ };
28
+
29
+ const config = { ...defaults, ...options };
30
+
31
+ // Parameter type conversion
32
+ function parseValue(value, options) {
33
+ if (value === undefined || value === null) return value;
34
+
35
+ let result = String(value);
36
+
37
+ // Trim strings
38
+ if (options.trimStrings) {
39
+ result = result.trim();
40
+ }
41
+
42
+ // Parse numbers
43
+ if (options.parseNumbers && /^-?\d+(\.\d+)?$/.test(result)) {
44
+ const num = parseFloat(result);
45
+ if (!isNaN(num)) return num;
46
+ }
47
+
48
+ // Parse booleans
49
+ if (options.parseBooleans) {
50
+ const lower = result.toLowerCase();
51
+ if (lower === "true") return true;
52
+ if (lower === "false") return false;
53
+ if (lower === "1") return true;
54
+ if (lower === "0") return false;
55
+ }
56
+
57
+ // Parse arrays (comma-separated)
58
+ if (options.parseArrays && result.includes(",")) {
59
+ return result.split(",").map(item => parseValue(item.trim(), options));
60
+ }
61
+
62
+ return result;
63
+ }
64
+
65
+ // Validate parameters
66
+ function validateParams(params, rules) {
67
+ if (!config.validation.enabled || !rules) return null;
68
+
69
+ const errors = [];
70
+
71
+ for (const [key, rule] of Object.entries(rules)) {
72
+ const value = params[key];
73
+ const isRequired = rule.required !== false;
74
+
75
+ // Required check
76
+ if (isRequired && (value === undefined || value === null || value === "")) {
77
+ errors.push({
78
+ field: key,
79
+ message: rule.message || `${key} is required`,
80
+ type: "required"
81
+ });
82
+ continue;
83
+ }
84
+
85
+ // Type checking
86
+ if (value !== undefined && value !== null) {
87
+ if (rule.type === "number" && typeof value !== "number") {
88
+ errors.push({
89
+ field: key,
90
+ message: rule.message || `${key} must be a number`,
91
+ type: "type"
92
+ });
93
+ } else if (rule.type === "string" && typeof value !== "string") {
94
+ errors.push({
95
+ field: key,
96
+ message: rule.message || `${key} must be a string`,
97
+ type: "type"
98
+ });
99
+ } else if (rule.type === "boolean" && typeof value !== "boolean") {
100
+ errors.push({
101
+ field: key,
102
+ message: rule.message || `${key} must be a boolean`,
103
+ type: "type"
104
+ });
105
+ } else if (rule.type === "array" && !Array.isArray(value)) {
106
+ errors.push({
107
+ field: key,
108
+ message: rule.message || `${key} must be an array`,
109
+ type: "type"
110
+ });
111
+ } else if (rule.type === "object" && (typeof value !== "object" || Array.isArray(value))) {
112
+ errors.push({
113
+ field: key,
114
+ message: rule.message || `${key} must be an object`,
115
+ type: "type"
116
+ });
117
+ }
118
+
119
+ // Length validation
120
+ if (rule.minLength !== undefined && String(value).length < rule.minLength) {
121
+ errors.push({
122
+ field: key,
123
+ message: rule.message || `${key} must be at least ${rule.minLength} characters`,
124
+ type: "minLength"
125
+ });
126
+ }
127
+
128
+ if (rule.maxLength !== undefined && String(value).length > rule.maxLength) {
129
+ errors.push({
130
+ field: key,
131
+ message: rule.message || `${key} must be at most ${rule.maxLength} characters`,
132
+ type: "maxLength"
133
+ });
134
+ }
135
+
136
+ // Range validation
137
+ if (rule.min !== undefined && Number(value) < rule.min) {
138
+ errors.push({
139
+ field: key,
140
+ message: rule.message || `${key} must be at least ${rule.min}`,
141
+ type: "min"
142
+ });
143
+ }
144
+
145
+ if (rule.max !== undefined && Number(value) > rule.max) {
146
+ errors.push({
147
+ field: key,
148
+ message: rule.message || `${key} must be at most ${rule.max}`,
149
+ type: "max"
150
+ });
151
+ }
152
+
153
+ // Pattern validation
154
+ if (rule.pattern && !new RegExp(rule.pattern).test(String(value))) {
155
+ errors.push({
156
+ field: key,
157
+ message: rule.message || `${key} does not match the required pattern`,
158
+ type: "pattern"
159
+ });
160
+ }
161
+
162
+ // Enum validation
163
+ if (rule.enum && !rule.enum.includes(value)) {
164
+ errors.push({
165
+ field: key,
166
+ message: rule.message || `${key} must be one of: ${rule.enum.join(", ")}`,
167
+ type: "enum"
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ return errors.length > 0 ? errors : null;
174
+ }
175
+
176
+ return async function paramsMiddleware(context, next) {
177
+ // Merge all parameters
178
+ const allParams = {
179
+ ...context.params || {},
180
+ ...context.query || {},
181
+ ...(context.getState("parsedBody") || {})
182
+ };
183
+
184
+ // Parameter type conversion
185
+ const parsedParams = {};
186
+ for (const [key, value] of Object.entries(allParams)) {
187
+ parsedParams[key] = parseValue(value, config);
188
+ }
189
+
190
+ // Set to context
191
+ context.allParams = parsedParams;
192
+
193
+ // Get validation rules (from route metadata)
194
+ const validationRules = context.route?.validationRules;
195
+
196
+ // Execute validation
197
+ if (validationRules) {
198
+ const errors = validateParams(parsedParams, validationRules);
199
+ if (errors) {
200
+ return config.validation.onError(context, errors);
201
+ }
202
+ }
203
+
204
+ // Add convenience methods
205
+ context.getParam = (key, defaultValue) => {
206
+ return parsedParams[key] !== undefined ? parsedParams[key] : defaultValue;
207
+ };
208
+
209
+ context.hasParam = (key) => {
210
+ return parsedParams[key] !== undefined;
211
+ };
212
+
213
+ context.requireParam = (key) => {
214
+ const value = parsedParams[key];
215
+ if (value === undefined) {
216
+ throw new Error(`Parameter ${key} is required`);
217
+ }
218
+ return value;
219
+ };
220
+
221
+ if (typeof next === "function") {
222
+ await next();
223
+ }
224
+ };
225
+ }
226
+
227
+ export default createParamsMiddleware;
@@ -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/rate-limit.js
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,36 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/middleware/router.js
6
+ */
7
+
8
+
9
+ import AetherRouter from "../core/AetherRouter.js";
10
+
11
+ /**
12
+ * Create router middleware for AetherJS
13
+ * @param {Object} routes - Route definitions
14
+ * @param {Object} options - Router options
15
+ * @returns {Function} - Router middleware function
16
+ */
17
+ function createRouterMiddleware(routes = {}, options = {}) {
18
+ const router = new AetherRouter(options);
19
+
20
+ // Register routes from configuration
21
+ for (const [methodPath, handler] of Object.entries(routes)) {
22
+ const [method, path] = methodPath.split(" ");
23
+ if (method && path && handler) {
24
+ router[method.toLowerCase()](path, handler);
25
+ }
26
+ }
27
+
28
+ return router.middleware();
29
+ }
30
+
31
+
32
+ createRouterMiddleware.Router = AetherRouter;
33
+
34
+
35
+ export default createRouterMiddleware;
36
+ export { createRouterMiddleware as createRouter, AetherRouter };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/middleware/security.js
6
+ */
7
+
8
+
9
+ function parsePermissionsPolicy(directivesString) {
10
+ if (!directivesString || typeof directivesString !== "string") {
11
+ return null;
12
+ }
13
+ const directives = {};
14
+ const pairs = directivesString.split(/[,;]/);
15
+ for (const pair of pairs) {
16
+ const trimmed = pair.trim();
17
+ if (!trimmed) continue;
18
+ const equalIndex = trimmed.indexOf("=");
19
+ if (equalIndex !== -1) {
20
+ const feature = trimmed.substring(0, equalIndex).trim();
21
+ const value = trimmed.substring(equalIndex + 1).trim();
22
+ if (feature && value) directives[feature] = value;
23
+ }
24
+ }
25
+ return Object.keys(directives).length > 0 ? directives : null;
26
+ }
27
+
28
+ /**
29
+ * Create security headers middleware for AetherJS
30
+ * @param {Object} options - Security headers configuration
31
+ * @returns {Function} - Standard AetherJS middleware (ctx, next)
32
+ */
33
+ function createSecurityMiddleware(options = {}) {
34
+ const envConfig = {
35
+ hstsEnabled: process.env.SECURITY_HSTS_ENABLED,
36
+ hstsMaxAge: process.env.SECURITY_HSTS_MAX_AGE,
37
+ noSniffEnabled: process.env.SECURITY_NO_SNIFF,
38
+ xssFilterEnabled: process.env.SECURITY_XSS_FILTER,
39
+ frameguardAction: process.env.SECURITY_FRAMEGUARD_ACTION,
40
+ hidePoweredBy: process.env.SECURITY_HIDE_POWERED_BY,
41
+ referrerPolicy: process.env.SECURITY_REFERRER_POLICY,
42
+ };
43
+
44
+ const defaults = {
45
+ hsts: {
46
+ enabled: envConfig.hstsEnabled !== "false",
47
+ maxAge: envConfig.hstsMaxAge ? parseInt(envConfig.hstsMaxAge) : 31536000,
48
+ includeSubDomains: true,
49
+ preload: false,
50
+ },
51
+ noSniff: { enabled: envConfig.noSniffEnabled !== "false" },
52
+ xssFilter: { enabled: envConfig.xssFilterEnabled !== "false" },
53
+ frameguard: { enabled: true, action: envConfig.frameguardAction || "DENY" },
54
+ hidePoweredBy: envConfig.hidePoweredBy !== "false",
55
+ referrerPolicy: {
56
+ enabled: true,
57
+ value: envConfig.referrerPolicy || "strict-origin-when-cross-origin",
58
+ },
59
+ permissionsPolicy: {
60
+ enabled: true,
61
+ directives: { camera: "()", microphone: "()", geolocation: "()" },
62
+ },
63
+ };
64
+
65
+ // Deep merge simple version for performance
66
+ const config = { ...defaults, ...options };
67
+
68
+ // 🚀 性能优化:预计算所有 Header 字符串,避免在请求响应循环中构造字符串
69
+ const staticHeaders = [];
70
+
71
+ if (config.hsts.enabled) {
72
+ let val = `max-age=${config.hsts.maxAge}${config.hsts.includeSubDomains ? "; includeSubDomains" : ""}${config.hsts.preload ? "; preload" : ""}`;
73
+ staticHeaders.push(["Strict-Transport-Security", val]);
74
+ }
75
+ if (config.noSniff.enabled)
76
+ staticHeaders.push(["X-Content-Type-Options", "nosniff"]);
77
+ if (config.xssFilter.enabled)
78
+ staticHeaders.push(["X-XSS-Protection", "1; mode=block"]);
79
+ if (config.frameguard.enabled)
80
+ staticHeaders.push([
81
+ "X-Frame-Options",
82
+ config.frameguard.action.toUpperCase(),
83
+ ]);
84
+ if (config.referrerPolicy.enabled)
85
+ staticHeaders.push(["Referrer-Policy", config.referrerPolicy.value]);
86
+ if (config.permissionsPolicy.enabled) {
87
+ const p = Object.entries(config.permissionsPolicy.directives)
88
+ .map(([f, v]) => `${f}=${v}`)
89
+ .join(", ");
90
+ staticHeaders.push(["Permissions-Policy", p]);
91
+ }
92
+
93
+ /**
94
+ * 💡 修复后的核心中间件函数
95
+ * 使用 (context, next) 签名替代旧版的 (context, signal)
96
+ */
97
+ return async function securityMiddleware(context, next) {
98
+ console.log("Security middleware executing for URL:", context.url);
99
+ // 1. 批量写入预计算的 Header
100
+ for (let i = 0; i < staticHeaders.length; i++) {
101
+ context.setHeader(staticHeaders[i][0], staticHeaders[i][1]);
102
+ }
103
+
104
+ // 2. 移除敏感 Header
105
+ if (config.hidePoweredBy && context._response) {
106
+ context._response.removeHeader("X-Powered-By");
107
+ }
108
+
109
+ // 3. 💡 修复点:调用标准 next() 而不是 signal.next()
110
+ if (typeof next === "function") {
111
+ return next();
112
+ }
113
+ };
114
+ }
115
+
116
+ export default createSecurityMiddleware;