@server/next 0.34.0 → 0.34.2

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 +28 -12
  2. package/index.js +417 -252
  3. package/package.json +2 -1
package/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import * as http from 'http';
2
+
1
3
  type LimitOptions = {
2
4
  maxSize?: number | string;
3
5
  minSize?: number | string;
@@ -6,12 +8,12 @@ type LimitOptions = {
6
8
  declare class UploadPipeline {
7
9
  private _bucket;
8
10
  private _limits;
9
- constructor(bucket?: Bucket | null);
11
+ constructor(bucket?: Bucket | string | null);
10
12
  limit(options: LimitOptions): this;
11
- store(bucket: Bucket): this;
13
+ store(bucket: Bucket | string): this;
12
14
  processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
13
15
  }
14
- declare function upload(bucket?: Bucket | null): UploadPipeline;
16
+ declare function upload(bucket?: Bucket | string | null): UploadPipeline;
15
17
 
16
18
  type Method = "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "socket";
17
19
  type ServerConfig<Session = {}, User = {}> = {
@@ -49,12 +51,21 @@ type Cookie = {
49
51
  sameSite?: "Strict" | "Lax" | "None";
50
52
  };
51
53
  type RouterMethod = "*" | Method;
54
+ type BucketFile = {
55
+ readonly path: string;
56
+ readonly id: string;
57
+ readonly name: string;
58
+ exists(): Promise<boolean>;
59
+ write(content: string | Buffer | ReadableStream, options?: {
60
+ type?: string;
61
+ }): Promise<void>;
62
+ stream(): ReadableStream;
63
+ bytes(): Promise<Uint8Array>;
64
+ remove(): Promise<void>;
65
+ };
52
66
  type Bucket = {
53
- location?: string;
54
- read: (path: string) => Promise<ReadableStream | null>;
55
- write: (path: string, data: string | Buffer | ReadableStream) => Promise<void | string>;
56
- delete: (path: string) => Promise<boolean>;
57
- folder?: (prefix: string) => Bucket;
67
+ file(name: string): BucketFile;
68
+ folder?(prefix: string): Bucket;
58
69
  };
59
70
  type UploadedFile = {
60
71
  name: string;
@@ -160,7 +171,7 @@ type Options = {
160
171
  openapi?: any;
161
172
  onError?: OnError;
162
173
  log?: LogLevel | boolean;
163
- favicon?: string | Bucket;
174
+ favicon?: string | BucketFile;
164
175
  security?: boolean | SecurityOptions;
165
176
  body?: BodyMode;
166
177
  };
@@ -179,7 +190,7 @@ type Settings = {
179
190
  openapi?: any;
180
191
  onError?: OnError;
181
192
  log: Logger;
182
- favicon?: string | Bucket;
193
+ favicon?: string | BucketFile;
183
194
  security: SecuritySettings;
184
195
  body: BodyMode;
185
196
  };
@@ -340,10 +351,15 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
340
351
  platform: Platform;
341
352
  sockets: any[];
342
353
  websocket: any;
354
+ faviconCache?: {
355
+ bytes: Buffer;
356
+ type: string;
357
+ etag: string;
358
+ } | null;
343
359
  port?: number;
344
360
  constructor(options?: Options);
345
361
  self(): this;
346
- node(): Promise<void>;
362
+ node(): Promise<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>>;
347
363
  fetch(request: Request, env?: BunEnv): Promise<Response>;
348
364
  callback(request: Request, context: unknown): Promise<Response>;
349
365
  test(): {
@@ -462,4 +478,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
462
478
  }
463
479
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
464
480
 
465
- 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 };
481
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type Bucket, type BucketFile, 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
@@ -130,6 +130,163 @@ function createId(source, size = 16) {
130
130
  return randomId(size);
131
131
  }
132
132
 
133
+ // src/helpers/mimes.ts
134
+ var mimes_default = {
135
+ aac: "audio/aac",
136
+ abw: "application/x-abiword",
137
+ arc: "application/x-freearc",
138
+ avif: "image/avif",
139
+ avi: "video/x-msvideo",
140
+ azw: "application/vnd.amazon.ebook",
141
+ bin: "application/octet-stream",
142
+ bmp: "image/bmp",
143
+ bz: "application/x-bzip",
144
+ bz2: "application/x-bzip2",
145
+ cda: "application/x-cdf",
146
+ csh: "application/x-csh",
147
+ css: "text/css",
148
+ csv: "text/csv",
149
+ doc: "application/msword",
150
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
151
+ eot: "application/vnd.ms-fontobject",
152
+ epub: "application/epub+zip",
153
+ gz: "application/gzip",
154
+ gif: "image/gif",
155
+ htm: "text/html",
156
+ html: "text/html",
157
+ ico: "image/vnd.microsoft.icon",
158
+ ics: "text/calendar",
159
+ jar: "application/java-archive",
160
+ jpeg: "image/jpeg",
161
+ jpg: "image/jpeg",
162
+ js: "text/javascript",
163
+ json: "application/json",
164
+ jsonld: "application/ld+json",
165
+ md: "text/markdown",
166
+ mid: "audio/midi",
167
+ midi: "audio/midi",
168
+ mjs: "text/javascript",
169
+ mp3: "audio/mpeg",
170
+ mp4: "video/mp4",
171
+ mpeg: "video/mpeg",
172
+ mpkg: "application/vnd.apple.installer+xml",
173
+ odp: "application/vnd.oasis.opendocument.presentation",
174
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
175
+ odt: "application/vnd.oasis.opendocument.text",
176
+ oga: "audio/ogg",
177
+ ogv: "video/ogg",
178
+ ogx: "application/ogg",
179
+ opus: "audio/opus",
180
+ otf: "font/otf",
181
+ png: "image/png",
182
+ pdf: "application/pdf",
183
+ php: "application/x-httpd-php",
184
+ ppt: "application/vnd.ms-powerpoint",
185
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
186
+ rar: "application/vnd.rar",
187
+ rtf: "application/rtf",
188
+ sh: "application/x-sh",
189
+ svg: "image/svg+xml",
190
+ tar: "application/x-tar",
191
+ text: "text/plain",
192
+ tif: "image/tiff",
193
+ tiff: "image/tiff",
194
+ ts: "video/mp2t",
195
+ ttf: "font/ttf",
196
+ txt: "text/plain",
197
+ vsd: "application/vnd.visio",
198
+ wav: "audio/wav",
199
+ weba: "audio/webm",
200
+ webm: "video/webm",
201
+ webp: "image/webp",
202
+ woff: "font/woff",
203
+ woff2: "font/woff2",
204
+ xhtml: "application/xhtml+xml",
205
+ xls: "application/vnd.ms-excel",
206
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
207
+ xml: "application/xml",
208
+ xul: "application/vnd.mozilla.xul+xml",
209
+ zip: "application/zip",
210
+ "3gp": "video/3gpp",
211
+ "3g2": "video/3gpp2",
212
+ "7z": "application/x-7z-compressed"
213
+ };
214
+
215
+ // src/helpers/bucket.ts
216
+ import * as fs from "fs";
217
+ import * as fsp from "fs/promises";
218
+ import * as path from "path";
219
+ function localBucket(root) {
220
+ const base = path.resolve(root);
221
+ const resolveKey = (name) => {
222
+ if (!name) throw new Error("File name is required");
223
+ const full = path.resolve(base, name.replace(/^\/+/, ""));
224
+ if (full !== base && !full.startsWith(base + path.sep)) {
225
+ throw new Error(`Path "${name}" escapes the bucket root`);
226
+ }
227
+ return full;
228
+ };
229
+ const file2 = (name) => {
230
+ const full = resolveKey(name);
231
+ return {
232
+ path: full,
233
+ id: name.replace(/^\/+/, ""),
234
+ name: path.basename(name),
235
+ async exists() {
236
+ const stats = await fsp.stat(full).catch(() => null);
237
+ return !!stats?.isFile();
238
+ },
239
+ async write(content) {
240
+ await fsp.mkdir(path.dirname(full), { recursive: true });
241
+ if (content instanceof ReadableStream) {
242
+ const writable = fs.createWriteStream(full);
243
+ for await (const chunk of content) {
244
+ writable.write(chunk);
245
+ }
246
+ await new Promise((resolve2, reject) => {
247
+ writable.on("error", reject);
248
+ writable.end(() => resolve2());
249
+ });
250
+ return;
251
+ }
252
+ await fsp.writeFile(full, content);
253
+ },
254
+ stream() {
255
+ const nodeStream = fs.createReadStream(full);
256
+ return new ReadableStream({
257
+ start(controller) {
258
+ nodeStream.on("data", (chunk) => controller.enqueue(chunk));
259
+ nodeStream.on("end", () => controller.close());
260
+ nodeStream.on("error", (err) => controller.error(err));
261
+ },
262
+ cancel() {
263
+ nodeStream.destroy();
264
+ }
265
+ });
266
+ },
267
+ async bytes() {
268
+ return new Uint8Array(await fsp.readFile(full));
269
+ },
270
+ async remove() {
271
+ await fsp.unlink(full).catch(() => {
272
+ });
273
+ }
274
+ };
275
+ };
276
+ return {
277
+ file: file2,
278
+ folder: (prefix) => localBucket(path.join(base, prefix))
279
+ };
280
+ }
281
+ function bucket(root) {
282
+ if (!root) return null;
283
+ if (typeof root === "string") return localBucket(root);
284
+ if (typeof root.file === "function") return root;
285
+ throw new Error(
286
+ "Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
287
+ );
288
+ }
289
+
133
290
  // src/helpers/upload.ts
134
291
  function parseBytes(value) {
135
292
  if (typeof value === "number") return value;
@@ -148,24 +305,31 @@ function getExt(filename) {
148
305
  if (i <= 0) return ".bin";
149
306
  return filename.slice(i).toLowerCase();
150
307
  }
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 };
308
+ async function saveFileToBucket(originalName, data, bucket2, contentType) {
309
+ const ext2 = getExt(originalName);
310
+ const id = `${createId()}${ext2}`;
311
+ const file2 = bucket2.file(id);
312
+ await file2.write(data, { type: contentType });
313
+ return {
314
+ name: originalName,
315
+ id,
316
+ path: file2.path,
317
+ type: contentType,
318
+ size: data.length
319
+ };
156
320
  }
157
321
  var UploadPipeline = class {
158
322
  _bucket;
159
323
  _limits = {};
160
- constructor(bucket) {
161
- this._bucket = bucket ?? null;
324
+ constructor(bucket2) {
325
+ this._bucket = bucket(bucket2 ?? void 0);
162
326
  }
163
327
  limit(options) {
164
328
  this._limits = { ...this._limits, ...options };
165
329
  return this;
166
330
  }
167
- store(bucket) {
168
- this._bucket = bucket;
331
+ store(bucket2) {
332
+ this._bucket = bucket(bucket2);
169
333
  return this;
170
334
  }
171
335
  async processFile(originalName, data, contentType) {
@@ -181,10 +345,10 @@ var UploadPipeline = class {
181
345
  );
182
346
  }
183
347
  if (fileType && fileType.length > 0) {
184
- const ext = getExt(originalName);
348
+ const ext2 = getExt(originalName);
185
349
  const mime = contentType.toLowerCase();
186
350
  const allowed = fileType.some(
187
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
351
+ (t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
188
352
  );
189
353
  if (!allowed) {
190
354
  throw new Error(
@@ -200,8 +364,8 @@ var UploadPipeline = class {
200
364
  return saveFileToBucket(originalName, data, this._bucket, contentType);
201
365
  }
202
366
  };
203
- function upload(bucket) {
204
- return new UploadPipeline(bucket);
367
+ function upload(bucket2) {
368
+ return new UploadPipeline(bucket2);
205
369
  }
206
370
 
207
371
  // src/helpers/parseBody.ts
@@ -231,24 +395,12 @@ function isProbablyText(buffer) {
231
395
  }
232
396
  return true;
233
397
  }
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
- };
398
+ var extByMime = {};
399
+ for (const ext2 in mimes_default) extByMime[mimes_default[ext2]] = ext2;
249
400
  function extFromType(type2) {
250
401
  const base = (type2 || "").split(";")[0].trim().toLowerCase();
251
- if (MIME_EXT[base]) return MIME_EXT[base];
402
+ const ext2 = extByMime[base];
403
+ if (ext2) return `.${ext2}`;
252
404
  const sub = base.split("/")[1];
253
405
  return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
254
406
  }
@@ -303,6 +455,7 @@ function startPart(headerStr, dest) {
303
455
  controller = c;
304
456
  }
305
457
  });
458
+ const file2 = dest.file(id);
306
459
  return {
307
460
  kind: "file",
308
461
  name,
@@ -310,7 +463,8 @@ function startPart(headerStr, dest) {
310
463
  type: type2,
311
464
  id,
312
465
  controller,
313
- write: dest.write(id, readable),
466
+ file: file2,
467
+ write: file2.write(readable, { type: type2 }),
314
468
  size: 0
315
469
  };
316
470
  }
@@ -333,11 +487,11 @@ async function endPart(part, body) {
333
487
  addField(body, part.name, ref);
334
488
  } else if (part.kind === "file") {
335
489
  part.controller.close();
336
- const path2 = await part.write;
490
+ await part.write;
337
491
  addField(body, part.name, {
338
492
  name: part.filename,
339
493
  id: part.id,
340
- path: path2,
494
+ path: part.file.path,
341
495
  type: part.type,
342
496
  size: part.size
343
497
  });
@@ -401,8 +555,9 @@ async function parseMultipart(stream, boundary, dest) {
401
555
  if (part) await endPart(part, body);
402
556
  return body;
403
557
  }
404
- async function streamToBucket(stream, type2, bucket) {
558
+ async function streamToBucket(stream, type2, bucket2) {
405
559
  const id = `${createId()}${extFromType(type2)}`;
560
+ const file2 = bucket2.file(id);
406
561
  let size = 0;
407
562
  let controller;
408
563
  const readable = new ReadableStream({
@@ -410,15 +565,15 @@ async function streamToBucket(stream, type2, bucket) {
410
565
  controller = c;
411
566
  }
412
567
  });
413
- const write = bucket.write(id, readable);
568
+ const write = file2.write(readable, { type: type2 });
414
569
  for await (const chunk of asIterable(stream)) {
415
570
  controller.enqueue(chunk);
416
571
  size += chunk.byteLength;
417
572
  }
418
573
  controller.close();
419
- const path2 = await write;
574
+ await write;
420
575
  if (!size) return void 0;
421
- return { name: id, id, path: path2, type: type2, size };
576
+ return { name: id, id, path: file2.path, type: type2, size };
422
577
  }
423
578
  async function parseBody(input, contentType, dest) {
424
579
  const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
@@ -524,13 +679,13 @@ var Reply = class {
524
679
  }
525
680
  type(type2) {
526
681
  if (!type2) return this;
527
- type2 = types_default[type2.replace(/^\./, "")] || type2;
682
+ type2 = mimes_default[type2.replace(/^\./, "")] || type2;
528
683
  this.res.headers.set("content-type", type2);
529
684
  return this;
530
685
  }
531
686
  download(name) {
532
- const ext = name?.split(".").pop();
533
- if (ext && !this.res.headers.get("content-type")) this.type(ext);
687
+ const ext2 = name?.split(".").pop();
688
+ if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
534
689
  const filename = name ? `; filename="${encodeURIComponent(name)}"` : "";
535
690
  return this.headers("content-disposition", `attachment${filename}`);
536
691
  }
@@ -570,9 +725,9 @@ var Reply = class {
570
725
  async file(path2) {
571
726
  try {
572
727
  const fs2 = await import("fs");
573
- const ext = path2.split(".").pop();
728
+ const ext2 = path2.split(".").pop();
574
729
  const stream = fs2.createReadStream(path2);
575
- return this.type(ext).send(stream);
730
+ return this.type(ext2).send(stream);
576
731
  } catch (error) {
577
732
  if (error.code === "ENOENT") {
578
733
  return this.status(404).send();
@@ -582,6 +737,9 @@ var Reply = class {
582
737
  }
583
738
  send(body = "") {
584
739
  const { status: status2 = 200, headers: headers2 } = this.res;
740
+ if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
741
+ return new Response(null, { status: status2, headers: headers2 });
742
+ }
585
743
  if (typeof body === "string") {
586
744
  if (!headers2.get("content-type")) {
587
745
  const isHtml = body.trim().startsWith("<");
@@ -1154,101 +1312,6 @@ function parseAuthOptions(auth2, all) {
1154
1312
  };
1155
1313
  }
1156
1314
 
1157
- // src/helpers/bucket.ts
1158
- import * as fs from "fs";
1159
- import * as fsp from "fs/promises";
1160
- import * as path from "path";
1161
- function thinLocalBucket(root) {
1162
- const absolute = (name) => {
1163
- if (!name) throw new Error("File name is required");
1164
- return path.resolve(path.join(root, name));
1165
- };
1166
- return {
1167
- location: path.resolve(root),
1168
- read: async (name) => {
1169
- const fullPath = absolute(name);
1170
- const stats = await fsp.stat(fullPath).catch(() => null);
1171
- if (!stats?.isFile()) return null;
1172
- const nodeStream = fs.createReadStream(fullPath);
1173
- return new ReadableStream({
1174
- start(controller) {
1175
- nodeStream.on("data", (chunk) => controller.enqueue(chunk));
1176
- nodeStream.on("end", () => controller.close());
1177
- nodeStream.on("error", (err) => controller.error(err));
1178
- },
1179
- cancel() {
1180
- nodeStream.destroy();
1181
- }
1182
- });
1183
- },
1184
- write: async (name, value, type2) => {
1185
- const fullPath = absolute(name);
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;
1198
- }
1199
- await fsp.writeFile(fullPath, value, type2);
1200
- return fullPath;
1201
- },
1202
- delete: async (name) => {
1203
- const fullPath = absolute(name);
1204
- try {
1205
- await fsp.unlink(fullPath);
1206
- return true;
1207
- } catch {
1208
- return false;
1209
- }
1210
- },
1211
- folder: (prefix) => thinLocalBucket(path.join(root, prefix))
1212
- };
1213
- }
1214
- function thinBunBucket(s3, prefix = "") {
1215
- const key = (name) => prefix ? `${prefix}/${name}` : name;
1216
- return {
1217
- read: async (name) => {
1218
- const file2 = s3.file(key(name));
1219
- if (!await file2.exists()) return null;
1220
- return await file2.stream();
1221
- },
1222
- write: async (name, value) => {
1223
- const file2 = s3.file(key(name));
1224
- if (value) {
1225
- await file2.write(value);
1226
- return key(name);
1227
- }
1228
- return s3.presign(key(name), {
1229
- expiresIn: 3600,
1230
- acl: "public-read-write"
1231
- });
1232
- },
1233
- delete: async (name) => {
1234
- const file2 = s3.file(key(name));
1235
- if (!await file2.exists()) return null;
1236
- return await file2.delete();
1237
- },
1238
- folder: (sub) => thinBunBucket(s3, key(sub))
1239
- };
1240
- }
1241
- function bucket_default(root) {
1242
- if (!root) return null;
1243
- if (typeof root === "string") {
1244
- return thinLocalBucket(root);
1245
- }
1246
- if (root.file && root.write) {
1247
- return thinBunBucket(root);
1248
- }
1249
- return root;
1250
- }
1251
-
1252
1315
  // src/helpers/color.ts
1253
1316
  var map = {
1254
1317
  reset: 0,
@@ -1452,8 +1515,8 @@ function config(options = {}) {
1452
1515
  }
1453
1516
  settings.cors = cors2;
1454
1517
  }
1455
- settings.public = options.public ? bucket_default(options.public) : null;
1456
- settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
1518
+ settings.public = options.public ? bucket(options.public) : null;
1519
+ settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket(options.uploads) : null;
1457
1520
  if (options.favicon) settings.favicon = options.favicon;
1458
1521
  settings.store = options.store ?? null;
1459
1522
  settings.cookies = options.cookies ?? null;
@@ -1527,6 +1590,16 @@ function applyCors(res, ctx) {
1527
1590
  }
1528
1591
  }
1529
1592
 
1593
+ // src/helpers/etag.ts
1594
+ function etag(bytes) {
1595
+ let h = 2166136261;
1596
+ for (let i = 0; i < bytes.length; i++) {
1597
+ h ^= bytes[i];
1598
+ h = Math.imul(h, 16777619);
1599
+ }
1600
+ return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
1601
+ }
1602
+
1530
1603
  // src/helpers/createWebsocket.ts
1531
1604
  function createWebsocket(sockets, handlers) {
1532
1605
  const run = (event, socket, body) => {
@@ -1916,89 +1989,6 @@ function toWeb(nodeStream) {
1916
1989
  });
1917
1990
  }
1918
1991
 
1919
- // src/helpers/types.ts
1920
- var types = {
1921
- aac: "audio/aac",
1922
- abw: "application/x-abiword",
1923
- arc: "application/x-freearc",
1924
- avif: "image/avif",
1925
- avi: "video/x-msvideo",
1926
- azw: "application/vnd.amazon.ebook",
1927
- bin: "application/octet-stream",
1928
- bmp: "image/bmp",
1929
- bz: "application/x-bzip",
1930
- bz2: "application/x-bzip2",
1931
- cda: "application/x-cdf",
1932
- csh: "application/x-csh",
1933
- css: "text/css",
1934
- csv: "text/csv",
1935
- doc: "application/msword",
1936
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1937
- eot: "application/vnd.ms-fontobject",
1938
- epub: "application/epub+zip",
1939
- gz: "application/gzip",
1940
- gif: "image/gif",
1941
- htm: "text/html",
1942
- html: "text/html",
1943
- ico: "image/vnd.microsoft.icon",
1944
- ics: "text/calendar",
1945
- jar: "application/java-archive",
1946
- jpeg: "image/jpeg",
1947
- jpg: "image/jpeg",
1948
- js: "text/javascript",
1949
- json: "application/json",
1950
- jsonld: "application/ld+json",
1951
- md: "text/markdown",
1952
- mid: "audio/midi",
1953
- midi: "audio/midi",
1954
- mjs: "text/javascript",
1955
- mp3: "audio/mpeg",
1956
- mp4: "video/mp4",
1957
- mpeg: "video/mpeg",
1958
- mpkg: "application/vnd.apple.installer+xml",
1959
- odp: "application/vnd.oasis.opendocument.presentation",
1960
- ods: "application/vnd.oasis.opendocument.spreadsheet",
1961
- odt: "application/vnd.oasis.opendocument.text",
1962
- oga: "audio/ogg",
1963
- ogv: "video/ogg",
1964
- ogx: "application/ogg",
1965
- opus: "audio/opus",
1966
- otf: "font/otf",
1967
- png: "image/png",
1968
- pdf: "application/pdf",
1969
- php: "application/x-httpd-php",
1970
- ppt: "application/vnd.ms-powerpoint",
1971
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1972
- rar: "application/vnd.rar",
1973
- rtf: "application/rtf",
1974
- sh: "application/x-sh",
1975
- svg: "image/svg+xml",
1976
- tar: "application/x-tar",
1977
- text: "text/plain",
1978
- tif: "image/tiff",
1979
- tiff: "image/tiff",
1980
- ts: "video/mp2t",
1981
- ttf: "font/ttf",
1982
- txt: "text/plain",
1983
- vsd: "application/vnd.visio",
1984
- wav: "audio/wav",
1985
- weba: "audio/webm",
1986
- webm: "video/webm",
1987
- webp: "image/webp",
1988
- woff: "font/woff",
1989
- woff2: "font/woff2",
1990
- xhtml: "application/xhtml+xml",
1991
- xls: "application/vnd.ms-excel",
1992
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1993
- xml: "application/xml",
1994
- xul: "application/vnd.mozilla.xul+xml",
1995
- zip: "application/zip",
1996
- "3gp": "video/3gpp",
1997
- "3g2": "video/3gpp2",
1998
- "7z": "application/x-7z-compressed"
1999
- };
2000
- var types_default = types;
2001
-
2002
1992
  // src/helpers/verify.ts
2003
1993
  import * as crypto3 from "crypto";
2004
1994
  function timingSafeEqual(a, b) {
@@ -2168,28 +2158,38 @@ async function assets(ctx) {
2168
2158
  if (ctx.method !== "get") return;
2169
2159
  if (ctx.url.pathname === "/") return;
2170
2160
  try {
2171
- const asset = await ctx.options.public.read(ctx.url.pathname);
2172
- if (!asset) return;
2173
- return type(ctx.url.pathname.split(".").pop()).send(asset);
2161
+ const asset = ctx.options.public.file(ctx.url.pathname);
2162
+ if (!await asset.exists()) return;
2163
+ return type(ctx.url.pathname.split(".").pop()).send(asset.stream());
2174
2164
  } catch {
2175
2165
  }
2176
2166
  }
2177
2167
 
2178
2168
  // src/middle/favicon.ts
2169
+ var CACHE_CONTROL = "public, max-age=86400";
2170
+ var ext = (name) => name.split(".").pop() || "ico";
2171
+ async function loadFavicon(fav) {
2172
+ try {
2173
+ const type2 = ext(typeof fav === "string" ? fav : fav?.name);
2174
+ const bytes = typeof fav === "string" ? await (await import("fs/promises")).readFile(fav) : Buffer.from(await fav.bytes());
2175
+ return { bytes, type: type2, etag: etag(bytes) };
2176
+ } catch {
2177
+ return null;
2178
+ }
2179
+ }
2179
2180
  async function favicon(ctx) {
2180
- if (ctx.method !== "get") return;
2181
- if (ctx.url.pathname !== "/favicon.ico") return;
2182
2181
  const fav = ctx.options.favicon;
2183
- if (fav) {
2184
- if (typeof fav === "string") return file(fav);
2185
- const icon = await fav.read("favicon.ico");
2186
- return icon ? type("ico").send(icon) : 204;
2182
+ if (!fav) return;
2183
+ if (ctx.app.faviconCache === void 0) {
2184
+ ctx.app.faviconCache = await loadFavicon(fav);
2187
2185
  }
2188
- const handled = ctx.app.handlers.get.some(
2189
- (route) => pathPattern(route.path, "/favicon.ico")
2190
- );
2191
- if (handled) return;
2192
- return 204;
2186
+ const entry = ctx.app.faviconCache;
2187
+ if (!entry) return 204;
2188
+ const headers2 = { "cache-control": CACHE_CONTROL, etag: entry.etag };
2189
+ if (ctx.headers["if-none-match"] === entry.etag) {
2190
+ return status(304).headers(headers2).send();
2191
+ }
2192
+ return type(entry.type).headers(headers2).send(entry.bytes);
2193
2193
  }
2194
2194
 
2195
2195
  // src/middle/openapi.ts
@@ -2409,6 +2409,168 @@ function timer(ctx) {
2409
2409
  ctx.time = createTime();
2410
2410
  }
2411
2411
 
2412
+ // src/helpers/wsNode.ts
2413
+ var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2414
+ var CONTINUATION = 0;
2415
+ var TEXT = 1;
2416
+ var BINARY = 2;
2417
+ var CLOSE = 8;
2418
+ var PING = 9;
2419
+ var PONG = 10;
2420
+ function encodeFrame(payload, opcode) {
2421
+ const len = payload.length;
2422
+ let header;
2423
+ if (len < 126) {
2424
+ header = Buffer.from([128 | opcode, len]);
2425
+ } else if (len < 65536) {
2426
+ header = Buffer.allocUnsafe(4);
2427
+ header[0] = 128 | opcode;
2428
+ header[1] = 126;
2429
+ header.writeUInt16BE(len, 2);
2430
+ } else {
2431
+ header = Buffer.allocUnsafe(10);
2432
+ header[0] = 128 | opcode;
2433
+ header[1] = 127;
2434
+ header.writeBigUInt64BE(BigInt(len), 2);
2435
+ }
2436
+ return Buffer.concat([header, payload]);
2437
+ }
2438
+ var NodeWebSocket = class {
2439
+ socket;
2440
+ handlers;
2441
+ buffer;
2442
+ fragments;
2443
+ fragmentOpcode;
2444
+ closed;
2445
+ readyState;
2446
+ constructor(socket, handlers) {
2447
+ this.socket = socket;
2448
+ this.handlers = handlers;
2449
+ this.buffer = Buffer.alloc(0);
2450
+ this.fragments = [];
2451
+ this.fragmentOpcode = TEXT;
2452
+ this.closed = false;
2453
+ this.readyState = 1;
2454
+ }
2455
+ send(data) {
2456
+ if (this.closed) return;
2457
+ const isString = typeof data === "string";
2458
+ const payload = isString ? Buffer.from(data) : Buffer.from(data);
2459
+ this.socket.write(encodeFrame(payload, isString ? TEXT : BINARY));
2460
+ }
2461
+ close(code = 1e3, reason = "") {
2462
+ if (this.closed) return;
2463
+ const payload = Buffer.alloc(2 + Buffer.byteLength(reason));
2464
+ payload.writeUInt16BE(code, 0);
2465
+ payload.write(reason, 2);
2466
+ try {
2467
+ this.socket.write(encodeFrame(payload, CLOSE));
2468
+ } catch {
2469
+ }
2470
+ this.shutdown();
2471
+ }
2472
+ // Called once, whether the peer closed, the socket died, or we closed.
2473
+ shutdown() {
2474
+ if (this.closed) return;
2475
+ this.closed = true;
2476
+ this.readyState = 3;
2477
+ try {
2478
+ this.socket.end();
2479
+ } catch {
2480
+ }
2481
+ this.handlers.onClose();
2482
+ }
2483
+ // Feed raw bytes from the TCP socket; parses as many complete frames as it can
2484
+ // and buffers the remainder for the next chunk.
2485
+ receive(chunk) {
2486
+ this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
2487
+ while (true) {
2488
+ const buf = this.buffer;
2489
+ if (buf.length < 2) return;
2490
+ const fin = (buf[0] & 128) !== 0;
2491
+ const opcode = buf[0] & 15;
2492
+ const masked = (buf[1] & 128) !== 0;
2493
+ let len = buf[1] & 127;
2494
+ let offset = 2;
2495
+ if (len === 126) {
2496
+ if (buf.length < 4) return;
2497
+ len = buf.readUInt16BE(2);
2498
+ offset = 4;
2499
+ } else if (len === 127) {
2500
+ if (buf.length < 10) return;
2501
+ len = Number(buf.readBigUInt64BE(2));
2502
+ offset = 10;
2503
+ }
2504
+ let mask = null;
2505
+ if (masked) {
2506
+ if (buf.length < offset + 4) return;
2507
+ mask = buf.subarray(offset, offset + 4);
2508
+ offset += 4;
2509
+ }
2510
+ if (buf.length < offset + len) return;
2511
+ const payload = Buffer.from(buf.subarray(offset, offset + len));
2512
+ if (mask) {
2513
+ for (let i = 0; i < len; i++) payload[i] ^= mask[i & 3];
2514
+ }
2515
+ this.buffer = buf.subarray(offset + len);
2516
+ this.frame(fin, opcode, payload);
2517
+ }
2518
+ }
2519
+ frame(fin, opcode, payload) {
2520
+ if (opcode === CLOSE) {
2521
+ this.shutdown();
2522
+ return;
2523
+ }
2524
+ if (opcode === PING) {
2525
+ if (!this.closed) this.socket.write(encodeFrame(payload, PONG));
2526
+ return;
2527
+ }
2528
+ if (opcode === PONG) return;
2529
+ if (opcode === CONTINUATION) {
2530
+ this.fragments.push(payload);
2531
+ } else {
2532
+ this.fragments = [payload];
2533
+ this.fragmentOpcode = opcode;
2534
+ }
2535
+ if (!fin) return;
2536
+ const full = this.fragments.length === 1 ? this.fragments[0] : Buffer.concat(this.fragments);
2537
+ this.fragments = [];
2538
+ const body = this.fragmentOpcode === TEXT ? full.toString("utf8") : full;
2539
+ this.handlers.onMessage(body);
2540
+ }
2541
+ };
2542
+ async function attachWebsocket(server2, app) {
2543
+ const { createHash } = await import("crypto");
2544
+ server2.on("upgrade", (req, socket, head) => {
2545
+ const key = req.headers["sec-websocket-key"];
2546
+ const upgrade = String(req.headers.upgrade || "").toLowerCase();
2547
+ if (upgrade !== "websocket" || !key || !app.handlers.socket.length) {
2548
+ socket.destroy();
2549
+ return;
2550
+ }
2551
+ const accept = createHash("sha1").update(key + GUID).digest("base64");
2552
+ socket.write(
2553
+ `HTTP/1.1 101 Switching Protocols\r
2554
+ Upgrade: websocket\r
2555
+ Connection: Upgrade\r
2556
+ Sec-WebSocket-Accept: ${accept}\r
2557
+ \r
2558
+ `
2559
+ );
2560
+ socket.setTimeout(0);
2561
+ socket.setNoDelay(true);
2562
+ const ws = new NodeWebSocket(socket, {
2563
+ onMessage: (body) => app.websocket.message(ws, body),
2564
+ onClose: () => app.websocket.close(ws)
2565
+ });
2566
+ app.websocket.open(ws);
2567
+ if (head?.length) ws.receive(head);
2568
+ socket.on("data", (chunk) => ws.receive(chunk));
2569
+ socket.on("close", () => ws.shutdown());
2570
+ socket.on("error", () => ws.shutdown());
2571
+ });
2572
+ }
2573
+
2412
2574
  // src/context/node.ts
2413
2575
  import { TLSSocket } from "tls";
2414
2576
 
@@ -2525,25 +2687,25 @@ var Winter = async (app, request, env2) => {
2525
2687
  };
2526
2688
  var Node = async (app) => {
2527
2689
  const http = await import("http");
2528
- http.createServer(async (request, response) => {
2529
- const ctx = await createNode(request, app);
2530
- if ("error" in ctx) throw ctx.error;
2531
- const out = await handleRequest(app, ctx);
2532
- response.writeHead(out.status || 200, parseHeaders_default(out.headers));
2533
- if (out.body instanceof ReadableStream) {
2534
- await iterate(out.body, (chunk) => response.write(chunk));
2535
- } else {
2536
- response.write(out.body || "");
2690
+ const server2 = http.createServer(
2691
+ async (request, response) => {
2692
+ const ctx = await createNode(request, app);
2693
+ if ("error" in ctx) throw ctx.error;
2694
+ const out = await handleRequest(app, ctx);
2695
+ response.writeHead(out.status || 200, parseHeaders_default(out.headers));
2696
+ if (out.body instanceof ReadableStream) {
2697
+ await iterate(out.body, (chunk) => response.write(chunk));
2698
+ } else {
2699
+ response.write(out.body || "");
2700
+ }
2701
+ response.end();
2537
2702
  }
2538
- response.end();
2539
- }).listen(app.settings.port, () => {
2703
+ );
2704
+ await attachWebsocket(server2, app);
2705
+ server2.listen(app.settings.port, () => {
2540
2706
  app.settings.log.start(`http://localhost:${app.settings.port}/`);
2541
- if (app.handlers.socket.length) {
2542
- console.warn(
2543
- "[server] WebSockets (.socket()) are only supported on Bun, not Node"
2544
- );
2545
- }
2546
2707
  });
2708
+ return server2;
2547
2709
  };
2548
2710
  var Netlify = async (app, request, context) => {
2549
2711
  request.context = context;
@@ -2686,6 +2848,9 @@ var Server = class extends Router {
2686
2848
  platform;
2687
2849
  sockets;
2688
2850
  websocket;
2851
+ // Lazily-loaded favicon bytes, cached per server until restart (see favicon
2852
+ // middleware). `undefined` = not loaded yet; `null` = configured but missing.
2853
+ faviconCache;
2689
2854
  port;
2690
2855
  constructor(options = {}) {
2691
2856
  super();
@@ -2704,7 +2869,7 @@ var Server = class extends Router {
2704
2869
  this.use(timer);
2705
2870
  if (this.settings.cors) this.use(preflight);
2706
2871
  this.use(assets);
2707
- this.use(favicon);
2872
+ if (this.settings.favicon) this.get("/favicon.ico", favicon);
2708
2873
  this.use(session);
2709
2874
  if (this.settings.auth) {
2710
2875
  auth(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.34.0",
3
+ "version": "0.34.2",
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",
@@ -51,6 +51,7 @@
51
51
  "@types/bun": "^1.3.0",
52
52
  "@types/jest": "^30.0.0",
53
53
  "@types/node": "^24.10.0",
54
+ "bucket": "^0.2.0",
54
55
  "bun": "^1.3.13",
55
56
  "check-dts": "^0.8.2",
56
57
  "jest": "^29.7.0",