remix 3.0.0-beta.0 → 3.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/node-tsx/load-module.d.ts +2 -0
- package/dist/node-tsx/load-module.d.ts.map +1 -0
- package/dist/node-tsx/load-module.js +2 -0
- package/dist/node-tsx.d.ts +3 -0
- package/dist/node-tsx.d.ts.map +1 -0
- package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
- package/dist/render-middleware.d.ts +2 -0
- package/dist/render-middleware.d.ts.map +1 -0
- package/dist/render-middleware.js +2 -0
- package/dist/route-pattern/href.d.ts +2 -0
- package/dist/route-pattern/href.d.ts.map +1 -0
- package/dist/route-pattern/href.js +2 -0
- package/dist/route-pattern/join.d.ts +2 -0
- package/dist/route-pattern/join.d.ts.map +1 -0
- package/dist/route-pattern/join.js +2 -0
- package/dist/route-pattern/match.d.ts +2 -0
- package/dist/route-pattern/match.d.ts.map +1 -0
- package/dist/route-pattern/match.js +2 -0
- package/package.json +158 -44
- package/src/assert/README.md +109 -0
- package/src/assets/README.md +539 -0
- package/src/async-context-middleware/README.md +100 -0
- package/src/auth/README.md +445 -0
- package/src/auth-middleware/README.md +246 -0
- package/src/cli/README.md +78 -0
- package/src/compression-middleware/README.md +176 -0
- package/src/cookie/README.md +106 -0
- package/src/cop-middleware/README.md +117 -0
- package/src/cors-middleware/README.md +174 -0
- package/src/csrf-middleware/README.md +99 -0
- package/src/data-schema/README.md +422 -0
- package/src/data-table/README.md +552 -0
- package/src/data-table-mysql/README.md +97 -0
- package/src/data-table-postgres/README.md +74 -0
- package/src/data-table-sqlite/README.md +84 -0
- package/src/fetch-proxy/README.md +46 -0
- package/src/fetch-router/README.md +902 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +47 -0
- package/src/form-data-middleware/README.md +109 -0
- package/src/form-data-parser/README.md +160 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +629 -0
- package/src/html-template/README.md +101 -0
- package/src/lazy-file/README.md +109 -0
- package/src/logger-middleware/README.md +132 -0
- package/src/method-override-middleware/README.md +71 -0
- package/src/mime/README.md +110 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +352 -0
- package/src/node-tsx/README.md +79 -0
- package/src/node-tsx/load-module.ts +2 -0
- package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
- package/src/render-middleware/README.md +99 -0
- package/src/render-middleware.ts +2 -0
- package/src/route-pattern/README.md +291 -0
- package/src/route-pattern/href.ts +2 -0
- package/src/route-pattern/join.ts +2 -0
- package/src/route-pattern/match.ts +2 -0
- package/src/session/README.md +171 -0
- package/src/session-middleware/README.md +109 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +37 -0
- package/src/static-middleware/README.md +89 -0
- package/src/tar-parser/README.md +74 -0
- package/src/terminal/README.md +92 -0
- package/src/test/README.md +430 -0
- package/src/ui/README.md +219 -0
- package/src/ui/accordion/README.md +166 -0
- package/src/ui/anchor/README.md +153 -0
- package/src/ui/animation/README.md +316 -0
- package/src/ui/breadcrumbs/README.md +55 -0
- package/src/ui/button/README.md +44 -0
- package/src/ui/combobox/README.md +145 -0
- package/src/ui/glyph/README.md +72 -0
- package/src/ui/listbox/README.md +115 -0
- package/src/ui/menu/README.md +96 -0
- package/src/ui/popover/README.md +122 -0
- package/src/ui/scroll-lock/README.md +33 -0
- package/src/ui/select/README.md +107 -0
- package/src/ui/server/README.md +90 -0
- package/src/ui/test/README.md +107 -0
- package/src/ui/theme/README.md +103 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for creating and managing Remix projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Create new Remix projects with `npx remix@next new` or installed `remix new`
|
|
8
|
+
- Print shell completion scripts with `remix completion`
|
|
9
|
+
- Check project environment and Remix app conventions with `remix doctor`
|
|
10
|
+
- Create low-risk project and controller files with `remix doctor --fix`
|
|
11
|
+
- Inspect the current app route tree with `remix routes`
|
|
12
|
+
- Run project tests with `remix test`
|
|
13
|
+
- Print the current Remix version with `remix version`
|
|
14
|
+
- Use the same CLI through the `remix` package or the `remix/cli` API
|
|
15
|
+
- Scaffold a starter app that matches the Remix project layout conventions
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Use `npx remix@next new <target-dir>` to scaffold a new Remix app. Install `remix` when you want the local `remix` command:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm i remix
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Shell completion
|
|
26
|
+
|
|
27
|
+
Install bash completion:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
remix completion bash >> ~/.bashrc
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install zsh completion:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
remix completion zsh >> ~/.zshrc
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Use `npx remix@next new my-remix-app` to scaffold a new Remix app. After installing Remix, the equivalent local command is `remix new my-remix-app`.
|
|
42
|
+
|
|
43
|
+
The rest of the CLI is available through the installed `remix` command:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
remix new my-remix-app
|
|
47
|
+
remix completion bash >> ~/.bashrc
|
|
48
|
+
remix doctor
|
|
49
|
+
remix doctor --fix
|
|
50
|
+
remix routes
|
|
51
|
+
remix routes --table
|
|
52
|
+
remix routes --table --no-headers
|
|
53
|
+
remix test
|
|
54
|
+
remix version
|
|
55
|
+
remix --no-color doctor
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You can also run the CLI programmatically:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { runRemix } from 'remix/cli'
|
|
62
|
+
|
|
63
|
+
await runRemix(['new', 'my-remix-app'])
|
|
64
|
+
await runRemix(['completion', 'bash'])
|
|
65
|
+
await runRemix(['doctor'])
|
|
66
|
+
await runRemix(['doctor', '--fix'])
|
|
67
|
+
await runRemix(['routes'])
|
|
68
|
+
await runRemix(['routes', '--table'])
|
|
69
|
+
await runRemix(['routes', '--table', '--no-headers'])
|
|
70
|
+
await runRemix(['test'])
|
|
71
|
+
await runRemix(['version'])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`runRemix()` returns the CLI exit code as a promise.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# compression-middleware
|
|
2
|
+
|
|
3
|
+
Response compression middleware for Remix. It negotiates `br`, `gzip`, and `deflate` from `Accept-Encoding` and applies sensible defaults for when compression is useful.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Encoding Negotiation** - Selects the best supported encoding from `Accept-Encoding`
|
|
8
|
+
- **Compression Guards** - Skips already-compressed responses and range-enabled responses
|
|
9
|
+
- **Size Thresholds** - Configurable minimum response size for compression
|
|
10
|
+
- **MIME Filtering** - Compresses only content types likely to benefit
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i remix
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createRouter } from 'remix/router'
|
|
22
|
+
import { compression } from 'remix/middleware/compression'
|
|
23
|
+
|
|
24
|
+
let router = createRouter({
|
|
25
|
+
middleware: [compression()],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The middleware will automatically compress responses for compressible MIME types when:
|
|
30
|
+
|
|
31
|
+
- The client supports compression (`Accept-Encoding` header with a supported encoding)
|
|
32
|
+
- The response is large enough to benefit from compression (≥1024 bytes if `Content-Length` is present, by default)
|
|
33
|
+
- The response hasn't already been compressed
|
|
34
|
+
- The response doesn't advertise range support (`Accept-Ranges: bytes`)
|
|
35
|
+
|
|
36
|
+
### Threshold
|
|
37
|
+
|
|
38
|
+
**Default:** `1024` (only enforced if `Content-Length` is present)
|
|
39
|
+
|
|
40
|
+
Set the minimum response size in bytes to compress:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createRouter } from 'remix/router'
|
|
44
|
+
import { compression } from 'remix/middleware/compression'
|
|
45
|
+
|
|
46
|
+
let router = createRouter({
|
|
47
|
+
middleware: [
|
|
48
|
+
compression({
|
|
49
|
+
threshold: 2048, // Only compress responses ≥2KB
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Encodings
|
|
56
|
+
|
|
57
|
+
**Default:** `['br', 'gzip', 'deflate']`
|
|
58
|
+
|
|
59
|
+
Customize which compression algorithms to support:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { createRouter } from 'remix/router'
|
|
63
|
+
import { compression } from 'remix/middleware/compression'
|
|
64
|
+
|
|
65
|
+
let router = createRouter({
|
|
66
|
+
middleware: [
|
|
67
|
+
compression({
|
|
68
|
+
encodings: ['br', 'gzip'], // Only use Brotli and Gzip
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The `encodings` option can also be a function that receives the response:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createRouter } from 'remix/router'
|
|
78
|
+
import { compression } from 'remix/middleware/compression'
|
|
79
|
+
|
|
80
|
+
let router = createRouter({
|
|
81
|
+
middleware: [
|
|
82
|
+
compression({
|
|
83
|
+
encodings: (response) => {
|
|
84
|
+
// Use different encodings for server-sent events
|
|
85
|
+
let contentType = response.headers.get('Content-Type')
|
|
86
|
+
return contentType?.startsWith('text/event-stream;')
|
|
87
|
+
? ['gzip', 'deflate']
|
|
88
|
+
: ['br', 'gzip', 'deflate']
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Filter Media Type
|
|
96
|
+
|
|
97
|
+
**Default:** Uses `isCompressibleMimeType()` from [`remix/mime`](https://github.com/remix-run/remix/tree/main/packages/mime)
|
|
98
|
+
|
|
99
|
+
You can customize this behavior with the `filterMediaType` option:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createRouter } from 'remix/router'
|
|
103
|
+
import { compression } from 'remix/middleware/compression'
|
|
104
|
+
import { isCompressibleMimeType } from 'remix/mime'
|
|
105
|
+
|
|
106
|
+
let router = createRouter({
|
|
107
|
+
middleware: [
|
|
108
|
+
compression({
|
|
109
|
+
filterMediaType(mediaType) {
|
|
110
|
+
// Add a custom media type to the default compressible list
|
|
111
|
+
return isCompressibleMimeType(mediaType) || mediaType === 'application/vnd.example+data'
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Compression Options
|
|
119
|
+
|
|
120
|
+
**Default:** Uses Node.js defaults for [zlib](https://nodejs.org/api/zlib.html#class-options) and [Brotli](https://nodejs.org/api/zlib.html#class-brotlioptions), with automatic flush handling for server-sent events.
|
|
121
|
+
|
|
122
|
+
You can pass options options to the underlying Node.js `zlib` and `brotli` compressors for fine-grained control:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createRouter } from 'remix/router'
|
|
126
|
+
import { compression } from 'remix/middleware/compression'
|
|
127
|
+
import { zlib } from 'node:zlib'
|
|
128
|
+
|
|
129
|
+
let router = createRouter({
|
|
130
|
+
middleware: [
|
|
131
|
+
compression({
|
|
132
|
+
zlib: {
|
|
133
|
+
level: 6,
|
|
134
|
+
},
|
|
135
|
+
brotli: {
|
|
136
|
+
params: {
|
|
137
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
],
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Like `encodings`, both `zlib` and `brotli` options can also be functions that receive the response:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import zlib from 'node:zlib'
|
|
149
|
+
import { createRouter } from 'remix/router'
|
|
150
|
+
import { compression } from 'remix/middleware/compression'
|
|
151
|
+
|
|
152
|
+
let router = createRouter({
|
|
153
|
+
middleware: [
|
|
154
|
+
compression({
|
|
155
|
+
brotli: (response) => {
|
|
156
|
+
let contentType = response.headers.get('Content-Type')
|
|
157
|
+
return {
|
|
158
|
+
params: {
|
|
159
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: contentType?.startsWith('text/html;') ? 4 : 11,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
],
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Related Packages
|
|
169
|
+
|
|
170
|
+
- [`remix/router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
|
|
171
|
+
- [`remix/mime`](https://github.com/remix-run/remix/tree/main/packages/mime) - MIME type utilities
|
|
172
|
+
- [`remix/response`](https://github.com/remix-run/remix/tree/main/packages/response) - Response helpers
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# cookie
|
|
2
|
+
|
|
3
|
+
Type-safe cookie parsing and serialization for Remix. It supports secure signing, secret rotation, and complete cookie attribute control.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Secure Cookie Signing:** Built-in cryptographic signing using HMAC-SHA256 to prevent cookie tampering, with support for secret rotation without breaking existing cookies.
|
|
8
|
+
- **Secret Rotation Support:** Seamlessly rotate signing secrets while maintaining backward compatibility with existing cookies.
|
|
9
|
+
- **Web Standards Compliant:** Built on Web Crypto API and standard cookie parsing, making it runtime-agnostic (Node.js, Bun, Deno, Cloudflare Workers).
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i remix
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { createCookie } from 'remix/cookie'
|
|
21
|
+
|
|
22
|
+
let sessionCookie = createCookie('session', {
|
|
23
|
+
httpOnly: true,
|
|
24
|
+
secrets: ['s3cret1'],
|
|
25
|
+
secure: true,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
cookie.name // "session"
|
|
29
|
+
cookie.httpOnly // true
|
|
30
|
+
cookie.secure // true
|
|
31
|
+
cookie.signed // true
|
|
32
|
+
|
|
33
|
+
// Get the value of the "session" cookie from the request's `Cookie` header
|
|
34
|
+
let value = await sessionCookie.parse(request.headers.get('Cookie'))
|
|
35
|
+
|
|
36
|
+
// Set the value of the cookie in a Response's `Set-Cookie` header
|
|
37
|
+
let response = new Response('Hello, world!', {
|
|
38
|
+
headers: {
|
|
39
|
+
'Set-Cookie': await sessionCookie.serialize(value),
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Signing Cookies
|
|
45
|
+
|
|
46
|
+
This library supports signing cookies, which is useful for ensuring the integrity of the cookie value and preventing tampering. Signing happens automatically when you provide a `secrets` option to the `Cookie` constructor.
|
|
47
|
+
|
|
48
|
+
Secret rotation is also supported, so you can easily rotate in new secrets without breaking existing cookies.
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Cookie } from 'remix/cookie'
|
|
52
|
+
|
|
53
|
+
// Start with a single secret
|
|
54
|
+
let sessionCookie = createCookie('session', {
|
|
55
|
+
secrets: ['secret1'],
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
console.log(sessionCookie.signed) // true
|
|
59
|
+
|
|
60
|
+
let response = new Response('Hello, world!', {
|
|
61
|
+
headers: {
|
|
62
|
+
'Set-Cookie': await sessionCookie.serialize(value),
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
All cookies sent in this scenario will be signed with the secret `secret1`. Later, when it's time to rotate secrets, add a new secret to the beginning of the array and all existing cookies will still be able to be parsed.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
let sessionCookie = createCookie('session', {
|
|
71
|
+
secrets: ['secret2', 'secret1'],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// This works for cookies signed with either secret
|
|
75
|
+
let value = await sessionCookie.parse(request.headers.get('Cookie'))
|
|
76
|
+
|
|
77
|
+
// Newly serialized cookies will be signed with the new secret
|
|
78
|
+
let response = new Response('Hello, world!', {
|
|
79
|
+
headers: {
|
|
80
|
+
'Set-Cookie': await sessionCookie.serialize(value),
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Custom Encoding
|
|
86
|
+
|
|
87
|
+
By default, [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) and [`decodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent) are used to encode and decode the cookie value. This is suitable for most use cases, but you can provide your own functions to customize the encoding and decoding of the cookie value.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
let sessionCookie = createCookie('session', {
|
|
91
|
+
encode: (value) => value,
|
|
92
|
+
decode: (value) => value,
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This can be useful for viewing the value of cookies in a human-readable format in the browser's developer tools. But you should be sure that the cookie value contains only characters that are [valid in a cookie value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#attributes).
|
|
97
|
+
|
|
98
|
+
## Related Packages
|
|
99
|
+
|
|
100
|
+
- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Type-safe HTTP header manipulation
|
|
101
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Build HTTP routers using the web fetch API
|
|
102
|
+
- [`node-fetch-server`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server) - Build HTTP servers on Node.js using the web fetch API
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# cop-middleware
|
|
2
|
+
|
|
3
|
+
Cross-origin protection middleware for Remix. It mirrors Go's `CrossOriginProtection` by rejecting unsafe cross-origin browser requests without synchronizer tokens.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Browser Provenance Checks** - Uses `Sec-Fetch-Site` when present and falls back to `Origin`
|
|
8
|
+
- **Trusted Origins** - Allow specific cross-origin callers by exact origin
|
|
9
|
+
- **Explicit Escape Hatches** - Support insecure bypass patterns for endpoints like webhooks
|
|
10
|
+
- **No Session State** - Does not require synchronizer tokens or server-side CSRF storage
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i remix
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createRouter } from 'remix/router'
|
|
22
|
+
import { cop } from 'remix/middleware/cop'
|
|
23
|
+
|
|
24
|
+
let router = createRouter({
|
|
25
|
+
middleware: [cop()],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Behavior
|
|
30
|
+
|
|
31
|
+
For unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), `cop()` follows the same broad model as Go's `CrossOriginProtection`:
|
|
32
|
+
|
|
33
|
+
- Allow `Sec-Fetch-Site: same-origin`
|
|
34
|
+
- Allow `Sec-Fetch-Site: none`
|
|
35
|
+
- Reject other `Sec-Fetch-Site` values unless the request matches a trusted origin or insecure bypass
|
|
36
|
+
- If `Sec-Fetch-Site` is missing, compare `Origin` to the request host
|
|
37
|
+
- If both `Sec-Fetch-Site` and `Origin` are missing, allow the request
|
|
38
|
+
|
|
39
|
+
This middleware is intentionally tokenless. If you cannot guarantee the deployment assumptions behind that model, prefer [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware).
|
|
40
|
+
|
|
41
|
+
## Caveats
|
|
42
|
+
|
|
43
|
+
- `cop()` is a browser-origin guard, not a universal CSRF solution. It is designed for deployments that can rely on modern browser provenance signals and same-origin request handling.
|
|
44
|
+
- If both `Sec-Fetch-Site` and `Origin` are missing on an unsafe request, `cop()` allows the request to continue. This is intentional so older clients and non-browser callers do not fail closed by default.
|
|
45
|
+
- If `Sec-Fetch-Site` is missing, `cop()` only rejects when `Origin` is present and does not match the request host.
|
|
46
|
+
- If you need stronger guarantees for session-backed form workflows, mixed deployment environments, or requests that should not fall through when browser provenance headers are missing, use [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware) or layer both middlewares together.
|
|
47
|
+
|
|
48
|
+
## Using with csrf-middleware
|
|
49
|
+
|
|
50
|
+
You can also layer `cop()` in front of `csrf()` when you want both browser provenance checks and session-backed synchronizer tokens.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createCookie } from 'remix/cookie'
|
|
54
|
+
import { createRouter } from 'remix/router'
|
|
55
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
56
|
+
import { session } from 'remix/middleware/session'
|
|
57
|
+
import { cop } from 'remix/middleware/cop'
|
|
58
|
+
import { csrf } from 'remix/middleware/csrf'
|
|
59
|
+
|
|
60
|
+
let sessionCookie = createCookie('__session', { secrets: ['secret1'] })
|
|
61
|
+
let sessionStorage = createCookieSessionStorage()
|
|
62
|
+
|
|
63
|
+
let router = createRouter({
|
|
64
|
+
middleware: [cop(), session(sessionCookie, sessionStorage), csrf()],
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In this setup, `cop()` runs first and rejects unsafe cross-origin browser requests early using `Sec-Fetch-Site` and `Origin`. Requests that pass `cop()` continue into `csrf()`, which still enforces synchronizer-token validation and origin checks for the remaining traffic.
|
|
69
|
+
|
|
70
|
+
## Trusted Origins
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createRouter } from 'remix/router'
|
|
74
|
+
import { cop } from 'remix/middleware/cop'
|
|
75
|
+
|
|
76
|
+
let router = createRouter({
|
|
77
|
+
middleware: [
|
|
78
|
+
cop({
|
|
79
|
+
trustedOrigins: ['https://admin.example.com'],
|
|
80
|
+
}),
|
|
81
|
+
],
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Trusted origins must be exact origin values in the form `scheme://host[:port]`.
|
|
86
|
+
|
|
87
|
+
## Insecure Bypass Patterns
|
|
88
|
+
|
|
89
|
+
Bypass patterns intentionally weaken protection for specific endpoints. They support:
|
|
90
|
+
|
|
91
|
+
- Optional method prefixes, for example `POST /webhooks/{provider}`
|
|
92
|
+
- Exact paths, for example `/healthz`
|
|
93
|
+
- Trailing-slash subtree patterns, for example `/webhooks/`
|
|
94
|
+
- Single-segment wildcards with `{name}`
|
|
95
|
+
- Tail wildcards with `{name...}`
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createRouter } from 'remix/router'
|
|
99
|
+
import { cop } from 'remix/middleware/cop'
|
|
100
|
+
|
|
101
|
+
let router = createRouter({
|
|
102
|
+
middleware: [
|
|
103
|
+
cop({
|
|
104
|
+
insecureBypassPatterns: ['POST /webhooks/{provider}', '/healthz'],
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Related Packages
|
|
111
|
+
|
|
112
|
+
- [`csrf-middleware`](https://github.com/remix-run/remix/tree/main/packages/csrf-middleware) - Session-backed CSRF protection with synchronizer tokens
|
|
113
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# cors-middleware
|
|
2
|
+
|
|
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/router'
|
|
23
|
+
import { cors } from 'remix/middleware/cors'
|
|
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)
|