@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.
Files changed (3) hide show
  1. package/index.d.ts +18 -12
  2. package/index.js +546 -333
  3. 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
- handlers: Record<Method, FullRoute>;
264
+ middleware: Middleware[];
265
+ handlers: Record<Method, Route[]>;
258
266
  self(): this;
259
- handle(method: RouterMethod, path: PathOrMiddle<O>, ...middleware: Middleware<O>[]): this;
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 hash(password),
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 hash(passwords.updated);
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
- return fsp.writeFile(fullPath, value, type2).then(() => fullPath);
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
- return fs.createWriteStream(fullPath);
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, { expiresIn: 3600, acl: "public-read-write" });
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: async (socket, body) => {
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
- handlers.socket?.filter((s) => s[1] === "open")?.map((s) => s[2]({ socket, sockets, body: void 0 }));
1518
+ run("open", socket);
1213
1519
  },
1214
1520
  close: (socket) => {
1215
1521
  sockets.splice(sockets.indexOf(socket), 1);
1216
- handlers.socket?.filter((s) => s[1] === "close")?.map((s) => s[2]({ socket, sockets, body: void 0 }));
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(handlers, ctx) {
1424
- const res = await getResponse(handlers, ctx);
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(handlers, ctx) {
1737
+ async function getResponse(app, ctx) {
1429
1738
  try {
1430
- for (const [method, matcher, ...cbs] of handlers[ctx.method]) {
1431
- const match = pathPattern(matcher, ctx.url.pathname || "/");
1432
- if (!match) continue;
1433
- define(ctx.url, "params", () => match);
1434
- for (const cb of cbs) {
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
- if (method !== "*") break;
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 hash(password) {
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
- ([method, matcher]) => method !== "*" && pathPattern(matcher, "/favicon.ico")
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 = (routes) => {
1957
- const config2 = routes.find(
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 [_, path2, fn, meta] = [
2006
- route[0],
2007
- route[1],
2008
- route.find((p) => typeof p === "function"),
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
- ([method, matcher]) => method !== "*" && pathPattern(matcher, ctx.url.pathname)
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 rawBody = await new Promise((resolve2, reject) => {
2222
- const body2 = [];
2223
- req.on("data", (chunk) => body2.push(chunk)).on("end", () => resolve2(Buffer.concat(body2))).on("error", reject);
2224
- });
2225
- if (rawBody.length && !headers2["content-length"]) {
2226
- headers2["content-length"] = String(rawBody.length);
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
- return {
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 rawBody = Buffer.from(await req.arrayBuffer());
2266
- if (rawBody.length && !headers2["content-length"]) {
2267
- headers2["content-length"] = String(rawBody.length);
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
- return {
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.handlers, ctx);
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.handlers, ctx);
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.handlers, ctx);
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
- handle(method, path2, ...middleware) {
2348
- if (typeof path2 !== "string") {
2349
- middleware.unshift(path2);
2350
- path2 = "*";
2351
- }
2352
- const methods2 = method === "*" ? Object.keys(this.handlers) : [method];
2353
- for (const m of methods2) {
2354
- this.handlers[m].push([method, path2, ...middleware]);
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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
- if (typeof pathOrMid === "string" && isMiddleware(optionsOrMid)) {
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 path2 = typeof args[0] === "string" ? args.shift() : "*";
2408
- if (args[0] instanceof _Router) {
2409
- const basePath = `/${path2.replace(/\*$/, "")}/`.replace(/^\/+/, "/").replace(/\/+$/, "/");
2410
- const handlers = args[0].handlers;
2411
- for (const m in handlers) {
2412
- for (const [method, path3, ...middleware] of handlers[m]) {
2413
- const fullPath = basePath + path3.replace(/^\//, "");
2414
- this.handlers[m].push([method, fullPath, ...middleware]);
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.handle("*", path2, ...args);
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.30.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",