te.js 2.0.3 → 2.1.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.
Files changed (68) hide show
  1. package/README.md +197 -187
  2. package/auto-docs/analysis/handler-analyzer.js +58 -58
  3. package/auto-docs/analysis/source-resolver.js +101 -101
  4. package/auto-docs/constants.js +37 -37
  5. package/auto-docs/docs-llm/index.js +7 -0
  6. package/auto-docs/{llm → docs-llm}/prompts.js +222 -222
  7. package/auto-docs/{llm → docs-llm}/provider.js +132 -187
  8. package/auto-docs/index.js +146 -146
  9. package/auto-docs/openapi/endpoint-processor.js +277 -277
  10. package/auto-docs/openapi/generator.js +107 -107
  11. package/auto-docs/openapi/level3.js +131 -131
  12. package/auto-docs/openapi/spec-builders.js +244 -244
  13. package/auto-docs/ui/docs-ui.js +186 -186
  14. package/auto-docs/utils/logger.js +17 -17
  15. package/auto-docs/utils/strip-usage.js +10 -10
  16. package/cli/docs-command.js +315 -315
  17. package/cli/fly-command.js +71 -71
  18. package/cli/index.js +56 -56
  19. package/database/index.js +165 -165
  20. package/database/mongodb.js +146 -146
  21. package/database/redis.js +201 -201
  22. package/docs/README.md +36 -36
  23. package/docs/ammo.md +362 -362
  24. package/docs/api-reference.md +490 -489
  25. package/docs/auto-docs.md +216 -215
  26. package/docs/cli.md +152 -152
  27. package/docs/configuration.md +275 -233
  28. package/docs/database.md +390 -391
  29. package/docs/error-handling.md +438 -417
  30. package/docs/file-uploads.md +333 -334
  31. package/docs/getting-started.md +214 -215
  32. package/docs/middleware.md +355 -356
  33. package/docs/rate-limiting.md +393 -394
  34. package/docs/routing.md +302 -302
  35. package/package.json +62 -62
  36. package/rate-limit/algorithms/fixed-window.js +141 -141
  37. package/rate-limit/algorithms/sliding-window.js +147 -147
  38. package/rate-limit/algorithms/token-bucket.js +115 -115
  39. package/rate-limit/base.js +165 -165
  40. package/rate-limit/index.js +147 -147
  41. package/rate-limit/storage/base.js +104 -104
  42. package/rate-limit/storage/memory.js +101 -101
  43. package/rate-limit/storage/redis.js +88 -88
  44. package/server/ammo/body-parser.js +220 -220
  45. package/server/ammo/dispatch-helper.js +103 -103
  46. package/server/ammo/enhancer.js +57 -57
  47. package/server/ammo.js +454 -356
  48. package/server/endpoint.js +97 -74
  49. package/server/error.js +9 -9
  50. package/server/errors/code-context.js +125 -0
  51. package/server/errors/llm-error-service.js +140 -0
  52. package/server/files/helper.js +33 -33
  53. package/server/files/uploader.js +143 -143
  54. package/server/handler.js +158 -113
  55. package/server/target.js +185 -175
  56. package/server/targets/middleware-validator.js +22 -22
  57. package/server/targets/path-validator.js +21 -21
  58. package/server/targets/registry.js +160 -160
  59. package/server/targets/shoot-validator.js +21 -21
  60. package/te.js +428 -363
  61. package/utils/auto-register.js +17 -17
  62. package/utils/configuration.js +64 -64
  63. package/utils/errors-llm-config.js +84 -0
  64. package/utils/request-logger.js +43 -43
  65. package/utils/status-codes.js +82 -82
  66. package/utils/tejas-entrypoint-html.js +18 -18
  67. package/auto-docs/llm/index.js +0 -6
  68. package/auto-docs/llm/parse.js +0 -88
@@ -1,334 +1,333 @@
1
- # File Uploads
2
-
3
- Tejas provides a built-in `TejFileUploader` class for handling file uploads with ease.
4
-
5
- ## Quick Start
6
-
7
- ```javascript
8
- import { Target, TejFileUploader } from 'te.js';
9
-
10
- const upload = new TejFileUploader({
11
- destination: 'uploads/',
12
- maxFileSize: 5 * 1024 * 1024 // 5MB
13
- });
14
-
15
- const target = new Target('/files');
16
-
17
- target.register('/upload', upload.file('avatar'), (ammo) => {
18
- ammo.fire({ file: ammo.payload.avatar });
19
- });
20
- ```
21
-
22
- ## Configuration
23
-
24
- ```javascript
25
- const upload = new TejFileUploader({
26
- destination: 'public/uploads', // Where to save files
27
- name: 'custom-name', // Optional: custom filename
28
- maxFileSize: 10 * 1024 * 1024 // Max file size in bytes (10MB)
29
- });
30
- ```
31
-
32
- ### Options
33
-
34
- | Option | Type | Description |
35
- |--------|------|-------------|
36
- | `destination` | string | Directory to save uploaded files |
37
- | `name` | string | Optional custom filename |
38
- | `maxFileSize` | number | Maximum file size in bytes |
39
-
40
- ## Single File Upload
41
-
42
- Use `upload.file()` for single file uploads:
43
-
44
- ```javascript
45
- // Expects a file field named 'avatar'
46
- target.register('/avatar', upload.file('avatar'), (ammo) => {
47
- const file = ammo.payload.avatar;
48
-
49
- ammo.fire({
50
- filename: file.filename,
51
- path: file.path.relative,
52
- mimetype: file.mimetype,
53
- size: file.size
54
- });
55
- });
56
- ```
57
-
58
- ### File Object Structure
59
-
60
- When a file is uploaded, `ammo.payload[fieldName]` contains:
61
-
62
- ```javascript
63
- {
64
- filename: 'photo.jpg', // Original filename
65
- extension: 'jpg', // File extension
66
- path: {
67
- absolute: '/var/www/uploads/photo.jpg', // Absolute path on disk
68
- relative: '\\uploads\\photo.jpg' // Relative to cwd
69
- },
70
- mimetype: 'image/jpeg', // MIME type
71
- size: { // From the filesize library
72
- value: 245, // Numeric value
73
- symbol: 'KB' // Unit (B, KB, MB, etc.)
74
- }
75
- }
76
- ```
77
-
78
- The `size` object is produced by the [filesize](https://www.npmjs.com/package/filesize) library. Use `${size.value} ${size.symbol}` for display (e.g. "245 KB").
79
-
80
- ## Multiple File Upload
81
-
82
- Use `upload.files()` for multiple files:
83
-
84
- ```javascript
85
- // Expects files in 'photos' and 'documents' fields
86
- target.register('/documents', upload.files('photos', 'documents'), (ammo) => {
87
- const { photos, documents } = ammo.payload;
88
-
89
- ammo.fire({
90
- photos: photos || [], // Array of file objects
91
- documents: documents || [] // Array of file objects
92
- });
93
- });
94
- ```
95
-
96
- ### Multiple Files Response
97
-
98
- Each field contains an array of file objects:
99
-
100
- ```javascript
101
- {
102
- photos: [
103
- { filename: 'photo1.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} },
104
- { filename: 'photo2.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} }
105
- ],
106
- documents: [
107
- { filename: 'doc.pdf', path: {...}, mimetype: 'application/pdf', size: {...} }
108
- ]
109
- }
110
- ```
111
-
112
- ## Mixed Fields (Files + Data)
113
-
114
- File uploads can include regular form fields:
115
-
116
- ```javascript
117
- target.register('/profile', upload.file('avatar'), (ammo) => {
118
- const { avatar, name, bio } = ammo.payload;
119
-
120
- ammo.fire({
121
- name, // Regular form field
122
- bio, // Regular form field
123
- avatar: avatar // File object
124
- });
125
- });
126
- ```
127
-
128
- ## File Size Limits
129
-
130
- When a file exceeds `maxFileSize`, a `413 Payload Too Large` error is thrown automatically. The error message includes the human-readable limit (e.g. "File size exceeds 2 MB"):
131
-
132
- ```javascript
133
- const upload = new TejFileUploader({
134
- destination: 'uploads/',
135
- maxFileSize: 2 * 1024 * 1024 // 2MB limit
136
- });
137
-
138
- target.register('/upload', upload.file('file'), (ammo) => {
139
- // If file > 2MB, this handler never runs
140
- // Client receives: 413 "File size exceeds 2 MB"
141
- ammo.fire({ success: true });
142
- });
143
- ```
144
-
145
- Note that the overall request body is also subject to the global `body.max_size` limit (default 10 MB). See [Configuration](./configuration.md).
146
-
147
- ## Client-Side Examples
148
-
149
- ### HTML Form
150
-
151
- ```html
152
- <form action="/files/upload" method="POST" enctype="multipart/form-data">
153
- <input type="file" name="avatar" />
154
- <input type="text" name="username" />
155
- <button type="submit">Upload</button>
156
- </form>
157
- ```
158
-
159
- ### JavaScript (Fetch)
160
-
161
- ```javascript
162
- const formData = new FormData();
163
- formData.append('avatar', fileInput.files[0]);
164
- formData.append('username', 'john');
165
-
166
- const response = await fetch('/files/upload', {
167
- method: 'POST',
168
- body: formData
169
- });
170
- ```
171
-
172
- ### JavaScript (Multiple Files)
173
-
174
- ```javascript
175
- const formData = new FormData();
176
-
177
- // Add multiple files to same field
178
- for (const file of fileInput.files) {
179
- formData.append('photos', file);
180
- }
181
-
182
- const response = await fetch('/files/documents', {
183
- method: 'POST',
184
- body: formData
185
- });
186
- ```
187
-
188
- ## Complete Example
189
-
190
- ```javascript
191
- import { Target, TejFileUploader, TejError } from 'te.js';
192
- import fs from 'fs';
193
- import path from 'path';
194
-
195
- const target = new Target('/api/files');
196
-
197
- // Configure uploader
198
- const imageUpload = new TejFileUploader({
199
- destination: 'public/images',
200
- maxFileSize: 5 * 1024 * 1024 // 5MB
201
- });
202
-
203
- const documentUpload = new TejFileUploader({
204
- destination: 'private/documents',
205
- maxFileSize: 20 * 1024 * 1024 // 20MB
206
- });
207
-
208
- // Upload profile image
209
- target.register('/profile-image', imageUpload.file('image'), (ammo) => {
210
- if (!ammo.POST) return ammo.notAllowed();
211
-
212
- const { image } = ammo.payload;
213
-
214
- if (!image) {
215
- throw new TejError(400, 'No image provided');
216
- }
217
-
218
- // Validate image type
219
- if (!image.mimetype.startsWith('image/')) {
220
- // Delete uploaded file
221
- fs.unlinkSync(image.path.absolute);
222
- throw new TejError(400, 'File must be an image');
223
- }
224
-
225
- ammo.fire({
226
- message: 'Profile image uploaded',
227
- url: `/images/${image.filename}`
228
- });
229
- });
230
-
231
- // Upload multiple documents
232
- target.register('/documents', documentUpload.files('files'), (ammo) => {
233
- if (!ammo.POST) return ammo.notAllowed();
234
-
235
- const { files } = ammo.payload;
236
-
237
- if (!files || files.length === 0) {
238
- throw new TejError(400, 'No files provided');
239
- }
240
-
241
- ammo.fire({
242
- message: `${files.length} files uploaded`,
243
- files: files.map(f => ({
244
- name: f.filename,
245
- size: `${f.size.value} ${f.size.symbol}`
246
- }))
247
- });
248
- });
249
-
250
- // Delete a file
251
- target.register('/delete/:filename', (ammo) => {
252
- if (!ammo.DELETE) return ammo.notAllowed();
253
-
254
- const { filename } = ammo.payload;
255
- const filepath = path.join('public/images', filename);
256
-
257
- if (!fs.existsSync(filepath)) {
258
- throw new TejError(404, 'File not found');
259
- }
260
-
261
- fs.unlinkSync(filepath);
262
- ammo.fire({ message: 'File deleted' });
263
- });
264
- ```
265
-
266
- ## Validation Middleware
267
-
268
- Create reusable validation middleware:
269
-
270
- ```javascript
271
- // middleware/validate-image.js
272
- import { TejError } from 'te.js';
273
- import fs from 'fs';
274
-
275
- const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
276
-
277
- export const validateImage = (fieldName) => (ammo, next) => {
278
- const file = ammo.payload[fieldName];
279
-
280
- if (!file) {
281
- throw new TejError(400, `${fieldName} is required`);
282
- }
283
-
284
- if (!allowedTypes.includes(file.mimetype)) {
285
- fs.unlinkSync(file.path.absolute);
286
- throw new TejError(400, 'Only JPEG, PNG, GIF, and WebP images are allowed');
287
- }
288
-
289
- next();
290
- };
291
-
292
- // Usage
293
- target.register('/avatar',
294
- upload.file('avatar'),
295
- validateImage('avatar'),
296
- (ammo) => {
297
- ammo.fire({ success: true });
298
- }
299
- );
300
- ```
301
-
302
- ## Serving Uploaded Files
303
-
304
- Tejas doesn't include a static file server, but you can serve files manually:
305
-
306
- ```javascript
307
- import fs from 'fs';
308
- import path from 'path';
309
- import mime from 'mime';
310
-
311
- target.register('/images/:filename', (ammo) => {
312
- const { filename } = ammo.payload;
313
- const filepath = path.join('public/images', filename);
314
-
315
- if (!fs.existsSync(filepath)) {
316
- return ammo.notFound();
317
- }
318
-
319
- const file = fs.readFileSync(filepath);
320
- const contentType = mime.getType(filepath) || 'application/octet-stream';
321
-
322
- ammo.fire(200, file, contentType);
323
- });
324
- ```
325
-
326
- ## Best Practices
327
-
328
- 1. **Validate file types** — Don't trust client-reported MIME types
329
- 2. **Set size limits** — Prevent disk exhaustion attacks
330
- 3. **Use unique filenames** — Avoid overwrites with UUID or timestamps
331
- 4. **Store outside web root** — For sensitive files, store in private directories
332
- 5. **Clean up on errors** — Delete uploaded files if validation fails
333
- 6. **Scan for malware** — For production systems, integrate virus scanning
334
-
1
+ # File Uploads
2
+
3
+ Tejas provides a built-in `TejFileUploader` class for handling file uploads with ease.
4
+
5
+ ## Quick Start
6
+
7
+ ```javascript
8
+ import { Target, TejFileUploader } from 'te.js';
9
+
10
+ const upload = new TejFileUploader({
11
+ destination: 'uploads/',
12
+ maxFileSize: 5 * 1024 * 1024 // 5MB
13
+ });
14
+
15
+ const target = new Target('/files');
16
+
17
+ target.register('/upload', upload.file('avatar'), (ammo) => {
18
+ ammo.fire({ file: ammo.payload.avatar });
19
+ });
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ ```javascript
25
+ const upload = new TejFileUploader({
26
+ destination: 'public/uploads', // Where to save files
27
+ name: 'custom-name', // Optional: custom filename
28
+ maxFileSize: 10 * 1024 * 1024 // Max file size in bytes (10MB)
29
+ });
30
+ ```
31
+
32
+ ### Options
33
+
34
+ | Option | Type | Description |
35
+ |--------|------|-------------|
36
+ | `destination` | string | Directory to save uploaded files |
37
+ | `name` | string | Optional custom filename |
38
+ | `maxFileSize` | number | Maximum file size in bytes |
39
+
40
+ ## Single File Upload
41
+
42
+ Use `upload.file()` for single file uploads:
43
+
44
+ ```javascript
45
+ // Expects a file field named 'avatar'
46
+ target.register('/avatar', upload.file('avatar'), (ammo) => {
47
+ const file = ammo.payload.avatar;
48
+
49
+ ammo.fire({
50
+ filename: file.filename,
51
+ path: file.path.relative,
52
+ mimetype: file.mimetype,
53
+ size: file.size
54
+ });
55
+ });
56
+ ```
57
+
58
+ ### File Object Structure
59
+
60
+ When a file is uploaded, `ammo.payload[fieldName]` contains:
61
+
62
+ ```javascript
63
+ {
64
+ filename: 'photo.jpg', // Original filename
65
+ extension: 'jpg', // File extension
66
+ path: {
67
+ absolute: '/var/www/uploads/photo.jpg', // Absolute path on disk
68
+ relative: '\\uploads\\photo.jpg' // Relative to cwd
69
+ },
70
+ mimetype: 'image/jpeg', // MIME type
71
+ size: { // From the filesize library
72
+ value: 245, // Numeric value
73
+ symbol: 'KB' // Unit (B, KB, MB, etc.)
74
+ }
75
+ }
76
+ ```
77
+
78
+ The `size` object is produced by the [filesize](https://www.npmjs.com/package/filesize) library. Use `${size.value} ${size.symbol}` for display (e.g. "245 KB").
79
+
80
+ ## Multiple File Upload
81
+
82
+ Use `upload.files()` for multiple files:
83
+
84
+ ```javascript
85
+ // Expects files in 'photos' and 'documents' fields
86
+ target.register('/documents', upload.files('photos', 'documents'), (ammo) => {
87
+ const { photos, documents } = ammo.payload;
88
+
89
+ ammo.fire({
90
+ photos: photos || [], // Array of file objects
91
+ documents: documents || [] // Array of file objects
92
+ });
93
+ });
94
+ ```
95
+
96
+ ### Multiple Files Response
97
+
98
+ Each field contains an array of file objects:
99
+
100
+ ```javascript
101
+ {
102
+ photos: [
103
+ { filename: 'photo1.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} },
104
+ { filename: 'photo2.jpg', path: {...}, mimetype: 'image/jpeg', size: {...} }
105
+ ],
106
+ documents: [
107
+ { filename: 'doc.pdf', path: {...}, mimetype: 'application/pdf', size: {...} }
108
+ ]
109
+ }
110
+ ```
111
+
112
+ ## Mixed Fields (Files + Data)
113
+
114
+ File uploads can include regular form fields:
115
+
116
+ ```javascript
117
+ target.register('/profile', upload.file('avatar'), (ammo) => {
118
+ const { avatar, name, bio } = ammo.payload;
119
+
120
+ ammo.fire({
121
+ name, // Regular form field
122
+ bio, // Regular form field
123
+ avatar: avatar // File object
124
+ });
125
+ });
126
+ ```
127
+
128
+ ## File Size Limits
129
+
130
+ When a file exceeds `maxFileSize`, a `413 Payload Too Large` error is thrown automatically. The error message includes the human-readable limit (e.g. "File size exceeds 2 MB"):
131
+
132
+ ```javascript
133
+ const upload = new TejFileUploader({
134
+ destination: 'uploads/',
135
+ maxFileSize: 2 * 1024 * 1024 // 2MB limit
136
+ });
137
+
138
+ target.register('/upload', upload.file('file'), (ammo) => {
139
+ // If file > 2MB, this handler never runs
140
+ // Client receives: 413 "File size exceeds 2 MB"
141
+ ammo.fire({ success: true });
142
+ });
143
+ ```
144
+
145
+ Note that the overall request body is also subject to the global `body.max_size` limit (default 10 MB). See [Configuration](./configuration.md).
146
+
147
+ ## Client-Side Examples
148
+
149
+ ### HTML Form
150
+
151
+ ```html
152
+ <form action="/files/upload" method="POST" enctype="multipart/form-data">
153
+ <input type="file" name="avatar" />
154
+ <input type="text" name="username" />
155
+ <button type="submit">Upload</button>
156
+ </form>
157
+ ```
158
+
159
+ ### JavaScript (Fetch)
160
+
161
+ ```javascript
162
+ const formData = new FormData();
163
+ formData.append('avatar', fileInput.files[0]);
164
+ formData.append('username', 'john');
165
+
166
+ const response = await fetch('/files/upload', {
167
+ method: 'POST',
168
+ body: formData
169
+ });
170
+ ```
171
+
172
+ ### JavaScript (Multiple Files)
173
+
174
+ ```javascript
175
+ const formData = new FormData();
176
+
177
+ // Add multiple files to same field
178
+ for (const file of fileInput.files) {
179
+ formData.append('photos', file);
180
+ }
181
+
182
+ const response = await fetch('/files/documents', {
183
+ method: 'POST',
184
+ body: formData
185
+ });
186
+ ```
187
+
188
+ ## Complete Example
189
+
190
+ ```javascript
191
+ import { Target, TejFileUploader, TejError } from 'te.js';
192
+ import fs from 'fs';
193
+ import path from 'path';
194
+
195
+ const target = new Target('/api/files');
196
+
197
+ // Configure uploader
198
+ const imageUpload = new TejFileUploader({
199
+ destination: 'public/images',
200
+ maxFileSize: 5 * 1024 * 1024 // 5MB
201
+ });
202
+
203
+ const documentUpload = new TejFileUploader({
204
+ destination: 'private/documents',
205
+ maxFileSize: 20 * 1024 * 1024 // 20MB
206
+ });
207
+
208
+ // Upload profile image
209
+ target.register('/profile-image', imageUpload.file('image'), (ammo) => {
210
+ if (!ammo.POST) return ammo.notAllowed();
211
+
212
+ const { image } = ammo.payload;
213
+
214
+ if (!image) {
215
+ throw new TejError(400, 'No image provided');
216
+ }
217
+
218
+ // Validate image type
219
+ if (!image.mimetype.startsWith('image/')) {
220
+ // Delete uploaded file
221
+ fs.unlinkSync(image.path.absolute);
222
+ throw new TejError(400, 'File must be an image');
223
+ }
224
+
225
+ ammo.fire({
226
+ message: 'Profile image uploaded',
227
+ url: `/images/${image.filename}`
228
+ });
229
+ });
230
+
231
+ // Upload multiple documents
232
+ target.register('/documents', documentUpload.files('files'), (ammo) => {
233
+ if (!ammo.POST) return ammo.notAllowed();
234
+
235
+ const { files } = ammo.payload;
236
+
237
+ if (!files || files.length === 0) {
238
+ throw new TejError(400, 'No files provided');
239
+ }
240
+
241
+ ammo.fire({
242
+ message: `${files.length} files uploaded`,
243
+ files: files.map(f => ({
244
+ name: f.filename,
245
+ size: `${f.size.value} ${f.size.symbol}`
246
+ }))
247
+ });
248
+ });
249
+
250
+ // Delete a file
251
+ target.register('/delete/:filename', (ammo) => {
252
+ if (!ammo.DELETE) return ammo.notAllowed();
253
+
254
+ const { filename } = ammo.payload;
255
+ const filepath = path.join('public/images', filename);
256
+
257
+ if (!fs.existsSync(filepath)) {
258
+ throw new TejError(404, 'File not found');
259
+ }
260
+
261
+ fs.unlinkSync(filepath);
262
+ ammo.fire({ message: 'File deleted' });
263
+ });
264
+ ```
265
+
266
+ ## Validation Middleware
267
+
268
+ Create reusable validation middleware:
269
+
270
+ ```javascript
271
+ // middleware/validate-image.js
272
+ import { TejError } from 'te.js';
273
+ import fs from 'fs';
274
+
275
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
276
+
277
+ export const validateImage = (fieldName) => (ammo, next) => {
278
+ const file = ammo.payload[fieldName];
279
+
280
+ if (!file) {
281
+ throw new TejError(400, `${fieldName} is required`);
282
+ }
283
+
284
+ if (!allowedTypes.includes(file.mimetype)) {
285
+ fs.unlinkSync(file.path.absolute);
286
+ throw new TejError(400, 'Only JPEG, PNG, GIF, and WebP images are allowed');
287
+ }
288
+
289
+ next();
290
+ };
291
+
292
+ // Usage
293
+ target.register('/avatar',
294
+ upload.file('avatar'),
295
+ validateImage('avatar'),
296
+ (ammo) => {
297
+ ammo.fire({ success: true });
298
+ }
299
+ );
300
+ ```
301
+
302
+ ## Serving Uploaded Files
303
+
304
+ Tejas doesn't include a static file server, but you can serve files manually:
305
+
306
+ ```javascript
307
+ import fs from 'fs';
308
+ import path from 'path';
309
+ import mime from 'mime';
310
+
311
+ target.register('/images/:filename', (ammo) => {
312
+ const { filename } = ammo.payload;
313
+ const filepath = path.join('public/images', filename);
314
+
315
+ if (!fs.existsSync(filepath)) {
316
+ return ammo.notFound();
317
+ }
318
+
319
+ const file = fs.readFileSync(filepath);
320
+ const contentType = mime.getType(filepath) || 'application/octet-stream';
321
+
322
+ ammo.fire(200, file, contentType);
323
+ });
324
+ ```
325
+
326
+ ## Best Practices
327
+
328
+ 1. **Validate file types** — Don't trust client-reported MIME types
329
+ 2. **Set size limits** — Prevent disk exhaustion attacks
330
+ 3. **Use unique filenames** — Avoid overwrites with UUID or timestamps
331
+ 4. **Store outside web root** — For sensitive files, store in private directories
332
+ 5. **Clean up on errors** — Delete uploaded files if validation fails
333
+ 6. **Scan for malware** — For production systems, integrate virus scanning