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