@venizia/ignis-docs 0.0.1-5 → 0.0.1-6

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.
@@ -64,3 +64,153 @@ export class FileController extends BaseController {
64
64
  }
65
65
  }
66
66
  ```
67
+
68
+ ---
69
+
70
+ ## Content-Disposition Utilities
71
+
72
+ These utilities help create secure, RFC-compliant `Content-Disposition` headers for file downloads.
73
+
74
+ ### `createContentDispositionHeader`
75
+
76
+ Creates a safe Content-Disposition header with proper filename encoding for file downloads.
77
+
78
+ #### `createContentDispositionHeader(filename: string): string`
79
+
80
+ - `filename` (string): The filename to use in the Content-Disposition header.
81
+
82
+ The function returns a properly formatted `Content-Disposition` header string with both ASCII and UTF-8 encoded filenames for maximum browser compatibility.
83
+
84
+ **Features:**
85
+ - Automatic filename sanitization (removes path components and dangerous characters)
86
+ - UTF-8 encoding support for international characters
87
+ - RFC 5987 compliant
88
+ - Fallback for older browsers
89
+
90
+ **Example:**
91
+
92
+ ```typescript
93
+ import { createContentDispositionHeader } from '@venizia/ignis-helpers';
94
+
95
+ // In a download endpoint
96
+ ctx.header('content-disposition', createContentDispositionHeader('my-document.pdf'));
97
+
98
+ // Output: attachment; filename="my-document.pdf"; filename*=UTF-8''my-document.pdf
99
+ ```
100
+
101
+ ---
102
+
103
+ ### `sanitizeFilename`
104
+
105
+ Sanitizes a filename for safe use, removing path components and dangerous characters. Useful for HTTP headers (e.g., Content-Disposition) and general file handling.
106
+
107
+ #### `sanitizeFilename(filename: string): string`
108
+
109
+ - `filename` (string): The filename to sanitize.
110
+
111
+ Returns a safe filename suitable for use in headers or filesystem operations.
112
+
113
+ **Features:**
114
+ - Removes path components (prevents directory traversal attacks)
115
+ - Allows only alphanumeric characters, spaces, hyphens, underscores, and dots
116
+ - Replaces dangerous characters with underscores
117
+ - Removes leading dots (prevents hidden files)
118
+ - Replaces consecutive dots with a single dot
119
+ - Removes ".." patterns (additional path traversal protection)
120
+ - Prevents empty filenames and suspicious patterns
121
+
122
+ **Example:**
123
+
124
+ ```typescript
125
+ import { sanitizeFilename } from '@venizia/ignis-helpers';
126
+
127
+ sanitizeFilename('../../etc/passwd'); // Returns: 'passwd'
128
+ sanitizeFilename('my<file>name.txt'); // Returns: 'my_file_name.txt'
129
+ sanitizeFilename('.hidden'); // Returns: 'hidden'
130
+ sanitizeFilename('file...txt'); // Returns: 'file.txt'
131
+ sanitizeFilename('документ.pdf'); // Returns: '_________.pdf'
132
+ sanitizeFilename(''); // Returns: 'download'
133
+ sanitizeFilename('..'); // Returns: 'download'
134
+ ```
135
+
136
+ ---
137
+
138
+ ### `encodeRFC5987`
139
+
140
+ Encodes a filename according to RFC 5987 for use in HTTP headers.
141
+
142
+ #### `encodeRFC5987(filename: string): string`
143
+
144
+ - `filename` (string): The filename to encode.
145
+
146
+ Returns an RFC 5987 encoded string suitable for the `filename*` parameter in Content-Disposition headers.
147
+
148
+ **Example:**
149
+
150
+ ```typescript
151
+ import { encodeRFC5987 } from '@venizia/ignis-helpers';
152
+
153
+ encodeRFC5987('my document.pdf'); // Returns: 'my%20document.pdf'
154
+ encodeRFC5987('файл.txt'); // Returns: '%D1%84%D0%B0%D0%B9%D0%BB.txt'
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Complete File Download Example
160
+
161
+ Here's a complete example combining multipart upload parsing with secure file downloads:
162
+
163
+ ```typescript
164
+ import { BaseController, controller } from '@venizia/ignis';
165
+ import { parseMultipartBody, createContentDispositionHeader, HTTP } from '@venizia/ignis-helpers';
166
+ import fs from 'node:fs';
167
+ import path from 'node:path';
168
+
169
+ @controller({ path: '/files' })
170
+ export class FileController extends BaseController {
171
+ override binding() {
172
+ // Upload endpoint
173
+ this.bindRoute({
174
+ configs: { path: '/upload', method: 'post' },
175
+ }).to({
176
+ handler: async (ctx) => {
177
+ const files = await parseMultipartBody({
178
+ context: ctx,
179
+ storage: 'disk',
180
+ uploadDir: './uploads',
181
+ });
182
+
183
+ return ctx.json({
184
+ message: 'Files uploaded successfully',
185
+ files: files.map(f => ({ name: f.originalname, size: f.size })),
186
+ });
187
+ },
188
+ });
189
+
190
+ // Download endpoint
191
+ this.bindRoute({
192
+ configs: { path: '/:filename', method: 'get' },
193
+ }).to({
194
+ handler: async (ctx) => {
195
+ const { filename } = ctx.req.valid('param');
196
+ const filePath = path.join('./uploads', filename);
197
+
198
+ // Read file
199
+ const fileStat = fs.statSync(filePath);
200
+ const fileStream = fs.createReadStream(filePath);
201
+
202
+ // Set secure headers
203
+ ctx.header('content-type', 'application/octet-stream');
204
+ ctx.header('content-length', fileStat.size.toString());
205
+ ctx.header('content-disposition', createContentDispositionHeader(filename));
206
+ ctx.header('x-content-type-options', 'nosniff');
207
+
208
+ return new Response(fileStream, {
209
+ headers: ctx.res.headers,
210
+ status: HTTP.ResultCodes.RS_2.Ok,
211
+ });
212
+ },
213
+ });
214
+ }
215
+ }
216
+ ```