@server/next 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.d.ts +18 -23
  2. package/index.js +304 -322
  3. package/package.json +23 -4
  4. package/readme.md +28 -0
package/index.d.ts CHANGED
@@ -1,17 +1,12 @@
1
1
  import * as http from 'http';
2
+ export { default as kv } from 'polystore';
3
+ export { default as bucket } from 'bucket';
2
4
 
3
5
  type LimitOptions = {
4
6
  maxSize?: number | string;
5
7
  minSize?: number | string;
6
8
  fileType?: string[];
7
9
  };
8
- declare class UploadPipeline {
9
- private _bucket;
10
- private _limits;
11
- constructor(bucket?: Bucket | string | null);
12
- limit(options: LimitOptions): this;
13
- processFile(originalName: string, data: Buffer, contentType: string): Promise<UploadedFile>;
14
- }
15
10
 
16
11
  type CookieOptions = string | string[] | Cookie | Cookie[] | null;
17
12
  interface ResponseData {
@@ -24,7 +19,7 @@ declare class Reply$1 {
24
19
  status(status: number): this;
25
20
  type(type?: string): this;
26
21
  download(name?: string): this;
27
- headers(key: string | Record<string, string>, value?: string): this;
22
+ headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
28
23
  cache(value: CacheOption): this;
29
24
  cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
30
25
  json(body: unknown): Response;
@@ -88,18 +83,16 @@ type Cookie = {
88
83
  };
89
84
  type RouterMethod = "*" | Method;
90
85
  type FileInfo = {
91
- exists: boolean;
92
86
  size: number;
93
- date: Date | null;
94
- type?: string | null;
87
+ type: string | null;
88
+ modified: Date;
95
89
  };
96
90
  type BucketFile = {
97
91
  readonly path: string;
98
- readonly id: string;
99
92
  readonly name: string;
100
93
  readonly type?: string;
101
94
  exists(): Promise<boolean>;
102
- info?(): Promise<FileInfo>;
95
+ info?(): Promise<FileInfo | null>;
103
96
  write(content: string | Buffer | ReadableStream, options?: {
104
97
  type?: string;
105
98
  }): Promise<void>;
@@ -114,7 +107,6 @@ type Bucket = {
114
107
  };
115
108
  type UploadedFile = {
116
109
  name: string;
117
- id: string;
118
110
  path: string;
119
111
  type: string;
120
112
  size: number;
@@ -138,6 +130,7 @@ type BasicValue = string | number | boolean | null;
138
130
  type SerializableValue = BasicValue | {
139
131
  [key: string]: SerializableValue;
140
132
  } | Array<SerializableValue>;
133
+ type StoreSource = KVStore | Map<string, any> | string | Record<string, any>;
141
134
  type KVStore = {
142
135
  name?: string;
143
136
  prefix: (prefix?: string) => KVStore;
@@ -167,8 +160,8 @@ type AuthOption = `${Strategy}:${Provider}` | "key" | {
167
160
  strategy: Strategy;
168
161
  providers?: Provider | Provider[];
169
162
  key?: string;
170
- session?: KVStore;
171
- store?: KVStore;
163
+ session?: StoreSource;
164
+ store?: StoreSource;
172
165
  redirect?: string;
173
166
  cleanUser?: <T = AuthUser>(user: T) => T | Promise<T>;
174
167
  };
@@ -195,6 +188,7 @@ type SecurityOptions = {
195
188
  referrerPolicy?: boolean | string;
196
189
  hsts?: boolean | string;
197
190
  xssProtection?: boolean;
191
+ traversalProtection?: boolean;
198
192
  csp?: boolean | string;
199
193
  coop?: boolean | string;
200
194
  corp?: boolean | string;
@@ -202,6 +196,7 @@ type SecurityOptions = {
202
196
  };
203
197
  type SecuritySettings = {
204
198
  trustProxy: boolean;
199
+ traversalProtection: boolean;
205
200
  headers: Record<string, string>;
206
201
  hsts: string | null;
207
202
  };
@@ -212,10 +207,9 @@ type Options = {
212
207
  secret?: string;
213
208
  public?: string | Bucket;
214
209
  uploads?: string | Bucket | UploadOptions;
215
- store?: KVStore;
216
- cookies?: KVStore;
217
- session?: KVStore | {
218
- store: KVStore;
210
+ store?: StoreSource;
211
+ session?: StoreSource | {
212
+ store: StoreSource;
219
213
  };
220
214
  cors?: CorsOptions;
221
215
  auth?: AuthOption;
@@ -232,9 +226,10 @@ type Settings = {
232
226
  port: number;
233
227
  secret: string;
234
228
  public?: Bucket;
235
- uploads?: Bucket | UploadPipeline;
229
+ uploads?: ({
230
+ bucket: Bucket;
231
+ } & LimitOptions) | null;
236
232
  store?: KVStore;
237
- cookies?: KVStore;
238
233
  session?: {
239
234
  store: KVStore;
240
235
  };
@@ -506,4 +501,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
506
501
  }
507
502
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
508
503
 
509
- export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
504
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type StoreSource, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
package/index.js CHANGED
@@ -39,6 +39,10 @@ var ServerError_default = TypedServerError;
39
39
 
40
40
  // src/errors/index.ts
41
41
  ServerError_default.extend({
42
+ PATH_TRAVERSAL: {
43
+ status: 400,
44
+ message: "The route param '{param}' tries to climb the path ('{value}'). If this route legitimately receives paths, set security: { traversalProtection: false }"
45
+ },
42
46
  NO_STORE: "You need a 'store' to write 'ctx.session'",
43
47
  NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
44
48
  NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
@@ -76,6 +80,7 @@ ServerError_default.extend({
76
80
  REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
77
81
  REGISTER_EMAIL_EXISTS: "Email is already registered"
78
82
  });
83
+ var errors_default = ServerError_default;
79
84
 
80
85
  // src/polyfill.ts
81
86
  globalThis.env = {};
@@ -98,191 +103,6 @@ var StatusError = class extends Error {
98
103
  }
99
104
  };
100
105
 
101
- // src/helpers/bucket.ts
102
- import * as fs from "fs";
103
- import * as fsp from "fs/promises";
104
- import * as path from "path";
105
-
106
- // src/helpers/mimes.ts
107
- var mimes_default = {
108
- aac: "audio/aac",
109
- abw: "application/x-abiword",
110
- arc: "application/x-freearc",
111
- avif: "image/avif",
112
- avi: "video/x-msvideo",
113
- azw: "application/vnd.amazon.ebook",
114
- bin: "application/octet-stream",
115
- bmp: "image/bmp",
116
- bz: "application/x-bzip",
117
- bz2: "application/x-bzip2",
118
- cda: "application/x-cdf",
119
- csh: "application/x-csh",
120
- css: "text/css",
121
- csv: "text/csv",
122
- doc: "application/msword",
123
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
124
- eot: "application/vnd.ms-fontobject",
125
- epub: "application/epub+zip",
126
- gz: "application/gzip",
127
- gif: "image/gif",
128
- htm: "text/html",
129
- html: "text/html",
130
- ico: "image/vnd.microsoft.icon",
131
- ics: "text/calendar",
132
- jar: "application/java-archive",
133
- jpeg: "image/jpeg",
134
- jpg: "image/jpeg",
135
- js: "text/javascript",
136
- json: "application/json",
137
- jsonld: "application/ld+json",
138
- md: "text/markdown",
139
- mid: "audio/midi",
140
- midi: "audio/midi",
141
- mjs: "text/javascript",
142
- mp3: "audio/mpeg",
143
- mp4: "video/mp4",
144
- mpeg: "video/mpeg",
145
- mpkg: "application/vnd.apple.installer+xml",
146
- odp: "application/vnd.oasis.opendocument.presentation",
147
- ods: "application/vnd.oasis.opendocument.spreadsheet",
148
- odt: "application/vnd.oasis.opendocument.text",
149
- oga: "audio/ogg",
150
- ogv: "video/ogg",
151
- ogx: "application/ogg",
152
- opus: "audio/opus",
153
- otf: "font/otf",
154
- png: "image/png",
155
- pdf: "application/pdf",
156
- php: "application/x-httpd-php",
157
- ppt: "application/vnd.ms-powerpoint",
158
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
159
- rar: "application/vnd.rar",
160
- rtf: "application/rtf",
161
- sh: "application/x-sh",
162
- svg: "image/svg+xml",
163
- tar: "application/x-tar",
164
- text: "text/plain",
165
- tif: "image/tiff",
166
- tiff: "image/tiff",
167
- ts: "video/mp2t",
168
- ttf: "font/ttf",
169
- txt: "text/plain",
170
- vsd: "application/vnd.visio",
171
- wav: "audio/wav",
172
- weba: "audio/webm",
173
- webm: "video/webm",
174
- webp: "image/webp",
175
- woff: "font/woff",
176
- woff2: "font/woff2",
177
- xhtml: "application/xhtml+xml",
178
- xls: "application/vnd.ms-excel",
179
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
180
- xml: "application/xml",
181
- xul: "application/vnd.mozilla.xul+xml",
182
- zip: "application/zip",
183
- "3gp": "video/3gpp",
184
- "3g2": "video/3gpp2",
185
- "7z": "application/x-7z-compressed"
186
- };
187
-
188
- // src/helpers/bucket.ts
189
- function localBucket(root) {
190
- const base = path.resolve(root);
191
- const resolveKey = (name) => {
192
- if (!name) throw new Error("File name is required");
193
- const full = path.resolve(base, name.replace(/^\/+/, ""));
194
- if (full !== base && !full.startsWith(base + path.sep)) {
195
- throw new Error(`Path "${name}" escapes the bucket root`);
196
- }
197
- return full;
198
- };
199
- const file2 = (name, win) => {
200
- const full = resolveKey(name);
201
- const type2 = mimes_default[path.extname(name).slice(1).toLowerCase()];
202
- const read = () => {
203
- let opts;
204
- if (win) {
205
- opts = { start: win.start };
206
- if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
207
- }
208
- const nodeStream = fs.createReadStream(full, opts);
209
- return new ReadableStream({
210
- start(controller) {
211
- nodeStream.on("data", (chunk) => controller.enqueue(chunk));
212
- nodeStream.on("end", () => controller.close());
213
- nodeStream.on("error", (err) => controller.error(err));
214
- },
215
- cancel() {
216
- nodeStream.destroy();
217
- }
218
- });
219
- };
220
- return {
221
- path: full,
222
- id: name.replace(/^\/+/, ""),
223
- name: path.basename(name),
224
- type: type2,
225
- async exists() {
226
- const stats = await fsp.stat(full).catch(() => null);
227
- return !!stats?.isFile();
228
- },
229
- async info() {
230
- const stats = await fsp.stat(full).catch(() => null);
231
- const exists = !!stats?.isFile();
232
- const total = stats?.size ?? 0;
233
- const size = win ? Math.max(0, Math.min(win.end, total) - win.start) : total;
234
- return { exists, size, date: stats?.mtime ?? null, type: type2 };
235
- },
236
- // Read-only view of [start, end), composed relative to the current window.
237
- slice(start, end) {
238
- const base2 = win?.start ?? 0;
239
- const cap = win?.end ?? Number.POSITIVE_INFINITY;
240
- const s = Math.min(cap, base2 + Math.max(0, start));
241
- const e = end === void 0 ? cap : Math.min(cap, base2 + end);
242
- return file2(name, { start: s, end: e });
243
- },
244
- async write(content) {
245
- await fsp.mkdir(path.dirname(full), { recursive: true });
246
- if (content instanceof ReadableStream) {
247
- const writable = fs.createWriteStream(full);
248
- for await (const chunk of content) {
249
- writable.write(chunk);
250
- }
251
- await new Promise((resolve2, reject) => {
252
- writable.on("error", reject);
253
- writable.end(() => resolve2());
254
- });
255
- return;
256
- }
257
- await fsp.writeFile(full, content);
258
- },
259
- stream() {
260
- return read();
261
- },
262
- async bytes() {
263
- if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
264
- return new Uint8Array(await fsp.readFile(full));
265
- },
266
- async remove() {
267
- await fsp.unlink(full).catch(() => {
268
- });
269
- }
270
- };
271
- };
272
- return {
273
- file: file2,
274
- folder: (prefix) => localBucket(path.join(base, prefix))
275
- };
276
- }
277
- function bucket(root) {
278
- if (!root) return null;
279
- if (typeof root === "string") return localBucket(root);
280
- if (typeof root.file === "function") return root;
281
- throw new Error(
282
- "Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
283
- );
284
- }
285
-
286
106
  // src/helpers/createId.ts
287
107
  var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
288
108
  var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
@@ -349,52 +169,36 @@ async function saveFileToBucket(originalName, data, bucket2, contentType) {
349
169
  await file2.write(data, { type: contentType });
350
170
  return {
351
171
  name: originalName,
352
- id,
353
172
  path: file2.path,
354
173
  type: contentType,
355
174
  size: data.length
356
175
  };
357
176
  }
358
- var UploadPipeline = class {
359
- _bucket;
360
- _limits = {};
361
- constructor(bucket2) {
362
- this._bucket = bucket(bucket2 ?? void 0);
177
+ function validateFile(originalName, data, contentType, limits) {
178
+ const { maxSize, minSize, fileType: fileType2 } = limits;
179
+ if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
180
+ throw new Error(
181
+ `File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
182
+ );
363
183
  }
364
- limit(options) {
365
- this._limits = { ...this._limits, ...options };
366
- return this;
184
+ if (minSize !== void 0 && data.length < parseBytes(minSize)) {
185
+ throw new Error(
186
+ `File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
187
+ );
367
188
  }
368
- async processFile(originalName, data, contentType) {
369
- const { maxSize, minSize, fileType } = this._limits;
370
- if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
371
- throw new Error(
372
- `File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
373
- );
374
- }
375
- if (minSize !== void 0 && data.length < parseBytes(minSize)) {
189
+ if (fileType2 && fileType2.length > 0) {
190
+ const ext2 = getExt(originalName);
191
+ const mime = contentType.toLowerCase();
192
+ const allowed = fileType2.some(
193
+ (t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
194
+ );
195
+ if (!allowed) {
376
196
  throw new Error(
377
- `File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
378
- );
379
- }
380
- if (fileType && fileType.length > 0) {
381
- const ext2 = getExt(originalName);
382
- const mime = contentType.toLowerCase();
383
- const allowed = fileType.some(
384
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
197
+ `File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType2.join(", ")})`
385
198
  );
386
- if (!allowed) {
387
- throw new Error(
388
- `File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType.join(", ")})`
389
- );
390
- }
391
199
  }
392
- if (!this._bucket) {
393
- throw new Error(`No upload destination configured (missing bucket)`);
394
- }
395
- return saveFileToBucket(originalName, data, this._bucket, contentType);
396
200
  }
397
- };
201
+ }
398
202
 
399
203
  // src/helpers/bodyLimit.ts
400
204
  var INF = Number.POSITIVE_INFINITY;
@@ -416,6 +220,88 @@ var tooLarge = (max) => new StatusError(
416
220
  413
417
221
  );
418
222
 
223
+ // src/helpers/mimes.ts
224
+ var mimes_default = {
225
+ aac: "audio/aac",
226
+ abw: "application/x-abiword",
227
+ arc: "application/x-freearc",
228
+ avif: "image/avif",
229
+ avi: "video/x-msvideo",
230
+ azw: "application/vnd.amazon.ebook",
231
+ bin: "application/octet-stream",
232
+ bmp: "image/bmp",
233
+ bz: "application/x-bzip",
234
+ bz2: "application/x-bzip2",
235
+ cda: "application/x-cdf",
236
+ csh: "application/x-csh",
237
+ css: "text/css; charset=utf-8",
238
+ csv: "text/csv; charset=utf-8",
239
+ doc: "application/msword",
240
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
241
+ eot: "application/vnd.ms-fontobject",
242
+ epub: "application/epub+zip",
243
+ gz: "application/gzip",
244
+ gif: "image/gif",
245
+ htm: "text/html; charset=utf-8",
246
+ html: "text/html; charset=utf-8",
247
+ ico: "image/vnd.microsoft.icon",
248
+ ics: "text/calendar; charset=utf-8",
249
+ jar: "application/java-archive",
250
+ jpeg: "image/jpeg",
251
+ jpg: "image/jpeg",
252
+ js: "text/javascript; charset=utf-8",
253
+ json: "application/json",
254
+ jsonld: "application/ld+json",
255
+ md: "text/markdown; charset=utf-8",
256
+ mid: "audio/midi",
257
+ midi: "audio/midi",
258
+ mjs: "text/javascript; charset=utf-8",
259
+ mp3: "audio/mpeg",
260
+ mp4: "video/mp4",
261
+ mpeg: "video/mpeg",
262
+ mpkg: "application/vnd.apple.installer+xml",
263
+ odp: "application/vnd.oasis.opendocument.presentation",
264
+ ods: "application/vnd.oasis.opendocument.spreadsheet",
265
+ odt: "application/vnd.oasis.opendocument.text",
266
+ oga: "audio/ogg",
267
+ ogv: "video/ogg",
268
+ ogx: "application/ogg",
269
+ opus: "audio/opus",
270
+ otf: "font/otf",
271
+ png: "image/png",
272
+ pdf: "application/pdf",
273
+ php: "application/x-httpd-php",
274
+ ppt: "application/vnd.ms-powerpoint",
275
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
276
+ rar: "application/vnd.rar",
277
+ rtf: "application/rtf",
278
+ sh: "application/x-sh",
279
+ svg: "image/svg+xml",
280
+ tar: "application/x-tar",
281
+ text: "text/plain; charset=utf-8",
282
+ tif: "image/tiff",
283
+ tiff: "image/tiff",
284
+ ts: "video/mp2t",
285
+ ttf: "font/ttf",
286
+ txt: "text/plain; charset=utf-8",
287
+ vsd: "application/vnd.visio",
288
+ wav: "audio/wav",
289
+ weba: "audio/webm",
290
+ webm: "video/webm",
291
+ webp: "image/webp",
292
+ woff: "font/woff",
293
+ woff2: "font/woff2",
294
+ xhtml: "application/xhtml+xml",
295
+ xls: "application/vnd.ms-excel",
296
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
297
+ xml: "application/xml",
298
+ xul: "application/vnd.mozilla.xul+xml",
299
+ zip: "application/zip",
300
+ "3gp": "video/3gpp",
301
+ "3g2": "video/3gpp2",
302
+ "7z": "application/x-7z-compressed"
303
+ };
304
+
419
305
  // src/helpers/parseBody.ts
420
306
  function getBoundary(header) {
421
307
  if (!header) return null;
@@ -494,15 +380,15 @@ function addField(body, name, value) {
494
380
  if (!Array.isArray(body[name])) body[name] = [body[name]];
495
381
  body[name].push(value);
496
382
  }
497
- function startPart(headerStr, dest) {
383
+ function startPart(headerStr, bucket2, limits) {
498
384
  const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
499
385
  if (!name) return { kind: "skip" };
500
386
  const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
501
387
  if (!filename) return { kind: "text", name, chunks: [] };
502
388
  const type2 = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
503
- if (!dest) return { kind: "drop" };
504
- if (dest instanceof UploadPipeline) {
505
- return { kind: "pipefile", name, filename, type: type2, pipeline: dest, chunks: [] };
389
+ if (!bucket2) return { kind: "drop" };
390
+ if (limits) {
391
+ return { kind: "validated", name, filename, type: type2, bucket: bucket2, limits, chunks: [] };
506
392
  }
507
393
  const id = `${createId()}${getExt(filename)}`;
508
394
  let controller;
@@ -511,7 +397,7 @@ function startPart(headerStr, dest) {
511
397
  controller = c;
512
398
  }
513
399
  });
514
- const file2 = dest.file(id);
400
+ const file2 = bucket2.file(id);
515
401
  return {
516
402
  kind: "file",
517
403
  name,
@@ -526,7 +412,7 @@ function startPart(headerStr, dest) {
526
412
  }
527
413
  function feedPart(part, data) {
528
414
  if (data.length === 0) return;
529
- if (part.kind === "text" || part.kind === "pipefile") part.chunks.push(data);
415
+ if (part.kind === "text" || part.kind === "validated") part.chunks.push(data);
530
416
  else if (part.kind === "file") {
531
417
  part.controller.enqueue(data);
532
418
  part.size += data.length;
@@ -537,16 +423,16 @@ async function endPart(part, body) {
537
423
  const buf = Buffer.concat(part.chunks);
538
424
  const value = isProbablyText(buf) ? buf.toString("utf-8").trim() : buf;
539
425
  addField(body, part.name, value);
540
- } else if (part.kind === "pipefile") {
426
+ } else if (part.kind === "validated") {
541
427
  const buf = Buffer.concat(part.chunks);
542
- const ref = await part.pipeline.processFile(part.filename, buf, part.type);
428
+ validateFile(part.filename, buf, part.type, part.limits);
429
+ const ref = await saveFileToBucket(part.filename, buf, part.bucket, part.type);
543
430
  addField(body, part.name, ref);
544
431
  } else if (part.kind === "file") {
545
432
  part.controller.close();
546
433
  await part.write;
547
434
  addField(body, part.name, {
548
435
  name: part.filename,
549
- id: part.id,
550
436
  path: part.file.path,
551
437
  type: part.type,
552
438
  size: part.size
@@ -554,7 +440,7 @@ async function endPart(part, body) {
554
440
  }
555
441
  }
556
442
  var BREAK = Buffer.from("\r\n\r\n");
557
- async function parseMultipart(stream, boundary, dest, max = INF) {
443
+ async function parseMultipart(stream, boundary, bucket2, limits, max = INF) {
558
444
  const delim = Buffer.from(`\r
559
445
  --${boundary}`);
560
446
  const body = {};
@@ -591,7 +477,7 @@ async function parseMultipart(stream, boundary, dest, max = INF) {
591
477
  } else if (state === "headers") {
592
478
  const i = buf.indexOf(BREAK);
593
479
  if (i === -1) break;
594
- part = startPart(buf.subarray(0, i).toString("utf-8"), dest);
480
+ part = startPart(buf.subarray(0, i).toString("utf-8"), bucket2, limits);
595
481
  buf = buf.subarray(i + BREAK.length);
596
482
  state = "body";
597
483
  advanced = true;
@@ -637,12 +523,25 @@ async function streamToBucket(stream, type2, bucket2) {
637
523
  controller.close();
638
524
  await write;
639
525
  if (!size) return void 0;
640
- return { name: id, id, path: file2.path, type: type2, size };
526
+ return { name: id, path: file2.path, type: type2, size };
641
527
  }
642
528
  async function parseBody(input, contentType, dest, max = INF) {
643
529
  const type2 = Array.isArray(contentType) ? contentType[0] : contentType;
530
+ let bucket2;
531
+ let limits;
532
+ if (dest && "bucket" in dest) {
533
+ bucket2 = dest.bucket;
534
+ const { maxSize, minSize, fileType: fileType2 } = dest;
535
+ if (maxSize != null || minSize != null || fileType2 != null) {
536
+ limits = { maxSize, minSize, fileType: fileType2 };
537
+ }
538
+ } else {
539
+ bucket2 = dest;
540
+ }
644
541
  const boundary = type2 && /multipart\/form-data/i.test(type2) ? getBoundary(type2) : null;
645
- if (boundary) return parseMultipart(toStream(input), boundary, dest, max);
542
+ if (boundary) {
543
+ return parseMultipart(toStream(input), boundary, bucket2, limits, max);
544
+ }
646
545
  if (!type2 || /^text\//i.test(type2)) {
647
546
  const buf = await toBuffer(input, max);
648
547
  return buf.length ? buf.toString("utf-8") : void 0;
@@ -655,15 +554,18 @@ async function parseBody(input, contentType, dest, max = INF) {
655
554
  const buf = await toBuffer(input, max);
656
555
  return buf.length ? parseUrlEncoded(buf.toString("utf-8")) : void 0;
657
556
  }
658
- if (!dest) {
557
+ if (!bucket2) {
659
558
  const buf = await toBuffer(input, max);
660
559
  return buf.length ? buf : void 0;
661
560
  }
662
- if (dest instanceof UploadPipeline) {
561
+ if (limits) {
663
562
  const buf = await toBuffer(input);
664
- return buf.length ? dest.processFile(`upload${extFromType(type2)}`, buf, type2) : void 0;
563
+ if (!buf.length) return void 0;
564
+ const name = `upload${extFromType(type2)}`;
565
+ validateFile(name, buf, type2, limits);
566
+ return saveFileToBucket(name, buf, bucket2, type2);
665
567
  }
666
- return streamToBucket(toStream(input), type2, dest);
568
+ return streamToBucket(toStream(input), type2, bucket2);
667
569
  }
668
570
 
669
571
  // src/helpers/body.ts
@@ -759,8 +661,8 @@ function normalizeExpires(expires) {
759
661
  }
760
662
  function createCookies(key, val) {
761
663
  if (val.value === null) val.expires = EXPIRED;
762
- const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
763
- let str = `${key}=${value || ""};Path=${path2 || "/"}`;
664
+ const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
665
+ let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
764
666
  if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
765
667
  if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
766
668
  if (httpOnly) str += ";HttpOnly";
@@ -823,6 +725,45 @@ function clientIp(headers2, opts = {}) {
823
725
  return normalize(remoteAddress);
824
726
  }
825
727
 
728
+ // src/helpers/store.ts
729
+ import kv from "polystore";
730
+ function toStore(source) {
731
+ const store = source;
732
+ if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
733
+ return store;
734
+ }
735
+ return kv(source);
736
+ }
737
+
738
+ // src/helpers/disposition.ts
739
+ var encodeExt = (name) => encodeURIComponent(name).replace(
740
+ /['()*]/g,
741
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
742
+ );
743
+ function disposition(name) {
744
+ if (!name) return "attachment";
745
+ const clean = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
746
+ if (!clean) return "attachment";
747
+ const ascii = clean.replace(/[^\x20-\x7e]/g, "?");
748
+ const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
749
+ if (clean === ascii) return value;
750
+ return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
751
+ }
752
+
753
+ // src/helpers/fileType.ts
754
+ function fileType(file2) {
755
+ if (file2.type) return file2.type;
756
+ const name = file2.path || file2.name || "";
757
+ const ext2 = name.split(".").pop()?.toLowerCase();
758
+ return ext2 ? mimes_default[ext2] : void 0;
759
+ }
760
+
761
+ // src/helpers/isHtml.ts
762
+ var TAG = /^\s*<[a-zA-Z!/]/;
763
+ function isHtml(body) {
764
+ return TAG.test(body);
765
+ }
766
+
826
767
  // src/helpers/isReadableStream.ts
827
768
  function isReadableStream(obj) {
828
769
  return obj !== null && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.read === "function" && typeof obj.on === "function";
@@ -850,8 +791,7 @@ var Reply = class {
850
791
  download(name) {
851
792
  const ext2 = name?.split(".").pop();
852
793
  if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
853
- const filename = name ? `; filename="${encodeURIComponent(name)}"` : "";
854
- return this.headers("content-disposition", `attachment${filename}`);
794
+ return this.headers("content-disposition", disposition(name));
855
795
  }
856
796
  headers(key, value) {
857
797
  if (typeof key !== "string") {
@@ -859,10 +799,15 @@ var Reply = class {
859
799
  return this;
860
800
  }
861
801
  if (Array.isArray(value)) {
862
- Object.values(value).map((val) => this.headers(key, val));
802
+ this.res.headers.delete(key);
803
+ for (const val of value) this.res.headers.append(key, val);
863
804
  return this;
864
805
  }
865
- this.res.headers.append(key, value);
806
+ if (key.toLowerCase() === "set-cookie") {
807
+ this.res.headers.append(key, value);
808
+ } else {
809
+ this.res.headers.set(key, value);
810
+ }
866
811
  return this;
867
812
  }
868
813
  cache(value) {
@@ -884,25 +829,31 @@ var Reply = class {
884
829
  return this.headers("set-cookie", createCookies(key, value));
885
830
  }
886
831
  json(body) {
887
- return this.headers("content-type", "application/json").send(
888
- JSON.stringify(body)
889
- );
832
+ if (body === void 0) body = null;
833
+ if (!this.res.headers.get("content-type")) {
834
+ this.res.headers.set("content-type", "application/json");
835
+ }
836
+ return this.send(JSON.stringify(body));
890
837
  }
891
- redirect(path2) {
892
- return this.headers("location", path2).status(302).send();
838
+ redirect(path) {
839
+ this.headers("location", path);
840
+ if (this.res.status == null) this.res.status = 302;
841
+ return this.send();
893
842
  }
894
- async file(path2) {
895
- if (typeof path2 !== "string") {
896
- if (!await path2.exists()) return this.status(404).send();
897
- return this.type(path2.type).send(path2.stream());
843
+ async file(path) {
844
+ if (typeof path !== "string") {
845
+ if (!await path.exists()) return this.status(404).send();
846
+ return this.type(fileType(path)).send(path.stream());
898
847
  }
848
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
899
849
  try {
900
- const fs2 = await import("fs");
901
- const ext2 = path2.split(".").pop();
902
- const stream = fs2.createReadStream(path2);
850
+ const fs = await import("fs");
851
+ const ext2 = path.split(".").pop();
852
+ await fs.promises.access(path);
853
+ const stream = fs.createReadStream(path);
903
854
  return this.type(ext2).send(stream);
904
855
  } catch (error) {
905
- if (error.code === "ENOENT") {
856
+ if (error.code === "ENOENT" || error.code === "EISDIR") {
906
857
  return this.status(404).send();
907
858
  }
908
859
  throw error;
@@ -913,10 +864,10 @@ var Reply = class {
913
864
  if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
914
865
  return new Response(null, { status: status2, headers: headers2 });
915
866
  }
867
+ if (body === null) body = "";
916
868
  if (typeof body === "string") {
917
869
  if (!headers2.get("content-type")) {
918
- const isHtml = body.trim().startsWith("<");
919
- headers2.set("content-type", isHtml ? "text/html" : "text/plain");
870
+ headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
920
871
  }
921
872
  if (!headers2.has("content-length")) {
922
873
  headers2.set("content-length", String(Buffer.byteLength(body)));
@@ -1368,8 +1319,8 @@ var oauth = async (code) => {
1368
1319
  code
1369
1320
  })
1370
1321
  });
1371
- return (path2) => {
1372
- return fch(`https://api.github.com${path2}`, {
1322
+ return (path) => {
1323
+ return fch(`https://api.github.com${path}`, {
1373
1324
  headers: { Authorization: `Bearer ${res.access_token}` }
1374
1325
  });
1375
1326
  };
@@ -1504,18 +1455,30 @@ function parseAuthOptions(auth2, all) {
1504
1455
  if (!auth2.session && !all.store) {
1505
1456
  throw new Error("Need a sessionStore store for Auth");
1506
1457
  }
1507
- const store = auth2.store || all.store.prefix("user:");
1508
- const session2 = auth2.session || all.store.prefix("auth:");
1458
+ const store = all.store ? toStore(all.store) : null;
1459
+ const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
1460
+ const sessionStore = auth2.session ? toStore(auth2.session) : store.prefix("auth:");
1509
1461
  return {
1510
1462
  strategy,
1511
1463
  providers: list,
1512
1464
  redirect: redirect2,
1513
1465
  cleanUser,
1514
- store,
1515
- session: session2
1466
+ store: authStore,
1467
+ session: sessionStore
1516
1468
  };
1517
1469
  }
1518
1470
 
1471
+ // src/helpers/bucket.ts
1472
+ import FileSystem from "bucket/fs";
1473
+ function bucket(root) {
1474
+ if (!root) return null;
1475
+ if (typeof root === "string") return FileSystem(root);
1476
+ if (typeof root.file === "function") return root;
1477
+ throw new Error(
1478
+ "Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
1479
+ );
1480
+ }
1481
+
1519
1482
  // src/helpers/color.ts
1520
1483
  var map = {
1521
1484
  reset: 0,
@@ -1605,14 +1568,14 @@ function createLogger(level) {
1605
1568
  const request = (ctx, res) => {
1606
1569
  if (!enabled) return;
1607
1570
  const method = ctx.method.toUpperCase();
1608
- const path2 = ctx.url.pathname;
1571
+ const path = ctx.url.pathname;
1609
1572
  const reqLen = Number(ctx.headers["content-length"]) || 0;
1610
1573
  const resLen = Number(res.headers.get("content-length")) || 0;
1611
1574
  const status2 = res.status;
1612
1575
  const text = STATUS_TEXT[status2] || "";
1613
1576
  const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
1614
1577
  const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
1615
- let line = `${method} ${path2}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
1578
+ let line = `${method} ${path}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
1616
1579
  const location = res.headers.get("location");
1617
1580
  if (location) line += ` \u2192 ${location}`;
1618
1581
  message("api", line);
@@ -1651,10 +1614,23 @@ function resolveSecurity(security) {
1651
1614
  }
1652
1615
  return {
1653
1616
  trustProxy: o.trustProxy ?? true,
1617
+ traversalProtection: off ? false : o.traversalProtection !== false,
1654
1618
  headers: headers2,
1655
1619
  hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
1656
1620
  };
1657
1621
  }
1622
+ var CLIMBS = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
1623
+ var ABSOLUTE = /^(?:[\\/]|[a-zA-Z]:)/;
1624
+ function checkTraversal(params, ctx) {
1625
+ if (!ctx.options.security?.traversalProtection) return;
1626
+ for (const param in params) {
1627
+ const value = params[param];
1628
+ if (typeof value !== "string") continue;
1629
+ if (CLIMBS.test(value) || ABSOLUTE.test(value)) {
1630
+ throw errors_default.PATH_TRAVERSAL({ param, value });
1631
+ }
1632
+ }
1633
+ }
1658
1634
  function applySecurity(res, ctx) {
1659
1635
  const security = ctx.options.security;
1660
1636
  if (!security) return;
@@ -1726,21 +1702,22 @@ function config(options = {}) {
1726
1702
  if (!up) {
1727
1703
  settings.uploads = null;
1728
1704
  } else if (typeof up === "object" && "bucket" in up) {
1729
- const { bucket: bucket2, maxSize, minSize, fileType } = up;
1730
- const hasLimits = maxSize != null || minSize != null || fileType != null;
1731
- settings.uploads = hasLimits ? new UploadPipeline(bucket2).limit({ maxSize, minSize, fileType }) : bucket(bucket2);
1705
+ const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
1706
+ if (maxSize != null) parseBytes(maxSize);
1707
+ if (minSize != null) parseBytes(minSize);
1708
+ settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
1732
1709
  } else {
1733
- settings.uploads = bucket(up);
1710
+ settings.uploads = { bucket: bucket(up) };
1734
1711
  }
1735
1712
  const favicon2 = options.favicon || env2.FAVICON;
1736
1713
  if (favicon2) settings.favicon = favicon2;
1737
- settings.store = options.store ?? null;
1738
- settings.cookies = options.cookies ?? null;
1714
+ settings.store = options.store ? toStore(options.store) : null;
1739
1715
  if (options.session) {
1740
- settings.session = "store" in options.session ? options.session : { store: options.session };
1716
+ const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
1717
+ settings.session = { store: toStore(store) };
1741
1718
  }
1742
- if (options.store && !options.session) {
1743
- settings.session = { store: options.store.prefix("session:") };
1719
+ if (settings.store && !options.session) {
1720
+ settings.session = { store: settings.store.prefix("session:") };
1744
1721
  }
1745
1722
  if (options.auth || env2.AUTH) {
1746
1723
  settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
@@ -1893,9 +1870,10 @@ async function parseResponse(out, ctx) {
1893
1870
  if (!await out.exists()) {
1894
1871
  out = new Response(null, { status: 404 });
1895
1872
  } else {
1873
+ const type2 = fileType(out);
1896
1874
  out = new Response(
1897
1875
  out.stream(),
1898
- out.type ? { headers: { "content-type": out.type } } : void 0
1876
+ type2 ? { headers: { "content-type": type2 } } : void 0
1899
1877
  );
1900
1878
  }
1901
1879
  }
@@ -1909,7 +1887,7 @@ async function parseResponse(out, ctx) {
1909
1887
  out = new Response(void 0, { status: out });
1910
1888
  }
1911
1889
  if (typeof out === "string") {
1912
- const type2 = /^\s*</.test(out) ? "text/html" : "text/plain";
1890
+ const type2 = isHtml(out) ? mimes_default.html : mimes_default.text;
1913
1891
  out = new Response(out, {
1914
1892
  headers: {
1915
1893
  "content-type": type2,
@@ -1964,13 +1942,6 @@ async function parseResponse(out, ctx) {
1964
1942
  }
1965
1943
  ctx.options.session.store.set(id, ctx.session);
1966
1944
  }
1967
- if (ctx.options.cookies) {
1968
- if (Object.keys(ctx.res?.cookies || {}).length) {
1969
- for (const cookie of Object.values(ctx.res.cookies)) {
1970
- ctx.res.headers.append("set-cookie", cookie);
1971
- }
1972
- }
1973
- }
1974
1945
  if (ctx?.res?.headers) {
1975
1946
  for (const key in ctx.res.headers) {
1976
1947
  out.headers[key] = ctx.res.headers[key];
@@ -1980,14 +1951,14 @@ async function parseResponse(out, ctx) {
1980
1951
  }
1981
1952
 
1982
1953
  // src/pathPattern.ts
1983
- function pathPattern(pattern, path2) {
1984
- if (pattern === "*" && path2 === "/") return {};
1954
+ function pathPattern(pattern, path) {
1955
+ if (pattern === "*" && path === "/") return {};
1985
1956
  pattern = `/${pattern.replace(/^\//, "")}`;
1986
1957
  pattern = pattern.replace(/\/$/, "") || "/";
1987
- path2 = path2.replace(/\/$/, "") || "/";
1988
- if (pattern === path2) return {};
1958
+ path = path.replace(/\/$/, "") || "/";
1959
+ if (pattern === path) return {};
1989
1960
  const params = {};
1990
- const pathParts = path2.split("/").slice(1).map((u) => decodeURIComponent(u));
1961
+ const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
1991
1962
  const pattParts = pattern.split("/").slice(1);
1992
1963
  let allSame = true;
1993
1964
  for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
@@ -2046,7 +2017,7 @@ function validate(ctx, schema) {
2046
2017
  } catch (error) {
2047
2018
  if (error.name === "ZodError" || error.constructor.name === "ZodError") {
2048
2019
  const message = error.issues.map(
2049
- ({ path: path2, message: message2 }) => `[${base}.${path2.join(".")}]: ${message2}`
2020
+ ({ path, message: message2 }) => `[${base}.${path.join(".")}]: ${message2}`
2050
2021
  ).sort().join("\n");
2051
2022
  throw new StatusError(message, 422);
2052
2023
  }
@@ -2075,6 +2046,7 @@ async function getResponse(app, ctx) {
2075
2046
  if (Object.keys(route.options).length) {
2076
2047
  ctx.options = { ...app.settings, ...route.options };
2077
2048
  }
2049
+ checkTraversal(params, ctx);
2078
2050
  ctx.body = await resolveBody(ctx, ctx.options.body);
2079
2051
  for (const cb of route.fns) {
2080
2052
  if (typeof cb === "function") {
@@ -2174,7 +2146,12 @@ function parseCookies(cookies2) {
2174
2146
  return Object.fromEntries(
2175
2147
  cookieStr.split(/;\s*/).map((part) => {
2176
2148
  const [key, ...rest] = part.split("=");
2177
- return [key, decodeURIComponent(rest.join("="))];
2149
+ const value = rest.join("=");
2150
+ try {
2151
+ return [key, decodeURIComponent(value)];
2152
+ } catch {
2153
+ return [key, value];
2154
+ }
2178
2155
  })
2179
2156
  );
2180
2157
  }
@@ -2236,7 +2213,7 @@ async function verify(password, hash3) {
2236
2213
  const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
2237
2214
  const nonce = Buffer.from(saltB64, "base64");
2238
2215
  const expected = Buffer.from(hashB64, "base64");
2239
- return new Promise((resolve2, reject) => {
2216
+ return new Promise((resolve, reject) => {
2240
2217
  crypto3.argon2(
2241
2218
  `argon2${variant}`,
2242
2219
  {
@@ -2250,9 +2227,9 @@ async function verify(password, hash3) {
2250
2227
  (err, derivedKey) => {
2251
2228
  if (err) return reject(err);
2252
2229
  if (derivedKey.length === expected.length && timingSafeEqual(derivedKey, expected)) {
2253
- resolve2(true);
2230
+ resolve(true);
2254
2231
  } else {
2255
- resolve2(false);
2232
+ resolve(false);
2256
2233
  }
2257
2234
  }
2258
2235
  );
@@ -2444,17 +2421,18 @@ async function assets(ctx) {
2444
2421
  try {
2445
2422
  const key = ctx.url.pathname.replace(/^\/+/, "");
2446
2423
  const file2 = ctx.options.public.file(key);
2447
- const meta = file2.info ? await file2.info() : null;
2448
- if (meta ? !meta.exists : !await file2.exists()) return;
2449
- const ext2 = ctx.url.pathname.split(".").pop();
2450
- const ctype = meta?.type || ext2;
2424
+ const info = file2.info?.bind(file2);
2425
+ const meta = info ? await info() : null;
2426
+ if (info ? !meta : !await file2.exists()) return;
2427
+ const ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
2428
+ const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
2451
2429
  const headers2 = { "cache-control": CACHE_CONTROL };
2452
2430
  let tag;
2453
2431
  if (meta) {
2454
- const stamp = meta.date ? meta.date.getTime() : 0;
2432
+ const stamp = meta.modified ? meta.modified.getTime() : 0;
2455
2433
  tag = `W/"${meta.size.toString(16)}-${stamp.toString(16)}"`;
2456
2434
  headers2.etag = tag;
2457
- if (meta.date) headers2["last-modified"] = meta.date.toUTCString();
2435
+ if (meta.modified) headers2["last-modified"] = meta.modified.toUTCString();
2458
2436
  }
2459
2437
  const canRange = !!(meta && file2.slice);
2460
2438
  if (canRange) headers2["accept-ranges"] = "bytes";
@@ -2510,7 +2488,7 @@ async function favicon(ctx) {
2510
2488
  }
2511
2489
 
2512
2490
  // src/middle/openapi.ts
2513
- import * as fsp2 from "fs/promises";
2491
+ import * as fsp from "fs/promises";
2514
2492
  var entities = {
2515
2493
  "&": "&amp;",
2516
2494
  "<": "&lt;",
@@ -2556,7 +2534,7 @@ function zodToSchema(schema) {
2556
2534
  }
2557
2535
  return { type: type2 };
2558
2536
  }
2559
- var pkgProm = fsp2.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
2537
+ var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
2560
2538
  var getTag = (name, fn) => {
2561
2539
  const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
2562
2540
  if (!found) return "";
@@ -2568,14 +2546,14 @@ var generateOpenApiPaths = (handlers) => {
2568
2546
  const paths = {};
2569
2547
  for (const [method, routes] of Object.entries(handlers)) {
2570
2548
  for (const route of routes) {
2571
- const path2 = route.path;
2549
+ const path = route.path;
2572
2550
  const fn = route.fns.find((p) => typeof p === "function");
2573
2551
  const meta = route.fns.find((p) => typeof p === "object");
2574
2552
  const config2 = getConfig(route.options);
2575
- if (typeof path2 !== "string" || path2 === "*" || path2 === "/docs" || !fn) {
2553
+ if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
2576
2554
  continue;
2577
2555
  }
2578
- const normalizedPath = path2.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
2556
+ const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
2579
2557
  if (!paths[normalizedPath]) {
2580
2558
  paths[normalizedPath] = {};
2581
2559
  }
@@ -2602,7 +2580,7 @@ var generateOpenApiPaths = (handlers) => {
2602
2580
  };
2603
2581
  }
2604
2582
  const parameters = [];
2605
- const matched = Array.from(path2.matchAll(/:[\w()]+/gi));
2583
+ const matched = Array.from(path.matchAll(/:[\w()]+/gi));
2606
2584
  matched.forEach((match) => {
2607
2585
  const [name, type2 = "string"] = match[0].slice(1).replace(/\)/, "").split("(");
2608
2586
  parameters.push({
@@ -2941,18 +2919,18 @@ async function createNode(req, app) {
2941
2919
  const cookies2 = parseCookies(headers2.cookie);
2942
2920
  const scheme = req.socket instanceof TLSSocket ? "https" : "http";
2943
2921
  const host = headers2.host || `localhost:${app.settings.port}`;
2944
- const path2 = (req.url || "/").replace(/\/$/, "") || "/";
2922
+ const path = (req.url || "/").replace(/\/$/, "") || "/";
2945
2923
  const baseUrl = `${scheme}://${host}`;
2946
- const url = new URL(path2, baseUrl);
2924
+ const url = new URL(path, baseUrl);
2947
2925
  define(
2948
2926
  url,
2949
2927
  "query",
2950
2928
  (url2) => Object.fromEntries(url2.searchParams.entries())
2951
2929
  );
2952
2930
  const source = {
2953
- getBuffer: () => new Promise((resolve2, reject) => {
2931
+ getBuffer: () => new Promise((resolve, reject) => {
2954
2932
  const chunks2 = [];
2955
- req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve2(Buffer.concat(chunks2))).on("error", reject);
2933
+ req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve(Buffer.concat(chunks2))).on("error", reject);
2956
2934
  }),
2957
2935
  getStream: () => toWeb(req)
2958
2936
  };
@@ -3104,9 +3082,9 @@ var Router = class _Router {
3104
3082
  // functions into a single flat `fns` list. A plain options object may sit
3105
3083
  // between the path and the handlers, and it's pulled out here.
3106
3084
  handle(method, pathOrFn, ...rest) {
3107
- let path2 = "*";
3085
+ let path = "*";
3108
3086
  if (typeof pathOrFn === "string") {
3109
- path2 = pathOrFn;
3087
+ path = pathOrFn;
3110
3088
  } else if (pathOrFn != null) {
3111
3089
  rest.unshift(pathOrFn);
3112
3090
  }
@@ -3116,7 +3094,7 @@ var Router = class _Router {
3116
3094
  }
3117
3095
  const base = method === "socket" ? [] : this.middleware;
3118
3096
  const fns = [...base, ...rest].filter((fn) => fn != null);
3119
- this.handlers[method].push({ path: path2, options, fns });
3097
+ this.handlers[method].push({ path, options, fns });
3120
3098
  return this.self();
3121
3099
  }
3122
3100
  socket(pathOrMid, optionsOrMid, ...middleware) {
@@ -3181,31 +3159,33 @@ function isSerializable(body) {
3181
3159
  }
3182
3160
  function ServerTest(app) {
3183
3161
  const port = app.settings.port;
3184
- const fetch2 = async (method, path2, options = {}) => {
3162
+ const fetch2 = async (method, path, options = {}) => {
3185
3163
  if (!options.headers) options.headers = {};
3186
3164
  if (isSerializable(options.body)) {
3187
3165
  options.headers["content-type"] = "application/json";
3188
3166
  options.body = JSON.stringify(options.body);
3189
3167
  }
3190
3168
  return await app.fetch(
3191
- new Request(`http://localhost:${port}${path2}`, {
3169
+ new Request(`http://localhost:${port}${path}`, {
3192
3170
  method,
3193
3171
  ...options
3194
3172
  })
3195
3173
  );
3196
3174
  };
3197
3175
  return {
3198
- get: (path2, options) => fetch2("get", path2, options),
3199
- head: (path2, options) => fetch2("head", path2, options),
3200
- post: (path2, body, options) => fetch2("post", path2, { body, ...options }),
3201
- put: (path2, body, options) => fetch2("put", path2, { body, ...options }),
3202
- patch: (path2, body, options) => fetch2("patch", path2, { body, ...options }),
3203
- delete: (path2, options) => fetch2("delete", path2, options),
3204
- options: (path2, options) => fetch2("options", path2, options)
3176
+ get: (path, options) => fetch2("get", path, options),
3177
+ head: (path, options) => fetch2("head", path, options),
3178
+ post: (path, body, options) => fetch2("post", path, { body, ...options }),
3179
+ put: (path, body, options) => fetch2("put", path, { body, ...options }),
3180
+ patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
3181
+ delete: (path, options) => fetch2("delete", path, options),
3182
+ options: (path, options) => fetch2("options", path, options)
3205
3183
  };
3206
3184
  }
3207
3185
 
3208
3186
  // src/index.ts
3187
+ import { default as default2 } from "polystore";
3188
+ import { default as default3 } from "bucket";
3209
3189
  var Server = class extends Router {
3210
3190
  settings;
3211
3191
  platform;
@@ -3273,6 +3253,7 @@ function server(options) {
3273
3253
  export {
3274
3254
  Server,
3275
3255
  ServerError_default as ServerError,
3256
+ default3 as bucket,
3276
3257
  cache,
3277
3258
  cookies,
3278
3259
  server as default,
@@ -3280,6 +3261,7 @@ export {
3280
3261
  file,
3281
3262
  headers,
3282
3263
  json,
3264
+ default2 as kv,
3283
3265
  redirect,
3284
3266
  router,
3285
3267
  send,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.37.1",
3
+ "version": "0.39.0",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "github:franciscop/server-next",
@@ -44,18 +44,37 @@
44
44
  "home": "./docs/index.html",
45
45
  "menu": {
46
46
  "Documentation": "/documentation",
47
+ "Tutorials": "/tutorials",
47
48
  "Github": "https://github.com/franciscop/server-next"
48
- }
49
+ },
50
+ "documentation": {
51
+ "Documentation": [
52
+ "docs/0. Documentation.md",
53
+ "docs/1. Getting Started.md"
54
+ ],
55
+ "Guides": "docs/2. Guides.md",
56
+ "Options": "docs/3. Options.md",
57
+ "Router": "docs/4. Router.md",
58
+ "Context": "docs/5. Context.md",
59
+ "Reply": "docs/6. Reply.md",
60
+ "Authentication": "docs/7. Authentication.md",
61
+ "Testing": "docs/8. Testing.md",
62
+ "Platforms": "docs/9. Platforms.md",
63
+ "FAQ": "docs/A. FAQ.md"
64
+ },
65
+ "tutorials": "docs/tutorials"
66
+ },
67
+ "dependencies": {
68
+ "bucket": "^0.6.0",
69
+ "polystore": "^0.23.2"
49
70
  },
50
71
  "devDependencies": {
51
72
  "@types/bun": "^1.3.0",
52
73
  "@types/jest": "^30.0.0",
53
74
  "@types/node": "^24.10.0",
54
- "bucket": "^0.4.0",
55
75
  "bun": "^1.3.13",
56
76
  "check-dts": "^0.8.2",
57
77
  "jest": "^29.7.0",
58
- "polystore": "^0.21.1",
59
78
  "tsup": "^8.5.1",
60
79
  "typescript": "^6.0.2"
61
80
  },
package/readme.md CHANGED
@@ -1 +1,29 @@
1
1
  # Server JS [![@server/next](https://img.shields.io/npm/v/@server/next?label=@server/next&color=greenlime)](https://www.npmjs.com/package/@server/next) [![tests](https://github.com/franciscop/server-next/workflows/tests/badge.svg)](https://github.com/franciscop/server-next/actions)
2
+
3
+ A modern web server for Bun and Node, with routing, authentication, uploads, WebSockets and testing built in.
4
+
5
+ ```bash
6
+ npm install @server/next
7
+ ```
8
+
9
+ ```js
10
+ import server from '@server/next';
11
+
12
+ export default server({ store: new Map(), uploads: './uploads' })
13
+ .get('/', () => 'Hello world')
14
+ .get('/users/:id', (ctx) => db.users.find(ctx.url.params.id))
15
+ .post('/avatar', (ctx) => ctx.body.avatar.path);
16
+ ```
17
+
18
+ Key-value stores and file storage come included, so `store` takes a plain `Map` and `uploads` takes a folder path. For Redis, S3 and the rest, `kv` and `bucket` are exported too:
19
+
20
+ ```js
21
+ import server, { kv, bucket } from '@server/next';
22
+
23
+ const store = kv(createClient({ url }).connect());
24
+ const uploads = bucket.S3('my-bucket', { id, key });
25
+
26
+ export default server({ store, uploads });
27
+ ```
28
+
29
+ See the [full documentation](https://serverjs.io/documentation).