@remix-run/cors-middleware 0.0.0 → 0.1.1
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/LICENSE +21 -0
- package/README.md +173 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/cors.d.ts +100 -0
- package/dist/lib/cors.d.ts.map +1 -0
- package/dist/lib/cors.js +218 -0
- package/package.json +45 -5
- package/src/index.ts +9 -0
- package/src/lib/cors.ts +406 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shopify Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cors-middleware
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CORS middleware for Remix. It adds standard CORS response headers to Fetch API servers and can either short-circuit preflight requests or pass them through to app-defined `OPTIONS` handlers.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Preflight Handling** - Automatically handles `OPTIONS` preflight requests
|
|
8
|
+
- **Flexible Origin Rules** - Supports static, regex, list, and function-based origin policies
|
|
9
|
+
- **Credential Support** - Supports credentialed requests with spec-safe origin reflection
|
|
10
|
+
- **Header Controls** - Configure allowed and exposed headers, preflight methods, and max age
|
|
11
|
+
- **Private Network Support** - Optionally allow private network preflight requests
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm i remix
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createRouter } from 'remix/fetch-router'
|
|
23
|
+
import { cors } from 'remix/cors-middleware'
|
|
24
|
+
|
|
25
|
+
let router = createRouter({
|
|
26
|
+
middleware: [
|
|
27
|
+
cors({
|
|
28
|
+
origin: ['https://app.example.com', 'https://admin.example.com'],
|
|
29
|
+
credentials: true,
|
|
30
|
+
exposedHeaders: ['X-Request-Id'],
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
router.get('/api/projects', () => {
|
|
36
|
+
return Response.json([{ id: 'p1', name: 'Remix' }], {
|
|
37
|
+
headers: {
|
|
38
|
+
'X-Request-Id': 'req_123',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Origin Policies
|
|
45
|
+
|
|
46
|
+
`origin` supports:
|
|
47
|
+
|
|
48
|
+
- `'*'` to allow all origins
|
|
49
|
+
- `string` for a single exact origin
|
|
50
|
+
- `RegExp` for pattern-based matching
|
|
51
|
+
- `Array<string | RegExp>` for multiple exact and pattern matches
|
|
52
|
+
- `true` to reflect the request origin
|
|
53
|
+
- `(origin, context) => boolean | string` for dynamic policies
|
|
54
|
+
|
|
55
|
+
### Restrict Origins
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
let router = createRouter({
|
|
59
|
+
middleware: [
|
|
60
|
+
cors({
|
|
61
|
+
origin: ['https://app.example.com', 'https://admin.example.com'],
|
|
62
|
+
credentials: true,
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Dynamic Origin Policies
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
let router = createRouter({
|
|
72
|
+
middleware: [
|
|
73
|
+
cors({
|
|
74
|
+
origin(origin, context) {
|
|
75
|
+
if (context.url.pathname.startsWith('/public/')) {
|
|
76
|
+
return '*'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return origin.endsWith('.trusted.example')
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Preflight Behavior
|
|
87
|
+
|
|
88
|
+
By default, preflight requests are short-circuited with status `204`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
let router = createRouter({
|
|
92
|
+
middleware: [
|
|
93
|
+
cors({
|
|
94
|
+
methods: ['GET', 'POST', 'PATCH'],
|
|
95
|
+
allowedHeaders: ['Authorization', 'Content-Type'],
|
|
96
|
+
maxAge: 600,
|
|
97
|
+
}),
|
|
98
|
+
],
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Use a function-based `allowedHeaders` policy when the header allowlist depends on the request:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
let router = createRouter({
|
|
106
|
+
middleware: [
|
|
107
|
+
cors({
|
|
108
|
+
allowedHeaders(request) {
|
|
109
|
+
let requestedHeaders = request.headers.get('Access-Control-Request-Headers')
|
|
110
|
+
|
|
111
|
+
if (requestedHeaders?.includes('x-admin-token')) {
|
|
112
|
+
return ['Authorization', 'Content-Type', 'X-Admin-Token']
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return ['Authorization', 'Content-Type']
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
],
|
|
119
|
+
})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Function-based `allowedHeaders` responses vary on `Access-Control-Request-Headers`, so caches do not reuse a preflight response for a different requested-header set.
|
|
123
|
+
|
|
124
|
+
Set `preflightContinue: true` to let downstream handlers process preflight requests. Use `preflightStatusCode` when you want short-circuited preflight responses to return a status other than `204`.
|
|
125
|
+
|
|
126
|
+
## Private Network Preflights
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
let router = createRouter({
|
|
130
|
+
middleware: [
|
|
131
|
+
cors({
|
|
132
|
+
allowPrivateNetwork: true,
|
|
133
|
+
}),
|
|
134
|
+
],
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When `allowPrivateNetwork` is enabled, the middleware adds `Access-Control-Allow-Private-Network: true` for preflight requests that ask for private network access.
|
|
139
|
+
|
|
140
|
+
## Expose Response Headers
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
let router = createRouter({
|
|
144
|
+
middleware: [
|
|
145
|
+
cors({
|
|
146
|
+
exposedHeaders: ['X-Request-Id', 'X-Trace-Id'],
|
|
147
|
+
}),
|
|
148
|
+
],
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Caveats
|
|
153
|
+
|
|
154
|
+
- CORS is primarily a browser enforcement mechanism. Disallowed non-preflight requests still reach your handlers unless you add separate request validation.
|
|
155
|
+
- When `credentials: true` is used with `origin: '*'`, the middleware reflects the request origin and adds `Vary: Origin` so the response stays cache-safe.
|
|
156
|
+
- When `allowedHeaders` is a function, preflight responses vary on `Access-Control-Request-Headers` so caches do not reuse a response for a different requested-header set.
|
|
157
|
+
- `preflightContinue` and `preflightStatusCode` only affect how preflight `OPTIONS` requests are handled. They do not change actual request authorization.
|
|
158
|
+
|
|
159
|
+
## Related Packages
|
|
160
|
+
|
|
161
|
+
- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - Browser-origin protection middleware for unsafe cross-origin requests
|
|
162
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
|
|
163
|
+
- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Typed HTTP header utilities
|
|
164
|
+
|
|
165
|
+
## Related Work
|
|
166
|
+
|
|
167
|
+
- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
|
168
|
+
- [Fetch Standard: CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol)
|
|
169
|
+
- [expressjs/cors](https://github.com/expressjs/cors)
|
|
170
|
+
- [rack-cors](https://github.com/cyu/rack-cors)
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,KAAK,0BAA0B,EAC/B,KAAK,gCAAgC,EACrC,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,wBAAwB,GAC9B,MAAM,eAAe,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cors, } from "./lib/cors.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Middleware, RequestContext } from '@remix-run/fetch-router';
|
|
2
|
+
type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>;
|
|
3
|
+
/**
|
|
4
|
+
* Return shape for a dynamic CORS origin resolver.
|
|
5
|
+
*/
|
|
6
|
+
export type CorsOriginResolverResult = '*' | string | boolean | null | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the allowed origin for a given request origin.
|
|
9
|
+
*/
|
|
10
|
+
export interface CorsOriginResolver {
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the allowed origin for a request with an `Origin` header.
|
|
13
|
+
*/
|
|
14
|
+
(origin: string, context: RequestContext): CorsOriginResolverResult | Promise<CorsOriginResolverResult>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Accepted forms for configuring allowed CORS origins.
|
|
18
|
+
*/
|
|
19
|
+
export type CorsOrigin = OriginMatcher | boolean | CorsOriginResolver;
|
|
20
|
+
/**
|
|
21
|
+
* Return shape for a dynamic allowed-headers resolver.
|
|
22
|
+
*/
|
|
23
|
+
export type CorsAllowedHeadersResolverResult = readonly string[] | null | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the allowed request headers for a preflight request.
|
|
26
|
+
*/
|
|
27
|
+
export interface CorsAllowedHeadersResolver {
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the request headers allowed by a preflight request.
|
|
30
|
+
*/
|
|
31
|
+
(request: Request, context: RequestContext): CorsAllowedHeadersResolverResult | Promise<CorsAllowedHeadersResolverResult>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Options for CORS middleware.
|
|
35
|
+
*/
|
|
36
|
+
export interface CorsOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Allowed origins. Defaults to '*'.
|
|
39
|
+
*
|
|
40
|
+
* - `true` reflects the request Origin
|
|
41
|
+
* - `false` disables CORS headers
|
|
42
|
+
* - `'*'` allows all origins
|
|
43
|
+
* - `string`/`RegExp`/array allow matching origins
|
|
44
|
+
* - `function` allows dynamic origin checks
|
|
45
|
+
*/
|
|
46
|
+
origin?: CorsOrigin;
|
|
47
|
+
/**
|
|
48
|
+
* Allowed methods for preflight responses.
|
|
49
|
+
*
|
|
50
|
+
* @default ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']
|
|
51
|
+
*/
|
|
52
|
+
methods?: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Allowed request headers for preflight responses.
|
|
55
|
+
*
|
|
56
|
+
* Defaults to reflecting Access-Control-Request-Headers.
|
|
57
|
+
*/
|
|
58
|
+
allowedHeaders?: readonly string[] | CorsAllowedHeadersResolver;
|
|
59
|
+
/**
|
|
60
|
+
* Exposed response headers for non-preflight requests.
|
|
61
|
+
*/
|
|
62
|
+
exposedHeaders?: readonly string[];
|
|
63
|
+
/**
|
|
64
|
+
* Include Access-Control-Allow-Credentials: true.
|
|
65
|
+
*
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
credentials?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Access-Control-Max-Age value for preflight responses (seconds).
|
|
71
|
+
*/
|
|
72
|
+
maxAge?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Continue to downstream handlers for preflight requests.
|
|
75
|
+
*
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
preflightContinue?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Status code to use when short-circuiting preflight responses.
|
|
81
|
+
*
|
|
82
|
+
* @default 204
|
|
83
|
+
*/
|
|
84
|
+
preflightStatusCode?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Include Access-Control-Allow-Private-Network: true when requested.
|
|
87
|
+
*
|
|
88
|
+
* @default false
|
|
89
|
+
*/
|
|
90
|
+
allowPrivateNetwork?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Middleware that adds CORS headers and handles CORS preflight requests.
|
|
94
|
+
*
|
|
95
|
+
* @param options CORS options
|
|
96
|
+
* @returns CORS middleware
|
|
97
|
+
*/
|
|
98
|
+
export declare function cors(options?: CorsOptions): Middleware;
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=cors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../src/lib/cors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAKzE,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,MAAM,GAAG,MAAM,CAAC,CAAA;AAErE;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG,GAAG,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAEhF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,CACE,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,cAAc,GACtB,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAA;CAChE;AAED;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,OAAO,GAAG,kBAAkB,CAAA;AAErE;;GAEG;AACH,MAAM,MAAM,gCAAgC,GAAG,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,CAAA;AAEnF;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;OAEG;IACH,CACE,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,cAAc,GACtB,gCAAgC,GAAG,OAAO,CAAC,gCAAgC,CAAC,CAAA;CAChF;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;IAEnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,0BAA0B,CAAA;IAE/D;;OAEG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAElC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAE3B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAOD;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,UAAU,CA0F1D"}
|
package/dist/lib/cors.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Vary } from '@remix-run/headers';
|
|
2
|
+
const defaultCorsMethods = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'];
|
|
3
|
+
/**
|
|
4
|
+
* Middleware that adds CORS headers and handles CORS preflight requests.
|
|
5
|
+
*
|
|
6
|
+
* @param options CORS options
|
|
7
|
+
* @returns CORS middleware
|
|
8
|
+
*/
|
|
9
|
+
export function cors(options = {}) {
|
|
10
|
+
let methods = normalizeMethodList(options.methods ?? defaultCorsMethods);
|
|
11
|
+
let exposedHeaders = options.exposedHeaders ? normalizeHeaderList(options.exposedHeaders) : '';
|
|
12
|
+
let allowCredentials = options.credentials ?? false;
|
|
13
|
+
let preflightContinue = options.preflightContinue ?? false;
|
|
14
|
+
let preflightStatusCode = options.preflightStatusCode ?? 204;
|
|
15
|
+
return async (context, next) => {
|
|
16
|
+
let requestOrigin = context.headers.get('Origin');
|
|
17
|
+
let preflightRequest = isPreflightRequest(context);
|
|
18
|
+
if (requestOrigin == null) {
|
|
19
|
+
if (preflightRequest && !preflightContinue) {
|
|
20
|
+
return new Response(null, { status: preflightStatusCode });
|
|
21
|
+
}
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
let allowedOrigin = await resolveAllowedOrigin(requestOrigin, context, options.origin);
|
|
25
|
+
if (allowedOrigin == null) {
|
|
26
|
+
if (preflightRequest && !preflightContinue) {
|
|
27
|
+
return new Response(null, { status: 403 });
|
|
28
|
+
}
|
|
29
|
+
return next();
|
|
30
|
+
}
|
|
31
|
+
let corsHeaders = new Headers();
|
|
32
|
+
let vary = new Vary();
|
|
33
|
+
let allowOriginHeader = allowedOrigin;
|
|
34
|
+
if (allowCredentials && allowedOrigin === '*') {
|
|
35
|
+
allowOriginHeader = requestOrigin;
|
|
36
|
+
}
|
|
37
|
+
corsHeaders.set('Access-Control-Allow-Origin', allowOriginHeader);
|
|
38
|
+
if (allowOriginHeader !== '*') {
|
|
39
|
+
vary.add('Origin');
|
|
40
|
+
}
|
|
41
|
+
if (allowCredentials) {
|
|
42
|
+
corsHeaders.set('Access-Control-Allow-Credentials', 'true');
|
|
43
|
+
}
|
|
44
|
+
if (preflightRequest) {
|
|
45
|
+
corsHeaders.set('Access-Control-Allow-Methods', methods);
|
|
46
|
+
vary.add('Access-Control-Request-Method');
|
|
47
|
+
let allowedHeaders = await resolveAllowedHeaders(context, options.allowedHeaders);
|
|
48
|
+
if (allowedHeaders.headerValue != null) {
|
|
49
|
+
corsHeaders.set('Access-Control-Allow-Headers', allowedHeaders.headerValue);
|
|
50
|
+
}
|
|
51
|
+
if (allowedHeaders.varyOnRequestHeaders) {
|
|
52
|
+
vary.add('Access-Control-Request-Headers');
|
|
53
|
+
}
|
|
54
|
+
if (options.maxAge != null) {
|
|
55
|
+
let maxAge = Math.max(0, Math.floor(options.maxAge));
|
|
56
|
+
corsHeaders.set('Access-Control-Max-Age', String(maxAge));
|
|
57
|
+
}
|
|
58
|
+
if (options.allowPrivateNetwork &&
|
|
59
|
+
context.headers.get('Access-Control-Request-Private-Network')?.toLowerCase() === 'true') {
|
|
60
|
+
corsHeaders.set('Access-Control-Allow-Private-Network', 'true');
|
|
61
|
+
vary.add('Access-Control-Request-Private-Network');
|
|
62
|
+
}
|
|
63
|
+
if (!preflightContinue) {
|
|
64
|
+
if (vary.size > 0) {
|
|
65
|
+
corsHeaders.set('Vary', vary.toString());
|
|
66
|
+
}
|
|
67
|
+
return new Response(null, {
|
|
68
|
+
status: preflightStatusCode,
|
|
69
|
+
headers: corsHeaders,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (exposedHeaders !== '') {
|
|
74
|
+
corsHeaders.set('Access-Control-Expose-Headers', exposedHeaders);
|
|
75
|
+
}
|
|
76
|
+
let response = await next();
|
|
77
|
+
return withCorsHeaders(response, corsHeaders, vary);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function isPreflightRequest(context) {
|
|
81
|
+
return context.method === 'OPTIONS' && context.headers.has('Access-Control-Request-Method');
|
|
82
|
+
}
|
|
83
|
+
function normalizeMethodList(methods) {
|
|
84
|
+
let normalized = [];
|
|
85
|
+
for (let method of methods) {
|
|
86
|
+
let value = method.trim().toUpperCase();
|
|
87
|
+
if (value === '') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (normalized.includes(value)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
normalized.push(value);
|
|
94
|
+
}
|
|
95
|
+
return normalized.join(', ');
|
|
96
|
+
}
|
|
97
|
+
function normalizeHeaderList(headerNames) {
|
|
98
|
+
let normalized = [];
|
|
99
|
+
for (let headerName of headerNames) {
|
|
100
|
+
let value = headerName.trim();
|
|
101
|
+
if (value === '') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let duplicate = normalized.some((existing) => existing.toLowerCase() === value.toLowerCase());
|
|
105
|
+
if (duplicate) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
normalized.push(value);
|
|
109
|
+
}
|
|
110
|
+
return normalized.join(', ');
|
|
111
|
+
}
|
|
112
|
+
async function resolveAllowedOrigin(requestOrigin, context, configuredOrigin) {
|
|
113
|
+
let origin = configuredOrigin ?? '*';
|
|
114
|
+
if (typeof origin === 'function') {
|
|
115
|
+
let result = await origin(requestOrigin, context);
|
|
116
|
+
return normalizeResolvedOrigin(result, requestOrigin);
|
|
117
|
+
}
|
|
118
|
+
if (origin === true) {
|
|
119
|
+
return requestOrigin;
|
|
120
|
+
}
|
|
121
|
+
if (origin === false) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (typeof origin === 'string') {
|
|
125
|
+
if (origin === '*') {
|
|
126
|
+
return '*';
|
|
127
|
+
}
|
|
128
|
+
return origin === requestOrigin ? requestOrigin : null;
|
|
129
|
+
}
|
|
130
|
+
if (origin instanceof RegExp) {
|
|
131
|
+
return matchesOriginPattern(origin, requestOrigin) ? requestOrigin : null;
|
|
132
|
+
}
|
|
133
|
+
for (let allowed of origin) {
|
|
134
|
+
if (allowed === '*') {
|
|
135
|
+
return '*';
|
|
136
|
+
}
|
|
137
|
+
if (typeof allowed === 'string' && allowed === requestOrigin) {
|
|
138
|
+
return requestOrigin;
|
|
139
|
+
}
|
|
140
|
+
if (allowed instanceof RegExp && matchesOriginPattern(allowed, requestOrigin)) {
|
|
141
|
+
return requestOrigin;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function matchesOriginPattern(pattern, requestOrigin) {
|
|
147
|
+
let normalizedPattern = new RegExp(pattern.source, pattern.flags);
|
|
148
|
+
return normalizedPattern.test(requestOrigin);
|
|
149
|
+
}
|
|
150
|
+
function normalizeResolvedOrigin(resolved, requestOrigin) {
|
|
151
|
+
if (resolved == null || resolved === false) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (resolved === true) {
|
|
155
|
+
return requestOrigin;
|
|
156
|
+
}
|
|
157
|
+
if (resolved === '*') {
|
|
158
|
+
return '*';
|
|
159
|
+
}
|
|
160
|
+
return resolved;
|
|
161
|
+
}
|
|
162
|
+
async function resolveAllowedHeaders(context, configuredAllowedHeaders) {
|
|
163
|
+
if (Array.isArray(configuredAllowedHeaders)) {
|
|
164
|
+
let headerValue = normalizeHeaderList(configuredAllowedHeaders);
|
|
165
|
+
return {
|
|
166
|
+
headerValue: headerValue === '' ? null : headerValue,
|
|
167
|
+
varyOnRequestHeaders: false,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (typeof configuredAllowedHeaders === 'function') {
|
|
171
|
+
let resolved = await configuredAllowedHeaders(context.request, context);
|
|
172
|
+
if (resolved != null) {
|
|
173
|
+
let headerValue = normalizeHeaderList(resolved);
|
|
174
|
+
return {
|
|
175
|
+
headerValue: headerValue === '' ? null : headerValue,
|
|
176
|
+
varyOnRequestHeaders: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
let requestedHeaders = context.headers.get('Access-Control-Request-Headers');
|
|
180
|
+
if (requestedHeaders == null || requestedHeaders.trim() === '') {
|
|
181
|
+
return {
|
|
182
|
+
headerValue: null,
|
|
183
|
+
varyOnRequestHeaders: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
headerValue: requestedHeaders,
|
|
188
|
+
varyOnRequestHeaders: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
let requestedHeaders = context.headers.get('Access-Control-Request-Headers');
|
|
192
|
+
if (requestedHeaders == null || requestedHeaders.trim() === '') {
|
|
193
|
+
return {
|
|
194
|
+
headerValue: null,
|
|
195
|
+
varyOnRequestHeaders: false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
headerValue: requestedHeaders,
|
|
200
|
+
varyOnRequestHeaders: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function withCorsHeaders(response, corsHeaders, vary) {
|
|
204
|
+
let responseHeaders = new Headers(response.headers);
|
|
205
|
+
for (let [headerName, headerValue] of corsHeaders) {
|
|
206
|
+
responseHeaders.set(headerName, headerValue);
|
|
207
|
+
}
|
|
208
|
+
if (vary.size > 0) {
|
|
209
|
+
let responseVary = Vary.from(responseHeaders.get('Vary'));
|
|
210
|
+
vary.forEach((headerName) => responseVary.add(headerName));
|
|
211
|
+
responseHeaders.set('Vary', responseVary.toString());
|
|
212
|
+
}
|
|
213
|
+
return new Response(response.body, {
|
|
214
|
+
status: response.status,
|
|
215
|
+
statusText: response.statusText,
|
|
216
|
+
headers: responseHeaders,
|
|
217
|
+
});
|
|
218
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/cors-middleware",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Middleware for handling CORS in Fetch API servers",
|
|
5
|
+
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/remix-run/remix.git",
|
|
9
10
|
"directory": "packages/cors-middleware"
|
|
10
11
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
12
|
+
"homepage": "https://github.com/remix-run/remix/tree/main/packages/cors-middleware#readme",
|
|
13
|
+
"files": [
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md",
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"!src/**/*.test.ts"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^24.6.0",
|
|
30
|
+
"@typescript/native-preview": "7.0.0-dev.20251125.1",
|
|
31
|
+
"@remix-run/assert": "0.1.0",
|
|
32
|
+
"@remix-run/test": "0.1.0",
|
|
33
|
+
"@remix-run/headers": "0.19.0",
|
|
34
|
+
"@remix-run/fetch-router": "0.18.1"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@remix-run/headers": "^0.19.0",
|
|
38
|
+
"@remix-run/fetch-router": "^0.18.1"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"fetch",
|
|
42
|
+
"router",
|
|
43
|
+
"middleware",
|
|
44
|
+
"cors",
|
|
45
|
+
"cross-origin",
|
|
46
|
+
"preflight"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsgo -p tsconfig.build.json",
|
|
50
|
+
"clean": "git clean -fdX",
|
|
51
|
+
"test": "remix-test",
|
|
52
|
+
"typecheck": "tsgo --noEmit"
|
|
13
53
|
}
|
|
14
|
-
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
package/src/lib/cors.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { Middleware, RequestContext } from '@remix-run/fetch-router'
|
|
2
|
+
import { Vary } from '@remix-run/headers'
|
|
3
|
+
|
|
4
|
+
const defaultCorsMethods = ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']
|
|
5
|
+
|
|
6
|
+
type OriginMatcher = string | RegExp | ReadonlyArray<string | RegExp>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return shape for a dynamic CORS origin resolver.
|
|
10
|
+
*/
|
|
11
|
+
export type CorsOriginResolverResult = '*' | string | boolean | null | undefined
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolves the allowed origin for a given request origin.
|
|
15
|
+
*/
|
|
16
|
+
export interface CorsOriginResolver {
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the allowed origin for a request with an `Origin` header.
|
|
19
|
+
*/
|
|
20
|
+
(
|
|
21
|
+
origin: string,
|
|
22
|
+
context: RequestContext,
|
|
23
|
+
): CorsOriginResolverResult | Promise<CorsOriginResolverResult>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Accepted forms for configuring allowed CORS origins.
|
|
28
|
+
*/
|
|
29
|
+
export type CorsOrigin = OriginMatcher | boolean | CorsOriginResolver
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return shape for a dynamic allowed-headers resolver.
|
|
33
|
+
*/
|
|
34
|
+
export type CorsAllowedHeadersResolverResult = readonly string[] | null | undefined
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the allowed request headers for a preflight request.
|
|
38
|
+
*/
|
|
39
|
+
export interface CorsAllowedHeadersResolver {
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the request headers allowed by a preflight request.
|
|
42
|
+
*/
|
|
43
|
+
(
|
|
44
|
+
request: Request,
|
|
45
|
+
context: RequestContext,
|
|
46
|
+
): CorsAllowedHeadersResolverResult | Promise<CorsAllowedHeadersResolverResult>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for CORS middleware.
|
|
51
|
+
*/
|
|
52
|
+
export interface CorsOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Allowed origins. Defaults to '*'.
|
|
55
|
+
*
|
|
56
|
+
* - `true` reflects the request Origin
|
|
57
|
+
* - `false` disables CORS headers
|
|
58
|
+
* - `'*'` allows all origins
|
|
59
|
+
* - `string`/`RegExp`/array allow matching origins
|
|
60
|
+
* - `function` allows dynamic origin checks
|
|
61
|
+
*/
|
|
62
|
+
origin?: CorsOrigin
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Allowed methods for preflight responses.
|
|
66
|
+
*
|
|
67
|
+
* @default ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE']
|
|
68
|
+
*/
|
|
69
|
+
methods?: readonly string[]
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Allowed request headers for preflight responses.
|
|
73
|
+
*
|
|
74
|
+
* Defaults to reflecting Access-Control-Request-Headers.
|
|
75
|
+
*/
|
|
76
|
+
allowedHeaders?: readonly string[] | CorsAllowedHeadersResolver
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Exposed response headers for non-preflight requests.
|
|
80
|
+
*/
|
|
81
|
+
exposedHeaders?: readonly string[]
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Include Access-Control-Allow-Credentials: true.
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
credentials?: boolean
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Access-Control-Max-Age value for preflight responses (seconds).
|
|
92
|
+
*/
|
|
93
|
+
maxAge?: number
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Continue to downstream handlers for preflight requests.
|
|
97
|
+
*
|
|
98
|
+
* @default false
|
|
99
|
+
*/
|
|
100
|
+
preflightContinue?: boolean
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Status code to use when short-circuiting preflight responses.
|
|
104
|
+
*
|
|
105
|
+
* @default 204
|
|
106
|
+
*/
|
|
107
|
+
preflightStatusCode?: number
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Include Access-Control-Allow-Private-Network: true when requested.
|
|
111
|
+
*
|
|
112
|
+
* @default false
|
|
113
|
+
*/
|
|
114
|
+
allowPrivateNetwork?: boolean
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type ResolvedAllowedHeaders = {
|
|
118
|
+
headerValue: string | null
|
|
119
|
+
varyOnRequestHeaders: boolean
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Middleware that adds CORS headers and handles CORS preflight requests.
|
|
124
|
+
*
|
|
125
|
+
* @param options CORS options
|
|
126
|
+
* @returns CORS middleware
|
|
127
|
+
*/
|
|
128
|
+
export function cors(options: CorsOptions = {}): Middleware {
|
|
129
|
+
let methods = normalizeMethodList(options.methods ?? defaultCorsMethods)
|
|
130
|
+
let exposedHeaders = options.exposedHeaders ? normalizeHeaderList(options.exposedHeaders) : ''
|
|
131
|
+
let allowCredentials = options.credentials ?? false
|
|
132
|
+
let preflightContinue = options.preflightContinue ?? false
|
|
133
|
+
let preflightStatusCode = options.preflightStatusCode ?? 204
|
|
134
|
+
|
|
135
|
+
return async (context, next) => {
|
|
136
|
+
let requestOrigin = context.headers.get('Origin')
|
|
137
|
+
let preflightRequest = isPreflightRequest(context)
|
|
138
|
+
|
|
139
|
+
if (requestOrigin == null) {
|
|
140
|
+
if (preflightRequest && !preflightContinue) {
|
|
141
|
+
return new Response(null, { status: preflightStatusCode })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return next()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let allowedOrigin = await resolveAllowedOrigin(requestOrigin, context, options.origin)
|
|
148
|
+
if (allowedOrigin == null) {
|
|
149
|
+
if (preflightRequest && !preflightContinue) {
|
|
150
|
+
return new Response(null, { status: 403 })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return next()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let corsHeaders = new Headers()
|
|
157
|
+
let vary = new Vary()
|
|
158
|
+
|
|
159
|
+
let allowOriginHeader = allowedOrigin
|
|
160
|
+
if (allowCredentials && allowedOrigin === '*') {
|
|
161
|
+
allowOriginHeader = requestOrigin
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
corsHeaders.set('Access-Control-Allow-Origin', allowOriginHeader)
|
|
165
|
+
|
|
166
|
+
if (allowOriginHeader !== '*') {
|
|
167
|
+
vary.add('Origin')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (allowCredentials) {
|
|
171
|
+
corsHeaders.set('Access-Control-Allow-Credentials', 'true')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (preflightRequest) {
|
|
175
|
+
corsHeaders.set('Access-Control-Allow-Methods', methods)
|
|
176
|
+
vary.add('Access-Control-Request-Method')
|
|
177
|
+
|
|
178
|
+
let allowedHeaders = await resolveAllowedHeaders(context, options.allowedHeaders)
|
|
179
|
+
if (allowedHeaders.headerValue != null) {
|
|
180
|
+
corsHeaders.set('Access-Control-Allow-Headers', allowedHeaders.headerValue)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (allowedHeaders.varyOnRequestHeaders) {
|
|
184
|
+
vary.add('Access-Control-Request-Headers')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.maxAge != null) {
|
|
188
|
+
let maxAge = Math.max(0, Math.floor(options.maxAge))
|
|
189
|
+
corsHeaders.set('Access-Control-Max-Age', String(maxAge))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
options.allowPrivateNetwork &&
|
|
194
|
+
context.headers.get('Access-Control-Request-Private-Network')?.toLowerCase() === 'true'
|
|
195
|
+
) {
|
|
196
|
+
corsHeaders.set('Access-Control-Allow-Private-Network', 'true')
|
|
197
|
+
vary.add('Access-Control-Request-Private-Network')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!preflightContinue) {
|
|
201
|
+
if (vary.size > 0) {
|
|
202
|
+
corsHeaders.set('Vary', vary.toString())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return new Response(null, {
|
|
206
|
+
status: preflightStatusCode,
|
|
207
|
+
headers: corsHeaders,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
} else if (exposedHeaders !== '') {
|
|
211
|
+
corsHeaders.set('Access-Control-Expose-Headers', exposedHeaders)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let response = await next()
|
|
215
|
+
|
|
216
|
+
return withCorsHeaders(response, corsHeaders, vary)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isPreflightRequest(context: RequestContext): boolean {
|
|
221
|
+
return context.method === 'OPTIONS' && context.headers.has('Access-Control-Request-Method')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeMethodList(methods: readonly string[]): string {
|
|
225
|
+
let normalized: string[] = []
|
|
226
|
+
|
|
227
|
+
for (let method of methods) {
|
|
228
|
+
let value = method.trim().toUpperCase()
|
|
229
|
+
if (value === '') {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (normalized.includes(value)) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
normalized.push(value)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return normalized.join(', ')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeHeaderList(headerNames: readonly string[]): string {
|
|
244
|
+
let normalized: string[] = []
|
|
245
|
+
|
|
246
|
+
for (let headerName of headerNames) {
|
|
247
|
+
let value = headerName.trim()
|
|
248
|
+
if (value === '') {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let duplicate = normalized.some((existing) => existing.toLowerCase() === value.toLowerCase())
|
|
253
|
+
if (duplicate) {
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
normalized.push(value)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return normalized.join(', ')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function resolveAllowedOrigin(
|
|
264
|
+
requestOrigin: string,
|
|
265
|
+
context: RequestContext,
|
|
266
|
+
configuredOrigin: CorsOrigin | undefined,
|
|
267
|
+
): Promise<string | '*' | null> {
|
|
268
|
+
let origin = configuredOrigin ?? '*'
|
|
269
|
+
|
|
270
|
+
if (typeof origin === 'function') {
|
|
271
|
+
let result = await origin(requestOrigin, context)
|
|
272
|
+
return normalizeResolvedOrigin(result, requestOrigin)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (origin === true) {
|
|
276
|
+
return requestOrigin
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (origin === false) {
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (typeof origin === 'string') {
|
|
284
|
+
if (origin === '*') {
|
|
285
|
+
return '*'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return origin === requestOrigin ? requestOrigin : null
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (origin instanceof RegExp) {
|
|
292
|
+
return matchesOriginPattern(origin, requestOrigin) ? requestOrigin : null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (let allowed of origin) {
|
|
296
|
+
if (allowed === '*') {
|
|
297
|
+
return '*'
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof allowed === 'string' && allowed === requestOrigin) {
|
|
301
|
+
return requestOrigin
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (allowed instanceof RegExp && matchesOriginPattern(allowed, requestOrigin)) {
|
|
305
|
+
return requestOrigin
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function matchesOriginPattern(pattern: RegExp, requestOrigin: string): boolean {
|
|
313
|
+
let normalizedPattern = new RegExp(pattern.source, pattern.flags)
|
|
314
|
+
return normalizedPattern.test(requestOrigin)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeResolvedOrigin(
|
|
318
|
+
resolved: CorsOriginResolverResult,
|
|
319
|
+
requestOrigin: string,
|
|
320
|
+
): string | '*' | null {
|
|
321
|
+
if (resolved == null || resolved === false) {
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (resolved === true) {
|
|
326
|
+
return requestOrigin
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (resolved === '*') {
|
|
330
|
+
return '*'
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return resolved
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function resolveAllowedHeaders(
|
|
337
|
+
context: RequestContext,
|
|
338
|
+
configuredAllowedHeaders: readonly string[] | CorsAllowedHeadersResolver | undefined,
|
|
339
|
+
): Promise<ResolvedAllowedHeaders> {
|
|
340
|
+
if (Array.isArray(configuredAllowedHeaders)) {
|
|
341
|
+
let headerValue = normalizeHeaderList(configuredAllowedHeaders)
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
headerValue: headerValue === '' ? null : headerValue,
|
|
345
|
+
varyOnRequestHeaders: false,
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (typeof configuredAllowedHeaders === 'function') {
|
|
350
|
+
let resolved = await configuredAllowedHeaders(context.request, context)
|
|
351
|
+
if (resolved != null) {
|
|
352
|
+
let headerValue = normalizeHeaderList(resolved)
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
headerValue: headerValue === '' ? null : headerValue,
|
|
356
|
+
varyOnRequestHeaders: true,
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let requestedHeaders = context.headers.get('Access-Control-Request-Headers')
|
|
361
|
+
if (requestedHeaders == null || requestedHeaders.trim() === '') {
|
|
362
|
+
return {
|
|
363
|
+
headerValue: null,
|
|
364
|
+
varyOnRequestHeaders: true,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
headerValue: requestedHeaders,
|
|
370
|
+
varyOnRequestHeaders: true,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let requestedHeaders = context.headers.get('Access-Control-Request-Headers')
|
|
375
|
+
if (requestedHeaders == null || requestedHeaders.trim() === '') {
|
|
376
|
+
return {
|
|
377
|
+
headerValue: null,
|
|
378
|
+
varyOnRequestHeaders: false,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
headerValue: requestedHeaders,
|
|
384
|
+
varyOnRequestHeaders: true,
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function withCorsHeaders(response: Response, corsHeaders: Headers, vary: Vary): Response {
|
|
389
|
+
let responseHeaders = new Headers(response.headers)
|
|
390
|
+
|
|
391
|
+
for (let [headerName, headerValue] of corsHeaders) {
|
|
392
|
+
responseHeaders.set(headerName, headerValue)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (vary.size > 0) {
|
|
396
|
+
let responseVary = Vary.from(responseHeaders.get('Vary'))
|
|
397
|
+
vary.forEach((headerName) => responseVary.add(headerName))
|
|
398
|
+
responseHeaders.set('Vary', responseVary.toString())
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return new Response(response.body, {
|
|
402
|
+
status: response.status,
|
|
403
|
+
statusText: response.statusText,
|
|
404
|
+
headers: responseHeaders,
|
|
405
|
+
})
|
|
406
|
+
}
|