skyguard-js 1.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.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/dist/app.d.ts +123 -0
- package/dist/app.js +198 -0
- package/dist/container/container.d.ts +60 -0
- package/dist/container/container.js +71 -0
- package/dist/exceptions/baseException.d.ts +6 -0
- package/dist/exceptions/baseException.js +15 -0
- package/dist/exceptions/contentDispositionException.d.ts +4 -0
- package/dist/exceptions/contentDispositionException.js +11 -0
- package/dist/exceptions/contentParserException.d.ts +10 -0
- package/dist/exceptions/contentParserException.js +21 -0
- package/dist/exceptions/fileDownloadException.d.ts +4 -0
- package/dist/exceptions/fileDownloadException.js +11 -0
- package/dist/exceptions/fileExistsException.d.ts +4 -0
- package/dist/exceptions/fileExistsException.js +11 -0
- package/dist/exceptions/helperExceptions.d.ts +10 -0
- package/dist/exceptions/helperExceptions.js +25 -0
- package/dist/exceptions/httpNotFoundException.d.ts +7 -0
- package/dist/exceptions/httpNotFoundException.js +14 -0
- package/dist/exceptions/index.d.ts +9 -0
- package/dist/exceptions/index.js +25 -0
- package/dist/exceptions/invalidHttpStatusException.d.ts +4 -0
- package/dist/exceptions/invalidHttpStatusException.js +11 -0
- package/dist/exceptions/sessionException.d.ts +4 -0
- package/dist/exceptions/sessionException.js +11 -0
- package/dist/exceptions/validationException.d.ts +13 -0
- package/dist/exceptions/validationException.js +32 -0
- package/dist/helpers/app.d.ts +4 -0
- package/dist/helpers/app.js +12 -0
- package/dist/helpers/http.d.ts +59 -0
- package/dist/helpers/http.js +77 -0
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +9 -0
- package/dist/http/httpAdapter.d.ts +26 -0
- package/dist/http/httpAdapter.js +2 -0
- package/dist/http/httpMethods.d.ts +14 -0
- package/dist/http/httpMethods.js +18 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +13 -0
- package/dist/http/logger.d.ts +8 -0
- package/dist/http/logger.js +36 -0
- package/dist/http/nodeNativeHttp.d.ts +41 -0
- package/dist/http/nodeNativeHttp.js +73 -0
- package/dist/http/request.d.ts +85 -0
- package/dist/http/request.js +127 -0
- package/dist/http/response.d.ts +118 -0
- package/dist/http/response.js +179 -0
- package/dist/http/statusCodes.d.ts +1 -0
- package/dist/http/statusCodes.js +38 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -0
- package/dist/middlewares/cors.d.ts +103 -0
- package/dist/middlewares/cors.js +91 -0
- package/dist/middlewares/session.d.ts +26 -0
- package/dist/middlewares/session.js +88 -0
- package/dist/parsers/contentParser.d.ts +27 -0
- package/dist/parsers/contentParser.js +2 -0
- package/dist/parsers/contentParserManager.d.ts +50 -0
- package/dist/parsers/contentParserManager.js +91 -0
- package/dist/parsers/index.d.ts +8 -0
- package/dist/parsers/index.js +30 -0
- package/dist/parsers/jsonParser.d.ts +10 -0
- package/dist/parsers/jsonParser.js +24 -0
- package/dist/parsers/multipartParser.d.ts +35 -0
- package/dist/parsers/multipartParser.js +120 -0
- package/dist/parsers/parserInterface.d.ts +37 -0
- package/dist/parsers/parserInterface.js +2 -0
- package/dist/parsers/textParser.d.ts +11 -0
- package/dist/parsers/textParser.js +19 -0
- package/dist/parsers/urlEncodedParser.d.ts +10 -0
- package/dist/parsers/urlEncodedParser.js +19 -0
- package/dist/parsers/xmlParser.d.ts +47 -0
- package/dist/parsers/xmlParser.js +158 -0
- package/dist/routing/index.d.ts +3 -0
- package/dist/routing/index.js +9 -0
- package/dist/routing/layer.d.ts +85 -0
- package/dist/routing/layer.js +117 -0
- package/dist/routing/router.d.ts +143 -0
- package/dist/routing/router.js +210 -0
- package/dist/routing/routerGroup.d.ts +79 -0
- package/dist/routing/routerGroup.js +103 -0
- package/dist/server/nodeNativeServer.d.ts +29 -0
- package/dist/server/nodeNativeServer.js +42 -0
- package/dist/sessions/cookieOptions.d.ts +72 -0
- package/dist/sessions/cookieOptions.js +2 -0
- package/dist/sessions/index.d.ts +4 -0
- package/dist/sessions/index.js +7 -0
- package/dist/sessions/memorySessionStorage.d.ts +112 -0
- package/dist/sessions/memorySessionStorage.js +170 -0
- package/dist/sessions/session.d.ts +80 -0
- package/dist/sessions/session.js +101 -0
- package/dist/sessions/sessionStorage.d.ts +105 -0
- package/dist/sessions/sessionStorage.js +2 -0
- package/dist/static/contentDisposition.d.ts +71 -0
- package/dist/static/contentDisposition.js +159 -0
- package/dist/static/fileDownload.d.ts +44 -0
- package/dist/static/fileDownload.js +88 -0
- package/dist/static/fileStaticHandler.d.ts +61 -0
- package/dist/static/fileStaticHandler.js +110 -0
- package/dist/static/index.d.ts +4 -0
- package/dist/static/index.js +11 -0
- package/dist/static/mimeTypes.d.ts +1 -0
- package/dist/static/mimeTypes.js +40 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +122 -0
- package/dist/types/index.js +2 -0
- package/dist/validators/index.d.ts +5 -0
- package/dist/validators/index.js +22 -0
- package/dist/validators/rules/booleanRule.d.ts +11 -0
- package/dist/validators/rules/booleanRule.js +22 -0
- package/dist/validators/rules/dateRule.d.ts +16 -0
- package/dist/validators/rules/dateRule.js +44 -0
- package/dist/validators/rules/emailRule.d.ts +12 -0
- package/dist/validators/rules/emailRule.js +24 -0
- package/dist/validators/rules/index.d.ts +6 -0
- package/dist/validators/rules/index.js +15 -0
- package/dist/validators/rules/numberRule.d.ts +17 -0
- package/dist/validators/rules/numberRule.js +30 -0
- package/dist/validators/rules/requiredRule.d.ts +11 -0
- package/dist/validators/rules/requiredRule.js +21 -0
- package/dist/validators/rules/stringRule.d.ts +18 -0
- package/dist/validators/rules/stringRule.js +32 -0
- package/dist/validators/types.d.ts +55 -0
- package/dist/validators/types.js +2 -0
- package/dist/validators/validationRule.d.ts +53 -0
- package/dist/validators/validationRule.js +37 -0
- package/dist/validators/validationSchema.d.ts +145 -0
- package/dist/validators/validationSchema.js +198 -0
- package/dist/validators/validator.d.ts +58 -0
- package/dist/validators/validator.js +91 -0
- package/dist/views/helpersTemplate.d.ts +104 -0
- package/dist/views/helpersTemplate.js +186 -0
- package/dist/views/index.d.ts +4 -0
- package/dist/views/index.js +9 -0
- package/dist/views/raptorEngine.d.ts +127 -0
- package/dist/views/raptorEngine.js +165 -0
- package/dist/views/templateEngine.d.ts +80 -0
- package/dist/views/templateEngine.js +204 -0
- package/dist/views/view.d.ts +55 -0
- package/dist/views/view.js +2 -0
- package/package.json +79 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ContentDisposition = void 0;
|
|
4
|
+
const contentDispositionException_1 = require("../exceptions/contentDispositionException");
|
|
5
|
+
/**
|
|
6
|
+
* Generates safe `Content-Disposition` values for file downloads.
|
|
7
|
+
*
|
|
8
|
+
* Implements RFC 6266 and RFC 8187 for broad browser compatibility.
|
|
9
|
+
* Prevents header injection and handles non-ASCII filenames correctly.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const cd = new ContentDisposition();
|
|
13
|
+
*
|
|
14
|
+
* cd.attachment("report.pdf");
|
|
15
|
+
* // => 'attachment; filename="report.pdf"'
|
|
16
|
+
*
|
|
17
|
+
* cd.attachment("reporte año 2024.pdf");
|
|
18
|
+
* // => 'attachment; filename="reporte ano 2024.pdf"; filename*=UTF-8\'\'reporte%20a%C3%B1o%202024.pdf'
|
|
19
|
+
*
|
|
20
|
+
* cd.inline("image.jpg");
|
|
21
|
+
* // => 'inline; filename="image.jpg"'
|
|
22
|
+
*/
|
|
23
|
+
class ContentDisposition {
|
|
24
|
+
/**
|
|
25
|
+
* Builds a `Content-Disposition` value for file downloads.
|
|
26
|
+
*
|
|
27
|
+
* @param filename - File name
|
|
28
|
+
* @returns `Content-Disposition` value
|
|
29
|
+
*/
|
|
30
|
+
attachment(filename) {
|
|
31
|
+
return this.create("attachment", filename);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds a `Content-Disposition` value for inline rendering.
|
|
35
|
+
*
|
|
36
|
+
* @param filename - File name
|
|
37
|
+
* @returns `Content-Disposition` value
|
|
38
|
+
*/
|
|
39
|
+
inline(filename) {
|
|
40
|
+
return this.create("inline", filename);
|
|
41
|
+
}
|
|
42
|
+
create(type, filename) {
|
|
43
|
+
if (!filename || typeof filename !== "string") {
|
|
44
|
+
throw new contentDispositionException_1.ContentDispositionException();
|
|
45
|
+
}
|
|
46
|
+
const sanitized = this.sanitizeFilename(filename);
|
|
47
|
+
const needsEncoding = this.needsEncoding(sanitized);
|
|
48
|
+
let disposition = type;
|
|
49
|
+
if (needsEncoding) {
|
|
50
|
+
const fallback = this.createAsciiFallback(sanitized);
|
|
51
|
+
disposition += `; filename="${this.escapeQuotes(fallback)}"`;
|
|
52
|
+
const encoded = this.encodeRFC8187(sanitized);
|
|
53
|
+
disposition += `; filename*=UTF-8''${encoded}`;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
disposition += `; filename="${this.escapeQuotes(sanitized)}"`;
|
|
57
|
+
}
|
|
58
|
+
return disposition;
|
|
59
|
+
}
|
|
60
|
+
sanitizeFilename(filename) {
|
|
61
|
+
return filename
|
|
62
|
+
.replace(/[\x00-\x1F\x7F-\x9F]/g, "")
|
|
63
|
+
.replace(/["\r\n]/g, "")
|
|
64
|
+
.replace(/[/\\]/g, "")
|
|
65
|
+
.replace(/\s+/g, " ")
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
needsEncoding(filename) {
|
|
69
|
+
return /[^\x20-\x7E]/.test(filename);
|
|
70
|
+
}
|
|
71
|
+
createAsciiFallback(filename) {
|
|
72
|
+
const charMap = {
|
|
73
|
+
á: "a",
|
|
74
|
+
é: "e",
|
|
75
|
+
í: "i",
|
|
76
|
+
ó: "o",
|
|
77
|
+
ú: "u",
|
|
78
|
+
ñ: "n",
|
|
79
|
+
Á: "A",
|
|
80
|
+
É: "E",
|
|
81
|
+
Í: "I",
|
|
82
|
+
Ó: "O",
|
|
83
|
+
Ú: "U",
|
|
84
|
+
Ñ: "N",
|
|
85
|
+
ü: "u",
|
|
86
|
+
Ü: "U",
|
|
87
|
+
ç: "c",
|
|
88
|
+
Ç: "C",
|
|
89
|
+
ß: "ss",
|
|
90
|
+
æ: "ae",
|
|
91
|
+
œ: "oe",
|
|
92
|
+
" ": " ",
|
|
93
|
+
};
|
|
94
|
+
let result = filename;
|
|
95
|
+
for (const [char, replacement] of Object.entries(charMap)) {
|
|
96
|
+
result = result.replace(new RegExp(char, "g"), replacement);
|
|
97
|
+
}
|
|
98
|
+
result = result.replace(/[^\x20-\x7E]/g, "");
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
escapeQuotes(filename) {
|
|
102
|
+
return filename.replace(/"/g, '\\"');
|
|
103
|
+
}
|
|
104
|
+
encodeRFC8187(filename) {
|
|
105
|
+
const attrChar = /[a-zA-Z0-9!#$&+.^_`|~-]/;
|
|
106
|
+
let encoded = "";
|
|
107
|
+
for (let i = 0; i < filename.length; i++) {
|
|
108
|
+
const char = filename[i];
|
|
109
|
+
if (attrChar.test(char)) {
|
|
110
|
+
encoded += char;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const bytes = Buffer.from(char, "utf-8");
|
|
114
|
+
for (const byte of bytes) {
|
|
115
|
+
encoded += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return encoded;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Parses an existing `Content-Disposition` header value.
|
|
123
|
+
*
|
|
124
|
+
* Supports both `filename=` and `filename*=` (RFC 8187).
|
|
125
|
+
*
|
|
126
|
+
* @param header - Raw `Content-Disposition` header value
|
|
127
|
+
* @returns Parsed disposition type and filename (if present)
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* const cd = new ContentDisposition();
|
|
131
|
+
* cd.parse('attachment; filename="report.pdf"');
|
|
132
|
+
* // => { type: "attachment", filename: "report.pdf" }
|
|
133
|
+
*/
|
|
134
|
+
parse(header) {
|
|
135
|
+
const parts = header.split(";").map((p) => p.trim());
|
|
136
|
+
const type = parts[0];
|
|
137
|
+
let filename = null;
|
|
138
|
+
for (let i = 1; i < parts.length; i++) {
|
|
139
|
+
const part = parts[i];
|
|
140
|
+
if (part.startsWith("filename*=")) {
|
|
141
|
+
const value = part.substring(10);
|
|
142
|
+
const match = value.match(/^UTF-8''(.+)$/i);
|
|
143
|
+
if (match) {
|
|
144
|
+
filename = decodeURIComponent(match[1]);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (part.startsWith("filename=") && !filename) {
|
|
149
|
+
let value = part.substring(9);
|
|
150
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
151
|
+
value = value.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
filename = value.replace(/\\"/g, '"');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { type, filename };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
exports.ContentDisposition = ContentDisposition;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Response } from "../http/response";
|
|
2
|
+
/**
|
|
3
|
+
* Helper utility for serving file downloads.
|
|
4
|
+
*
|
|
5
|
+
* Builds a {@link Response} configured with safe headers
|
|
6
|
+
* (`Content-Disposition`, `Content-Type`, `Content-Length`)
|
|
7
|
+
* to send files to the client.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FileDownloadHelper {
|
|
10
|
+
private contentDisposition;
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Creates a file download response.
|
|
14
|
+
*
|
|
15
|
+
* Resolves the file path, validates that it is a file,
|
|
16
|
+
* reads its contents, and returns a {@link Response}
|
|
17
|
+
* ready to be sent to the client.
|
|
18
|
+
*
|
|
19
|
+
* @param filePath - Path to the file on disk
|
|
20
|
+
* @param filename - Optional custom filename for the download
|
|
21
|
+
* @param headers - Optional additional response headers
|
|
22
|
+
* @returns A {@link Response} configured for file download
|
|
23
|
+
*
|
|
24
|
+
* @throws {FileDownloadException}
|
|
25
|
+
* Thrown if the file does not exist, is not a file,
|
|
26
|
+
* or cannot be read.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const helper = new FileDownloadHelper();
|
|
30
|
+
*
|
|
31
|
+
* app.get("/download", async () => {
|
|
32
|
+
* return helper.download("./files/report.pdf");
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* // Custom filename and headers
|
|
36
|
+
* return helper.download(
|
|
37
|
+
* "./files/data.csv",
|
|
38
|
+
* "export.csv",
|
|
39
|
+
* { "Cache-Control": "no-store" }
|
|
40
|
+
* );
|
|
41
|
+
*/
|
|
42
|
+
download(filePath: string, filename?: string, headers?: Record<string, string>): Promise<Response>;
|
|
43
|
+
private getMimeType;
|
|
44
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileDownloadHelper = void 0;
|
|
4
|
+
const promises_1 = require("node:fs/promises");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const mimeTypes_1 = require("./mimeTypes");
|
|
7
|
+
const contentDisposition_1 = require("./contentDisposition");
|
|
8
|
+
const response_1 = require("../http/response");
|
|
9
|
+
const fileDownloadException_1 = require("../exceptions/fileDownloadException");
|
|
10
|
+
/**
|
|
11
|
+
* Helper utility for serving file downloads.
|
|
12
|
+
*
|
|
13
|
+
* Builds a {@link Response} configured with safe headers
|
|
14
|
+
* (`Content-Disposition`, `Content-Type`, `Content-Length`)
|
|
15
|
+
* to send files to the client.
|
|
16
|
+
*/
|
|
17
|
+
class FileDownloadHelper {
|
|
18
|
+
contentDisposition;
|
|
19
|
+
constructor() {
|
|
20
|
+
this.contentDisposition = new contentDisposition_1.ContentDisposition();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a file download response.
|
|
24
|
+
*
|
|
25
|
+
* Resolves the file path, validates that it is a file,
|
|
26
|
+
* reads its contents, and returns a {@link Response}
|
|
27
|
+
* ready to be sent to the client.
|
|
28
|
+
*
|
|
29
|
+
* @param filePath - Path to the file on disk
|
|
30
|
+
* @param filename - Optional custom filename for the download
|
|
31
|
+
* @param headers - Optional additional response headers
|
|
32
|
+
* @returns A {@link Response} configured for file download
|
|
33
|
+
*
|
|
34
|
+
* @throws {FileDownloadException}
|
|
35
|
+
* Thrown if the file does not exist, is not a file,
|
|
36
|
+
* or cannot be read.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* const helper = new FileDownloadHelper();
|
|
40
|
+
*
|
|
41
|
+
* app.get("/download", async () => {
|
|
42
|
+
* return helper.download("./files/report.pdf");
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Custom filename and headers
|
|
46
|
+
* return helper.download(
|
|
47
|
+
* "./files/data.csv",
|
|
48
|
+
* "export.csv",
|
|
49
|
+
* { "Cache-Control": "no-store" }
|
|
50
|
+
* );
|
|
51
|
+
*/
|
|
52
|
+
async download(filePath, filename, headers) {
|
|
53
|
+
try {
|
|
54
|
+
const fullPath = (0, node_path_1.resolve)(filePath);
|
|
55
|
+
const stats = await (0, promises_1.stat)(fullPath);
|
|
56
|
+
if (!stats.isFile()) {
|
|
57
|
+
throw new fileDownloadException_1.FileDownloadException(`Path is not a file: ${fullPath}`);
|
|
58
|
+
}
|
|
59
|
+
const content = await (0, promises_1.readFile)(fullPath);
|
|
60
|
+
const downloadName = filename || (0, node_path_1.basename)(fullPath);
|
|
61
|
+
const downloadHeaders = {
|
|
62
|
+
"Content-Disposition": this.contentDisposition.attachment(downloadName),
|
|
63
|
+
"Content-Length": content.length.toString(),
|
|
64
|
+
"Content-Type": this.getMimeType(fullPath),
|
|
65
|
+
};
|
|
66
|
+
if (headers) {
|
|
67
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
68
|
+
// Prevent overriding Content-Disposition
|
|
69
|
+
if (key.toLowerCase() !== "content-disposition") {
|
|
70
|
+
downloadHeaders[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return new response_1.Response()
|
|
75
|
+
.setStatus(200)
|
|
76
|
+
.setContent(content)
|
|
77
|
+
.setHeaders(downloadHeaders);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new fileDownloadException_1.FileDownloadException("Failed to download file");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
getMimeType(filePath) {
|
|
84
|
+
const ext = (0, node_path_1.extname)(filePath).toLowerCase();
|
|
85
|
+
return mimeTypes_1.mimeTypesObject[ext] || "application/octet-stream";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.FileDownloadHelper = FileDownloadHelper;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Response } from "../http/response";
|
|
2
|
+
/**
|
|
3
|
+
* Static file handler for a public directory.
|
|
4
|
+
*
|
|
5
|
+
* Resolves and serves files from a configured `publicPath` using a URL prefix
|
|
6
|
+
* derived from the directory name (e.g. `/public`).
|
|
7
|
+
*
|
|
8
|
+
* Includes basic path traversal protection and sets cache-related headers.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const staticFiles = new StaticFileHandler("./public");
|
|
12
|
+
*
|
|
13
|
+
* // In your request pipeline:
|
|
14
|
+
* const response = await staticFiles.tryServeFile(request.getUrl);
|
|
15
|
+
* if (response) return response;
|
|
16
|
+
*/
|
|
17
|
+
export declare class StaticFileHandler {
|
|
18
|
+
private publicPath;
|
|
19
|
+
private urlPrefix;
|
|
20
|
+
private mimeTypes;
|
|
21
|
+
constructor(publicPath: string);
|
|
22
|
+
/**
|
|
23
|
+
* Returns the configured URL prefix.
|
|
24
|
+
*
|
|
25
|
+
* @returns URL prefix (e.g. `"/public"`)
|
|
26
|
+
*/
|
|
27
|
+
getUrlPrefix(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Checks whether the request path matches the static prefix.
|
|
30
|
+
*
|
|
31
|
+
* @param requestPath - Requested path
|
|
32
|
+
* @returns `true` if the path starts with the prefix
|
|
33
|
+
*/
|
|
34
|
+
matchesPrefix(requestPath: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Attempts to serve a static file.
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` if:
|
|
39
|
+
* - the request path does not match the prefix
|
|
40
|
+
* - the resolved path escapes the public directory
|
|
41
|
+
* - the file does not exist or is not a file
|
|
42
|
+
* - an I/O error occurs
|
|
43
|
+
*
|
|
44
|
+
* @param requestPath - Requested path (e.g. `"/public/css/style.css"`)
|
|
45
|
+
* @returns A {@link Response} containing the file, or `null` if not found
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const response = await staticFiles.tryServeFile("/public/app.js");
|
|
49
|
+
* if (response) return response;
|
|
50
|
+
*/
|
|
51
|
+
tryServeFile(requestPath: string): Promise<Response | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Checks whether a request path looks like a static file request.
|
|
54
|
+
*
|
|
55
|
+
* This is a fast heuristic: it requires a file extension and a known mime type.
|
|
56
|
+
*
|
|
57
|
+
* @param requestPath - Requested path
|
|
58
|
+
* @returns `true` if it looks like a static file request
|
|
59
|
+
*/
|
|
60
|
+
isStaticFileRequest(requestPath: string): boolean;
|
|
61
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StaticFileHandler = void 0;
|
|
4
|
+
const node_path_1 = require("node:path");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const mimeTypes_1 = require("./mimeTypes");
|
|
7
|
+
const response_1 = require("../http/response");
|
|
8
|
+
/**
|
|
9
|
+
* Static file handler for a public directory.
|
|
10
|
+
*
|
|
11
|
+
* Resolves and serves files from a configured `publicPath` using a URL prefix
|
|
12
|
+
* derived from the directory name (e.g. `/public`).
|
|
13
|
+
*
|
|
14
|
+
* Includes basic path traversal protection and sets cache-related headers.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const staticFiles = new StaticFileHandler("./public");
|
|
18
|
+
*
|
|
19
|
+
* // In your request pipeline:
|
|
20
|
+
* const response = await staticFiles.tryServeFile(request.getUrl);
|
|
21
|
+
* if (response) return response;
|
|
22
|
+
*/
|
|
23
|
+
class StaticFileHandler {
|
|
24
|
+
publicPath = "";
|
|
25
|
+
urlPrefix = "";
|
|
26
|
+
mimeTypes;
|
|
27
|
+
constructor(publicPath) {
|
|
28
|
+
this.publicPath = (0, node_path_1.normalize)(publicPath);
|
|
29
|
+
this.urlPrefix = "/" + (0, node_path_1.basename)(this.publicPath);
|
|
30
|
+
this.mimeTypes = mimeTypes_1.mimeTypesObject;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the configured URL prefix.
|
|
34
|
+
*
|
|
35
|
+
* @returns URL prefix (e.g. `"/public"`)
|
|
36
|
+
*/
|
|
37
|
+
getUrlPrefix() {
|
|
38
|
+
return this.urlPrefix;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Checks whether the request path matches the static prefix.
|
|
42
|
+
*
|
|
43
|
+
* @param requestPath - Requested path
|
|
44
|
+
* @returns `true` if the path starts with the prefix
|
|
45
|
+
*/
|
|
46
|
+
matchesPrefix(requestPath) {
|
|
47
|
+
return requestPath.startsWith(this.urlPrefix);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Attempts to serve a static file.
|
|
51
|
+
*
|
|
52
|
+
* Returns `null` if:
|
|
53
|
+
* - the request path does not match the prefix
|
|
54
|
+
* - the resolved path escapes the public directory
|
|
55
|
+
* - the file does not exist or is not a file
|
|
56
|
+
* - an I/O error occurs
|
|
57
|
+
*
|
|
58
|
+
* @param requestPath - Requested path (e.g. `"/public/css/style.css"`)
|
|
59
|
+
* @returns A {@link Response} containing the file, or `null` if not found
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const response = await staticFiles.tryServeFile("/public/app.js");
|
|
63
|
+
* if (response) return response;
|
|
64
|
+
*/
|
|
65
|
+
async tryServeFile(requestPath) {
|
|
66
|
+
try {
|
|
67
|
+
if (!this.matchesPrefix(requestPath))
|
|
68
|
+
return null;
|
|
69
|
+
const relativePath = requestPath.slice(this.urlPrefix.length);
|
|
70
|
+
const filePath = (0, node_path_1.resolve)(this.publicPath, relativePath.replace(/^[/\\]+/, ""));
|
|
71
|
+
// Prevent path traversal (must stay within publicPath)
|
|
72
|
+
if (!filePath.startsWith(this.publicPath + node_path_1.sep))
|
|
73
|
+
return null;
|
|
74
|
+
const stats = await (0, promises_1.stat)(filePath);
|
|
75
|
+
if (!stats.isFile())
|
|
76
|
+
return null;
|
|
77
|
+
const content = await (0, promises_1.readFile)(filePath);
|
|
78
|
+
const ext = (0, node_path_1.extname)(filePath).toLowerCase();
|
|
79
|
+
const contentType = this.mimeTypes[ext] || this.mimeTypes["default"];
|
|
80
|
+
return new response_1.Response()
|
|
81
|
+
.setContent(content)
|
|
82
|
+
.setHeaders({
|
|
83
|
+
"content-type": contentType,
|
|
84
|
+
"content-length": content.length.toString(),
|
|
85
|
+
"cache-control": "public, max-age=31536000",
|
|
86
|
+
"last-modified": stats.mtime.toUTCString(),
|
|
87
|
+
ETag: `"${stats.size}-${stats.mtime.getTime()}"`,
|
|
88
|
+
})
|
|
89
|
+
.setStatus(200);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Checks whether a request path looks like a static file request.
|
|
97
|
+
*
|
|
98
|
+
* This is a fast heuristic: it requires a file extension and a known mime type.
|
|
99
|
+
*
|
|
100
|
+
* @param requestPath - Requested path
|
|
101
|
+
* @returns `true` if it looks like a static file request
|
|
102
|
+
*/
|
|
103
|
+
isStaticFileRequest(requestPath) {
|
|
104
|
+
if (!this.matchesPrefix(requestPath))
|
|
105
|
+
return false;
|
|
106
|
+
const ext = (0, node_path_1.extname)(requestPath);
|
|
107
|
+
return ext !== "" && ext in this.mimeTypes;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.StaticFileHandler = StaticFileHandler;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mimeTypesObject = exports.StaticFileHandler = exports.FileDownloadHelper = exports.ContentDisposition = void 0;
|
|
4
|
+
var contentDisposition_1 = require("./contentDisposition");
|
|
5
|
+
Object.defineProperty(exports, "ContentDisposition", { enumerable: true, get: function () { return contentDisposition_1.ContentDisposition; } });
|
|
6
|
+
var fileDownload_1 = require("./fileDownload");
|
|
7
|
+
Object.defineProperty(exports, "FileDownloadHelper", { enumerable: true, get: function () { return fileDownload_1.FileDownloadHelper; } });
|
|
8
|
+
var fileStaticHandler_1 = require("./fileStaticHandler");
|
|
9
|
+
Object.defineProperty(exports, "StaticFileHandler", { enumerable: true, get: function () { return fileStaticHandler_1.StaticFileHandler; } });
|
|
10
|
+
var mimeTypes_1 = require("./mimeTypes");
|
|
11
|
+
Object.defineProperty(exports, "mimeTypesObject", { enumerable: true, get: function () { return mimeTypes_1.mimeTypesObject; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const mimeTypesObject: Record<string, string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mimeTypesObject = void 0;
|
|
4
|
+
exports.mimeTypesObject = {
|
|
5
|
+
// Text
|
|
6
|
+
".html": "text/html",
|
|
7
|
+
".css": "text/css",
|
|
8
|
+
".js": "text/javascript",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".xml": "application/xml",
|
|
11
|
+
".txt": "text/plain",
|
|
12
|
+
// Images
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".jpg": "image/jpeg",
|
|
15
|
+
".jpeg": "image/jpeg",
|
|
16
|
+
".gif": "image/gif",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".webp": "image/webp",
|
|
19
|
+
".ico": "image/x-icon",
|
|
20
|
+
// Fonts
|
|
21
|
+
".woff": "font/woff",
|
|
22
|
+
".woff2": "font/woff2",
|
|
23
|
+
".ttf": "font/ttf",
|
|
24
|
+
".otf": "font/otf",
|
|
25
|
+
".eot": "application/vnd.ms-fontobject",
|
|
26
|
+
// Documents
|
|
27
|
+
".pdf": "application/pdf",
|
|
28
|
+
".doc": "application/msword",
|
|
29
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
30
|
+
// Archives
|
|
31
|
+
".zip": "application/zip",
|
|
32
|
+
".rar": "application/x-rar-compressed",
|
|
33
|
+
// Audio/Video
|
|
34
|
+
".mp3": "audio/mpeg",
|
|
35
|
+
".mp4": "video/mp4",
|
|
36
|
+
".webm": "video/webm",
|
|
37
|
+
".ogg": "audio/ogg",
|
|
38
|
+
// Default
|
|
39
|
+
default: "application/octet-stream",
|
|
40
|
+
};
|