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
@@ -97,7 +97,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
97
97
  const app = new Koa();
98
98
  app.use(koaClassicServer(tmpDir, {
99
99
  index: ['index.html'],
100
- showDirContents: true
100
+ dirListing: { enabled: true }
101
101
  }));
102
102
  const server = app.listen();
103
103
  const request = supertest(server);
@@ -120,7 +120,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
120
120
  const app = new Koa();
121
121
  app.use(koaClassicServer(tmpDir, {
122
122
  index: ['subdir'],
123
- showDirContents: true
123
+ dirListing: { enabled: true }
124
124
  }));
125
125
  const server = app.listen();
126
126
  const request = supertest(server);
@@ -143,7 +143,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
143
143
  const app = new Koa();
144
144
  app.use(koaClassicServer(tmpDir, {
145
145
  index: ['nonexistent.html'],
146
- showDirContents: true
146
+ dirListing: { enabled: true }
147
147
  }));
148
148
  const server = app.listen();
149
149
  const request = supertest(server);
@@ -172,7 +172,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
172
172
  const app = new Koa();
173
173
  app.use(koaClassicServer(tmpDir, {
174
174
  index: ['index.html'],
175
- showDirContents: true
175
+ dirListing: { enabled: true }
176
176
  }));
177
177
  const server = app.listen();
178
178
  const request = supertest(server);
@@ -190,14 +190,18 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
190
190
  });
191
191
 
192
192
  test('should return false for DT_UNKNOWN entry pointing to a regular file', async () => {
193
- // Files should NOT be treated as directories
194
- // If only files exist and no index pattern matches, dir listing should show
193
+ // Files should NOT be treated as directories.
194
+ // Use a RegExp index pattern to force the readdir-based slow-path in
195
+ // findIndexFile (the string fast-path uses stat() directly and would bypass
196
+ // the mock). The regex never matches, so no index file is found and the
197
+ // directory listing is shown, confirming the DT_UNKNOWN files are treated
198
+ // as regular files (not directories).
195
199
  const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
196
200
 
197
201
  const app = new Koa();
198
202
  app.use(koaClassicServer(tmpDir, {
199
- index: ['index.html'],
200
- showDirContents: true
203
+ index: [/NOMATCH_SENTINEL/],
204
+ dirListing: { enabled: true }
201
205
  }));
202
206
  const server = app.listen();
203
207
  const request = supertest(server);
@@ -224,7 +228,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
224
228
  const app = new Koa();
225
229
  app.use(koaClassicServer(tmpDir, {
226
230
  index: ['index.html'],
227
- showDirContents: true
231
+ dirListing: { enabled: true }
228
232
  }));
229
233
  const server = app.listen();
230
234
  const request = supertest(server);
@@ -246,7 +250,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
246
250
  const app = new Koa();
247
251
  app.use(koaClassicServer(tmpDir, {
248
252
  index: ['index.ejs'],
249
- showDirContents: true
253
+ dirListing: { enabled: true }
250
254
  }));
251
255
  const server = app.listen();
252
256
  const request = supertest(server);
@@ -254,8 +258,9 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
254
258
  try {
255
259
  const res = await request.get('/');
256
260
  expect(res.status).toBe(200);
257
- expect(res.text).toContain('EJS DT_UNKNOWN');
258
- expect(res.text).not.toContain('Index of');
261
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
262
+ expect(body).toContain('EJS DT_UNKNOWN');
263
+ expect(body).not.toContain('Index of');
259
264
  } finally {
260
265
  server.close();
261
266
  spy.mockRestore();
@@ -268,7 +273,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
268
273
  const app = new Koa();
269
274
  app.use(koaClassicServer(tmpDir, {
270
275
  index: [/index\.[eE][jJ][sS]/],
271
- showDirContents: true
276
+ dirListing: { enabled: true }
272
277
  }));
273
278
  const server = app.listen();
274
279
  const request = supertest(server);
@@ -276,8 +281,9 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
276
281
  try {
277
282
  const res = await request.get('/');
278
283
  expect(res.status).toBe(200);
279
- expect(res.text).toContain('EJS DT_UNKNOWN');
280
- expect(res.text).not.toContain('Index of');
284
+ const body = res.text !== undefined ? res.text : res.body.toString('utf8');
285
+ expect(body).toContain('EJS DT_UNKNOWN');
286
+ expect(body).not.toContain('Index of');
281
287
  } finally {
282
288
  server.close();
283
289
  spy.mockRestore();
@@ -285,12 +291,17 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
285
291
  });
286
292
 
287
293
  test('should not find index in directory with only subdirectories (all DT_UNKNOWN)', async () => {
294
+ // Use a RegExp to force the readdir-based slow-path (the string fast-path
295
+ // would stat() index.html directly, finding the real file on disk).
296
+ // The regex never matches, so the mocked readdir result (['subdir']) is used:
297
+ // 'subdir' has DT_UNKNOWN → stat fallback → isDirectory() → not a file →
298
+ // fileNames stays empty → no index found → directory listing shown.
288
299
  const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
289
300
 
290
301
  const app = new Koa();
291
302
  app.use(koaClassicServer(tmpDir, {
292
- index: ['index.html'],
293
- showDirContents: true
303
+ index: [/NOMATCH_SENTINEL/],
304
+ dirListing: { enabled: true }
294
305
  }));
295
306
  const server = app.listen();
296
307
  const request = supertest(server);
@@ -298,7 +309,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
298
309
  try {
299
310
  const res = await request.get('/');
300
311
  expect(res.status).toBe(200);
301
- // No file matches index.html, so directory listing should appear
312
+ // No file matches the pattern, so directory listing should appear
302
313
  expect(res.text).toContain('Index of');
303
314
  } finally {
304
315
  server.close();
@@ -317,7 +328,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
317
328
  const app = new Koa();
318
329
  app.use(koaClassicServer(tmpDir, {
319
330
  index: [],
320
- showDirContents: true
331
+ dirListing: { enabled: true }
321
332
  }));
322
333
  const server = app.listen();
323
334
  const request = supertest(server);
@@ -345,7 +356,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
345
356
  const app = new Koa();
346
357
  app.use(koaClassicServer(tmpDir, {
347
358
  index: [],
348
- showDirContents: true
359
+ dirListing: { enabled: true }
349
360
  }));
350
361
  const server = app.listen();
351
362
  const request = supertest(server);
@@ -375,7 +386,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
375
386
  const app = new Koa();
376
387
  app.use(koaClassicServer(tmpDir, {
377
388
  index: [],
378
- showDirContents: true
389
+ dirListing: { enabled: true }
379
390
  }));
380
391
  const server = app.listen();
381
392
  const request = supertest(server);
@@ -399,7 +410,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
399
410
  const app = new Koa();
400
411
  app.use(koaClassicServer(tmpDir, {
401
412
  index: [],
402
- showDirContents: true
413
+ dirListing: { enabled: true }
403
414
  }));
404
415
  const server = app.listen();
405
416
  const request = supertest(server);
@@ -429,7 +440,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
429
440
  const app = new Koa();
430
441
  app.use(koaClassicServer(tmpDir, {
431
442
  index: ['index.html'],
432
- showDirContents: true
443
+ dirListing: { enabled: true }
433
444
  }));
434
445
  const server = app.listen();
435
446
  const request = supertest(server);
@@ -454,7 +465,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
454
465
  const app = new Koa();
455
466
  app.use(koaClassicServer(tmpDir, {
456
467
  index: [],
457
- showDirContents: true
468
+ dirListing: { enabled: true }
458
469
  }));
459
470
  const server = app.listen();
460
471
  const request = supertest(server);
@@ -475,7 +486,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
475
486
  const app = new Koa();
476
487
  app.use(koaClassicServer(tmpDir, {
477
488
  index: [],
478
- showDirContents: true
489
+ dirListing: { enabled: true }
479
490
  }));
480
491
  const server = app.listen();
481
492
  const request = supertest(server);
@@ -511,7 +522,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
511
522
  const app = new Koa();
512
523
  app.use(koaClassicServer(tmpDir, {
513
524
  index: ['index.ejs'],
514
- showDirContents: true,
525
+ dirListing: { enabled: true },
515
526
  template: {
516
527
  ext: ['ejs'],
517
528
  render: async (ctx, next, filePath) => {
@@ -562,7 +573,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
562
573
  const app = new Koa();
563
574
  app.use(koaClassicServer(tmpDir, {
564
575
  index: [],
565
- showDirContents: true
576
+ dirListing: { enabled: true }
566
577
  }));
567
578
  const server = app.listen();
568
579
  const request = supertest(server);
@@ -604,7 +615,7 @@ describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', (
604
615
  const app = new Koa();
605
616
  app.use(koaClassicServer(tmpDir, {
606
617
  index: ['index.html', 'index.ejs'],
607
- showDirContents: true
618
+ dirListing: { enabled: true }
608
619
  }));
609
620
  const server = app.listen();
610
621
  const request = supertest(server);
@@ -19,7 +19,7 @@ describe('EJS Template Engine Integration Tests', () => {
19
19
  app.use(
20
20
  koaClassicServer(rootDir, {
21
21
  method: ['GET'],
22
- showDirContents: true,
22
+ dirListing: { enabled: true },
23
23
  template: {
24
24
  ext: ['ejs'],
25
25
  render: async (ctx, next, filePath) => {
@@ -0,0 +1 @@
1
+ dot dir file
@@ -0,0 +1,2 @@
1
+ SECRET_KEY=mysecretvalue
2
+ DB_PASSWORD=hidden123
@@ -0,0 +1 @@
1
+ key data
@@ -0,0 +1 @@
1
+ secret data
@@ -0,0 +1 @@
1
+ regular
@@ -0,0 +1 @@
1
+ normal
@@ -0,0 +1 @@
1
+ NESTED_SECRET=nestedvalue
@@ -0,0 +1 @@
1
+ regular in subdir
@@ -0,0 +1,407 @@
1
+ const supertest = require('supertest');
2
+ const koaClassicServer = require('../index.cjs');
3
+ const Koa = require('koa');
4
+ const path = require('path');
5
+
6
+ const root = path.join(__dirname, 'hidden-fixtures');
7
+
8
+ function createApp(hiddenOpts) {
9
+ const app = new Koa();
10
+ app.use(koaClassicServer(root, { dirListing: { enabled: true }, hidden: hiddenOpts }));
11
+ return app.listen();
12
+ }
13
+
14
+ // Note: prior to v3.0.0 the dotFiles default was 'hidden' (a v3-alpha
15
+ // security-by-default choice) and a once-per-process warning was emitted
16
+ // when the option was implicit. The default was reverted to 'visible' to
17
+ // align with the "HTTP file server first" design philosophy (see
18
+ // CLAUDE.md). The warning is no longer emitted because the default is no
19
+ // longer a surprising restriction; tests for it have been removed.
20
+
21
+ // ─── Option validation ────────────────────────────────────────────────────────
22
+
23
+ describe('hidden option — validation', () => {
24
+ test('throws when dotFiles.default is not "hidden" or "visible"', () => {
25
+ expect(() =>
26
+ koaClassicServer(root, { hidden: { dotFiles: { default: 'yes' } } })
27
+ ).toThrow(/hidden\.dotFiles\.default must be "hidden" or "visible"/);
28
+ });
29
+
30
+ test('throws when dotDirs.default is not "hidden" or "visible"', () => {
31
+ expect(() =>
32
+ koaClassicServer(root, { hidden: { dotDirs: { default: 'yes' } } })
33
+ ).toThrow(/hidden\.dotDirs\.default must be "hidden" or "visible"/);
34
+ });
35
+
36
+ test('does not throw when hidden option is omitted', () => {
37
+ expect(() => koaClassicServer(root, {})).not.toThrow();
38
+ });
39
+
40
+ test('does not throw when hidden is a valid object', () => {
41
+ expect(() =>
42
+ koaClassicServer(root, {
43
+ hidden: {
44
+ dotFiles: { default: 'hidden', whitelist: ['.well-known'], blacklist: ['.env'] },
45
+ dotDirs: { default: 'visible', blacklist: [/^\.git/] },
46
+ alwaysHide: ['*.secret', /\.key$/]
47
+ }
48
+ })
49
+ ).not.toThrow();
50
+ });
51
+ });
52
+
53
+ // ─── dotFiles — default visible (system default in v3.0+) ────────────────────
54
+
55
+ describe('dotFiles — default visible (system default)', () => {
56
+ let server;
57
+ beforeAll(() => { server = createApp(undefined); });
58
+ afterAll(() => server.close());
59
+
60
+ // Per the V3 "HTTP file server first" philosophy (see CLAUDE.md), dot-files
61
+ // are visible by default. Operators harden by setting hidden.dotFiles.default
62
+ // to 'hidden' or by using blacklist / alwaysHide.
63
+ test('GET /.env returns 200 by default', async () => {
64
+ const res = await supertest(server).get('/.env');
65
+ expect(res.status).toBe(200);
66
+ });
67
+
68
+ test('GET /.gitignore returns 200 by default', async () => {
69
+ const res = await supertest(server).get('/.gitignore');
70
+ expect(res.status).toBe(200);
71
+ });
72
+
73
+ test('directory listing includes dot-files', async () => {
74
+ const res = await supertest(server).get('/');
75
+ expect(res.status).toBe(200);
76
+ expect(res.text).toContain('.env');
77
+ expect(res.text).toContain('.gitignore');
78
+ });
79
+
80
+ test('regular files remain accessible', async () => {
81
+ const res = await supertest(server).get('/normal.txt');
82
+ expect(res.status).toBe(200);
83
+ });
84
+ });
85
+
86
+ // ─── dotFiles default: 'hidden' (opt-in hardening) ───────────────────────────
87
+
88
+ describe('dotFiles — explicit default hidden (opt-in)', () => {
89
+ let server;
90
+ beforeAll(() => {
91
+ server = createApp({ dotFiles: { default: 'hidden' } });
92
+ });
93
+ afterAll(() => server.close());
94
+
95
+ test('GET /.env returns 404 when dotFiles.default = "hidden"', async () => {
96
+ const res = await supertest(server).get('/.env');
97
+ expect(res.status).toBe(404);
98
+ });
99
+
100
+ test('GET /.gitignore returns 404', async () => {
101
+ const res = await supertest(server).get('/.gitignore');
102
+ expect(res.status).toBe(404);
103
+ });
104
+
105
+ test('GET /subdir/.env returns 404 (dot-file in subdirectory)', async () => {
106
+ const res = await supertest(server).get('/subdir/.env');
107
+ expect(res.status).toBe(404);
108
+ });
109
+
110
+ test('directory listing does not include .env', async () => {
111
+ const res = await supertest(server).get('/');
112
+ expect(res.status).toBe(200);
113
+ expect(res.text).not.toContain('.env');
114
+ });
115
+
116
+ test('directory listing does not include .gitignore', async () => {
117
+ const res = await supertest(server).get('/');
118
+ expect(res.text).not.toContain('.gitignore');
119
+ });
120
+
121
+ test('regular files remain accessible', async () => {
122
+ const res = await supertest(server).get('/normal.txt');
123
+ expect(res.status).toBe(200);
124
+ });
125
+ });
126
+
127
+ // ─── dotFiles default: 'visible' (kept for completeness with explicit opt-in) ─
128
+
129
+ describe('dotFiles — default visible', () => {
130
+ let server;
131
+ beforeAll(() => {
132
+ server = createApp({ dotFiles: { default: 'visible' } });
133
+ });
134
+ afterAll(() => server.close());
135
+
136
+ test('GET /.env returns 200 when dotFiles.default is "visible"', async () => {
137
+ const res = await supertest(server).get('/.env');
138
+ expect(res.status).toBe(200);
139
+ });
140
+
141
+ test('directory listing includes .env when dotFiles.default is "visible"', async () => {
142
+ const res = await supertest(server).get('/');
143
+ expect(res.text).toContain('.env');
144
+ });
145
+ });
146
+
147
+ // ─── dotFiles whitelist ───────────────────────────────────────────────────────
148
+
149
+ describe('dotFiles — whitelist exceptions', () => {
150
+ let server;
151
+ beforeAll(() => {
152
+ server = createApp({
153
+ dotFiles: { default: 'hidden', whitelist: ['.well-known'] },
154
+ dotDirs: { default: 'hidden', whitelist: ['.well-known'] }
155
+ });
156
+ });
157
+ afterAll(() => server.close());
158
+
159
+ test('GET /.well-known/ is accessible (whitelisted dir)', async () => {
160
+ const res = await supertest(server).get('/.well-known/');
161
+ expect(res.status).toBe(200);
162
+ });
163
+
164
+ test('GET /.well-known/acme-challenge.txt is accessible (inside whitelisted dir)', async () => {
165
+ const res = await supertest(server).get('/.well-known/acme-challenge.txt');
166
+ expect(res.status).toBe(200);
167
+ });
168
+
169
+ test('.well-known appears in root listing', async () => {
170
+ const res = await supertest(server).get('/');
171
+ expect(res.text).toContain('.well-known');
172
+ });
173
+
174
+ test('GET /.env still returns 404 (not whitelisted)', async () => {
175
+ const res = await supertest(server).get('/.env');
176
+ expect(res.status).toBe(404);
177
+ });
178
+
179
+ test('whitelist with RegExp: /^\\.public/ matches .public-assets', async () => {
180
+ // This tests the regex matching logic; .public-assets doesn't exist so we
181
+ // expect 404 from "not found", not from "hidden" — indirectly confirms regex is not blocking
182
+ const server2 = createApp({
183
+ dotFiles: { default: 'hidden', whitelist: [/^\.public/] }
184
+ });
185
+ // .env is not matched by /^\.public/ so it stays hidden
186
+ const res = await supertest(server2).get('/.env');
187
+ expect(res.status).toBe(404);
188
+ server2.close();
189
+ });
190
+ });
191
+
192
+ // ─── dotFiles blacklist ───────────────────────────────────────────────────────
193
+
194
+ describe('dotFiles — blacklist', () => {
195
+ let server;
196
+ beforeAll(() => {
197
+ server = createApp({
198
+ dotFiles: { default: 'visible', blacklist: ['.env'] }
199
+ });
200
+ });
201
+ afterAll(() => server.close());
202
+
203
+ test('GET /.env returns 404 (blacklisted even with default: visible)', async () => {
204
+ const res = await supertest(server).get('/.env');
205
+ expect(res.status).toBe(404);
206
+ });
207
+
208
+ test('GET /.gitignore returns 200 (visible, not blacklisted)', async () => {
209
+ const res = await supertest(server).get('/.gitignore');
210
+ expect(res.status).toBe(200);
211
+ });
212
+
213
+ test('.env not in listing when blacklisted', async () => {
214
+ const res = await supertest(server).get('/');
215
+ expect(res.text).not.toContain('.env');
216
+ });
217
+ });
218
+
219
+ // ─── blacklist beats whitelist ────────────────────────────────────────────────
220
+
221
+ describe('dotFiles — blacklist beats whitelist', () => {
222
+ let server;
223
+ beforeAll(() => {
224
+ server = createApp({
225
+ dotFiles: { default: 'visible', whitelist: ['.env'], blacklist: ['.env'] }
226
+ });
227
+ });
228
+ afterAll(() => server.close());
229
+
230
+ test('GET /.env returns 404 (blacklist wins over whitelist)', async () => {
231
+ const res = await supertest(server).get('/.env');
232
+ expect(res.status).toBe(404);
233
+ });
234
+ });
235
+
236
+ // ─── dotDirs default: 'visible' (system default) ─────────────────────────────
237
+
238
+ describe('dotDirs — default visible (system default)', () => {
239
+ let server;
240
+ beforeAll(() => { server = createApp(undefined); });
241
+ afterAll(() => server.close());
242
+
243
+ test('.dot-dir appears in root listing by default', async () => {
244
+ const res = await supertest(server).get('/');
245
+ expect(res.text).toContain('.dot-dir');
246
+ });
247
+
248
+ test('GET /.dot-dir/ returns 200 (directory listing) by default', async () => {
249
+ const res = await supertest(server).get('/.dot-dir/');
250
+ expect(res.status).toBe(200);
251
+ });
252
+
253
+ test('GET /.dot-dir/inside.txt returns 200 by default', async () => {
254
+ const res = await supertest(server).get('/.dot-dir/inside.txt');
255
+ expect(res.status).toBe(200);
256
+ });
257
+ });
258
+
259
+ // ─── dotDirs blacklist ────────────────────────────────────────────────────────
260
+
261
+ describe('dotDirs — blacklist', () => {
262
+ let server;
263
+ beforeAll(() => {
264
+ server = createApp({ dotDirs: { blacklist: ['.dot-dir'] } });
265
+ });
266
+ afterAll(() => server.close());
267
+
268
+ test('GET /.dot-dir/ returns 404 (blacklisted dir)', async () => {
269
+ const res = await supertest(server).get('/.dot-dir/');
270
+ expect(res.status).toBe(404);
271
+ });
272
+
273
+ test('GET /.dot-dir/inside.txt returns 404 (inside blocked dir)', async () => {
274
+ const res = await supertest(server).get('/.dot-dir/inside.txt');
275
+ expect(res.status).toBe(404);
276
+ });
277
+
278
+ test('.dot-dir not in root listing when blacklisted', async () => {
279
+ const res = await supertest(server).get('/');
280
+ expect(res.text).not.toContain('.dot-dir');
281
+ });
282
+ });
283
+
284
+ // ─── dotDirs blacklist with RegExp ────────────────────────────────────────────
285
+
286
+ describe('dotDirs — blacklist with RegExp', () => {
287
+ let server;
288
+ beforeAll(() => {
289
+ server = createApp({ dotDirs: { blacklist: [/^\.dot/] } });
290
+ });
291
+ afterAll(() => server.close());
292
+
293
+ test('GET /.dot-dir/ returns 404 (RegExp blacklist match)', async () => {
294
+ const res = await supertest(server).get('/.dot-dir/');
295
+ expect(res.status).toBe(404);
296
+ });
297
+
298
+ test('.well-known accessible (does not match /^\\.dot/)', async () => {
299
+ const res = await supertest(server).get('/.well-known/');
300
+ expect(res.status).toBe(200);
301
+ });
302
+ });
303
+
304
+ // ─── alwaysHide ───────────────────────────────────────────────────────────────
305
+
306
+ describe('alwaysHide — glob and regex patterns', () => {
307
+ let server;
308
+ beforeAll(() => {
309
+ server = createApp({
310
+ dotFiles: { default: 'visible' }, // dot-files visible so we can test alwaysHide separately
311
+ alwaysHide: ['*.secret', /\.key$/]
312
+ });
313
+ });
314
+ afterAll(() => server.close());
315
+
316
+ test('GET /file.secret returns 404 (glob *.secret)', async () => {
317
+ const res = await supertest(server).get('/file.secret');
318
+ expect(res.status).toBe(404);
319
+ });
320
+
321
+ test('GET /data.key returns 404 (regex /\\.key$/)', async () => {
322
+ const res = await supertest(server).get('/data.key');
323
+ expect(res.status).toBe(404);
324
+ });
325
+
326
+ test('file.secret not in directory listing', async () => {
327
+ const res = await supertest(server).get('/');
328
+ expect(res.text).not.toContain('file.secret');
329
+ });
330
+
331
+ test('data.key not in directory listing', async () => {
332
+ const res = await supertest(server).get('/');
333
+ expect(res.text).not.toContain('data.key');
334
+ });
335
+
336
+ test('regular files remain accessible', async () => {
337
+ const res = await supertest(server).get('/normal.txt');
338
+ expect(res.status).toBe(200);
339
+ });
340
+ });
341
+
342
+ // ─── alwaysHide — path-anchored patterns ─────────────────────────────────────
343
+
344
+ describe('alwaysHide — path-anchored glob (config/secrets/**)', () => {
345
+ let server;
346
+ beforeAll(() => {
347
+ server = createApp({ alwaysHide: ['config/secrets/**'] });
348
+ });
349
+ afterAll(() => server.close());
350
+
351
+ test('GET /config/secrets/password.txt returns 404', async () => {
352
+ const res = await supertest(server).get('/config/secrets/password.txt');
353
+ expect(res.status).toBe(404);
354
+ });
355
+
356
+ test('password.txt not in /config/secrets/ listing', async () => {
357
+ const res = await supertest(server).get('/config/secrets/');
358
+ expect(res.status).toBe(200);
359
+ expect(res.text).not.toContain('password.txt');
360
+ });
361
+
362
+ test('GET /normal.txt remains accessible (not under config/secrets/)', async () => {
363
+ const res = await supertest(server).get('/normal.txt');
364
+ expect(res.status).toBe(200);
365
+ });
366
+ });
367
+
368
+ // ─── alwaysHide secondary to whitelist ───────────────────────────────────────
369
+
370
+ describe('alwaysHide — secondary to dotFiles whitelist', () => {
371
+ let server;
372
+ beforeAll(() => {
373
+ server = createApp({
374
+ dotFiles: { default: 'hidden', whitelist: ['.env'] },
375
+ alwaysHide: ['.env'] // both alwaysHide and whitelist target .env
376
+ });
377
+ });
378
+ afterAll(() => server.close());
379
+
380
+ test('GET /.env returns 200 (whitelist wins over alwaysHide)', async () => {
381
+ const res = await supertest(server).get('/.env');
382
+ expect(res.status).toBe(200);
383
+ });
384
+ });
385
+
386
+ // ─── deep tree: dot-files hidden at any depth ─────────────────────────────────
387
+
388
+ describe('hidden entries at any depth in directory tree (opt-in dotFiles.default=hidden)', () => {
389
+ let server;
390
+ beforeAll(() => { server = createApp({ dotFiles: { default: 'hidden' } }); });
391
+ afterAll(() => server.close());
392
+
393
+ test('GET /subdir/.env returns 404 (dot-file hidden at any depth)', async () => {
394
+ const res = await supertest(server).get('/subdir/.env');
395
+ expect(res.status).toBe(404);
396
+ });
397
+
398
+ test('/subdir/.env not in /subdir/ listing', async () => {
399
+ const res = await supertest(server).get('/subdir/');
400
+ expect(res.text).not.toContain('.env');
401
+ });
402
+
403
+ test('GET /subdir/regular.txt returns 200 (regular file accessible)', async () => {
404
+ const res = await supertest(server).get('/subdir/regular.txt');
405
+ expect(res.status).toBe(200);
406
+ });
407
+ });