@server/next 0.37.0 → 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 +17 -21
- package/index.js +266 -193
- package/package.json +20 -3
package/index.d.ts
CHANGED
|
@@ -5,15 +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
|
-
store(bucket: Bucket | string): this;
|
|
14
|
-
processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
|
|
15
|
-
}
|
|
16
|
-
declare function upload(bucket?: Bucket | string | null): UploadPipeline;
|
|
17
8
|
|
|
18
9
|
type CookieOptions = string | string[] | Cookie | Cookie[] | null;
|
|
19
10
|
interface ResponseData {
|
|
@@ -26,12 +17,12 @@ declare class Reply$1 {
|
|
|
26
17
|
status(status: number): this;
|
|
27
18
|
type(type?: string): this;
|
|
28
19
|
download(name?: string): this;
|
|
29
|
-
headers(key: string | Record<string, string>, value?: string): this;
|
|
20
|
+
headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
|
|
30
21
|
cache(value: CacheOption): this;
|
|
31
22
|
cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
|
|
32
23
|
json(body: unknown): Response;
|
|
33
24
|
redirect(path: string): Response;
|
|
34
|
-
file(path: string): Promise<Response>;
|
|
25
|
+
file(path: string | BucketFile): Promise<Response>;
|
|
35
26
|
send(body?: string | Buffer | ReadableStream | any): Response;
|
|
36
27
|
}
|
|
37
28
|
type Params<K extends keyof Reply$1> = Reply$1[K] extends (...args: infer A) => any ? A : never;
|
|
@@ -90,17 +81,16 @@ type Cookie = {
|
|
|
90
81
|
};
|
|
91
82
|
type RouterMethod = "*" | Method;
|
|
92
83
|
type FileInfo = {
|
|
93
|
-
exists: boolean;
|
|
94
84
|
size: number;
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
type: string | null;
|
|
86
|
+
modified: Date;
|
|
97
87
|
};
|
|
98
88
|
type BucketFile = {
|
|
99
89
|
readonly path: string;
|
|
100
|
-
readonly id: string;
|
|
101
90
|
readonly name: string;
|
|
91
|
+
readonly type?: string;
|
|
102
92
|
exists(): Promise<boolean>;
|
|
103
|
-
info?(): Promise<FileInfo>;
|
|
93
|
+
info?(): Promise<FileInfo | null>;
|
|
104
94
|
write(content: string | Buffer | ReadableStream, options?: {
|
|
105
95
|
type?: string;
|
|
106
96
|
}): Promise<void>;
|
|
@@ -115,11 +105,13 @@ type Bucket = {
|
|
|
115
105
|
};
|
|
116
106
|
type UploadedFile = {
|
|
117
107
|
name: string;
|
|
118
|
-
id: string;
|
|
119
108
|
path: string;
|
|
120
109
|
type: string;
|
|
121
110
|
size: number;
|
|
122
111
|
};
|
|
112
|
+
type UploadOptions = LimitOptions & {
|
|
113
|
+
bucket: string | Bucket;
|
|
114
|
+
};
|
|
123
115
|
type CorsSettings = {
|
|
124
116
|
origin: string | boolean;
|
|
125
117
|
methods: string;
|
|
@@ -193,6 +185,7 @@ type SecurityOptions = {
|
|
|
193
185
|
referrerPolicy?: boolean | string;
|
|
194
186
|
hsts?: boolean | string;
|
|
195
187
|
xssProtection?: boolean;
|
|
188
|
+
traversalProtection?: boolean;
|
|
196
189
|
csp?: boolean | string;
|
|
197
190
|
coop?: boolean | string;
|
|
198
191
|
corp?: boolean | string;
|
|
@@ -200,6 +193,7 @@ type SecurityOptions = {
|
|
|
200
193
|
};
|
|
201
194
|
type SecuritySettings = {
|
|
202
195
|
trustProxy: boolean;
|
|
196
|
+
traversalProtection: boolean;
|
|
203
197
|
headers: Record<string, string>;
|
|
204
198
|
hsts: string | null;
|
|
205
199
|
};
|
|
@@ -209,7 +203,7 @@ type Options = {
|
|
|
209
203
|
port?: number;
|
|
210
204
|
secret?: string;
|
|
211
205
|
public?: string | Bucket;
|
|
212
|
-
uploads?: string | Bucket |
|
|
206
|
+
uploads?: string | Bucket | UploadOptions;
|
|
213
207
|
store?: KVStore;
|
|
214
208
|
cookies?: KVStore;
|
|
215
209
|
session?: KVStore | {
|
|
@@ -230,7 +224,9 @@ type Settings = {
|
|
|
230
224
|
port: number;
|
|
231
225
|
secret: string;
|
|
232
226
|
public?: Bucket;
|
|
233
|
-
uploads?:
|
|
227
|
+
uploads?: ({
|
|
228
|
+
bucket: Bucket;
|
|
229
|
+
} & LimitOptions) | null;
|
|
234
230
|
store?: KVStore;
|
|
235
231
|
cookies?: KVStore;
|
|
236
232
|
session?: {
|
|
@@ -301,7 +297,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
|
|
|
301
297
|
};
|
|
302
298
|
app: Server;
|
|
303
299
|
};
|
|
304
|
-
type InlineReply = Response | Reply | {
|
|
300
|
+
type InlineReply = Response | Reply | BucketFile | {
|
|
305
301
|
body: string;
|
|
306
302
|
headers?: Headers;
|
|
307
303
|
} | SerializableValue | JSX.Element | Buffer | ReadableStream;
|
|
@@ -504,4 +500,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
504
500
|
}
|
|
505
501
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
506
502
|
|
|
507
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type
|
|
503
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
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,105 +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
|
-
function localBucket(root) {
|
|
106
|
-
const base = path.resolve(root);
|
|
107
|
-
const resolveKey = (name) => {
|
|
108
|
-
if (!name) throw new Error("File name is required");
|
|
109
|
-
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
110
|
-
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
111
|
-
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
112
|
-
}
|
|
113
|
-
return full;
|
|
114
|
-
};
|
|
115
|
-
const file2 = (name, win) => {
|
|
116
|
-
const full = resolveKey(name);
|
|
117
|
-
const read = () => {
|
|
118
|
-
let opts;
|
|
119
|
-
if (win) {
|
|
120
|
-
opts = { start: win.start };
|
|
121
|
-
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
122
|
-
}
|
|
123
|
-
const nodeStream = fs.createReadStream(full, opts);
|
|
124
|
-
return new ReadableStream({
|
|
125
|
-
start(controller) {
|
|
126
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
127
|
-
nodeStream.on("end", () => controller.close());
|
|
128
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
129
|
-
},
|
|
130
|
-
cancel() {
|
|
131
|
-
nodeStream.destroy();
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
};
|
|
135
|
-
return {
|
|
136
|
-
path: full,
|
|
137
|
-
id: name.replace(/^\/+/, ""),
|
|
138
|
-
name: path.basename(name),
|
|
139
|
-
async exists() {
|
|
140
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
141
|
-
return !!stats?.isFile();
|
|
142
|
-
},
|
|
143
|
-
async info() {
|
|
144
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
145
|
-
const exists = !!stats?.isFile();
|
|
146
|
-
const total = stats?.size ?? 0;
|
|
147
|
-
const size = win ? Math.max(0, Math.min(win.end, total) - win.start) : total;
|
|
148
|
-
return { exists, size, date: stats?.mtime ?? null };
|
|
149
|
-
},
|
|
150
|
-
// Read-only view of [start, end), composed relative to the current window.
|
|
151
|
-
slice(start, end) {
|
|
152
|
-
const base2 = win?.start ?? 0;
|
|
153
|
-
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
154
|
-
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
155
|
-
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
156
|
-
return file2(name, { start: s, end: e });
|
|
157
|
-
},
|
|
158
|
-
async write(content) {
|
|
159
|
-
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
160
|
-
if (content instanceof ReadableStream) {
|
|
161
|
-
const writable = fs.createWriteStream(full);
|
|
162
|
-
for await (const chunk of content) {
|
|
163
|
-
writable.write(chunk);
|
|
164
|
-
}
|
|
165
|
-
await new Promise((resolve2, reject) => {
|
|
166
|
-
writable.on("error", reject);
|
|
167
|
-
writable.end(() => resolve2());
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
await fsp.writeFile(full, content);
|
|
172
|
-
},
|
|
173
|
-
stream() {
|
|
174
|
-
return read();
|
|
175
|
-
},
|
|
176
|
-
async bytes() {
|
|
177
|
-
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
178
|
-
return new Uint8Array(await fsp.readFile(full));
|
|
179
|
-
},
|
|
180
|
-
async remove() {
|
|
181
|
-
await fsp.unlink(full).catch(() => {
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
};
|
|
186
|
-
return {
|
|
187
|
-
file: file2,
|
|
188
|
-
folder: (prefix) => localBucket(path.join(base, prefix))
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function bucket(root) {
|
|
192
|
-
if (!root) return null;
|
|
193
|
-
if (typeof root === "string") return localBucket(root);
|
|
194
|
-
if (typeof root.file === "function") return root;
|
|
195
|
-
throw new Error(
|
|
196
|
-
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
106
|
// src/helpers/createId.ts
|
|
201
107
|
var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
202
108
|
var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
@@ -263,60 +169,35 @@ async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
|
263
169
|
await file2.write(data, { type: contentType });
|
|
264
170
|
return {
|
|
265
171
|
name: originalName,
|
|
266
|
-
id,
|
|
267
172
|
path: file2.path,
|
|
268
173
|
type: contentType,
|
|
269
174
|
size: data.length
|
|
270
175
|
};
|
|
271
176
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
limit(options) {
|
|
279
|
-
this._limits = { ...this._limits, ...options };
|
|
280
|
-
return this;
|
|
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
|
+
);
|
|
281
183
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
+
);
|
|
285
188
|
}
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
294
|
-
throw new Error(
|
|
295
|
-
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
if (fileType && fileType.length > 0) {
|
|
299
|
-
const ext2 = getExt(originalName);
|
|
300
|
-
const mime = contentType.toLowerCase();
|
|
301
|
-
const allowed = fileType.some(
|
|
302
|
-
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
303
|
-
);
|
|
304
|
-
if (!allowed) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (!this._bucket) {
|
|
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) {
|
|
311
196
|
throw new Error(
|
|
312
|
-
`
|
|
197
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
313
198
|
);
|
|
314
199
|
}
|
|
315
|
-
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
316
200
|
}
|
|
317
|
-
};
|
|
318
|
-
function upload(bucket2) {
|
|
319
|
-
return new UploadPipeline(bucket2);
|
|
320
201
|
}
|
|
321
202
|
|
|
322
203
|
// src/helpers/bodyLimit.ts
|
|
@@ -353,28 +234,28 @@ var mimes_default = {
|
|
|
353
234
|
bz2: "application/x-bzip2",
|
|
354
235
|
cda: "application/x-cdf",
|
|
355
236
|
csh: "application/x-csh",
|
|
356
|
-
css: "text/css",
|
|
357
|
-
csv: "text/csv",
|
|
237
|
+
css: "text/css; charset=utf-8",
|
|
238
|
+
csv: "text/csv; charset=utf-8",
|
|
358
239
|
doc: "application/msword",
|
|
359
240
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
360
241
|
eot: "application/vnd.ms-fontobject",
|
|
361
242
|
epub: "application/epub+zip",
|
|
362
243
|
gz: "application/gzip",
|
|
363
244
|
gif: "image/gif",
|
|
364
|
-
htm: "text/html",
|
|
365
|
-
html: "text/html",
|
|
245
|
+
htm: "text/html; charset=utf-8",
|
|
246
|
+
html: "text/html; charset=utf-8",
|
|
366
247
|
ico: "image/vnd.microsoft.icon",
|
|
367
|
-
ics: "text/calendar",
|
|
248
|
+
ics: "text/calendar; charset=utf-8",
|
|
368
249
|
jar: "application/java-archive",
|
|
369
250
|
jpeg: "image/jpeg",
|
|
370
251
|
jpg: "image/jpeg",
|
|
371
|
-
js: "text/javascript",
|
|
252
|
+
js: "text/javascript; charset=utf-8",
|
|
372
253
|
json: "application/json",
|
|
373
254
|
jsonld: "application/ld+json",
|
|
374
|
-
md: "text/markdown",
|
|
255
|
+
md: "text/markdown; charset=utf-8",
|
|
375
256
|
mid: "audio/midi",
|
|
376
257
|
midi: "audio/midi",
|
|
377
|
-
mjs: "text/javascript",
|
|
258
|
+
mjs: "text/javascript; charset=utf-8",
|
|
378
259
|
mp3: "audio/mpeg",
|
|
379
260
|
mp4: "video/mp4",
|
|
380
261
|
mpeg: "video/mpeg",
|
|
@@ -397,12 +278,12 @@ var mimes_default = {
|
|
|
397
278
|
sh: "application/x-sh",
|
|
398
279
|
svg: "image/svg+xml",
|
|
399
280
|
tar: "application/x-tar",
|
|
400
|
-
text: "text/plain",
|
|
281
|
+
text: "text/plain; charset=utf-8",
|
|
401
282
|
tif: "image/tiff",
|
|
402
283
|
tiff: "image/tiff",
|
|
403
284
|
ts: "video/mp2t",
|
|
404
285
|
ttf: "font/ttf",
|
|
405
|
-
txt: "text/plain",
|
|
286
|
+
txt: "text/plain; charset=utf-8",
|
|
406
287
|
vsd: "application/vnd.visio",
|
|
407
288
|
wav: "audio/wav",
|
|
408
289
|
weba: "audio/webm",
|
|
@@ -499,15 +380,15 @@ function addField(body, name, value) {
|
|
|
499
380
|
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
500
381
|
body[name].push(value);
|
|
501
382
|
}
|
|
502
|
-
function startPart(headerStr,
|
|
383
|
+
function startPart(headerStr, bucket2, limits) {
|
|
503
384
|
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
504
385
|
if (!name) return { kind: "skip" };
|
|
505
386
|
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
506
387
|
if (!filename) return { kind: "text", name, chunks: [] };
|
|
507
388
|
const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
508
|
-
if (!
|
|
509
|
-
if (
|
|
510
|
-
return { kind: "
|
|
389
|
+
if (!bucket2) return { kind: "drop" };
|
|
390
|
+
if (limits) {
|
|
391
|
+
return { kind: "validated", name, filename, type: type2, bucket: bucket2, limits, chunks: [] };
|
|
511
392
|
}
|
|
512
393
|
const id = `${createId()}${getExt(filename)}`;
|
|
513
394
|
let controller;
|
|
@@ -516,7 +397,7 @@ function startPart(headerStr, dest) {
|
|
|
516
397
|
controller = c;
|
|
517
398
|
}
|
|
518
399
|
});
|
|
519
|
-
const file2 =
|
|
400
|
+
const file2 = bucket2.file(id);
|
|
520
401
|
return {
|
|
521
402
|
kind: "file",
|
|
522
403
|
name,
|
|
@@ -531,7 +412,7 @@ function startPart(headerStr, dest) {
|
|
|
531
412
|
}
|
|
532
413
|
function feedPart(part, data) {
|
|
533
414
|
if (data.length === 0) return;
|
|
534
|
-
if (part.kind === "text" || part.kind === "
|
|
415
|
+
if (part.kind === "text" || part.kind === "validated") part.chunks.push(data);
|
|
535
416
|
else if (part.kind === "file") {
|
|
536
417
|
part.controller.enqueue(data);
|
|
537
418
|
part.size += data.length;
|
|
@@ -542,16 +423,16 @@ async function endPart(part, body) {
|
|
|
542
423
|
const buf = Buffer.concat(part.chunks);
|
|
543
424
|
const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
|
|
544
425
|
addField(body, part.name, value);
|
|
545
|
-
} else if (part.kind === "
|
|
426
|
+
} else if (part.kind === "validated") {
|
|
546
427
|
const buf = Buffer.concat(part.chunks);
|
|
547
|
-
|
|
428
|
+
validateFile(part.filename, buf, part.type, part.limits);
|
|
429
|
+
const ref = await saveFileToBucket(part.filename, buf, part.bucket, part.type);
|
|
548
430
|
addField(body, part.name, ref);
|
|
549
431
|
} else if (part.kind === "file") {
|
|
550
432
|
part.controller.close();
|
|
551
433
|
await part.write;
|
|
552
434
|
addField(body, part.name, {
|
|
553
435
|
name: part.filename,
|
|
554
|
-
id: part.id,
|
|
555
436
|
path: part.file.path,
|
|
556
437
|
type: part.type,
|
|
557
438
|
size: part.size
|
|
@@ -559,7 +440,7 @@ async function endPart(part, body) {
|
|
|
559
440
|
}
|
|
560
441
|
}
|
|
561
442
|
var BREAK = Buffer.from("\r\n\r\n");
|
|
562
|
-
async function parseMultipart(stream, boundary,
|
|
443
|
+
async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
|
|
563
444
|
const delim = Buffer.from(`\r
|
|
564
445
|
--${boundary}`);
|
|
565
446
|
const body = {};
|
|
@@ -596,7 +477,7 @@ async function parseMultipart(stream, boundary, dest, max = INF) {
|
|
|
596
477
|
} else if (state === "headers") {
|
|
597
478
|
const i = buf.indexOf(BREAK);
|
|
598
479
|
if (i === -1) break;
|
|
599
|
-
part = startPart(buf.subarray(0, i).toString("utf-8"),
|
|
480
|
+
part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits);
|
|
600
481
|
buf = buf.subarray(i + BREAK.length);
|
|
601
482
|
state = "body";
|
|
602
483
|
advanced = true;
|
|
@@ -642,12 +523,25 @@ async function streamToBucket(stream, type2, bucket2) {
|
|
|
642
523
|
controller.close();
|
|
643
524
|
await write;
|
|
644
525
|
if (!size) return void 0;
|
|
645
|
-
return { name: id,
|
|
526
|
+
return { name: id, path: file2.path, type: type2, size };
|
|
646
527
|
}
|
|
647
528
|
async function parseBody(input, contentType, dest, max = INF) {
|
|
648
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
|
+
}
|
|
649
541
|
const boundary = type2 && /multipart\/form-data/i.test(type2) ? getBoundary(type2) : null;
|
|
650
|
-
if (boundary)
|
|
542
|
+
if (boundary) {
|
|
543
|
+
return parseMultipart(toStream(input), boundary, bucket2, limits, max);
|
|
544
|
+
}
|
|
651
545
|
if (!type2 || /^text\//i.test(type2)) {
|
|
652
546
|
const buf = await toBuffer(input, max);
|
|
653
547
|
return buf.length ? buf.toString("utf-8") : void 0;
|
|
@@ -660,15 +554,18 @@ async function parseBody(input, contentType, dest, max = INF) {
|
|
|
660
554
|
const buf = await toBuffer(input, max);
|
|
661
555
|
return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
|
|
662
556
|
}
|
|
663
|
-
if (!
|
|
557
|
+
if (!bucket2) {
|
|
664
558
|
const buf = await toBuffer(input, max);
|
|
665
559
|
return buf.length ? buf : void 0;
|
|
666
560
|
}
|
|
667
|
-
if (
|
|
561
|
+
if (limits) {
|
|
668
562
|
const buf = await toBuffer(input);
|
|
669
|
-
|
|
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);
|
|
670
567
|
}
|
|
671
|
-
return streamToBucket(toStream(input), type2,
|
|
568
|
+
return streamToBucket(toStream(input), type2, bucket2);
|
|
672
569
|
}
|
|
673
570
|
|
|
674
571
|
// src/helpers/body.ts
|
|
@@ -765,7 +662,7 @@ function normalizeExpires(expires) {
|
|
|
765
662
|
function createCookies(key, val) {
|
|
766
663
|
if (val.value === null) val.expires = EXPIRED;
|
|
767
664
|
const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
768
|
-
let str = `${key}=${value
|
|
665
|
+
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path2 || "/"}`;
|
|
769
666
|
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
770
667
|
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
771
668
|
if (httpOnly) str += ";HttpOnly";
|
|
@@ -828,6 +725,27 @@ function clientIp(headers2, opts = {}) {
|
|
|
828
725
|
return normalize(remoteAddress);
|
|
829
726
|
}
|
|
830
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
|
+
|
|
831
749
|
// src/helpers/isReadableStream.ts
|
|
832
750
|
function isReadableStream(obj) {
|
|
833
751
|
return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
|
|
@@ -855,8 +773,7 @@ var Reply = class {
|
|
|
855
773
|
download(name) {
|
|
856
774
|
const ext2 = name?.split(".").pop();
|
|
857
775
|
if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
|
|
858
|
-
|
|
859
|
-
return this.headers("content-disposition", `attachment${filename}`);
|
|
776
|
+
return this.headers("content-disposition", disposition(name));
|
|
860
777
|
}
|
|
861
778
|
headers(key, value) {
|
|
862
779
|
if (typeof key !== "string") {
|
|
@@ -864,10 +781,15 @@ var Reply = class {
|
|
|
864
781
|
return this;
|
|
865
782
|
}
|
|
866
783
|
if (Array.isArray(value)) {
|
|
867
|
-
|
|
784
|
+
this.res.headers.delete(key);
|
|
785
|
+
for (const val of value) this.res.headers.append(key, val);
|
|
868
786
|
return this;
|
|
869
787
|
}
|
|
870
|
-
|
|
788
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
789
|
+
this.res.headers.append(key, value);
|
|
790
|
+
} else {
|
|
791
|
+
this.res.headers.set(key, value);
|
|
792
|
+
}
|
|
871
793
|
return this;
|
|
872
794
|
}
|
|
873
795
|
cache(value) {
|
|
@@ -889,21 +811,31 @@ var Reply = class {
|
|
|
889
811
|
return this.headers("set-cookie", createCookies(key, value));
|
|
890
812
|
}
|
|
891
813
|
json(body) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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));
|
|
895
819
|
}
|
|
896
820
|
redirect(path2) {
|
|
897
|
-
|
|
821
|
+
this.headers("location", path2);
|
|
822
|
+
if (this.res.status == null) this.res.status = 302;
|
|
823
|
+
return this.send();
|
|
898
824
|
}
|
|
899
825
|
async file(path2) {
|
|
826
|
+
if (typeof path2 !== "string") {
|
|
827
|
+
if (!await path2.exists()) return this.status(404).send();
|
|
828
|
+
return this.type(path2.type).send(path2.stream());
|
|
829
|
+
}
|
|
830
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path2)) return this.status(404).send();
|
|
900
831
|
try {
|
|
901
832
|
const fs2 = await import("fs");
|
|
902
833
|
const ext2 = path2.split(".").pop();
|
|
834
|
+
await fs2.promises.access(path2);
|
|
903
835
|
const stream = fs2.createReadStream(path2);
|
|
904
836
|
return this.type(ext2).send(stream);
|
|
905
837
|
} catch (error) {
|
|
906
|
-
if (error.code === "ENOENT") {
|
|
838
|
+
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
907
839
|
return this.status(404).send();
|
|
908
840
|
}
|
|
909
841
|
throw error;
|
|
@@ -914,10 +846,10 @@ var Reply = class {
|
|
|
914
846
|
if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
|
|
915
847
|
return new Response(null, { status: status2, headers: headers2 });
|
|
916
848
|
}
|
|
849
|
+
if (body === null) body = "";
|
|
917
850
|
if (typeof body === "string") {
|
|
918
851
|
if (!headers2.get("content-type")) {
|
|
919
|
-
|
|
920
|
-
headers2.set("content-type", isHtml ? "text/html" : "text/plain");
|
|
852
|
+
headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
|
|
921
853
|
}
|
|
922
854
|
if (!headers2.has("content-length")) {
|
|
923
855
|
headers2.set("content-length", String(Buffer.byteLength(body)));
|
|
@@ -925,7 +857,7 @@ var Reply = class {
|
|
|
925
857
|
return new Response(body, { status: status2, headers: headers2 });
|
|
926
858
|
}
|
|
927
859
|
const name = body?.constructor?.name;
|
|
928
|
-
if (
|
|
860
|
+
if (body instanceof Uint8Array) {
|
|
929
861
|
if (!headers2.has("content-length")) {
|
|
930
862
|
headers2.set("content-length", String(body.length));
|
|
931
863
|
}
|
|
@@ -1517,6 +1449,106 @@ function parseAuthOptions(auth2, all) {
|
|
|
1517
1449
|
};
|
|
1518
1450
|
}
|
|
1519
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
|
+
|
|
1520
1552
|
// src/helpers/color.ts
|
|
1521
1553
|
var map = {
|
|
1522
1554
|
reset: 0,
|
|
@@ -1652,10 +1684,23 @@ function resolveSecurity(security) {
|
|
|
1652
1684
|
}
|
|
1653
1685
|
return {
|
|
1654
1686
|
trustProxy: o.trustProxy ?? true,
|
|
1687
|
+
traversalProtection: off ? false : o.traversalProtection !== false,
|
|
1655
1688
|
headers: headers2,
|
|
1656
1689
|
hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
|
|
1657
1690
|
};
|
|
1658
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
|
+
}
|
|
1659
1704
|
function applySecurity(res, ctx) {
|
|
1660
1705
|
const security = ctx.options.security;
|
|
1661
1706
|
if (!security) return;
|
|
@@ -1721,9 +1766,21 @@ function config(options = {}) {
|
|
|
1721
1766
|
}
|
|
1722
1767
|
settings.cors = cors2;
|
|
1723
1768
|
}
|
|
1724
|
-
|
|
1725
|
-
settings.
|
|
1726
|
-
|
|
1769
|
+
const publicDir = options.public || env2.PUBLIC;
|
|
1770
|
+
settings.public = publicDir ? bucket(publicDir) : null;
|
|
1771
|
+
const up = options.uploads;
|
|
1772
|
+
if (!up) {
|
|
1773
|
+
settings.uploads = null;
|
|
1774
|
+
} else if (typeof up === "object" && "bucket" in up) {
|
|
1775
|
+
const { bucket: bucket2, maxSize, minSize, fileType } = up;
|
|
1776
|
+
if (maxSize != null) parseBytes(maxSize);
|
|
1777
|
+
if (minSize != null) parseBytes(minSize);
|
|
1778
|
+
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType };
|
|
1779
|
+
} else {
|
|
1780
|
+
settings.uploads = { bucket: bucket(up) };
|
|
1781
|
+
}
|
|
1782
|
+
const favicon2 = options.favicon || env2.FAVICON;
|
|
1783
|
+
if (favicon2) settings.favicon = favicon2;
|
|
1727
1784
|
settings.store = options.store ?? null;
|
|
1728
1785
|
settings.cookies = options.cookies ?? null;
|
|
1729
1786
|
if (options.session) {
|
|
@@ -1879,6 +1936,16 @@ async function parseResponse(out, ctx) {
|
|
|
1879
1936
|
if (out instanceof Blob) {
|
|
1880
1937
|
out = new Response(out, { headers: { "Content-Type": out.type } });
|
|
1881
1938
|
}
|
|
1939
|
+
if (out && typeof out.stream === "function" && typeof out.bytes === "function" && typeof out.exists === "function" && typeof out.name === "string") {
|
|
1940
|
+
if (!await out.exists()) {
|
|
1941
|
+
out = new Response(null, { status: 404 });
|
|
1942
|
+
} else {
|
|
1943
|
+
out = new Response(
|
|
1944
|
+
out.stream(),
|
|
1945
|
+
out.type ? { headers: { "content-type": out.type } } : void 0
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1882
1949
|
if (out instanceof ReadableStream) {
|
|
1883
1950
|
out = new Response(out);
|
|
1884
1951
|
}
|
|
@@ -1889,7 +1956,7 @@ async function parseResponse(out, ctx) {
|
|
|
1889
1956
|
out = new Response(void 0, { status: out });
|
|
1890
1957
|
}
|
|
1891
1958
|
if (typeof out === "string") {
|
|
1892
|
-
const type2 =
|
|
1959
|
+
const type2 = isHtml(out) ? mimes_default.html : mimes_default.text;
|
|
1893
1960
|
out = new Response(out, {
|
|
1894
1961
|
headers: {
|
|
1895
1962
|
"content-type": type2,
|
|
@@ -2055,6 +2122,7 @@ async function getResponse(app, ctx) {
|
|
|
2055
2122
|
if (Object.keys(route.options).length) {
|
|
2056
2123
|
ctx.options = { ...app.settings, ...route.options };
|
|
2057
2124
|
}
|
|
2125
|
+
checkTraversal(params, ctx);
|
|
2058
2126
|
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
2059
2127
|
for (const cb of route.fns) {
|
|
2060
2128
|
if (typeof cb === "function") {
|
|
@@ -2154,7 +2222,12 @@ function parseCookies(cookies2) {
|
|
|
2154
2222
|
return Object.fromEntries(
|
|
2155
2223
|
cookieStr.split(/;\s*/).map((part) => {
|
|
2156
2224
|
const [key, ...rest] = part.split("=");
|
|
2157
|
-
|
|
2225
|
+
const value = rest.join("=");
|
|
2226
|
+
try {
|
|
2227
|
+
return [key, decodeURIComponent(value)];
|
|
2228
|
+
} catch {
|
|
2229
|
+
return [key, value];
|
|
2230
|
+
}
|
|
2158
2231
|
})
|
|
2159
2232
|
);
|
|
2160
2233
|
}
|
|
@@ -2424,17 +2497,18 @@ async function assets(ctx) {
|
|
|
2424
2497
|
try {
|
|
2425
2498
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2426
2499
|
const file2 = ctx.options.public.file(key);
|
|
2427
|
-
const
|
|
2428
|
-
|
|
2500
|
+
const info = file2.info?.bind(file2);
|
|
2501
|
+
const meta = info ? await info() : null;
|
|
2502
|
+
if (info ? !meta : !await file2.exists()) return;
|
|
2429
2503
|
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2430
2504
|
const ctype = meta?.type || ext2;
|
|
2431
2505
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2432
2506
|
let tag;
|
|
2433
2507
|
if (meta) {
|
|
2434
|
-
const stamp = meta.
|
|
2508
|
+
const stamp = meta.modified ? meta.modified.getTime() : 0;
|
|
2435
2509
|
tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2436
2510
|
headers2.etag = tag;
|
|
2437
|
-
if (meta.
|
|
2511
|
+
if (meta.modified) headers2["last-modified"] = meta.modified.toUTCString();
|
|
2438
2512
|
}
|
|
2439
2513
|
const canRange = !!(meta && file2.slice);
|
|
2440
2514
|
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
@@ -3264,6 +3338,5 @@ export {
|
|
|
3264
3338
|
router,
|
|
3265
3339
|
send,
|
|
3266
3340
|
status,
|
|
3267
|
-
type
|
|
3268
|
-
upload
|
|
3341
|
+
type
|
|
3269
3342
|
};
|
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",
|