@shaferllc/keel 0.66.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +2 -2
- package/dist/core/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/index.d.ts +15 -10
- package/dist/core/index.js +7 -4
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +26 -7
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Social authentication
|
|
2
|
+
|
|
3
|
+
"Sign in with GitHub / Google / Discord" — OAuth 2.0, without an SDK. Keel owns
|
|
4
|
+
the OAuth handshake only: it hands you a normalized **social user**, and *you*
|
|
5
|
+
find-or-create your own user and log them in (with a [session](./authentication.md),
|
|
6
|
+
[`jwt`](./authentication.md#token-api-authentication), or an
|
|
7
|
+
[access token](./authentication.md#opaque-access-tokens)). It stores nothing.
|
|
8
|
+
|
|
9
|
+
Every driver is `fetch`-based — no dependencies, no native bindings — so it runs
|
|
10
|
+
on Node and the edge alike.
|
|
11
|
+
|
|
12
|
+
## Configure a provider
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { social } from "@shaferllc/keel/core";
|
|
16
|
+
|
|
17
|
+
const github = social.github({
|
|
18
|
+
clientId: config("services.github.id"),
|
|
19
|
+
clientSecret: config("services.github.secret"),
|
|
20
|
+
redirectUri: "https://app.example.com/auth/github/callback",
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Presets: `social.github`, `social.google`, `social.discord`. Each defaults to the
|
|
25
|
+
scopes needed for id + email + profile; override with `scopes` in the config or
|
|
26
|
+
per-redirect.
|
|
27
|
+
|
|
28
|
+
## The two-step flow
|
|
29
|
+
|
|
30
|
+
**1. Redirect** the user to the provider. Generate a `state` for CSRF and stash it
|
|
31
|
+
(in the session) to check on the way back:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { social, session, redirect } from "@shaferllc/keel/core";
|
|
35
|
+
|
|
36
|
+
router.get("/auth/github", () => {
|
|
37
|
+
const state = social.state();
|
|
38
|
+
session().put("oauth_state", state);
|
|
39
|
+
return redirect(github.redirect({ state }));
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Handle the callback.** Verify `state`, then exchange the `code` for the user:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
router.get("/auth/github/callback", async () => {
|
|
47
|
+
if (request.query("state") !== session().pull("oauth_state")) {
|
|
48
|
+
throw new ForbiddenException("Invalid OAuth state");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const gh = await github.user(request.query("code")); // exchange + fetch profile
|
|
52
|
+
|
|
53
|
+
const user = await User.query()
|
|
54
|
+
.where("github_id", gh.id)
|
|
55
|
+
.first() ?? await User.create({ github_id: gh.id, email: gh.email, name: gh.name });
|
|
56
|
+
|
|
57
|
+
auth().login(user.id);
|
|
58
|
+
return redirect("/dashboard");
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The social user
|
|
63
|
+
|
|
64
|
+
`user()` returns a shape that's the same across every provider:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
{
|
|
68
|
+
id: string; // the provider's stable id (always a string)
|
|
69
|
+
email: string | null;
|
|
70
|
+
name: string | null;
|
|
71
|
+
nickname: string | null; // handle / username
|
|
72
|
+
avatarUrl: string | null;
|
|
73
|
+
token: OAuthToken; // { accessToken, refreshToken?, expiresIn?, … }
|
|
74
|
+
raw: Record<string, unknown>; // the untouched provider payload
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Reach `raw` for provider-specific fields not in the normalized shape. Use `token`
|
|
79
|
+
to call the provider's API on the user's behalf.
|
|
80
|
+
|
|
81
|
+
## Issuing your own credential
|
|
82
|
+
|
|
83
|
+
After you've found-or-created the user, log them in however your app authenticates
|
|
84
|
+
— they're independent of the OAuth token:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
// server-rendered app → session
|
|
88
|
+
auth().login(user.id);
|
|
89
|
+
|
|
90
|
+
// SPA / mobile → an opaque access token
|
|
91
|
+
const { token } = await createToken(user.id);
|
|
92
|
+
return response.json({ token });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Splitting the steps
|
|
96
|
+
|
|
97
|
+
`user(code)` is `exchangeCode(code)` then `userFromToken(token)`. Call them apart
|
|
98
|
+
when you already hold a token (e.g. a native mobile SDK did the OAuth dance):
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const token = await github.exchangeCode(code); // { accessToken, … }
|
|
102
|
+
const gh = await github.userFromToken(token); // normalized user
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A failed exchange or profile fetch throws `OAuthError` (with the `provider` name).
|
|
106
|
+
|
|
107
|
+
## OAuth 1.0a (Twitter/X)
|
|
108
|
+
|
|
109
|
+
Some providers still speak the older, three-legged **OAuth 1.0a** — every request
|
|
110
|
+
is HMAC-SHA1-signed (done here with Web Crypto, so it stays edge-native). The flow
|
|
111
|
+
has an extra hop: get a temporary *request token*, send the user to authorize,
|
|
112
|
+
then swap the returned `oauth_verifier` for the access token. Stash the request
|
|
113
|
+
token's secret between the two steps.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { social, session, redirect } from "@shaferllc/keel/core";
|
|
117
|
+
|
|
118
|
+
const twitter = social.twitter({
|
|
119
|
+
clientId: config("services.twitter.key"),
|
|
120
|
+
clientSecret: config("services.twitter.secret"),
|
|
121
|
+
redirectUri: "https://app.example.com/auth/twitter/callback",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// 1. request token → redirect
|
|
125
|
+
router.get("/auth/twitter", async () => {
|
|
126
|
+
const request = await twitter.requestToken();
|
|
127
|
+
session().put("twitter_secret", request.tokenSecret); // needed on the way back
|
|
128
|
+
return redirect(twitter.redirect(request));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 2. callback → access token → user
|
|
132
|
+
router.get("/auth/twitter/callback", async () => {
|
|
133
|
+
const tw = await twitter.user(
|
|
134
|
+
request.query("oauth_token"),
|
|
135
|
+
request.query("oauth_verifier"),
|
|
136
|
+
session().pull("twitter_secret"),
|
|
137
|
+
);
|
|
138
|
+
// tw is a SocialUser — same shape as OAuth2, but tw.token is an OAuth1Token
|
|
139
|
+
const user = await User.firstOrCreate({ twitter_id: tw.id }, { name: tw.name });
|
|
140
|
+
auth().login(user.id);
|
|
141
|
+
return redirect("/dashboard");
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For any other OAuth 1.0a provider, use `social.driver1(spec, config)` with
|
|
146
|
+
`requestTokenUrl` / `authorizeUrl` / `accessTokenUrl` and a `fetchUser` that calls
|
|
147
|
+
`driver.get(url, token)` (a signed request). The low-level `oauth1Signature()` is
|
|
148
|
+
exported too, if you need to sign an arbitrary API call yourself.
|
|
149
|
+
|
|
150
|
+
## Any other OAuth2 provider
|
|
151
|
+
|
|
152
|
+
Build a driver for anything OAuth2 with `social.driver(spec, config)` — supply the
|
|
153
|
+
`authorizeUrl`, `tokenUrl`, default scopes, and a `fetchUser(token)` that returns a
|
|
154
|
+
`SocialUser`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const gitlab = social.driver(
|
|
158
|
+
{
|
|
159
|
+
name: "gitlab",
|
|
160
|
+
authorizeUrl: "https://gitlab.com/oauth/authorize",
|
|
161
|
+
tokenUrl: "https://gitlab.com/oauth/token",
|
|
162
|
+
defaultScopes: ["read_user"],
|
|
163
|
+
async fetchUser(token) {
|
|
164
|
+
const res = await fetch("https://gitlab.com/api/v4/user", {
|
|
165
|
+
headers: { authorization: `Bearer ${token.accessToken}` },
|
|
166
|
+
});
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
return { id: String(data.id), email: data.email, name: data.name,
|
|
169
|
+
nickname: data.username, avatarUrl: data.avatar_url, token, raw: data };
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{ clientId, clientSecret, redirectUri },
|
|
173
|
+
);
|
|
174
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Static Files
|
|
2
|
+
|
|
3
|
+
`serveStatic()` serves files from a directory (default `public/`) **before** your
|
|
4
|
+
routes run. If a file matches the request path it's sent with caching headers;
|
|
5
|
+
otherwise the request falls through to your routes.
|
|
6
|
+
|
|
7
|
+
## Enable it
|
|
8
|
+
|
|
9
|
+
Add the middleware to your HTTP kernel:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { HttpKernel, serveStatic } from "@shaferllc/keel/core";
|
|
13
|
+
|
|
14
|
+
export class Kernel extends HttpKernel {
|
|
15
|
+
constructor(app: Application) {
|
|
16
|
+
super(app);
|
|
17
|
+
this.use(serveStatic()); // serves ./public
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Now `./public/css/style.css` is served at `/css/style.css`, and `./public/index.html`
|
|
23
|
+
at `/`.
|
|
24
|
+
|
|
25
|
+
## How a request is matched
|
|
26
|
+
|
|
27
|
+
For each request the middleware:
|
|
28
|
+
|
|
29
|
+
1. **Skips non-reads.** Only `GET` and `HEAD` are handled; any other method falls
|
|
30
|
+
straight through to your routes.
|
|
31
|
+
2. **Decodes and guards the path.** The URL pathname is `decodeURIComponent`'d,
|
|
32
|
+
then any path containing `..` is rejected (falls through) — so percent-encoded
|
|
33
|
+
traversal (`%2e%2e`) is caught too.
|
|
34
|
+
3. **Applies the dot-file policy** (see below).
|
|
35
|
+
4. **Resolves the file.** It looks for `root + urlPath` on disk. If that's a
|
|
36
|
+
directory, it appends `index` (`index.html`) and looks again. If nothing
|
|
37
|
+
resolves to a real file, the request falls through.
|
|
38
|
+
5. **Sends the file** with `Content-Type`, `Last-Modified`, `ETag`, and — when
|
|
39
|
+
configured — `Cache-Control` headers.
|
|
40
|
+
|
|
41
|
+
Because the middleware calls `next()` (rather than returning a 404) whenever it
|
|
42
|
+
can't serve a file, a missing asset is handled by your routes, not by the static
|
|
43
|
+
server. That's what lets a client-side app fall back to an SPA catch-all route.
|
|
44
|
+
|
|
45
|
+
## Options
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
serveStatic({
|
|
49
|
+
root: "./public", // directory to serve
|
|
50
|
+
index: "index.html", // directory index file
|
|
51
|
+
dotFiles: "ignore", // "ignore" (404) · "deny" (403) · "allow"
|
|
52
|
+
maxAge: 86400, // Cache-Control: public, max-age=…
|
|
53
|
+
immutable: true, // add the immutable directive (hashed filenames)
|
|
54
|
+
headers: (path) => // extra per-file headers
|
|
55
|
+
path.endsWith(".html") ? { "X-Frame-Options": "DENY" } : undefined,
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every response carries an `ETag` and `Last-Modified`, and a matching
|
|
60
|
+
`If-None-Match` returns a `304`. Dot-files (`.env`, `.git`) are 404'd by default
|
|
61
|
+
so secrets aren't exposed. Path traversal (`..`) is blocked.
|
|
62
|
+
|
|
63
|
+
## Caching & conditional requests
|
|
64
|
+
|
|
65
|
+
The `ETag` is **weak** — derived from the file's byte size and modified time
|
|
66
|
+
(`W/"<size>-<mtime>"`) — so it changes whenever the file changes without hashing
|
|
67
|
+
its contents. On a repeat request the browser echoes it back in `If-None-Match`;
|
|
68
|
+
if it still matches, the middleware short-circuits with `304 Not Modified` and an
|
|
69
|
+
empty body, saving the read and the transfer.
|
|
70
|
+
|
|
71
|
+
`Cache-Control` is only sent when you set `maxAge`; omit it and the response has
|
|
72
|
+
no `Cache-Control` header at all (the browser falls back to its heuristic
|
|
73
|
+
freshness). Add `immutable: true` for content-hashed filenames so conditional
|
|
74
|
+
revalidation is skipped entirely for the cache lifetime:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// Long-lived, fingerprinted build assets: cache hard, never revalidate.
|
|
78
|
+
this.use(serveStatic({ root: "./dist", maxAge: 31536000, immutable: true }));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`HEAD` requests get the full header set with an empty body, so clients can probe
|
|
82
|
+
an asset's `ETag`/`Last-Modified` without downloading it.
|
|
83
|
+
|
|
84
|
+
## Per-file headers
|
|
85
|
+
|
|
86
|
+
The `headers` callback runs for every file about to be served and merges its
|
|
87
|
+
result into the response. It receives the **resolved filesystem path** (root
|
|
88
|
+
included, e.g. `./public/app.js`), not the URL path — match on the extension or
|
|
89
|
+
suffix rather than a leading slash:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
serveStatic({
|
|
93
|
+
headers: (path) => {
|
|
94
|
+
if (path.endsWith(".html")) return { "X-Frame-Options": "DENY" };
|
|
95
|
+
if (path.endsWith(".wasm")) return { "Cross-Origin-Embedder-Policy": "require-corp" };
|
|
96
|
+
return undefined; // no extra headers
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Returning `undefined` (or an empty object) adds nothing. These headers are set
|
|
102
|
+
after the built-ins, so a `Cache-Control` you return here overrides the one
|
|
103
|
+
derived from `maxAge`.
|
|
104
|
+
|
|
105
|
+
## Dot-files & traversal
|
|
106
|
+
|
|
107
|
+
Any path segment that starts with `.` — not just the last one — is a "dot
|
|
108
|
+
segment", so `/.git/config` and `/assets/.env` both trip the policy:
|
|
109
|
+
|
|
110
|
+
- `"ignore"` (default) — falls through to your routes, so it reads as a 404.
|
|
111
|
+
- `"deny"` — responds `403 Forbidden`.
|
|
112
|
+
- `"allow"` — serves the file like any other.
|
|
113
|
+
|
|
114
|
+
Separately, any decoded path containing `..` is always rejected regardless of the
|
|
115
|
+
dot-file policy, so `../` traversal can't escape `root`.
|
|
116
|
+
|
|
117
|
+
## Error behavior
|
|
118
|
+
|
|
119
|
+
The file-resolution block is wrapped in a `try/catch` that swallows errors by
|
|
120
|
+
calling `next()`. A permission error, a mid-request delete, or a malformed path
|
|
121
|
+
never becomes a `500` — it falls through to your routes exactly like a miss. The
|
|
122
|
+
trade-off: genuine filesystem faults are invisible here, so don't rely on this
|
|
123
|
+
middleware to surface disk problems.
|
|
124
|
+
|
|
125
|
+
## Edge note
|
|
126
|
+
|
|
127
|
+
`serveStatic()` reads from the filesystem (via a dynamically-imported `node:fs`),
|
|
128
|
+
so it's for **Node** apps. On Cloudflare Workers, serve assets through the
|
|
129
|
+
platform's static-assets binding instead — the framework core still imports
|
|
130
|
+
cleanly either way (the `node:fs` import is deferred until the first request the
|
|
131
|
+
middleware actually handles).
|
|
132
|
+
|
|
133
|
+
## Production
|
|
134
|
+
|
|
135
|
+
For high-traffic sites, prefer a CDN or reverse proxy (Nginx, Caddy, Cloudflare)
|
|
136
|
+
in front of static assets rather than serving them from the Node process.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## API reference
|
|
141
|
+
|
|
142
|
+
### `serveStatic(options?)`
|
|
143
|
+
|
|
144
|
+
`serveStatic(options?: StaticOptions): MiddlewareHandler`
|
|
145
|
+
|
|
146
|
+
Builds a Hono middleware that serves files from `options.root` before the request
|
|
147
|
+
reaches your routes, falling through to `next()` on any miss.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { serveStatic } from "@shaferllc/keel/core";
|
|
151
|
+
|
|
152
|
+
const assets = serveStatic({ root: "./public", maxAge: 86400 });
|
|
153
|
+
this.use(assets);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Notes:** all options are optional (`serveStatic()` serves `./public`). Only
|
|
157
|
+
`GET`/`HEAD` are handled — other methods pass through untouched. Returns the
|
|
158
|
+
middleware synchronously; `node:fs/promises` is imported lazily on the first
|
|
159
|
+
handled request, so importing this on a non-Node runtime is safe until a request
|
|
160
|
+
hits it. Sends `Content-Type` (via Hono's `getMimeType`, defaulting to
|
|
161
|
+
`application/octet-stream`), `Last-Modified`, and a weak `ETag`; honors
|
|
162
|
+
`If-None-Match` with a `304`. A trailing slash on `root` is stripped. The `..`
|
|
163
|
+
guard is a plain substring check, so a (rare) legitimate filename containing `..`
|
|
164
|
+
is also rejected.
|
|
165
|
+
|
|
166
|
+
### Interfaces & types
|
|
167
|
+
|
|
168
|
+
#### `StaticOptions`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
interface StaticOptions {
|
|
172
|
+
root?: string;
|
|
173
|
+
index?: string;
|
|
174
|
+
dotFiles?: "ignore" | "deny" | "allow";
|
|
175
|
+
maxAge?: number;
|
|
176
|
+
immutable?: boolean;
|
|
177
|
+
headers?: (path: string) => Record<string, string> | undefined;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The configuration bag for `serveStatic()`. Pass it to tune the served directory,
|
|
182
|
+
directory index, dot-file policy, and caching. Every field has a default, so an
|
|
183
|
+
empty object (or no argument) is valid.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
const options: StaticOptions = {
|
|
187
|
+
root: "./dist",
|
|
188
|
+
index: "index.html",
|
|
189
|
+
dotFiles: "deny",
|
|
190
|
+
maxAge: 31536000,
|
|
191
|
+
immutable: true,
|
|
192
|
+
headers: (path) => (path.endsWith(".html") ? { "X-Frame-Options": "DENY" } : undefined),
|
|
193
|
+
};
|
|
194
|
+
serveStatic(options);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Field defaults & behavior:**
|
|
198
|
+
|
|
199
|
+
- `root` — directory to serve from. Default `"./public"`; trailing slashes are
|
|
200
|
+
stripped.
|
|
201
|
+
- `index` — file served for a directory request. Default `"index.html"`.
|
|
202
|
+
- `dotFiles` — policy for paths with a `.`-prefixed segment: `"ignore"` (fall
|
|
203
|
+
through, reads as 404), `"deny"` (`403 Forbidden`), or `"allow"` (serve).
|
|
204
|
+
Default `"ignore"`.
|
|
205
|
+
- `maxAge` — `Cache-Control: public, max-age=<seconds>`. Omit for no
|
|
206
|
+
`Cache-Control` header at all.
|
|
207
|
+
- `immutable` — appends `, immutable` to `Cache-Control` (only meaningful
|
|
208
|
+
alongside `maxAge`). Default `false`.
|
|
209
|
+
- `headers` — called with the resolved filesystem path (root included) for each
|
|
210
|
+
served file; return extra headers to merge, or `undefined` for none. These are
|
|
211
|
+
applied last, so they can override the built-in headers.
|