@server/next 0.31.0 → 0.33.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 +13 -16
- package/index.js +480 -301
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -27,10 +27,12 @@ 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;
|
|
34
36
|
};
|
|
35
37
|
type Route = {
|
|
36
38
|
path: string;
|
|
@@ -48,9 +50,11 @@ type Cookie = {
|
|
|
48
50
|
};
|
|
49
51
|
type RouterMethod = "*" | Method;
|
|
50
52
|
type Bucket = {
|
|
53
|
+
location?: string;
|
|
51
54
|
read: (path: string) => Promise<ReadableStream | null>;
|
|
52
|
-
write: (path: string, data: string | Buffer) => Promise<void | string>;
|
|
55
|
+
write: (path: string, data: string | Buffer | ReadableStream) => Promise<void | string>;
|
|
53
56
|
delete: (path: string) => Promise<boolean>;
|
|
57
|
+
folder?: (prefix: string) => Bucket;
|
|
54
58
|
};
|
|
55
59
|
type UploadedFile = {
|
|
56
60
|
name: string;
|
|
@@ -100,17 +104,16 @@ type AuthUser<T = Record<string, any>> = T & {
|
|
|
100
104
|
strategy: Strategy;
|
|
101
105
|
email: string;
|
|
102
106
|
};
|
|
103
|
-
type
|
|
104
|
-
type AuthOption = `${Strategy}:${Provider | ProviderString}` | {
|
|
105
|
-
provider: Provider | ProviderString | Provider[];
|
|
107
|
+
type AuthOption = `${Strategy}:${Provider}` | {
|
|
106
108
|
strategy: Strategy;
|
|
109
|
+
providers: Provider | Provider[];
|
|
107
110
|
session?: KVStore;
|
|
108
111
|
store?: KVStore;
|
|
109
112
|
redirect?: string;
|
|
110
113
|
cleanUser?: <T = AuthUser>(user: T) => T | Promise<T>;
|
|
111
114
|
};
|
|
112
115
|
type AuthSettings = {
|
|
113
|
-
|
|
116
|
+
providers: Provider[];
|
|
114
117
|
strategy: Strategy;
|
|
115
118
|
store: KVStore;
|
|
116
119
|
session: KVStore;
|
|
@@ -134,7 +137,6 @@ type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
|
|
|
134
137
|
type Options = {
|
|
135
138
|
port?: number;
|
|
136
139
|
secret?: string;
|
|
137
|
-
views?: string | Bucket;
|
|
138
140
|
public?: string | Bucket;
|
|
139
141
|
uploads?: string | Bucket | UploadPipeline;
|
|
140
142
|
store?: KVStore;
|
|
@@ -149,11 +151,11 @@ type Options = {
|
|
|
149
151
|
log?: LogLevel | boolean;
|
|
150
152
|
favicon?: string | Bucket;
|
|
151
153
|
security?: SecurityOptions;
|
|
154
|
+
body?: BodyMode;
|
|
152
155
|
};
|
|
153
156
|
type Settings = {
|
|
154
157
|
port: number;
|
|
155
158
|
secret: string;
|
|
156
|
-
views?: Bucket;
|
|
157
159
|
public?: Bucket;
|
|
158
160
|
uploads?: Bucket | UploadPipeline;
|
|
159
161
|
store?: KVStore;
|
|
@@ -168,6 +170,7 @@ type Settings = {
|
|
|
168
170
|
log: Logger;
|
|
169
171
|
favicon?: string | Bucket;
|
|
170
172
|
security: SecuritySettings;
|
|
173
|
+
body: BodyMode;
|
|
171
174
|
};
|
|
172
175
|
type Time = {
|
|
173
176
|
(name: string): void;
|
|
@@ -193,17 +196,12 @@ type PathToParams<Path extends string> = ParamsToObject<ExtractPathParams<Path>>
|
|
|
193
196
|
type BunEnv = Record<string, string> & {
|
|
194
197
|
upgrade?: (req: Request) => boolean;
|
|
195
198
|
};
|
|
196
|
-
type EventCallback = (data: Context & SerializableValue) => void;
|
|
197
|
-
type Events = Record<string, EventCallback[]> & {
|
|
198
|
-
on?: (key: string, cb: (value?: Context & SerializableValue) => void) => void;
|
|
199
|
-
trigger?: (key: string, value?: Partial<Context & SerializableValue>) => void;
|
|
200
|
-
};
|
|
201
199
|
type Context<Params extends Record<string, string | undefined> = Record<string, string>, O extends ServerConfig = object> = {
|
|
202
200
|
method: Method;
|
|
203
201
|
ip: string;
|
|
204
202
|
headers: Record<string, string | string[]>;
|
|
205
203
|
cookies: Record<string, string>;
|
|
206
|
-
body?: SerializableValue;
|
|
204
|
+
body?: SerializableValue | Buffer | ReadableStream;
|
|
207
205
|
url: URL & {
|
|
208
206
|
params: Params;
|
|
209
207
|
query: Record<string, string>;
|
|
@@ -220,7 +218,6 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
|
|
|
220
218
|
User?: infer U;
|
|
221
219
|
} ? U extends Record<"User", infer Inner> ? Inner : AuthUser : AuthUser;
|
|
222
220
|
init: number;
|
|
223
|
-
events: Events;
|
|
224
221
|
req?: Request;
|
|
225
222
|
res?: Response & {
|
|
226
223
|
cookies?: Record<string, string>;
|
|
@@ -230,7 +227,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
|
|
|
230
227
|
type InlineReply = Response | {
|
|
231
228
|
body: string;
|
|
232
229
|
headers?: Headers;
|
|
233
|
-
} | SerializableValue | JSX.Element;
|
|
230
|
+
} | SerializableValue | JSX.Element | Buffer | ReadableStream;
|
|
234
231
|
type Body = InlineReply;
|
|
235
232
|
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>;
|
|
236
233
|
|
|
@@ -454,4 +451,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
454
451
|
}
|
|
455
452
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
456
453
|
|
|
457
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type
|
|
454
|
+
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 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
|
@@ -54,7 +54,7 @@ ServerError_default.extend({
|
|
|
54
54
|
message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
|
|
55
55
|
},
|
|
56
56
|
AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
|
|
57
|
-
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.
|
|
57
|
+
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.providers'",
|
|
58
58
|
AUTH_INVALID_PROVIDER: {
|
|
59
59
|
status: 401,
|
|
60
60
|
message: "Invalid provider '{provider}', valid ones are: '{valid}'"
|
|
@@ -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
|
}
|
|
@@ -716,37 +1113,27 @@ function defaultCleanUser(fullUser) {
|
|
|
716
1113
|
const { password: _password, ...user } = fullUser;
|
|
717
1114
|
return user;
|
|
718
1115
|
}
|
|
719
|
-
var
|
|
720
|
-
function getProviders(provider) {
|
|
721
|
-
if (typeof provider === "string") {
|
|
722
|
-
provider = provider.split("|");
|
|
723
|
-
}
|
|
724
|
-
const invalidProvider = provider.find((p) => !providersKeys.includes(p));
|
|
725
|
-
if (invalidProvider) {
|
|
726
|
-
throw new Error(
|
|
727
|
-
`Provider "${invalidProvider}" not found, available ones are "${providersKeys.join('", "')}"`
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
return provider;
|
|
731
|
-
}
|
|
1116
|
+
var available = Object.keys(providers_default);
|
|
732
1117
|
function parseAuthOptions(auth2, all) {
|
|
733
1118
|
if (!auth2) return null;
|
|
734
1119
|
if (typeof auth2 === "string") {
|
|
735
|
-
const [strategy2,
|
|
736
|
-
|
|
737
|
-
auth2 = { strategy: strategy2, provider: provider2 };
|
|
1120
|
+
const [strategy2, provider] = auth2.split(":");
|
|
1121
|
+
auth2 = { strategy: strategy2, providers: provider ? [provider] : [] };
|
|
738
1122
|
}
|
|
739
|
-
if (!auth2.strategy) {
|
|
740
|
-
throw new Error("Auth options needs a strategy");
|
|
741
|
-
}
|
|
742
|
-
if (!auth2.strategy.length) {
|
|
1123
|
+
if (!auth2.strategy?.length) {
|
|
743
1124
|
throw new Error("Auth options needs a strategy");
|
|
744
1125
|
}
|
|
745
1126
|
const strategy = auth2.strategy;
|
|
746
|
-
|
|
1127
|
+
const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
|
|
1128
|
+
if (!list.length) {
|
|
747
1129
|
throw new Error("Auth options needs a provider");
|
|
748
1130
|
}
|
|
749
|
-
const
|
|
1131
|
+
const invalid = list.find((p) => !available.includes(p));
|
|
1132
|
+
if (invalid) {
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`Provider "${invalid}" not found, available ones are "${available.join('", "')}"`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
750
1137
|
const redirect2 = auth2.redirect || defaultRedirect;
|
|
751
1138
|
const cleanUser = auth2.cleanUser || defaultCleanUser;
|
|
752
1139
|
if (!auth2.store && !all.store) {
|
|
@@ -758,13 +1145,10 @@ function parseAuthOptions(auth2, all) {
|
|
|
758
1145
|
const store = auth2.store || all.store.prefix("user:");
|
|
759
1146
|
const session2 = auth2.session || all.store.prefix("auth:");
|
|
760
1147
|
return {
|
|
761
|
-
// Base main configuration
|
|
762
1148
|
strategy,
|
|
763
|
-
|
|
764
|
-
// Extra configuration
|
|
1149
|
+
providers: list,
|
|
765
1150
|
redirect: redirect2,
|
|
766
1151
|
cleanUser,
|
|
767
|
-
// Stores for the auth session and users
|
|
768
1152
|
store,
|
|
769
1153
|
session: session2
|
|
770
1154
|
};
|
|
@@ -797,12 +1181,23 @@ function thinLocalBucket(root) {
|
|
|
797
1181
|
}
|
|
798
1182
|
});
|
|
799
1183
|
},
|
|
800
|
-
write: (name, value, type2) => {
|
|
1184
|
+
write: async (name, value, type2) => {
|
|
801
1185
|
const fullPath = absolute(name);
|
|
802
|
-
if (value)
|
|
803
|
-
|
|
1186
|
+
if (!value) return fs.createWriteStream(fullPath);
|
|
1187
|
+
await fsp.mkdir(path.dirname(fullPath), { recursive: true });
|
|
1188
|
+
if (value instanceof ReadableStream) {
|
|
1189
|
+
const writable = fs.createWriteStream(fullPath);
|
|
1190
|
+
for await (const chunk of value) {
|
|
1191
|
+
writable.write(chunk);
|
|
1192
|
+
}
|
|
1193
|
+
await new Promise((resolve2, reject) => {
|
|
1194
|
+
writable.on("error", reject);
|
|
1195
|
+
writable.end(resolve2);
|
|
1196
|
+
});
|
|
1197
|
+
return fullPath;
|
|
804
1198
|
}
|
|
805
|
-
|
|
1199
|
+
await fsp.writeFile(fullPath, value, type2);
|
|
1200
|
+
return fullPath;
|
|
806
1201
|
},
|
|
807
1202
|
delete: async (name) => {
|
|
808
1203
|
const fullPath = absolute(name);
|
|
@@ -812,29 +1207,35 @@ function thinLocalBucket(root) {
|
|
|
812
1207
|
} catch {
|
|
813
1208
|
return false;
|
|
814
1209
|
}
|
|
815
|
-
}
|
|
1210
|
+
},
|
|
1211
|
+
folder: (prefix) => thinLocalBucket(path.join(root, prefix))
|
|
816
1212
|
};
|
|
817
1213
|
}
|
|
818
|
-
function thinBunBucket(s3) {
|
|
1214
|
+
function thinBunBucket(s3, prefix = "") {
|
|
1215
|
+
const key = (name) => prefix ? `${prefix}/${name}` : name;
|
|
819
1216
|
return {
|
|
820
1217
|
read: async (name) => {
|
|
821
|
-
const file2 = s3.file(name);
|
|
1218
|
+
const file2 = s3.file(key(name));
|
|
822
1219
|
if (!await file2.exists()) return null;
|
|
823
1220
|
return await file2.stream();
|
|
824
1221
|
},
|
|
825
1222
|
write: async (name, value) => {
|
|
826
|
-
const file2 = s3.file(name);
|
|
1223
|
+
const file2 = s3.file(key(name));
|
|
827
1224
|
if (value) {
|
|
828
1225
|
await file2.write(value);
|
|
829
|
-
return name;
|
|
1226
|
+
return key(name);
|
|
830
1227
|
}
|
|
831
|
-
return s3.presign(name, {
|
|
1228
|
+
return s3.presign(key(name), {
|
|
1229
|
+
expiresIn: 3600,
|
|
1230
|
+
acl: "public-read-write"
|
|
1231
|
+
});
|
|
832
1232
|
},
|
|
833
1233
|
delete: async (name) => {
|
|
834
|
-
const file2 = s3.file(name);
|
|
1234
|
+
const file2 = s3.file(key(name));
|
|
835
1235
|
if (!await file2.exists()) return null;
|
|
836
1236
|
return await file2.delete();
|
|
837
|
-
}
|
|
1237
|
+
},
|
|
1238
|
+
folder: (sub) => thinBunBucket(s3, key(sub))
|
|
838
1239
|
};
|
|
839
1240
|
}
|
|
840
1241
|
function bucket_default(root) {
|
|
@@ -848,47 +1249,6 @@ function bucket_default(root) {
|
|
|
848
1249
|
return root;
|
|
849
1250
|
}
|
|
850
1251
|
|
|
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
1252
|
// src/helpers/color.ts
|
|
893
1253
|
var map = {
|
|
894
1254
|
reset: 0,
|
|
@@ -998,80 +1358,6 @@ function createLogger(level) {
|
|
|
998
1358
|
};
|
|
999
1359
|
}
|
|
1000
1360
|
|
|
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
1361
|
// src/helpers/config.ts
|
|
1076
1362
|
function config(options = {}) {
|
|
1077
1363
|
const env2 = globalThis.env;
|
|
@@ -1082,6 +1368,9 @@ function config(options = {}) {
|
|
|
1082
1368
|
port: options.port || env2.PORT || 3e3,
|
|
1083
1369
|
secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
|
|
1084
1370
|
log,
|
|
1371
|
+
// How request bodies are read: parsed into ctx.body by default; `raw` keeps
|
|
1372
|
+
// the Buffer, `stream` hands the handler the unread web ReadableStream.
|
|
1373
|
+
body: options.body ?? "parse",
|
|
1085
1374
|
// Trust X-Forwarded-* headers for ctx.ip (on by default; set it to false
|
|
1086
1375
|
// when clients connect directly so a client can't spoof its IP).
|
|
1087
1376
|
security: {
|
|
@@ -1124,7 +1413,6 @@ function config(options = {}) {
|
|
|
1124
1413
|
}
|
|
1125
1414
|
settings.cors = cors2;
|
|
1126
1415
|
}
|
|
1127
|
-
settings.views = options.views ? bucket_default(options.views) : null;
|
|
1128
1416
|
settings.public = options.public ? bucket_default(options.public) : null;
|
|
1129
1417
|
settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
|
|
1130
1418
|
if (options.favicon) settings.favicon = options.favicon;
|
|
@@ -1151,10 +1439,9 @@ function config(options = {}) {
|
|
|
1151
1439
|
});
|
|
1152
1440
|
const loc = (v) => typeof v === "string" ? v : "enabled";
|
|
1153
1441
|
if (settings.auth) {
|
|
1154
|
-
log.message("auth", `${settings.auth.
|
|
1442
|
+
log.message("auth", `${settings.auth.providers.join(", ")} auth enabled`);
|
|
1155
1443
|
}
|
|
1156
1444
|
if (settings.public) log.message("public", loc(options.public));
|
|
1157
|
-
if (settings.views) log.message("views", loc(options.views));
|
|
1158
1445
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1159
1446
|
if (settings.session) log.message("session", "enabled");
|
|
1160
1447
|
if (settings.cors) {
|
|
@@ -1276,6 +1563,9 @@ async function parseResponse(out, ctx) {
|
|
|
1276
1563
|
if (out instanceof ReadableStream) {
|
|
1277
1564
|
out = new Response(out);
|
|
1278
1565
|
}
|
|
1566
|
+
if (out instanceof Uint8Array) {
|
|
1567
|
+
out = new Response(out);
|
|
1568
|
+
}
|
|
1279
1569
|
if (typeof out === "number") {
|
|
1280
1570
|
out = new Response(void 0, { status: out });
|
|
1281
1571
|
}
|
|
@@ -1442,6 +1732,7 @@ async function getResponse(app, ctx) {
|
|
|
1442
1732
|
if (Object.keys(route.options).length) {
|
|
1443
1733
|
ctx.options = { ...app.settings, ...route.options };
|
|
1444
1734
|
}
|
|
1735
|
+
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
1445
1736
|
for (const cb of route.fns) {
|
|
1446
1737
|
if (typeof cb === "function") {
|
|
1447
1738
|
const res = await cb(ctx);
|
|
@@ -1454,6 +1745,7 @@ async function getResponse(app, ctx) {
|
|
|
1454
1745
|
break;
|
|
1455
1746
|
}
|
|
1456
1747
|
if (!matched) {
|
|
1748
|
+
ctx.body = await resolveBody(ctx, ctx.options.body);
|
|
1457
1749
|
for (const mw of app.middleware) {
|
|
1458
1750
|
const out = await parseResponse(await mw(ctx), ctx);
|
|
1459
1751
|
if (out) return out;
|
|
@@ -1472,7 +1764,7 @@ async function getResponse(app, ctx) {
|
|
|
1472
1764
|
import * as crypto2 from "crypto";
|
|
1473
1765
|
import { getRandomValues } from "crypto";
|
|
1474
1766
|
import { promisify } from "util";
|
|
1475
|
-
async function
|
|
1767
|
+
async function hash2(password) {
|
|
1476
1768
|
if ("argon2" in crypto2) {
|
|
1477
1769
|
const argon23 = promisify(crypto2.argon2);
|
|
1478
1770
|
const buf = await argon23("argon2id", {
|
|
@@ -1536,102 +1828,6 @@ function iteratorToReadable(generator) {
|
|
|
1536
1828
|
});
|
|
1537
1829
|
}
|
|
1538
1830
|
|
|
1539
|
-
// src/helpers/parseBody.ts
|
|
1540
|
-
function getBoundary(header) {
|
|
1541
|
-
if (!header) return null;
|
|
1542
|
-
if (header.includes("multipart/form-data") && !header.includes("boundary=")) {
|
|
1543
|
-
console.error("Do not set the `Content-Type` manually for FormData");
|
|
1544
|
-
}
|
|
1545
|
-
const items = header.split(";");
|
|
1546
|
-
for (const item of items) {
|
|
1547
|
-
const trimmedItem = item.trim();
|
|
1548
|
-
if (trimmedItem.startsWith("boundary=")) {
|
|
1549
|
-
return trimmedItem.split("=")[1].trim();
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
return null;
|
|
1553
|
-
}
|
|
1554
|
-
function getMatching(string, regex) {
|
|
1555
|
-
const matches = string.match(regex);
|
|
1556
|
-
return matches?.[1] ?? "";
|
|
1557
|
-
}
|
|
1558
|
-
function splitBuffer(buffer, delimiter) {
|
|
1559
|
-
const result = [];
|
|
1560
|
-
let start = 0;
|
|
1561
|
-
let index = buffer.indexOf(delimiter);
|
|
1562
|
-
while (index !== -1) {
|
|
1563
|
-
result.push(buffer.slice(start, index));
|
|
1564
|
-
start = index + delimiter.length;
|
|
1565
|
-
index = buffer.indexOf(delimiter, start);
|
|
1566
|
-
}
|
|
1567
|
-
result.push(buffer.slice(start));
|
|
1568
|
-
return result;
|
|
1569
|
-
}
|
|
1570
|
-
var BREAK_BUFFER = Buffer.from("\r\n\r\n");
|
|
1571
|
-
var END_BUFFER = Buffer.from("--\r\n");
|
|
1572
|
-
function isProbablyText(buffer) {
|
|
1573
|
-
for (let i = 0; i < Math.min(buffer.length, 512); i++) {
|
|
1574
|
-
const byte = buffer[i];
|
|
1575
|
-
if (byte === 0) return false;
|
|
1576
|
-
if (byte < 7 || byte > 13 && byte < 32) return false;
|
|
1577
|
-
}
|
|
1578
|
-
return true;
|
|
1579
|
-
}
|
|
1580
|
-
async function parseBody(raw, contentType, bucket) {
|
|
1581
|
-
const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
1582
|
-
if (!raw || raw.length === 0) return {};
|
|
1583
|
-
if (!contentTypeStr || /^text\//.test(contentTypeStr)) {
|
|
1584
|
-
return raw.toString("utf-8");
|
|
1585
|
-
}
|
|
1586
|
-
if (/application\/json/.test(contentTypeStr)) {
|
|
1587
|
-
return JSON.parse(raw.toString("utf-8"));
|
|
1588
|
-
}
|
|
1589
|
-
const boundary = getBoundary(contentTypeStr);
|
|
1590
|
-
if (!boundary) return null;
|
|
1591
|
-
const body = {};
|
|
1592
|
-
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
1593
|
-
const parts = splitBuffer(raw, boundaryBuffer);
|
|
1594
|
-
for (const part of parts) {
|
|
1595
|
-
if (part.length === 0 || part.equals(END_BUFFER)) continue;
|
|
1596
|
-
const idx = part.indexOf(BREAK_BUFFER);
|
|
1597
|
-
if (idx === -1) continue;
|
|
1598
|
-
const headerStr = part.slice(0, idx).toString("utf-8");
|
|
1599
|
-
const contentBuf = part.slice(idx + BREAK_BUFFER.length, part.length - 2);
|
|
1600
|
-
const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
|
|
1601
|
-
if (!name) continue;
|
|
1602
|
-
const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
|
|
1603
|
-
if (filename) {
|
|
1604
|
-
const partContentType = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
|
|
1605
|
-
if (!bucket) {
|
|
1606
|
-
continue;
|
|
1607
|
-
}
|
|
1608
|
-
if (bucket instanceof UploadPipeline) {
|
|
1609
|
-
body[name] = await bucket.processFile(
|
|
1610
|
-
filename,
|
|
1611
|
-
contentBuf,
|
|
1612
|
-
partContentType
|
|
1613
|
-
);
|
|
1614
|
-
} else {
|
|
1615
|
-
body[name] = await saveFileToBucket(
|
|
1616
|
-
filename,
|
|
1617
|
-
contentBuf,
|
|
1618
|
-
bucket,
|
|
1619
|
-
partContentType
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
|
-
} else {
|
|
1623
|
-
const value = isProbablyText(contentBuf) ? contentBuf.toString("utf-8").trim() : contentBuf;
|
|
1624
|
-
if (body[name]) {
|
|
1625
|
-
if (!Array.isArray(body[name])) body[name] = [body[name]];
|
|
1626
|
-
body[name].push(value);
|
|
1627
|
-
} else {
|
|
1628
|
-
body[name] = value;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
return body;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
1831
|
// src/helpers/parseCookies.ts
|
|
1636
1832
|
function parseCookies(cookies2) {
|
|
1637
1833
|
if (!cookies2) return {};
|
|
@@ -1853,10 +2049,10 @@ async function getUser(ctx) {
|
|
|
1853
2049
|
valid: options.strategy
|
|
1854
2050
|
});
|
|
1855
2051
|
}
|
|
1856
|
-
if (!options.
|
|
2052
|
+
if (!options.providers.includes(auth2.provider)) {
|
|
1857
2053
|
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
1858
2054
|
provider: auth2.provider,
|
|
1859
|
-
valid: options.
|
|
2055
|
+
valid: options.providers
|
|
1860
2056
|
});
|
|
1861
2057
|
}
|
|
1862
2058
|
const user = await ctx.options.auth.store.get(auth2.user);
|
|
@@ -1899,13 +2095,13 @@ function auth(app) {
|
|
|
1899
2095
|
app.use(async function middle(ctx) {
|
|
1900
2096
|
ctx.user = await getUser(ctx);
|
|
1901
2097
|
});
|
|
1902
|
-
|
|
2098
|
+
app.post("/auth/logout", logout);
|
|
2099
|
+
const enabled = app.settings.auth.providers;
|
|
1903
2100
|
for (const name of oauth2) {
|
|
1904
2101
|
if (!enabled.includes(name)) continue;
|
|
1905
2102
|
const key = name.toUpperCase();
|
|
1906
2103
|
if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
|
|
1907
2104
|
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
1908
|
-
app.get("/auth/logout", logout);
|
|
1909
2105
|
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
1910
2106
|
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
1911
2107
|
}
|
|
@@ -1914,12 +2110,10 @@ function auth(app) {
|
|
|
1914
2110
|
for (const key of keys) {
|
|
1915
2111
|
if (!env[key]) throw new Error(`${key} not defined`);
|
|
1916
2112
|
}
|
|
1917
|
-
app.get("/auth/logout", logout);
|
|
1918
2113
|
app.get("/auth/login/apple", providers_default.apple.login);
|
|
1919
2114
|
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
1920
2115
|
}
|
|
1921
2116
|
if (enabled.includes("email")) {
|
|
1922
|
-
app.post("/auth/logout", logout);
|
|
1923
2117
|
app.post("/auth/register/email", providers_default.email.register);
|
|
1924
2118
|
app.post("/auth/login/email", providers_default.email.login);
|
|
1925
2119
|
app.put("/auth/password/email", providers_default.email.password);
|
|
@@ -2177,22 +2371,6 @@ function timer(ctx) {
|
|
|
2177
2371
|
// src/context/node.ts
|
|
2178
2372
|
import { TLSSocket } from "tls";
|
|
2179
2373
|
|
|
2180
|
-
// src/context/createEvents.ts
|
|
2181
|
-
function createEvents() {
|
|
2182
|
-
const events = {};
|
|
2183
|
-
events.on = (name, callback3) => {
|
|
2184
|
-
events[name] = events[name] || [];
|
|
2185
|
-
events[name].push(callback3);
|
|
2186
|
-
};
|
|
2187
|
-
events.trigger = (name, data) => {
|
|
2188
|
-
if (!events[name]) return;
|
|
2189
|
-
for (const cb of events[name]) {
|
|
2190
|
-
cb(data);
|
|
2191
|
-
}
|
|
2192
|
-
};
|
|
2193
|
-
return events;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
2374
|
// src/context/isValidMethod.ts
|
|
2197
2375
|
var methods = [
|
|
2198
2376
|
"get",
|
|
@@ -2229,32 +2407,31 @@ async function createNode(req, app) {
|
|
|
2229
2407
|
"query",
|
|
2230
2408
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
2231
2409
|
);
|
|
2232
|
-
const
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
}
|
|
2239
|
-
const
|
|
2240
|
-
const events = createEvents();
|
|
2241
|
-
return {
|
|
2410
|
+
const source = {
|
|
2411
|
+
getBuffer: () => new Promise((resolve2, reject) => {
|
|
2412
|
+
const chunks2 = [];
|
|
2413
|
+
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve2(Buffer.concat(chunks2))).on("error", reject);
|
|
2414
|
+
}),
|
|
2415
|
+
getStream: () => toWeb(req)
|
|
2416
|
+
};
|
|
2417
|
+
const ctx = {
|
|
2242
2418
|
options: app.settings,
|
|
2243
2419
|
platform: app.platform,
|
|
2244
2420
|
url,
|
|
2245
2421
|
method,
|
|
2246
|
-
body,
|
|
2422
|
+
body: void 0,
|
|
2247
2423
|
headers: headers2,
|
|
2248
2424
|
cookies: cookies2,
|
|
2249
2425
|
session: {},
|
|
2250
2426
|
init,
|
|
2251
|
-
events,
|
|
2252
2427
|
app,
|
|
2253
2428
|
ip: clientIp(headers2, {
|
|
2254
2429
|
remoteAddress: req.socket.remoteAddress || "",
|
|
2255
2430
|
trustProxy: app.settings.security.trustProxy
|
|
2256
2431
|
})
|
|
2257
2432
|
};
|
|
2433
|
+
setBodySource(ctx, source);
|
|
2434
|
+
return ctx;
|
|
2258
2435
|
}
|
|
2259
2436
|
|
|
2260
2437
|
// src/context/winter.ts
|
|
@@ -2273,29 +2450,28 @@ async function createWinter(req, app, server2) {
|
|
|
2273
2450
|
"query",
|
|
2274
2451
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
2275
2452
|
);
|
|
2276
|
-
const
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
}
|
|
2280
|
-
const
|
|
2281
|
-
const events = createEvents();
|
|
2282
|
-
return {
|
|
2453
|
+
const source = {
|
|
2454
|
+
getBuffer: async () => Buffer.from(await req.arrayBuffer()),
|
|
2455
|
+
getStream: () => req.body ?? void 0
|
|
2456
|
+
};
|
|
2457
|
+
const ctx = {
|
|
2283
2458
|
options: app.settings,
|
|
2284
2459
|
platform: app.platform,
|
|
2285
2460
|
url,
|
|
2286
2461
|
method,
|
|
2287
|
-
body,
|
|
2462
|
+
body: void 0,
|
|
2288
2463
|
headers: headers2,
|
|
2289
2464
|
cookies: cookies2,
|
|
2290
2465
|
session: {},
|
|
2291
2466
|
init,
|
|
2292
|
-
events,
|
|
2293
2467
|
app,
|
|
2294
2468
|
ip: clientIp(headers2, {
|
|
2295
2469
|
remoteAddress: server2?.requestIP?.(req)?.address || "",
|
|
2296
2470
|
trustProxy: app.settings.security.trustProxy
|
|
2297
2471
|
})
|
|
2298
2472
|
};
|
|
2473
|
+
setBodySource(ctx, source);
|
|
2474
|
+
return ctx;
|
|
2299
2475
|
}
|
|
2300
2476
|
|
|
2301
2477
|
// src/context/handlers.ts
|
|
@@ -2304,7 +2480,6 @@ var Winter = async (app, request, env2) => {
|
|
|
2304
2480
|
Object.assign(globalThis.env, env2);
|
|
2305
2481
|
const ctx = await createWinter(request, app, env2);
|
|
2306
2482
|
const res = await handleRequest(app, ctx);
|
|
2307
|
-
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
2308
2483
|
return res;
|
|
2309
2484
|
};
|
|
2310
2485
|
var Node = async (app) => {
|
|
@@ -2322,6 +2497,11 @@ var Node = async (app) => {
|
|
|
2322
2497
|
response.end();
|
|
2323
2498
|
}).listen(app.settings.port, () => {
|
|
2324
2499
|
app.settings.log.start(`http://localhost:${app.settings.port}/`);
|
|
2500
|
+
if (app.handlers.socket.length) {
|
|
2501
|
+
console.warn(
|
|
2502
|
+
"[server] WebSockets (.socket()) are only supported on Bun, not Node"
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2325
2505
|
});
|
|
2326
2506
|
};
|
|
2327
2507
|
var Netlify = async (app, request, context) => {
|
|
@@ -2331,7 +2511,6 @@ var Netlify = async (app, request, context) => {
|
|
|
2331
2511
|
}
|
|
2332
2512
|
const ctx = await createWinter(request, app);
|
|
2333
2513
|
const res = await handleRequest(app, ctx);
|
|
2334
|
-
ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
|
|
2335
2514
|
return res;
|
|
2336
2515
|
};
|
|
2337
2516
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|