create-acrylblends 0.0.0-stage → 0.1.0
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/bin/create-acrylblends.js +4 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +274 -0
- package/dist/index.js.map +1 -0
- package/docs/framework/LICENSE +23 -0
- package/docs/framework/README.md +63 -0
- package/docs/framework/UPSTREAM.md +3 -0
- package/docs/framework/agents/index.md +24 -0
- package/docs/framework/api/acryl.md +259 -0
- package/docs/framework/api/context.md +527 -0
- package/docs/framework/api/exception.md +80 -0
- package/docs/framework/api/index.md +12 -0
- package/docs/framework/api/presets.md +65 -0
- package/docs/framework/api/request.md +396 -0
- package/docs/framework/api/routing.md +303 -0
- package/docs/framework/api/summary.md +140 -0
- package/docs/framework/concepts/benchmarks.md +26 -0
- package/docs/framework/concepts/developer-experience.md +5 -0
- package/docs/framework/concepts/middleware.md +21 -0
- package/docs/framework/concepts/motivation.md +14 -0
- package/docs/framework/concepts/routers.md +63 -0
- package/docs/framework/concepts/stacks.md +235 -0
- package/docs/framework/concepts/web-standard.md +37 -0
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +146 -0
- package/docs/framework/cordis/concepts.md +105 -0
- package/docs/framework/cordis/middleware.md +8 -0
- package/docs/framework/cordis/migration-from-0-3.md +111 -0
- package/docs/framework/cordis/migration-from-0-4.md +14 -0
- package/docs/framework/cordis/migration-from-dsh-webserver.md +103 -0
- package/docs/framework/cordis/mounting.md +85 -0
- package/docs/framework/cordis/node-server.md +75 -0
- package/docs/framework/cordis/plugin-lifecycle.md +111 -0
- package/docs/framework/cordis/surfaces.md +71 -0
- package/docs/framework/cordis/transport-and-cancellation.md +57 -0
- package/docs/framework/cordis/validation.md +135 -0
- package/docs/framework/getting-started/basic.md +196 -0
- package/docs/framework/getting-started/bun.md +221 -0
- package/docs/framework/getting-started/nodejs.md +344 -0
- package/docs/framework/getting-started/other-runtimes.md +23 -0
- package/docs/framework/guides/best-practices.md +231 -0
- package/docs/framework/guides/create-acrylblends.md +79 -0
- package/docs/framework/guides/examples.md +3 -0
- package/docs/framework/guides/faq.md +19 -0
- package/docs/framework/guides/helpers.md +33 -0
- package/docs/framework/guides/jsx-dom.md +299 -0
- package/docs/framework/guides/jsx.md +440 -0
- package/docs/framework/guides/middleware.md +253 -0
- package/docs/framework/guides/rpc.md +886 -0
- package/docs/framework/guides/testing.md +133 -0
- package/docs/framework/guides/validation.md +383 -0
- package/docs/framework/helpers/accepts.md +60 -0
- package/docs/framework/helpers/adapter.md +87 -0
- package/docs/framework/helpers/conninfo.md +112 -0
- package/docs/framework/helpers/cookie.md +232 -0
- package/docs/framework/helpers/css.md +285 -0
- package/docs/framework/helpers/dev.md +67 -0
- package/docs/framework/helpers/factory.md +180 -0
- package/docs/framework/helpers/html.md +123 -0
- package/docs/framework/helpers/jwt.md +184 -0
- package/docs/framework/helpers/proxy.md +118 -0
- package/docs/framework/helpers/route.md +143 -0
- package/docs/framework/helpers/ssg.md +393 -0
- package/docs/framework/helpers/streaming.md +119 -0
- package/docs/framework/helpers/testing.md +101 -0
- package/docs/framework/helpers/websocket.md +215 -0
- package/docs/framework/middleware/builtin/basic-auth.md +165 -0
- package/docs/framework/middleware/builtin/bearer-auth.md +138 -0
- package/docs/framework/middleware/builtin/body-limit.md +71 -0
- package/docs/framework/middleware/builtin/cache.md +122 -0
- package/docs/framework/middleware/builtin/combine.md +94 -0
- package/docs/framework/middleware/builtin/compress.md +59 -0
- package/docs/framework/middleware/builtin/context-storage.md +78 -0
- package/docs/framework/middleware/builtin/cors.md +135 -0
- package/docs/framework/middleware/builtin/csrf.md +105 -0
- package/docs/framework/middleware/builtin/etag.md +54 -0
- package/docs/framework/middleware/builtin/ip-restriction.md +69 -0
- package/docs/framework/middleware/builtin/jsx-renderer.md +286 -0
- package/docs/framework/middleware/builtin/jwk.md +192 -0
- package/docs/framework/middleware/builtin/jwt.md +145 -0
- package/docs/framework/middleware/builtin/language.md +232 -0
- package/docs/framework/middleware/builtin/logger.md +74 -0
- package/docs/framework/middleware/builtin/loopback-only.md +31 -0
- package/docs/framework/middleware/builtin/method-not-allowed.md +48 -0
- package/docs/framework/middleware/builtin/method-override.md +80 -0
- package/docs/framework/middleware/builtin/mount.md +106 -0
- package/docs/framework/middleware/builtin/pretty-json.md +53 -0
- package/docs/framework/middleware/builtin/request-id.md +94 -0
- package/docs/framework/middleware/builtin/secure-headers.md +247 -0
- package/docs/framework/middleware/builtin/timeout.md +98 -0
- package/docs/framework/middleware/builtin/timing.md +101 -0
- package/docs/framework/middleware/builtin/trailing-slash.md +74 -0
- package/docs/framework/middleware/third-party.md +4 -0
- package/docs/framework/upstream.json +9 -0
- package/package.json +47 -3
- package/templates/bun/gitignore +1 -0
- package/templates/bun/package.json +19 -0
- package/templates/bun/src/index.ts +7 -0
- package/templates/bun/test/app.test.ts +8 -0
- package/templates/bun/tsconfig.json +15 -0
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +22 -0
- package/templates/nodejs/src/app.ts +7 -0
- package/templates/nodejs/src/index.ts +9 -0
- package/templates/nodejs/test/app.test.ts +9 -0
- package/templates/nodejs/tsconfig.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Cache Middleware
|
|
2
|
+
|
|
3
|
+
> **Not yet available.** The cache middleware (`acrylblends/cache`) is documented here so the page structure matches the rest of the framework, and is not part of acrylblends yet. The code on this page is not checked by the docs tests.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
The Cache middleware uses the Web Standards' [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
|
|
7
|
+
|
|
8
|
+
The Cache middleware currently supports Cloudflare Workers projects using custom domains and Deno projects using [Deno 1.26+](https://github.com/denoland/deno/releases/tag/v1.26.0). Also available with Deno Deploy.
|
|
9
|
+
|
|
10
|
+
Cloudflare Workers respects the `Cache-Control` header and return cached responses. For details, refer to [Cache on Cloudflare Docs](https://developers.cloudflare.com/workers/runtime-apis/cache/). Deno does not respect headers, so if you need to update the cache, you will need to implement your own mechanism.
|
|
11
|
+
|
|
12
|
+
See [Usage](#usage) below for instructions on each platform.
|
|
13
|
+
|
|
14
|
+
## Import
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { Acryl } from 'acrylblends'
|
|
18
|
+
import { cache } from 'acrylblends/cache'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
**Cloudflare Workers**
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
app.get(
|
|
27
|
+
'*',
|
|
28
|
+
cache({
|
|
29
|
+
cacheName: 'my-app',
|
|
30
|
+
cacheControl: 'max-age=3600',
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Deno**
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// Must use `wait: true` for the Deno runtime
|
|
39
|
+
app.get(
|
|
40
|
+
'*',
|
|
41
|
+
cache({
|
|
42
|
+
cacheName: 'my-app',
|
|
43
|
+
cacheControl: 'max-age=3600',
|
|
44
|
+
wait: true,
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Caching QUERY requests
|
|
50
|
+
|
|
51
|
+
The Cache middleware also caches responses to [QUERY](https://www.rfc-editor.org/rfc/rfc10008.html) requests. As required by RFC 10008, the cache key for a QUERY request incorporates a digest of the request content and its representation metadata, so requests with different bodies are cached separately.
|
|
52
|
+
|
|
53
|
+
QUERY requests with a body larger than `maxQueryBodySize` (64 KiB by default) bypass the cache.
|
|
54
|
+
|
|
55
|
+
> **Note.** To support this, cached entries are stored under an internal key, so they do not collide with your own requests.
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
### **(required)** cacheName: `string` | `(c: Context) => string` | `Promise<string>`
|
|
60
|
+
|
|
61
|
+
The name of the cache. Can be used to store multiple caches with different identifiers.
|
|
62
|
+
|
|
63
|
+
### *(optional)* wait: `boolean`
|
|
64
|
+
|
|
65
|
+
A boolean indicating if Acryl should wait for the Promise of the `cache.put` function to resolve before continuing with the request. _Required to be true for the Deno environment_. The default is `false`.
|
|
66
|
+
|
|
67
|
+
### *(optional)* cacheControl: `string`
|
|
68
|
+
|
|
69
|
+
A string of directives for the `Cache-Control` header. See the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) for more information. When this option is not provided, no `Cache-Control` header is added to requests.
|
|
70
|
+
|
|
71
|
+
### *(optional)* vary: `string` | `string[]`
|
|
72
|
+
|
|
73
|
+
Sets the `Vary` header in the response. If the original response header already contains a `Vary` header, the values are merged, removing any duplicates. Setting this to `*` will result in an error. For more details on the Vary header and its implications for caching strategies, refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary).
|
|
74
|
+
|
|
75
|
+
### *(optional)* keyGenerator: `(c: Context) => string | Promise<string>`
|
|
76
|
+
|
|
77
|
+
Generates keys for every request in the `cacheName` store. This can be used to cache data based on request parameters or context parameters. The default is `c.req.url`. For QUERY requests, the key additionally includes a digest of the request content and its representation metadata.
|
|
78
|
+
|
|
79
|
+
### *(optional)* maxQueryBodySize: `number`
|
|
80
|
+
|
|
81
|
+
The maximum QUERY request body size in bytes that can be cached. QUERY requests with a larger body bypass the cache. The default is `65536` (64 KiB).
|
|
82
|
+
|
|
83
|
+
### *(optional)* cacheableStatusCodes: `number[]`
|
|
84
|
+
|
|
85
|
+
An array of status codes that should be cached. The default is `[200]`. Use this option to cache responses with specific status codes.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
app.get(
|
|
89
|
+
'*',
|
|
90
|
+
cache({
|
|
91
|
+
cacheName: 'my-app',
|
|
92
|
+
cacheControl: 'max-age=3600',
|
|
93
|
+
cacheableStatusCodes: [200, 404, 412],
|
|
94
|
+
})
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### *(optional)* onCacheNotAvailable: `((reason: string) => void | Promise<void>)` | `false`
|
|
99
|
+
|
|
100
|
+
A callback function or `false` that controls the behavior when the Cache API is not available in the global scope, or when QUERY caching cannot use Web Crypto. The callback is invoked with the reason. By default, the reason is logged with `console.log`. You can provide a custom function to customize the behavior, or set it to `false` to suppress the log entirely.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
// Custom logging
|
|
104
|
+
app.use(
|
|
105
|
+
cache({
|
|
106
|
+
cacheName: 'my-app-v1',
|
|
107
|
+
onCacheNotAvailable: () => {
|
|
108
|
+
console.log('Custom log: Cache API is not available.')
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// Suppress logging
|
|
116
|
+
app.use(
|
|
117
|
+
cache({
|
|
118
|
+
cacheName: 'my-app-v1',
|
|
119
|
+
onCacheNotAvailable: false,
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Combine Middleware
|
|
2
|
+
|
|
3
|
+
Combine Middleware combines multiple middleware functions into a single middleware. It provides three functions:
|
|
4
|
+
|
|
5
|
+
- `some` - Runs only one of the given middleware.
|
|
6
|
+
- `every` - Runs all given middleware.
|
|
7
|
+
- `except` - Runs all given middleware only if a condition is not met.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Acryl } from 'acrylblends'
|
|
13
|
+
import { some, every, except } from 'acrylblends/combine'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Here's an example of complex access control rules using Combine Middleware.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Acryl } from 'acrylblends'
|
|
22
|
+
import { bearerAuth } from 'acrylblends/bearer-auth'
|
|
23
|
+
import { getConnInfo } from 'acrylblends/node-server'
|
|
24
|
+
import { every, some } from 'acrylblends/combine'
|
|
25
|
+
import { ipRestriction } from 'acrylblends/ip-restriction'
|
|
26
|
+
import { rateLimit } from '@/my-rate-limit'
|
|
27
|
+
|
|
28
|
+
const app = new Acryl()
|
|
29
|
+
|
|
30
|
+
app.use(
|
|
31
|
+
'*',
|
|
32
|
+
some(
|
|
33
|
+
every(
|
|
34
|
+
ipRestriction(getConnInfo, { allowList: ['192.168.0.2'] }),
|
|
35
|
+
bearerAuth({ token })
|
|
36
|
+
),
|
|
37
|
+
// If both conditions are met, rateLimit will not execute.
|
|
38
|
+
rateLimit()
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app.get('/', (c) => c.text('Hello Acryl!'))
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### some
|
|
46
|
+
|
|
47
|
+
Runs the first middleware that returns true. Middleware is applied in order, and if any middleware exits successfully, subsequent middleware will not run.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { some } from 'acrylblends/combine'
|
|
51
|
+
import { bearerAuth } from 'acrylblends/bearer-auth'
|
|
52
|
+
import { myRateLimit } from '@/rate-limit'
|
|
53
|
+
|
|
54
|
+
// If client has a valid token, skip rate limiting.
|
|
55
|
+
// Otherwise, apply rate limiting.
|
|
56
|
+
app.use(
|
|
57
|
+
'/api/*',
|
|
58
|
+
some(bearerAuth({ token }), myRateLimit({ limit: 100 }))
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### every
|
|
63
|
+
|
|
64
|
+
Runs all middleware and stops if any of them fail. Middleware is applied in order, and if any middleware throws an error, subsequent middleware will not run.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { some, every } from 'acrylblends/combine'
|
|
68
|
+
import { bearerAuth } from 'acrylblends/bearer-auth'
|
|
69
|
+
import { myCheckLocalNetwork } from '@/check-local-network'
|
|
70
|
+
import { myRateLimit } from '@/rate-limit'
|
|
71
|
+
|
|
72
|
+
// If client is in local network, skip authentication and rate limiting.
|
|
73
|
+
// Otherwise, apply authentication and rate limiting.
|
|
74
|
+
app.use(
|
|
75
|
+
'/api/*',
|
|
76
|
+
some(
|
|
77
|
+
myCheckLocalNetwork(),
|
|
78
|
+
every(bearerAuth({ token }), myRateLimit({ limit: 100 }))
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### except
|
|
84
|
+
|
|
85
|
+
Runs all middleware except when the condition is met. You can pass a string or function as the condition. If multiple targets need to be matched, pass them as an array.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { except } from 'acrylblends/combine'
|
|
89
|
+
import { bearerAuth } from 'acrylblends/bearer-auth'
|
|
90
|
+
|
|
91
|
+
// If client is accessing public API, skip authentication.
|
|
92
|
+
// Otherwise, require a valid token.
|
|
93
|
+
app.use('/api/*', except('/api/public/*', bearerAuth({ token })))
|
|
94
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Compress Middleware
|
|
2
|
+
|
|
3
|
+
This middleware compresses the response body, according to `Accept-Encoding` request header.
|
|
4
|
+
|
|
5
|
+
> **Note.** On Cloudflare Workers and Deno Deploy, the response body will be compressed automatically, so there is no need to use this middleware.
|
|
6
|
+
|
|
7
|
+
## Import
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Acryl } from 'acrylblends'
|
|
11
|
+
import { compress } from 'acrylblends/compress'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const app = new Acryl()
|
|
18
|
+
|
|
19
|
+
app.use(compress())
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Options
|
|
23
|
+
|
|
24
|
+
### *(optional)* encoding: `'gzip'` | `'deflate'`
|
|
25
|
+
|
|
26
|
+
The compression scheme to allow for response compression. Either `gzip` or `deflate`. If not defined, both are allowed and will be used based on the `Accept-Encoding` header. `gzip` is prioritized if this option is not provided and the client provides both in the `Accept-Encoding` header.
|
|
27
|
+
|
|
28
|
+
### *(optional)* threshold: `number`
|
|
29
|
+
|
|
30
|
+
The minimum size in bytes to compress. Defaults to 1024 bytes.
|
|
31
|
+
|
|
32
|
+
### *(optional)* contentTypeFilter: `RegExp` | `(contentType: string) => boolean`
|
|
33
|
+
|
|
34
|
+
A `RegExp` or function to determine whether the response should be compressed based on its `Content-Type`. By default, a built-in list of compressible Content-Types is used.
|
|
35
|
+
|
|
36
|
+
You can pass a `RegExp` to compress only matching Content-Types:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// Compress only JSON responses
|
|
40
|
+
app.use(compress({ contentTypeFilter: /^application\/json/ }))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or pass a function for custom logic. The built-in `COMPRESSIBLE_CONTENT_TYPE_REGEX` is also exported so you can extend the default behavior:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import {
|
|
47
|
+
compress,
|
|
48
|
+
COMPRESSIBLE_CONTENT_TYPE_REGEX,
|
|
49
|
+
} from 'acrylblends/compress'
|
|
50
|
+
|
|
51
|
+
// Compress the default Content-Types plus a custom one
|
|
52
|
+
app.use(
|
|
53
|
+
compress({
|
|
54
|
+
contentTypeFilter: (type) =>
|
|
55
|
+
COMPRESSIBLE_CONTENT_TYPE_REGEX.test(type) ||
|
|
56
|
+
type === 'application/x-myformat',
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Context Storage Middleware
|
|
2
|
+
|
|
3
|
+
The Context Storage Middleware stores the Acryl `Context` in the `AsyncLocalStorage`, to make it globally accessible.
|
|
4
|
+
|
|
5
|
+
> **Note.** This middleware uses `AsyncLocalStorage`. The runtime should support it.
|
|
6
|
+
>
|
|
7
|
+
> **Cloudflare Workers**: To enable `AsyncLocalStorage`, add the [`nodejs_compat` or `nodejs_als` flag](https://developers.cloudflare.com/workers/configuration/compatibility-dates/#nodejs-compatibility-flag) to your `wrangler.toml` file.
|
|
8
|
+
|
|
9
|
+
## Import
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Acryl } from 'acrylblends'
|
|
13
|
+
import {
|
|
14
|
+
contextStorage,
|
|
15
|
+
getContext,
|
|
16
|
+
tryGetContext,
|
|
17
|
+
} from 'acrylblends/context-storage'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
The `getContext()` will return the current Context object if the `contextStorage()` is applied as a middleware.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
type Env = {
|
|
26
|
+
Variables: {
|
|
27
|
+
message: string
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const app = new Acryl<Env>()
|
|
32
|
+
|
|
33
|
+
app.use(contextStorage())
|
|
34
|
+
|
|
35
|
+
app.use(async (c, next) => {
|
|
36
|
+
c.set('message', 'Hello!')
|
|
37
|
+
await next()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// You can access the variable outside the handler.
|
|
41
|
+
const getMessage = () => {
|
|
42
|
+
return getContext<Env>().var.message
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
app.get('/', (c) => {
|
|
46
|
+
return c.text(getMessage())
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
On Cloudflare Workers, you can access the bindings outside the handler.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
type Env = {
|
|
54
|
+
Bindings: {
|
|
55
|
+
KV: KVNamespace
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const app = new Acryl<Env>()
|
|
60
|
+
|
|
61
|
+
app.use(contextStorage())
|
|
62
|
+
|
|
63
|
+
const setKV = (value: string) => {
|
|
64
|
+
return getContext<Env>().env.KV.put('key', value)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## tryGetContext
|
|
69
|
+
|
|
70
|
+
`tryGetContext()` works like `getContext()`, but returns `undefined` instead of throwing an error when the context is not available:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const context = tryGetContext<Env>()
|
|
74
|
+
if (context) {
|
|
75
|
+
// Context is available
|
|
76
|
+
console.log(context.var.message)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# CORS Middleware
|
|
2
|
+
|
|
3
|
+
There are many use cases of Cloudflare Workers as Web APIs and calling them from external front-end application.
|
|
4
|
+
For them we have to implement CORS, let's do this with middleware as well.
|
|
5
|
+
|
|
6
|
+
## Import
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { Acryl } from 'acrylblends'
|
|
10
|
+
import { cors } from 'acrylblends/cors'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const app = new Acryl()
|
|
17
|
+
|
|
18
|
+
// CORS should be called before the route
|
|
19
|
+
app.use('/api/*', cors())
|
|
20
|
+
app.use(
|
|
21
|
+
'/api2/*',
|
|
22
|
+
cors({
|
|
23
|
+
origin: 'http://example.com',
|
|
24
|
+
allowHeaders: ['X-Custom-Header', 'Upgrade-Insecure-Requests'],
|
|
25
|
+
allowMethods: ['POST', 'GET', 'OPTIONS'],
|
|
26
|
+
exposeHeaders: ['Content-Length', 'X-Kuma-Revision'],
|
|
27
|
+
maxAge: 600,
|
|
28
|
+
credentials: true,
|
|
29
|
+
})
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
app.all('/api/abc', (c) => {
|
|
33
|
+
return c.json({ success: true })
|
|
34
|
+
})
|
|
35
|
+
app.all('/api2/abc', (c) => {
|
|
36
|
+
return c.json({ success: true })
|
|
37
|
+
})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Multiple origins:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
app.use(
|
|
44
|
+
'/api3/*',
|
|
45
|
+
cors({
|
|
46
|
+
origin: ['https://example.com', 'https://example.org'],
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// Or you can use "function"
|
|
51
|
+
app.use(
|
|
52
|
+
'/api4/*',
|
|
53
|
+
cors({
|
|
54
|
+
// `c` is a `Context` object
|
|
55
|
+
origin: (origin, c) => {
|
|
56
|
+
return origin.endsWith('.example.com')
|
|
57
|
+
? origin
|
|
58
|
+
: 'http://example.com'
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Dynamic allowed methods based on origin:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
app.use(
|
|
68
|
+
'/api5/*',
|
|
69
|
+
cors({
|
|
70
|
+
origin: (origin) =>
|
|
71
|
+
origin === 'https://example.com' ? origin : '*',
|
|
72
|
+
// `c` is a `Context` object
|
|
73
|
+
allowMethods: (origin, c) =>
|
|
74
|
+
origin === 'https://example.com'
|
|
75
|
+
? ['GET', 'HEAD', 'POST', 'PATCH', 'DELETE']
|
|
76
|
+
: ['GET', 'HEAD'],
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Options
|
|
82
|
+
|
|
83
|
+
### *(optional)* origin: `string` | `string[]` | `(origin:string, c:Context) => string`
|
|
84
|
+
|
|
85
|
+
The value of "_Access-Control-Allow-Origin_" CORS header. You can also pass the callback function like `origin: (origin) => (origin.endsWith('.example.com') ? origin : 'http://example.com')`. The default is `*`.
|
|
86
|
+
|
|
87
|
+
### *(optional)* allowMethods: `string[]` | `(origin:string, c:Context) => string[]`
|
|
88
|
+
|
|
89
|
+
The value of "_Access-Control-Allow-Methods_" CORS header. You can also pass a callback function to dynamically determine allowed methods based on the origin. The default is `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH', 'QUERY']`.
|
|
90
|
+
|
|
91
|
+
### *(optional)* allowHeaders: `string[]`
|
|
92
|
+
|
|
93
|
+
The value of "_Access-Control-Allow-Headers_" CORS header. The default is `[]`.
|
|
94
|
+
|
|
95
|
+
### *(optional)* maxAge: `number`
|
|
96
|
+
|
|
97
|
+
The value of "_Access-Control-Max-Age_" CORS header.
|
|
98
|
+
|
|
99
|
+
### *(optional)* credentials: `boolean`
|
|
100
|
+
|
|
101
|
+
The value of "_Access-Control-Allow-Credentials_" CORS header.
|
|
102
|
+
|
|
103
|
+
### *(optional)* exposeHeaders: `string[]`
|
|
104
|
+
|
|
105
|
+
The value of "_Access-Control-Expose-Headers_" CORS header. The default is `[]`.
|
|
106
|
+
|
|
107
|
+
## Environment-dependent CORS configuration
|
|
108
|
+
|
|
109
|
+
If you want to adjust CORS configuration according to the execution environment, such as development or production, injecting values from environment variables is convenient as it eliminates the need for the application to be aware of its own execution environment. See the example below for clarification.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
app.use('*', async (c, next) => {
|
|
113
|
+
const corsMiddlewareHandler = cors({
|
|
114
|
+
origin: c.env.CORS_ORIGIN,
|
|
115
|
+
})
|
|
116
|
+
return corsMiddlewareHandler(c, next)
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Using with Vite
|
|
121
|
+
|
|
122
|
+
When using Acryl with Vite, you should disable Vite's built-in CORS feature by setting `server.cors` to `false` in your `vite.config.ts`. This prevents conflicts with Acryl's CORS middleware.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// vite.config.ts
|
|
126
|
+
import { cloudflare } from '@cloudflare/vite-plugin'
|
|
127
|
+
import { defineConfig } from 'vite'
|
|
128
|
+
|
|
129
|
+
export default defineConfig({
|
|
130
|
+
server: {
|
|
131
|
+
cors: false, // disable Vite's built-in CORS setting
|
|
132
|
+
},
|
|
133
|
+
plugins: [cloudflare()],
|
|
134
|
+
})
|
|
135
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# CSRF Protection
|
|
2
|
+
|
|
3
|
+
This middleware protects against CSRF attacks by checking both the `Origin` header and the `Sec-Fetch-Site` header. The request is allowed if either validation passes.
|
|
4
|
+
|
|
5
|
+
The middleware only validates requests that:
|
|
6
|
+
|
|
7
|
+
- Use unsafe HTTP methods (not GET, HEAD, or OPTIONS)
|
|
8
|
+
- Have content types that can be sent by HTML forms (`application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`)
|
|
9
|
+
|
|
10
|
+
Old browsers that do not send `Origin` headers, or environments that use reverse proxies to remove these headers, may not work well. In such environments, use other CSRF token methods.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { Acryl } from 'acrylblends'
|
|
16
|
+
import { csrf } from 'acrylblends/csrf'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const app = new Acryl()
|
|
23
|
+
|
|
24
|
+
// Default: both origin and sec-fetch-site validation
|
|
25
|
+
app.use(csrf())
|
|
26
|
+
|
|
27
|
+
// Allow specific origins
|
|
28
|
+
app.use(csrf({ origin: 'https://myapp.example.com' }))
|
|
29
|
+
|
|
30
|
+
// Allow multiple origins
|
|
31
|
+
app.use(
|
|
32
|
+
csrf({
|
|
33
|
+
origin: [
|
|
34
|
+
'https://myapp.example.com',
|
|
35
|
+
'https://development.myapp.example.com',
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Allow specific sec-fetch-site values
|
|
41
|
+
app.use(csrf({ secFetchSite: 'same-origin' }))
|
|
42
|
+
app.use(csrf({ secFetchSite: ['same-origin', 'none'] }))
|
|
43
|
+
|
|
44
|
+
// Dynamic origin validation
|
|
45
|
+
// It is strongly recommended that the protocol be verified to ensure a match to `$`.
|
|
46
|
+
// You should *never* do a forward match.
|
|
47
|
+
app.use(
|
|
48
|
+
'*',
|
|
49
|
+
csrf({
|
|
50
|
+
origin: (origin) =>
|
|
51
|
+
/https:\/\/(\w+\.)?myapp\.example\.com$/.test(origin),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Dynamic sec-fetch-site validation
|
|
56
|
+
app.use(
|
|
57
|
+
csrf({
|
|
58
|
+
secFetchSite: (secFetchSite, c) => {
|
|
59
|
+
// Always allow same-origin
|
|
60
|
+
if (secFetchSite === 'same-origin') return true
|
|
61
|
+
// Allow cross-site for webhook endpoints
|
|
62
|
+
if (
|
|
63
|
+
secFetchSite === 'cross-site' &&
|
|
64
|
+
c.req.path.startsWith('/webhook/')
|
|
65
|
+
) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
return false
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
### *(optional)* origin: `string` | `string[]` | `Function`
|
|
77
|
+
|
|
78
|
+
Specify allowed origins for CSRF protection.
|
|
79
|
+
|
|
80
|
+
- **`string`**: Single allowed origin (e.g., `'https://example.com'`)
|
|
81
|
+
- **`string[]`**: Array of allowed origins
|
|
82
|
+
- **`Function`**: Custom handler `(origin: string, context: Context) => boolean` for flexible origin validation and bypass logic
|
|
83
|
+
|
|
84
|
+
**Default**: Only same origin as the request URL
|
|
85
|
+
|
|
86
|
+
The function handler receives the request's `Origin` header value and the request context, allowing for dynamic validation based on request properties like path, headers, or other context data.
|
|
87
|
+
|
|
88
|
+
### *(optional)* secFetchSite: `string` | `string[]` | `Function`
|
|
89
|
+
|
|
90
|
+
Specify allowed Sec-Fetch-Site header values for CSRF protection using [Fetch Metadata](https://web.dev/articles/fetch-metadata).
|
|
91
|
+
|
|
92
|
+
- **`string`**: Single allowed value (e.g., `'same-origin'`)
|
|
93
|
+
- **`string[]`**: Array of allowed values (e.g., `['same-origin', 'none']`)
|
|
94
|
+
- **`Function`**: Custom handler `(secFetchSite: string, context: Context) => boolean` for flexible validation
|
|
95
|
+
|
|
96
|
+
**Default**: Only allows `'same-origin'`
|
|
97
|
+
|
|
98
|
+
Standard Sec-Fetch-Site values:
|
|
99
|
+
|
|
100
|
+
- `same-origin`: Request from same origin
|
|
101
|
+
- `same-site`: Request from same site (different subdomain)
|
|
102
|
+
- `cross-site`: Request from different site
|
|
103
|
+
- `none`: Request not from a web page (e.g., browser address bar, bookmark)
|
|
104
|
+
|
|
105
|
+
The function handler receives the request's `Sec-Fetch-Site` header value and the request context, enabling dynamic validation based on request properties.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# ETag Middleware
|
|
2
|
+
|
|
3
|
+
Using this middleware, you can add ETag headers easily.
|
|
4
|
+
|
|
5
|
+
## Import
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Acryl } from 'acrylblends'
|
|
9
|
+
import { etag } from 'acrylblends/etag'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const app = new Acryl()
|
|
16
|
+
|
|
17
|
+
app.use('/etag/*', etag())
|
|
18
|
+
app.get('/etag/abc', (c) => {
|
|
19
|
+
return c.text('Acryl is cool')
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The retained headers
|
|
24
|
+
|
|
25
|
+
The 304 Response must include the headers that would have been sent in an equivalent 200 OK response. The default headers are Cache-Control, Content-Location, Date, ETag, Expires, and Vary.
|
|
26
|
+
|
|
27
|
+
If you want to add the header that is sent, you can use `retainedHeaders` option and `RETAINED_304_HEADERS` strings array variable that includes the default headers:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { etag, RETAINED_304_HEADERS } from 'acrylblends/etag'
|
|
31
|
+
|
|
32
|
+
// ...
|
|
33
|
+
|
|
34
|
+
app.use(
|
|
35
|
+
'/etag/*',
|
|
36
|
+
etag({
|
|
37
|
+
retainedHeaders: ['x-message', ...RETAINED_304_HEADERS],
|
|
38
|
+
})
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
### *(optional)* weak: `boolean`
|
|
45
|
+
|
|
46
|
+
Define using or not using a [weak validation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests#weak_validation). If `true` is set, then `w/` is added to the prefix of the value. The default is `false`.
|
|
47
|
+
|
|
48
|
+
### *(optional)* retainedHeaders: `string[]`
|
|
49
|
+
|
|
50
|
+
The headers that you want to retain in the 304 Response.
|
|
51
|
+
|
|
52
|
+
### *(optional)* generateDigest: `(body: Uint8Array) => ArrayBuffer | Promise<ArrayBuffer>`
|
|
53
|
+
|
|
54
|
+
A custom digest generation function. By default, it uses `SHA-1`. This function is called with the response body as a `Uint8Array` and should return a hash as an `ArrayBuffer` or a Promise of one.
|