@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,299 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/middleware/middleware/body-parser.js
6
+ */
7
+
8
+ import { StringDecoder } from 'string_decoder';
9
+
10
+ /**
11
+ * Parse size string to bytes
12
+ * @param {string|number} size - Size string like '1mb', '2kb', or bytes count
13
+ * @returns {number} - Size in bytes
14
+ */
15
+ function parseSize(size) {
16
+ if (typeof size === 'number') return size;
17
+ if (typeof size !== 'string') return 0;
18
+
19
+ const units = {
20
+ 'b': 1,
21
+ 'kb': 1024,
22
+ 'mb': 1024 * 1024,
23
+ 'gb': 1024 * 1024 * 1024
24
+ };
25
+
26
+ const parsedStr = size.toLowerCase();
27
+ const match = parsedStr.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
28
+ if (!match) {
29
+ throw new Error(`Invalid size format: ${size}`);
30
+ }
31
+
32
+ // 💡 修复:正确读取捕获组
33
+ const value = parseFloat(match[1]); // 第一捕获组:纯数字
34
+ const unit = match[2]; // 第二捕获组:单位 (如 'mb')
35
+ return value * (units[unit] || 1);
36
+ }
37
+
38
+ /**
39
+ * Parse request body as buffer
40
+ * @param {Object} request - HTTP request object
41
+ * @param {number} limit - Maximum size in bytes
42
+ * @returns {Promise<Buffer>} - Request body buffer
43
+ */
44
+ async function parseBodyBuffer(request, limit) {
45
+ return new Promise((resolve, reject) => {
46
+ const chunks = [];
47
+ let totalLength = 0;
48
+
49
+ request.on('data', (chunk) => {
50
+ totalLength += chunk.length;
51
+
52
+ if (totalLength > limit) {
53
+ request.destroy();
54
+ reject(new Error(`Request body exceeded limit of ${limit} bytes`));
55
+ return;
56
+ }
57
+
58
+ chunks.push(chunk);
59
+ });
60
+
61
+ request.on('end', () => {
62
+ resolve(Buffer.concat(chunks));
63
+ });
64
+
65
+ request.on('error', (error) => {
66
+ reject(error);
67
+ });
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Parse request body as JSON
73
+ * @param {Object} request - HTTP request object
74
+ * @param {number} limit - Maximum size in bytes
75
+ * @returns {Promise<Object>} - Parsed JSON object
76
+ */
77
+ async function parseBodyJson(request, limit) {
78
+ const buffer = await parseBodyBuffer(request, limit);
79
+ const text = buffer.toString('utf8');
80
+
81
+ try {
82
+ return JSON.parse(text);
83
+ } catch (error) {
84
+ throw new Error(`Invalid JSON: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Parse request body as URL-encoded
90
+ * @param {Object} request - HTTP request object
91
+ * @param {number} limit - Maximum size in bytes
92
+ * @returns {Promise<Object>} - Parsed URL-encoded object
93
+ */
94
+ async function parseBodyUrlEncoded(request, limit) {
95
+ const buffer = await parseBodyBuffer(request, limit);
96
+ const text = buffer.toString('utf8');
97
+
98
+ const result = {};
99
+ const pairs = text.split('&');
100
+
101
+ for (const pair of pairs) {
102
+ const [key, value] = pair.split('=');
103
+ if (key) {
104
+ result[decodeURIComponent(key)] = decodeURIComponent(value || '');
105
+ }
106
+ }
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Parse request body as text
113
+ * @param {Object} request - HTTP request object
114
+ * @param {number} limit - Maximum size in bytes
115
+ * @returns {Promise<string>} - Parsed text
116
+ */
117
+ async function parseBodyText(request, limit) {
118
+ const buffer = await parseBodyBuffer(request, limit);
119
+ return buffer.toString('utf8');
120
+ }
121
+
122
+ /**
123
+ * Create body parser middleware for AetherJS
124
+ * @param {Object} options - Body parser configuration
125
+ * @returns {Function} - Body parser middleware function
126
+ */
127
+ function createBodyParserMiddleware(options = {}) {
128
+ // Load configuration from environment variables
129
+ const envConfig = {
130
+ jsonLimit: process.env.BODY_LIMIT_JSON,
131
+ urlencodedLimit: process.env.BODY_LIMIT_URLENCODED,
132
+ textLimit: process.env.BODY_LIMIT_TEXT,
133
+ rawLimit: process.env.BODY_LIMIT_RAW,
134
+ enableJson: process.env.BODY_ENABLE_JSON,
135
+ enableUrlencoded: process.env.BODY_ENABLE_URLENCODED,
136
+ enableText: process.env.BODY_ENABLE_TEXT,
137
+ enableRaw: process.env.BODY_ENABLE_RAW
138
+ };
139
+
140
+ // Default configuration
141
+ const defaults = {
142
+ json: {
143
+ enabled: envConfig.enableJson !== 'false',
144
+ limit: parseSize(envConfig.jsonLimit || '1mb'),
145
+ strict: true,
146
+ reviver: null
147
+ },
148
+ urlencoded: {
149
+ enabled: envConfig.enableUrlencoded !== 'false',
150
+ limit: parseSize(envConfig.urlencodedLimit || '1mb'),
151
+ extended: false,
152
+ parameterLimit: 1000
153
+ },
154
+ text: {
155
+ enabled: envConfig.enableText !== 'false',
156
+ limit: parseSize(envConfig.textLimit || '1mb'),
157
+ defaultCharset: 'utf-8'
158
+ },
159
+ raw: {
160
+ enabled: envConfig.enableRaw !== 'false',
161
+ limit: parseSize(envConfig.rawLimit || '10mb'),
162
+ type: 'application/octet-stream'
163
+ }
164
+ };
165
+
166
+ // 💡 修复:安全的嵌套字段深层合并,防止 options 传参直接覆盖 defaults 细节配置
167
+ const config = {
168
+ json: { ...defaults.json, ...options.json },
169
+ urlencoded: { ...defaults.urlencoded, ...options.urlencoded },
170
+ text: { ...defaults.text, ...options.text },
171
+ raw: { ...defaults.raw, ...options.raw }
172
+ };
173
+
174
+ // 转换各类型的解析上限
175
+ if (options.json?.limit) config.json.limit = parseSize(options.json.limit);
176
+ if (options.urlencoded?.limit) config.urlencoded.limit = parseSize(options.urlencoded.limit);
177
+ if (options.text?.limit) config.text.limit = parseSize(options.text.limit);
178
+ if (options.raw?.limit) config.raw.limit = parseSize(options.raw.limit);
179
+
180
+ const parsers = new Map();
181
+
182
+ if (config.json.enabled) {
183
+ parsers.set('application/json', async (request) => {
184
+ const data = await parseBodyJson(request, config.json.limit);
185
+ if (config.json.reviver) {
186
+ return JSON.parse(JSON.stringify(data), config.json.reviver);
187
+ }
188
+ return data;
189
+ });
190
+
191
+ parsers.set('application/json; charset=utf-8', parsers.get('application/json'));
192
+ parsers.set('application/json; charset=utf8', parsers.get('application/json'));
193
+ }
194
+
195
+ if (config.urlencoded.enabled) {
196
+ parsers.set('application/x-www-form-urlencoded', async (request) => {
197
+ return await parseBodyUrlEncoded(request, config.urlencoded.limit);
198
+ });
199
+ }
200
+
201
+ if (config.text.enabled) {
202
+ parsers.set('text/plain', async (request) => {
203
+ return await parseBodyText(request, config.text.limit);
204
+ });
205
+
206
+ parsers.set('text/html', parsers.get('text/plain'));
207
+ parsers.set('text/xml', parsers.get('text/plain'));
208
+ parsers.set('text/css', parsers.get('text/plain'));
209
+ parsers.set('text/javascript', parsers.get('text/plain'));
210
+ }
211
+
212
+ if (config.raw.enabled) {
213
+ parsers.set(config.raw.type, async (request) => {
214
+ return await parseBodyBuffer(request, config.raw.limit);
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Body parser middleware function (Fully Aligned to (context, next) standard)
220
+ * @param {AetherContext} context - AetherJS execution context
221
+ * @param {Function} next - Continuation callback
222
+ */
223
+ return async function bodyParserMiddleware(context, next) {
224
+ // Skip if no body is expected
225
+ if (context.method === 'GET' || context.method === 'HEAD') {
226
+ return typeof next === 'function' ? next() : null;
227
+ }
228
+
229
+ const contentType = context.getHeader('content-type') || '';
230
+ const contentLength = parseInt(context.getHeader('content-length')) || 0;
231
+
232
+ // Skip if no content
233
+ if (contentLength === 0) {
234
+ return typeof next === 'function' ? next() : null;
235
+ }
236
+
237
+ // Check content type
238
+ let parser = null;
239
+ for (const [type, parserFunc] of parsers) {
240
+ if (contentType.includes(type)) {
241
+ parser = parserFunc;
242
+ break;
243
+ }
244
+ }
245
+
246
+ // If no parser found and raw is enabled, use raw parser
247
+ if (!parser && config.raw.enabled) {
248
+ parser = parsers.get(config.raw.type);
249
+ }
250
+
251
+ if (!parser) {
252
+ // No suitable parser found, continue without parsing
253
+ return typeof next === 'function' ? next() : null;
254
+ }
255
+
256
+ try {
257
+ // Parse body
258
+ const body = await parser(context._request);
259
+
260
+ // Store parsed body in context state
261
+ if (contentType.includes('application/json')) {
262
+ context.setState('parsedBody', { json: body });
263
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
264
+ context.setState('parsedBody', { urlencoded: body });
265
+ } else if (contentType.includes('text/')) {
266
+ context.setState('parsedBody', { text: body });
267
+ } else {
268
+ context.setState('parsedBody', { raw: body });
269
+ }
270
+
271
+ // Add convenience methods to context
272
+ context.body = body; // 触发我们在 AetherContext 加的 setter
273
+ context.getBody = () => body;
274
+
275
+ return typeof next === 'function' ? next() : null;
276
+
277
+ } catch (error) {
278
+ // Handle parsing errors
279
+ if (error.message.includes('exceeded limit')) {
280
+ context.setStatus(413).json({
281
+ error: 'Payload Too Large',
282
+ message: error.message
283
+ });
284
+ } else if (error.message.includes('Invalid JSON')) {
285
+ context.setStatus(400).json({
286
+ error: 'Bad Request',
287
+ message: 'Invalid JSON format'
288
+ });
289
+ } else {
290
+ context.setStatus(400).json({
291
+ error: 'Bad Request',
292
+ message: error.message
293
+ });
294
+ }
295
+ }
296
+ };
297
+ }
298
+
299
+ export default createBodyParserMiddleware;
@@ -0,0 +1,248 @@
1
+
2
+ /**
3
+ * @license MIT
4
+ * Copyright (c) 2026-present AetherFramework Contributors.
5
+ * SPDX-License-Identifier: MIT
6
+ * @module @aetherframework/middleware/middleware/compression.js
7
+ */
8
+
9
+ import zlib from "zlib";
10
+
11
+ /**
12
+ * Parse compression types from string
13
+ * @param {string} types - Comma-separated list of content types
14
+ * @returns {Array<string>} - Array of content types
15
+ */
16
+ function parseCompressionTypes(types) {
17
+ if (!types || typeof types !== "string") {
18
+ return [
19
+ "text/plain",
20
+ "text/html",
21
+ "text/css",
22
+ "application/javascript",
23
+ "application/json",
24
+ "application/xml",
25
+ "text/xml",
26
+ "application/xhtml+xml",
27
+ "text/javascript",
28
+ ];
29
+ }
30
+
31
+ return types
32
+ .split(",")
33
+ .map((type) => type.trim())
34
+ .filter(Boolean);
35
+ }
36
+
37
+ /**
38
+ * Check if content type should be compressed
39
+ * @param {string} contentType - Response content type
40
+ * @param {Array<string>} compressibleTypes - List of compressible types
41
+ * @returns {boolean} - Whether to compress
42
+ */
43
+ function shouldCompress(contentType, compressibleTypes) {
44
+ if (!contentType) return false;
45
+
46
+ for (const type of compressibleTypes) {
47
+ if (contentType.includes(type)) {
48
+ return true;
49
+ }
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Create compression middleware for AetherJS
57
+ * @param {Object} options - Compression configuration
58
+ * @returns {Function} - Compression middleware function
59
+ */
60
+ function createCompressionMiddleware(options = {}) {
61
+ // Load configuration from environment variables
62
+ const envConfig = {
63
+ enabled: process.env.COMPRESSION_ENABLED,
64
+ threshold: process.env.COMPRESSION_THRESHOLD,
65
+ level: process.env.COMPRESSION_LEVEL,
66
+ memLevel: process.env.COMPRESSION_MEM_LEVEL,
67
+ strategy: process.env.COMPRESSION_STRATEGY,
68
+ chunkSize: process.env.COMPRESSION_CHUNK_SIZE,
69
+ windowBits: process.env.COMPRESSION_WINDOW_BITS,
70
+ gzip: process.env.COMPRESSION_GZIP,
71
+ deflate: process.env.COMPRESSION_DEFLATE,
72
+ brotli: process.env.COMPRESSION_BROTLI,
73
+ types: process.env.COMPRESSION_TYPES,
74
+ };
75
+
76
+ // Default configuration
77
+ const defaults = {
78
+ enabled: envConfig.enabled !== "false",
79
+ threshold: parseInt(envConfig.threshold) || 1024,
80
+ level: parseInt(envConfig.level) || zlib.constants.Z_DEFAULT_COMPRESSION,
81
+ memLevel: parseInt(envConfig.memLevel) || 8,
82
+ strategy: parseInt(envConfig.strategy) || zlib.constants.Z_DEFAULT_STRATEGY,
83
+ chunkSize: parseInt(envConfig.chunkSize) || 16 * 1024,
84
+ windowBits: parseInt(envConfig.windowBits) || 15,
85
+ gzip: envConfig.gzip !== "false",
86
+ deflate: envConfig.deflate === "true",
87
+ brotli:
88
+ envConfig.brotli === "true" &&
89
+ typeof zlib.createBrotliCompress === "function",
90
+ types: parseCompressionTypes(envConfig.types),
91
+ filter: (contentType) =>
92
+ shouldCompress(contentType, parseCompressionTypes(envConfig.types)),
93
+ };
94
+
95
+ // Merge with provided options
96
+ const config = { ...defaults, ...options };
97
+
98
+ // Create compression options
99
+ const gzipOptions = {
100
+ level: config.level,
101
+ memLevel: config.memLevel,
102
+ strategy: config.strategy,
103
+ chunkSize: config.chunkSize,
104
+ windowBits: config.windowBits,
105
+ };
106
+
107
+ const deflateOptions = { ...gzipOptions };
108
+ const brotliOptions = {
109
+ params: {
110
+ [zlib.constants.BROTLI_PARAM_QUALITY]: config.level,
111
+ [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
112
+ [zlib.constants.BROTLI_PARAM_SIZE_HINT]: config.chunkSize,
113
+ },
114
+ };
115
+
116
+ /**
117
+ * Compress data using specified algorithm
118
+ * @param {Buffer} data - Data to compress
119
+ * @param {string} encoding - Compression algorithm
120
+ * @returns {Promise<Buffer>} - Compressed data
121
+ */
122
+ async function compressData(data, encoding) {
123
+ return new Promise((resolve, reject) => {
124
+ let compressor;
125
+
126
+ switch (encoding) {
127
+ case "gzip":
128
+ compressor = zlib.createGzip(gzipOptions);
129
+ break;
130
+ case "deflate":
131
+ compressor = zlib.createDeflate(deflateOptions);
132
+ break;
133
+ case "br":
134
+ if (!config.brotli) {
135
+ reject(new Error("Brotli compression not supported"));
136
+ return;
137
+ }
138
+ compressor = zlib.createBrotliCompress(brotliOptions);
139
+ break;
140
+ default:
141
+ reject(new Error(`Unsupported compression: ${encoding}`));
142
+ return;
143
+ }
144
+
145
+ const chunks = [];
146
+ compressor.on("data", (chunk) => chunks.push(chunk));
147
+ compressor.on("end", () => resolve(Buffer.concat(chunks)));
148
+ compressor.on("error", reject);
149
+
150
+ compressor.write(data);
151
+ compressor.end();
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Compression middleware function
157
+ * @param {AetherContext} context - AetherJS execution context
158
+ * @param {Object} signal - Signal object or next function for flow control
159
+ */
160
+ return async function compressionMiddleware(context, signal) {
161
+ // Safe invoker for compatible pipeline flow control
162
+ const invokeNext = async () => {
163
+ if (signal && typeof signal.next === "function") {
164
+ await signal.next();
165
+ } else if (typeof signal === "function") {
166
+ await signal();
167
+ }
168
+ };
169
+
170
+ if (!config.enabled) {
171
+ return await invokeNext();
172
+ }
173
+
174
+ // Store original finalize method
175
+ const originalize = context._finalize;
176
+
177
+ // Override finalize to add compression
178
+ context._finalize = async function () {
179
+ if (this._terminated) return;
180
+
181
+ const body = this._body;
182
+
183
+ // Safe fallback logic for grabbing the outbound content type
184
+ let contentType = "";
185
+ if (typeof this.getHeader === "function") {
186
+ contentType = this.getHeader("content-type") || "";
187
+ } else if (this._headers && typeof this._headers.get === "function") {
188
+ contentType = this._headers.get("content-type") || "";
189
+ }
190
+
191
+ // Check if compression should be applied
192
+ if (
193
+ !body ||
194
+ (!Buffer.isBuffer(body) && typeof body !== "string") ||
195
+ Buffer.byteLength(body) < config.threshold ||
196
+ !config.filter(contentType)
197
+ ) {
198
+ return originalize.call(this);
199
+ }
200
+
201
+ // Get accepted encodings
202
+ const acceptEncoding =
203
+ (typeof this.getHeader === "function"
204
+ ? this.getHeader("accept-encoding")
205
+ : "") || "";
206
+ let encoding = null;
207
+
208
+ // Determine best compression algorithm
209
+ if (config.gzip && acceptEncoding.includes("gzip")) {
210
+ encoding = "gzip";
211
+ } else if (config.deflate && acceptEncoding.includes("deflate")) {
212
+ encoding = "deflate";
213
+ } else if (config.brotli && acceptEncoding.includes("br")) {
214
+ encoding = "br";
215
+ }
216
+
217
+ if (!encoding) {
218
+ return originalize.call(this);
219
+ }
220
+
221
+ try {
222
+ // Convert body to buffer if needed
223
+ const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
224
+
225
+ // Compress data
226
+ const compressed = await compressData(bodyBuffer, encoding);
227
+
228
+ // Update response
229
+ this._body = compressed;
230
+ if (typeof this.setHeader === "function") {
231
+ this.setHeader("content-encoding", encoding);
232
+ this.setHeader("vary", "Accept-Encoding");
233
+ }
234
+
235
+ // Call original finalize
236
+ return originalize.call(this);
237
+ } catch (error) {
238
+ // Compression failed, use original body
239
+ console.error("Compression error:", error);
240
+ return originalize.call(this);
241
+ }
242
+ };
243
+
244
+ await invokeNext();
245
+ };
246
+ }
247
+
248
+ export default createCompressionMiddleware;
@@ -0,0 +1,162 @@
1
+
2
+ /**
3
+ * @license MIT
4
+ * Copyright (c) 2026-present AetherFramework Contributors.
5
+ * SPDX-License-Identifier: MIT
6
+ * @module @aetherframework/middleware/middleware/cors.js
7
+ */
8
+
9
+ function parseOrigin(origin) {
10
+ if (origin === "*") {
11
+ return (requestOrigin) => "*";
12
+ }
13
+
14
+ if (typeof origin === "string") {
15
+ const origins = origin
16
+ .split(",")
17
+ .map((o) => o.trim())
18
+ .filter(Boolean);
19
+ if (origins.length === 1) {
20
+ const singleOrigin = origins[0];
21
+ return (requestOrigin) => {
22
+ if (singleOrigin === "*") return "*";
23
+ return requestOrigin === singleOrigin ? requestOrigin : null;
24
+ };
25
+ }
26
+ return (requestOrigin) =>
27
+ origins.includes(requestOrigin) ? requestOrigin : null;
28
+ }
29
+
30
+ if (Array.isArray(origin)) {
31
+ return (requestOrigin) =>
32
+ origin.includes(requestOrigin) ? requestOrigin : null;
33
+ }
34
+
35
+ if (typeof origin === "function") {
36
+ return origin;
37
+ }
38
+
39
+ return () => null;
40
+ }
41
+
42
+ function createCorsMiddleware(options = {}) {
43
+ const envConfig = {
44
+ enabled: process.env.CORS_ENABLED,
45
+ origin: process.env.CORS_ORIGIN,
46
+ methods: process.env.CORS_METHODS,
47
+ allowedHeaders: process.env.CORS_ALLOWED_HEADERS,
48
+ credentials: process.env.CORS_CREDENTIALS,
49
+ maxAge: process.env.CORS_MAX_AGE,
50
+ preflightContinue: process.env.CORS_PREFLIGHT_CONTINUE,
51
+ optionsSuccessStatus: process.env.CORS_OPTIONS_STATUS,
52
+ };
53
+
54
+ const defaults = {
55
+ enabled: envConfig.enabled !== "false",
56
+ origin: envConfig.origin || "*",
57
+ methods: envConfig.methods
58
+ ? envConfig.methods.split(",").map((m) => m.trim())
59
+ : ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
60
+ allowedHeaders: envConfig.allowedHeaders
61
+ ? envConfig.allowedHeaders.split(",").map((h) => h.trim())
62
+ : ["Content-Type", "Authorization"],
63
+ exposedHeaders: [],
64
+ credentials: envConfig.credentials === "true",
65
+ maxAge: envConfig.maxAge ? parseInt(envConfig.maxAge) : 86400,
66
+ preflightContinue: envConfig.preflightContinue === "true",
67
+ optionsSuccessStatus: envConfig.optionsSuccessStatus
68
+ ? parseInt(envConfig.optionsSuccessStatus)
69
+ : 204,
70
+ };
71
+
72
+ const config = { ...defaults, ...options };
73
+ const originValidator = parseOrigin(config.origin);
74
+ const staticHeaders = new Map();
75
+
76
+ if (config.methods && config.methods.length > 0) {
77
+ staticHeaders.set(
78
+ "access-control-allow-methods",
79
+ config.methods.join(", "),
80
+ );
81
+ }
82
+ if (config.allowedHeaders && config.allowedHeaders.length > 0) {
83
+ staticHeaders.set(
84
+ "access-control-allow-headers",
85
+ config.allowedHeaders.join(", "),
86
+ );
87
+ }
88
+ if (config.maxAge && config.maxAge > 0) {
89
+ staticHeaders.set("access-control-max-age", config.maxAge.toString());
90
+ }
91
+ if (config.exposedHeaders && config.exposedHeaders.length > 0) {
92
+ staticHeaders.set(
93
+ "access-control-expose-headers",
94
+ config.exposedHeaders.join(", "),
95
+ );
96
+ }
97
+
98
+ return async function corsMiddleware(context, signal) {
99
+ if (!config.enabled) {
100
+ return signal && signal.next
101
+ ? await signal.next()
102
+ : typeof signal === "function"
103
+ ? await signal()
104
+ : void 0;
105
+ }
106
+
107
+ const requestOrigin = context.getHeader("origin") || "";
108
+ let allowOrigin = originValidator(requestOrigin);
109
+
110
+ // 💡 Critical Fix: According to W3C specs, if credentials are enabled and origin is '*',
111
+ // we must dynamically mirror the incoming request origin instead of returning a literal '*'.
112
+ if (config.credentials && allowOrigin === "*") {
113
+ allowOrigin = requestOrigin || "*";
114
+ }
115
+
116
+ if (allowOrigin && allowOrigin !== null) {
117
+ context.setHeader("access-control-allow-origin", allowOrigin);
118
+
119
+ for (const [header, value] of staticHeaders) {
120
+ context.setHeader(header, value);
121
+ }
122
+
123
+ if (config.credentials) {
124
+ context.setHeader("access-control-allow-credentials", "true");
125
+ }
126
+
127
+ if (allowOrigin !== "*") {
128
+ const varyHeader = context.getHeader("vary");
129
+ const varyValues = varyHeader
130
+ ? varyHeader.split(",").map((v) => v.trim())
131
+ : [];
132
+ if (!varyValues.includes("Origin")) {
133
+ varyValues.push("Origin");
134
+ context.setHeader("vary", varyValues.join(", "));
135
+ }
136
+ }
137
+ }
138
+
139
+ // Handle CORS Preflight OPTIONS requests
140
+ if (context.method === "OPTIONS") {
141
+ if (!config.preflightContinue) {
142
+ context.setStatus(config.optionsSuccessStatus);
143
+ context.setHeader("content-length", "0");
144
+ if (typeof context.json === "function") {
145
+ context.json("");
146
+ } else {
147
+ context.raw("");
148
+ }
149
+ return; // Short-circuit the request pipeline instantly, bypassing signal.next()
150
+ }
151
+ }
152
+
153
+ // Backward compatibility: Supports either signal.next() or traditional functional next() dispatching
154
+ if (signal && typeof signal.next === "function") {
155
+ await signal.next();
156
+ } else if (typeof signal === "function") {
157
+ await signal();
158
+ }
159
+ };
160
+ }
161
+
162
+ export default createCorsMiddleware;