@server/next 0.37.1 → 0.39.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 +18 -23
- package/index.js +304 -322
- package/package.json +23 -4
- package/readme.md +28 -0
package/index.d.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
+
export { default as kv } from 'polystore';
|
|
3
|
+
export { default as bucket } from 'bucket';
|
|
2
4
|
|
|
3
5
|
type LimitOptions = {
|
|
4
6
|
maxSize?: number | string;
|
|
5
7
|
minSize?: number | string;
|
|
6
8
|
fileType?: string[];
|
|
7
9
|
};
|
|
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
10
|
|
|
16
11
|
type CookieOptions = string | string[] | Cookie | Cookie[] | null;
|
|
17
12
|
interface ResponseData {
|
|
@@ -24,7 +19,7 @@ declare class Reply$1 {
|
|
|
24
19
|
status(status: number): this;
|
|
25
20
|
type(type?: string): this;
|
|
26
21
|
download(name?: string): this;
|
|
27
|
-
headers(key: string | Record<string, string>, value?: string): this;
|
|
22
|
+
headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
|
|
28
23
|
cache(value: CacheOption): this;
|
|
29
24
|
cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
|
|
30
25
|
json(body: unknown): Response;
|
|
@@ -88,18 +83,16 @@ type Cookie = {
|
|
|
88
83
|
};
|
|
89
84
|
type RouterMethod = "*" | Method;
|
|
90
85
|
type FileInfo = {
|
|
91
|
-
exists: boolean;
|
|
92
86
|
size: number;
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
type: string | null;
|
|
88
|
+
modified: Date;
|
|
95
89
|
};
|
|
96
90
|
type BucketFile = {
|
|
97
91
|
readonly path: string;
|
|
98
|
-
readonly id: string;
|
|
99
92
|
readonly name: string;
|
|
100
93
|
readonly type?: string;
|
|
101
94
|
exists(): Promise<boolean>;
|
|
102
|
-
info?(): Promise<FileInfo>;
|
|
95
|
+
info?(): Promise<FileInfo | null>;
|
|
103
96
|
write(content: string | Buffer | ReadableStream, options?: {
|
|
104
97
|
type?: string;
|
|
105
98
|
}): Promise<void>;
|
|
@@ -114,7 +107,6 @@ type Bucket = {
|
|
|
114
107
|
};
|
|
115
108
|
type UploadedFile = {
|
|
116
109
|
name: string;
|
|
117
|
-
id: string;
|
|
118
110
|
path: string;
|
|
119
111
|
type: string;
|
|
120
112
|
size: number;
|
|
@@ -138,6 +130,7 @@ type BasicValue = string | number | boolean | null;
|
|
|
138
130
|
type SerializableValue = BasicValue | {
|
|
139
131
|
[key: string]: SerializableValue;
|
|
140
132
|
} | Array<SerializableValue>;
|
|
133
|
+
type StoreSource = KVStore | Map<string, any> | string | Record<string, any>;
|
|
141
134
|
type KVStore = {
|
|
142
135
|
name?: string;
|
|
143
136
|
prefix: (prefix?: string) => KVStore;
|
|
@@ -167,8 +160,8 @@ type AuthOption = `${Strategy}:${Provider}` | "key" | {
|
|
|
167
160
|
strategy: Strategy;
|
|
168
161
|
providers?: Provider | Provider[];
|
|
169
162
|
key?: string;
|
|
170
|
-
session?:
|
|
171
|
-
store?:
|
|
163
|
+
session?: StoreSource;
|
|
164
|
+
store?: StoreSource;
|
|
172
165
|
redirect?: string;
|
|
173
166
|
cleanUser?: <T = AuthUser>(user: T) => T | Promise<T>;
|
|
174
167
|
};
|
|
@@ -195,6 +188,7 @@ type SecurityOptions = {
|
|
|
195
188
|
referrerPolicy?: boolean | string;
|
|
196
189
|
hsts?: boolean | string;
|
|
197
190
|
xssProtection?: boolean;
|
|
191
|
+
traversalProtection?: boolean;
|
|
198
192
|
csp?: boolean | string;
|
|
199
193
|
coop?: boolean | string;
|
|
200
194
|
corp?: boolean | string;
|
|
@@ -202,6 +196,7 @@ type SecurityOptions = {
|
|
|
202
196
|
};
|
|
203
197
|
type SecuritySettings = {
|
|
204
198
|
trustProxy: boolean;
|
|
199
|
+
traversalProtection: boolean;
|
|
205
200
|
headers: Record<string, string>;
|
|
206
201
|
hsts: string | null;
|
|
207
202
|
};
|
|
@@ -212,10 +207,9 @@ type Options = {
|
|
|
212
207
|
secret?: string;
|
|
213
208
|
public?: string | Bucket;
|
|
214
209
|
uploads?: string | Bucket | UploadOptions;
|
|
215
|
-
store?:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
store: KVStore;
|
|
210
|
+
store?: StoreSource;
|
|
211
|
+
session?: StoreSource | {
|
|
212
|
+
store: StoreSource;
|
|
219
213
|
};
|
|
220
214
|
cors?: CorsOptions;
|
|
221
215
|
auth?: AuthOption;
|
|
@@ -232,9 +226,10 @@ type Settings = {
|
|
|
232
226
|
port: number;
|
|
233
227
|
secret: string;
|
|
234
228
|
public?: Bucket;
|
|
235
|
-
uploads?:
|
|
229
|
+
uploads?: ({
|
|
230
|
+
bucket: Bucket;
|
|
231
|
+
} & LimitOptions) | null;
|
|
236
232
|
store?: KVStore;
|
|
237
|
-
cookies?: KVStore;
|
|
238
233
|
session?: {
|
|
239
234
|
store: KVStore;
|
|
240
235
|
};
|
|
@@ -506,4 +501,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
506
501
|
}
|
|
507
502
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
508
503
|
|
|
509
|
-
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 };
|
|
504
|
+
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 StoreSource, 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,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: fileType2 } = 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 (fileType2 && fileType2.length > 0) {
|
|
190
|
+
const ext2 = getExt(originalName);
|
|
191
|
+
const mime = contentType.toLowerCase();
|
|
192
|
+
const allowed = fileType2.some(
|
|
193
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
194
|
+
);
|
|
195
|
+
if (!allowed) {
|
|
376
196
|
throw new Error(
|
|
377
|
-
`File "${originalName}"
|
|
378
|
-
);
|
|
379
|
-
}
|
|
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
|
|
197
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType2.join(", ")})`
|
|
385
198
|
);
|
|
386
|
-
if (!allowed) {
|
|
387
|
-
throw new Error(
|
|
388
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
199
|
}
|
|
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: fileType2 } = dest;
|
|
535
|
+
if (maxSize != null || minSize != null || fileType2 != null) {
|
|
536
|
+
limits = { maxSize, minSize, fileType: fileType2 };
|
|
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
|
|
@@ -759,8 +661,8 @@ function normalizeExpires(expires) {
|
|
|
759
661
|
}
|
|
760
662
|
function createCookies(key, val) {
|
|
761
663
|
if (val.value === null) val.expires = EXPIRED;
|
|
762
|
-
const { value, path
|
|
763
|
-
let str = `${key}=${value
|
|
664
|
+
const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
665
|
+
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
|
|
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,45 @@ function clientIp(headers2, opts = {}) {
|
|
|
823
725
|
return normalize(remoteAddress);
|
|
824
726
|
}
|
|
825
727
|
|
|
728
|
+
// src/helpers/store.ts
|
|
729
|
+
import kv from "polystore";
|
|
730
|
+
function toStore(source) {
|
|
731
|
+
const store = source;
|
|
732
|
+
if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
|
|
733
|
+
return store;
|
|
734
|
+
}
|
|
735
|
+
return kv(source);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/helpers/disposition.ts
|
|
739
|
+
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
740
|
+
/['()*]/g,
|
|
741
|
+
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
742
|
+
);
|
|
743
|
+
function disposition(name) {
|
|
744
|
+
if (!name) return "attachment";
|
|
745
|
+
const clean = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
|
|
746
|
+
if (!clean) return "attachment";
|
|
747
|
+
const ascii = clean.replace(/[^\x20-\x7e]/g, "?");
|
|
748
|
+
const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
|
|
749
|
+
if (clean === ascii) return value;
|
|
750
|
+
return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/helpers/fileType.ts
|
|
754
|
+
function fileType(file2) {
|
|
755
|
+
if (file2.type) return file2.type;
|
|
756
|
+
const name = file2.path || file2.name || "";
|
|
757
|
+
const ext2 = name.split(".").pop()?.toLowerCase();
|
|
758
|
+
return ext2 ? mimes_default[ext2] : void 0;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/helpers/isHtml.ts
|
|
762
|
+
var TAG = /^\s*<[a-zA-Z!/]/;
|
|
763
|
+
function isHtml(body) {
|
|
764
|
+
return TAG.test(body);
|
|
765
|
+
}
|
|
766
|
+
|
|
826
767
|
// src/helpers/isReadableStream.ts
|
|
827
768
|
function isReadableStream(obj) {
|
|
828
769
|
return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
|
|
@@ -850,8 +791,7 @@ var Reply = class {
|
|
|
850
791
|
download(name) {
|
|
851
792
|
const ext2 = name?.split(".").pop();
|
|
852
793
|
if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
|
|
853
|
-
|
|
854
|
-
return this.headers("content-disposition", `attachment${filename}`);
|
|
794
|
+
return this.headers("content-disposition", disposition(name));
|
|
855
795
|
}
|
|
856
796
|
headers(key, value) {
|
|
857
797
|
if (typeof key !== "string") {
|
|
@@ -859,10 +799,15 @@ var Reply = class {
|
|
|
859
799
|
return this;
|
|
860
800
|
}
|
|
861
801
|
if (Array.isArray(value)) {
|
|
862
|
-
|
|
802
|
+
this.res.headers.delete(key);
|
|
803
|
+
for (const val of value) this.res.headers.append(key, val);
|
|
863
804
|
return this;
|
|
864
805
|
}
|
|
865
|
-
|
|
806
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
807
|
+
this.res.headers.append(key, value);
|
|
808
|
+
} else {
|
|
809
|
+
this.res.headers.set(key, value);
|
|
810
|
+
}
|
|
866
811
|
return this;
|
|
867
812
|
}
|
|
868
813
|
cache(value) {
|
|
@@ -884,25 +829,31 @@ var Reply = class {
|
|
|
884
829
|
return this.headers("set-cookie", createCookies(key, value));
|
|
885
830
|
}
|
|
886
831
|
json(body) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
832
|
+
if (body === void 0) body = null;
|
|
833
|
+
if (!this.res.headers.get("content-type")) {
|
|
834
|
+
this.res.headers.set("content-type", "application/json");
|
|
835
|
+
}
|
|
836
|
+
return this.send(JSON.stringify(body));
|
|
890
837
|
}
|
|
891
|
-
redirect(
|
|
892
|
-
|
|
838
|
+
redirect(path) {
|
|
839
|
+
this.headers("location", path);
|
|
840
|
+
if (this.res.status == null) this.res.status = 302;
|
|
841
|
+
return this.send();
|
|
893
842
|
}
|
|
894
|
-
async file(
|
|
895
|
-
if (typeof
|
|
896
|
-
if (!await
|
|
897
|
-
return this.type(
|
|
843
|
+
async file(path) {
|
|
844
|
+
if (typeof path !== "string") {
|
|
845
|
+
if (!await path.exists()) return this.status(404).send();
|
|
846
|
+
return this.type(fileType(path)).send(path.stream());
|
|
898
847
|
}
|
|
848
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
|
|
899
849
|
try {
|
|
900
|
-
const
|
|
901
|
-
const ext2 =
|
|
902
|
-
|
|
850
|
+
const fs = await import("fs");
|
|
851
|
+
const ext2 = path.split(".").pop();
|
|
852
|
+
await fs.promises.access(path);
|
|
853
|
+
const stream = fs.createReadStream(path);
|
|
903
854
|
return this.type(ext2).send(stream);
|
|
904
855
|
} catch (error) {
|
|
905
|
-
if (error.code === "ENOENT") {
|
|
856
|
+
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
906
857
|
return this.status(404).send();
|
|
907
858
|
}
|
|
908
859
|
throw error;
|
|
@@ -913,10 +864,10 @@ var Reply = class {
|
|
|
913
864
|
if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
|
|
914
865
|
return new Response(null, { status: status2, headers: headers2 });
|
|
915
866
|
}
|
|
867
|
+
if (body === null) body = "";
|
|
916
868
|
if (typeof body === "string") {
|
|
917
869
|
if (!headers2.get("content-type")) {
|
|
918
|
-
|
|
919
|
-
headers2.set("content-type", isHtml ? "text/html" : "text/plain");
|
|
870
|
+
headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
|
|
920
871
|
}
|
|
921
872
|
if (!headers2.has("content-length")) {
|
|
922
873
|
headers2.set("content-length", String(Buffer.byteLength(body)));
|
|
@@ -1368,8 +1319,8 @@ var oauth = async (code) => {
|
|
|
1368
1319
|
code
|
|
1369
1320
|
})
|
|
1370
1321
|
});
|
|
1371
|
-
return (
|
|
1372
|
-
return fch(`https://api.github.com${
|
|
1322
|
+
return (path) => {
|
|
1323
|
+
return fch(`https://api.github.com${path}`, {
|
|
1373
1324
|
headers: { Authorization: `Bearer ${res.access_token}` }
|
|
1374
1325
|
});
|
|
1375
1326
|
};
|
|
@@ -1504,18 +1455,30 @@ function parseAuthOptions(auth2, all) {
|
|
|
1504
1455
|
if (!auth2.session && !all.store) {
|
|
1505
1456
|
throw new Error("Need a sessionStore store for Auth");
|
|
1506
1457
|
}
|
|
1507
|
-
const store =
|
|
1508
|
-
const
|
|
1458
|
+
const store = all.store ? toStore(all.store) : null;
|
|
1459
|
+
const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
|
|
1460
|
+
const sessionStore = auth2.session ? toStore(auth2.session) : store.prefix("auth:");
|
|
1509
1461
|
return {
|
|
1510
1462
|
strategy,
|
|
1511
1463
|
providers: list,
|
|
1512
1464
|
redirect: redirect2,
|
|
1513
1465
|
cleanUser,
|
|
1514
|
-
store,
|
|
1515
|
-
session:
|
|
1466
|
+
store: authStore,
|
|
1467
|
+
session: sessionStore
|
|
1516
1468
|
};
|
|
1517
1469
|
}
|
|
1518
1470
|
|
|
1471
|
+
// src/helpers/bucket.ts
|
|
1472
|
+
import FileSystem from "bucket/fs";
|
|
1473
|
+
function bucket(root) {
|
|
1474
|
+
if (!root) return null;
|
|
1475
|
+
if (typeof root === "string") return FileSystem(root);
|
|
1476
|
+
if (typeof root.file === "function") return root;
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1519
1482
|
// src/helpers/color.ts
|
|
1520
1483
|
var map = {
|
|
1521
1484
|
reset: 0,
|
|
@@ -1605,14 +1568,14 @@ function createLogger(level) {
|
|
|
1605
1568
|
const request = (ctx, res) => {
|
|
1606
1569
|
if (!enabled) return;
|
|
1607
1570
|
const method = ctx.method.toUpperCase();
|
|
1608
|
-
const
|
|
1571
|
+
const path = ctx.url.pathname;
|
|
1609
1572
|
const reqLen = Number(ctx.headers["content-length"]) || 0;
|
|
1610
1573
|
const resLen = Number(res.headers.get("content-length")) || 0;
|
|
1611
1574
|
const status2 = res.status;
|
|
1612
1575
|
const text = STATUS_TEXT[status2] || "";
|
|
1613
1576
|
const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
|
|
1614
1577
|
const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
|
|
1615
|
-
let line = `${method} ${
|
|
1578
|
+
let line = `${method} ${path}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
|
|
1616
1579
|
const location = res.headers.get("location");
|
|
1617
1580
|
if (location) line += ` \u2192 ${location}`;
|
|
1618
1581
|
message("api", line);
|
|
@@ -1651,10 +1614,23 @@ function resolveSecurity(security) {
|
|
|
1651
1614
|
}
|
|
1652
1615
|
return {
|
|
1653
1616
|
trustProxy: o.trustProxy ?? true,
|
|
1617
|
+
traversalProtection: off ? false : o.traversalProtection !== false,
|
|
1654
1618
|
headers: headers2,
|
|
1655
1619
|
hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
|
|
1656
1620
|
};
|
|
1657
1621
|
}
|
|
1622
|
+
var CLIMBS = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
1623
|
+
var ABSOLUTE = /^(?:[\\/]|[a-zA-Z]:)/;
|
|
1624
|
+
function checkTraversal(params, ctx) {
|
|
1625
|
+
if (!ctx.options.security?.traversalProtection) return;
|
|
1626
|
+
for (const param in params) {
|
|
1627
|
+
const value = params[param];
|
|
1628
|
+
if (typeof value !== "string") continue;
|
|
1629
|
+
if (CLIMBS.test(value) || ABSOLUTE.test(value)) {
|
|
1630
|
+
throw errors_default.PATH_TRAVERSAL({ param, value });
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1658
1634
|
function applySecurity(res, ctx) {
|
|
1659
1635
|
const security = ctx.options.security;
|
|
1660
1636
|
if (!security) return;
|
|
@@ -1726,21 +1702,22 @@ function config(options = {}) {
|
|
|
1726
1702
|
if (!up) {
|
|
1727
1703
|
settings.uploads = null;
|
|
1728
1704
|
} else if (typeof up === "object" && "bucket" in up) {
|
|
1729
|
-
const { bucket: bucket2, maxSize, minSize, fileType } = up;
|
|
1730
|
-
|
|
1731
|
-
|
|
1705
|
+
const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
|
|
1706
|
+
if (maxSize != null) parseBytes(maxSize);
|
|
1707
|
+
if (minSize != null) parseBytes(minSize);
|
|
1708
|
+
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
|
|
1732
1709
|
} else {
|
|
1733
|
-
settings.uploads = bucket(up);
|
|
1710
|
+
settings.uploads = { bucket: bucket(up) };
|
|
1734
1711
|
}
|
|
1735
1712
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1736
1713
|
if (favicon2) settings.favicon = favicon2;
|
|
1737
|
-
settings.store = options.store
|
|
1738
|
-
settings.cookies = options.cookies ?? null;
|
|
1714
|
+
settings.store = options.store ? toStore(options.store) : null;
|
|
1739
1715
|
if (options.session) {
|
|
1740
|
-
|
|
1716
|
+
const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
|
|
1717
|
+
settings.session = { store: toStore(store) };
|
|
1741
1718
|
}
|
|
1742
|
-
if (
|
|
1743
|
-
settings.session = { store:
|
|
1719
|
+
if (settings.store && !options.session) {
|
|
1720
|
+
settings.session = { store: settings.store.prefix("session:") };
|
|
1744
1721
|
}
|
|
1745
1722
|
if (options.auth || env2.AUTH) {
|
|
1746
1723
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
@@ -1893,9 +1870,10 @@ async function parseResponse(out, ctx) {
|
|
|
1893
1870
|
if (!await out.exists()) {
|
|
1894
1871
|
out = new Response(null, { status: 404 });
|
|
1895
1872
|
} else {
|
|
1873
|
+
const type2 = fileType(out);
|
|
1896
1874
|
out = new Response(
|
|
1897
1875
|
out.stream(),
|
|
1898
|
-
|
|
1876
|
+
type2 ? { headers: { "content-type": type2 } } : void 0
|
|
1899
1877
|
);
|
|
1900
1878
|
}
|
|
1901
1879
|
}
|
|
@@ -1909,7 +1887,7 @@ async function parseResponse(out, ctx) {
|
|
|
1909
1887
|
out = new Response(void 0, { status: out });
|
|
1910
1888
|
}
|
|
1911
1889
|
if (typeof out === "string") {
|
|
1912
|
-
const type2 =
|
|
1890
|
+
const type2 = isHtml(out) ? mimes_default.html : mimes_default.text;
|
|
1913
1891
|
out = new Response(out, {
|
|
1914
1892
|
headers: {
|
|
1915
1893
|
"content-type": type2,
|
|
@@ -1964,13 +1942,6 @@ async function parseResponse(out, ctx) {
|
|
|
1964
1942
|
}
|
|
1965
1943
|
ctx.options.session.store.set(id, ctx.session);
|
|
1966
1944
|
}
|
|
1967
|
-
if (ctx.options.cookies) {
|
|
1968
|
-
if (Object.keys(ctx.res?.cookies || {}).length) {
|
|
1969
|
-
for (const cookie of Object.values(ctx.res.cookies)) {
|
|
1970
|
-
ctx.res.headers.append("set-cookie", cookie);
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
1945
|
if (ctx?.res?.headers) {
|
|
1975
1946
|
for (const key in ctx.res.headers) {
|
|
1976
1947
|
out.headers[key] = ctx.res.headers[key];
|
|
@@ -1980,14 +1951,14 @@ async function parseResponse(out, ctx) {
|
|
|
1980
1951
|
}
|
|
1981
1952
|
|
|
1982
1953
|
// src/pathPattern.ts
|
|
1983
|
-
function pathPattern(pattern,
|
|
1984
|
-
if (pattern === "*" &&
|
|
1954
|
+
function pathPattern(pattern, path) {
|
|
1955
|
+
if (pattern === "*" && path === "/") return {};
|
|
1985
1956
|
pattern = `/${pattern.replace(/^\//, "")}`;
|
|
1986
1957
|
pattern = pattern.replace(/\/$/, "") || "/";
|
|
1987
|
-
|
|
1988
|
-
if (pattern ===
|
|
1958
|
+
path = path.replace(/\/$/, "") || "/";
|
|
1959
|
+
if (pattern === path) return {};
|
|
1989
1960
|
const params = {};
|
|
1990
|
-
const pathParts =
|
|
1961
|
+
const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
|
|
1991
1962
|
const pattParts = pattern.split("/").slice(1);
|
|
1992
1963
|
let allSame = true;
|
|
1993
1964
|
for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
|
|
@@ -2046,7 +2017,7 @@ function validate(ctx, schema) {
|
|
|
2046
2017
|
} catch (error) {
|
|
2047
2018
|
if (error.name === "ZodError" || error.constructor.name === "ZodError") {
|
|
2048
2019
|
const message = error.issues.map(
|
|
2049
|
-
({ path
|
|
2020
|
+
({ path, message: message2 }) => `[${base}.${path.join(".")}]: ${message2}`
|
|
2050
2021
|
).sort().join("\n");
|
|
2051
2022
|
throw new StatusError(message, 422);
|
|
2052
2023
|
}
|
|
@@ -2075,6 +2046,7 @@ async function getResponse(app, ctx) {
|
|
|
2075
2046
|
if (Object.keys(route.options).length) {
|
|
2076
2047
|
ctx.options = { ...app.settings, ...route.options };
|
|
2077
2048
|
}
|
|
2049
|
+
checkTraversal(params, ctx);
|
|
2078
2050
|
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
2079
2051
|
for (const cb of route.fns) {
|
|
2080
2052
|
if (typeof cb === "function") {
|
|
@@ -2174,7 +2146,12 @@ function parseCookies(cookies2) {
|
|
|
2174
2146
|
return Object.fromEntries(
|
|
2175
2147
|
cookieStr.split(/;\s*/).map((part) => {
|
|
2176
2148
|
const [key, ...rest] = part.split("=");
|
|
2177
|
-
|
|
2149
|
+
const value = rest.join("=");
|
|
2150
|
+
try {
|
|
2151
|
+
return [key, decodeURIComponent(value)];
|
|
2152
|
+
} catch {
|
|
2153
|
+
return [key, value];
|
|
2154
|
+
}
|
|
2178
2155
|
})
|
|
2179
2156
|
);
|
|
2180
2157
|
}
|
|
@@ -2236,7 +2213,7 @@ async function verify(password, hash3) {
|
|
|
2236
2213
|
const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
|
|
2237
2214
|
const nonce = Buffer.from(saltB64, "base64");
|
|
2238
2215
|
const expected = Buffer.from(hashB64, "base64");
|
|
2239
|
-
return new Promise((
|
|
2216
|
+
return new Promise((resolve, reject) => {
|
|
2240
2217
|
crypto3.argon2(
|
|
2241
2218
|
`argon2${variant}`,
|
|
2242
2219
|
{
|
|
@@ -2250,9 +2227,9 @@ async function verify(password, hash3) {
|
|
|
2250
2227
|
(err, derivedKey) => {
|
|
2251
2228
|
if (err) return reject(err);
|
|
2252
2229
|
if (derivedKey.length === expected.length && timingSafeEqual(derivedKey, expected)) {
|
|
2253
|
-
|
|
2230
|
+
resolve(true);
|
|
2254
2231
|
} else {
|
|
2255
|
-
|
|
2232
|
+
resolve(false);
|
|
2256
2233
|
}
|
|
2257
2234
|
}
|
|
2258
2235
|
);
|
|
@@ -2444,17 +2421,18 @@ async function assets(ctx) {
|
|
|
2444
2421
|
try {
|
|
2445
2422
|
const key = ctx.url.pathname.replace(/^\/+/, "");
|
|
2446
2423
|
const file2 = ctx.options.public.file(key);
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
const
|
|
2424
|
+
const info = file2.info?.bind(file2);
|
|
2425
|
+
const meta = info ? await info() : null;
|
|
2426
|
+
if (info ? !meta : !await file2.exists()) return;
|
|
2427
|
+
const ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2428
|
+
const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
|
|
2451
2429
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2452
2430
|
let tag;
|
|
2453
2431
|
if (meta) {
|
|
2454
|
-
const stamp = meta.
|
|
2432
|
+
const stamp = meta.modified ? meta.modified.getTime() : 0;
|
|
2455
2433
|
tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
|
|
2456
2434
|
headers2.etag = tag;
|
|
2457
|
-
if (meta.
|
|
2435
|
+
if (meta.modified) headers2["last-modified"] = meta.modified.toUTCString();
|
|
2458
2436
|
}
|
|
2459
2437
|
const canRange = !!(meta && file2.slice);
|
|
2460
2438
|
if (canRange) headers2["accept-ranges"] = "bytes";
|
|
@@ -2510,7 +2488,7 @@ async function favicon(ctx) {
|
|
|
2510
2488
|
}
|
|
2511
2489
|
|
|
2512
2490
|
// src/middle/openapi.ts
|
|
2513
|
-
import * as
|
|
2491
|
+
import * as fsp from "fs/promises";
|
|
2514
2492
|
var entities = {
|
|
2515
2493
|
"&": "&",
|
|
2516
2494
|
"<": "<",
|
|
@@ -2556,7 +2534,7 @@ function zodToSchema(schema) {
|
|
|
2556
2534
|
}
|
|
2557
2535
|
return { type: type2 };
|
|
2558
2536
|
}
|
|
2559
|
-
var pkgProm =
|
|
2537
|
+
var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
|
|
2560
2538
|
var getTag = (name, fn) => {
|
|
2561
2539
|
const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
|
|
2562
2540
|
if (!found) return "";
|
|
@@ -2568,14 +2546,14 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2568
2546
|
const paths = {};
|
|
2569
2547
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2570
2548
|
for (const route of routes) {
|
|
2571
|
-
const
|
|
2549
|
+
const path = route.path;
|
|
2572
2550
|
const fn = route.fns.find((p) => typeof p === "function");
|
|
2573
2551
|
const meta = route.fns.find((p) => typeof p === "object");
|
|
2574
2552
|
const config2 = getConfig(route.options);
|
|
2575
|
-
if (typeof
|
|
2553
|
+
if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
|
|
2576
2554
|
continue;
|
|
2577
2555
|
}
|
|
2578
|
-
const normalizedPath =
|
|
2556
|
+
const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
2579
2557
|
if (!paths[normalizedPath]) {
|
|
2580
2558
|
paths[normalizedPath] = {};
|
|
2581
2559
|
}
|
|
@@ -2602,7 +2580,7 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2602
2580
|
};
|
|
2603
2581
|
}
|
|
2604
2582
|
const parameters = [];
|
|
2605
|
-
const matched = Array.from(
|
|
2583
|
+
const matched = Array.from(path.matchAll(/:[\w()]+/gi));
|
|
2606
2584
|
matched.forEach((match) => {
|
|
2607
2585
|
const [name, type2 = "string"] = match[0].slice(1).replace(/\)/, "").split("(");
|
|
2608
2586
|
parameters.push({
|
|
@@ -2941,18 +2919,18 @@ async function createNode(req, app) {
|
|
|
2941
2919
|
const cookies2 = parseCookies(headers2.cookie);
|
|
2942
2920
|
const scheme = req.socket instanceof TLSSocket ? "https" : "http";
|
|
2943
2921
|
const host = headers2.host || `localhost:${app.settings.port}`;
|
|
2944
|
-
const
|
|
2922
|
+
const path = (req.url || "/").replace(/\/$/, "") || "/";
|
|
2945
2923
|
const baseUrl = `${scheme}://${host}`;
|
|
2946
|
-
const url = new URL(
|
|
2924
|
+
const url = new URL(path, baseUrl);
|
|
2947
2925
|
define(
|
|
2948
2926
|
url,
|
|
2949
2927
|
"query",
|
|
2950
2928
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
2951
2929
|
);
|
|
2952
2930
|
const source = {
|
|
2953
|
-
getBuffer: () => new Promise((
|
|
2931
|
+
getBuffer: () => new Promise((resolve, reject) => {
|
|
2954
2932
|
const chunks2 = [];
|
|
2955
|
-
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () =>
|
|
2933
|
+
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve(Buffer.concat(chunks2))).on("error", reject);
|
|
2956
2934
|
}),
|
|
2957
2935
|
getStream: () => toWeb(req)
|
|
2958
2936
|
};
|
|
@@ -3104,9 +3082,9 @@ var Router = class _Router {
|
|
|
3104
3082
|
// functions into a single flat `fns` list. A plain options object may sit
|
|
3105
3083
|
// between the path and the handlers, and it's pulled out here.
|
|
3106
3084
|
handle(method, pathOrFn, ...rest) {
|
|
3107
|
-
let
|
|
3085
|
+
let path = "*";
|
|
3108
3086
|
if (typeof pathOrFn === "string") {
|
|
3109
|
-
|
|
3087
|
+
path = pathOrFn;
|
|
3110
3088
|
} else if (pathOrFn != null) {
|
|
3111
3089
|
rest.unshift(pathOrFn);
|
|
3112
3090
|
}
|
|
@@ -3116,7 +3094,7 @@ var Router = class _Router {
|
|
|
3116
3094
|
}
|
|
3117
3095
|
const base = method === "socket" ? [] : this.middleware;
|
|
3118
3096
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
3119
|
-
this.handlers[method].push({ path
|
|
3097
|
+
this.handlers[method].push({ path, options, fns });
|
|
3120
3098
|
return this.self();
|
|
3121
3099
|
}
|
|
3122
3100
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
@@ -3181,31 +3159,33 @@ function isSerializable(body) {
|
|
|
3181
3159
|
}
|
|
3182
3160
|
function ServerTest(app) {
|
|
3183
3161
|
const port = app.settings.port;
|
|
3184
|
-
const fetch2 = async (method,
|
|
3162
|
+
const fetch2 = async (method, path, options = {}) => {
|
|
3185
3163
|
if (!options.headers) options.headers = {};
|
|
3186
3164
|
if (isSerializable(options.body)) {
|
|
3187
3165
|
options.headers["content-type"] = "application/json";
|
|
3188
3166
|
options.body = JSON.stringify(options.body);
|
|
3189
3167
|
}
|
|
3190
3168
|
return await app.fetch(
|
|
3191
|
-
new Request(`http://localhost:${port}${
|
|
3169
|
+
new Request(`http://localhost:${port}${path}`, {
|
|
3192
3170
|
method,
|
|
3193
3171
|
...options
|
|
3194
3172
|
})
|
|
3195
3173
|
);
|
|
3196
3174
|
};
|
|
3197
3175
|
return {
|
|
3198
|
-
get: (
|
|
3199
|
-
head: (
|
|
3200
|
-
post: (
|
|
3201
|
-
put: (
|
|
3202
|
-
patch: (
|
|
3203
|
-
delete: (
|
|
3204
|
-
options: (
|
|
3176
|
+
get: (path, options) => fetch2("get", path, options),
|
|
3177
|
+
head: (path, options) => fetch2("head", path, options),
|
|
3178
|
+
post: (path, body, options) => fetch2("post", path, { body, ...options }),
|
|
3179
|
+
put: (path, body, options) => fetch2("put", path, { body, ...options }),
|
|
3180
|
+
patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
|
|
3181
|
+
delete: (path, options) => fetch2("delete", path, options),
|
|
3182
|
+
options: (path, options) => fetch2("options", path, options)
|
|
3205
3183
|
};
|
|
3206
3184
|
}
|
|
3207
3185
|
|
|
3208
3186
|
// src/index.ts
|
|
3187
|
+
import { default as default2 } from "polystore";
|
|
3188
|
+
import { default as default3 } from "bucket";
|
|
3209
3189
|
var Server = class extends Router {
|
|
3210
3190
|
settings;
|
|
3211
3191
|
platform;
|
|
@@ -3273,6 +3253,7 @@ function server(options) {
|
|
|
3273
3253
|
export {
|
|
3274
3254
|
Server,
|
|
3275
3255
|
ServerError_default as ServerError,
|
|
3256
|
+
default3 as bucket,
|
|
3276
3257
|
cache,
|
|
3277
3258
|
cookies,
|
|
3278
3259
|
server as default,
|
|
@@ -3280,6 +3261,7 @@ export {
|
|
|
3280
3261
|
file,
|
|
3281
3262
|
headers,
|
|
3282
3263
|
json,
|
|
3264
|
+
default2 as kv,
|
|
3283
3265
|
redirect,
|
|
3284
3266
|
router,
|
|
3285
3267
|
send,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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,18 +44,37 @@
|
|
|
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"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"bucket": "^0.6.0",
|
|
69
|
+
"polystore": "^0.23.2"
|
|
49
70
|
},
|
|
50
71
|
"devDependencies": {
|
|
51
72
|
"@types/bun": "^1.3.0",
|
|
52
73
|
"@types/jest": "^30.0.0",
|
|
53
74
|
"@types/node": "^24.10.0",
|
|
54
|
-
"bucket": "^0.4.0",
|
|
55
75
|
"bun": "^1.3.13",
|
|
56
76
|
"check-dts": "^0.8.2",
|
|
57
77
|
"jest": "^29.7.0",
|
|
58
|
-
"polystore": "^0.21.1",
|
|
59
78
|
"tsup": "^8.5.1",
|
|
60
79
|
"typescript": "^6.0.2"
|
|
61
80
|
},
|
package/readme.md
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
1
|
# Server JS [](https://www.npmjs.com/package/@server/next) [](https://github.com/franciscop/server-next/actions)
|
|
2
|
+
|
|
3
|
+
A modern web server for Bun and Node, with routing, authentication, uploads, WebSockets and testing built in.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @server/next
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import server from '@server/next';
|
|
11
|
+
|
|
12
|
+
export default server({ store: new Map(), uploads: './uploads' })
|
|
13
|
+
.get('/', () => 'Hello world')
|
|
14
|
+
.get('/users/:id', (ctx) => db.users.find(ctx.url.params.id))
|
|
15
|
+
.post('/avatar', (ctx) => ctx.body.avatar.path);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Key-value stores and file storage come included, so `store` takes a plain `Map` and `uploads` takes a folder path. For Redis, S3 and the rest, `kv` and `bucket` are exported too:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import server, { kv, bucket } from '@server/next';
|
|
22
|
+
|
|
23
|
+
const store = kv(createClient({ url }).connect());
|
|
24
|
+
const uploads = bucket.S3('my-bucket', { id, key });
|
|
25
|
+
|
|
26
|
+
export default server({ store, uploads });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See the [full documentation](https://serverjs.io/documentation).
|