remix 3.0.0-beta.0 → 3.0.0-beta.10
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/README.md +4 -2
- package/dist/assets/types/hmr.d.ts +2 -0
- package/dist/cli-entry.js +1 -1
- package/dist/data-table/cli.d.ts +2 -0
- package/dist/data-table/cli.d.ts.map +1 -0
- package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
- package/dist/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/headers/accept-encoding.d.ts +2 -0
- package/dist/headers/accept-encoding.d.ts.map +1 -0
- package/dist/headers/accept-encoding.js +2 -0
- package/dist/headers/accept-language.d.ts +2 -0
- package/dist/headers/accept-language.d.ts.map +1 -0
- package/dist/headers/accept-language.js +2 -0
- package/dist/headers/accept.d.ts +2 -0
- package/dist/headers/accept.d.ts.map +1 -0
- package/dist/headers/accept.js +2 -0
- package/dist/headers/cache-control.d.ts +2 -0
- package/dist/headers/cache-control.d.ts.map +1 -0
- package/dist/headers/cache-control.js +2 -0
- package/dist/headers/content-disposition.d.ts +2 -0
- package/dist/headers/content-disposition.d.ts.map +1 -0
- package/dist/headers/content-disposition.js +2 -0
- package/dist/headers/content-range.d.ts +2 -0
- package/dist/headers/content-range.d.ts.map +1 -0
- package/dist/headers/content-range.js +2 -0
- package/dist/headers/content-type.d.ts +2 -0
- package/dist/headers/content-type.d.ts.map +1 -0
- package/dist/headers/content-type.js +2 -0
- package/dist/headers/cookie.d.ts +2 -0
- package/dist/headers/cookie.d.ts.map +1 -0
- package/dist/headers/cookie.js +2 -0
- package/dist/headers/if-match.d.ts +2 -0
- package/dist/headers/if-match.d.ts.map +1 -0
- package/dist/headers/if-match.js +2 -0
- package/dist/headers/if-none-match.d.ts +2 -0
- package/dist/headers/if-none-match.d.ts.map +1 -0
- package/dist/headers/if-none-match.js +2 -0
- package/dist/headers/if-range.d.ts +2 -0
- package/dist/headers/if-range.d.ts.map +1 -0
- package/dist/headers/if-range.js +2 -0
- package/dist/headers/range.d.ts +2 -0
- package/dist/headers/range.d.ts.map +1 -0
- package/dist/headers/range.js +2 -0
- package/dist/headers/raw-headers.d.ts +2 -0
- package/dist/headers/raw-headers.d.ts.map +1 -0
- package/dist/headers/raw-headers.js +2 -0
- package/dist/headers/set-cookie.d.ts +2 -0
- package/dist/headers/set-cookie.d.ts.map +1 -0
- package/dist/headers/set-cookie.js +2 -0
- package/dist/headers/vary.d.ts +2 -0
- package/dist/headers/vary.d.ts.map +1 -0
- package/dist/{ui/separator.js → headers/vary.js} +1 -1
- package/dist/node-hmr/runtime.d.ts +2 -0
- package/dist/node-hmr/runtime.d.ts.map +1 -0
- package/dist/node-hmr/runtime.js +2 -0
- package/dist/node-hmr/types.d.ts +2 -0
- package/dist/node-hmr.d.ts +2 -0
- package/dist/node-hmr.d.ts.map +1 -0
- package/dist/{ui/glyph.js → node-hmr.js} +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/dist/node-tsx.js +3 -0
- 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/dist/ui/accordion/primitives.d.ts +2 -0
- package/dist/ui/accordion/primitives.d.ts.map +1 -0
- package/dist/ui/accordion/primitives.js +2 -0
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.d.ts.map +1 -1
- package/dist/ui/button.js +1 -0
- package/dist/ui/checkbox.d.ts +3 -0
- package/dist/ui/checkbox.d.ts.map +1 -0
- package/dist/ui/checkbox.js +3 -0
- package/dist/ui/combobox/primitives.d.ts +2 -0
- package/dist/ui/combobox/primitives.d.ts.map +1 -0
- package/dist/ui/combobox/primitives.js +2 -0
- package/dist/ui/dev/refresh.d.ts +2 -0
- package/dist/ui/dev/refresh.d.ts.map +1 -0
- package/dist/ui/dev/refresh.js +2 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.d.ts.map +1 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/menu/primitives.d.ts +2 -0
- package/dist/ui/menu/primitives.d.ts.map +1 -0
- package/dist/ui/menu/primitives.js +2 -0
- package/dist/ui/radio.d.ts +3 -0
- package/dist/ui/radio.d.ts.map +1 -0
- package/dist/ui/radio.js +3 -0
- package/dist/ui/select/primitives.d.ts +2 -0
- package/dist/ui/select/primitives.d.ts.map +1 -0
- package/dist/ui/select/primitives.js +2 -0
- package/dist/ui/tabs/primitives.d.ts +2 -0
- package/dist/ui/tabs/primitives.d.ts.map +1 -0
- package/dist/ui/tabs/primitives.js +2 -0
- package/dist/ui/tabs.d.ts +2 -0
- package/dist/ui/tabs.d.ts.map +1 -0
- package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
- package/dist/ui/toggle/primitives.d.ts +2 -0
- package/dist/ui/toggle/primitives.d.ts.map +1 -0
- package/dist/ui/toggle/primitives.js +2 -0
- package/dist/ui/toggle.d.ts +3 -0
- package/dist/ui/toggle.d.ts.map +1 -0
- package/dist/ui/toggle.js +3 -0
- package/dist/ui-hmr/assets.d.ts +2 -0
- package/dist/ui-hmr/assets.d.ts.map +1 -0
- package/dist/ui-hmr/assets.js +2 -0
- package/dist/ui-hmr/node.d.ts +3 -0
- package/dist/ui-hmr/node.d.ts.map +1 -0
- package/dist/ui-hmr/node.js +3 -0
- package/dist/ui-hmr/runtime/browser.d.ts +2 -0
- package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/browser.js +2 -0
- package/dist/ui-hmr/runtime/server.d.ts +2 -0
- package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
- package/dist/ui-hmr/runtime/server.js +2 -0
- package/dist/ui-hmr.d.ts +2 -0
- package/dist/ui-hmr.d.ts.map +1 -0
- package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
- package/package.json +284 -129
- package/src/assert/README.md +116 -0
- package/src/assets/README.md +805 -0
- package/src/assets/types/hmr.d.ts +2 -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 +182 -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 +92 -0
- package/src/data-schema/README.md +416 -0
- package/src/data-table/README.md +678 -0
- package/src/data-table/cli.ts +2 -0
- package/src/data-table-mysql/README.md +120 -0
- package/src/data-table-postgres/README.md +98 -0
- package/src/data-table-sqlite/README.md +100 -0
- package/src/fetch-proxy/README.md +70 -0
- package/src/fetch-router/README.md +982 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +46 -0
- package/src/form-data-middleware/README.md +108 -0
- package/src/form-data-parser/README.md +158 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +639 -0
- package/src/headers/accept-encoding.ts +2 -0
- package/src/headers/accept-language.ts +2 -0
- package/src/headers/accept.ts +2 -0
- package/src/headers/cache-control.ts +2 -0
- package/src/headers/content-disposition.ts +2 -0
- package/src/headers/content-range.ts +2 -0
- package/src/headers/content-type.ts +2 -0
- package/src/headers/cookie.ts +2 -0
- package/src/headers/if-match.ts +2 -0
- package/src/headers/if-none-match.ts +2 -0
- package/src/headers/if-range.ts +2 -0
- package/src/headers/range.ts +2 -0
- package/src/headers/raw-headers.ts +2 -0
- package/src/headers/set-cookie.ts +2 -0
- package/src/{node-serve.ts → headers/vary.ts} +1 -1
- 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 +117 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +377 -0
- package/src/node-hmr/README.md +307 -0
- package/src/node-hmr/runtime.ts +2 -0
- package/src/node-hmr/types.d.ts +2 -0
- package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
- package/src/node-tsx/README.md +74 -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 +424 -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 +111 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +36 -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 +500 -0
- package/src/ui/README.md +216 -0
- package/src/ui/accordion/README.md +202 -0
- package/src/ui/accordion/primitives/README.md +202 -0
- package/src/ui/accordion/primitives.ts +2 -0
- package/src/ui/anchor/README.md +196 -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/button.ts +1 -0
- package/src/ui/checkbox/README.md +59 -0
- package/src/ui/checkbox.ts +3 -0
- package/src/ui/combobox/README.md +194 -0
- package/src/ui/combobox/primitives/README.md +194 -0
- package/src/ui/combobox/primitives.ts +2 -0
- package/src/ui/dev/refresh.ts +2 -0
- package/src/ui/input/README.md +52 -0
- package/src/ui/input.ts +3 -0
- package/src/ui/listbox/README.md +83 -0
- package/src/ui/menu/README.md +161 -0
- package/src/ui/menu/primitives/README.md +161 -0
- package/src/ui/menu/primitives.ts +2 -0
- package/src/ui/popover/README.md +103 -0
- package/src/ui/radio/README.md +53 -0
- package/src/ui/radio.ts +3 -0
- package/src/ui/select/README.md +117 -0
- package/src/ui/select/primitives/README.md +117 -0
- package/src/ui/select/primitives.ts +2 -0
- package/src/ui/server/README.md +103 -0
- package/src/ui/tabs/README.md +141 -0
- package/src/ui/tabs/primitives/README.md +141 -0
- package/src/ui/tabs/primitives.ts +2 -0
- package/src/ui/tabs.ts +2 -0
- package/src/ui/test/README.md +198 -0
- package/src/ui/toggle/README.md +56 -0
- package/src/ui/toggle/primitives/README.md +56 -0
- package/src/ui/toggle/primitives.ts +2 -0
- package/src/ui/toggle.ts +3 -0
- package/src/ui-hmr/README.md +119 -0
- package/src/ui-hmr/assets.ts +2 -0
- package/src/ui-hmr/node.ts +3 -0
- package/src/ui-hmr/runtime/browser.ts +2 -0
- package/src/ui-hmr/runtime/server.ts +2 -0
- package/src/ui-hmr.ts +2 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
- package/dist/ui/glyph.d.ts +0 -2
- package/dist/ui/glyph.d.ts.map +0 -1
- package/dist/ui/scroll-lock.d.ts +0 -2
- package/dist/ui/scroll-lock.d.ts.map +0 -1
- package/dist/ui/separator.d.ts +0 -2
- package/dist/ui/separator.d.ts.map +0 -1
- package/dist/ui/theme.d.ts +0 -2
- package/dist/ui/theme.d.ts.map +0 -1
- package/src/ui/scroll-lock.ts +0 -2
- package/src/ui/separator.ts +0 -2
|
@@ -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, cookie values are percent-encoded and wrapped in base64 so arbitrary string values can be safely stored in cookies. 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.replaceAll(' ', '-'),
|
|
92
|
+
decode: (value) => value.replaceAll('-', ' '),
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Custom `encode` functions are used as the full cookie value codec. Their return values are signed when `secrets` are configured and then serialized as-is, without the default base64 wrapper. 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 encoded 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) and safe for HTTP headers. Raw Unicode characters should be escaped or encoded by your custom function before they are returned.
|
|
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)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# csrf-middleware
|
|
2
|
+
|
|
3
|
+
CSRF protection middleware for Remix. It provides synchronizer-token validation backed by session storage, plus origin checks for unsafe requests.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Session-Backed Tokens** - Creates and persists CSRF tokens in the request session
|
|
8
|
+
- **Flexible Token Extraction** - Reads tokens from headers, form fields, query params, or a custom resolver
|
|
9
|
+
- **Origin Validation** - Validates `Origin`/`Referer` for unsafe methods with customizable policies
|
|
10
|
+
- **Configurable Enforcement** - Control safe methods, token keys, and failure responses
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm i remix
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
This middleware requires [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) to run before it.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { createCookie } from 'remix/cookie'
|
|
24
|
+
import { createRouter } from 'remix/router'
|
|
25
|
+
import { createCookieSessionStorage } from 'remix/session-storage/cookie'
|
|
26
|
+
import { session } from 'remix/middleware/session'
|
|
27
|
+
import { csrf, getCsrfToken } from 'remix/middleware/csrf'
|
|
28
|
+
|
|
29
|
+
let sessionCookie = createCookie('__session', { secrets: ['secret1'] })
|
|
30
|
+
let sessionStorage = createCookieSessionStorage()
|
|
31
|
+
|
|
32
|
+
let router = createRouter({
|
|
33
|
+
middleware: [session(sessionCookie, sessionStorage), csrf()],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
router.get('/form', (context) => {
|
|
37
|
+
let token = getCsrfToken(context)
|
|
38
|
+
|
|
39
|
+
return new Response(`
|
|
40
|
+
<form method="post" action="/submit">
|
|
41
|
+
<input type="hidden" name="_csrf" value="${token}" />
|
|
42
|
+
<button type="submit">Submit</button>
|
|
43
|
+
</form>
|
|
44
|
+
`)
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Token Sources
|
|
49
|
+
|
|
50
|
+
By default, `csrf()` checks token values in this order:
|
|
51
|
+
|
|
52
|
+
1. Request headers: `X-Csrf-Token`, `X-Xsrf-Token`, `Csrf-Token`
|
|
53
|
+
2. Form field: `_csrf` (requires `formData()` middleware to parse request bodies)
|
|
54
|
+
3. Query param: `_csrf`
|
|
55
|
+
|
|
56
|
+
You can override extraction using `value(context)`.
|
|
57
|
+
|
|
58
|
+
Headers and form fields are the preferred transports. Query param fallback exists for compatibility, but it is the weakest option because tokens are more likely to be exposed in logs, history, and copied URLs.
|
|
59
|
+
|
|
60
|
+
## Origin Validation
|
|
61
|
+
|
|
62
|
+
For unsafe methods (`POST`, `PUT`, `PATCH`, `DELETE`), the middleware validates request origin.
|
|
63
|
+
|
|
64
|
+
- Default: same-origin validation when `Origin` or `Referer` is present
|
|
65
|
+
- Custom: provide `origin` as string, regex, array, or function
|
|
66
|
+
- Missing origin behavior: controlled by `allowMissingOrigin` (default `true`)
|
|
67
|
+
|
|
68
|
+
## Caveats
|
|
69
|
+
|
|
70
|
+
- The synchronizer token is the primary defense in `csrf()`. `Origin` and `Referer` checks are an additional signal, not the only protection.
|
|
71
|
+
- By default, unsafe requests with a valid token still pass when `Origin` and `Referer` are both missing. Set `allowMissingOrigin: false` if your deployment wants to require provenance headers on unsafe requests.
|
|
72
|
+
- Query param tokens are supported for compatibility, but they should not be the default recommendation. Prefer headers or hidden form fields when you control the client.
|
|
73
|
+
- If you want to reject more unsafe requests before token validation, especially when browser provenance headers are available, layer [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) in front of `csrf()`.
|
|
74
|
+
|
|
75
|
+
## Why This Exists
|
|
76
|
+
|
|
77
|
+
Modern browsers now provide stronger cross-origin signals like `Sec-Fetch-Site`, and explicit `SameSite=Lax` cookies already block many CSRF attacks. We have considered the lighter, tokenless model used by Go's `CrossOriginProtection`, and we think it is a good fit when a deployment can make all of the guarantees that model depends on.
|
|
78
|
+
|
|
79
|
+
Remix cannot assume those guarantees for every app. `csrf()` still exists as the conservative option for apps that want synchronizer tokens in addition to origin checks, especially for session-backed HTML form workflows and mixed deployment environments.
|
|
80
|
+
|
|
81
|
+
If your deployment can guarantee the prerequisites for the tokenless model, this middleware is optional. In that case, [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) may be a better fit.
|
|
82
|
+
|
|
83
|
+
## Related Packages
|
|
84
|
+
|
|
85
|
+
- [`cop-middleware`](https://github.com/remix-run/remix/tree/main/packages/cop-middleware) - Middleware for tokenless cross-origin protection using browser provenance headers
|
|
86
|
+
- [`fetch-router`](https://github.com/remix-run/remix/tree/main/packages/fetch-router) - Router for the web Fetch API
|
|
87
|
+
- [`session-middleware`](https://github.com/remix-run/remix/tree/main/packages/session-middleware) - Session middleware required by `csrf()`
|
|
88
|
+
- [`form-data-middleware`](https://github.com/remix-run/remix/tree/main/packages/form-data-middleware) - Needed for form body token extraction
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|