@server/next 0.30.0 → 0.32.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 -12
- package/index.js +546 -333
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -27,10 +27,17 @@ declare namespace JSX {
|
|
|
27
27
|
[elem: string]: any;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
type BodyMode = "parse" | "raw" | "stream";
|
|
30
31
|
type RouteOptions = {
|
|
31
32
|
tags?: string | string[];
|
|
32
33
|
title?: string;
|
|
33
34
|
description?: string;
|
|
35
|
+
body?: BodyMode;
|
|
36
|
+
};
|
|
37
|
+
type Route = {
|
|
38
|
+
path: string;
|
|
39
|
+
options: RouteOptions;
|
|
40
|
+
fns: Middleware[];
|
|
34
41
|
};
|
|
35
42
|
type Cookie = {
|
|
36
43
|
value?: string | null;
|
|
@@ -43,9 +50,11 @@ type Cookie = {
|
|
|
43
50
|
};
|
|
44
51
|
type RouterMethod = "*" | Method;
|
|
45
52
|
type Bucket = {
|
|
53
|
+
location?: string;
|
|
46
54
|
read: (path: string) => Promise<ReadableStream | null>;
|
|
47
|
-
write: (path: string, data: string | Buffer) => Promise<void | string>;
|
|
55
|
+
write: (path: string, data: string | Buffer | ReadableStream) => Promise<void | string>;
|
|
48
56
|
delete: (path: string) => Promise<boolean>;
|
|
57
|
+
folder?: (prefix: string) => Bucket;
|
|
49
58
|
};
|
|
50
59
|
type UploadedFile = {
|
|
51
60
|
name: string;
|
|
@@ -129,7 +138,6 @@ type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
|
|
|
129
138
|
type Options = {
|
|
130
139
|
port?: number;
|
|
131
140
|
secret?: string;
|
|
132
|
-
views?: string | Bucket;
|
|
133
141
|
public?: string | Bucket;
|
|
134
142
|
uploads?: string | Bucket | UploadPipeline;
|
|
135
143
|
store?: KVStore;
|
|
@@ -144,11 +152,11 @@ type Options = {
|
|
|
144
152
|
log?: LogLevel | boolean;
|
|
145
153
|
favicon?: string | Bucket;
|
|
146
154
|
security?: SecurityOptions;
|
|
155
|
+
body?: BodyMode;
|
|
147
156
|
};
|
|
148
157
|
type Settings = {
|
|
149
158
|
port: number;
|
|
150
159
|
secret: string;
|
|
151
|
-
views?: Bucket;
|
|
152
160
|
public?: Bucket;
|
|
153
161
|
uploads?: Bucket | UploadPipeline;
|
|
154
162
|
store?: KVStore;
|
|
@@ -163,6 +171,7 @@ type Settings = {
|
|
|
163
171
|
log: Logger;
|
|
164
172
|
favicon?: string | Bucket;
|
|
165
173
|
security: SecuritySettings;
|
|
174
|
+
body: BodyMode;
|
|
166
175
|
};
|
|
167
176
|
type Time = {
|
|
168
177
|
(name: string): void;
|
|
@@ -198,7 +207,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
|
|
|
198
207
|
ip: string;
|
|
199
208
|
headers: Record<string, string | string[]>;
|
|
200
209
|
cookies: Record<string, string>;
|
|
201
|
-
body?: SerializableValue;
|
|
210
|
+
body?: SerializableValue | Buffer | ReadableStream;
|
|
202
211
|
url: URL & {
|
|
203
212
|
params: Params;
|
|
204
213
|
query: Record<string, string>;
|
|
@@ -225,7 +234,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
|
|
|
225
234
|
type InlineReply = Response | {
|
|
226
235
|
body: string;
|
|
227
236
|
headers?: Headers;
|
|
228
|
-
} | SerializableValue | JSX.Element;
|
|
237
|
+
} | SerializableValue | JSX.Element | Buffer | ReadableStream;
|
|
229
238
|
type Body = InlineReply;
|
|
230
239
|
type Middleware<O extends ServerConfig = object, Params extends Record<string, string | undefined> = Record<string, string>> = (ctx: Context<Params, O>) => InlineReply | Promise<InlineReply> | void | Promise<void>;
|
|
231
240
|
|
|
@@ -251,12 +260,11 @@ declare global {
|
|
|
251
260
|
}
|
|
252
261
|
|
|
253
262
|
type Mids<O extends ServerConfig, Path extends string> = Middleware<O, PathToParams<Path>>[];
|
|
254
|
-
type PathOrMiddle<O extends ServerConfig = object> = string | Middleware<O>;
|
|
255
|
-
type FullRoute = [RouterMethod, string, ...Middleware[]][];
|
|
256
263
|
declare class Router<O extends ServerConfig = object> {
|
|
257
|
-
|
|
264
|
+
middleware: Middleware[];
|
|
265
|
+
handlers: Record<Method, Route[]>;
|
|
258
266
|
self(): this;
|
|
259
|
-
handle(method:
|
|
267
|
+
handle(method: Method, pathOrFn?: any, ...rest: any[]): this;
|
|
260
268
|
socket<Path extends string>(path: Path, ...middleware: Mids<O, Path>): this;
|
|
261
269
|
socket<Path extends string>(path: Path, options: RouteOptions, ...middleware: Mids<O, Path>): this;
|
|
262
270
|
socket(...middleware: Middleware<O>[]): this;
|
|
@@ -290,9 +298,7 @@ declare class Router<O extends ServerConfig = object> {
|
|
|
290
298
|
options(...middleware: Middleware<O>[]): this;
|
|
291
299
|
options(options: RouteOptions, ...middleware: Middleware<O>[]): this;
|
|
292
300
|
use(...middleware: Middleware[]): this;
|
|
293
|
-
use(path: string, ...middleware: Middleware[]): this;
|
|
294
301
|
use(router: Router): this;
|
|
295
|
-
use(path: string, router: Router): this;
|
|
296
302
|
}
|
|
297
303
|
declare function router(): Router;
|
|
298
304
|
|
|
@@ -452,4 +458,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
452
458
|
}
|
|
453
459
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
454
460
|
|
|
455
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
|
|
461
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, 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, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
|
package/index.js
CHANGED
|
@@ -89,6 +89,403 @@ if (typeof process !== "undefined") {
|
|
|
89
89
|
Object.assign(globalThis.env, process.env);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// src/helpers/createId.ts
|
|
93
|
+
var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
94
|
+
var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
95
|
+
var cyrb53 = (str, seed = 0) => {
|
|
96
|
+
if (typeof str !== "string") str = String(str);
|
|
97
|
+
let h1 = 3735928559 ^ seed;
|
|
98
|
+
let h2 = 1103547991 ^ seed;
|
|
99
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
100
|
+
ch = str.charCodeAt(i);
|
|
101
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
102
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
103
|
+
}
|
|
104
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
105
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
106
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
107
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
108
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
109
|
+
};
|
|
110
|
+
var hash = (str, size) => {
|
|
111
|
+
let chars = "";
|
|
112
|
+
let num = cyrb53(str);
|
|
113
|
+
for (let i = 0; i < size; i++) {
|
|
114
|
+
if (num < alphabet.length) num = cyrb53(str, i);
|
|
115
|
+
chars += alphabet[num % alphabet.length];
|
|
116
|
+
num = Math.floor(num / alphabet.length);
|
|
117
|
+
}
|
|
118
|
+
return chars;
|
|
119
|
+
};
|
|
120
|
+
var randomId = (size = 16) => {
|
|
121
|
+
let id = "";
|
|
122
|
+
const bytes = random(size);
|
|
123
|
+
while (size--) {
|
|
124
|
+
id += alphabet[bytes[size] & 61];
|
|
125
|
+
}
|
|
126
|
+
return id;
|
|
127
|
+
};
|
|
128
|
+
function createId(source, size = 16) {
|
|
129
|
+
if (source) return hash(source, size);
|
|
130
|
+
return randomId(size);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/helpers/upload.ts
|
|
134
|
+
function parseBytes(value) {
|
|
135
|
+
if (typeof value === "number") return value;
|
|
136
|
+
const units = {
|
|
137
|
+
b: 1,
|
|
138
|
+
kb: 1024,
|
|
139
|
+
mb: 1024 ** 2,
|
|
140
|
+
gb: 1024 ** 3
|
|
141
|
+
};
|
|
142
|
+
const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
143
|
+
if (!match) throw new Error(`Invalid size: "${value}"`);
|
|
144
|
+
return parseFloat(match[1]) * (units[match[2]] ?? 1);
|
|
145
|
+
}
|
|
146
|
+
function getExt(filename) {
|
|
147
|
+
const i = filename.lastIndexOf(".");
|
|
148
|
+
if (i <= 0) return ".bin";
|
|
149
|
+
return filename.slice(i).toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
async function saveFileToBucket(originalName, data, bucket, contentType) {
|
|
152
|
+
const ext = getExt(originalName);
|
|
153
|
+
const id = `${createId()}${ext}`;
|
|
154
|
+
const path2 = await bucket.write(id, data);
|
|
155
|
+
return { name: originalName, id, path: path2, type: contentType, size: data.length };
|
|
156
|
+
}
|
|
157
|
+
var UploadPipeline = class {
|
|
158
|
+
_bucket;
|
|
159
|
+
_limits = {};
|
|
160
|
+
constructor(bucket) {
|
|
161
|
+
this._bucket = bucket ?? null;
|
|
162
|
+
}
|
|
163
|
+
limit(options) {
|
|
164
|
+
this._limits = { ...this._limits, ...options };
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
store(bucket) {
|
|
168
|
+
this._bucket = bucket;
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
async processFile(originalName, data, contentType) {
|
|
172
|
+
const { maxSize, minSize, fileType } = this._limits;
|
|
173
|
+
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (fileType && fileType.length > 0) {
|
|
184
|
+
const ext = getExt(originalName);
|
|
185
|
+
const mime = contentType.toLowerCase();
|
|
186
|
+
const allowed = fileType.some(
|
|
187
|
+
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext
|
|
188
|
+
);
|
|
189
|
+
if (!allowed) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!this._bucket) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`No destination configured. Pass a bucket to upload() or call .store()`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
function upload(bucket) {
|
|
204
|
+
return new UploadPipeline(bucket);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/helpers/parseBody.ts
|
|
208
|
+
function getBoundary(header) {
|
|
209
|
+
if (!header) return null;
|
|
210
|
+
if (header.includes("multipart/form-data") && !header.includes("boundary=")) {
|
|
211
|
+
console.error("Do not set the `Content-Type` manually for FormData");
|
|
212
|
+
}
|
|
213
|
+
const items = header.split(";");
|
|
214
|
+
for (const item of items) {
|
|
215
|
+
const trimmedItem = item.trim();
|
|
216
|
+
if (trimmedItem.startsWith("boundary=")) {
|
|
217
|
+
return trimmedItem.split("=")[1].trim();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
function getMatching(string, regex) {
|
|
223
|
+
const matches = string.match(regex);
|
|
224
|
+
return matches?.[1] ?? "";
|
|
225
|
+
}
|
|
226
|
+
function isProbablyText(buffer) {
|
|
227
|
+
for (let i = 0; i < Math.min(buffer.length, 512); i++) {
|
|
228
|
+
const byte = buffer[i];
|
|
229
|
+
if (byte === 0) return false;
|
|
230
|
+
if (byte < 7 || byte > 13 && byte < 32) return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
var MIME_EXT = {
|
|
235
|
+
"application/json": ".json",
|
|
236
|
+
"application/pdf": ".pdf",
|
|
237
|
+
"application/zip": ".zip",
|
|
238
|
+
"text/plain": ".txt",
|
|
239
|
+
"text/html": ".html",
|
|
240
|
+
"text/csv": ".csv",
|
|
241
|
+
"image/jpeg": ".jpg",
|
|
242
|
+
"image/png": ".png",
|
|
243
|
+
"image/gif": ".gif",
|
|
244
|
+
"image/webp": ".webp",
|
|
245
|
+
"image/svg+xml": ".svg",
|
|
246
|
+
"video/mp4": ".mp4",
|
|
247
|
+
"audio/mpeg": ".mp3"
|
|
248
|
+
};
|
|
249
|
+
function extFromType(type2) {
|
|
250
|
+
const base = (type2 || "").split(";")[0].trim().toLowerCase();
|
|
251
|
+
if (MIME_EXT[base]) return MIME_EXT[base];
|
|
252
|
+
const sub = base.split("/")[1];
|
|
253
|
+
return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
|
|
254
|
+
}
|
|
255
|
+
var asIterable = (s) => s;
|
|
256
|
+
function toStream(input) {
|
|
257
|
+
if (input instanceof ReadableStream) return input;
|
|
258
|
+
return new ReadableStream({
|
|
259
|
+
start(controller) {
|
|
260
|
+
controller.enqueue(input);
|
|
261
|
+
controller.close();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function toBuffer(input) {
|
|
266
|
+
if (!(input instanceof ReadableStream)) return input;
|
|
267
|
+
const chunks = [];
|
|
268
|
+
for await (const chunk of asIterable(input)) chunks.push(Buffer.from(chunk));
|
|
269
|
+
return Buffer.concat(chunks);
|
|
270
|
+
}
|
|
271
|
+
function parseUrlEncoded(text) {
|
|
272
|
+
const out = {};
|
|
273
|
+
for (const [key, value] of new URLSearchParams(text)) {
|
|
274
|
+
const existing = out[key];
|
|
275
|
+
if (existing === void 0) out[key] = value;
|
|
276
|
+
else if (Array.isArray(existing)) existing.push(value);
|
|
277
|
+
else out[key] = [existing, value];
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function addField(body, name, value) {
|
|
282
|
+
if (body[name] === void 0) {
|
|
283
|
+
body[name] = value;
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
287
|
+
body[name].push(value);
|
|
288
|
+
}
|
|
289
|
+
function startPart(headerStr, dest) {
|
|
290
|
+
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
291
|
+
if (!name) return { kind: "skip" };
|
|
292
|
+
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
293
|
+
if (!filename) return { kind: "text", name, chunks: [] };
|
|
294
|
+
const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
295
|
+
if (!dest) return { kind: "drop" };
|
|
296
|
+
if (dest instanceof UploadPipeline) {
|
|
297
|
+
return { kind: "pipefile", name, filename, type: type2, pipeline: dest, chunks: [] };
|
|
298
|
+
}
|
|
299
|
+
const id = `${createId()}${getExt(filename)}`;
|
|
300
|
+
let controller;
|
|
301
|
+
const readable = new ReadableStream({
|
|
302
|
+
start(c) {
|
|
303
|
+
controller = c;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
kind: "file",
|
|
308
|
+
name,
|
|
309
|
+
filename,
|
|
310
|
+
type: type2,
|
|
311
|
+
id,
|
|
312
|
+
controller,
|
|
313
|
+
write: dest.write(id, readable),
|
|
314
|
+
size: 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function feedPart(part, data) {
|
|
318
|
+
if (data.length === 0) return;
|
|
319
|
+
if (part.kind === "text" || part.kind === "pipefile") part.chunks.push(data);
|
|
320
|
+
else if (part.kind === "file") {
|
|
321
|
+
part.controller.enqueue(data);
|
|
322
|
+
part.size += data.length;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async function endPart(part, body) {
|
|
326
|
+
if (part.kind === "text") {
|
|
327
|
+
const buf = Buffer.concat(part.chunks);
|
|
328
|
+
const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
|
|
329
|
+
addField(body, part.name, value);
|
|
330
|
+
} else if (part.kind === "pipefile") {
|
|
331
|
+
const buf = Buffer.concat(part.chunks);
|
|
332
|
+
const ref = await part.pipeline.processFile(part.filename, buf, part.type);
|
|
333
|
+
addField(body, part.name, ref);
|
|
334
|
+
} else if (part.kind === "file") {
|
|
335
|
+
part.controller.close();
|
|
336
|
+
const path2 = await part.write;
|
|
337
|
+
addField(body, part.name, {
|
|
338
|
+
name: part.filename,
|
|
339
|
+
id: part.id,
|
|
340
|
+
path: path2,
|
|
341
|
+
type: part.type,
|
|
342
|
+
size: part.size
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var BREAK = Buffer.from("\r\n\r\n");
|
|
347
|
+
async function parseMultipart(stream, boundary, dest) {
|
|
348
|
+
const delim = Buffer.from(`\r
|
|
349
|
+
--${boundary}`);
|
|
350
|
+
const body = {};
|
|
351
|
+
let buf = Buffer.from("\r\n");
|
|
352
|
+
let state = "boundary";
|
|
353
|
+
let part = null;
|
|
354
|
+
for await (const chunk of asIterable(stream)) {
|
|
355
|
+
buf = Buffer.concat([buf, Buffer.from(chunk)]);
|
|
356
|
+
let advanced = true;
|
|
357
|
+
while (advanced) {
|
|
358
|
+
advanced = false;
|
|
359
|
+
if (state === "boundary") {
|
|
360
|
+
const i = buf.indexOf(delim);
|
|
361
|
+
if (i === -1) {
|
|
362
|
+
if (buf.length >= delim.length) {
|
|
363
|
+
buf = buf.subarray(buf.length - delim.length + 1);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
if (buf.length < i + delim.length + 2) break;
|
|
368
|
+
const after = i + delim.length;
|
|
369
|
+
if (buf[after] === 45 && buf[after + 1] === 45) return body;
|
|
370
|
+
buf = buf.subarray(after + 2);
|
|
371
|
+
state = "headers";
|
|
372
|
+
advanced = true;
|
|
373
|
+
} else if (state === "headers") {
|
|
374
|
+
const i = buf.indexOf(BREAK);
|
|
375
|
+
if (i === -1) break;
|
|
376
|
+
part = startPart(buf.subarray(0, i).toString("utf-8"), dest);
|
|
377
|
+
buf = buf.subarray(i + BREAK.length);
|
|
378
|
+
state = "body";
|
|
379
|
+
advanced = true;
|
|
380
|
+
} else {
|
|
381
|
+
const i = buf.indexOf(delim);
|
|
382
|
+
if (i === -1) {
|
|
383
|
+
const safe = buf.length - (delim.length - 1);
|
|
384
|
+
if (safe > 0 && part) {
|
|
385
|
+
feedPart(part, buf.subarray(0, safe));
|
|
386
|
+
buf = buf.subarray(safe);
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (part) {
|
|
391
|
+
feedPart(part, buf.subarray(0, i));
|
|
392
|
+
await endPart(part, body);
|
|
393
|
+
part = null;
|
|
394
|
+
}
|
|
395
|
+
buf = buf.subarray(i);
|
|
396
|
+
state = "boundary";
|
|
397
|
+
advanced = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (part) await endPart(part, body);
|
|
402
|
+
return body;
|
|
403
|
+
}
|
|
404
|
+
async function streamToBucket(stream, type2, bucket) {
|
|
405
|
+
const id = `${createId()}${extFromType(type2)}`;
|
|
406
|
+
let size = 0;
|
|
407
|
+
let controller;
|
|
408
|
+
const readable = new ReadableStream({
|
|
409
|
+
start(c) {
|
|
410
|
+
controller = c;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const write = bucket.write(id, readable);
|
|
414
|
+
for await (const chunk of asIterable(stream)) {
|
|
415
|
+
controller.enqueue(chunk);
|
|
416
|
+
size += chunk.byteLength;
|
|
417
|
+
}
|
|
418
|
+
controller.close();
|
|
419
|
+
const path2 = await write;
|
|
420
|
+
if (!size) return void 0;
|
|
421
|
+
return { name: id, id, path: path2, type: type2, size };
|
|
422
|
+
}
|
|
423
|
+
async function parseBody(input, contentType, dest) {
|
|
424
|
+
const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
425
|
+
const boundary = type2 && /multipart\/form-data/i.test(type2) ? getBoundary(type2) : null;
|
|
426
|
+
if (boundary) return parseMultipart(toStream(input), boundary, dest);
|
|
427
|
+
if (!type2 || /^text\//i.test(type2)) {
|
|
428
|
+
const buf = await toBuffer(input);
|
|
429
|
+
return buf.length ? buf.toString("utf-8") : void 0;
|
|
430
|
+
}
|
|
431
|
+
if (/application\/json/i.test(type2)) {
|
|
432
|
+
const buf = await toBuffer(input);
|
|
433
|
+
return buf.length ? JSON.parse(buf.toString("utf-8")) : void 0;
|
|
434
|
+
}
|
|
435
|
+
if (/application\/x-www-form-urlencoded/i.test(type2)) {
|
|
436
|
+
const buf = await toBuffer(input);
|
|
437
|
+
return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
|
|
438
|
+
}
|
|
439
|
+
if (!dest) {
|
|
440
|
+
const buf = await toBuffer(input);
|
|
441
|
+
return buf.length ? buf : void 0;
|
|
442
|
+
}
|
|
443
|
+
if (dest instanceof UploadPipeline) {
|
|
444
|
+
const buf = await toBuffer(input);
|
|
445
|
+
return buf.length ? dest.processFile(`upload${extFromType(type2)}`, buf, type2) : void 0;
|
|
446
|
+
}
|
|
447
|
+
return streamToBucket(toStream(input), type2, dest);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/helpers/body.ts
|
|
451
|
+
var sources = /* @__PURE__ */ new WeakMap();
|
|
452
|
+
function setBodySource(ctx, source) {
|
|
453
|
+
sources.set(ctx, source);
|
|
454
|
+
}
|
|
455
|
+
async function resolveBody(ctx, mode) {
|
|
456
|
+
const source = sources.get(ctx);
|
|
457
|
+
if (!source) return void 0;
|
|
458
|
+
if (mode === "stream") return source.getStream();
|
|
459
|
+
if (mode === "raw") {
|
|
460
|
+
const raw = await source.getBuffer();
|
|
461
|
+
if (!raw.length) return void 0;
|
|
462
|
+
if (!ctx.headers["content-length"]) {
|
|
463
|
+
ctx.headers["content-length"] = String(raw.length);
|
|
464
|
+
}
|
|
465
|
+
return raw;
|
|
466
|
+
}
|
|
467
|
+
const stream = source.getStream();
|
|
468
|
+
if (!stream) return void 0;
|
|
469
|
+
let size = 0;
|
|
470
|
+
const counted = stream.pipeThrough(
|
|
471
|
+
new TransformStream({
|
|
472
|
+
transform(chunk, controller) {
|
|
473
|
+
size += chunk.byteLength;
|
|
474
|
+
controller.enqueue(chunk);
|
|
475
|
+
}
|
|
476
|
+
})
|
|
477
|
+
);
|
|
478
|
+
const body = await parseBody(
|
|
479
|
+
counted,
|
|
480
|
+
ctx.headers["content-type"],
|
|
481
|
+
ctx.options.uploads
|
|
482
|
+
);
|
|
483
|
+
if (size && !ctx.headers["content-length"]) {
|
|
484
|
+
ctx.headers["content-length"] = String(size);
|
|
485
|
+
}
|
|
486
|
+
return body;
|
|
487
|
+
}
|
|
488
|
+
|
|
92
489
|
// src/helpers/clientIp.ts
|
|
93
490
|
var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
|
|
94
491
|
var normalize = (ip) => ip.replace(/^::ffff:/, "");
|
|
@@ -558,7 +955,7 @@ async function emailRegister(ctx) {
|
|
|
558
955
|
strategy: ctx.options.auth.strategy,
|
|
559
956
|
provider: "email",
|
|
560
957
|
email,
|
|
561
|
-
password: await
|
|
958
|
+
password: await hash2(password),
|
|
562
959
|
time,
|
|
563
960
|
...data
|
|
564
961
|
};
|
|
@@ -579,7 +976,7 @@ async function emailUpdatePassword(ctx) {
|
|
|
579
976
|
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
580
977
|
const isValid = await verify(passwords.previous, fullUser.password);
|
|
581
978
|
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
582
|
-
fullUser.password = await
|
|
979
|
+
fullUser.password = await hash2(passwords.updated);
|
|
583
980
|
await updateUser(fullUser, ctx.user, ctx.options.auth.store);
|
|
584
981
|
return 200;
|
|
585
982
|
}
|
|
@@ -797,12 +1194,23 @@ function thinLocalBucket(root) {
|
|
|
797
1194
|
}
|
|
798
1195
|
});
|
|
799
1196
|
},
|
|
800
|
-
write: (name, value, type2) => {
|
|
1197
|
+
write: async (name, value, type2) => {
|
|
801
1198
|
const fullPath = absolute(name);
|
|
802
|
-
if (value)
|
|
803
|
-
|
|
1199
|
+
if (!value) return fs.createWriteStream(fullPath);
|
|
1200
|
+
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
|
|
1201
|
+
if (value instanceof ReadableStream) {
|
|
1202
|
+
const writable = fs.createWriteStream(fullPath);
|
|
1203
|
+
for await (const chunk of value) {
|
|
1204
|
+
writable.write(chunk);
|
|
1205
|
+
}
|
|
1206
|
+
await new Promise((resolve2, reject) => {
|
|
1207
|
+
writable.on("error", reject);
|
|
1208
|
+
writable.end(resolve2);
|
|
1209
|
+
});
|
|
1210
|
+
return fullPath;
|
|
804
1211
|
}
|
|
805
|
-
|
|
1212
|
+
await fsp.writeFile(fullPath, value, type2);
|
|
1213
|
+
return fullPath;
|
|
806
1214
|
},
|
|
807
1215
|
delete: async (name) => {
|
|
808
1216
|
const fullPath = absolute(name);
|
|
@@ -812,29 +1220,35 @@ function thinLocalBucket(root) {
|
|
|
812
1220
|
} catch {
|
|
813
1221
|
return false;
|
|
814
1222
|
}
|
|
815
|
-
}
|
|
1223
|
+
},
|
|
1224
|
+
folder: (prefix) => thinLocalBucket(path.join(root, prefix))
|
|
816
1225
|
};
|
|
817
1226
|
}
|
|
818
|
-
function thinBunBucket(s3) {
|
|
1227
|
+
function thinBunBucket(s3, prefix = "") {
|
|
1228
|
+
const key = (name) => prefix ? `${prefix}/${name}` : name;
|
|
819
1229
|
return {
|
|
820
1230
|
read: async (name) => {
|
|
821
|
-
const file2 = s3.file(name);
|
|
1231
|
+
const file2 = s3.file(key(name));
|
|
822
1232
|
if (!await file2.exists()) return null;
|
|
823
1233
|
return await file2.stream();
|
|
824
1234
|
},
|
|
825
1235
|
write: async (name, value) => {
|
|
826
|
-
const file2 = s3.file(name);
|
|
1236
|
+
const file2 = s3.file(key(name));
|
|
827
1237
|
if (value) {
|
|
828
1238
|
await file2.write(value);
|
|
829
|
-
return name;
|
|
1239
|
+
return key(name);
|
|
830
1240
|
}
|
|
831
|
-
return s3.presign(name, {
|
|
1241
|
+
return s3.presign(key(name), {
|
|
1242
|
+
expiresIn: 3600,
|
|
1243
|
+
acl: "public-read-write"
|
|
1244
|
+
});
|
|
832
1245
|
},
|
|
833
1246
|
delete: async (name) => {
|
|
834
|
-
const file2 = s3.file(name);
|
|
1247
|
+
const file2 = s3.file(key(name));
|
|
835
1248
|
if (!await file2.exists()) return null;
|
|
836
1249
|
return await file2.delete();
|
|
837
|
-
}
|
|
1250
|
+
},
|
|
1251
|
+
folder: (sub) => thinBunBucket(s3, key(sub))
|
|
838
1252
|
};
|
|
839
1253
|
}
|
|
840
1254
|
function bucket_default(root) {
|
|
@@ -848,47 +1262,6 @@ function bucket_default(root) {
|
|
|
848
1262
|
return root;
|
|
849
1263
|
}
|
|
850
1264
|
|
|
851
|
-
// src/helpers/createId.ts
|
|
852
|
-
var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
853
|
-
var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
854
|
-
var cyrb53 = (str, seed = 0) => {
|
|
855
|
-
if (typeof str !== "string") str = String(str);
|
|
856
|
-
let h1 = 3735928559 ^ seed;
|
|
857
|
-
let h2 = 1103547991 ^ seed;
|
|
858
|
-
for (let i = 0, ch; i < str.length; i++) {
|
|
859
|
-
ch = str.charCodeAt(i);
|
|
860
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
861
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
862
|
-
}
|
|
863
|
-
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
864
|
-
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
865
|
-
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
866
|
-
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
867
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
868
|
-
};
|
|
869
|
-
var hash2 = (str, size) => {
|
|
870
|
-
let chars = "";
|
|
871
|
-
let num = cyrb53(str);
|
|
872
|
-
for (let i = 0; i < size; i++) {
|
|
873
|
-
if (num < alphabet.length) num = cyrb53(str, i);
|
|
874
|
-
chars += alphabet[num % alphabet.length];
|
|
875
|
-
num = Math.floor(num / alphabet.length);
|
|
876
|
-
}
|
|
877
|
-
return chars;
|
|
878
|
-
};
|
|
879
|
-
var randomId = (size = 16) => {
|
|
880
|
-
let id = "";
|
|
881
|
-
const bytes = random(size);
|
|
882
|
-
while (size--) {
|
|
883
|
-
id += alphabet[bytes[size] & 61];
|
|
884
|
-
}
|
|
885
|
-
return id;
|
|
886
|
-
};
|
|
887
|
-
function createId(source, size = 16) {
|
|
888
|
-
if (source) return hash2(source, size);
|
|
889
|
-
return randomId(size);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
1265
|
// src/helpers/color.ts
|
|
893
1266
|
var map = {
|
|
894
1267
|
reset: 0,
|
|
@@ -998,80 +1371,6 @@ function createLogger(level) {
|
|
|
998
1371
|
};
|
|
999
1372
|
}
|
|
1000
1373
|
|
|
1001
|
-
// src/helpers/upload.ts
|
|
1002
|
-
function parseBytes(value) {
|
|
1003
|
-
if (typeof value === "number") return value;
|
|
1004
|
-
const units = {
|
|
1005
|
-
b: 1,
|
|
1006
|
-
kb: 1024,
|
|
1007
|
-
mb: 1024 ** 2,
|
|
1008
|
-
gb: 1024 ** 3
|
|
1009
|
-
};
|
|
1010
|
-
const match = value.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
1011
|
-
if (!match) throw new Error(`Invalid size: "${value}"`);
|
|
1012
|
-
return parseFloat(match[1]) * (units[match[2]] ?? 1);
|
|
1013
|
-
}
|
|
1014
|
-
function getExt(filename) {
|
|
1015
|
-
const i = filename.lastIndexOf(".");
|
|
1016
|
-
if (i <= 0) return ".bin";
|
|
1017
|
-
return filename.slice(i).toLowerCase();
|
|
1018
|
-
}
|
|
1019
|
-
async function saveFileToBucket(originalName, data, bucket, contentType) {
|
|
1020
|
-
const ext = getExt(originalName);
|
|
1021
|
-
const id = `${createId()}${ext}`;
|
|
1022
|
-
const path2 = await bucket.write(id, data);
|
|
1023
|
-
return { name: originalName, id, path: path2, type: contentType, size: data.length };
|
|
1024
|
-
}
|
|
1025
|
-
var UploadPipeline = class {
|
|
1026
|
-
_bucket;
|
|
1027
|
-
_limits = {};
|
|
1028
|
-
constructor(bucket) {
|
|
1029
|
-
this._bucket = bucket ?? null;
|
|
1030
|
-
}
|
|
1031
|
-
limit(options) {
|
|
1032
|
-
this._limits = { ...this._limits, ...options };
|
|
1033
|
-
return this;
|
|
1034
|
-
}
|
|
1035
|
-
store(bucket) {
|
|
1036
|
-
this._bucket = bucket;
|
|
1037
|
-
return this;
|
|
1038
|
-
}
|
|
1039
|
-
async processFile(originalName, data, contentType) {
|
|
1040
|
-
const { maxSize, minSize, fileType } = this._limits;
|
|
1041
|
-
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
1042
|
-
throw new Error(
|
|
1043
|
-
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
if (minSize !== void 0 && data.length < parseBytes(minSize)) {
|
|
1047
|
-
throw new Error(
|
|
1048
|
-
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
if (fileType && fileType.length > 0) {
|
|
1052
|
-
const ext = getExt(originalName);
|
|
1053
|
-
const mime = contentType.toLowerCase();
|
|
1054
|
-
const allowed = fileType.some(
|
|
1055
|
-
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext
|
|
1056
|
-
);
|
|
1057
|
-
if (!allowed) {
|
|
1058
|
-
throw new Error(
|
|
1059
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
if (!this._bucket) {
|
|
1064
|
-
throw new Error(
|
|
1065
|
-
`No destination configured. Pass a bucket to upload() or call .store()`
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
return saveFileToBucket(originalName, data, this._bucket, contentType);
|
|
1069
|
-
}
|
|
1070
|
-
};
|
|
1071
|
-
function upload(bucket) {
|
|
1072
|
-
return new UploadPipeline(bucket);
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
1374
|
// src/helpers/config.ts
|
|
1076
1375
|
function config(options = {}) {
|
|
1077
1376
|
const env2 = globalThis.env;
|
|
@@ -1082,6 +1381,9 @@ function config(options = {}) {
|
|
|
1082
1381
|
port: options.port || env2.PORT || 3e3,
|
|
1083
1382
|
secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
|
|
1084
1383
|
log,
|
|
1384
|
+
// How request bodies are read: parsed into ctx.body by default; `raw` keeps
|
|
1385
|
+
// the Buffer, `stream` hands the handler the unread web ReadableStream.
|
|
1386
|
+
body: options.body ?? "parse",
|
|
1085
1387
|
// Trust X-Forwarded-* headers for ctx.ip (on by default; set it to false
|
|
1086
1388
|
// when clients connect directly so a client can't spoof its IP).
|
|
1087
1389
|
security: {
|
|
@@ -1124,7 +1426,6 @@ function config(options = {}) {
|
|
|
1124
1426
|
}
|
|
1125
1427
|
settings.cors = cors2;
|
|
1126
1428
|
}
|
|
1127
|
-
settings.views = options.views ? bucket_default(options.views) : null;
|
|
1128
1429
|
settings.public = options.public ? bucket_default(options.public) : null;
|
|
1129
1430
|
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
|
|
1130
1431
|
if (options.favicon) settings.favicon = options.favicon;
|
|
@@ -1154,7 +1455,6 @@ function config(options = {}) {
|
|
|
1154
1455
|
log.message("auth", `${settings.auth.provider.join(", ")} auth enabled`);
|
|
1155
1456
|
}
|
|
1156
1457
|
if (settings.public) log.message("public", loc(options.public));
|
|
1157
|
-
if (settings.views) log.message("views", loc(options.views));
|
|
1158
1458
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1159
1459
|
if (settings.session) log.message("session", "enabled");
|
|
1160
1460
|
if (settings.cors) {
|
|
@@ -1203,17 +1503,23 @@ function applyCors(res, ctx) {
|
|
|
1203
1503
|
|
|
1204
1504
|
// src/helpers/createWebsocket.ts
|
|
1205
1505
|
function createWebsocket(sockets, handlers) {
|
|
1506
|
+
const run = (event, socket, body) => {
|
|
1507
|
+
const routes = handlers.socket?.filter((r2) => r2.path === event) ?? [];
|
|
1508
|
+
for (const route of routes) {
|
|
1509
|
+
for (const fn of route.fns) {
|
|
1510
|
+
fn({ socket, sockets, body });
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1206
1514
|
return {
|
|
1207
|
-
message:
|
|
1208
|
-
handlers.socket?.filter((s) => s[1] === "message")?.map((s) => s[2]({ socket, sockets, body }));
|
|
1209
|
-
},
|
|
1515
|
+
message: (socket, body) => run("message", socket, body),
|
|
1210
1516
|
open: (socket) => {
|
|
1211
1517
|
sockets.push(socket);
|
|
1212
|
-
|
|
1518
|
+
run("open", socket);
|
|
1213
1519
|
},
|
|
1214
1520
|
close: (socket) => {
|
|
1215
1521
|
sockets.splice(sockets.indexOf(socket), 1);
|
|
1216
|
-
|
|
1522
|
+
run("close", socket);
|
|
1217
1523
|
}
|
|
1218
1524
|
};
|
|
1219
1525
|
}
|
|
@@ -1270,6 +1576,9 @@ async function parseResponse(out, ctx) {
|
|
|
1270
1576
|
if (out instanceof ReadableStream) {
|
|
1271
1577
|
out = new Response(out);
|
|
1272
1578
|
}
|
|
1579
|
+
if (out instanceof Uint8Array) {
|
|
1580
|
+
out = new Response(out);
|
|
1581
|
+
}
|
|
1273
1582
|
if (typeof out === "number") {
|
|
1274
1583
|
out = new Response(void 0, { status: out });
|
|
1275
1584
|
}
|
|
@@ -1420,18 +1729,24 @@ function validate(ctx, schema) {
|
|
|
1420
1729
|
}
|
|
1421
1730
|
|
|
1422
1731
|
// src/helpers/handleRequest.ts
|
|
1423
|
-
async function handleRequest(
|
|
1424
|
-
const res = await getResponse(
|
|
1732
|
+
async function handleRequest(app, ctx) {
|
|
1733
|
+
const res = await getResponse(app, ctx);
|
|
1425
1734
|
if (res) ctx.options.log.request(ctx, res);
|
|
1426
1735
|
return res;
|
|
1427
1736
|
}
|
|
1428
|
-
async function getResponse(
|
|
1737
|
+
async function getResponse(app, ctx) {
|
|
1429
1738
|
try {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1739
|
+
let matched = false;
|
|
1740
|
+
for (const route of app.handlers[ctx.method]) {
|
|
1741
|
+
const params = pathPattern(route.path, ctx.url.pathname || "/");
|
|
1742
|
+
if (!params) continue;
|
|
1743
|
+
matched = true;
|
|
1744
|
+
define(ctx.url, "params", () => params);
|
|
1745
|
+
if (Object.keys(route.options).length) {
|
|
1746
|
+
ctx.options = { ...app.settings, ...route.options };
|
|
1747
|
+
}
|
|
1748
|
+
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
1749
|
+
for (const cb of route.fns) {
|
|
1435
1750
|
if (typeof cb === "function") {
|
|
1436
1751
|
const res = await cb(ctx);
|
|
1437
1752
|
const out = await parseResponse(res, ctx);
|
|
@@ -1440,7 +1755,14 @@ async function getResponse(handlers, ctx) {
|
|
|
1440
1755
|
validate(ctx, cb);
|
|
1441
1756
|
}
|
|
1442
1757
|
}
|
|
1443
|
-
|
|
1758
|
+
break;
|
|
1759
|
+
}
|
|
1760
|
+
if (!matched) {
|
|
1761
|
+
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
1762
|
+
for (const mw of app.middleware) {
|
|
1763
|
+
const out = await parseResponse(await mw(ctx), ctx);
|
|
1764
|
+
if (out) return out;
|
|
1765
|
+
}
|
|
1444
1766
|
}
|
|
1445
1767
|
if (ctx.platform.provider === "netlify") return;
|
|
1446
1768
|
throw new ServerError_default("NOT_FOUND", 404, "Not Found");
|
|
@@ -1455,7 +1777,7 @@ async function getResponse(handlers, ctx) {
|
|
|
1455
1777
|
import * as crypto2 from "crypto";
|
|
1456
1778
|
import { getRandomValues } from "crypto";
|
|
1457
1779
|
import { promisify } from "util";
|
|
1458
|
-
async function
|
|
1780
|
+
async function hash2(password) {
|
|
1459
1781
|
if ("argon2" in crypto2) {
|
|
1460
1782
|
const argon23 = promisify(crypto2.argon2);
|
|
1461
1783
|
const buf = await argon23("argon2id", {
|
|
@@ -1519,102 +1841,6 @@ function iteratorToReadable(generator) {
|
|
|
1519
1841
|
});
|
|
1520
1842
|
}
|
|
1521
1843
|
|
|
1522
|
-
// src/helpers/parseBody.ts
|
|
1523
|
-
function getBoundary(header) {
|
|
1524
|
-
if (!header) return null;
|
|
1525
|
-
if (header.includes("multipart/form-data") && !header.includes("boundary=")) {
|
|
1526
|
-
console.error("Do not set the `Content-Type` manually for FormData");
|
|
1527
|
-
}
|
|
1528
|
-
const items = header.split(";");
|
|
1529
|
-
for (const item of items) {
|
|
1530
|
-
const trimmedItem = item.trim();
|
|
1531
|
-
if (trimmedItem.startsWith("boundary=")) {
|
|
1532
|
-
return trimmedItem.split("=")[1].trim();
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
return null;
|
|
1536
|
-
}
|
|
1537
|
-
function getMatching(string, regex) {
|
|
1538
|
-
const matches = string.match(regex);
|
|
1539
|
-
return matches?.[1] ?? "";
|
|
1540
|
-
}
|
|
1541
|
-
function splitBuffer(buffer, delimiter) {
|
|
1542
|
-
const result = [];
|
|
1543
|
-
let start = 0;
|
|
1544
|
-
let index = buffer.indexOf(delimiter);
|
|
1545
|
-
while (index !== -1) {
|
|
1546
|
-
result.push(buffer.slice(start, index));
|
|
1547
|
-
start = index + delimiter.length;
|
|
1548
|
-
index = buffer.indexOf(delimiter, start);
|
|
1549
|
-
}
|
|
1550
|
-
result.push(buffer.slice(start));
|
|
1551
|
-
return result;
|
|
1552
|
-
}
|
|
1553
|
-
var BREAK_BUFFER = Buffer.from("\r\n\r\n");
|
|
1554
|
-
var END_BUFFER = Buffer.from("--\r\n");
|
|
1555
|
-
function isProbablyText(buffer) {
|
|
1556
|
-
for (let i = 0; i < Math.min(buffer.length, 512); i++) {
|
|
1557
|
-
const byte = buffer[i];
|
|
1558
|
-
if (byte === 0) return false;
|
|
1559
|
-
if (byte < 7 || byte > 13 && byte < 32) return false;
|
|
1560
|
-
}
|
|
1561
|
-
return true;
|
|
1562
|
-
}
|
|
1563
|
-
async function parseBody(raw, contentType, bucket) {
|
|
1564
|
-
const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
1565
|
-
if (!raw || raw.length === 0) return {};
|
|
1566
|
-
if (!contentTypeStr || /^text\//.test(contentTypeStr)) {
|
|
1567
|
-
return raw.toString("utf-8");
|
|
1568
|
-
}
|
|
1569
|
-
if (/application\/json/.test(contentTypeStr)) {
|
|
1570
|
-
return JSON.parse(raw.toString("utf-8"));
|
|
1571
|
-
}
|
|
1572
|
-
const boundary = getBoundary(contentTypeStr);
|
|
1573
|
-
if (!boundary) return null;
|
|
1574
|
-
const body = {};
|
|
1575
|
-
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
1576
|
-
const parts = splitBuffer(raw, boundaryBuffer);
|
|
1577
|
-
for (const part of parts) {
|
|
1578
|
-
if (part.length === 0 || part.equals(END_BUFFER)) continue;
|
|
1579
|
-
const idx = part.indexOf(BREAK_BUFFER);
|
|
1580
|
-
if (idx === -1) continue;
|
|
1581
|
-
const headerStr = part.slice(0, idx).toString("utf-8");
|
|
1582
|
-
const contentBuf = part.slice(idx + BREAK_BUFFER.length, part.length - 2);
|
|
1583
|
-
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
1584
|
-
if (!name) continue;
|
|
1585
|
-
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
1586
|
-
if (filename) {
|
|
1587
|
-
const partContentType = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
1588
|
-
if (!bucket) {
|
|
1589
|
-
continue;
|
|
1590
|
-
}
|
|
1591
|
-
if (bucket instanceof UploadPipeline) {
|
|
1592
|
-
body[name] = await bucket.processFile(
|
|
1593
|
-
filename,
|
|
1594
|
-
contentBuf,
|
|
1595
|
-
partContentType
|
|
1596
|
-
);
|
|
1597
|
-
} else {
|
|
1598
|
-
body[name] = await saveFileToBucket(
|
|
1599
|
-
filename,
|
|
1600
|
-
contentBuf,
|
|
1601
|
-
bucket,
|
|
1602
|
-
partContentType
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
|
-
} else {
|
|
1606
|
-
const value = isProbablyText(contentBuf) ? contentBuf.toString("utf-8").trim() : contentBuf;
|
|
1607
|
-
if (body[name]) {
|
|
1608
|
-
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
1609
|
-
body[name].push(value);
|
|
1610
|
-
} else {
|
|
1611
|
-
body[name] = value;
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
return body;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
1844
|
// src/helpers/parseCookies.ts
|
|
1619
1845
|
function parseCookies(cookies2) {
|
|
1620
1846
|
if (!cookies2) return {};
|
|
@@ -1882,13 +2108,13 @@ function auth(app) {
|
|
|
1882
2108
|
app.use(async function middle(ctx) {
|
|
1883
2109
|
ctx.user = await getUser(ctx);
|
|
1884
2110
|
});
|
|
2111
|
+
app.post("/auth/logout", logout);
|
|
1885
2112
|
const enabled = app.settings.auth.provider;
|
|
1886
2113
|
for (const name of oauth2) {
|
|
1887
2114
|
if (!enabled.includes(name)) continue;
|
|
1888
2115
|
const key = name.toUpperCase();
|
|
1889
2116
|
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
1890
2117
|
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
1891
|
-
app.get("/auth/logout", logout);
|
|
1892
2118
|
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
1893
2119
|
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
1894
2120
|
}
|
|
@@ -1897,12 +2123,10 @@ function auth(app) {
|
|
|
1897
2123
|
for (const key of keys) {
|
|
1898
2124
|
if (!env[key]) throw new Error(`${key} not defined`);
|
|
1899
2125
|
}
|
|
1900
|
-
app.get("/auth/logout", logout);
|
|
1901
2126
|
app.get("/auth/login/apple", providers_default.apple.login);
|
|
1902
2127
|
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
1903
2128
|
}
|
|
1904
2129
|
if (enabled.includes("email")) {
|
|
1905
|
-
app.post("/auth/logout", logout);
|
|
1906
2130
|
app.post("/auth/register/email", providers_default.email.register);
|
|
1907
2131
|
app.post("/auth/login/email", providers_default.email.login);
|
|
1908
2132
|
app.put("/auth/password/email", providers_default.email.password);
|
|
@@ -1934,7 +2158,7 @@ async function favicon(ctx) {
|
|
|
1934
2158
|
return icon ? type("ico").send(icon) : 204;
|
|
1935
2159
|
}
|
|
1936
2160
|
const handled = ctx.app.handlers.get.some(
|
|
1937
|
-
(
|
|
2161
|
+
(route) => pathPattern(route.path, "/favicon.ico")
|
|
1938
2162
|
);
|
|
1939
2163
|
if (handled) return;
|
|
1940
2164
|
return 204;
|
|
@@ -1953,11 +2177,8 @@ var encode = (str = "") => {
|
|
|
1953
2177
|
if (typeof str !== "string") return "";
|
|
1954
2178
|
return str.replace(/[&<>"]/g, (tag) => entities[tag]);
|
|
1955
2179
|
};
|
|
1956
|
-
var getConfig = (
|
|
1957
|
-
const config2 =
|
|
1958
|
-
(r2) => typeof r2 !== "string" && typeof r2 !== "function" && typeof r2 === "object"
|
|
1959
|
-
);
|
|
1960
|
-
if (!config2) return {};
|
|
2180
|
+
var getConfig = (options = {}) => {
|
|
2181
|
+
const config2 = { ...options };
|
|
1961
2182
|
if (config2.tags) {
|
|
1962
2183
|
if (typeof config2.tags === "string") {
|
|
1963
2184
|
config2.tags = config2.tags.split(/\s*,\s*/g);
|
|
@@ -2002,13 +2223,10 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2002
2223
|
const paths = {};
|
|
2003
2224
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2004
2225
|
for (const route of routes) {
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
route.find((p) => typeof p === "object")
|
|
2010
|
-
];
|
|
2011
|
-
const config2 = getConfig(route);
|
|
2226
|
+
const path2 = route.path;
|
|
2227
|
+
const fn = route.fns.find((p) => typeof p === "function");
|
|
2228
|
+
const meta = route.fns.find((p) => typeof p === "object");
|
|
2229
|
+
const config2 = getConfig(route.options);
|
|
2012
2230
|
if (typeof path2 !== "string" || path2 === "*" || path2 === "/docs" || !fn) {
|
|
2013
2231
|
continue;
|
|
2014
2232
|
}
|
|
@@ -2107,7 +2325,7 @@ function preflight(ctx) {
|
|
|
2107
2325
|
if (ctx.method !== "options") return;
|
|
2108
2326
|
if (!ctx.headers["access-control-request-method"]) return;
|
|
2109
2327
|
const handled = ctx.app.handlers.options.some(
|
|
2110
|
-
(
|
|
2328
|
+
(route) => pathPattern(route.path, ctx.url.pathname)
|
|
2111
2329
|
);
|
|
2112
2330
|
if (handled) return;
|
|
2113
2331
|
return 204;
|
|
@@ -2218,21 +2436,20 @@ async function createNode(req, app) {
|
|
|
2218
2436
|
"query",
|
|
2219
2437
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
2220
2438
|
);
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
}
|
|
2228
|
-
const body = rawBody ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
|
|
2439
|
+
const source = {
|
|
2440
|
+
getBuffer: () => new Promise((resolve2, reject) => {
|
|
2441
|
+
const chunks2 = [];
|
|
2442
|
+
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve2(Buffer.concat(chunks2))).on("error", reject);
|
|
2443
|
+
}),
|
|
2444
|
+
getStream: () => toWeb(req)
|
|
2445
|
+
};
|
|
2229
2446
|
const events = createEvents();
|
|
2230
|
-
|
|
2447
|
+
const ctx = {
|
|
2231
2448
|
options: app.settings,
|
|
2232
2449
|
platform: app.platform,
|
|
2233
2450
|
url,
|
|
2234
2451
|
method,
|
|
2235
|
-
body,
|
|
2452
|
+
body: void 0,
|
|
2236
2453
|
headers: headers2,
|
|
2237
2454
|
cookies: cookies2,
|
|
2238
2455
|
session: {},
|
|
@@ -2244,6 +2461,8 @@ async function createNode(req, app) {
|
|
|
2244
2461
|
trustProxy: app.settings.security.trustProxy
|
|
2245
2462
|
})
|
|
2246
2463
|
};
|
|
2464
|
+
setBodySource(ctx, source);
|
|
2465
|
+
return ctx;
|
|
2247
2466
|
}
|
|
2248
2467
|
|
|
2249
2468
|
// src/context/winter.ts
|
|
@@ -2262,18 +2481,17 @@ async function createWinter(req, app, server2) {
|
|
|
2262
2481
|
"query",
|
|
2263
2482
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
2264
2483
|
);
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
}
|
|
2269
|
-
const body = req.body ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
|
|
2484
|
+
const source = {
|
|
2485
|
+
getBuffer: async () => Buffer.from(await req.arrayBuffer()),
|
|
2486
|
+
getStream: () => req.body ?? void 0
|
|
2487
|
+
};
|
|
2270
2488
|
const events = createEvents();
|
|
2271
|
-
|
|
2489
|
+
const ctx = {
|
|
2272
2490
|
options: app.settings,
|
|
2273
2491
|
platform: app.platform,
|
|
2274
2492
|
url,
|
|
2275
2493
|
method,
|
|
2276
|
-
body,
|
|
2494
|
+
body: void 0,
|
|
2277
2495
|
headers: headers2,
|
|
2278
2496
|
cookies: cookies2,
|
|
2279
2497
|
session: {},
|
|
@@ -2285,6 +2503,8 @@ async function createWinter(req, app, server2) {
|
|
|
2285
2503
|
trustProxy: app.settings.security.trustProxy
|
|
2286
2504
|
})
|
|
2287
2505
|
};
|
|
2506
|
+
setBodySource(ctx, source);
|
|
2507
|
+
return ctx;
|
|
2288
2508
|
}
|
|
2289
2509
|
|
|
2290
2510
|
// src/context/handlers.ts
|
|
@@ -2292,7 +2512,7 @@ var Winter = async (app, request, env2) => {
|
|
|
2292
2512
|
if (env2?.upgrade(request)) return;
|
|
2293
2513
|
Object.assign(globalThis.env, env2);
|
|
2294
2514
|
const ctx = await createWinter(request, app, env2);
|
|
2295
|
-
const res = await handleRequest(app
|
|
2515
|
+
const res = await handleRequest(app, ctx);
|
|
2296
2516
|
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
2297
2517
|
return res;
|
|
2298
2518
|
};
|
|
@@ -2301,7 +2521,7 @@ var Node = async (app) => {
|
|
|
2301
2521
|
http.createServer(async (request, response) => {
|
|
2302
2522
|
const ctx = await createNode(request, app);
|
|
2303
2523
|
if ("error" in ctx) throw ctx.error;
|
|
2304
|
-
const out = await handleRequest(app
|
|
2524
|
+
const out = await handleRequest(app, ctx);
|
|
2305
2525
|
response.writeHead(out.status || 200, parseHeaders_default(out.headers));
|
|
2306
2526
|
if (out.body instanceof ReadableStream) {
|
|
2307
2527
|
await iterate(out.body, (chunk) => response.write(chunk));
|
|
@@ -2311,6 +2531,11 @@ var Node = async (app) => {
|
|
|
2311
2531
|
response.end();
|
|
2312
2532
|
}).listen(app.settings.port, () => {
|
|
2313
2533
|
app.settings.log.start(`http://localhost:${app.settings.port}/`);
|
|
2534
|
+
if (app.handlers.socket.length) {
|
|
2535
|
+
console.warn(
|
|
2536
|
+
"[server] WebSockets (.socket()) are only supported on Bun, not Node"
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2314
2539
|
});
|
|
2315
2540
|
};
|
|
2316
2541
|
var Netlify = async (app, request, context) => {
|
|
@@ -2319,16 +2544,16 @@ var Netlify = async (app, request, context) => {
|
|
|
2319
2544
|
throw new Error("Netlify doesn't exist");
|
|
2320
2545
|
}
|
|
2321
2546
|
const ctx = await createWinter(request, app);
|
|
2322
|
-
const res = await handleRequest(app
|
|
2547
|
+
const res = await handleRequest(app, ctx);
|
|
2323
2548
|
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
2324
2549
|
return res;
|
|
2325
2550
|
};
|
|
2326
2551
|
|
|
2327
2552
|
// src/router.ts
|
|
2328
|
-
function isMiddleware(x) {
|
|
2329
|
-
return typeof x === "function";
|
|
2330
|
-
}
|
|
2331
2553
|
var Router = class _Router {
|
|
2554
|
+
// Cross-cutting middleware added with .use(); they run on every request
|
|
2555
|
+
middleware = [];
|
|
2556
|
+
// Routes per method, each carrying its own (already-flattened) chain of fns
|
|
2332
2557
|
handlers = {
|
|
2333
2558
|
socket: [],
|
|
2334
2559
|
get: [],
|
|
@@ -2344,79 +2569,67 @@ var Router = class _Router {
|
|
|
2344
2569
|
self() {
|
|
2345
2570
|
return this;
|
|
2346
2571
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2572
|
+
// Registers one route: bakes the current middleware + the route's own
|
|
2573
|
+
// functions into a single flat `fns` list. A plain options object may sit
|
|
2574
|
+
// between the path and the handlers, and it's pulled out here.
|
|
2575
|
+
handle(method, pathOrFn, ...rest) {
|
|
2576
|
+
let path2 = "*";
|
|
2577
|
+
if (typeof pathOrFn === "string") {
|
|
2578
|
+
path2 = pathOrFn;
|
|
2579
|
+
} else if (pathOrFn != null) {
|
|
2580
|
+
rest.unshift(pathOrFn);
|
|
2581
|
+
}
|
|
2582
|
+
let options = {};
|
|
2583
|
+
if (rest[0] != null && typeof rest[0] !== "function") {
|
|
2584
|
+
options = rest.shift();
|
|
2585
|
+
}
|
|
2586
|
+
const base = method === "socket" ? [] : this.middleware;
|
|
2587
|
+
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
2588
|
+
this.handlers[method].push({ path: path2, options, fns });
|
|
2356
2589
|
return this.self();
|
|
2357
2590
|
}
|
|
2358
2591
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
2359
|
-
|
|
2360
|
-
return this.handle("socket", pathOrMid, optionsOrMid, ...middleware);
|
|
2361
|
-
}
|
|
2362
|
-
return this.handle("socket", pathOrMid, ...middleware);
|
|
2592
|
+
return this.handle("socket", pathOrMid, optionsOrMid, ...middleware);
|
|
2363
2593
|
}
|
|
2364
2594
|
get(pathOrMid, optionsOrMid, ...middleware) {
|
|
2365
|
-
|
|
2366
|
-
return this.handle("get", pathOrMid, optionsOrMid, ...middleware);
|
|
2367
|
-
}
|
|
2368
|
-
return this.handle("get", pathOrMid, ...middleware);
|
|
2595
|
+
return this.handle("get", pathOrMid, optionsOrMid, ...middleware);
|
|
2369
2596
|
}
|
|
2370
2597
|
head(pathOrMid, optionsOrMid, ...middleware) {
|
|
2371
|
-
|
|
2372
|
-
return this.handle("head", pathOrMid, optionsOrMid, ...middleware);
|
|
2373
|
-
}
|
|
2374
|
-
return this.handle("head", pathOrMid, ...middleware);
|
|
2598
|
+
return this.handle("head", pathOrMid, optionsOrMid, ...middleware);
|
|
2375
2599
|
}
|
|
2376
2600
|
post(pathOrMid, optionsOrMid, ...middleware) {
|
|
2377
|
-
|
|
2378
|
-
return this.handle("post", pathOrMid, optionsOrMid, ...middleware);
|
|
2379
|
-
}
|
|
2380
|
-
return this.handle("post", pathOrMid, ...middleware);
|
|
2601
|
+
return this.handle("post", pathOrMid, optionsOrMid, ...middleware);
|
|
2381
2602
|
}
|
|
2382
2603
|
put(pathOrMid, optionsOrMid, ...middleware) {
|
|
2383
|
-
|
|
2384
|
-
return this.handle("put", pathOrMid, optionsOrMid, ...middleware);
|
|
2385
|
-
}
|
|
2386
|
-
return this.handle("put", pathOrMid, ...middleware);
|
|
2604
|
+
return this.handle("put", pathOrMid, optionsOrMid, ...middleware);
|
|
2387
2605
|
}
|
|
2388
2606
|
patch(pathOrMid, optionsOrMid, ...middleware) {
|
|
2389
|
-
|
|
2390
|
-
return this.handle("patch", pathOrMid, optionsOrMid, ...middleware);
|
|
2391
|
-
}
|
|
2392
|
-
return this.handle("patch", pathOrMid, ...middleware);
|
|
2607
|
+
return this.handle("patch", pathOrMid, optionsOrMid, ...middleware);
|
|
2393
2608
|
}
|
|
2394
2609
|
delete(pathOrMid, optionsOrMid, ...middleware) {
|
|
2395
|
-
|
|
2396
|
-
return this.handle("delete", pathOrMid, optionsOrMid, ...middleware);
|
|
2397
|
-
}
|
|
2398
|
-
return this.handle("delete", pathOrMid, ...middleware);
|
|
2610
|
+
return this.handle("delete", pathOrMid, optionsOrMid, ...middleware);
|
|
2399
2611
|
}
|
|
2400
2612
|
options(pathOrMid, optionsOrMid, ...middleware) {
|
|
2401
|
-
|
|
2402
|
-
return this.handle("options", pathOrMid, optionsOrMid, ...middleware);
|
|
2403
|
-
}
|
|
2404
|
-
return this.handle("options", pathOrMid, ...middleware);
|
|
2613
|
+
return this.handle("options", pathOrMid, optionsOrMid, ...middleware);
|
|
2405
2614
|
}
|
|
2406
2615
|
use(...args) {
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2616
|
+
for (const arg of args) {
|
|
2617
|
+
if (arg instanceof _Router) {
|
|
2618
|
+
for (const m of Object.keys(arg.handlers)) {
|
|
2619
|
+
for (const route of arg.handlers[m]) {
|
|
2620
|
+
const base = m === "socket" ? [] : this.middleware;
|
|
2621
|
+
this.handlers[m].push({
|
|
2622
|
+
path: route.path,
|
|
2623
|
+
options: route.options,
|
|
2624
|
+
fns: [...base, ...route.fns]
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2415
2627
|
}
|
|
2628
|
+
} else {
|
|
2629
|
+
this.middleware.push(arg);
|
|
2416
2630
|
}
|
|
2417
|
-
return this.self();
|
|
2418
2631
|
}
|
|
2419
|
-
return this.
|
|
2632
|
+
return this.self();
|
|
2420
2633
|
}
|
|
2421
2634
|
};
|
|
2422
2635
|
function router() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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",
|