@tekir/cors 0.1.3 → 0.1.5
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 +4 -0
- package/dist/cors.js +60 -84
- package/package.json +1 -1
- package/src/cors.ts +61 -82
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']`.
|
|
@@ -38,19 +42,24 @@ const defaults = {
|
|
|
38
42
|
*/
|
|
39
43
|
export function cors(userConfig = {}) {
|
|
40
44
|
const cfg = { ...defaults, ...userConfig };
|
|
45
|
+
// Reflecting an arbitrary Origin with `Access-Control-Allow-Credentials: true`
|
|
46
|
+
// hands any site credentialed access to this origin's responses. Refuse the
|
|
47
|
+
// dangerous `origin: true` + `credentials: true` combination at construction
|
|
48
|
+
// time and require an explicit allowlist (string/array/function) instead.
|
|
49
|
+
if (cfg.credentials && cfg.origin === true) {
|
|
50
|
+
throw new Error('@tekir/cors: `credentials: true` cannot be combined with `origin: true`. ' +
|
|
51
|
+
'Reflecting every Origin with credentials enabled lets any site read authenticated responses. ' +
|
|
52
|
+
'Provide an explicit allowlist (string, string[], or a validator function).');
|
|
53
|
+
}
|
|
41
54
|
return async (ctx, next) => {
|
|
42
55
|
if (!cfg.enabled)
|
|
43
56
|
return next();
|
|
44
57
|
const origin = ctx.request.header('origin') || ctx.headers?.origin || '';
|
|
45
58
|
let allowOrigin = '';
|
|
46
59
|
if (cfg.origin === true) {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
allowOrigin = origin || '*';
|
|
53
|
-
}
|
|
60
|
+
// No credentials here (the credentials+true combo is rejected above), so
|
|
61
|
+
// the wildcard is safe. Reflect the request origin when present, else `*`.
|
|
62
|
+
allowOrigin = origin || '*';
|
|
54
63
|
}
|
|
55
64
|
else if (cfg.origin === false) {
|
|
56
65
|
allowOrigin = '';
|
|
@@ -59,95 +68,62 @@ export function cors(userConfig = {}) {
|
|
|
59
68
|
allowOrigin = cfg.origin;
|
|
60
69
|
}
|
|
61
70
|
else if (Array.isArray(cfg.origin)) {
|
|
62
|
-
|
|
63
|
-
allowOrigin = cfg.origin.
|
|
71
|
+
// RFC 6454 origins are compared exactly (case-sensitive scheme/host).
|
|
72
|
+
allowOrigin = cfg.origin.includes(origin) ? origin : '';
|
|
64
73
|
}
|
|
65
74
|
else if (typeof cfg.origin === 'function') {
|
|
66
75
|
allowOrigin = cfg.origin(origin) ? origin : '';
|
|
67
76
|
}
|
|
77
|
+
// A `null` origin (sandboxed iframes, data:/file: schemes) must never be
|
|
78
|
+
// trusted with credentials — it is not bound to any real site.
|
|
79
|
+
if (cfg.credentials && allowOrigin === 'null')
|
|
80
|
+
return next();
|
|
68
81
|
if (!allowOrigin)
|
|
69
82
|
return next();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|
|
83
|
+
// Stash the negotiated CORS headers on ctx so the framework merges them
|
|
84
|
+
// onto whatever response goes out. Writing here (instead of mutating
|
|
85
|
+
// `ctx.$result` after `next()`) makes CORS ordering-independent: it
|
|
86
|
+
// works whether `cors()` sits before or after error handlers and slots
|
|
87
|
+
// headers onto framework-handled 404s and 500s alike.
|
|
88
|
+
const headers = (ctx.$responseHeaders ??= new Headers());
|
|
89
|
+
headers.set('Access-Control-Allow-Origin', allowOrigin);
|
|
129
90
|
if (cfg.credentials)
|
|
130
|
-
|
|
91
|
+
headers.set('Access-Control-Allow-Credentials', 'true');
|
|
131
92
|
if (cfg.exposeHeaders?.length)
|
|
132
|
-
|
|
93
|
+
headers.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '));
|
|
133
94
|
// Vary: Origin keeps caches honest when the allow-origin is request-derived.
|
|
134
|
-
const existingVary =
|
|
95
|
+
const existingVary = headers.get('Vary');
|
|
135
96
|
if (!existingVary) {
|
|
136
|
-
|
|
97
|
+
headers.set('Vary', 'Origin');
|
|
137
98
|
}
|
|
138
99
|
else if (!existingVary.split(',').map(s => s.trim().toLowerCase()).includes('origin')) {
|
|
139
|
-
|
|
100
|
+
headers.set('Vary', `${existingVary}, Origin`);
|
|
101
|
+
}
|
|
102
|
+
const method = ctx.request?.method || ctx.request?.raw?.method || '';
|
|
103
|
+
if (method === 'OPTIONS') {
|
|
104
|
+
// Skip empty header values: an empty Allow-Methods/Allow-Headers silently
|
|
105
|
+
// breaks the preflight instead of leaving the browser's defaults in place.
|
|
106
|
+
if (cfg.methods?.length)
|
|
107
|
+
headers.set('Access-Control-Allow-Methods', cfg.methods.join(', '));
|
|
108
|
+
let allowHeaders = '';
|
|
109
|
+
if (cfg.headers === true) {
|
|
110
|
+
// `headers: true` reflects the requested headers. With credentials we
|
|
111
|
+
// must echo the explicit list (a `*` is invalid for credentialed
|
|
112
|
+
// requests and would fail the preflight), never a wildcard.
|
|
113
|
+
const requested = ctx.request.header('access-control-request-headers') || '';
|
|
114
|
+
allowHeaders = cfg.credentials ? requested : (requested || '*');
|
|
115
|
+
}
|
|
116
|
+
else if (Array.isArray(cfg.headers)) {
|
|
117
|
+
allowHeaders = cfg.headers.join(', ');
|
|
118
|
+
}
|
|
119
|
+
if (allowHeaders)
|
|
120
|
+
headers.set('Access-Control-Allow-Headers', allowHeaders);
|
|
121
|
+
if (cfg.maxAge)
|
|
122
|
+
headers.set('Access-Control-Max-Age', String(cfg.maxAge));
|
|
123
|
+
// Short-circuit the chain. Framework merges $responseHeaders onto this
|
|
124
|
+
// bare 204, so the preflight response carries all the negotiated bits.
|
|
125
|
+
return new Response(null, { status: 204 });
|
|
140
126
|
}
|
|
141
|
-
|
|
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;
|
|
127
|
+
await next();
|
|
152
128
|
};
|
|
153
129
|
}
|
package/package.json
CHANGED
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']`.
|
|
@@ -42,6 +46,18 @@ const defaults: CorsConfig = {
|
|
|
42
46
|
export function cors(userConfig: CorsConfig = {}) {
|
|
43
47
|
const cfg = { ...defaults, ...userConfig }
|
|
44
48
|
|
|
49
|
+
// Reflecting an arbitrary Origin with `Access-Control-Allow-Credentials: true`
|
|
50
|
+
// hands any site credentialed access to this origin's responses. Refuse the
|
|
51
|
+
// dangerous `origin: true` + `credentials: true` combination at construction
|
|
52
|
+
// time and require an explicit allowlist (string/array/function) instead.
|
|
53
|
+
if (cfg.credentials && cfg.origin === true) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'@tekir/cors: `credentials: true` cannot be combined with `origin: true`. ' +
|
|
56
|
+
'Reflecting every Origin with credentials enabled lets any site read authenticated responses. ' +
|
|
57
|
+
'Provide an explicit allowlist (string, string[], or a validator function).'
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
return async (ctx: any, next: () => Promise<void>) => {
|
|
46
62
|
if (!cfg.enabled) return next()
|
|
47
63
|
|
|
@@ -49,104 +65,67 @@ export function cors(userConfig: CorsConfig = {}) {
|
|
|
49
65
|
|
|
50
66
|
let allowOrigin = ''
|
|
51
67
|
if (cfg.origin === true) {
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} else {
|
|
56
|
-
allowOrigin = origin || '*'
|
|
57
|
-
}
|
|
68
|
+
// No credentials here (the credentials+true combo is rejected above), so
|
|
69
|
+
// the wildcard is safe. Reflect the request origin when present, else `*`.
|
|
70
|
+
allowOrigin = origin || '*'
|
|
58
71
|
} else if (cfg.origin === false) {
|
|
59
72
|
allowOrigin = ''
|
|
60
73
|
} else if (typeof cfg.origin === 'string') {
|
|
61
74
|
allowOrigin = cfg.origin
|
|
62
75
|
} else if (Array.isArray(cfg.origin)) {
|
|
63
|
-
|
|
64
|
-
allowOrigin = cfg.origin.
|
|
76
|
+
// RFC 6454 origins are compared exactly (case-sensitive scheme/host).
|
|
77
|
+
allowOrigin = cfg.origin.includes(origin) ? origin : ''
|
|
65
78
|
} else if (typeof cfg.origin === 'function') {
|
|
66
79
|
allowOrigin = cfg.origin(origin) ? origin : ''
|
|
67
80
|
}
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
82
|
+
// A `null` origin (sandboxed iframes, data:/file: schemes) must never be
|
|
83
|
+
// trusted with credentials — it is not bound to any real site.
|
|
84
|
+
if (cfg.credentials && allowOrigin === 'null') return next()
|
|
99
85
|
|
|
100
|
-
|
|
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
|
-
}
|
|
86
|
+
if (!allowOrigin) return next()
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
88
|
+
// Stash the negotiated CORS headers on ctx so the framework merges them
|
|
89
|
+
// onto whatever response goes out. Writing here (instead of mutating
|
|
90
|
+
// `ctx.$result` after `next()`) makes CORS ordering-independent: it
|
|
91
|
+
// works whether `cors()` sits before or after error handlers and slots
|
|
92
|
+
// headers onto framework-handled 404s and 500s alike.
|
|
93
|
+
const headers: Headers = (ctx.$responseHeaders ??= new Headers())
|
|
94
|
+
headers.set('Access-Control-Allow-Origin', allowOrigin)
|
|
95
|
+
if (cfg.credentials) headers.set('Access-Control-Allow-Credentials', 'true')
|
|
96
|
+
if (cfg.exposeHeaders?.length) headers.set('Access-Control-Expose-Headers', cfg.exposeHeaders.join(', '))
|
|
132
97
|
// Vary: Origin keeps caches honest when the allow-origin is request-derived.
|
|
133
|
-
const existingVary =
|
|
98
|
+
const existingVary = headers.get('Vary')
|
|
134
99
|
if (!existingVary) {
|
|
135
|
-
|
|
100
|
+
headers.set('Vary', 'Origin')
|
|
136
101
|
} else if (!existingVary.split(',').map(s => s.trim().toLowerCase()).includes('origin')) {
|
|
137
|
-
|
|
102
|
+
headers.set('Vary', `${existingVary}, Origin`)
|
|
138
103
|
}
|
|
139
104
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
105
|
+
const method = ctx.request?.method || ctx.request?.raw?.method || ''
|
|
106
|
+
if (method === 'OPTIONS') {
|
|
107
|
+
// Skip empty header values: an empty Allow-Methods/Allow-Headers silently
|
|
108
|
+
// breaks the preflight instead of leaving the browser's defaults in place.
|
|
109
|
+
if (cfg.methods?.length) headers.set('Access-Control-Allow-Methods', cfg.methods.join(', '))
|
|
110
|
+
|
|
111
|
+
let allowHeaders = ''
|
|
112
|
+
if (cfg.headers === true) {
|
|
113
|
+
// `headers: true` reflects the requested headers. With credentials we
|
|
114
|
+
// must echo the explicit list (a `*` is invalid for credentialed
|
|
115
|
+
// requests and would fail the preflight), never a wildcard.
|
|
116
|
+
const requested = ctx.request.header('access-control-request-headers') || ''
|
|
117
|
+
allowHeaders = cfg.credentials ? requested : (requested || '*')
|
|
118
|
+
} else if (Array.isArray(cfg.headers)) {
|
|
119
|
+
allowHeaders = cfg.headers.join(', ')
|
|
120
|
+
}
|
|
121
|
+
if (allowHeaders) headers.set('Access-Control-Allow-Headers', allowHeaders)
|
|
122
|
+
|
|
123
|
+
if (cfg.maxAge) headers.set('Access-Control-Max-Age', String(cfg.maxAge))
|
|
124
|
+
// Short-circuit the chain. Framework merges $responseHeaders onto this
|
|
125
|
+
// bare 204, so the preflight response carries all the negotiated bits.
|
|
126
|
+
return new Response(null, { status: 204 })
|
|
127
|
+
}
|
|
145
128
|
|
|
146
|
-
|
|
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
|
|
129
|
+
await next()
|
|
151
130
|
}
|
|
152
131
|
}
|