koa-classic-server 2.6.1 → 3.0.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.
Files changed (57) hide show
  1. package/CLAUDE.md +101 -0
  2. package/README.md +564 -591
  3. package/__tests__/benchmark-results-v3.0.0.txt +372 -0
  4. package/__tests__/benchmark.js +1 -1
  5. package/__tests__/caching-headers.test.js +30 -30
  6. package/__tests__/compression-fixtures/data.json +1 -0
  7. package/__tests__/compression-fixtures/large.txt +1 -0
  8. package/__tests__/compression-fixtures/small.txt +1 -0
  9. package/__tests__/compression.test.js +284 -0
  10. package/__tests__/customTest/serversToLoad.util.js +5 -5
  11. package/__tests__/demo-regex-index.js +4 -4
  12. package/__tests__/deprecation-warnings.test.js +71 -183
  13. package/__tests__/directory-sorting-links.test.js +1 -1
  14. package/__tests__/dt-unknown.test.js +39 -28
  15. package/__tests__/ejs.test.js +1 -1
  16. package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
  17. package/__tests__/hidden-fixtures/.env +2 -0
  18. package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
  19. package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
  20. package/__tests__/hidden-fixtures/data.key +1 -0
  21. package/__tests__/hidden-fixtures/file.secret +1 -0
  22. package/__tests__/hidden-fixtures/index.html +1 -0
  23. package/__tests__/hidden-fixtures/normal.txt +1 -0
  24. package/__tests__/hidden-fixtures/subdir/.env +1 -0
  25. package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
  26. package/__tests__/hidden-option.test.js +407 -0
  27. package/__tests__/hideExtension.test.js +70 -13
  28. package/__tests__/index-option.test.js +18 -16
  29. package/__tests__/index.test.js +14 -10
  30. package/__tests__/listing.test.js +437 -0
  31. package/__tests__/logger.test.js +232 -0
  32. package/__tests__/range-fixtures/sample.txt +1 -0
  33. package/__tests__/range.test.js +223 -0
  34. package/__tests__/security-headers.test.js +165 -0
  35. package/__tests__/security.test.js +148 -162
  36. package/__tests__/server-cache-fixtures/large.txt +1 -0
  37. package/__tests__/server-cache-fixtures/small.txt +1 -0
  38. package/__tests__/server-cache.test.js +594 -0
  39. package/__tests__/symlink.test.js +18 -15
  40. package/__tests__/template-timeout.test.js +321 -0
  41. package/docs/ACTION_PLAN.md +293 -0
  42. package/docs/CHANGELOG.md +289 -0
  43. package/docs/CODE_REVIEW.md +2 -0
  44. package/docs/DOCUMENTATION.md +259 -32
  45. package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
  46. package/docs/FLOW_DIAGRAM.md +15 -13
  47. package/docs/INDEX_OPTION_PRIORITY.md +2 -2
  48. package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
  49. package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
  50. package/docs/PERFORMANCE_COMPARISON.md +7 -7
  51. package/docs/security_improvement_for_V3.md +421 -0
  52. package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
  53. package/docs/template-engine/esempi-incrementali.js +1 -1
  54. package/eslint.config.mjs +17 -0
  55. package/index.cjs +1507 -429
  56. package/index.mjs +1 -5
  57. package/package.json +9 -1
package/README.md CHANGED
@@ -1,35 +1,34 @@
1
1
  # koa-classic-server
2
2
 
3
- 🚀 **Production-ready Koa middleware** for serving static files with Apache2-like directory listing, sortable columns, HTTP caching, template engine support, and enterprise-grade security.
3
+ 🚀 **Production-ready Koa middleware** for serving static files with Apache2-like directory listing, sortable columns, pagination, hash-based CSP, template-engine timeouts, injectable logging, and enterprise-grade security.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/koa-classic-server.svg)](https://www.npmjs.com/package/koa-classic-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![Tests](https://img.shields.io/badge/tests-309%20passing-brightgreen.svg)]()
7
+ [![Tests](https://img.shields.io/badge/tests-532%20passing-brightgreen.svg)]()
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-blue.svg)]()
8
9
 
9
10
  ---
10
11
 
11
- ## 🎉 Version 2.X - Production-Ready Release
12
-
13
- The 2.X series brings major performance improvements, enhanced security, and powerful new features while maintaining full backward compatibility.
14
-
15
- ### Key Features in Version 2.X
16
-
17
- ✅ **URL Rewriting Support** - Compatible with i18n and routing middleware via `useOriginalUrl` option
18
- ✅ **Improved Caching Controls** - Clear `browserCacheEnabled` and `browserCacheMaxAge` options
19
- ✅ **Development-Friendly Defaults** - Caching disabled by default for easier development
20
- ✅ **Production Optimized** - Enable caching in production for 80-95% bandwidth reduction
21
- ✅ **Sortable Directory Columns** - Click Name/Type/Size to sort (Apache2-like)
22
- ✅ **File Size Display** - Human-readable file sizes (B, KB, MB, GB, TB)
23
- ✅ **HTTP Caching** - ETag and Last-Modified headers with 304 responses
24
- ✅ **Async/Await** - Non-blocking I/O for high performance
25
- ✅ **Performance Optimized** - 50-70% faster directory listings
26
- ✅ **Enhanced Index Option** - Array format with RegExp support
27
- ✅ **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, and more
28
- ✅ **Enterprise Security** - Path traversal, XSS, race condition protection
29
- ✅ **Clean URLs** - Hide file extensions with `hideExtension` (mod_rewrite-like behavior)
30
- ✅ **Symlink Support** - Full symbolic link support (NixOS, Docker, npm link, Capistrano)
31
- ✅ **Comprehensive Testing** - 309 tests passing with extensive coverage
32
- ✅ **Complete Documentation** - Detailed guides and examples
12
+ ## 🎉 Version 3.0 File Server First, Observable, Bounded
13
+
14
+ The 3.0 series builds on 2.x with new observability hooks, bounded resource usage on accidentally-large directories, and a more focused **design philosophy**: koa-classic-server is an **HTTP file server first** — defaults serve files without applying surprise restrictions, and hardening is opt-in via explicit configuration plus a documented Security Checklist.
15
+
16
+ ### Key Features in Version 3.x
17
+
18
+ ✅ **Design philosophy made explicit** *"if a file is in `rootDir`, `GET` returns it"* — codified in [`CLAUDE.md`](./CLAUDE.md), with a **Security Checklist** + **Suggested Production Security Configuration** in this README and `docs/DOCUMENTATION.md`
19
+ **`dirListing` namespace** listing options grouped under one structured object (`enabled`, `maxEntries`, `entriesPerPage`); the v2 `showDirContents` flag is kept as a deprecated alias with a one-time warning
20
+ ✅ **Soft cap on listing rendering** — `dirListing.maxEntries` defaults to `100000` as a *safety net* against accidentally-huge directories (broken log rotation, mistakenly mounted FS), NOT as a policy restriction; banner + `X-Dir-Truncated` header on the rare hit. Opt-in RAM-bounded streaming reads planned for v3.1.
21
+ ✅ **Paginated listings** — `dirListing.entriesPerPage` adds 0-based `?page=N` navigation with First/Prev/Next/Last + `X-Dir-Pagination` header
22
+ ✅ **Template render timeout + AbortSignal** — `template.renderTimeout` (default 30s) + a per-request `template.signal` so slow renders never wedge the server
23
+ ✅ **Injectable logger** pass any `{ error, warn, info, debug }`-shaped logger (Pino, Bunyan, Winston, console) for full observability
24
+ ✅ **Hash-based CSP on listing page** — automatic SHA-256 of inline CSS, recomputed at module load
25
+ ✅ **Security headers on generated pages** — `CSP`, `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy` on listing + error pages
26
+ ✅ **Sortable Directory Columns** Click Name/Type/Size to sort (Apache2-like) with sort/order preserved across paginator links
27
+ ✅ **HTTP Caching** — ETag, Last-Modified, conditional 304 responses (opt-in via `browserCacheEnabled`)
28
+ ✅ **Template Engine Support** EJS, Pug, Handlebars, Nunjucks, and more — with full async/await, AbortSignal forwarding, and timeout enforcement
29
+ ✅ **Clean URLs** Hide file extensions via `hideExtension` (mod_rewrite-like)
30
+ ✅ **Symlink Support** Transparent resolution + clear indicators in the listing
31
+ ✅ **532 tests passing** comprehensive coverage including security, listing pagination, logger injection, template timeouts, and edge cases
33
32
 
34
33
  [See full changelog →](./docs/CHANGELOG.md)
35
34
 
@@ -37,23 +36,26 @@ The 2.X series brings major performance improvements, enhanced security, and pow
37
36
 
38
37
  ## Features
39
38
 
40
- **koa-classic-server** is a high-performance middleware for serving static files with Apache2-like behavior, making file browsing intuitive and powerful.
39
+ **koa-classic-server** is a high-performance middleware for serving static files with Apache2-like behavior, making file browsing intuitive, observable, and safe.
41
40
 
42
41
  ### Core Features
43
42
 
44
- - 🗂️ **Apache2-like Directory Listing** - Sortable columns (Name, Type, Size)
45
- - 📄 **Static File Serving** - Automatic MIME type detection with streaming
46
- - 📊 **Sortable Columns** - Click headers to sort ascending/descending
47
- - 📏 **File Sizes** - Human-readable display (B, KB, MB, GB, TB)
48
- - **HTTP Caching** - ETag, Last-Modified, 304 responses
49
- - 🎨 **Template Engine Support** - EJS, Pug, Handlebars, Nunjucks, etc.
50
- - 🔒 **Enterprise Security** - Path traversal, XSS, race condition protection
51
- - ⚙️ **Highly Configurable** - URL prefixes, reserved paths, index files
52
- - 🚀 **High Performance** - Async/await, non-blocking I/O, optimized algorithms
53
- - 🔗 **Symlink Support** - Transparent symlink resolution with directory listing indicators
54
- - 🌐 **Clean URLs** - Hide file extensions for SEO-friendly URLs via `hideExtension`
55
- - 🧪 **Well-Tested** - 309 passing tests with comprehensive coverage
56
- - 📦 **Dual Module Support** - CommonJS and ES Modules
43
+ - 🗂️ **Apache2-like Directory Listing** Sortable columns (Name, Type, Size)
44
+ - 📄 **Static File Serving** Automatic MIME type detection with streaming
45
+ - 📊 **Sortable Columns** Click headers to sort ascending/descending
46
+ - 📏 **File Sizes** Human-readable display (B, KB, MB, GB, TB)
47
+ - 📃 **Bounded + Paginated Listings** `dirListing.maxEntries` cap + `dirListing.entriesPerPage` navigation
48
+ - ⏱️ **Template Render Timeout** Configurable timeout with AbortSignal propagation
49
+ - 📝 **Injectable Logger** Plug Pino/Bunyan/Winston/console at construction time
50
+ - **HTTP Caching** ETag, Last-Modified, 304 responses (opt-in)
51
+ - 🎨 **Template Engine Support** EJS, Pug, Handlebars, Nunjucks, etc.
52
+ - 🔒 **Enterprise Security** Path traversal, XSS, race condition protection, CSP, dot-file hiding
53
+ - ⚙️ **Highly Configurable** URL prefixes, reserved paths, index files, hidden patterns
54
+ - 🚀 **High Performance** Async/await, non-blocking I/O, single-syscall directory reads
55
+ - 🔗 **Symlink Support** Transparent resolution with directory listing indicators
56
+ - 🌐 **Clean URLs** — Hide file extensions for SEO-friendly URLs via `hideExtension`
57
+ - 🧪 **Well-Tested** — 532 passing tests with comprehensive coverage
58
+ - 📦 **Dual Module Support** — CommonJS and ES Modules
57
59
 
58
60
  ---
59
61
 
@@ -95,11 +97,15 @@ const koaClassicServer = require('koa-classic-server');
95
97
  const app = new Koa();
96
98
 
97
99
  app.use(koaClassicServer(__dirname + '/public', {
98
- showDirContents: true,
99
100
  index: ['index.html', 'index.htm'],
100
101
  urlPrefix: '/static',
101
- browserCacheMaxAge: 3600,
102
- browserCacheEnabled: true
102
+ dirListing: {
103
+ enabled: true,
104
+ maxEntries: 5000, // cap huge directories
105
+ entriesPerPage: 50, // 50 entries per listing page
106
+ },
107
+ browserCacheEnabled: true,
108
+ browserCacheMaxAge: 3600,
103
109
  }));
104
110
 
105
111
  app.listen(3000);
@@ -121,365 +127,222 @@ import koaClassicServer from 'koa-classic-server';
121
127
 
122
128
  ### 2. Basic File Server
123
129
 
124
- Serve static files from a directory:
125
-
126
130
  ```javascript
127
131
  const Koa = require('koa');
132
+ const path = require('path');
128
133
  const koaClassicServer = require('koa-classic-server');
129
134
 
130
135
  const app = new Koa();
131
136
 
132
- app.use(koaClassicServer(__dirname + '/public', {
133
- showDirContents: true,
134
- index: ['index.html']
137
+ app.use(koaClassicServer(path.join(__dirname, 'public'), {
138
+ dirListing: { enabled: true },
139
+ index: ['index.html'],
135
140
  }));
136
141
 
137
142
  app.listen(3000);
138
143
  ```
139
144
 
140
- **What it does:**
141
- - Serves files from `/public` directory
142
- - Shows directory listing when accessing folders
143
- - Looks for `index.html` in directories
144
- - Sortable columns (Name, Type, Size)
145
- - File sizes displayed in human-readable format
146
-
147
145
  ### 3. With URL Prefix
148
146
 
149
- Serve files under a specific URL path:
150
-
151
147
  ```javascript
152
- app.use(koaClassicServer(__dirname + '/assets', {
148
+ app.use(koaClassicServer(__dirname + '/public', {
153
149
  urlPrefix: '/static',
154
- showDirContents: true
155
150
  }));
151
+ // http://localhost:3000/static/image.png → public/image.png
156
152
  ```
157
153
 
158
- **Result:**
159
- - `http://localhost:3000/static/image.png` → serves `/assets/image.png`
160
- - `http://localhost:3000/static/` → shows `/assets` directory listing
161
-
162
154
  ### 4. With Reserved Paths
163
155
 
164
- Protect specific directories from being accessed:
165
-
166
156
  ```javascript
167
- app.use(koaClassicServer(__dirname + '/www', {
168
- urlsReserved: ['/admin', '/config', '/.git', '/node_modules']
157
+ app.use(koaClassicServer(__dirname, {
158
+ urlsReserved: ['/api', '/admin', '/.git', '/node_modules'],
169
159
  }));
160
+ // /api/* is passed through to the next middleware untouched.
170
161
  ```
171
162
 
172
- **Result:**
173
- - `/admin/*` → passed to next middleware (not served)
174
- - `/config/*` → protected
175
- - Other paths → served normally
163
+ ### 5. Bounded + Paginated Directory Listings (V3)
176
164
 
177
- ### 5. With Template Engine (EJS)
178
-
179
- Dynamically render templates with data:
165
+ For directories that may grow without bound (uploads, archives, logs), cap the maximum number of entries the middleware will enumerate and paginate what's visible:
180
166
 
181
167
  ```javascript
182
- const ejs = require('ejs');
183
-
184
- app.use(koaClassicServer(__dirname + '/views', {
185
- template: {
186
- ext: ['ejs', 'html.ejs'],
187
- render: async (ctx, next, filePath) => {
188
- const data = {
189
- title: 'My App',
190
- user: ctx.state.user || { name: 'Guest' },
191
- items: ['Item 1', 'Item 2', 'Item 3'],
192
- timestamp: new Date().toISOString()
193
- };
194
-
195
- ctx.body = await ejs.renderFile(filePath, data);
196
- ctx.type = 'text/html';
197
- }
198
- }
168
+ app.use(koaClassicServer(__dirname + '/uploads', {
169
+ dirListing: {
170
+ enabled: true,
171
+ maxEntries: 10000, // cap visible / sorted / stat'd entries (default; 0 = disabled)
172
+ entriesPerPage: 100, // entries per page in the listing UI (default; 0 = disabled)
173
+ },
199
174
  }));
200
175
  ```
201
176
 
202
- **Template example (`views/dashboard.ejs`):**
203
- ```html
204
- <!DOCTYPE html>
205
- <html>
206
- <head>
207
- <title><%= title %></title>
208
- </head>
209
- <body>
210
- <h1>Welcome, <%= user.name %>!</h1>
211
- <ul>
212
- <% items.forEach(item => { %>
213
- <li><%= item %></li>
214
- <% }); %>
215
- </ul>
216
- <p>Generated at: <%= timestamp %></p>
217
- </body>
218
- </html>
219
- ```
177
+ **What happens on a directory with 1,000,000 files**
220
178
 
221
- **See complete guide:** [Template Engine Documentation →](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)
179
+ - The middleware calls `fs.promises.readdir()` once and slices the result to `dirListing.maxEntries` — sorting, stat'ing, and rendering are CPU-bounded by `dirListing.maxEntries`. The initial `readdir()` itself is **not** bounded (see v3.1 roadmap for an opt-in streaming mode targeting adversarial-directory workloads).
180
+ - A yellow banner appears at the top of the listing: *"Showing first 10000 entries (cap reached)…"*
181
+ - The response carries `X-Dir-Truncated: 10000` so monitoring can flag capped pages.
182
+ - Pagination is rendered below the table with `« First · ‹ Prev · 0 1 … N · Next › · Last »`, and an `X-Dir-Pagination: <current>/<last>` response header is set.
183
+ - Navigate via `?page=N` (0-based). Out-of-range values clamp silently to the nearest valid page. Active `sort` / `order` query params are preserved across paginator links.
222
184
 
223
- ### 6. Clean URLs with hideExtension
185
+ ### 6. Template Engine with Timeout + AbortSignal (V3)
224
186
 
225
- Hide file extensions from URLs, similar to Apache's `mod_rewrite`:
187
+ V3 hardens template rendering against runaway or hung renders: the middleware enforces a configurable timeout and forwards a `template.signal` (AbortSignal) you can use inside your renderer to abort I/O and long-running work.
226
188
 
227
189
  ```javascript
228
190
  const ejs = require('ejs');
191
+ const koaClassicServer = require('koa-classic-server');
229
192
 
230
- app.use(koaClassicServer(__dirname + '/public', {
231
- showDirContents: true,
232
- index: ['index.ejs'],
233
- hideExtension: {
234
- ext: '.ejs', // Extension to hide (required)
235
- redirect: 301 // HTTP redirect code (optional, default: 301)
236
- },
193
+ app.use(koaClassicServer(__dirname + '/views', {
237
194
  template: {
238
195
  ext: ['ejs'],
239
- render: async (ctx, next, filePath) => {
196
+ renderTimeout: 5000, // 5s hard cap (default 30000ms; 0 disables the cap)
197
+ render: async (ctx, next, filePath, { signal }) => {
198
+ // Forward the signal to your I/O — fetch, DB queries, async work.
199
+ const data = await fetchData({ signal });
200
+ if (signal.aborted) return;
240
201
  ctx.body = await ejs.renderFile(filePath, data);
241
- ctx.type = 'text/html';
242
- }
243
- }
202
+ ctx.type = 'text/html';
203
+ },
204
+ },
244
205
  }));
245
206
  ```
246
207
 
247
- **URL Behavior:**
248
-
249
- | Request URL | Action | Result |
250
- |-------------|--------|--------|
251
- | `/about` | Resolves `about.ejs` | Serves file (200) |
252
- | `/blog/article` | Resolves `blog/article.ejs` | Serves file (200) |
253
- | `/about.ejs` | Redirect | 301 → `/about` |
254
- | `/about.ejs?lang=it` | Redirect | 301 → `/about?lang=it` |
255
- | `/index.ejs` | Redirect | 301 → `/` |
256
- | `/section/index.ejs` | Redirect | 301 → `/section/` |
257
- | `/style.css` | No interference | Normal flow |
258
- | `/about/` | No interference | Shows directory listing |
259
-
260
- **Conflict Resolution:**
208
+ If the renderer exceeds `renderTimeout`, the request fails closed with a 500 and a single warning is emitted via the configured logger — the response stream is never left half-written.
261
209
 
262
- - **Directory vs file**: When both `about/` directory and `about.ejs` file exist, `/about` serves the file. Use `/about/` to access the directory.
263
- - **Extensionless vs extension**: When both `about` (no ext) and `about.ejs` exist, `/about` always serves `about.ejs`. The extensionless file becomes unreachable.
210
+ ### 7. Injectable Logger (V3)
264
211
 
265
- > **Note**: This conflict resolution behavior differs from Apache/Nginx, where directories typically take priority over files with the same base name.
266
-
267
- ### 7. URL Rewriting Support (useOriginalUrl)
268
-
269
- When using URL rewriting middleware (i18n, routing), set `useOriginalUrl: false` so koa-classic-server resolves files from the rewritten URL instead of the original one:
212
+ By default the middleware logs to `console`. Pass any object exposing `error`, `warn`, `info`, `debug` to integrate with your production logging stack:
270
213
 
271
214
  ```javascript
272
- const Koa = require('koa');
273
- const koaClassicServer = require('koa-classic-server');
215
+ const pino = require('pino')();
274
216
 
275
- const app = new Koa();
276
-
277
- // i18n middleware: strips language prefix and rewrites ctx.url
278
- app.use(async (ctx, next) => {
279
- const langMatch = ctx.path.match(/^\/(it|en|fr|de|es)\//);
280
- if (langMatch) {
281
- ctx.state.lang = langMatch[1]; // Save language for templates
282
- ctx.url = ctx.path.replace(/^\/(it|en|fr|de|es)/, '') + ctx.search;
283
- }
284
- await next();
285
- });
286
-
287
- // Serve files using the rewritten URL
288
217
  app.use(koaClassicServer(__dirname + '/public', {
289
- useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
218
+ logger: pino, // any { error, warn, info, debug }-shaped object works
290
219
  }));
291
-
292
- app.listen(3000);
293
220
  ```
294
221
 
295
- **How it works:**
296
-
297
- | Request | `ctx.originalUrl` | `ctx.url` (rewritten) | File resolved |
298
- |---------|-------------------|----------------------|---------------|
299
- | `/it/page.html` | `/it/page.html` | `/page.html` | `public/page.html` |
300
- | `/en/page.html` | `/en/page.html` | `/page.html` | `public/page.html` |
301
- | `/page.html` | `/page.html` | `/page.html` | `public/page.html` |
222
+ - Backward compatible: when `logger` is omitted, behavior is unchanged (uses `console`).
223
+ - All internal warnings and errors flow through the same logger — useful for routing them to Sentry, Datadog, or stdout JSON.
302
224
 
303
- - With `useOriginalUrl: true` (default): the server would look for `public/it/page.html` (which doesn't exist)
304
- - With `useOriginalUrl: false`: the server looks for `public/page.html` (correct)
225
+ ### 8. Hidden Files & Dot-File Protection (V3 default: hidden)
305
226
 
306
- ### 8. Advanced hideExtension Scenarios
227
+ Dot-files and dot-directories are **visible by default in v3** — aligned with the "file server first" philosophy (see [`CLAUDE.md`](./CLAUDE.md)). For production deployments where `.env`, `.git/config`, etc. could be served accidentally, **opt into hardening** explicitly via `hidden.dotFiles.default: 'hidden'`. This is the first item on the [Security Checklist](#design-philosophy--security-checklist).
307
228
 
308
- #### Recommended file structure
309
-
310
- ```
311
- views/
312
- ├── index.ejs ← / (home page)
313
- ├── about.ejs ← /about
314
- ├── contact.ejs ← /contact
315
- ├── blog/
316
- │ ├── index.ejs ← /blog/
317
- ├── first-post.ejs ← /blog/first-post
318
- │ └── second-post.ejs ← /blog/second-post
319
- ├── docs/
320
- │ ├── index.ejs ← /docs/
321
- │ ├── getting-started.ejs /docs/getting-started
322
- │ └── api-reference.ejs ← /docs/api-reference
323
- └── assets/
324
- ├── style.css ← /assets/style.css (served normally)
325
- └── script.js ← /assets/script.js (served normally)
229
+ ```javascript
230
+ app.use(koaClassicServer(__dirname + '/www', {
231
+ hidden: {
232
+ dotFiles: {
233
+ default: 'hidden', // 'hidden' | 'visible'
234
+ whitelist: ['.well-known', '.htaccess'],// exact name, glob, or RegExp
235
+ blacklist: [], // overrides whitelist
236
+ },
237
+ dotDirs: {
238
+ default: 'visible',
239
+ whitelist: [],
240
+ blacklist: ['.git'],
241
+ },
242
+ alwaysHide: ['*.key', /secret/i, '/private/**'], // path-aware patterns
243
+ },
244
+ }));
326
245
  ```
327
246
 
328
- #### hideExtension with i18n middleware
247
+ ### 9. Clean URLs with `hideExtension`
329
248
 
330
- Combine `hideExtension` with `useOriginalUrl: false` for multilingual sites with clean URLs:
249
+ Serve `.ejs` (or any extension) as extensionless URLs and 301-redirect the canonical form:
331
250
 
332
251
  ```javascript
333
- const Koa = require('koa');
334
- const koaClassicServer = require('koa-classic-server');
335
- const ejs = require('ejs');
336
-
337
- const app = new Koa();
338
-
339
- // i18n middleware: /it/about → ctx.url = /about, ctx.state.lang = 'it'
340
- app.use(async (ctx, next) => {
341
- const langMatch = ctx.path.match(/^\/(it|en|fr)\//);
342
- if (langMatch) {
343
- ctx.state.lang = langMatch[1];
344
- ctx.url = ctx.path.replace(/^\/(it|en|fr)/, '') + ctx.search;
345
- } else {
346
- ctx.state.lang = 'en'; // Default language
347
- }
348
- await next();
349
- });
350
-
351
252
  app.use(koaClassicServer(__dirname + '/views', {
352
- index: ['index.ejs'],
353
- useOriginalUrl: false, // Resolve files from rewritten URL
354
253
  hideExtension: {
355
- ext: '.ejs',
356
- redirect: 301
254
+ ext: '.ejs', // required, must start with '.'
255
+ redirect: 301, // optional, 301 (default) or 302
357
256
  },
358
257
  template: {
359
258
  ext: ['ejs'],
360
259
  render: async (ctx, next, filePath) => {
361
- ctx.body = await ejs.renderFile(filePath, { lang: ctx.state.lang });
362
- ctx.type = 'text/html';
363
- }
364
- }
260
+ ctx.body = await ejs.renderFile(filePath);
261
+ ctx.type = 'text/html';
262
+ },
263
+ },
365
264
  }));
366
-
367
- app.listen(3000);
265
+ // GET /about → serves views/about.ejs
266
+ // GET /about.ejs → 301 redirect to /about
368
267
  ```
369
268
 
370
- **Result:**
371
-
372
- | Request | Rewritten URL | File resolved | Redirect target |
373
- |---------|---------------|---------------|-----------------|
374
- | `/it/about` | `/about` | `views/about.ejs` | — |
375
- | `/en/blog/first-post` | `/blog/first-post` | `views/blog/first-post.ejs` | — |
376
- | `/it/about.ejs` | `/about.ejs` | — | 301 → `/it/about` (preserves `/it/` prefix) |
269
+ ### 10. URL Rewriting Support (`useOriginalUrl`)
377
270
 
378
- > **Note**: Redirects always use `ctx.originalUrl` to preserve the language prefix, regardless of the `useOriginalUrl` setting.
379
-
380
- #### Temporary redirect (302)
381
-
382
- Use `redirect: 302` instead of 301 when the URL mapping may change (staging, A/B testing, or during migration):
271
+ Set `useOriginalUrl: false` when running behind i18n routers or path-rewriters that mutate `ctx.url`:
383
272
 
384
273
  ```javascript
385
- hideExtension: {
386
- ext: '.ejs',
387
- redirect: 302 // Temporary redirect browsers won't cache it
388
- }
389
- ```
390
-
391
- > **When to use 302**: A 301 (permanent) tells browsers and search engines to cache the redirect. Use 302 (temporary) during development, staging, or when you're not yet sure the clean URL structure is final.
392
-
393
- ### 9. With HTTP Caching
394
-
395
- Enable aggressive caching for static files:
274
+ app.use(async (ctx, next) => {
275
+ if (ctx.path.startsWith('/it/')) {
276
+ ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html /page.html
277
+ }
278
+ await next();
279
+ });
396
280
 
397
- ```javascript
398
- app.use(koaClassicServer(__dirname + '/public', {
399
- browserCacheEnabled: true, // Enable ETag and Last-Modified
400
- browserCacheMaxAge: 86400, // Cache for 24 hours (in seconds)
281
+ app.use(koaClassicServer(__dirname + '/www', {
282
+ useOriginalUrl: false, // use ctx.url (rewritten) instead of ctx.originalUrl
401
283
  }));
402
284
  ```
403
285
 
404
- **⚠️ Important: Production Recommendation**
405
-
406
- The default value for `browserCacheEnabled` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `browserCacheEnabled: true`** to benefit from:
407
-
408
- - 80-95% bandwidth reduction
409
- - 304 Not Modified responses for unchanged files
410
- - Faster page loads for returning visitors
411
- - Reduced server load
412
-
413
- **See details:** [HTTP Caching Optimization →](./docs/OPTIMIZATION_HTTP_CACHING.md)
414
-
415
- ### 10. Multiple Index Files with Priority
416
-
417
- Search for multiple index files with custom order:
286
+ ### 11. HTTP Caching (opt-in)
418
287
 
419
288
  ```javascript
420
289
  app.use(koaClassicServer(__dirname + '/public', {
421
- index: [
422
- 'index.html', // First priority
423
- 'index.htm', // Second priority
424
- /index\.[eE][jJ][sS]/, // Third: index.ejs (case-insensitive)
425
- 'default.html' // Last priority
426
- ]
290
+ browserCacheEnabled: true, // emit ETag + Last-Modified, honor If-None-Match / If-Modified-Since
291
+ browserCacheMaxAge: 86400, // Cache-Control: max-age=86400 (24h)
427
292
  }));
428
293
  ```
429
294
 
430
- **See details:** [Index Option Priority →](./docs/INDEX_OPTION_PRIORITY.md)
295
+ Defaults: `browserCacheEnabled: false` (development-friendly). Enable in production for an 80–95% bandwidth reduction on cache hits.
431
296
 
432
- ### 11. Complete Production Example
433
-
434
- Real-world configuration for production:
297
+ ### 12. Complete Production Example
435
298
 
436
299
  ```javascript
437
- const Koa = require('koa');
438
- const koaClassicServer = require('koa-classic-server');
439
- const ejs = require('ejs');
300
+ const Koa = require('koa');
440
301
  const path = require('path');
302
+ const pino = require('pino')({ level: 'info' });
303
+ const ejs = require('ejs');
304
+ const koaClassicServer = require('koa-classic-server');
441
305
 
442
306
  const app = new Koa();
443
307
 
444
- // Serve static assets with caching
308
+ // Allowlist Host headers to mitigate DNS rebinding (see docs/DOCUMENTATION.md → Sicurezza).
309
+ const ALLOWED_HOSTS = new Set(['app.example.com', 'localhost:3000']);
310
+ app.use(async (ctx, next) => {
311
+ if (!ALLOWED_HOSTS.has(ctx.host)) { ctx.status = 421; ctx.body = 'Host not allowed'; return; }
312
+ await next();
313
+ });
314
+
315
+ // Static-file security headers (see docs/DOCUMENTATION.md → Limiti dei Security Headers).
316
+ app.use(async (ctx, next) => {
317
+ ctx.set('X-Content-Type-Options', 'nosniff');
318
+ ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
319
+ ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
320
+ await next();
321
+ });
322
+
445
323
  app.use(koaClassicServer(path.join(__dirname, 'public'), {
446
- method: ['GET', 'HEAD'],
447
- showDirContents: false, // Disable directory listing in production
448
- index: ['index.html', 'index.htm'],
449
- urlPrefix: '/assets',
450
- urlsReserved: ['/admin', '/api', '/.git'],
324
+ index: ['index.html'],
325
+ dirListing: {
326
+ enabled: process.env.NODE_ENV !== 'production',
327
+ maxEntries: 10000,
328
+ entriesPerPage: 100,
329
+ },
451
330
  browserCacheEnabled: true,
452
- browserCacheMaxAge: 86400, // 24 hours
453
- }));
454
-
455
- // Serve dynamic templates with clean URLs
456
- app.use(koaClassicServer(path.join(__dirname, 'views'), {
457
- showDirContents: false,
458
- index: ['index.ejs'],
459
- useOriginalUrl: false, // Use ctx.url (for i18n or routing middleware)
460
- hideExtension: {
461
- ext: '.ejs', // /about → serves about.ejs
462
- redirect: 301 // /about.ejs → 301 redirect to /about
331
+ browserCacheMaxAge: 86400,
332
+ logger: pino,
333
+ hidden: {
334
+ dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
335
+ dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
336
+ alwaysHide: ['*.key', /^backup-/],
463
337
  },
464
338
  template: {
465
- ext: ['ejs'],
466
- render: async (ctx, next, filePath) => {
467
- const data = {
468
- env: process.env.NODE_ENV,
469
- user: ctx.state.user,
470
- config: ctx.state.config
471
- };
472
-
473
- try {
474
- ctx.body = await ejs.renderFile(filePath, data);
475
- ctx.type = 'text/html';
476
- } catch (error) {
477
- console.error('Template error:', error);
478
- ctx.status = 500;
479
- ctx.body = 'Internal Server Error';
480
- }
481
- }
482
- }
339
+ ext: ['ejs'],
340
+ renderTimeout: 5000,
341
+ render: async (ctx, next, filePath, { signal }) => {
342
+ ctx.body = await ejs.renderFile(filePath, { user: ctx.state.user }, { signal });
343
+ ctx.type = 'text/html';
344
+ },
345
+ },
483
346
  }));
484
347
 
485
348
  app.listen(3000);
@@ -489,243 +352,324 @@ app.listen(3000);
489
352
 
490
353
  ## API Reference
491
354
 
492
- ### koaClassicServer(rootDir, options)
355
+ ### `koaClassicServer(rootDir, options)`
493
356
 
494
357
  Creates a Koa middleware for serving static files.
495
358
 
496
359
  **Parameters:**
497
-
498
- - **`rootDir`** (String, required): Absolute path to the directory containing files
499
- - **`options`** (Object, optional): Configuration options
360
+ - **`rootDir`** *(String, required)* — Absolute path to the directory containing files
361
+ - **`options`** *(Object, optional)* Configuration options
500
362
 
501
363
  **Returns:** Koa middleware function
502
364
 
503
- ### Options
365
+ ### Options Summary
504
366
 
505
367
  ```javascript
506
368
  {
507
369
  // HTTP methods allowed (default: ['GET'])
508
370
  method: ['GET', 'HEAD'],
509
371
 
510
- // Show directory contents (default: true)
511
- showDirContents: true,
372
+ // Directory listing (V3 namespace)
373
+ dirListing: {
374
+ enabled: true,
375
+ maxEntries: 10000, // cap visible entries (0 = disabled)
376
+ entriesPerPage: 100, // entries per page (0 = disabled)
377
+ },
512
378
 
513
- // Index file configuration
514
- // Array format (recommended):
515
- // - Strings: exact matches ['index.html', 'default.html']
516
- // - RegExp: pattern matches [/index\.html/i]
517
- // - Mixed: ['index.html', /INDEX\.HTM/i]
518
- // Priority determined by array order (first match wins)
519
- // See docs/INDEX_OPTION_PRIORITY.md for details
379
+ // Index file resolution (Array of strings and/or RegExp)
520
380
  index: ['index.html', 'index.htm'],
521
381
 
522
- // URL path prefix (default: '')
523
- // Files served under this prefix
524
- urlPrefix: '/static',
525
-
526
- // Reserved paths (default: [])
527
- // First-level directories passed to next middleware
528
- urlsReserved: ['/admin', '/api', '/.git'],
529
-
530
- // Template engine configuration
531
- template: {
532
- // Template rendering function
533
- render: async (ctx, next, filePath) => {
534
- // Your rendering logic
535
- ctx.body = await yourEngine.render(filePath, data);
536
- },
382
+ // URL routing
383
+ urlPrefix: '/static',
384
+ urlsReserved: ['/api', '/admin'],
385
+ useOriginalUrl: true,
537
386
 
538
- // File extensions to process
539
- ext: ['ejs', 'pug', 'hbs']
387
+ // Hidden files / dirs
388
+ hidden: {
389
+ dotFiles: { default: 'visible', whitelist: [], blacklist: [] },
390
+ dotDirs: { default: 'visible', whitelist: [], blacklist: [] },
391
+ alwaysHide: [],
540
392
  },
541
393
 
542
- // Browser HTTP caching configuration
543
- // NOTE: Default is false for development. Set to true in production for better performance!
544
- browserCacheEnabled: false, // Enable ETag & Last-Modified (default: false)
545
- browserCacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
394
+ // Clean URLs
395
+ hideExtension: { ext: '.ejs', redirect: 301 },
546
396
 
547
- // URL resolution
548
- useOriginalUrl: true, // Use ctx.originalUrl (default) or ctx.url
549
- // Set false for URL rewriting middleware (i18n, routing)
397
+ // Browser HTTP caching
398
+ browserCacheEnabled: false,
399
+ browserCacheMaxAge: 3600,
550
400
 
551
- // Clean URLs - hide file extension from URLs (mod_rewrite-like)
552
- hideExtension: {
553
- ext: '.ejs', // Extension to hide (required, must start with '.')
554
- redirect: 301 // HTTP redirect code (optional, default: 301)
401
+ // Template engine
402
+ template: {
403
+ ext: ['ejs'],
404
+ renderTimeout: 30000, // ms; 0 disables the cap
405
+ render: async (ctx, next, filePath, { signal }) => { /* ... */ },
555
406
  },
556
407
 
557
- // DEPRECATED (use new names above):
558
- // enableCaching: use browserCacheEnabled instead
559
- // cacheMaxAge: use browserCacheMaxAge instead
408
+ // Observability
409
+ logger: console, // any { error, warn, info, debug } shape
560
410
  }
561
411
  ```
562
412
 
563
413
  ### Options Details
564
414
 
565
415
  | Option | Type | Default | Description |
566
- |--------|------|---------|-------------|
567
- | `method` | Array | `['GET']` | Allowed HTTP methods |
568
- | `showDirContents` | Boolean | `true` | Show directory listing |
569
- | `index` | Array/String | `[]` | Index file patterns (array format recommended) |
570
- | `urlPrefix` | String | `''` | URL path prefix |
571
- | `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
572
- | `template.render` | Function | `undefined` | Template rendering function |
573
- | `template.ext` | Array | `[]` | Extensions for template rendering |
574
- | `browserCacheEnabled` | Boolean | `false` | Enable browser HTTP caching headers (recommended: `true` in production) |
575
- | `browserCacheMaxAge` | Number | `3600` | Browser cache duration in seconds |
576
- | `useOriginalUrl` | Boolean | `true` | Use `ctx.originalUrl` (default) or `ctx.url` for URL resolution |
577
- | `hideExtension.ext` | String | - | Extension to hide (e.g. `'.ejs'`). Enables clean URL feature |
578
- | `hideExtension.redirect` | Number | `301` | HTTP redirect code for URLs with extension |
579
- | ~~`enableCaching`~~ | Boolean | `false` | **DEPRECATED**: Use `browserCacheEnabled` instead |
580
- | ~~`cacheMaxAge`~~ | Number | `3600` | **DEPRECATED**: Use `browserCacheMaxAge` instead |
581
-
582
- #### useOriginalUrl (Boolean, default: true)
583
-
584
- Controls which URL property is used for file resolution:
585
- - **`true` (default)**: Uses `ctx.originalUrl` (immutable, reflects the original request)
586
- - **`false`**: Uses `ctx.url` (mutable, can be modified by middleware)
587
-
588
- **When to use `false`:**
589
-
590
- Set `useOriginalUrl: false` when using URL rewriting middleware such as i18n routers or path rewriters that modify `ctx.url`. This allows koa-classic-server to serve files based on the rewritten URL instead of the original request URL.
591
-
592
- **Example with i18n middleware:**
416
+ |---|---|---|---|
417
+ | `method` | `String[]` | `['GET']` | Allowed HTTP methods |
418
+ | `dirListing.enabled` | `Boolean` | `true` | **V3** Render directory listing HTML when no index file matches |
419
+ | `dirListing.maxEntries` | `Number` | `10000` | **V3** Cap entries shown / sorted / stat'd (0 = disabled) |
420
+ | `dirListing.entriesPerPage` | `Number` | `100` | **V3** Entries per listing page (0 = disabled) |
421
+ | `index` | `Array` | `[]` | Index file patterns (strings, RegExp, or mixed) |
422
+ | `urlPrefix` | `String` | `''` | URL path prefix |
423
+ | `urlsReserved` | `String[]` | `[]` | First-level paths passed through to next middleware |
424
+ | `useOriginalUrl` | `Boolean` | `true` | Use `ctx.originalUrl` (`true`) or `ctx.url` (`false`) |
425
+ | `hideExtension.ext` | `String` | | Extension to hide (`.ejs`, must start with `.`) |
426
+ | `hideExtension.redirect` | `Number` | `301` | HTTP redirect code |
427
+ | `hidden.dotFiles.default` | `String` | `'visible'` | Default visibility for `.foo` files (`'hidden'` to harden) |
428
+ | `hidden.dotFiles.whitelist` | `Array` | `[]` | Names always visible (string/glob/RegExp) |
429
+ | `hidden.dotFiles.blacklist` | `Array` | `[]` | Names always hidden (overrides whitelist) |
430
+ | `hidden.dotDirs.default` | `String` | `'visible'` | Default visibility for `.foo` directories |
431
+ | `hidden.dotDirs.whitelist` | `Array` | `[]` | Names always visible |
432
+ | `hidden.dotDirs.blacklist` | `Array` | `[]` | Names always hidden |
433
+ | `hidden.alwaysHide` | `Array` | `[]` | Path-aware patterns (string glob or RegExp) |
434
+ | `browserCacheEnabled` | `Boolean` | `false` | Emit ETag + Last-Modified (recommended `true` in production) |
435
+ | `browserCacheMaxAge` | `Number` | `3600` | `Cache-Control: max-age` in seconds |
436
+ | `template.render` | `Function` | – | `async (ctx, next, filePath, { signal }) => void` |
437
+ | `template.ext` | `String[]` | `[]` | Extensions handled by the template engine |
438
+ | `template.renderTimeout` | `Number` | `30000` | **V3** Max render time in ms (0 = disabled) |
439
+ | `logger` | `Object` | `console` | **V3** Logger with `{ error, warn, info, debug }` |
440
+
441
+ For deep dives, see [DOCUMENTATION.md](./docs/DOCUMENTATION.md) and the per-option guides in [`docs/`](./docs).
593
442
 
594
- ```javascript
595
- const Koa = require('koa');
596
- const koaClassicServer = require('koa-classic-server');
597
-
598
- const app = new Koa();
443
+ ---
599
444
 
600
- // i18n middleware that rewrites URLs
601
- app.use(async (ctx, next) => {
602
- if (ctx.path.match(/^\/it\//)) {
603
- ctx.url = ctx.path.replace(/^\/it/, ''); // /it/page.html → /page.html
604
- }
605
- await next();
606
- });
445
+ ## Directory Listing Features
607
446
 
608
- // Serve files using rewritten URL
609
- app.use(koaClassicServer(__dirname + '/www', {
610
- useOriginalUrl: false // Use ctx.url (rewritten) instead of ctx.originalUrl
611
- }));
447
+ ### Sortable Columns
612
448
 
613
- app.listen(3000);
614
- ```
449
+ Click any column header to sort:
450
+ - **Name** — Alphabetical (A→Z / Z→A)
451
+ - **Type** — By MIME type (directories first)
452
+ - **Size** — By byte size (directories first)
615
453
 
616
- **How it works:**
617
- - Request: `GET /it/page.html`
618
- - `ctx.originalUrl`: `/it/page.html` (unchanged)
619
- - `ctx.url`: `/page.html` (rewritten by middleware)
620
- - With `useOriginalUrl: false`: Server looks for `/www/page.html` ✅
621
- - With `useOriginalUrl: true` (default): Server looks for `/www/it/page.html` ❌ 404
454
+ Visual indicators: `↑` ascending, `↓` descending. Sort + order are preserved across pagination links.
622
455
 
623
- ---
456
+ ### Pagination (V3)
624
457
 
625
- ## Directory Listing Features
458
+ When the number of visible entries exceeds `dirListing.entriesPerPage`, a numbered paginator is rendered below the table:
626
459
 
627
- ### Sortable Columns
460
+ ```
461
+ « First · ‹ Prev · 0 · 1 · … · 7 · 8 · 9 · Next › · Last »
462
+ ```
628
463
 
629
- Click on column headers to sort:
464
+ - Page index is 0-based (`?page=N`).
465
+ - Invalid or out-of-range values clamp silently.
466
+ - Response header `X-Dir-Pagination: <current>/<last>` is emitted only when pagination is meaningful.
630
467
 
631
- - **Name** - Alphabetical sorting (A-Z or Z-A)
632
- - **Type** - Sort by MIME type (directories always first)
633
- - **Size** - Sort by file size (directories always first)
468
+ ### Truncation Banner (V3)
634
469
 
635
- Visual indicators:
636
- - **↑** - Ascending order
637
- - **↓** - Descending order
470
+ When `dirListing.maxEntries` is hit, a banner is rendered above the table and `X-Dir-Truncated: <N>` is set, so capped listings are visible both to users and to monitoring.
638
471
 
639
472
  ### File Size Display
640
473
 
641
- Human-readable format:
642
- - `1.5 KB` - Kilobytes
643
- - `2.3 MB` - Megabytes
644
- - `1.2 GB` - Gigabytes
645
- - `-` - Directories (no size)
474
+ Human-readable: `1.5 KB`, `2.3 MB`, `1.2 GB`. Directories show `-`.
646
475
 
647
476
  ### Navigation
648
477
 
649
- - **Click folder name** - Enter directory
650
- - **Click file name** - Download/view file
651
- - **Parent Directory** - Go up one level
478
+ - Click folder enter directory
479
+ - Click file serve / download
480
+ - **Parent Directory** link go up one level
652
481
 
653
482
  ### Symlink Support
654
483
 
655
- The middleware fully supports symbolic links, which is essential for environments where served files are symlinks rather than regular files:
484
+ The middleware follows symbolic links transparently via `fs.promises.stat()` useful in NixOS, Docker bind mounts, `npm link`, and Capistrano-style deploys.
656
485
 
657
- - **NixOS buildFHSEnv** - Files in www/ appear as symlinks to the Nix store
658
- - **Docker bind mounts** - Mounted files may appear as symlinks
659
- - **npm link** - Linked packages are symlinks
660
- - **Capistrano-style deploys** - The `current` directory is a symlink to the active release
486
+ | Entry type | Indicator | Clickable | Type column |
487
+ |---|---|---|---|
488
+ | Symlink to file | `( Symlink )` | yes | target MIME |
489
+ | Symlink to directory | `( Symlink )` | yes | `DIR` |
490
+ | Broken symlink | `( Broken Symlink )` | no | original MIME guess |
661
491
 
662
- **How it works:**
492
+ Regular files incur zero additional `stat()` overhead.
663
493
 
664
- Symlinks are followed transparently via `fs.promises.stat()`, but only when `dirent.isSymbolicLink()` is true. Regular files incur zero additional overhead.
494
+ ---
665
495
 
666
- **Directory listing indicators:**
496
+ ## Security
667
497
 
668
- | Entry type | Indicator | Clickable | Type shown |
669
- |------------|-----------|-----------|------------|
670
- | Symlink to file | `( Symlink )` | Yes | Target MIME type |
671
- | Symlink to directory | `( Symlink )` | Yes | `DIR` |
672
- | Broken/circular symlink | `( Broken Symlink )` | No | `unknown` |
673
- | Regular file/directory | none | Yes | Real type |
498
+ ### Built-in Protection
674
499
 
675
- **Edge cases handled:**
676
- - Broken symlinks (missing target) return 404 on direct access
677
- - Circular symlinks (A → B → A) are treated as broken, no infinite loops
678
- - Symlinks to directories are fully navigable
500
+ #### 1. Path Traversal
679
501
 
680
- ---
502
+ ```text
503
+ GET /../../etc/passwd → 403 Forbidden
504
+ GET /%2e%2e%2fpackage.json → 403 Forbidden
505
+ GET /file\0.txt → 400 Bad Request (null-byte guard)
506
+ ```
681
507
 
682
- ## Security
508
+ Defense in depth: null-byte rejection → `path.normalize()` → resolved-path boundary check against `rootDir`.
683
509
 
684
- ### Built-in Protection
510
+ #### 2. XSS in Directory Listing
685
511
 
686
- koa-classic-server includes enterprise-grade security:
512
+ All file and directory names are HTML-escaped. CSS is inlined under a hash-based `Content-Security-Policy` recomputed at module load — script execution from inline `<style>`/`<script>` is rejected by the browser.
687
513
 
688
- #### 1. Path Traversal Protection
514
+ #### 3. Dot-Files Hidden by Default (V3)
689
515
 
690
- Prevents access to files outside `rootDir`:
516
+ `.env`, `.git/config`, SSH keys, etc. return 404 unless explicitly whitelisted via `hidden.dotFiles.whitelist`. The `.well-known` whitelist pattern stays friendly to ACME / Let's Encrypt.
691
517
 
692
- ```javascript
693
- // ❌ Blocked requests
694
- GET /../../../etc/passwd → 403 Forbidden
695
- GET /../config/database.yml → 403 Forbidden
696
- GET /%2e%2e%2fpackage.json → 403 Forbidden
697
- ```
518
+ #### 4. Security Headers on Generated Pages
698
519
 
699
- #### 2. XSS Protection
520
+ The middleware emits the following on directory listings and error pages (404/405/500/etc.):
700
521
 
701
- All filenames and paths are HTML-escaped:
522
+ | Header | Value |
523
+ |---|---|
524
+ | `Content-Security-Policy` | hash-based on listing, fully restrictive on errors |
525
+ | `X-Content-Type-Options` | `nosniff` |
526
+ | `X-Frame-Options` | `DENY` |
527
+ | `Referrer-Policy` | `no-referrer` |
528
+ | `Permissions-Policy` | `camera=(), microphone=(), geolocation=(), payment=()` |
702
529
 
703
- ```javascript
704
- // Malicious filename: <script>alert('xss')</script>.txt
705
- // Displayed as: &lt;script&gt;alert('xss')&lt;/script&gt;.txt
706
- // ✅ Safe - script doesn't execute
707
- ```
530
+ > ⚠️ User-served static files (HTML/JS/CSS on disk) are returned **without** these headers — by design. See [docs/DOCUMENTATION.md → Limiti dei Security Headers](./docs/DOCUMENTATION.md#limiti-dei-security-headers-sui-file-statici) for an upstream-middleware example that applies your own CSP/HSTS to static files.
531
+
532
+ #### 5. DNS Rebinding
708
533
 
709
- #### 3. Reserved URLs
534
+ The middleware does not validate the `Host` header — that belongs to the reverse proxy or an application-level allowlist. See [docs/DOCUMENTATION.md → DNS Rebinding](./docs/DOCUMENTATION.md#dns-rebinding--valida-lheader-host-a-monte) for nginx + Koa allowlist examples.
710
535
 
711
- Protect sensitive directories:
536
+ #### 6. Reserved URLs
712
537
 
713
538
  ```javascript
714
539
  app.use(koaClassicServer(__dirname, {
715
- urlsReserved: ['/admin', '/config', '/.git', '/node_modules']
540
+ urlsReserved: ['/admin', '/api', '/.git', '/node_modules'],
716
541
  }));
717
542
  ```
718
543
 
719
- #### 4. Race Condition Protection
544
+ #### 7. Race-Condition Protection
545
+
546
+ File metadata is verified before streaming. A file deleted between check and access returns `404`, never a crash or partial response.
547
+
548
+ #### 8. Bounded Listings (V3)
720
549
 
721
- File access is verified before streaming:
550
+ `dirListing.maxEntries` caps the number of entries that are sorted, stat'd, and rendered per listing — bounds CPU and HTML size against accidentally-large folders. The initial `readdir()` is not bounded by this option; an opt-in streaming mode for adversarial-directory workloads is planned for v3.1.
551
+
552
+ #### 9. Template Render Timeout (V3)
553
+
554
+ `template.renderTimeout` (default 30 s) prevents a hung or runaway template render from blocking the request indefinitely; the AbortSignal forwarded to the renderer lets you abort downstream I/O cleanly.
555
+
556
+ **See:**
557
+ - [Security improvement roadmap →](./docs/security_improvement_for_V3.md)
558
+ - [Security tests →](./__tests__/security.test.js)
559
+
560
+ ### Design philosophy & Security Checklist
561
+
562
+ koa-classic-server follows the principle: **"if a file is in `rootDir`, `GET` on its path returns it"**. The defaults serve files without applying surprise restrictions — the operator is the source of truth. See [`CLAUDE.md`](./CLAUDE.md) for the full design philosophy.
563
+
564
+ This means hardening is **opt-in via explicit configuration**. The checklist below covers the most common production concerns. Each item is one or two lines of configuration; not all of them apply to every deployment.
565
+
566
+ #### ✅ Static site / public asset serving
567
+
568
+ - [ ] **Hide dot-files** that may contain secrets:
569
+ `hidden: { dotFiles: { default: 'hidden', whitelist: ['.well-known'] } }`
570
+ - [ ] **Block dot-directories** like `.git`:
571
+ `hidden: { dotDirs: { default: 'hidden', whitelist: ['.well-known'] } }`
572
+ - [ ] **Disable directory listing** in production:
573
+ `dirListing: { enabled: false }` (combine with an `index` file)
574
+ - [ ] **Enable browser HTTP caching**:
575
+ `browserCacheEnabled: true, browserCacheMaxAge: 86400`
576
+ - [ ] **Restrict methods** to read-only (default already `['GET']`):
577
+ `method: ['GET', 'HEAD']`
578
+ - [ ] **Reserve sensitive paths** for app routes:
579
+ `urlsReserved: ['/api', '/admin']`
580
+ - [ ] **Add upstream security headers** for user-served HTML (not auto-added by this middleware — see *DNS Rebinding / Headers* in `docs/DOCUMENTATION.md`).
581
+
582
+ #### ✅ User uploads, multi-tenant, untrusted-write directories
583
+
584
+ - [ ] **Lower the entry cap** for accidentally-large dirs:
585
+ `dirListing: { maxEntries: 1000 }` (default 100000 is a safety net, not a security feature)
586
+ - [ ] **Hide dot-files at every depth**:
587
+ `hidden: { dotFiles: { default: 'hidden' }, dotDirs: { default: 'hidden' } }`
588
+ - [ ] **Add path-aware blocklists** for known secret patterns:
589
+ `hidden: { alwaysHide: ['*.key', '*.pem', /\.secret$/, 'config/secrets/**'] }`
590
+ - [ ] **Monitor directory growth externally** (cron + alert) — the v3.0 cap bounds rendering CPU but not the initial `readdir()` allocation. See `[F-1]` in `docs/security_improvement_for_V3.md` for the v3.1 streaming-read opt-in tracking this gap.
591
+
592
+ #### ✅ Production hygiene (any deployment)
593
+
594
+ - [ ] **Validate `Host` header upstream** (nginx `server_name` allowlist or app-level middleware) — this middleware does NOT validate `Host`. See *DNS Rebinding* in `docs/DOCUMENTATION.md`.
595
+ - [ ] **Disable template-engine in production** if you don't use SSR — minimizes attack surface:
596
+ omit the `template` option entirely
597
+ - [ ] **Tune `template.renderTimeout`** if you do use SSR — default 30 s is conservative; tighten for tight-SLA services
598
+ - [ ] **Inject a real logger** instead of `console`:
599
+ `logger: pino()` so security-relevant warnings reach your aggregation
600
+ - [ ] **Pin the latest patch version** in `package.json` and run `npm audit` in CI
601
+
602
+ ### Suggested production security configuration
603
+
604
+ A single configuration block that covers most production deployments. Start here and tune for your workload (static site vs uploads vs internal admin):
722
605
 
723
606
  ```javascript
724
- // File deleted between check and access?
725
- // Returns 404 instead of crashing
607
+ const Koa = require('koa');
608
+ const pino = require('pino')({ level: 'info' });
609
+ const path = require('path');
610
+ const koaClassicServer = require('koa-classic-server');
611
+
612
+ const app = new Koa();
613
+
614
+ // 1) Validate Host header — mitigates DNS rebinding on LAN / loopback exposure.
615
+ const ALLOWED_HOSTS = new Set([
616
+ 'app.example.com',
617
+ 'localhost:3000',
618
+ ]);
619
+ app.use(async (ctx, next) => {
620
+ if (!ALLOWED_HOSTS.has(ctx.host)) {
621
+ ctx.status = 421;
622
+ ctx.body = 'Misdirected Request';
623
+ return;
624
+ }
625
+ await next();
626
+ });
627
+
628
+ // 2) Apply security headers to user-served HTML/JS/CSS. The middleware
629
+ // sets these only on its own generated pages (listing + errors).
630
+ app.use(async (ctx, next) => {
631
+ ctx.set('X-Content-Type-Options', 'nosniff');
632
+ ctx.set('Referrer-Policy', 'strict-origin-when-cross-origin');
633
+ ctx.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
634
+ await next();
635
+ });
636
+
637
+ // 3) The file server with hardened defaults.
638
+ app.use(koaClassicServer(path.join(__dirname, 'public'), {
639
+ method: ['GET', 'HEAD'], // read-only
640
+
641
+ index: ['index.html'], // serve index when present
642
+
643
+ dirListing: {
644
+ enabled: process.env.NODE_ENV !== 'production',
645
+ maxEntries: 10000, // tighten the soft cap below the 100k default
646
+ entriesPerPage: 100,
647
+ },
648
+
649
+ hidden: {
650
+ dotFiles: {
651
+ default: 'hidden', // hide .env / .htaccess / etc by default
652
+ whitelist: ['.well-known'], // expose ACME / Let's Encrypt
653
+ },
654
+ dotDirs: {
655
+ default: 'hidden',
656
+ whitelist: ['.well-known'],
657
+ },
658
+ alwaysHide: ['*.key', '*.pem', /^backup-/, /\.secret$/],
659
+ },
660
+
661
+ browserCacheEnabled: true,
662
+ browserCacheMaxAge: 86400, // 24 h — bandwidth savings on cache hits
663
+
664
+ logger: pino, // pipe internal warnings to structured logs
665
+
666
+ urlsReserved: ['/api', '/admin'], // routes handled by other middleware
667
+ }));
668
+
669
+ app.listen(3000);
726
670
  ```
727
671
 
728
- **See full security audit:** [Security Tests →](./__tests__/security.test.js)
672
+ For multi-tenant or user-upload scenarios, also drop `dirListing.maxEntries` to `1000` and monitor the served directory's size externally.
729
673
 
730
674
  ---
731
675
 
@@ -733,32 +677,21 @@ File access is verified before streaming:
733
677
 
734
678
  ### Optimizations
735
679
 
736
- Version 2.x includes major performance improvements:
737
-
738
- - **Async/Await** - Non-blocking I/O, event loop never blocked
739
- - **Array Join** - 30-40% less memory vs string concatenation
740
- - **HTTP Caching** - 80-95% bandwidth reduction
741
- - **Single stat() Call** - No double file system access
742
- - **Streaming** - Large files streamed efficiently
680
+ - **Single-syscall `readdir()`** — directory entries fetched in one batched syscall, then sliced to `dirListing.maxEntries` to cap rendering work
681
+ - **Single `stat()`** per item — no double filesystem traversal
682
+ - **Array `.join()`** for listing HTML significantly less GC pressure than `+=`
683
+ - **HTTP conditional responses** 304s with `If-None-Match` / `If-Modified-Since` (when caching enabled)
684
+ - **File streaming** large files streamed via `fs.createReadStream`, never buffered in full
685
+ - **Pre-computed CSP hash** — SHA-256 of inline CSS hashed once at module load, not per request
743
686
 
744
687
  ### Benchmarks
745
688
 
746
- Performance results on directory with 1,000 files:
747
-
748
- ```
749
- Before (v1.x): ~350ms per request
750
- After (v2.x): ~190ms per request
751
- Improvement: 46% faster
752
- ```
753
-
754
- **See detailed benchmarks:** [Performance Analysis →](./docs/PERFORMANCE_ANALYSIS.md)
689
+ See [`docs/BENCHMARKS.md`](./docs/BENCHMARKS.md) and [`docs/PERFORMANCE_COMPARISON.md`](./docs/PERFORMANCE_COMPARISON.md) for full benchmarks and methodology.
755
690
 
756
691
  ---
757
692
 
758
693
  ## Testing
759
694
 
760
- Run the comprehensive test suite:
761
-
762
695
  ```bash
763
696
  # Run all tests
764
697
  npm test
@@ -770,106 +703,127 @@ npm run test:security
770
703
  npm run test:performance
771
704
  ```
772
705
 
773
- **Test Coverage:**
774
- - ✅ 309 tests passing
775
- - ✅ Security tests (path traversal, XSS, race conditions)
776
- - ✅ EJS template integration tests
777
- - ✅ Index option tests (strings, arrays, RegExp)
778
- - ✅ hideExtension tests (clean URLs, redirects, conflicts, validation)
779
- - ✅ Symlink tests (file, directory, broken, circular, indicators)
706
+ **Coverage:**
707
+ - ✅ 532 tests passing across 20 suites
708
+ - ✅ Security (path traversal, XSS, race conditions, CSP, hidden-files)
709
+ - ✅ Directory listing (sorting, pagination, truncation cap, symlinks)
710
+ - ✅ Template engine (timeout, abort signal, error propagation, EJS integration)
711
+ - ✅ Logger injection (validation, custom logger, console default)
712
+ - ✅ Index option (arrays, RegExp, priority)
713
+ - ✅ `hideExtension` (clean URLs, redirects, conflicts, validation)
714
+ - ✅ HTTP caching (ETag, Last-Modified, 304)
780
715
  - ✅ Performance benchmarks
781
- - ✅ Directory sorting tests
782
716
 
783
717
  ---
784
718
 
785
719
  ## Complete Documentation
786
720
 
787
- ### Core Documentation
788
-
789
- - **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** - Complete API reference and usage guide
790
- - **[FLOW_DIAGRAM.md](./docs/FLOW_DIAGRAM.md)** - Visual flow diagrams and code execution paths
791
- - **[CHANGELOG.md](./docs/CHANGELOG.md)** - Version history and release notes
721
+ ### Core
722
+ - **[DOCUMENTATION.md](./docs/DOCUMENTATION.md)** — Full API reference and usage guide
723
+ - **[FLOW_DIAGRAM.md](./docs/FLOW_DIAGRAM.md)** Visual flow diagrams and execution paths
724
+ - **[CHANGELOG.md](./docs/CHANGELOG.md)** Version history and release notes
792
725
 
793
726
  ### Template Engine
794
-
795
- - **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** - Complete guide to template engine integration
796
- - Progressive examples (simple to enterprise)
797
- - EJS, Pug, Handlebars, Nunjucks support
798
- - Best practices and troubleshooting
727
+ - **[TEMPLATE_ENGINE_GUIDE.md](./docs/template-engine/TEMPLATE_ENGINE_GUIDE.md)** — EJS, Pug, Handlebars, Nunjucks; AbortSignal + timeout patterns
799
728
 
800
729
  ### Configuration
730
+ - **[INDEX_OPTION_PRIORITY.md](./docs/INDEX_OPTION_PRIORITY.md)** — Priority rules for `index`
731
+ - **[EXAMPLES_INDEX_OPTION.md](./docs/EXAMPLES_INDEX_OPTION.md)** — 10 practical examples
801
732
 
802
- - **[INDEX_OPTION_PRIORITY.md](./docs/INDEX_OPTION_PRIORITY.md)** - Detailed priority behavior for `index` option
803
- - String vs Array vs RegExp formats
804
- - Priority order examples
805
- - Migration guide from v1.x
806
-
807
- - **[EXAMPLES_INDEX_OPTION.md](./docs/EXAMPLES_INDEX_OPTION.md)** - 10 practical examples of `index` option with RegExp
808
- - Case-insensitive matching
809
- - Multiple extensions
810
- - Complex patterns
733
+ ### Security
734
+ - **[security_improvement_for_V3.md](./docs/security_improvement_for_V3.md)** Audit roadmap and status
811
735
 
812
736
  ### Performance
737
+ - **[PERFORMANCE_ANALYSIS.md](./docs/PERFORMANCE_ANALYSIS.md)** — Optimization analysis
738
+ - **[PERFORMANCE_COMPARISON.md](./docs/PERFORMANCE_COMPARISON.md)** — Latency, throughput, concurrency
739
+ - **[OPTIMIZATION_HTTP_CACHING.md](./docs/OPTIMIZATION_HTTP_CACHING.md)** — Caching internals
740
+ - **[BENCHMARKS.md](./docs/BENCHMARKS.md)** — Methodology and results
813
741
 
814
- - **[PERFORMANCE_ANALYSIS.md](./docs/PERFORMANCE_ANALYSIS.md)** - Performance optimization analysis
815
- - Before/after comparisons
816
- - Memory usage analysis
817
- - Bottleneck identification
818
-
819
- - **[PERFORMANCE_COMPARISON.md](./docs/PERFORMANCE_COMPARISON.md)** - Detailed performance benchmarks
820
- - Request latency
821
- - Throughput metrics
822
- - Concurrent request handling
742
+ ### Code Quality
743
+ - **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** — Code review and standards
744
+ - **[DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)** Known limitations and debugging
823
745
 
824
- - **[OPTIMIZATION_HTTP_CACHING.md](./docs/OPTIMIZATION_HTTP_CACHING.md)** - HTTP caching implementation details
825
- - ETag generation
826
- - Last-Modified headers
827
- - 304 Not Modified responses
746
+ ---
828
747
 
829
- - **[BENCHMARKS.md](./docs/BENCHMARKS.md)** - Benchmark results and methodology
748
+ ## Migration Guide
830
749
 
831
- ### Code Quality
750
+ ### From v2.x to v3.x
832
751
 
833
- - **[CODE_REVIEW.md](./docs/CODE_REVIEW.md)** - Code quality analysis and review
834
- - Security audit
835
- - Best practices
836
- - Standardization improvements
752
+ **Breaking changes**
837
753
 
838
- - **[DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)** - Known limitations and debugging info
839
- - Reserved URLs behavior
840
- - Edge cases
841
- - Troubleshooting tips
754
+ | What | v2.x | v3.x |
755
+ |---|---|---|
756
+ | `index: 'index.html'` | accepted | **throws** — must be an array |
757
+ | `cacheMaxAge` | accepted | **removed** — use `browserCacheMaxAge` |
758
+ | `enableCaching` | accepted | **removed** — use `browserCacheEnabled` |
759
+ | `showDirContents` | accepted | accepted as **deprecated alias** — emits a one-time warning, prefer `dirListing: { enabled: true }` |
760
+ | Dot-files | served | **served** (unchanged — opt into hiding via `hidden.dotFiles.default: 'hidden'`; see Security Checklist) |
761
+ | Logger | `console` only | `logger` option injects any logger; default still `console` |
762
+ | Template `render` signature | `(ctx, next, filePath)` | `(ctx, next, filePath, { signal })` — old signature still works, `signal` is opt-in |
842
763
 
843
- ---
764
+ **Quick migration**
844
765
 
845
- ## Migration Guide
846
-
847
- ### From v1.x to v2.x
766
+ ```javascript
767
+ // v2.x
768
+ app.use(koaClassicServer(root, {
769
+ index: 'index.html',
770
+ enableCaching: true,
771
+ cacheMaxAge: 3600,
772
+ showDirContents: true,
773
+ }));
848
774
 
849
- **Breaking Changes:**
850
- - `index` option: String format deprecated (still works), use array format
775
+ // v3.x
776
+ app.use(koaClassicServer(root, {
777
+ index: ['index.html'],
778
+ browserCacheEnabled: true,
779
+ browserCacheMaxAge: 3600,
780
+ dirListing: { enabled: true },
781
+ }));
782
+ ```
851
783
 
852
- **Migration:**
784
+ **Dot-files in v3**
853
785
 
854
786
  ```javascript
855
- // v1.x (deprecated)
787
+ // To restore v2.x behavior (serve dot-files):
788
+ { hidden: { dotFiles: { default: 'visible' } } }
789
+
790
+ // Recommended v3 — hide dot-files but expose .well-known for ACME / Let's Encrypt:
856
791
  {
857
- index: 'index.html'
792
+ hidden: {
793
+ dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
794
+ dotDirs: { default: 'hidden', whitelist: ['.well-known'] },
795
+ },
858
796
  }
797
+ ```
859
798
 
860
- // v2.x (recommended)
861
- {
862
- index: ['index.html']
799
+ **Template render in v3**
800
+
801
+ ```javascript
802
+ // v2.x — still works:
803
+ template: { render: async (ctx, next, filePath) => { /* ... */ } }
804
+
805
+ // v3.x — opt into the AbortSignal:
806
+ template: {
807
+ renderTimeout: 5000,
808
+ render: async (ctx, next, filePath, { signal }) => {
809
+ const data = await fetchData({ signal });
810
+ ctx.body = await ejs.renderFile(filePath, data, { signal });
811
+ ctx.type = 'text/html';
812
+ },
863
813
  }
864
814
  ```
865
815
 
866
- **New Features:**
867
- - HTTP caching (enabled by default)
868
- - Sortable directory columns
869
- - File size display
870
- - Enhanced index option with RegExp
816
+ ### From v1.x to v2.x
817
+
818
+ ```javascript
819
+ // v1.x
820
+ { index: 'index.html' }
821
+
822
+ // v2.x+
823
+ { index: ['index.html'] }
824
+ ```
871
825
 
872
- **See full migration guide:** [CHANGELOG.md](./docs/CHANGELOG.md)
826
+ See the full [CHANGELOG.md](./docs/CHANGELOG.md) for every change.
873
827
 
874
828
  ---
875
829
 
@@ -894,22 +848,26 @@ const koaClassicServer = require('koa-classic-server');
894
848
 
895
849
  const app = new Koa();
896
850
 
897
- // Serve static assets
851
+ // Static assets — no listing in production
898
852
  app.use(koaClassicServer(__dirname + '/public', {
899
853
  urlPrefix: '/static',
900
- showDirContents: false
854
+ dirListing: { enabled: false },
901
855
  }));
902
856
 
903
- // Serve user uploads
857
+ // User uploads — paginated browsable index
904
858
  app.use(koaClassicServer(__dirname + '/uploads', {
905
859
  urlPrefix: '/files',
906
- showDirContents: true
860
+ dirListing: {
861
+ enabled: true,
862
+ maxEntries: 5000,
863
+ entriesPerPage: 50,
864
+ },
907
865
  }));
908
866
 
909
867
  app.listen(3000);
910
868
  ```
911
869
 
912
- ### Example 3: Development Server with Templates
870
+ ### Example 3: Development Server with Templates + Timeout
913
871
 
914
872
  ```javascript
915
873
  const Koa = require('koa');
@@ -918,63 +876,77 @@ const ejs = require('ejs');
918
876
 
919
877
  const app = new Koa();
920
878
 
921
- // Development mode - show directories
922
879
  app.use(koaClassicServer(__dirname + '/src', {
923
- showDirContents: true,
880
+ dirListing: { enabled: true },
924
881
  template: {
925
882
  ext: ['ejs'],
926
- render: async (ctx, next, filePath) => {
883
+ renderTimeout: 3000,
884
+ render: async (ctx, next, filePath, { signal }) => {
927
885
  ctx.body = await ejs.renderFile(filePath, {
928
886
  dev: true,
929
- timestamp: Date.now()
930
- });
887
+ timestamp: Date.now(),
888
+ }, { signal });
931
889
  ctx.type = 'text/html';
932
- }
933
- }
890
+ },
891
+ },
934
892
  }));
935
893
 
936
894
  app.listen(3000);
937
895
  ```
938
896
 
939
- ---
897
+ ### Example 4: Production with Pino Logger + Caching
940
898
 
941
- ## Troubleshooting
899
+ ```javascript
900
+ const Koa = require('koa');
901
+ const pino = require('pino')({ level: 'info' });
902
+ const koaClassicServer = require('koa-classic-server');
942
903
 
943
- ### Common Issues
904
+ const app = new Koa();
944
905
 
945
- **Issue: 404 errors for all files**
906
+ app.use(koaClassicServer(__dirname + '/public', {
907
+ index: ['index.html'],
908
+ dirListing: { enabled: false },
909
+ browserCacheEnabled: true,
910
+ browserCacheMaxAge: 86400,
911
+ logger: pino,
912
+ }));
946
913
 
947
- Check that `rootDir` is an absolute path:
914
+ app.listen(3000);
915
+ ```
948
916
 
949
- ```javascript
950
- // ❌ Wrong (relative path)
951
- koaClassicServer('./public')
917
+ ---
952
918
 
953
- // ✅ Correct (absolute path)
954
- koaClassicServer(__dirname + '/public')
955
- koaClassicServer(path.join(__dirname, 'public'))
956
- ```
919
+ ## Troubleshooting
957
920
 
958
- **Issue: Reserved URLs not working**
921
+ **404 for all files**
959
922
 
960
- Reserved URLs only work for first-level directories:
923
+ Use an absolute path for `rootDir`:
961
924
 
962
925
  ```javascript
963
- urlsReserved: ['/admin'] // Blocks /admin/*
964
- urlsReserved: ['/admin/users'] // Doesn't work (nested)
926
+ koaClassicServer('./public') // relative
927
+ koaClassicServer(__dirname + '/public') // absolute
928
+ koaClassicServer(path.join(__dirname, 'pub')) // ✅ absolute
965
929
  ```
966
930
 
967
- **Issue: Directory sorting not working**
931
+ **Reserved URLs not matching nested paths**
932
+
933
+ `urlsReserved` only matches first-level path segments — use it for top-level routes (`/api`), not nested ones (`/api/users`).
934
+
935
+ **Directory listing shows fewer files than expected**
936
+
937
+ Check the response headers: `X-Dir-Truncated` indicates the `dirListing.maxEntries` cap was reached. Increase the cap or paginate via `?page=N`.
938
+
939
+ **Templates time out under load**
968
940
 
969
- Make sure you're accessing directories without query params initially. The sorting is applied when you click headers.
941
+ Lower `template.renderTimeout` to fail fast, forward the `signal` to your I/O, and check the logger output for `Template render timeout after Xms` warnings.
970
942
 
971
- **See full troubleshooting:** [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md)
943
+ See full troubleshooting: [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md).
972
944
 
973
945
  ---
974
946
 
975
947
  ## Contributing
976
948
 
977
- Contributions are welcome! Please:
949
+ Contributions are welcome:
978
950
 
979
951
  1. Fork the repository
980
952
  2. Create a feature branch
@@ -986,8 +958,9 @@ Contributions are welcome! Please:
986
958
 
987
959
  ## Known Limitations
988
960
 
989
- - Reserved URLs only work for first-level directories
990
- - Template rendering is synchronous per request
961
+ - `urlsReserved` only matches first-level path segments
962
+ - The middleware does not validate the `Host` header — configure a reverse proxy or an upstream allowlist (see [DOCUMENTATION.md → DNS Rebinding](./docs/DOCUMENTATION.md#dns-rebinding--valida-lheader-host-a-monte))
963
+ - Static files are returned without security headers — apply your own upstream middleware (see [DOCUMENTATION.md → Limiti dei Security Headers](./docs/DOCUMENTATION.md#limiti-dei-security-headers-sui-file-statici))
991
964
 
992
965
  See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
993
966
 
@@ -995,7 +968,7 @@ See [DEBUG_REPORT.md](./docs/DEBUG_REPORT.md) for technical details.
995
968
 
996
969
  ## License
997
970
 
998
- MIT License - see LICENSE file for details
971
+ MIT License see LICENSE file for details.
999
972
 
1000
973
  ---
1001
974
 
@@ -1007,10 +980,10 @@ Italo Paesano
1007
980
 
1008
981
  ## Links
1009
982
 
1010
- - **[npm Package](https://www.npmjs.com/package/koa-classic-server)** - Official npm package
1011
- - **[GitHub Repository](https://github.com/italopaesano/koa-classic-server)** - Source code
1012
- - **[Issue Tracker](https://github.com/italopaesano/koa-classic-server/issues)** - Report bugs
1013
- - **[Full Documentation](./docs/DOCUMENTATION.md)** - Complete reference
983
+ - **[npm Package](https://www.npmjs.com/package/koa-classic-server)** Official npm package
984
+ - **[GitHub Repository](https://github.com/italopaesano/koa-classic-server)** Source code
985
+ - **[Issue Tracker](https://github.com/italopaesano/koa-classic-server/issues)** Report bugs
986
+ - **[Full Documentation](./docs/DOCUMENTATION.md)** Complete reference
1014
987
 
1015
988
  ---
1016
989