koa-classic-server 2.6.0 → 3.0.0-alpha.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/README.md +68 -10
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +270 -0
- package/__tests__/customTest/serversToLoad.util.js +1 -1
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/dt-unknown.test.js +635 -0
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +422 -0
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +8 -4
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +153 -0
- package/__tests__/security.test.js +145 -159
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +423 -0
- package/__tests__/symlink.test.js +8 -5
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +118 -0
- package/docs/EXAMPLES_INDEX_OPTION.md +2 -2
- package/docs/FLOW_DIAGRAM.md +13 -13
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/eslint.config.mjs +17 -0
- package/index.cjs +1114 -378
- package/index.mjs +1 -5
- package/package.json +4 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Piano d'Azione — koa-classic-server
|
|
2
|
+
|
|
3
|
+
> Documento di lavoro per il miglioramento progressivo del progetto.
|
|
4
|
+
> Ogni fase segue il ciclo: **Descrizione → Soluzioni proposte → Domande → Implementazione → Test → Avanzamento**
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Indice delle fasi
|
|
9
|
+
|
|
10
|
+
| # | Fase | Priorità | Stato |
|
|
11
|
+
|---|------|----------|-------|
|
|
12
|
+
| 1 | [File nascosti nel directory listing](#fase-1--file-nascosti-nel-directory-listing) | Alta | ⬜ Da fare |
|
|
13
|
+
| 2 | [Opzione glob per escludere file sensibili](#fase-2--opzione-glob-per-escludere-file-sensibili) | Alta | ⬜ Da fare |
|
|
14
|
+
| 3 | [Content-Security-Policy nell'HTML generato](#fase-3--content-security-policy-nellhtml-generato) | Media | ⬜ Da fare |
|
|
15
|
+
| 4 | [Aggiunta ESLint e standardizzazione `===`](#fase-4--aggiunta-eslint-e-standardizzazione-) | Media | ⬜ Da fare |
|
|
16
|
+
| 5 | [Range Requests HTTP 206](#fase-5--range-requests-http-206) | Media | ⬜ Da fare |
|
|
17
|
+
| 6 | [Gzip / Brotli compression](#fase-6--gzip--brotli-compression) | Media | ⬜ Da fare |
|
|
18
|
+
| 7 | [Traduzione commenti in inglese nei test](#fase-7--traduzione-commenti-in-inglese-nei-test) | Bassa | ⬜ Da fare |
|
|
19
|
+
| 8 | [Roadmap rimozione opzioni deprecate (v3.0.0)](#fase-8--roadmap-rimozione-opzioni-deprecate-v300) | Bassa | ⬜ Da fare |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
> **Legenda stato:** ⬜ Da fare · 🔄 In corso · ✅ Completato · ⏸️ In pausa
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Processo standard per ogni fase
|
|
28
|
+
|
|
29
|
+
Per ogni fase si segue sempre questo ciclo:
|
|
30
|
+
|
|
31
|
+
1. **Descrizione** — Spiegazione del problema e del suo impatto
|
|
32
|
+
2. **Soluzioni proposte** — Almeno 2 alternative valutate con pro/contro
|
|
33
|
+
3. **Domande** — Se le informazioni disponibili sono < 90%, si chiedono chiarimenti prima di procedere
|
|
34
|
+
4. **Implementazione** — Modifica del codice sul branch `claude/project-review-EN6kt`
|
|
35
|
+
5. **Test** — Esecuzione della suite di test e verifica regressioni
|
|
36
|
+
6. **Avanzamento** — Aggiornamento dello stato in questo file e passaggio alla fase successiva
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Fase 1 — File nascosti nel directory listing
|
|
41
|
+
|
|
42
|
+
**Stato:** ⬜ Da fare
|
|
43
|
+
**Priorità:** Alta
|
|
44
|
+
**File coinvolti:** `index.cjs` (funzione `show_dir`)
|
|
45
|
+
|
|
46
|
+
### Descrizione del problema
|
|
47
|
+
|
|
48
|
+
Il directory listing mostra **tutti** i file e le cartelle presenti nella webroot, inclusi quelli che iniziano con `.` (dot files), come:
|
|
49
|
+
|
|
50
|
+
- `.env` — variabili d'ambiente con credenziali
|
|
51
|
+
- `.gitignore`, `.gitattributes` — metadati Git
|
|
52
|
+
- `.htpasswd` — password Apache
|
|
53
|
+
- `.DS_Store` — metadati macOS
|
|
54
|
+
- `.npmrc` — configurazioni npm con token
|
|
55
|
+
|
|
56
|
+
Server come Apache e Nginx nascondono questi file per default. Un file dimenticato nella webroot potrebbe esporre informazioni sensibili agli utenti del browser.
|
|
57
|
+
|
|
58
|
+
### Soluzioni proposte
|
|
59
|
+
|
|
60
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
61
|
+
|
|
62
|
+
### Domande
|
|
63
|
+
|
|
64
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
65
|
+
|
|
66
|
+
### Implementazione
|
|
67
|
+
|
|
68
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
69
|
+
|
|
70
|
+
### Test
|
|
71
|
+
|
|
72
|
+
*Da verificare dopo l'implementazione.*
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Fase 2 — Opzione glob per escludere file sensibili
|
|
77
|
+
|
|
78
|
+
**Stato:** ⬜ Da fare
|
|
79
|
+
**Priorità:** Alta
|
|
80
|
+
**File coinvolti:** `index.cjs` (opzioni di configurazione + `show_dir`)
|
|
81
|
+
|
|
82
|
+
### Descrizione del problema
|
|
83
|
+
|
|
84
|
+
Non esiste un'opzione per escludere file o cartelle specifici dal serving e dal directory listing tramite pattern (glob o RegExp). Se un file come `config.json`, `secrets.yaml` o qualsiasi altro file sensibile si trova nella webroot, viene servito senza restrizioni.
|
|
85
|
+
|
|
86
|
+
Esempi di pattern utili:
|
|
87
|
+
- `*.env` — tutti i file .env
|
|
88
|
+
- `**/*.secret` — file con estensione .secret
|
|
89
|
+
- `private/**` — intera cartella private
|
|
90
|
+
|
|
91
|
+
### Soluzioni proposte
|
|
92
|
+
|
|
93
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
94
|
+
|
|
95
|
+
### Domande
|
|
96
|
+
|
|
97
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
98
|
+
|
|
99
|
+
### Implementazione
|
|
100
|
+
|
|
101
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
102
|
+
|
|
103
|
+
### Test
|
|
104
|
+
|
|
105
|
+
*Da verificare dopo l'implementazione.*
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Fase 3 — Content-Security-Policy nell'HTML generato
|
|
110
|
+
|
|
111
|
+
**Stato:** ⬜ Da fare
|
|
112
|
+
**Priorità:** Media
|
|
113
|
+
**File coinvolti:** `index.cjs` (funzione `show_dir`)
|
|
114
|
+
|
|
115
|
+
### Descrizione del problema
|
|
116
|
+
|
|
117
|
+
Le pagine HTML generate dal directory listing non includono un header `Content-Security-Policy`. Senza CSP, il browser non ha indicazioni su quali risorse sono autorizzate, aumentando il rischio di attacchi XSS in caso di eventuali vulnerabilità future nel codice di generazione HTML.
|
|
118
|
+
|
|
119
|
+
Il progetto già implementa l'escape HTML (funzione `escapeHtml`), ma aggiungere CSP è un ulteriore livello di difesa in profondità.
|
|
120
|
+
|
|
121
|
+
### Soluzioni proposte
|
|
122
|
+
|
|
123
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
124
|
+
|
|
125
|
+
### Domande
|
|
126
|
+
|
|
127
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
128
|
+
|
|
129
|
+
### Implementazione
|
|
130
|
+
|
|
131
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
132
|
+
|
|
133
|
+
### Test
|
|
134
|
+
|
|
135
|
+
*Da verificare dopo l'implementazione.*
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Fase 4 — Aggiunta ESLint e standardizzazione `===`
|
|
140
|
+
|
|
141
|
+
**Stato:** ⬜ Da fare
|
|
142
|
+
**Priorità:** Media
|
|
143
|
+
**File coinvolti:** `index.cjs`, `package.json`, nuovi file di config
|
|
144
|
+
|
|
145
|
+
### Descrizione del problema
|
|
146
|
+
|
|
147
|
+
Il codice usa `==` (uguaglianza debole con coercizione di tipo) in circa 25 punti, documentati in `docs/CODE_REVIEW.md`. Anche se nel contesto attuale non causano bug, l'uso di `===` è la best practice JavaScript e previene errori sottili in futuro.
|
|
148
|
+
|
|
149
|
+
Inoltre il progetto non ha alcun linter o formatter configurato, il che rende difficile mantenere la coerenza stilistica nel tempo e nelle contribuzioni esterne.
|
|
150
|
+
|
|
151
|
+
### Soluzioni proposte
|
|
152
|
+
|
|
153
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
154
|
+
|
|
155
|
+
### Domande
|
|
156
|
+
|
|
157
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
158
|
+
|
|
159
|
+
### Implementazione
|
|
160
|
+
|
|
161
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
162
|
+
|
|
163
|
+
### Test
|
|
164
|
+
|
|
165
|
+
*Da verificare dopo l'implementazione.*
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Fase 5 — Range Requests HTTP 206
|
|
170
|
+
|
|
171
|
+
**Stato:** ⬜ Da fare
|
|
172
|
+
**Priorità:** Media
|
|
173
|
+
**File coinvolti:** `index.cjs` (logica di file serving)
|
|
174
|
+
|
|
175
|
+
### Descrizione del problema
|
|
176
|
+
|
|
177
|
+
Il middleware non supporta l'header `Range` delle richieste HTTP. Questo significa che:
|
|
178
|
+
|
|
179
|
+
- File audio e video non possono essere riprodotti in streaming dai browser
|
|
180
|
+
- Download di file grandi non possono essere ripresi dopo interruzione
|
|
181
|
+
- Strumenti come `curl --range` non funzionano
|
|
182
|
+
|
|
183
|
+
Il supporto HTTP 206 (Partial Content) è lo standard per la distribuzione di contenuti multimediali e file di grandi dimensioni.
|
|
184
|
+
|
|
185
|
+
### Soluzioni proposte
|
|
186
|
+
|
|
187
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
188
|
+
|
|
189
|
+
### Domande
|
|
190
|
+
|
|
191
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
192
|
+
|
|
193
|
+
### Implementazione
|
|
194
|
+
|
|
195
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
196
|
+
|
|
197
|
+
### Test
|
|
198
|
+
|
|
199
|
+
*Da verificare dopo l'implementazione.*
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Fase 6 — Gzip / Brotli compression
|
|
204
|
+
|
|
205
|
+
**Stato:** ⬜ Da fare
|
|
206
|
+
**Priorità:** Media
|
|
207
|
+
**File coinvolti:** `index.cjs`, `package.json`
|
|
208
|
+
|
|
209
|
+
### Descrizione del problema
|
|
210
|
+
|
|
211
|
+
Il middleware serve tutti i file senza compressione. Per file di testo (HTML, CSS, JS, JSON, SVG), la compressione può ridurre le dimensioni del 60-80%, con impatto diretto su:
|
|
212
|
+
|
|
213
|
+
- Velocità di caricamento per gli utenti
|
|
214
|
+
- Consumo di banda del server
|
|
215
|
+
- Score Lighthouse / Web Vitals
|
|
216
|
+
|
|
217
|
+
### Soluzioni proposte
|
|
218
|
+
|
|
219
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
220
|
+
|
|
221
|
+
### Domande
|
|
222
|
+
|
|
223
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
224
|
+
|
|
225
|
+
### Implementazione
|
|
226
|
+
|
|
227
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
228
|
+
|
|
229
|
+
### Test
|
|
230
|
+
|
|
231
|
+
*Da verificare dopo l'implementazione.*
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Fase 7 — Traduzione commenti in inglese nei test
|
|
236
|
+
|
|
237
|
+
**Stato:** ⬜ Da fare
|
|
238
|
+
**Priorità:** Bassa
|
|
239
|
+
**File coinvolti:** `__tests__/dt-unknown.test.js` e altri
|
|
240
|
+
|
|
241
|
+
### Descrizione del problema
|
|
242
|
+
|
|
243
|
+
Alcuni file di test contengono commenti in italiano, il che riduce l'accessibilità del codice per i contribuenti internazionali. Per un progetto open source pubblicato su npm, la lingua standard della codebase dovrebbe essere l'inglese.
|
|
244
|
+
|
|
245
|
+
### Soluzioni proposte
|
|
246
|
+
|
|
247
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
248
|
+
|
|
249
|
+
### Domande
|
|
250
|
+
|
|
251
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
252
|
+
|
|
253
|
+
### Implementazione
|
|
254
|
+
|
|
255
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
256
|
+
|
|
257
|
+
### Test
|
|
258
|
+
|
|
259
|
+
*Da verificare dopo l'implementazione.*
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Fase 8 — Roadmap rimozione opzioni deprecate (v3.0.0)
|
|
264
|
+
|
|
265
|
+
**Stato:** ⬜ Da fare
|
|
266
|
+
**Priorità:** Bassa
|
|
267
|
+
**File coinvolti:** `index.cjs`, `docs/CHANGELOG.md`, `README.md`
|
|
268
|
+
|
|
269
|
+
### Descrizione del problema
|
|
270
|
+
|
|
271
|
+
Le opzioni legacy `cacheMaxAge` ed `enableCaching` sono ancora supportate con deprecation warning ma non hanno una data di rimozione pianificata. Mantenerle a tempo indeterminato appesantisce il codice e può confondere i nuovi utenti che leggono la documentazione.
|
|
272
|
+
|
|
273
|
+
Una roadmap chiara verso `v3.0.0` con breaking changes definiti aiuta gli utenti a pianificare la migrazione.
|
|
274
|
+
|
|
275
|
+
### Soluzioni proposte
|
|
276
|
+
|
|
277
|
+
*Da definire nella sessione di lavoro dedicata a questa fase.*
|
|
278
|
+
|
|
279
|
+
### Domande
|
|
280
|
+
|
|
281
|
+
*Da porre prima dell'implementazione se necessario.*
|
|
282
|
+
|
|
283
|
+
### Implementazione
|
|
284
|
+
|
|
285
|
+
*Da eseguire dopo la scelta della soluzione.*
|
|
286
|
+
|
|
287
|
+
### Test
|
|
288
|
+
|
|
289
|
+
*Da verificare dopo l'implementazione.*
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
*Documento aggiornato: 2026-03-21*
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,124 @@ All notable changes to koa-classic-server will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.0.0] - Unreleased
|
|
9
|
+
|
|
10
|
+
### 🆕 New Features
|
|
11
|
+
|
|
12
|
+
#### `hidden` option — protect dot-files, dot-dirs and custom patterns (Fase 1 + Fase 2)
|
|
13
|
+
|
|
14
|
+
A new `hidden` option controls which files and directories are blocked from both directory listing and direct HTTP access (returning **404**). Applies recursively to the entire directory tree.
|
|
15
|
+
|
|
16
|
+
**Default behavior (secure out of the box):**
|
|
17
|
+
- Dot-files (names starting with `.`) → **hidden by default** (`dotFiles.default: 'hidden'`)
|
|
18
|
+
- Dot-directories → **visible by default** (`dotDirs.default: 'visible'`)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
app.use(koaClassicServer('/public', {
|
|
22
|
+
hidden: {
|
|
23
|
+
dotFiles: {
|
|
24
|
+
default: 'hidden', // 'hidden' | 'visible'
|
|
25
|
+
whitelist: ['.well-known'], // Always visible (string exact/glob or RegExp)
|
|
26
|
+
blacklist: [], // Always hidden — overrides whitelist
|
|
27
|
+
},
|
|
28
|
+
dotDirs: {
|
|
29
|
+
default: 'visible',
|
|
30
|
+
whitelist: [],
|
|
31
|
+
blacklist: ['.git', /^\.svn/],
|
|
32
|
+
},
|
|
33
|
+
alwaysHide: ['*.secret', 'config/secrets/**', /\.key$/], // Path-aware patterns
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Priority (highest to lowest):**
|
|
39
|
+
1. `blacklist` — always hidden, beats everything
|
|
40
|
+
2. `whitelist` — always visible, overrides `alwaysHide` and `default`
|
|
41
|
+
3. `alwaysHide` — path-aware patterns, beats `default`
|
|
42
|
+
4. `default` — fallback behavior for unmatched dot-entries
|
|
43
|
+
|
|
44
|
+
**`alwaysHide` pattern rules:**
|
|
45
|
+
- String without `/`: matches basename at any depth (e.g. `*.secret` hides `a/b/file.secret`)
|
|
46
|
+
- String with `/`: path-anchored from root (e.g. `config/secrets/**`)
|
|
47
|
+
- RegExp: tested against the full relative path
|
|
48
|
+
|
|
49
|
+
**Blocked dot-dirs block sub-paths too:**
|
|
50
|
+
`GET /.git/config` returns 404 if `.git` is in `dotDirs.blacklist`.
|
|
51
|
+
|
|
52
|
+
### ⚠️ Breaking Changes
|
|
53
|
+
|
|
54
|
+
#### Dot-files hidden by default
|
|
55
|
+
`hidden.dotFiles.default` is `'hidden'` by default. In v2.x, dot-files (`.env`, `.gitignore`, etc.) were served normally. In v3.x they return 404 unless explicitly allowed.
|
|
56
|
+
|
|
57
|
+
To restore v2.x behavior: `hidden: { dotFiles: { default: 'visible' } }`
|
|
58
|
+
|
|
59
|
+
#### Removed string format for `index` option
|
|
60
|
+
- **Removed**: `index: 'index.html'` — passing a non-empty string now throws an `Error` at startup
|
|
61
|
+
- **Empty string** `index: ''` is still silently treated as `[]` (no index file, show directory listing)
|
|
62
|
+
- **Migration**:
|
|
63
|
+
```js
|
|
64
|
+
// Before (v2.x — now throws)
|
|
65
|
+
app.use(koaClassicServer('/public', { index: 'index.html' }));
|
|
66
|
+
|
|
67
|
+
// After (v3.0.0)
|
|
68
|
+
app.use(koaClassicServer('/public', { index: ['index.html'] }));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Removed deprecated option names `cacheMaxAge` and `enableCaching`
|
|
72
|
+
- **Removed**: `cacheMaxAge` — use `browserCacheMaxAge` instead
|
|
73
|
+
- **Removed**: `enableCaching` — use `browserCacheEnabled` instead
|
|
74
|
+
- **Behaviour**: Passing either removed option now throws an `Error` at startup with a clear migration message pointing to the new name and the current value.
|
|
75
|
+
- **Migration**:
|
|
76
|
+
```js
|
|
77
|
+
// Before (v2.x — now throws)
|
|
78
|
+
app.use(koaClassicServer('/public', {
|
|
79
|
+
enableCaching: true,
|
|
80
|
+
cacheMaxAge: 3600
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// After (v3.0.0)
|
|
84
|
+
app.use(koaClassicServer('/public', {
|
|
85
|
+
browserCacheEnabled: true,
|
|
86
|
+
browserCacheMaxAge: 3600
|
|
87
|
+
}));
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## [2.6.1] - 2026-03-04
|
|
93
|
+
|
|
94
|
+
### 🐛 Bug Fix
|
|
95
|
+
|
|
96
|
+
#### Fixed DT_UNKNOWN Handling (type 0) on overlayfs, NFS, FUSE, NixOS buildFHSEnv, ecryptfs
|
|
97
|
+
- **Issue**: On filesystems where `readdir({ withFileTypes: true })` returns dirents with `DT_UNKNOWN` (type 0), all `dirent.is*()` methods return `false`. This caused three failures:
|
|
98
|
+
1. `isFileOrSymlinkToFile()` missed valid files — `findIndexFile()` returned empty results, so `GET /` showed a directory listing instead of rendering the index file
|
|
99
|
+
2. `isDirOrSymlinkToDir()` missed valid directories — directory type resolution failed
|
|
100
|
+
3. `show_dir()` skipped entries with type 0, logging `"Unknown file type: 0"` — directory listings appeared empty or partial
|
|
101
|
+
- **Affected environments**: overlayfs (Docker image layers), NFS (some implementations), FUSE filesystems (sshfs, s3fs, rclone mount), NixOS with buildFHSEnv, ecryptfs (encrypted home directories), and any filesystem that doesn't fill `d_type` in the kernel's `getdents64` syscall
|
|
102
|
+
- **Impact**: HIGH — Server unusable on affected filesystems (index file not served, directory listing empty)
|
|
103
|
+
- **Fix**: Added `fs.promises.stat()` fallback in all three locations when none of the `dirent.is*()` type methods return `true` (i.e., type is genuinely unknown). On standard filesystems (ext4, btrfs, xfs, APFS, NTFS), `d_type` is always filled correctly, so the `stat()` fallback is never reached — **zero performance overhead** on the fast path.
|
|
104
|
+
- **Code**:
|
|
105
|
+
- `isFileOrSymlinkToFile()` — DT_UNKNOWN fallback via `stat().isFile()`
|
|
106
|
+
- `isDirOrSymlinkToDir()` — DT_UNKNOWN fallback via `stat().isDirectory()`
|
|
107
|
+
- `show_dir()` — Accept type 0 entries and resolve via `stat()` instead of skipping them
|
|
108
|
+
- **Reference**: Linux `man 2 getdents` — *"Currently, only some filesystems have full support for returning the file type in d_type. All applications must properly handle a return of DT_UNKNOWN."*
|
|
109
|
+
|
|
110
|
+
### 🧪 Testing
|
|
111
|
+
- Added `__tests__/dt-unknown.test.js` with 20 tests covering:
|
|
112
|
+
- `isFileOrSymlinkToFile` / `isDirOrSymlinkToDir` with DT_UNKNOWN dirents
|
|
113
|
+
- `findIndexFile` with all-unknown-type entries (string and RegExp patterns)
|
|
114
|
+
- `show_dir` rendering (resolved types, no skipped entries, correct MIME types and sizes)
|
|
115
|
+
- Full integration tests (index file serving, direct file access, complete directory listing)
|
|
116
|
+
- Edge cases (mixed regular + DT_UNKNOWN dirents, index priority, Dirent type 0 verification)
|
|
117
|
+
- Tests use `jest.spyOn(fs.promises, 'readdir')` to mock DT_UNKNOWN dirents via `new fs.Dirent(name, 0)` while keeping `fs.promises.stat()` working normally
|
|
118
|
+
- All 329 tests pass across 12 test suites (zero regressions)
|
|
119
|
+
|
|
120
|
+
### 📦 Package Changes
|
|
121
|
+
- **Version**: `2.6.0` → `2.6.1`
|
|
122
|
+
- **Semver**: Patch version bump (bug fix only, no API changes)
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
8
126
|
## [2.6.0] - 2026-03-01
|
|
9
127
|
|
|
10
128
|
### 📦 Dependency Upgrades
|
package/docs/FLOW_DIAGRAM.md
CHANGED
|
@@ -141,13 +141,13 @@ module.exports = function koaClassicServer(rootDir, opts = {}) {
|
|
|
141
141
|
: [];
|
|
142
142
|
|
|
143
143
|
// 7. Configure HTTP caching
|
|
144
|
-
options.
|
|
145
|
-
|
|
146
|
-
? options.
|
|
144
|
+
options.browserCacheMaxAge = typeof options.browserCacheMaxAge === 'number' &&
|
|
145
|
+
options.browserCacheMaxAge >= 0
|
|
146
|
+
? options.browserCacheMaxAge
|
|
147
147
|
: 3600;
|
|
148
|
-
options.
|
|
149
|
-
? options.
|
|
150
|
-
:
|
|
148
|
+
options.browserCacheEnabled = typeof options.browserCacheEnabled === 'boolean'
|
|
149
|
+
? options.browserCacheEnabled
|
|
150
|
+
: false;
|
|
151
151
|
|
|
152
152
|
// 8. Return Koa middleware function
|
|
153
153
|
return async (ctx, next) => {
|
|
@@ -172,8 +172,8 @@ START
|
|
|
172
172
|
│ ├─ urlsReserved: []
|
|
173
173
|
│ ├─ template.render: undefined
|
|
174
174
|
│ ├─ template.ext: []
|
|
175
|
-
│ ├─
|
|
176
|
-
│ └─
|
|
175
|
+
│ ├─ browserCacheMaxAge: 3600 (1 hour)
|
|
176
|
+
│ └─ browserCacheEnabled: false
|
|
177
177
|
│
|
|
178
178
|
└─> Return async middleware function
|
|
179
179
|
```
|
|
@@ -415,7 +415,7 @@ Handles serving individual files with caching, template rendering, and streaming
|
|
|
415
415
|
│
|
|
416
416
|
▼
|
|
417
417
|
┌─────────────────────────────────────────────────────────────────┐
|
|
418
|
-
│ 3. HTTP Caching (if
|
|
418
|
+
│ 3. HTTP Caching (if browserCacheEnabled = true) │
|
|
419
419
|
│ Generate ETag: "mtime-size" │
|
|
420
420
|
│ Set Last-Modified: mtime.toUTCString() │
|
|
421
421
|
│ Set Cache-Control: public, max-age=3600 │
|
|
@@ -492,7 +492,7 @@ fileExt in template.ext? → YES
|
|
|
492
492
|
### Code Example: HTTP Caching
|
|
493
493
|
```javascript
|
|
494
494
|
// index.cjs:313-350
|
|
495
|
-
if (options.
|
|
495
|
+
if (options.browserCacheEnabled) {
|
|
496
496
|
// Generate ETag
|
|
497
497
|
const etag = `"${fileStat.mtime.getTime()}-${fileStat.size}"`;
|
|
498
498
|
|
|
@@ -502,7 +502,7 @@ if (options.enableCaching) {
|
|
|
502
502
|
// Set headers
|
|
503
503
|
ctx.set('ETag', etag);
|
|
504
504
|
ctx.set('Last-Modified', lastModified);
|
|
505
|
-
ctx.set('Cache-Control', `public, max-age=${options.
|
|
505
|
+
ctx.set('Cache-Control', `public, max-age=${options.browserCacheMaxAge}, must-revalidate`);
|
|
506
506
|
|
|
507
507
|
// Check If-None-Match (ETag validation)
|
|
508
508
|
const clientEtag = ctx.get('If-None-Match');
|
|
@@ -782,8 +782,8 @@ app.use(koaClassicServer('/var/www/public', {
|
|
|
782
782
|
},
|
|
783
783
|
|
|
784
784
|
// HTTP caching (1 hour)
|
|
785
|
-
|
|
786
|
-
|
|
785
|
+
browserCacheMaxAge: 3600,
|
|
786
|
+
browserCacheEnabled: true
|
|
787
787
|
}));
|
|
788
788
|
|
|
789
789
|
app.listen(3000);
|