@server/next 0.31.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 +10 -6
  2. package/index.js +462 -248
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -27,10 +27,12 @@ declare namespace JSX {
27
27
  [elem: string]: any;
28
28
  }
29
29
  }
30
+ type BodyMode = "parse" | "raw" | "stream";
30
31
  type RouteOptions = {
31
32
  tags?: string | string[];
32
33
  title?: string;
33
34
  description?: string;
35
+ body?: BodyMode;
34
36
  };
35
37
  type Route = {
36
38
  path: string;
@@ -48,9 +50,11 @@ type Cookie = {
48
50
  };
49
51
  type RouterMethod = "*" | Method;
50
52
  type Bucket = {
53
+ location?: string;
51
54
  read: (path: string) => Promise<ReadableStream | null>;
52
- write: (path: string, data: string | Buffer) => Promise<void | string>;
55
+ write: (path: string, data: string | Buffer | ReadableStream) => Promise<void | string>;
53
56
  delete: (path: string) => Promise<boolean>;
57
+ folder?: (prefix: string) => Bucket;
54
58
  };
55
59
  type UploadedFile = {
56
60
  name: string;
@@ -134,7 +138,6 @@ type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
134
138
  type Options = {
135
139
  port?: number;
136
140
  secret?: string;
137
- views?: string | Bucket;
138
141
  public?: string | Bucket;
139
142
  uploads?: string | Bucket | UploadPipeline;
140
143
  store?: KVStore;
@@ -149,11 +152,11 @@ type Options = {
149
152
  log?: LogLevel | boolean;
150
153
  favicon?: string | Bucket;
151
154
  security?: SecurityOptions;
155
+ body?: BodyMode;
152
156
  };
153
157
  type Settings = {
154
158
  port: number;
155
159
  secret: string;
156
- views?: Bucket;
157
160
  public?: Bucket;
158
161
  uploads?: Bucket | UploadPipeline;
159
162
  store?: KVStore;
@@ -168,6 +171,7 @@ type Settings = {
168
171
  log: Logger;
169
172
  favicon?: string | Bucket;
170
173
  security: SecuritySettings;
174
+ body: BodyMode;
171
175
  };
172
176
  type Time = {
173
177
  (name: string): void;
@@ -203,7 +207,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
203
207
  ip: string;
204
208
  headers: Record<string, string | string[]>;
205
209
  cookies: Record<string, string>;
206
- body?: SerializableValue;
210
+ body?: SerializableValue | Buffer | ReadableStream;
207
211
  url: URL & {
208
212
  params: Params;
209
213
  query: Record<string, string>;
@@ -230,7 +234,7 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
230
234
  type InlineReply = Response | {
231
235
  body: string;
232
236
  headers?: Headers;
233
- } | SerializableValue | JSX.Element;
237
+ } | SerializableValue | JSX.Element | Buffer | ReadableStream;
234
238
  type Body = InlineReply;
235
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>;
236
240
 
@@ -454,4 +458,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
454
458
  }
455
459
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
456
460
 
457
- export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type 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 };
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) {
@@ -1276,6 +1576,9 @@ async function parseResponse(out, ctx) {
1276
1576
  if (out instanceof ReadableStream) {
1277
1577
  out = new Response(out);
1278
1578
  }
1579
+ if (out instanceof Uint8Array) {
1580
+ out = new Response(out);
1581
+ }
1279
1582
  if (typeof out === "number") {
1280
1583
  out = new Response(void 0, { status: out });
1281
1584
  }
@@ -1442,6 +1745,7 @@ async function getResponse(app, ctx) {
1442
1745
  if (Object.keys(route.options).length) {
1443
1746
  ctx.options = { ...app.settings, ...route.options };
1444
1747
  }
1748
+ ctx.body = await resolveBody(ctx, ctx.options.body);
1445
1749
  for (const cb of route.fns) {
1446
1750
  if (typeof cb === "function") {
1447
1751
  const res = await cb(ctx);
@@ -1454,6 +1758,7 @@ async function getResponse(app, ctx) {
1454
1758
  break;
1455
1759
  }
1456
1760
  if (!matched) {
1761
+ ctx.body = await resolveBody(ctx, ctx.options.body);
1457
1762
  for (const mw of app.middleware) {
1458
1763
  const out = await parseResponse(await mw(ctx), ctx);
1459
1764
  if (out) return out;
@@ -1472,7 +1777,7 @@ async function getResponse(app, ctx) {
1472
1777
  import * as crypto2 from "crypto";
1473
1778
  import { getRandomValues } from "crypto";
1474
1779
  import { promisify } from "util";
1475
- async function hash(password) {
1780
+ async function hash2(password) {
1476
1781
  if ("argon2" in crypto2) {
1477
1782
  const argon23 = promisify(crypto2.argon2);
1478
1783
  const buf = await argon23("argon2id", {
@@ -1536,102 +1841,6 @@ function iteratorToReadable(generator) {
1536
1841
  });
1537
1842
  }
1538
1843
 
1539
- // src/helpers/parseBody.ts
1540
- function getBoundary(header) {
1541
- if (!header) return null;
1542
- if (header.includes("multipart/form-data") && !header.includes("boundary=")) {
1543
- console.error("Do not set the `Content-Type` manually for FormData");
1544
- }
1545
- const items = header.split(";");
1546
- for (const item of items) {
1547
- const trimmedItem = item.trim();
1548
- if (trimmedItem.startsWith("boundary=")) {
1549
- return trimmedItem.split("=")[1].trim();
1550
- }
1551
- }
1552
- return null;
1553
- }
1554
- function getMatching(string, regex) {
1555
- const matches = string.match(regex);
1556
- return matches?.[1] ?? "";
1557
- }
1558
- function splitBuffer(buffer, delimiter) {
1559
- const result = [];
1560
- let start = 0;
1561
- let index = buffer.indexOf(delimiter);
1562
- while (index !== -1) {
1563
- result.push(buffer.slice(start, index));
1564
- start = index + delimiter.length;
1565
- index = buffer.indexOf(delimiter, start);
1566
- }
1567
- result.push(buffer.slice(start));
1568
- return result;
1569
- }
1570
- var BREAK_BUFFER = Buffer.from("\r\n\r\n");
1571
- var END_BUFFER = Buffer.from("--\r\n");
1572
- function isProbablyText(buffer) {
1573
- for (let i = 0; i < Math.min(buffer.length, 512); i++) {
1574
- const byte = buffer[i];
1575
- if (byte === 0) return false;
1576
- if (byte < 7 || byte > 13 && byte < 32) return false;
1577
- }
1578
- return true;
1579
- }
1580
- async function parseBody(raw, contentType, bucket) {
1581
- const contentTypeStr = Array.isArray(contentType) ? contentType[0] : contentType;
1582
- if (!raw || raw.length === 0) return {};
1583
- if (!contentTypeStr || /^text\//.test(contentTypeStr)) {
1584
- return raw.toString("utf-8");
1585
- }
1586
- if (/application\/json/.test(contentTypeStr)) {
1587
- return JSON.parse(raw.toString("utf-8"));
1588
- }
1589
- const boundary = getBoundary(contentTypeStr);
1590
- if (!boundary) return null;
1591
- const body = {};
1592
- const boundaryBuffer = Buffer.from(`--${boundary}`);
1593
- const parts = splitBuffer(raw, boundaryBuffer);
1594
- for (const part of parts) {
1595
- if (part.length === 0 || part.equals(END_BUFFER)) continue;
1596
- const idx = part.indexOf(BREAK_BUFFER);
1597
- if (idx === -1) continue;
1598
- const headerStr = part.slice(0, idx).toString("utf-8");
1599
- const contentBuf = part.slice(idx + BREAK_BUFFER.length, part.length - 2);
1600
- const name = getMatching(headerStr, /name="(.+?)"/).trim().replace(/\[\]$/, "");
1601
- if (!name) continue;
1602
- const filename = getMatching(headerStr, /filename="(.+?)"/).trim();
1603
- if (filename) {
1604
- const partContentType = getMatching(headerStr, /Content-Type:\s*([^\r\n]+)/i).trim() || "application/octet-stream";
1605
- if (!bucket) {
1606
- continue;
1607
- }
1608
- if (bucket instanceof UploadPipeline) {
1609
- body[name] = await bucket.processFile(
1610
- filename,
1611
- contentBuf,
1612
- partContentType
1613
- );
1614
- } else {
1615
- body[name] = await saveFileToBucket(
1616
- filename,
1617
- contentBuf,
1618
- bucket,
1619
- partContentType
1620
- );
1621
- }
1622
- } else {
1623
- const value = isProbablyText(contentBuf) ? contentBuf.toString("utf-8").trim() : contentBuf;
1624
- if (body[name]) {
1625
- if (!Array.isArray(body[name])) body[name] = [body[name]];
1626
- body[name].push(value);
1627
- } else {
1628
- body[name] = value;
1629
- }
1630
- }
1631
- }
1632
- return body;
1633
- }
1634
-
1635
1844
  // src/helpers/parseCookies.ts
1636
1845
  function parseCookies(cookies2) {
1637
1846
  if (!cookies2) return {};
@@ -1899,13 +2108,13 @@ function auth(app) {
1899
2108
  app.use(async function middle(ctx) {
1900
2109
  ctx.user = await getUser(ctx);
1901
2110
  });
2111
+ app.post("/auth/logout", logout);
1902
2112
  const enabled = app.settings.auth.provider;
1903
2113
  for (const name of oauth2) {
1904
2114
  if (!enabled.includes(name)) continue;
1905
2115
  const key = name.toUpperCase();
1906
2116
  if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
1907
2117
  if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
1908
- app.get("/auth/logout", logout);
1909
2118
  app.get(`/auth/login/${name}`, providers_default[name].login);
1910
2119
  app.get(`/auth/callback/${name}`, providers_default[name].callback);
1911
2120
  }
@@ -1914,12 +2123,10 @@ function auth(app) {
1914
2123
  for (const key of keys) {
1915
2124
  if (!env[key]) throw new Error(`${key} not defined`);
1916
2125
  }
1917
- app.get("/auth/logout", logout);
1918
2126
  app.get("/auth/login/apple", providers_default.apple.login);
1919
2127
  app.post("/auth/callback/apple", providers_default.apple.callback);
1920
2128
  }
1921
2129
  if (enabled.includes("email")) {
1922
- app.post("/auth/logout", logout);
1923
2130
  app.post("/auth/register/email", providers_default.email.register);
1924
2131
  app.post("/auth/login/email", providers_default.email.login);
1925
2132
  app.put("/auth/password/email", providers_default.email.password);
@@ -2229,21 +2436,20 @@ async function createNode(req, app) {
2229
2436
  "query",
2230
2437
  (url2) => Object.fromEntries(url2.searchParams.entries())
2231
2438
  );
2232
- const rawBody = await new Promise((resolve2, reject) => {
2233
- const body2 = [];
2234
- req.on("data", (chunk) => body2.push(chunk)).on("end", () => resolve2(Buffer.concat(body2))).on("error", reject);
2235
- });
2236
- if (rawBody.length && !headers2["content-length"]) {
2237
- headers2["content-length"] = String(rawBody.length);
2238
- }
2239
- 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
+ };
2240
2446
  const events = createEvents();
2241
- return {
2447
+ const ctx = {
2242
2448
  options: app.settings,
2243
2449
  platform: app.platform,
2244
2450
  url,
2245
2451
  method,
2246
- body,
2452
+ body: void 0,
2247
2453
  headers: headers2,
2248
2454
  cookies: cookies2,
2249
2455
  session: {},
@@ -2255,6 +2461,8 @@ async function createNode(req, app) {
2255
2461
  trustProxy: app.settings.security.trustProxy
2256
2462
  })
2257
2463
  };
2464
+ setBodySource(ctx, source);
2465
+ return ctx;
2258
2466
  }
2259
2467
 
2260
2468
  // src/context/winter.ts
@@ -2273,18 +2481,17 @@ async function createWinter(req, app, server2) {
2273
2481
  "query",
2274
2482
  (url2) => Object.fromEntries(url2.searchParams.entries())
2275
2483
  );
2276
- const rawBody = Buffer.from(await req.arrayBuffer());
2277
- if (rawBody.length && !headers2["content-length"]) {
2278
- headers2["content-length"] = String(rawBody.length);
2279
- }
2280
- 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
+ };
2281
2488
  const events = createEvents();
2282
- return {
2489
+ const ctx = {
2283
2490
  options: app.settings,
2284
2491
  platform: app.platform,
2285
2492
  url,
2286
2493
  method,
2287
- body,
2494
+ body: void 0,
2288
2495
  headers: headers2,
2289
2496
  cookies: cookies2,
2290
2497
  session: {},
@@ -2296,6 +2503,8 @@ async function createWinter(req, app, server2) {
2296
2503
  trustProxy: app.settings.security.trustProxy
2297
2504
  })
2298
2505
  };
2506
+ setBodySource(ctx, source);
2507
+ return ctx;
2299
2508
  }
2300
2509
 
2301
2510
  // src/context/handlers.ts
@@ -2322,6 +2531,11 @@ var Node = async (app) => {
2322
2531
  response.end();
2323
2532
  }).listen(app.settings.port, () => {
2324
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
+ }
2325
2539
  });
2326
2540
  };
2327
2541
  var Netlify = async (app, request, context) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.31.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",