@tekir/cors 0.1.3 → 0.1.4

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.
package/dist/cors.d.ts CHANGED
@@ -4,6 +4,10 @@ import type { CorsConfig } from './types';
4
4
  * Supports wildcard, array, string, and function-based origin validation.
5
5
  * When `credentials: true` with `origin: true`, reflects the request origin instead of using `*`.
6
6
  *
7
+ * Headers are written to `ctx.$responseHeaders` so the framework merges them
8
+ * onto the outgoing response right before it goes on the wire, regardless of
9
+ * which middleware built the response or where in the chain CORS sits.
10
+ *
7
11
  * @param userConfig - CORS configuration options.
8
12
  * @param userConfig.origin - Allowed origins: `true` (all), `false` (none), `string`, `string[]`, or `(origin) => boolean`.
9
13
  * @param userConfig.methods - Allowed HTTP methods. Defaults to `['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']`.
package/dist/cors.js CHANGED
@@ -11,6 +11,10 @@ const defaults = {
11
11
  * Supports wildcard, array, string, and function-based origin validation.
12
12
  * When `credentials: true` with `origin: true`, reflects the request origin instead of using `*`.
13
13
  *
14
+ * Headers are written to `ctx.$responseHeaders` so the framework merges them
15
+ * onto the outgoing response right before it goes on the wire, regardless of
16
+ * which middleware built the response or where in the chain CORS sits.
17
+ *
14
18
  * @param userConfig - CORS configuration options.
15
19
  * @param userConfig.origin - Allowed origins: `true` (all), `false` (none), `string`, `string[]`, or `(origin) => boolean`.
16
20
  * @param userConfig.methods - Allowed HTTP methods. Defaults to `['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']`.
@@ -44,7 +48,7 @@ export function cors(userConfig = {}) {
44
48
  const origin = ctx.request.header('origin') || ctx.headers?.origin || '';
45
49
  let allowOrigin = '';
46
50
  if (cfg.origin === true) {
47
- // When credentials are enabled, never use wildcard reflect the request origin instead
51
+ // When credentials are enabled, never use wildcard, reflect the request origin instead.
48
52
  if (cfg.credentials && origin) {
49
53
  allowOrigin = origin;
50
54
  }
@@ -67,87 +71,37 @@ export function cors(userConfig = {}) {
67
71
  }
68
72
  if (!allowOrigin)
69
73
  return next();
70
- const method = ctx.request?.method || ctx.request?.raw?.method || '';
71
- if (method === 'OPTIONS') {
72
- return new Response(null, {
73
- status: 204,
74
- headers: {
75
- 'Access-Control-Allow-Origin': allowOrigin,
76
- 'Access-Control-Allow-Methods': (cfg.methods || []).join(', '),
77
- 'Access-Control-Allow-Headers': cfg.headers === true
78
- ? ctx.request.header('access-control-request-headers') || '*'
79
- : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '',
80
- ...(cfg.credentials ? { 'Access-Control-Allow-Credentials': 'true' } : {}),
81
- ...(cfg.maxAge ? { 'Access-Control-Max-Age': String(cfg.maxAge) } : {}),
82
- ...(cfg.exposeHeaders?.length ? { 'Access-Control-Expose-Headers': cfg.exposeHeaders.join(', ') } : {}),
83
- 'Vary': 'Origin',
84
- },
85
- });
86
- }
87
- // Capture any throw from `next()` so the CORS merge below still runs
88
- // even when an inner middleware caught the error and set
89
- // `ctx.$result` before re-throwing. Without this guard the browser
90
- // sees a "CORS error" on every error response.
91
- let nextError;
92
- try {
93
- await next();
94
- }
95
- catch (err) {
96
- nextError = err;
97
- }
98
- // When `next()` threw and nothing along the way set `$result`, do not
99
- // synthesize one here. An outer error-handling middleware is the
100
- // natural place to build the error response, and tekir's chain only
101
- // adopts a middleware's return value when `$result` is still
102
- // undefined; coercing to a 204 here would silently win over the real
103
- // error response and the client would see "CORS-OK 204" instead of a
104
- // 401/500. The throw still propagates, so the outer handler runs.
105
- const result = ctx.$result;
106
- if (nextError !== undefined && result == null) {
107
- throw nextError;
108
- }
109
- // Inject CORS headers into whatever the chain produced. Routes return
110
- // a mix of Response objects (SSE streams, file downloads, custom
111
- // payloads) and plain values that tekir wraps automatically. Coerce
112
- // both shapes into a Response here so the headers always land on the
113
- // wire.
114
- let response;
115
- if (result instanceof Response) {
116
- response = result;
117
- }
118
- else if (result == null) {
119
- response = new Response(null, { status: 204 });
120
- }
121
- else if (typeof result === 'object') {
122
- response = Response.json(result);
123
- }
124
- else {
125
- response = new Response(String(result));
126
- }
127
- const merged = new Headers(response.headers);
128
- merged.set('Access-Control-Allow-Origin', allowOrigin);
74
+ // Stash the negotiated CORS headers on ctx so the framework merges them
75
+ // onto whatever response goes out. Writing here (instead of mutating
76
+ // `ctx.$result` after `next()`) makes CORS ordering-independent: it
77
+ // works whether `cors()` sits before or after error handlers and slots
78
+ // headers onto framework-handled 404s and 500s alike.
79
+ const headers = (ctx.$responseHeaders ??= new Headers());
80
+ headers.set('Access-Control-Allow-Origin', allowOrigin);
129
81
  if (cfg.credentials)
130
- merged.set('Access-Control-Allow-Credentials', 'true');
82
+ headers.set('Access-Control-Allow-Credentials', 'true');
131
83
  if (cfg.exposeHeaders?.length)
132
- merged.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '));
84
+ headers.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '));
133
85
  // Vary: Origin keeps caches honest when the allow-origin is request-derived.
134
- const existingVary = merged.get('Vary');
86
+ const existingVary = headers.get('Vary');
135
87
  if (!existingVary) {
136
- merged.set('Vary', 'Origin');
88
+ headers.set('Vary', 'Origin');
137
89
  }
138
90
  else if (!existingVary.split(',').map(s => s.trim().toLowerCase()).includes('origin')) {
139
- merged.set('Vary', `${existingVary}, Origin`);
91
+ headers.set('Vary', `${existingVary}, Origin`);
92
+ }
93
+ const method = ctx.request?.method || ctx.request?.raw?.method || '';
94
+ if (method === 'OPTIONS') {
95
+ headers.set('Access-Control-Allow-Methods', (cfg.methods || []).join(', '));
96
+ headers.set('Access-Control-Allow-Headers', cfg.headers === true
97
+ ? ctx.request.header('access-control-request-headers') || '*'
98
+ : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '');
99
+ if (cfg.maxAge)
100
+ headers.set('Access-Control-Max-Age', String(cfg.maxAge));
101
+ // Short-circuit the chain. Framework merges $responseHeaders onto this
102
+ // bare 204, so the preflight response carries all the negotiated bits.
103
+ return new Response(null, { status: 204 });
140
104
  }
141
- ctx.$result = new Response(response.body, {
142
- status: response.status,
143
- statusText: response.statusText,
144
- headers: merged,
145
- });
146
- // Re-throw so outer error handlers and loggers still see the original
147
- // error. `$result` already carries the merged CORS headers; an outer
148
- // handler that builds its own response can copy them off the existing
149
- // one if it cares to.
150
- if (nextError !== undefined)
151
- throw nextError;
105
+ await next();
152
106
  };
153
107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekir/cors",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CORS middleware for cross-origin requests",
5
5
  "author": "dev@tekir.io",
6
6
  "license": "MIT",
package/src/cors.ts CHANGED
@@ -14,6 +14,10 @@ const defaults: CorsConfig = {
14
14
  * Supports wildcard, array, string, and function-based origin validation.
15
15
  * When `credentials: true` with `origin: true`, reflects the request origin instead of using `*`.
16
16
  *
17
+ * Headers are written to `ctx.$responseHeaders` so the framework merges them
18
+ * onto the outgoing response right before it goes on the wire, regardless of
19
+ * which middleware built the response or where in the chain CORS sits.
20
+ *
17
21
  * @param userConfig - CORS configuration options.
18
22
  * @param userConfig.origin - Allowed origins: `true` (all), `false` (none), `string`, `string[]`, or `(origin) => boolean`.
19
23
  * @param userConfig.methods - Allowed HTTP methods. Defaults to `['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']`.
@@ -49,7 +53,7 @@ export function cors(userConfig: CorsConfig = {}) {
49
53
 
50
54
  let allowOrigin = ''
51
55
  if (cfg.origin === true) {
52
- // When credentials are enabled, never use wildcard reflect the request origin instead
56
+ // When credentials are enabled, never use wildcard, reflect the request origin instead.
53
57
  if (cfg.credentials && origin) {
54
58
  allowOrigin = origin
55
59
  } else {
@@ -68,85 +72,35 @@ export function cors(userConfig: CorsConfig = {}) {
68
72
 
69
73
  if (!allowOrigin) return next()
70
74
 
71
- const method = ctx.request?.method || ctx.request?.raw?.method || ''
72
- if (method === 'OPTIONS') {
73
- return new Response(null, {
74
- status: 204,
75
- headers: {
76
- 'Access-Control-Allow-Origin': allowOrigin,
77
- 'Access-Control-Allow-Methods': (cfg.methods || []).join(', '),
78
- 'Access-Control-Allow-Headers': cfg.headers === true
79
- ? ctx.request.header('access-control-request-headers') || '*'
80
- : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '',
81
- ...(cfg.credentials ? { 'Access-Control-Allow-Credentials': 'true' } : {}),
82
- ...(cfg.maxAge ? { 'Access-Control-Max-Age': String(cfg.maxAge) } : {}),
83
- ...(cfg.exposeHeaders?.length ? { 'Access-Control-Expose-Headers': cfg.exposeHeaders.join(', ') } : {}),
84
- 'Vary': 'Origin',
85
- },
86
- })
87
- }
88
-
89
- // Capture any throw from `next()` so the CORS merge below still runs
90
- // even when an inner middleware caught the error and set
91
- // `ctx.$result` before re-throwing. Without this guard the browser
92
- // sees a "CORS error" on every error response.
93
- let nextError: unknown
94
- try {
95
- await next()
96
- } catch (err) {
97
- nextError = err
98
- }
99
-
100
- // When `next()` threw and nothing along the way set `$result`, do not
101
- // synthesize one here. An outer error-handling middleware is the
102
- // natural place to build the error response, and tekir's chain only
103
- // adopts a middleware's return value when `$result` is still
104
- // undefined; coercing to a 204 here would silently win over the real
105
- // error response and the client would see "CORS-OK 204" instead of a
106
- // 401/500. The throw still propagates, so the outer handler runs.
107
- const result = ctx.$result
108
- if (nextError !== undefined && result == null) {
109
- throw nextError
110
- }
111
-
112
- // Inject CORS headers into whatever the chain produced. Routes return
113
- // a mix of Response objects (SSE streams, file downloads, custom
114
- // payloads) and plain values that tekir wraps automatically. Coerce
115
- // both shapes into a Response here so the headers always land on the
116
- // wire.
117
- let response: Response
118
- if (result instanceof Response) {
119
- response = result
120
- } else if (result == null) {
121
- response = new Response(null, { status: 204 })
122
- } else if (typeof result === 'object') {
123
- response = Response.json(result)
124
- } else {
125
- response = new Response(String(result))
126
- }
127
-
128
- const merged = new Headers(response.headers)
129
- merged.set('Access-Control-Allow-Origin', allowOrigin)
130
- if (cfg.credentials) merged.set('Access-Control-Allow-Credentials', 'true')
131
- if (cfg.exposeHeaders?.length) merged.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '))
75
+ // Stash the negotiated CORS headers on ctx so the framework merges them
76
+ // onto whatever response goes out. Writing here (instead of mutating
77
+ // `ctx.$result` after `next()`) makes CORS ordering-independent: it
78
+ // works whether `cors()` sits before or after error handlers and slots
79
+ // headers onto framework-handled 404s and 500s alike.
80
+ const headers: Headers = (ctx.$responseHeaders ??= new Headers())
81
+ headers.set('Access-Control-Allow-Origin', allowOrigin)
82
+ if (cfg.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
83
+ if (cfg.exposeHeaders?.length) headers.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '))
132
84
  // Vary: Origin keeps caches honest when the allow-origin is request-derived.
133
- const existingVary = merged.get('Vary')
85
+ const existingVary = headers.get('Vary')
134
86
  if (!existingVary) {
135
- merged.set('Vary', 'Origin')
87
+ headers.set('Vary', 'Origin')
136
88
  } else if (!existingVary.split(',').map(s => s.trim().toLowerCase()).includes('origin')) {
137
- merged.set('Vary', `${existingVary}, Origin`)
89
+ headers.set('Vary', `${existingVary}, Origin`)
138
90
  }
139
91
 
140
- ctx.$result = new Response(response.body, {
141
- status: response.status,
142
- statusText: response.statusText,
143
- headers: merged,
144
- })
92
+ const method = ctx.request?.method || ctx.request?.raw?.method || ''
93
+ if (method === 'OPTIONS') {
94
+ headers.set('Access-Control-Allow-Methods', (cfg.methods || []).join(', '))
95
+ headers.set('Access-Control-Allow-Headers', cfg.headers === true
96
+ ? ctx.request.header('access-control-request-headers') || '*'
97
+ : Array.isArray(cfg.headers) ? cfg.headers.join(', ') : '')
98
+ if (cfg.maxAge) headers.set('Access-Control-Max-Age', String(cfg.maxAge))
99
+ // Short-circuit the chain. Framework merges $responseHeaders onto this
100
+ // bare 204, so the preflight response carries all the negotiated bits.
101
+ return new Response(null, { status: 204 })
102
+ }
145
103
 
146
- // Re-throw so outer error handlers and loggers still see the original
147
- // error. `$result` already carries the merged CORS headers; an outer
148
- // handler that builds its own response can copy them off the existing
149
- // one if it cares to.
150
- if (nextError !== undefined) throw nextError
104
+ await next()
151
105
  }
152
106
  }