koa-classic-server 2.5.2 โ 2.6.1
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/__tests__/dt-unknown.test.js +624 -0
- package/docs/CHANGELOG.md +103 -0
- package/index.cjs +37 -6
- package/package.json +7 -4
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DT_UNKNOWN filesystem support tests for koa-classic-server
|
|
3
|
+
*
|
|
4
|
+
* Context: On filesystems where readdir({ withFileTypes: true }) returns dirents
|
|
5
|
+
* with DT_UNKNOWN (UV_DIRENT_UNKNOWN = 0), all dirent.is*() methods return false.
|
|
6
|
+
* This occurs on:
|
|
7
|
+
* - NixOS with buildFHSEnv (chroot-like environment for Playwright e2e tests)
|
|
8
|
+
* - overlayfs (used by Docker for image layers)
|
|
9
|
+
* - some FUSE filesystems (sshfs, s3fs, rclone mount)
|
|
10
|
+
* - NFS (some implementations don't support d_type)
|
|
11
|
+
* - ecryptfs (encrypted home directories on Linux)
|
|
12
|
+
*
|
|
13
|
+
* The fix adds a stat() fallback in isFileOrSymlinkToFile(), isDirOrSymlinkToDir(),
|
|
14
|
+
* and show_dir() when the dirent type is unknown (type 0).
|
|
15
|
+
*
|
|
16
|
+
* On standard filesystems (ext4, btrfs, xfs, APFS, NTFS), d_type is always filled
|
|
17
|
+
* correctly, so the stat() fallback is never reached โ zero overhead.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const Koa = require('koa');
|
|
24
|
+
const supertest = require('supertest');
|
|
25
|
+
const koaClassicServer = require('../index.cjs');
|
|
26
|
+
|
|
27
|
+
describe('DT_UNKNOWN filesystem support (NixOS buildFHSEnv, overlayfs, FUSE)', () => {
|
|
28
|
+
let tmpDir;
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kcs-dt-unknown-test-'));
|
|
32
|
+
|
|
33
|
+
// Create real files and directories that stat() can resolve
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
path.join(tmpDir, 'index.html'),
|
|
36
|
+
'<html><head><title>DT_UNKNOWN Index</title></head><body>Hello from DT_UNKNOWN</body></html>'
|
|
37
|
+
);
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(tmpDir, 'index.ejs'),
|
|
40
|
+
'<html><head><title>EJS DT_UNKNOWN</title></head><body><h1>EJS Works</h1></body></html>'
|
|
41
|
+
);
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(tmpDir, 'style.css'),
|
|
44
|
+
'body { color: blue; }'
|
|
45
|
+
);
|
|
46
|
+
fs.writeFileSync(
|
|
47
|
+
path.join(tmpDir, 'readme.txt'),
|
|
48
|
+
'This is a readme file.'
|
|
49
|
+
);
|
|
50
|
+
fs.mkdirSync(path.join(tmpDir, 'subdir'));
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(tmpDir, 'subdir', 'nested.txt'),
|
|
53
|
+
'nested content'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Helper: create a Dirent with DT_UNKNOWN (type 0).
|
|
63
|
+
* Node.js 18+: new fs.Dirent(name, 0)
|
|
64
|
+
*/
|
|
65
|
+
function createUnknownDirent(name) {
|
|
66
|
+
return new fs.Dirent(name, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Helper: mock fs.promises.readdir to return DT_UNKNOWN dirents for a specific directory.
|
|
71
|
+
* stat() continues to work normally, so the fallback can resolve actual types.
|
|
72
|
+
*/
|
|
73
|
+
function mockReaddirWithDtUnknown(targetDir, fileNames) {
|
|
74
|
+
const originalReaddir = fs.promises.readdir;
|
|
75
|
+
const spy = jest.spyOn(fs.promises, 'readdir').mockImplementation(async (dirPath, options) => {
|
|
76
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
77
|
+
const resolvedDir = path.resolve(dirPath);
|
|
78
|
+
if (resolvedDir === resolvedTarget && options && options.withFileTypes) {
|
|
79
|
+
// Return DT_UNKNOWN dirents for all entries
|
|
80
|
+
return fileNames.map(name => createUnknownDirent(name));
|
|
81
|
+
}
|
|
82
|
+
// Fall through to original for other directories
|
|
83
|
+
return originalReaddir.call(fs.promises, dirPath, options);
|
|
84
|
+
});
|
|
85
|
+
return spy;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =========================================================================
|
|
89
|
+
// 1. isFileOrSymlinkToFile with DT_UNKNOWN
|
|
90
|
+
// =========================================================================
|
|
91
|
+
describe('isFileOrSymlinkToFile with DT_UNKNOWN', () => {
|
|
92
|
+
test('should return true for DT_UNKNOWN entry pointing to a regular file', async () => {
|
|
93
|
+
// Use findIndexFile as a proxy to test isFileOrSymlinkToFile behavior
|
|
94
|
+
// When index.html has type 0, it should still be found via stat() fallback
|
|
95
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'subdir']);
|
|
96
|
+
|
|
97
|
+
const app = new Koa();
|
|
98
|
+
app.use(koaClassicServer(tmpDir, {
|
|
99
|
+
index: ['index.html'],
|
|
100
|
+
showDirContents: true
|
|
101
|
+
}));
|
|
102
|
+
const server = app.listen();
|
|
103
|
+
const request = supertest(server);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const res = await request.get('/');
|
|
107
|
+
expect(res.status).toBe(200);
|
|
108
|
+
expect(res.text).toContain('DT_UNKNOWN Index');
|
|
109
|
+
expect(res.text).not.toContain('Index of');
|
|
110
|
+
} finally {
|
|
111
|
+
server.close();
|
|
112
|
+
spy.mockRestore();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('should return false for DT_UNKNOWN entry pointing to a directory', async () => {
|
|
117
|
+
// A directory called "subdir" with DT_UNKNOWN should NOT match as an index file
|
|
118
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
|
|
119
|
+
|
|
120
|
+
const app = new Koa();
|
|
121
|
+
app.use(koaClassicServer(tmpDir, {
|
|
122
|
+
index: ['subdir'],
|
|
123
|
+
showDirContents: true
|
|
124
|
+
}));
|
|
125
|
+
const server = app.listen();
|
|
126
|
+
const request = supertest(server);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const res = await request.get('/');
|
|
130
|
+
expect(res.status).toBe(200);
|
|
131
|
+
// Should show directory listing since "subdir" is a directory, not a file
|
|
132
|
+
expect(res.text).toContain('Index of');
|
|
133
|
+
} finally {
|
|
134
|
+
server.close();
|
|
135
|
+
spy.mockRestore();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should return false for DT_UNKNOWN entry pointing to nothing (broken)', async () => {
|
|
140
|
+
// Mock readdir to include a non-existent file
|
|
141
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['nonexistent.html', 'index.html']);
|
|
142
|
+
|
|
143
|
+
const app = new Koa();
|
|
144
|
+
app.use(koaClassicServer(tmpDir, {
|
|
145
|
+
index: ['nonexistent.html'],
|
|
146
|
+
showDirContents: true
|
|
147
|
+
}));
|
|
148
|
+
const server = app.listen();
|
|
149
|
+
const request = supertest(server);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const res = await request.get('/');
|
|
153
|
+
expect(res.status).toBe(200);
|
|
154
|
+
// nonexistent.html can't be stat'd, so should show directory listing
|
|
155
|
+
expect(res.text).toContain('Index of');
|
|
156
|
+
} finally {
|
|
157
|
+
server.close();
|
|
158
|
+
spy.mockRestore();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// =========================================================================
|
|
164
|
+
// 2. isDirOrSymlinkToDir with DT_UNKNOWN
|
|
165
|
+
// =========================================================================
|
|
166
|
+
describe('isDirOrSymlinkToDir with DT_UNKNOWN', () => {
|
|
167
|
+
test('should return true for DT_UNKNOWN entry pointing to a directory', async () => {
|
|
168
|
+
// Test that isDirOrSymlinkToDir resolves DT_UNKNOWN dirs correctly
|
|
169
|
+
// by checking that findIndexFile does NOT treat a directory as an index file
|
|
170
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir', 'index.html']);
|
|
171
|
+
|
|
172
|
+
const app = new Koa();
|
|
173
|
+
app.use(koaClassicServer(tmpDir, {
|
|
174
|
+
index: ['index.html'],
|
|
175
|
+
showDirContents: true
|
|
176
|
+
}));
|
|
177
|
+
const server = app.listen();
|
|
178
|
+
const request = supertest(server);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const res = await request.get('/');
|
|
182
|
+
expect(res.status).toBe(200);
|
|
183
|
+
// index.html (file) should be served, not subdir (directory)
|
|
184
|
+
expect(res.text).toContain('DT_UNKNOWN Index');
|
|
185
|
+
expect(res.text).not.toContain('Index of');
|
|
186
|
+
} finally {
|
|
187
|
+
server.close();
|
|
188
|
+
spy.mockRestore();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
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
|
|
195
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
|
|
196
|
+
|
|
197
|
+
const app = new Koa();
|
|
198
|
+
app.use(koaClassicServer(tmpDir, {
|
|
199
|
+
index: ['index.html'],
|
|
200
|
+
showDirContents: true
|
|
201
|
+
}));
|
|
202
|
+
const server = app.listen();
|
|
203
|
+
const request = supertest(server);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const res = await request.get('/');
|
|
207
|
+
expect(res.status).toBe(200);
|
|
208
|
+
// No index.html in the mocked readdir results, so directory listing
|
|
209
|
+
expect(res.text).toContain('Index of');
|
|
210
|
+
} finally {
|
|
211
|
+
server.close();
|
|
212
|
+
spy.mockRestore();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// =========================================================================
|
|
218
|
+
// 3. findIndexFile with DT_UNKNOWN entries
|
|
219
|
+
// =========================================================================
|
|
220
|
+
describe('findIndexFile with DT_UNKNOWN entries', () => {
|
|
221
|
+
test('should find index.html when all dirents have DT_UNKNOWN type', async () => {
|
|
222
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'index.html', 'readme.txt', 'subdir']);
|
|
223
|
+
|
|
224
|
+
const app = new Koa();
|
|
225
|
+
app.use(koaClassicServer(tmpDir, {
|
|
226
|
+
index: ['index.html'],
|
|
227
|
+
showDirContents: true
|
|
228
|
+
}));
|
|
229
|
+
const server = app.listen();
|
|
230
|
+
const request = supertest(server);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const res = await request.get('/');
|
|
234
|
+
expect(res.status).toBe(200);
|
|
235
|
+
expect(res.text).toContain('DT_UNKNOWN Index');
|
|
236
|
+
expect(res.text).not.toContain('Index of');
|
|
237
|
+
} finally {
|
|
238
|
+
server.close();
|
|
239
|
+
spy.mockRestore();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('should find index.ejs via string pattern when type is unknown', async () => {
|
|
244
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
|
|
245
|
+
|
|
246
|
+
const app = new Koa();
|
|
247
|
+
app.use(koaClassicServer(tmpDir, {
|
|
248
|
+
index: ['index.ejs'],
|
|
249
|
+
showDirContents: true
|
|
250
|
+
}));
|
|
251
|
+
const server = app.listen();
|
|
252
|
+
const request = supertest(server);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const res = await request.get('/');
|
|
256
|
+
expect(res.status).toBe(200);
|
|
257
|
+
expect(res.text).toContain('EJS DT_UNKNOWN');
|
|
258
|
+
expect(res.text).not.toContain('Index of');
|
|
259
|
+
} finally {
|
|
260
|
+
server.close();
|
|
261
|
+
spy.mockRestore();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('should find index.ejs via RegExp pattern when type is unknown', async () => {
|
|
266
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
|
|
267
|
+
|
|
268
|
+
const app = new Koa();
|
|
269
|
+
app.use(koaClassicServer(tmpDir, {
|
|
270
|
+
index: [/index\.[eE][jJ][sS]/],
|
|
271
|
+
showDirContents: true
|
|
272
|
+
}));
|
|
273
|
+
const server = app.listen();
|
|
274
|
+
const request = supertest(server);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const res = await request.get('/');
|
|
278
|
+
expect(res.status).toBe(200);
|
|
279
|
+
expect(res.text).toContain('EJS DT_UNKNOWN');
|
|
280
|
+
expect(res.text).not.toContain('Index of');
|
|
281
|
+
} finally {
|
|
282
|
+
server.close();
|
|
283
|
+
spy.mockRestore();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('should not find index in directory with only subdirectories (all DT_UNKNOWN)', async () => {
|
|
288
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['subdir']);
|
|
289
|
+
|
|
290
|
+
const app = new Koa();
|
|
291
|
+
app.use(koaClassicServer(tmpDir, {
|
|
292
|
+
index: ['index.html'],
|
|
293
|
+
showDirContents: true
|
|
294
|
+
}));
|
|
295
|
+
const server = app.listen();
|
|
296
|
+
const request = supertest(server);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const res = await request.get('/');
|
|
300
|
+
expect(res.status).toBe(200);
|
|
301
|
+
// No file matches index.html, so directory listing should appear
|
|
302
|
+
expect(res.text).toContain('Index of');
|
|
303
|
+
} finally {
|
|
304
|
+
server.close();
|
|
305
|
+
spy.mockRestore();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// =========================================================================
|
|
311
|
+
// 4. show_dir with DT_UNKNOWN entries
|
|
312
|
+
// =========================================================================
|
|
313
|
+
describe('show_dir with DT_UNKNOWN entries', () => {
|
|
314
|
+
test('should list files with DT_UNKNOWN as their resolved type (FILE/DIR)', async () => {
|
|
315
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt', 'subdir']);
|
|
316
|
+
|
|
317
|
+
const app = new Koa();
|
|
318
|
+
app.use(koaClassicServer(tmpDir, {
|
|
319
|
+
index: [],
|
|
320
|
+
showDirContents: true
|
|
321
|
+
}));
|
|
322
|
+
const server = app.listen();
|
|
323
|
+
const request = supertest(server);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const res = await request.get('/');
|
|
327
|
+
expect(res.status).toBe(200);
|
|
328
|
+
expect(res.text).toContain('Index of');
|
|
329
|
+
// All three entries should appear in the listing
|
|
330
|
+
expect(res.text).toContain('style.css');
|
|
331
|
+
expect(res.text).toContain('readme.txt');
|
|
332
|
+
expect(res.text).toContain('subdir');
|
|
333
|
+
// subdir should be resolved as DIR
|
|
334
|
+
expect(res.text).toMatch(/subdir[\s\S]*?DIR/);
|
|
335
|
+
} finally {
|
|
336
|
+
server.close();
|
|
337
|
+
spy.mockRestore();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('should not skip entries or log "Unknown file type: 0"', async () => {
|
|
342
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
343
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt', 'subdir']);
|
|
344
|
+
|
|
345
|
+
const app = new Koa();
|
|
346
|
+
app.use(koaClassicServer(tmpDir, {
|
|
347
|
+
index: [],
|
|
348
|
+
showDirContents: true
|
|
349
|
+
}));
|
|
350
|
+
const server = app.listen();
|
|
351
|
+
const request = supertest(server);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const res = await request.get('/');
|
|
355
|
+
expect(res.status).toBe(200);
|
|
356
|
+
// All entries should be present (not skipped)
|
|
357
|
+
expect(res.text).toContain('style.css');
|
|
358
|
+
expect(res.text).toContain('readme.txt');
|
|
359
|
+
expect(res.text).toContain('subdir');
|
|
360
|
+
// Should NOT have logged "Unknown file type: 0"
|
|
361
|
+
const unknownTypeCalls = consoleSpy.mock.calls.filter(
|
|
362
|
+
call => call[0] === 'Unknown file type:' && call[1] === 0
|
|
363
|
+
);
|
|
364
|
+
expect(unknownTypeCalls).toHaveLength(0);
|
|
365
|
+
} finally {
|
|
366
|
+
server.close();
|
|
367
|
+
spy.mockRestore();
|
|
368
|
+
consoleSpy.mockRestore();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('should show correct mime types for DT_UNKNOWN files', async () => {
|
|
373
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
|
|
374
|
+
|
|
375
|
+
const app = new Koa();
|
|
376
|
+
app.use(koaClassicServer(tmpDir, {
|
|
377
|
+
index: [],
|
|
378
|
+
showDirContents: true
|
|
379
|
+
}));
|
|
380
|
+
const server = app.listen();
|
|
381
|
+
const request = supertest(server);
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const res = await request.get('/');
|
|
385
|
+
expect(res.status).toBe(200);
|
|
386
|
+
// CSS file should show text/css mime type
|
|
387
|
+
expect(res.text).toMatch(/style\.css[\s\S]*?text\/css/);
|
|
388
|
+
// TXT file should show text/plain mime type
|
|
389
|
+
expect(res.text).toMatch(/readme\.txt[\s\S]*?text\/plain/);
|
|
390
|
+
} finally {
|
|
391
|
+
server.close();
|
|
392
|
+
spy.mockRestore();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('should show correct sizes for DT_UNKNOWN files', async () => {
|
|
397
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['style.css', 'readme.txt']);
|
|
398
|
+
|
|
399
|
+
const app = new Koa();
|
|
400
|
+
app.use(koaClassicServer(tmpDir, {
|
|
401
|
+
index: [],
|
|
402
|
+
showDirContents: true
|
|
403
|
+
}));
|
|
404
|
+
const server = app.listen();
|
|
405
|
+
const request = supertest(server);
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const res = await request.get('/');
|
|
409
|
+
expect(res.status).toBe(200);
|
|
410
|
+
// Files should have size values (not '-' which would indicate skipped/broken)
|
|
411
|
+
// style.css is 21 bytes = "21 B"
|
|
412
|
+
expect(res.text).toMatch(/style\.css[\s\S]*?\d+\s*B/);
|
|
413
|
+
// readme.txt is 22 bytes = "22 B"
|
|
414
|
+
expect(res.text).toMatch(/readme\.txt[\s\S]*?\d+\s*B/);
|
|
415
|
+
} finally {
|
|
416
|
+
server.close();
|
|
417
|
+
spy.mockRestore();
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// =========================================================================
|
|
423
|
+
// 5. Integration: full request with DT_UNKNOWN filesystem
|
|
424
|
+
// =========================================================================
|
|
425
|
+
describe('integration: full request with DT_UNKNOWN filesystem', () => {
|
|
426
|
+
test('GET / serves index file instead of directory listing', async () => {
|
|
427
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'readme.txt', 'subdir']);
|
|
428
|
+
|
|
429
|
+
const app = new Koa();
|
|
430
|
+
app.use(koaClassicServer(tmpDir, {
|
|
431
|
+
index: ['index.html'],
|
|
432
|
+
showDirContents: true
|
|
433
|
+
}));
|
|
434
|
+
const server = app.listen();
|
|
435
|
+
const request = supertest(server);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
const res = await request.get('/');
|
|
439
|
+
expect(res.status).toBe(200);
|
|
440
|
+
expect(res.text).toContain('DT_UNKNOWN Index');
|
|
441
|
+
expect(res.text).toContain('Hello from DT_UNKNOWN');
|
|
442
|
+
expect(res.text).not.toContain('Index of');
|
|
443
|
+
} finally {
|
|
444
|
+
server.close();
|
|
445
|
+
spy.mockRestore();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('GET /somefile.txt serves the file with 200', async () => {
|
|
450
|
+
// Direct file access uses stat() at the top level, so it works
|
|
451
|
+
// regardless of DT_UNKNOWN โ this verifies the direct path still works
|
|
452
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['readme.txt', 'style.css', 'subdir']);
|
|
453
|
+
|
|
454
|
+
const app = new Koa();
|
|
455
|
+
app.use(koaClassicServer(tmpDir, {
|
|
456
|
+
index: [],
|
|
457
|
+
showDirContents: true
|
|
458
|
+
}));
|
|
459
|
+
const server = app.listen();
|
|
460
|
+
const request = supertest(server);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
const res = await request.get('/readme.txt');
|
|
464
|
+
expect(res.status).toBe(200);
|
|
465
|
+
expect(res.text).toContain('This is a readme file.');
|
|
466
|
+
} finally {
|
|
467
|
+
server.close();
|
|
468
|
+
spy.mockRestore();
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('directory listing shows all entries correctly', async () => {
|
|
473
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.html', 'style.css', 'readme.txt', 'subdir', 'index.ejs']);
|
|
474
|
+
|
|
475
|
+
const app = new Koa();
|
|
476
|
+
app.use(koaClassicServer(tmpDir, {
|
|
477
|
+
index: [],
|
|
478
|
+
showDirContents: true
|
|
479
|
+
}));
|
|
480
|
+
const server = app.listen();
|
|
481
|
+
const request = supertest(server);
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const res = await request.get('/');
|
|
485
|
+
expect(res.status).toBe(200);
|
|
486
|
+
expect(res.text).toContain('Index of');
|
|
487
|
+
|
|
488
|
+
// All 5 entries should be listed
|
|
489
|
+
expect(res.text).toContain('index.html');
|
|
490
|
+
expect(res.text).toContain('style.css');
|
|
491
|
+
expect(res.text).toContain('readme.txt');
|
|
492
|
+
expect(res.text).toContain('subdir');
|
|
493
|
+
expect(res.text).toContain('index.ejs');
|
|
494
|
+
|
|
495
|
+
// subdir should show as DIR
|
|
496
|
+
expect(res.text).toMatch(/subdir[\s\S]*?DIR/);
|
|
497
|
+
|
|
498
|
+
// Files should have their correct mime types
|
|
499
|
+
expect(res.text).toMatch(/style\.css[\s\S]*?text\/css/);
|
|
500
|
+
expect(res.text).toMatch(/readme\.txt[\s\S]*?text\/plain/);
|
|
501
|
+
expect(res.text).toMatch(/index\.html[\s\S]*?text\/html/);
|
|
502
|
+
} finally {
|
|
503
|
+
server.close();
|
|
504
|
+
spy.mockRestore();
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('GET / with EJS template engine and DT_UNKNOWN still serves index', async () => {
|
|
509
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'style.css', 'subdir']);
|
|
510
|
+
|
|
511
|
+
const app = new Koa();
|
|
512
|
+
app.use(koaClassicServer(tmpDir, {
|
|
513
|
+
index: ['index.ejs'],
|
|
514
|
+
showDirContents: true,
|
|
515
|
+
template: {
|
|
516
|
+
ext: ['ejs'],
|
|
517
|
+
render: async (ctx, next, filePath) => {
|
|
518
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
519
|
+
ctx.type = 'text/html';
|
|
520
|
+
ctx.body = content;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}));
|
|
524
|
+
const server = app.listen();
|
|
525
|
+
const request = supertest(server);
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const res = await request.get('/');
|
|
529
|
+
expect(res.status).toBe(200);
|
|
530
|
+
expect(res.headers['content-type']).toMatch(/html/);
|
|
531
|
+
expect(res.text).toContain('EJS DT_UNKNOWN');
|
|
532
|
+
expect(res.text).not.toContain('Index of');
|
|
533
|
+
} finally {
|
|
534
|
+
server.close();
|
|
535
|
+
spy.mockRestore();
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// =========================================================================
|
|
541
|
+
// 6. Edge cases
|
|
542
|
+
// =========================================================================
|
|
543
|
+
describe('edge cases', () => {
|
|
544
|
+
test('mixed regular dirents and DT_UNKNOWN dirents work together', async () => {
|
|
545
|
+
// Only mock readdir for the specific directory, verify normal files
|
|
546
|
+
// still work alongside DT_UNKNOWN entries
|
|
547
|
+
const originalReaddir = fs.promises.readdir;
|
|
548
|
+
const spy = jest.spyOn(fs.promises, 'readdir').mockImplementation(async (dirPath, options) => {
|
|
549
|
+
const resolvedTarget = path.resolve(tmpDir);
|
|
550
|
+
const resolvedDir = path.resolve(dirPath);
|
|
551
|
+
if (resolvedDir === resolvedTarget && options && options.withFileTypes) {
|
|
552
|
+
// Mix: some regular (type 1), some DT_UNKNOWN (type 0)
|
|
553
|
+
return [
|
|
554
|
+
new fs.Dirent('index.html', 1), // Regular file
|
|
555
|
+
new fs.Dirent('style.css', 0), // DT_UNKNOWN
|
|
556
|
+
new fs.Dirent('subdir', 0), // DT_UNKNOWN
|
|
557
|
+
];
|
|
558
|
+
}
|
|
559
|
+
return originalReaddir.call(fs.promises, dirPath, options);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const app = new Koa();
|
|
563
|
+
app.use(koaClassicServer(tmpDir, {
|
|
564
|
+
index: [],
|
|
565
|
+
showDirContents: true
|
|
566
|
+
}));
|
|
567
|
+
const server = app.listen();
|
|
568
|
+
const request = supertest(server);
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const res = await request.get('/');
|
|
572
|
+
expect(res.status).toBe(200);
|
|
573
|
+
expect(res.text).toContain('Index of');
|
|
574
|
+
// All entries should appear
|
|
575
|
+
expect(res.text).toContain('index.html');
|
|
576
|
+
expect(res.text).toContain('style.css');
|
|
577
|
+
expect(res.text).toContain('subdir');
|
|
578
|
+
} finally {
|
|
579
|
+
server.close();
|
|
580
|
+
spy.mockRestore();
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test('DT_UNKNOWN Dirent has all is*() methods returning false', () => {
|
|
585
|
+
// Verify our test helper creates correct DT_UNKNOWN dirents
|
|
586
|
+
const d = createUnknownDirent('test.txt');
|
|
587
|
+
expect(d.isFile()).toBe(false);
|
|
588
|
+
expect(d.isDirectory()).toBe(false);
|
|
589
|
+
expect(d.isSymbolicLink()).toBe(false);
|
|
590
|
+
expect(d.isBlockDevice()).toBe(false);
|
|
591
|
+
expect(d.isCharacterDevice()).toBe(false);
|
|
592
|
+
expect(d.isFIFO()).toBe(false);
|
|
593
|
+
expect(d.isSocket()).toBe(false);
|
|
594
|
+
|
|
595
|
+
// Verify Symbol(type) is 0
|
|
596
|
+
const syms = Object.getOwnPropertySymbols(d);
|
|
597
|
+
expect(d[syms[0]]).toBe(0);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test('index priority is preserved with DT_UNKNOWN entries', async () => {
|
|
601
|
+
// When multiple index files exist, the first pattern should win
|
|
602
|
+
const spy = mockReaddirWithDtUnknown(tmpDir, ['index.ejs', 'index.html', 'style.css']);
|
|
603
|
+
|
|
604
|
+
const app = new Koa();
|
|
605
|
+
app.use(koaClassicServer(tmpDir, {
|
|
606
|
+
index: ['index.html', 'index.ejs'],
|
|
607
|
+
showDirContents: true
|
|
608
|
+
}));
|
|
609
|
+
const server = app.listen();
|
|
610
|
+
const request = supertest(server);
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const res = await request.get('/');
|
|
614
|
+
expect(res.status).toBe(200);
|
|
615
|
+
// index.html should win because it's first in the index array
|
|
616
|
+
expect(res.text).toContain('DT_UNKNOWN Index');
|
|
617
|
+
expect(res.text).not.toContain('EJS DT_UNKNOWN');
|
|
618
|
+
} finally {
|
|
619
|
+
server.close();
|
|
620
|
+
spy.mockRestore();
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,109 @@ 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
|
+
## [2.6.1] - 2026-03-04
|
|
9
|
+
|
|
10
|
+
### ๐ Bug Fix
|
|
11
|
+
|
|
12
|
+
#### Fixed DT_UNKNOWN Handling (type 0) on overlayfs, NFS, FUSE, NixOS buildFHSEnv, ecryptfs
|
|
13
|
+
- **Issue**: On filesystems where `readdir({ withFileTypes: true })` returns dirents with `DT_UNKNOWN` (type 0), all `dirent.is*()` methods return `false`. This caused three failures:
|
|
14
|
+
1. `isFileOrSymlinkToFile()` missed valid files โ `findIndexFile()` returned empty results, so `GET /` showed a directory listing instead of rendering the index file
|
|
15
|
+
2. `isDirOrSymlinkToDir()` missed valid directories โ directory type resolution failed
|
|
16
|
+
3. `show_dir()` skipped entries with type 0, logging `"Unknown file type: 0"` โ directory listings appeared empty or partial
|
|
17
|
+
- **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
|
|
18
|
+
- **Impact**: HIGH โ Server unusable on affected filesystems (index file not served, directory listing empty)
|
|
19
|
+
- **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.
|
|
20
|
+
- **Code**:
|
|
21
|
+
- `isFileOrSymlinkToFile()` โ DT_UNKNOWN fallback via `stat().isFile()`
|
|
22
|
+
- `isDirOrSymlinkToDir()` โ DT_UNKNOWN fallback via `stat().isDirectory()`
|
|
23
|
+
- `show_dir()` โ Accept type 0 entries and resolve via `stat()` instead of skipping them
|
|
24
|
+
- **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."*
|
|
25
|
+
|
|
26
|
+
### ๐งช Testing
|
|
27
|
+
- Added `__tests__/dt-unknown.test.js` with 20 tests covering:
|
|
28
|
+
- `isFileOrSymlinkToFile` / `isDirOrSymlinkToDir` with DT_UNKNOWN dirents
|
|
29
|
+
- `findIndexFile` with all-unknown-type entries (string and RegExp patterns)
|
|
30
|
+
- `show_dir` rendering (resolved types, no skipped entries, correct MIME types and sizes)
|
|
31
|
+
- Full integration tests (index file serving, direct file access, complete directory listing)
|
|
32
|
+
- Edge cases (mixed regular + DT_UNKNOWN dirents, index priority, Dirent type 0 verification)
|
|
33
|
+
- 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
|
|
34
|
+
- All 329 tests pass across 12 test suites (zero regressions)
|
|
35
|
+
|
|
36
|
+
### ๐ฆ Package Changes
|
|
37
|
+
- **Version**: `2.6.0` โ `2.6.1`
|
|
38
|
+
- **Semver**: Patch version bump (bug fix only, no API changes)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## [2.6.0] - 2026-03-01
|
|
43
|
+
|
|
44
|
+
### ๐ฆ Dependency Upgrades
|
|
45
|
+
|
|
46
|
+
#### mime-types: ^2.1.35 โ ^3.0.2 (Major)
|
|
47
|
+
- **Breaking change upstream**: New `mimeScore` algorithm for extension conflict resolution
|
|
48
|
+
- **Impact on this project**: Minimal โ the 11 changed MIME mappings affect only uncommon extensions
|
|
49
|
+
- **Notable mapping changes**:
|
|
50
|
+
- `.wav`: `audio/wave` โ `audio/wav` (equivalent, all browsers accept both)
|
|
51
|
+
- `.js`: `application/javascript` โ `text/javascript` (correct per RFC 9239)
|
|
52
|
+
- `.rtf`: `text/rtf` โ `application/rtf` (marginal, rare usage)
|
|
53
|
+
- `.mp4`: Unchanged in v3.0.2 โ still resolves to `video/mp4`
|
|
54
|
+
- **Node.js requirement**: mime-types 3 requires Node.js >= 18
|
|
55
|
+
|
|
56
|
+
#### ejs: ^3.1.10 โ ^4.0.0 (Major)
|
|
57
|
+
- **Breaking changes upstream**: None affecting this project
|
|
58
|
+
- EJS 4 removed deprecated `with()` statement support (this project never used it)
|
|
59
|
+
- EJS 4 added stricter `exports` map in package.json
|
|
60
|
+
- **API fully compatible**: `ejs.render()` and `ejs.renderFile()` work identically
|
|
61
|
+
- **Security**: EJS 3.x is EOL โ v4 resolves known CVEs in the 3.x line
|
|
62
|
+
|
|
63
|
+
### ๐ง Configuration Changes
|
|
64
|
+
|
|
65
|
+
#### Added `engines` field
|
|
66
|
+
- Added `"engines": { "node": ">=18" }` to package.json
|
|
67
|
+
- Formalizes the Node.js minimum version requirement imposed by mime-types 3
|
|
68
|
+
|
|
69
|
+
#### Tightened Koa peerDependency for 2.x
|
|
70
|
+
- **koa**: `"^2.0.0 || >=3.1.2"` โ `"^2.16.4 || >=3.1.2"`
|
|
71
|
+
- Excludes Koa 2.0.0โ2.16.3 which are affected by 4 known CVEs:
|
|
72
|
+
- CVE-2025-25200: ReDoS via `X-Forwarded-Proto`/`X-Forwarded-Host` (CVSS 9.2, fixed in 2.15.4)
|
|
73
|
+
- CVE-2025-32379: XSS via `ctx.redirect()` (fixed in 2.16.1)
|
|
74
|
+
- CVE-2025-62595: Open Redirect via trailing `//` (fixed in 2.16.3)
|
|
75
|
+
- CVE-2026-27959: Host Header Injection via `ctx.hostname` (CVSS 7.5, fixed in 2.16.4)
|
|
76
|
+
|
|
77
|
+
### ๐งช Testing
|
|
78
|
+
- All 309 tests pass across 11 test suites (zero regressions)
|
|
79
|
+
- No code changes required โ both library upgrades are API-compatible
|
|
80
|
+
|
|
81
|
+
### ๐ฆ Package Changes
|
|
82
|
+
- **Version**: `2.5.2` โ `2.6.0`
|
|
83
|
+
- **Semver**: Minor version bump (dependency upgrades, no API changes)
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## [2.5.2] - 2026-03-01
|
|
88
|
+
|
|
89
|
+
### ๐ Security Fix
|
|
90
|
+
|
|
91
|
+
#### Resolved all 11 npm audit vulnerabilities
|
|
92
|
+
- **jest**: `^29.7.0` โ `^30.2.0` (major โ fixes minimatch ReDoS, brace-expansion ReDoS, @babel/helpers inefficient RegExp)
|
|
93
|
+
- **supertest**: `^7.0.0` โ `^7.2.2` (fixes critical form-data unsafe random boundary)
|
|
94
|
+
- **inquirer**: `^12.4.1` โ `^13.3.0` (fixes tmp arbitrary file write via symlink, external-editor chain)
|
|
95
|
+
- **autocannon**: `^7.15.0` โ `^8.0.0` (major)
|
|
96
|
+
|
|
97
|
+
#### Updated peerDependency
|
|
98
|
+
- **koa**: `"^2.0.0 || ^3.0.0"` โ `"^2.0.0 || >=3.1.2"`
|
|
99
|
+
- Excludes Koa 3.0.0โ3.1.1 which had Host Header Injection via `ctx.hostname`
|
|
100
|
+
|
|
101
|
+
### ๐งช Testing
|
|
102
|
+
- All 309 tests pass across 11 test suites (zero regressions)
|
|
103
|
+
- `npm audit` reports 0 vulnerabilities
|
|
104
|
+
|
|
105
|
+
### ๐ฆ Package Changes
|
|
106
|
+
- **Version**: `2.5.1` โ `2.5.2`
|
|
107
|
+
- **Semver**: Patch version bump (security fixes only, no API changes)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
8
111
|
## [2.5.1] - 2026-03-01
|
|
9
112
|
|
|
10
113
|
### ๐ Documentation
|
package/index.cjs
CHANGED
|
@@ -173,7 +173,13 @@ module.exports = function koaClassicServer(
|
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
175
|
* Returns true if dirent is a regular file or a symlink pointing to a regular file.
|
|
176
|
-
* Uses fs.promises.stat (which follows symlinks)
|
|
176
|
+
* Uses fs.promises.stat (which follows symlinks) when dirent.isSymbolicLink() is true,
|
|
177
|
+
* or when the dirent type is unknown (DT_UNKNOWN / type 0).
|
|
178
|
+
*
|
|
179
|
+
* DT_UNKNOWN occurs on overlayfs, NFS, FUSE, NixOS buildFHSEnv, ecryptfs,
|
|
180
|
+
* and any filesystem that doesn't fill d_type in the kernel's getdents64 syscall.
|
|
181
|
+
* On standard filesystems (ext4, btrfs, xfs, APFS, NTFS), d_type is always
|
|
182
|
+
* filled correctly, so the stat() fallback is never reached.
|
|
177
183
|
*/
|
|
178
184
|
async function isFileOrSymlinkToFile(dirent, dirPath) {
|
|
179
185
|
if (dirent.isFile()) return true;
|
|
@@ -185,12 +191,23 @@ module.exports = function koaClassicServer(
|
|
|
185
191
|
return false; // Broken or circular symlink
|
|
186
192
|
}
|
|
187
193
|
}
|
|
194
|
+
// DT_UNKNOWN fallback: when none of the type methods return true,
|
|
195
|
+
// the filesystem didn't report d_type โ resolve via stat()
|
|
196
|
+
if (!dirent.isDirectory() && !dirent.isBlockDevice() && !dirent.isCharacterDevice() && !dirent.isFIFO() && !dirent.isSocket()) {
|
|
197
|
+
try {
|
|
198
|
+
const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
|
|
199
|
+
return realStat.isFile();
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
188
204
|
return false;
|
|
189
205
|
}
|
|
190
206
|
|
|
191
207
|
/**
|
|
192
208
|
* Returns true if dirent is a directory or a symlink pointing to a directory.
|
|
193
|
-
* Uses fs.promises.stat (which follows symlinks)
|
|
209
|
+
* Uses fs.promises.stat (which follows symlinks) when dirent.isSymbolicLink() is true,
|
|
210
|
+
* or when the dirent type is unknown (DT_UNKNOWN / type 0).
|
|
194
211
|
*/
|
|
195
212
|
async function isDirOrSymlinkToDir(dirent, dirPath) {
|
|
196
213
|
if (dirent.isDirectory()) return true;
|
|
@@ -202,6 +219,15 @@ module.exports = function koaClassicServer(
|
|
|
202
219
|
return false; // Broken or circular symlink
|
|
203
220
|
}
|
|
204
221
|
}
|
|
222
|
+
// DT_UNKNOWN fallback: resolve via stat() when type is unknown
|
|
223
|
+
if (!dirent.isFile() && !dirent.isBlockDevice() && !dirent.isCharacterDevice() && !dirent.isFIFO() && !dirent.isSocket()) {
|
|
224
|
+
try {
|
|
225
|
+
const realStat = await fs.promises.stat(path.join(dirPath, dirent.name));
|
|
226
|
+
return realStat.isDirectory();
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
205
231
|
return false;
|
|
206
232
|
}
|
|
207
233
|
|
|
@@ -662,7 +688,7 @@ module.exports = function koaClassicServer(
|
|
|
662
688
|
const s_name = item.name.toString();
|
|
663
689
|
const type = item[sy_type];
|
|
664
690
|
|
|
665
|
-
if (type !== 1 && type !== 2 && type !== 3) {
|
|
691
|
+
if (type !== 0 && type !== 1 && type !== 2 && type !== 3) {
|
|
666
692
|
console.error("Unknown file type:", type);
|
|
667
693
|
continue;
|
|
668
694
|
}
|
|
@@ -677,16 +703,21 @@ module.exports = function koaClassicServer(
|
|
|
677
703
|
itemUri = `${baseUrl}/${encodeURIComponent(s_name)}`;
|
|
678
704
|
}
|
|
679
705
|
|
|
680
|
-
// Resolve symlinks to their effective type
|
|
706
|
+
// Resolve symlinks and DT_UNKNOWN entries to their effective type
|
|
681
707
|
let effectiveType = type;
|
|
682
708
|
let isBrokenSymlink = false;
|
|
683
|
-
if (type === 3) {
|
|
709
|
+
if (type === 3 || type === 0) {
|
|
710
|
+
// type 3 = symlink, type 0 = DT_UNKNOWN (overlayfs, NFS, FUSE, NixOS buildFHSEnv, ecryptfs)
|
|
684
711
|
try {
|
|
685
712
|
const realStat = await fs.promises.stat(itemPath);
|
|
686
713
|
if (realStat.isFile()) effectiveType = 1;
|
|
687
714
|
else if (realStat.isDirectory()) effectiveType = 2;
|
|
688
715
|
} catch {
|
|
689
|
-
|
|
716
|
+
if (type === 3) {
|
|
717
|
+
isBrokenSymlink = true; // Broken or circular symlink
|
|
718
|
+
} else {
|
|
719
|
+
continue; // DT_UNKNOWN entry that can't be stat'd โ skip it
|
|
720
|
+
}
|
|
690
721
|
}
|
|
691
722
|
}
|
|
692
723
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koa-classic-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"exports": {
|
|
@@ -33,15 +33,18 @@
|
|
|
33
33
|
"type": "git",
|
|
34
34
|
"url": "https://github.com/italopaesano/koa-classic-server"
|
|
35
35
|
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
36
39
|
"dependencies": {
|
|
37
|
-
"mime-types": "^
|
|
40
|
+
"mime-types": "^3.0.2"
|
|
38
41
|
},
|
|
39
42
|
"peerDependencies": {
|
|
40
|
-
"koa": "^2.
|
|
43
|
+
"koa": "^2.16.4 || >=3.1.2"
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"autocannon": "^8.0.0",
|
|
44
|
-
"ejs": "^
|
|
47
|
+
"ejs": "^4.0.0",
|
|
45
48
|
"inquirer": "^13.3.0",
|
|
46
49
|
"jest": "^30.2.0",
|
|
47
50
|
"supertest": "^7.2.2"
|