@server/next 0.37.1 → 0.38.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/index.d.ts +9 -15
- package/index.js +312 -258
- package/package.json +20 -3
package/index.d.ts
CHANGED
|
@@ -5,13 +5,6 @@ type LimitOptions = {
|
|
|
5
5
|
minSize?: number | string;
|
|
6
6
|
fileType?: string[];
|
|
7
7
|
};
|
|
8
|
-
declare class UploadPipeline {
|
|
9
|
-
private _bucket;
|
|
10
|
-
private _limits;
|
|
11
|
-
constructor(bucket?: Bucket | string | null);
|
|
12
|
-
limit(options: LimitOptions): this;
|
|
13
|
-
processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
|
|
14
|
-
}
|
|
15
8
|
|
|
16
9
|
type CookieOptions = string | string[] | Cookie | Cookie[] | null;
|
|
17
10
|
interface ResponseData {
|
|
@@ -24,7 +17,7 @@ declare class Reply$1 {
|
|
|
24
17
|
status(status: number): this;
|
|
25
18
|
type(type?: string): this;
|
|
26
19
|
download(name?: string): this;
|
|
27
|
-
headers(key: string | Record<string, string>, value?: string): this;
|
|
20
|
+
headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
|
|
28
21
|
cache(value: CacheOption): this;
|
|
29
22
|
cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
|
|
30
23
|
json(body: unknown): Response;
|
|
@@ -88,18 +81,16 @@ type Cookie = {
|
|
|
88
81
|
};
|
|
89
82
|
type RouterMethod = "*" | Method;
|
|
90
83
|
type FileInfo = {
|
|
91
|
-
exists: boolean;
|
|
92
84
|
size: number;
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
type: string | null;
|
|
86
|
+
modified: Date;
|
|
95
87
|
};
|
|
96
88
|
type BucketFile = {
|
|
97
89
|
readonly path: string;
|
|
98
|
-
readonly id: string;
|
|
99
90
|
readonly name: string;
|
|
100
91
|
readonly type?: string;
|
|
101
92
|
exists(): Promise<boolean>;
|
|
102
|
-
info?(): Promise<FileInfo>;
|
|
93
|
+
info?(): Promise<FileInfo | null>;
|
|
103
94
|
write(content: string | Buffer | ReadableStream, options?: {
|
|
104
95
|
type?: string;
|
|
105
96
|
}): Promise<void>;
|
|
@@ -114,7 +105,6 @@ type Bucket = {
|
|
|
114
105
|
};
|
|
115
106
|
type UploadedFile = {
|
|
116
107
|
name: string;
|
|
117
|
-
id: string;
|
|
118
108
|
path: string;
|
|
119
109
|
type: string;
|
|
120
110
|
size: number;
|
|
@@ -195,6 +185,7 @@ type SecurityOptions = {
|
|
|
195
185
|
referrerPolicy?: boolean | string;
|
|
196
186
|
hsts?: boolean | string;
|
|
197
187
|
xssProtection?: boolean;
|
|
188
|
+
traversalProtection?: boolean;
|
|
198
189
|
csp?: boolean | string;
|
|
199
190
|
coop?: boolean | string;
|
|
200
191
|
corp?: boolean | string;
|
|
@@ -202,6 +193,7 @@ type SecurityOptions = {
|
|
|
202
193
|
};
|
|
203
194
|
type SecuritySettings = {
|
|
204
195
|
trustProxy: boolean;
|
|
196
|
+
traversalProtection: boolean;
|
|
205
197
|
headers: Record<string, string>;
|
|
206
198
|
hsts: string | null;
|
|
207
199
|
};
|
|
@@ -232,7 +224,9 @@ type Settings = {
|
|
|
232
224
|
port: number;
|
|
233
225
|
secret: string;
|
|
234
226
|
public?: Bucket;
|
|
235
|
-
uploads?:
|
|
227
|
+
uploads?: ({
|
|
228
|
+
bucket: Bucket;
|
|
229
|
+
} & LimitOptions) | null;
|
|
236
230
|
store?: KVStore;
|
|
237
231
|
cookies?: KVStore;
|
|
238
232
|
session?: {
|
package/index.js
CHANGED
|
@@ -39,6 +39,10 @@ var ServerError_default = TypedServerError;
|
|
|
39
39
|
|
|
40
40
|
// src/errors/index.ts
|
|
41
41
|
ServerError_default.extend({
|
|
42
|
+
PATH_TRAVERSAL: {
|
|
43
|
+
status: 400,
|
|
44
|
+
message: "The route param '{param}' tries to climb the path ('{value}'). If this route legitimately receives paths, set security: { traversalProtection: false }"
|
|
45
|
+
},
|
|
42
46
|
NO_STORE: "You need a 'store' to write 'ctx.session'",
|
|
43
47
|
NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
|
|
44
48
|
NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
|
|
@@ -76,6 +80,7 @@ ServerError_default.extend({
|
|
|
76
80
|
REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
|
|
77
81
|
REGISTER_EMAIL_EXISTS: "Email is already registered"
|
|
78
82
|
});
|
|
83
|
+
var errors_default = ServerError_default;
|
|
79
84
|
|
|
80
85
|
// src/polyfill.ts
|
|
81
86
|
globalThis.env = {};
|
|
@@ -98,191 +103,6 @@ var StatusError = class extends Error {
|
|
|
98
103
|
}
|
|
99
104
|
};
|
|
100
105
|
|
|
101
|
-
// src/helpers/bucket.ts
|
|
102
|
-
import * as fs from "fs";
|
|
103
|
-
import * as fsp from "fs/promises";
|
|
104
|
-
import * as path from "path";
|
|
105
|
-
|
|
106
|
-
// src/helpers/mimes.ts
|
|
107
|
-
var mimes_default = {
|
|
108
|
-
aac: "audio/aac",
|
|
109
|
-
abw: "application/x-abiword",
|
|
110
|
-
arc: "application/x-freearc",
|
|
111
|
-
avif: "image/avif",
|
|
112
|
-
avi: "video/x-msvideo",
|
|
113
|
-
azw: "application/vnd.amazon.ebook",
|
|
114
|
-
bin: "application/octet-stream",
|
|
115
|
-
bmp: "image/bmp",
|
|
116
|
-
bz: "application/x-bzip",
|
|
117
|
-
bz2: "application/x-bzip2",
|
|
118
|
-
cda: "application/x-cdf",
|
|
119
|
-
csh: "application/x-csh",
|
|
120
|
-
css: "text/css",
|
|
121
|
-
csv: "text/csv",
|
|
122
|
-
doc: "application/msword",
|
|
123
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
124
|
-
eot: "application/vnd.ms-fontobject",
|
|
125
|
-
epub: "application/epub+zip",
|
|
126
|
-
gz: "application/gzip",
|
|
127
|
-
gif: "image/gif",
|
|
128
|
-
htm: "text/html",
|
|
129
|
-
html: "text/html",
|
|
130
|
-
ico: "image/vnd.microsoft.icon",
|
|
131
|
-
ics: "text/calendar",
|
|
132
|
-
jar: "application/java-archive",
|
|
133
|
-
jpeg: "image/jpeg",
|
|
134
|
-
jpg: "image/jpeg",
|
|
135
|
-
js: "text/javascript",
|
|
136
|
-
json: "application/json",
|
|
137
|
-
jsonld: "application/ld+json",
|
|
138
|
-
md: "text/markdown",
|
|
139
|
-
mid: "audio/midi",
|
|
140
|
-
midi: "audio/midi",
|
|
141
|
-
mjs: "text/javascript",
|
|
142
|
-
mp3: "audio/mpeg",
|
|
143
|
-
mp4: "video/mp4",
|
|
144
|
-
mpeg: "video/mpeg",
|
|
145
|
-
mpkg: "application/vnd.apple.installer+xml",
|
|
146
|
-
odp: "application/vnd.oasis.opendocument.presentation",
|
|
147
|
-
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
148
|
-
odt: "application/vnd.oasis.opendocument.text",
|
|
149
|
-
oga: "audio/ogg",
|
|
150
|
-
ogv: "video/ogg",
|
|
151
|
-
ogx: "application/ogg",
|
|
152
|
-
opus: "audio/opus",
|
|
153
|
-
otf: "font/otf",
|
|
154
|
-
png: "image/png",
|
|
155
|
-
pdf: "application/pdf",
|
|
156
|
-
php: "application/x-httpd-php",
|
|
157
|
-
ppt: "application/vnd.ms-powerpoint",
|
|
158
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
159
|
-
rar: "application/vnd.rar",
|
|
160
|
-
rtf: "application/rtf",
|
|
161
|
-
sh: "application/x-sh",
|
|
162
|
-
svg: "image/svg+xml",
|
|
163
|
-
tar: "application/x-tar",
|
|
164
|
-
text: "text/plain",
|
|
165
|
-
tif: "image/tiff",
|
|
166
|
-
tiff: "image/tiff",
|
|
167
|
-
ts: "video/mp2t",
|
|
168
|
-
ttf: "font/ttf",
|
|
169
|
-
txt: "text/plain",
|
|
170
|
-
vsd: "application/vnd.visio",
|
|
171
|
-
wav: "audio/wav",
|
|
172
|
-
weba: "audio/webm",
|
|
173
|
-
webm: "video/webm",
|
|
174
|
-
webp: "image/webp",
|
|
175
|
-
woff: "font/woff",
|
|
176
|
-
woff2: "font/woff2",
|
|
177
|
-
xhtml: "application/xhtml+xml",
|
|
178
|
-
xls: "application/vnd.ms-excel",
|
|
179
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
180
|
-
xml: "application/xml",
|
|
181
|
-
xul: "application/vnd.mozilla.xul+xml",
|
|
182
|
-
zip: "application/zip",
|
|
183
|
-
"3gp": "video/3gpp",
|
|
184
|
-
"3g2": "video/3gpp2",
|
|
185
|
-
"7z": "application/x-7z-compressed"
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// src/helpers/bucket.ts
|
|
189
|
-
function localBucket(root) {
|
|
190
|
-
const base = path.resolve(root);
|
|
191
|
-
const resolveKey = (name) => {
|
|
192
|
-
if (!name) throw new Error("File name is required");
|
|
193
|
-
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
194
|
-
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
195
|
-
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
196
|
-
}
|
|
197
|
-
return full;
|
|
198
|
-
};
|
|
199
|
-
const file2 = (name, win) => {
|
|
200
|
-
const full = resolveKey(name);
|
|
201
|
-
const type2 = mimes_default[path.extname(name).slice(1).toLowerCase()];
|
|
202
|
-
const read = () => {
|
|
203
|
-
let opts;
|
|
204
|
-
if (win) {
|
|
205
|
-
opts = { start: win.start };
|
|
206
|
-
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
207
|
-
}
|
|
208
|
-
const nodeStream = fs.createReadStream(full, opts);
|
|
209
|
-
return new ReadableStream({
|
|
210
|
-
start(controller) {
|
|
211
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
212
|
-
nodeStream.on("end", () => controller.close());
|
|
213
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
214
|
-
},
|
|
215
|
-
cancel() {
|
|
216
|
-
nodeStream.destroy();
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
};
|
|
220
|
-
return {
|
|
221
|
-
path: full,
|
|
222
|
-
id: name.replace(/^\/+/, ""),
|
|
223
|
-
name: path.basename(name),
|
|
224
|
-
type: type2,
|
|
225
|
-
async exists() {
|
|
226
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
227
|
-
return !!stats?.isFile();
|
|
228
|
-
},
|
|
229
|
-
async info() {
|
|
230
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
231
|
-
const exists = !!stats?.isFile();
|
|
232
|
-
const total = stats?.size ?? 0;
|
|
233
|
-
const size = win ? Math.max(0, Math.min(win.end, total) - win.start) : total;
|
|
234
|
-
return { exists, size, date: stats?.mtime ?? null, type: type2 };
|
|
235
|
-
},
|
|
236
|
-
// Read-only view of [start, end), composed relative to the current window.
|
|
237
|
-
slice(start, end) {
|
|
238
|
-
const base2 = win?.start ?? 0;
|
|
239
|
-
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
240
|
-
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
241
|
-
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
242
|
-
return file2(name, { start: s, end: e });
|
|
243
|
-
},
|
|
244
|
-
async write(content) {
|
|
245
|
-
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
246
|
-
if (content instanceof ReadableStream) {
|
|
247
|
-
const writable = fs.createWriteStream(full);
|
|
248
|
-
for await (const chunk of content) {
|
|
249
|
-
writable.write(chunk);
|
|
250
|
-
}
|
|
251
|
-
await new Promise((resolve2, reject) => {
|
|
252
|
-
writable.on("error", reject);
|
|
253
|
-
writable.end(() => resolve2());
|
|
254
|
-
});
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
await fsp.writeFile(full, content);
|
|
258
|
-
},
|
|
259
|
-
stream() {
|
|
260
|
-
return read();
|
|
261
|
-
},
|
|
262
|
-
async bytes() {
|
|
263
|
-
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
264
|
-
return new Uint8Array(await fsp.readFile(full));
|
|
265
|
-
},
|
|
266
|
-
async remove() {
|
|
267
|
-
await fsp.unlink(full).catch(() => {
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
};
|
|
272
|
-
return {
|
|
273
|
-
file: file2,
|
|
274
|
-
folder: (prefix) => localBucket(path.join(base, prefix))
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
function bucket(root) {
|
|
278
|
-
if (!root) return null;
|
|
279
|
-
if (typeof root === "string") return localBucket(root);
|
|
280
|
-
if (typeof root.file === "function") return root;
|
|
281
|
-
throw new Error(
|
|
282
|
-
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
106
|
// src/helpers/createId.ts
|
|
287
107
|
var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
288
108
|
var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
@@ -349,52 +169,36 @@ async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
|
349
169
|
await file2.write(data, { type: contentType });
|
|
350
170
|
return {
|
|
351
171
|
name: originalName,
|
|
352
|
-
id,
|
|
353
172
|
path: file2.path,
|
|
354
173
|
type: contentType,
|
|
355
174
|
size: data.length
|
|
356
175
|
};
|
|
357
176
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
177
|
+
function validateFile(originalName, data, contentType, limits) {
|
|
178
|
+
const { maxSize, minSize, fileType } = limits;
|
|
179
|
+
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
182
|
+
);
|
|
363
183
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
184
|
+
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
187
|
+
);
|
|
367
188
|
}
|
|
368
|
-
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
189
|
+
if (fileType && fileType.length > 0) {
|
|
190
|
+
const ext2 = getExt(originalName);
|
|
191
|
+
const mime = contentType.toLowerCase();
|
|
192
|
+
const allowed = fileType.some(
|
|
193
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
194
|
+
);
|
|
195
|
+
if (!allowed) {
|
|
376
196
|
throw new Error(
|
|
377
|
-
`File "${originalName}"
|
|
197
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
378
198
|
);
|
|
379
199
|
}
|
|
380
|
-
if (fileType && fileType.length > 0) {
|
|
381
|
-
const ext2 = getExt(originalName);
|
|
382
|
-
const mime = contentType.toLowerCase();
|
|
383
|
-
const allowed = fileType.some(
|
|
384
|
-
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
385
|
-
);
|
|
386
|
-
if (!allowed) {
|
|
387
|
-
throw new Error(
|
|
388
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (!this._bucket) {
|
|
393
|
-
throw new Error(`No upload destination configured (missing bucket)`);
|
|
394
|
-
}
|
|
395
|
-
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
396
200
|
}
|
|
397
|
-
}
|
|
201
|
+
}
|
|
398
202
|
|
|
399
203
|
// src/helpers/bodyLimit.ts
|
|
400
204
|
var INF = Number.POSITIVE_INFINITY;
|
|
@@ -416,6 +220,88 @@ var tooLarge = (max) => new StatusError(
|
|
|
416
220
|
413
|
|
417
221
|
);
|
|
418
222
|
|
|
223
|
+
// src/helpers/mimes.ts
|
|
224
|
+
var mimes_default = {
|
|
225
|
+
aac: "audio/aac",
|
|
226
|
+
abw: "application/x-abiword",
|
|
227
|
+
arc: "application/x-freearc",
|
|
228
|
+
avif: "image/avif",
|
|
229
|
+
avi: "video/x-msvideo",
|
|
230
|
+
azw: "application/vnd.amazon.ebook",
|
|
231
|
+
bin: "application/octet-stream",
|
|
232
|
+
bmp: "image/bmp",
|
|
233
|
+
bz: "application/x-bzip",
|
|
234
|
+
bz2: "application/x-bzip2",
|
|
235
|
+
cda: "application/x-cdf",
|
|
236
|
+
csh: "application/x-csh",
|
|
237
|
+
css: "text/css; charset=utf-8",
|
|
238
|
+
csv: "text/csv; charset=utf-8",
|
|
239
|
+
doc: "application/msword",
|
|
240
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
241
|
+
eot: "application/vnd.ms-fontobject",
|
|
242
|
+
epub: "application/epub+zip",
|
|
243
|
+
gz: "application/gzip",
|
|
244
|
+
gif: "image/gif",
|
|
245
|
+
htm: "text/html; charset=utf-8",
|
|
246
|
+
html: "text/html; charset=utf-8",
|
|
247
|
+
ico: "image/vnd.microsoft.icon",
|
|
248
|
+
ics: "text/calendar; charset=utf-8",
|
|
249
|
+
jar: "application/java-archive",
|
|
250
|
+
jpeg: "image/jpeg",
|
|
251
|
+
jpg: "image/jpeg",
|
|
252
|
+
js: "text/javascript; charset=utf-8",
|
|
253
|
+
json: "application/json",
|
|
254
|
+
jsonld: "application/ld+json",
|
|
255
|
+
md: "text/markdown; charset=utf-8",
|
|
256
|
+
mid: "audio/midi",
|
|
257
|
+
midi: "audio/midi",
|
|
258
|
+
mjs: "text/javascript; charset=utf-8",
|
|
259
|
+
mp3: "audio/mpeg",
|
|
260
|
+
mp4: "video/mp4",
|
|
261
|
+
mpeg: "video/mpeg",
|
|
262
|
+
mpkg: "application/vnd.apple.installer+xml",
|
|
263
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
264
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
265
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
266
|
+
oga: "audio/ogg",
|
|
267
|
+
ogv: "video/ogg",
|
|
268
|
+
ogx: "application/ogg",
|
|
269
|
+
opus: "audio/opus",
|
|
270
|
+
otf: "font/otf",
|
|
271
|
+
png: "image/png",
|
|
272
|
+
pdf: "application/pdf",
|
|
273
|
+
php: "application/x-httpd-php",
|
|
274
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
275
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
276
|
+
rar: "application/vnd.rar",
|
|
277
|
+
rtf: "application/rtf",
|
|
278
|
+
sh: "application/x-sh",
|
|
279
|
+
svg: "image/svg+xml",
|
|
280
|
+
tar: "application/x-tar",
|
|
281
|
+
text: "text/plain; charset=utf-8",
|
|
282
|
+
tif: "image/tiff",
|
|
283
|
+
tiff: "image/tiff",
|
|
284
|
+
ts: "video/mp2t",
|
|
285
|
+
ttf: "font/ttf",
|
|
286
|
+
txt: "text/plain; charset=utf-8",
|
|
287
|
+
vsd: "application/vnd.visio",
|
|
288
|
+
wav: "audio/wav",
|
|
289
|
+
weba: "audio/webm",
|
|
290
|
+
webm: "video/webm",
|
|
291
|
+
webp: "image/webp",
|
|
292
|
+
woff: "font/woff",
|
|
293
|
+
woff2: "font/woff2",
|
|
294
|
+
xhtml: "application/xhtml+xml",
|
|
295
|
+
xls: "application/vnd.ms-excel",
|
|
296
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
297
|
+
xml: "application/xml",
|
|
298
|
+
xul: "application/vnd.mozilla.xul+xml",
|
|
299
|
+
zip: "application/zip",
|
|
300
|
+
"3gp": "video/3gpp",
|
|
301
|
+
"3g2": "video/3gpp2",
|
|
302
|
+
"7z": "application/x-7z-compressed"
|
|
303
|
+
};
|
|
304
|
+
|
|
419
305
|
// src/helpers/parseBody.ts
|
|
420
306
|
function getBoundary(header) {
|
|
421
307
|
if (!header) return null;
|
|
@@ -494,15 +380,15 @@ function addField(body, name, value) {
|
|
|
494
380
|
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
495
381
|
body[name].push(value);
|
|
496
382
|
}
|
|
497
|
-
function startPart(headerStr,
|
|
383
|
+
function startPart(headerStr, bucket2, limits) {
|
|
498
384
|
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
499
385
|
if (!name) return { kind: "skip" };
|
|
500
386
|
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
501
387
|
if (!filename) return { kind: "text", name, chunks: [] };
|
|
502
388
|
const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
503
|
-
if (!
|
|
504
|
-
if (
|
|
505
|
-
return { kind: "
|
|
389
|
+
if (!bucket2) return { kind: "drop" };
|
|
390
|
+
if (limits) {
|
|
391
|
+
return { kind: "validated", name, filename, type: type2, bucket: bucket2, limits, chunks: [] };
|
|
506
392
|
}
|
|
507
393
|
const id = `${createId()}${getExt(filename)}`;
|
|
508
394
|
let controller;
|
|
@@ -511,7 +397,7 @@ function startPart(headerStr, dest) {
|
|
|
511
397
|
controller = c;
|
|
512
398
|
}
|
|
513
399
|
});
|
|
514
|
-
const file2 =
|
|
400
|
+
const file2 = bucket2.file(id);
|
|
515
401
|
return {
|
|
516
402
|
kind: "file",
|
|
517
403
|
name,
|
|
@@ -526,7 +412,7 @@ function startPart(headerStr, dest) {
|
|
|
526
412
|
}
|
|
527
413
|
function feedPart(part, data) {
|
|
528
414
|
if (data.length === 0) return;
|
|
529
|
-
if (part.kind === "text" || part.kind === "
|
|
415
|
+
if (part.kind === "text" || part.kind === "validated") part.chunks.push(data);
|
|
530
416
|
else if (part.kind === "file") {
|
|
531
417
|
part.controller.enqueue(data);
|
|
532
418
|
part.size += data.length;
|
|
@@ -537,16 +423,16 @@ async function endPart(part, body) {
|
|
|
537
423
|
const buf = Buffer.concat(part.chunks);
|
|
538
424
|
const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
|
|
539
425
|
addField(body, part.name, value);
|
|
540
|
-
} else if (part.kind === "
|
|
426
|
+
} else if (part.kind === "validated") {
|
|
541
427
|
const buf = Buffer.concat(part.chunks);
|
|
542
|
-
|
|
428
|
+
validateFile(part.filename, buf, part.type, part.limits);
|
|
429
|
+
const ref = await saveFileToBucket(part.filename, buf, part.bucket, part.type);
|
|
543
430
|
addField(body, part.name, ref);
|
|
544
431
|
} else if (part.kind === "file") {
|
|
545
432
|
part.controller.close();
|
|
546
433
|
await part.write;
|
|
547
434
|
addField(body, part.name, {
|
|
548
435
|
name: part.filename,
|
|
549
|
-
id: part.id,
|
|
550
436
|
path: part.file.path,
|
|
551
437
|
type: part.type,
|
|
552
438
|
size: part.size
|
|
@@ -554,7 +440,7 @@ async function endPart(part, body) {
|
|
|
554
440
|
}
|
|
555
441
|
}
|
|
556
442
|
var BREAK = Buffer.from("\r\n\r\n");
|
|
557
|
-
async function parseMultipart(stream, boundary,
|
|
443
|
+
async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
|
|
558
444
|
const delim = Buffer.from(`\r
|
|
559
445
|
--${boundary}`);
|
|
560
446
|
const body = {};
|
|
@@ -591,7 +477,7 @@ async function parseMultipart(stream, boundary, dest, max = INF) {
|
|
|
591
477
|
} else if (state === "headers") {
|
|
592
478
|
const i = buf.indexOf(BREAK);
|
|
593
479
|
if (i === -1) break;
|
|
594
|
-
part = startPart(buf.subarray(0, i).toString("utf-8"),
|
|
480
|
+
part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits);
|
|
595
481
|
buf = buf.subarray(i + BREAK.length);
|
|
596
482
|
state = "body";
|
|
597
483
|
advanced = true;
|
|
@@ -637,12 +523,25 @@ async function streamToBucket(stream, type2, bucket2) {
|
|
|
637
523
|
controller.close();
|
|
638
524
|
await write;
|
|
639
525
|
if (!size) return void 0;
|
|
640
|
-
return { name: id,
|
|
526
|
+
return { name: id, path: file2.path, type: type2, size };
|
|
641
527
|
}
|
|
642
528
|
async function parseBody(input, contentType, dest, max = INF) {
|
|
643
529
|
const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
530
|
+
let bucket2;
|
|
531
|
+
let limits;
|
|
532
|
+
if (dest && "bucket" in dest) {
|
|
533
|
+
bucket2 = dest.bucket;
|
|
534
|
+
const { maxSize, minSize, fileType } = dest;
|
|
535
|
+
if (maxSize != null || minSize != null || fileType != null) {
|
|
536
|
+
limits = { maxSize, minSize, fileType };
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
bucket2 = dest;
|
|
540
|
+
}
|
|
644
541
|
const boundary = type2 && /multipart\/form-data/i.test(type2) ? getBoundary(type2) : null;
|
|
645
|
-
if (boundary)
|
|
542
|
+
if (boundary) {
|
|
543
|
+
return parseMultipart(toStream(input), boundary, bucket2, limits, max);
|
|
544
|
+
}
|
|
646
545
|
if (!type2 || /^text\//i.test(type2)) {
|
|
647
546
|
const buf = await toBuffer(input, max);
|
|
648
547
|
return buf.length ? buf.toString("utf-8") : void 0;
|
|
@@ -655,15 +554,18 @@ async function parseBody(input, contentType, dest, max = INF) {
|
|
|
655
554
|
const buf = await toBuffer(input, max);
|
|
656
555
|
return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
|
|
657
556
|
}
|
|
658
|
-
if (!
|
|
557
|
+
if (!bucket2) {
|
|
659
558
|
const buf = await toBuffer(input, max);
|
|
660
559
|
return buf.length ? buf : void 0;
|
|
661
560
|
}
|
|
662
|
-
if (
|
|
561
|
+
if (limits) {
|
|
663
562
|
const buf = await toBuffer(input);
|
|
664
|
-
|
|
563
|
+
if (!buf.length) return void 0;
|
|
564
|
+
const name = `upload${extFromType(type2)}`;
|
|
565
|
+
validateFile(name, buf, type2, limits);
|
|
566
|
+
return saveFileToBucket(name, buf, bucket2, type2);
|
|
665
567
|
}
|
|
666
|
-
return streamToBucket(toStream(input), type2,
|
|
568
|
+
return streamToBucket(toStream(input), type2, bucket2);
|
|
667
569
|
}
|
|
668
570
|
|
|
669
571
|
// src/helpers/body.ts
|
|
@@ -760,7 +662,7 @@ function normalizeExpires(expires) {
|
|
|
760
662
|
function createCookies(key, val) {
|
|
761
663
|
if (val.value === null) val.expires = EXPIRED;
|
|
762
664
|
const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
763
|
-
let str = `${key}=${value
|
|
665
|
+
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path2 || "/"}`;
|
|
764
666
|
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
765
667
|
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
766
668
|
if (httpOnly) str += ";HttpOnly";
|
|
@@ -823,6 +725,27 @@ function clientIp(headers2, opts = {}) {
|
|
|
823
725
|
return normalize(remoteAddress);
|
|
824
726
|
}
|
|
825
727
|
|
|
728
|
+
// src/helpers/disposition.ts
|
|
729
|
+
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
730
|
+
/['()*]/g,
|
|
731
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
732
|
+
);
|
|
733
|
+
function disposition(name) {
|
|
734
|
+
if (!name) return "attachment";
|
|
735
|
+
const clean = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
|
|
736
|
+
if (!clean) return "attachment";
|
|
737
|
+
const ascii = clean.replace(/[^\x20-\x7e]/g, "?");
|
|
738
|
+
const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
|
|
739
|
+
if (clean === ascii) return value;
|
|
740
|
+
return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/helpers/isHtml.ts
|
|
744
|
+
var TAG = /^\s*<[a-zA-Z!/]/;
|
|
745
|
+
function isHtml(body) {
|
|
746
|
+
return TAG.test(body);
|
|
747
|
+
}
|
|
748
|
+
|
|
826
749
|
// src/helpers/isReadableStream.ts
|
|
827
750
|
function isReadableStream(obj) {
|
|
828
751
|
return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
|
|
@@ -850,8 +773,7 @@ var Reply = class {
|
|
|
850
773
|
download(name) {
|
|
851
774
|
const ext2 = name?.split(".").pop();
|
|
852
775
|
if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
|
|
853
|
-
|
|
854
|
-
return this.headers("content-disposition", `attachment${filename}`);
|
|
776
|
+
return this.headers("content-disposition", disposition(name));
|
|
855
777
|
}
|
|
856
778
|
headers(key, value) {
|
|
857
779
|
if (typeof key !== "string") {
|
|
@@ -859,10 +781,15 @@ var Reply = class {
|
|
|
859
781
|
return this;
|
|
860
782
|
}
|
|
861
783
|
if (Array.isArray(value)) {
|
|
862
|
-
|
|
784
|
+
this.res.headers.delete(key);
|
|
785
|
+
for (const val of value) this.res.headers.append(key, val);
|
|
863
786
|
return this;
|
|
864
787
|
}
|
|
865
|
-
|
|
788
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
789
|
+
this.res.headers.append(key, value);
|
|
790
|
+
} else {
|
|
791
|
+
this.res.headers.set(key, value);
|
|
792
|
+
}
|
|
866
793
|
return this;
|
|
867
794
|
}
|
|
868
795
|
cache(value) {
|
|
@@ -884,25 +811,31 @@ var Reply = class {
|
|
|
884
811
|
return this.headers("set-cookie", createCookies(key, value));
|
|
885
812
|
}
|
|
886
813
|
json(body) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
814
|
+
if (body === void 0) body = null;
|
|
815
|
+
if (!this.res.headers.get("content-type")) {
|
|
816
|
+
this.res.headers.set("content-type", "application/json");
|
|
817
|
+
}
|
|
818
|
+
return this.send(JSON.stringify(body));
|
|
890
819
|
}
|
|
891
820
|
redirect(path2) {
|
|
892
|
-
|
|
821
|
+
this.headers("location", path2);
|
|
822
|
+
if (this.res.status == null) this.res.status = 302;
|
|
823
|
+
return this.send();
|
|
893
824
|
}
|
|
894
825
|
async file(path2) {
|
|
895
826
|
if (typeof path2 !== "string") {
|
|
896
827
|
if (!await path2.exists()) return this.status(404).send();
|
|
897
828
|
return this.type(path2.type).send(path2.stream());
|
|
898
829
|
}
|
|
830
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path2)) return this.status(404).send();
|
|
899
831
|
try {
|
|
900
832
|
const fs2 = await import("fs");
|
|
901
833
|
const ext2 = path2.split(".").pop();
|
|
834
|
+
await fs2.promises.access(path2);
|
|
902
835
|
const stream = fs2.createReadStream(path2);
|
|
903
836
|
return this.type(ext2).send(stream);
|
|
904
837
|
} catch (error) {
|
|
905
|
-
if (error.code === "ENOENT") {
|
|
838
|
+
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
906
839
|
return this.status(404).send();
|
|
907
840
|
}
|
|
908
841
|
throw error;
|
|
@@ -913,10 +846,10 @@ var Reply = class {
|
|
|
913
846
|
if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
|
|
914
847
|
return new Response(null, { status: status2, headers: headers2 });
|
|
915
848
|
}
|
|
849
|
+
if (body === null) body = "";
|
|
916
850
|
if (typeof body === "string") {
|
|
917
851
|
if (!headers2.get("content-type")) {
|
|
918
|
-
|
|
919
|
-
headers2.set("content-type", isHtml ? "text/html" : "text/plain");
|
|
852
|
+
headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
|
|
920
853
|
}
|
|
921
854
|
if (!headers2.has("content-length")) {
|
|
922
855
|
headers2.set("content-length", String(Buffer.byteLength(body)));
|
|
@@ -1516,6 +1449,106 @@ function parseAuthOptions(auth2, all) {
|
|
|
1516
1449
|
};
|
|
1517
1450
|
}
|
|
1518
1451
|
|
|
1452
|
+
// src/helpers/bucket.ts
|
|
1453
|
+
import * as fs from "fs";
|
|
1454
|
+
import * as fsp from "fs/promises";
|
|
1455
|
+
import * as path from "path";
|
|
1456
|
+
function localBucket(root, prefix = "") {
|
|
1457
|
+
const base = path.resolve(root);
|
|
1458
|
+
const resolveKey = (name) => {
|
|
1459
|
+
if (!name) throw new Error("File name is required");
|
|
1460
|
+
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
1461
|
+
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
1462
|
+
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
1463
|
+
}
|
|
1464
|
+
return full;
|
|
1465
|
+
};
|
|
1466
|
+
const file2 = (name, win) => {
|
|
1467
|
+
const full = resolveKey(name);
|
|
1468
|
+
const key = prefix + name.replace(/^\/+/, "");
|
|
1469
|
+
const type2 = mimes_default[path.extname(name).slice(1).toLowerCase()];
|
|
1470
|
+
const read = () => {
|
|
1471
|
+
let opts;
|
|
1472
|
+
if (win) {
|
|
1473
|
+
opts = { start: win.start };
|
|
1474
|
+
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
1475
|
+
}
|
|
1476
|
+
const nodeStream = fs.createReadStream(full, opts);
|
|
1477
|
+
return new ReadableStream({
|
|
1478
|
+
start(controller) {
|
|
1479
|
+
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
1480
|
+
nodeStream.on("end", () => controller.close());
|
|
1481
|
+
nodeStream.on("error", (err) => controller.error(err));
|
|
1482
|
+
},
|
|
1483
|
+
cancel() {
|
|
1484
|
+
nodeStream.destroy();
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
};
|
|
1488
|
+
return {
|
|
1489
|
+
path: key,
|
|
1490
|
+
name: path.basename(name),
|
|
1491
|
+
type: type2,
|
|
1492
|
+
async exists() {
|
|
1493
|
+
const stats = await fsp.stat(full).catch(() => null);
|
|
1494
|
+
return !!stats?.isFile();
|
|
1495
|
+
},
|
|
1496
|
+
async info() {
|
|
1497
|
+
const stats = await fsp.stat(full).catch(() => null);
|
|
1498
|
+
if (!stats?.isFile()) return null;
|
|
1499
|
+
const size = win ? Math.max(0, Math.min(win.end, stats.size) - win.start) : stats.size;
|
|
1500
|
+
return { size, type: type2 ?? null, modified: stats.mtime };
|
|
1501
|
+
},
|
|
1502
|
+
// Read-only view of [start, end), composed relative to the current window.
|
|
1503
|
+
slice(start, end) {
|
|
1504
|
+
const base2 = win?.start ?? 0;
|
|
1505
|
+
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
1506
|
+
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
1507
|
+
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
1508
|
+
return file2(name, { start: s, end: e });
|
|
1509
|
+
},
|
|
1510
|
+
async write(content) {
|
|
1511
|
+
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
1512
|
+
if (content instanceof ReadableStream) {
|
|
1513
|
+
const writable = fs.createWriteStream(full);
|
|
1514
|
+
for await (const chunk of content) {
|
|
1515
|
+
writable.write(chunk);
|
|
1516
|
+
}
|
|
1517
|
+
await new Promise((resolve2, reject) => {
|
|
1518
|
+
writable.on("error", reject);
|
|
1519
|
+
writable.end(() => resolve2());
|
|
1520
|
+
});
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
await fsp.writeFile(full, content);
|
|
1524
|
+
},
|
|
1525
|
+
stream() {
|
|
1526
|
+
return read();
|
|
1527
|
+
},
|
|
1528
|
+
async bytes() {
|
|
1529
|
+
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
1530
|
+
return new Uint8Array(await fsp.readFile(full));
|
|
1531
|
+
},
|
|
1532
|
+
async remove() {
|
|
1533
|
+
await fsp.unlink(full).catch(() => {
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
};
|
|
1538
|
+
return {
|
|
1539
|
+
file: file2,
|
|
1540
|
+
folder: (sub) => localBucket(path.join(base, sub), `${prefix}${sub.replace(/^\/+|\/+$/g, "")}/`)
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
function bucket(root) {
|
|
1544
|
+
if (!root) return null;
|
|
1545
|
+
if (typeof root === "string") return localBucket(root);
|
|
1546
|
+
if (typeof root.file === "function") return root;
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1519
1552
|
// src/helpers/color.ts
|
|
1520
1553
|
var map = {
|
|
1521
1554
|
reset: 0,
|
|
@@ -1651,10 +1684,23 @@ function resolveSecurity(security) {
|
|
|
1651
1684
|
}
|
|
1652
1685
|
return {
|
|
1653
1686
|
trustProxy: o.trustProxy ?? true,
|
|
1687
|
+
traversalProtection: off ? false : o.traversalProtection !== false,
|
|
1654
1688
|
headers: headers2,
|
|
1655
1689
|
hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
|
|
1656
1690
|
};
|
|
1657
1691
|
}
|
|
1692
|
+
var CLIMBS = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
1693
|
+
var ABSOLUTE = /^(?:[\\/]|[a-zA-Z]:)/;
|
|
1694
|
+
function checkTraversal(params, ctx) {
|
|
1695
|
+
if (!ctx.options.security?.traversalProtection) return;
|
|
1696
|
+
for (const param in params) {
|
|
1697
|
+
const value = params[param];
|
|
1698
|
+
if (typeof value !== "string") continue;
|
|
1699
|
+
if (CLIMBS.test(value) || ABSOLUTE.test(value)) {
|
|
1700
|
+
throw errors_default.PATH_TRAVERSAL({ param, value });
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1658
1704
|
function applySecurity(res, ctx) {
|
|
1659
1705
|
const security = ctx.options.security;
|
|
1660
1706
|
if (!security) return;
|
|
@@ -1727,10 +1773,11 @@ function config(options = {}) {
|
|
|
1727
1773
|
settings.uploads = null;
|
|
1728
1774
|
} else if (typeof up === "object" && "bucket" in up) {
|
|
1729
1775
|
const { bucket: bucket2, maxSize, minSize, fileType } = up;
|
|
1730
|
-
|
|
1731
|
-
|
|
1776
|
+
if (maxSize != null) parseBytes(maxSize);
|
|
1777
|
+
if (minSize != null) parseBytes(minSize);
|
|
1778
|
+
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType };
|
|
1732
1779
|
} else {
|
|
1733
|
-
settings.uploads = bucket(up);
|
|
1780
|
+
settings.uploads = { bucket: bucket(up) };
|
|
1734
1781
|
}
|
|
1735
1782
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1736
1783
|
if (favicon2) settings.favicon = favicon2;
|
|
@@ -1909,7 +1956,7 @@ async function parseResponse(out, ctx) {
|
|
|
1909
1956
|
out = new Response(void 0, { status: out });
|
|
1910
1957
|
}
|
|
1911
1958
|
if (typeof out === "string") {
|
|
1912
|
-
const type2 =
|
|
1959
|
+
const type2 = isHtml(out) ? mimes_default.html : mimes_default.text;
|
|
1913
1960
|
out = new Response(out, {
|
|
1914
1961
|
headers: {
|
|
1915
1962
|
"content-type": type2,
|
|
@@ -2075,6 +2122,7 @@ async function getResponse(app, ctx) {
|
|
|
2075
2122
|
if (Object.keys(route.options).length) {
|
|
2076
2123
|
ctx.options = { ...app.settings, ...route.options };
|
|
2077
2124
|
}
|
|
2125
|
+
checkTraversal(params, ctx);
|
|
2078
2126
|
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
2079
2127
|
for (const cb of route.fns) {
|
|
2080
2128
|
if (typeof cb === "function") {
|
|
@@ -2174,7 +2222,12 @@ function parseCookies(cookies2) {
|
|
|
2174
2222
|
return Object.fromEntries(
|
|
2175
2223
|
cookieStr.split(/;\s*/).map((part) => {
|
|
2176
2224
|
const [key, ...rest] = part.split("=");
|
|
2177
|
-
|
|
2225
|
+
const value = rest.join("=");
|
|
2226
|
+
try {
|
|
2227
|
+
return [key, decodeURIComponent(value)];
|
|
2228
|
+
} catch {
|
|
2229
|
+
return [key, value];
|
|
2230
|
+
}
|
|
2178
2231
|
})
|
|
2179
2232
|
);
|
|
2180
2233
|
}
|
|
@@ -2444,17 +2497,18 @@ async function assets(ctx) {
|
|
|
2444
2497
|
try {
|
|
2445
2498
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2446
2499
|
const file2 = ctx.options.public.file(key);
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2500
|
+
const info = file2.info?.bind(file2);
|
|
2501
|
+
const meta = info ? await info() : null;
|
|
2502
|
+
if (info ? !meta : !await file2.exists()) return;
|
|
2449
2503
|
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2450
2504
|
const ctype = meta?.type || ext2;
|
|
2451
2505
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2452
2506
|
let tag;
|
|
2453
2507
|
if (meta) {
|
|
2454
|
-
const stamp = meta.
|
|
2508
|
+
const stamp = meta.modified ? meta.modified.getTime() : 0;
|
|
2455
2509
|
tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2456
2510
|
headers2.etag = tag;
|
|
2457
|
-
if (meta.
|
|
2511
|
+
if (meta.modified) headers2["last-modified"] = meta.modified.toUTCString();
|
|
2458
2512
|
}
|
|
2459
2513
|
const canRange = !!(meta && file2.slice);
|
|
2460
2514
|
if (canRange) headers2["accept-ranges"] = "bytes";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
|
|
5
5
|
"homepage": "https://server-js.com/",
|
|
6
6
|
"repository": "github:franciscop/server-next",
|
|
@@ -44,14 +44,31 @@
|
|
|
44
44
|
"home": "./docs/index.html",
|
|
45
45
|
"menu": {
|
|
46
46
|
"Documentation": "/documentation",
|
|
47
|
+
"Tutorials": "/tutorials",
|
|
47
48
|
"Github": "https://github.com/franciscop/server-next"
|
|
48
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"documentation": {
|
|
51
|
+
"Documentation": [
|
|
52
|
+
"docs/0. Documentation.md",
|
|
53
|
+
"docs/1. Getting Started.md"
|
|
54
|
+
],
|
|
55
|
+
"Guides": "docs/2. Guides.md",
|
|
56
|
+
"Options": "docs/3. Options.md",
|
|
57
|
+
"Router": "docs/4. Router.md",
|
|
58
|
+
"Context": "docs/5. Context.md",
|
|
59
|
+
"Reply": "docs/6. Reply.md",
|
|
60
|
+
"Authentication": "docs/7. Authentication.md",
|
|
61
|
+
"Testing": "docs/8. Testing.md",
|
|
62
|
+
"Platforms": "docs/9. Platforms.md",
|
|
63
|
+
"FAQ": "docs/A. FAQ.md"
|
|
64
|
+
},
|
|
65
|
+
"tutorials": "docs/tutorials"
|
|
49
66
|
},
|
|
50
67
|
"devDependencies": {
|
|
51
68
|
"@types/bun": "^1.3.0",
|
|
52
69
|
"@types/jest": "^30.0.0",
|
|
53
70
|
"@types/node": "^24.10.0",
|
|
54
|
-
"bucket": "^0.
|
|
71
|
+
"bucket": "^0.5.0",
|
|
55
72
|
"bun": "^1.3.13",
|
|
56
73
|
"check-dts": "^0.8.2",
|
|
57
74
|
"jest": "^29.7.0",
|