@server/next 0.42.0 → 0.44.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 +30 -24
  2. package/index.js +143 -183
  3. package/package.json +5 -3
package/index.d.ts CHANGED
@@ -96,17 +96,20 @@ type RouteSchema = {
96
96
  description?: string;
97
97
  };
98
98
  type RouteOptions = {
99
- schema?: RouteSchema;
99
+ schema?: RouteSchema | false;
100
100
  parser?: BodyMode;
101
101
  body?: StandardSchemaV1<any, any>;
102
102
  query?: StandardSchemaV1<any, any>;
103
103
  params?: StandardSchemaV1<any, any>;
104
104
  response?: StandardSchemaV1<any, any>;
105
105
  cache?: CacheOption;
106
+ uploads?: string | Bucket | UploadOptions | false;
106
107
  };
107
108
  type Route = {
108
109
  path: string;
109
- options: RouteOptions;
110
+ options: Omit<RouteOptions, "uploads"> & {
111
+ uploads?: Settings["uploads"];
112
+ };
110
113
  fns: Middleware[];
111
114
  };
112
115
  type Cookie = {
@@ -256,11 +259,15 @@ type Options = {
256
259
  sessions?: StoreSource;
257
260
  cors?: CorsOptions;
258
261
  auth?: AuthOption;
259
- openapi?: any;
262
+ openapi?: boolean | string | {
263
+ path?: string;
264
+ title?: string;
265
+ description?: string;
266
+ version?: string;
267
+ };
260
268
  onError?: OnError;
261
269
  onResponse?: OnResponse;
262
270
  log?: LogLevel | boolean;
263
- favicon?: string | BucketFile;
264
271
  security?: boolean | SecurityOptions;
265
272
  parser?: BodyMode;
266
273
  cache?: CacheOption;
@@ -276,11 +283,15 @@ type Settings = {
276
283
  sessionsDefault?: boolean;
277
284
  cors?: CorsSettings;
278
285
  auth?: AuthSettings;
279
- openapi?: any;
286
+ openapi?: {
287
+ path: string;
288
+ title?: string;
289
+ description?: string;
290
+ version?: string;
291
+ };
280
292
  onError?: OnError;
281
293
  onResponse?: OnResponse;
282
294
  log: Logger;
283
- favicon?: string | BucketFile;
284
295
  security: SecuritySettings;
285
296
  parser: BodyMode;
286
297
  cache?: CacheOption;
@@ -316,9 +327,9 @@ interface ContextExtension {
316
327
  type Context<C extends ContextTypes = {}> = {
317
328
  method: Method;
318
329
  ip: string;
330
+ signal: AbortSignal;
319
331
  headers: Record<string, string | string[]>;
320
332
  cookies: Record<string, string>;
321
- body?: Field<C, "body", SerializableValue | Buffer | ReadableStream>;
322
333
  url: URL & {
323
334
  params: Field<C, "params", Record<string, any>>;
324
335
  query: Field<C, "query", Record<string, any>>;
@@ -331,12 +342,12 @@ type Context<C extends ContextTypes = {}> = {
331
342
  session: Field<C, "session", Record<string, any>>;
332
343
  user?: Field<C, "user", Record<string, any>>;
333
344
  init: number;
334
- req?: Request;
335
- res?: Response & {
336
- cookies?: Record<string, string>;
337
- };
338
345
  app: Server;
339
- } & ContextExtension;
346
+ } & ("body" extends keyof C ? {
347
+ body: Field<C, "body", never>;
348
+ } : {
349
+ body?: SerializableValue | Buffer | ReadableStream;
350
+ }) & ContextExtension;
340
351
  type InlineReply = Response | Reply | BucketFile | {
341
352
  body: string;
342
353
  headers?: Headers;
@@ -431,11 +442,6 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
431
442
  platform: Platform;
432
443
  sockets: any[];
433
444
  websocket: any;
434
- faviconCache?: {
435
- bytes: Buffer;
436
- type: string;
437
- etag: string;
438
- } | null;
439
445
  port?: number;
440
446
  constructor(options?: Options);
441
447
  self(): this;
@@ -445,6 +451,7 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
445
451
  test(): {
446
452
  get: (path: string, options?: {
447
453
  method?: string;
454
+ signal?: AbortSignal | null;
448
455
  headers?: HeadersInit;
449
456
  cache?: RequestCache;
450
457
  redirect?: RequestRedirect;
@@ -455,11 +462,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
455
462
  priority?: RequestPriority;
456
463
  referrer?: string;
457
464
  referrerPolicy?: ReferrerPolicy;
458
- signal?: AbortSignal | null;
459
465
  window?: null;
460
466
  }) => Promise<Response>;
461
467
  head: (path: string, options?: {
462
468
  method?: string;
469
+ signal?: AbortSignal | null;
463
470
  headers?: HeadersInit;
464
471
  cache?: RequestCache;
465
472
  redirect?: RequestRedirect;
@@ -470,13 +477,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
470
477
  priority?: RequestPriority;
471
478
  referrer?: string;
472
479
  referrerPolicy?: ReferrerPolicy;
473
- signal?: AbortSignal | null;
474
480
  window?: null;
475
481
  }) => Promise<Response>;
476
482
  post: (path: string, body?: string | number | boolean | ArrayBuffer | {
477
483
  [key: string]: SerializableValue;
478
484
  } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
479
485
  method?: string;
486
+ signal?: AbortSignal | null;
480
487
  headers?: HeadersInit;
481
488
  cache?: RequestCache;
482
489
  redirect?: RequestRedirect;
@@ -487,13 +494,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
487
494
  priority?: RequestPriority;
488
495
  referrer?: string;
489
496
  referrerPolicy?: ReferrerPolicy;
490
- signal?: AbortSignal | null;
491
497
  window?: null;
492
498
  }) => Promise<Response>;
493
499
  put: (path: string, body?: string | number | boolean | ArrayBuffer | {
494
500
  [key: string]: SerializableValue;
495
501
  } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
496
502
  method?: string;
503
+ signal?: AbortSignal | null;
497
504
  headers?: HeadersInit;
498
505
  cache?: RequestCache;
499
506
  redirect?: RequestRedirect;
@@ -504,13 +511,13 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
504
511
  priority?: RequestPriority;
505
512
  referrer?: string;
506
513
  referrerPolicy?: ReferrerPolicy;
507
- signal?: AbortSignal | null;
508
514
  window?: null;
509
515
  }) => Promise<Response>;
510
516
  patch: (path: string, body?: string | number | boolean | ArrayBuffer | {
511
517
  [key: string]: SerializableValue;
512
518
  } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
513
519
  method?: string;
520
+ signal?: AbortSignal | null;
514
521
  headers?: HeadersInit;
515
522
  cache?: RequestCache;
516
523
  redirect?: RequestRedirect;
@@ -521,11 +528,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
521
528
  priority?: RequestPriority;
522
529
  referrer?: string;
523
530
  referrerPolicy?: ReferrerPolicy;
524
- signal?: AbortSignal | null;
525
531
  window?: null;
526
532
  }) => Promise<Response>;
527
533
  delete: (path: string, options?: {
528
534
  method?: string;
535
+ signal?: AbortSignal | null;
529
536
  headers?: HeadersInit;
530
537
  cache?: RequestCache;
531
538
  redirect?: RequestRedirect;
@@ -536,11 +543,11 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
536
543
  priority?: RequestPriority;
537
544
  referrer?: string;
538
545
  referrerPolicy?: ReferrerPolicy;
539
- signal?: AbortSignal | null;
540
546
  window?: null;
541
547
  }) => Promise<Response>;
542
548
  options: (path: string, options?: {
543
549
  method?: string;
550
+ signal?: AbortSignal | null;
544
551
  headers?: HeadersInit;
545
552
  cache?: RequestCache;
546
553
  redirect?: RequestRedirect;
@@ -551,7 +558,6 @@ declare class Server<C extends ContextTypes = {}> extends Router<C> {
551
558
  priority?: RequestPriority;
552
559
  referrer?: string;
553
560
  referrerPolicy?: ReferrerPolicy;
554
- signal?: AbortSignal | null;
555
561
  window?: null;
556
562
  }) => Promise<Response>;
557
563
  };
package/index.js CHANGED
@@ -50,6 +50,7 @@ ServerError_default.extend({
50
50
  message: "Missing the OAuth 'code' in the request body"
51
51
  },
52
52
  SESSION_JWT: "The `jwt` strategy is stateless, so there is no `ctx.session` (tried '{key}'). Use the `token` strategy for server-side sessions, or `cookie` for browsers",
53
+ SESSION_GUEST: "No `ctx.session` for this request (tried '{key}'): the `token` strategy carries the session in the Authorization header, and this request has none. Sign in first, or use the `cookie` strategy for guest sessions",
53
54
  AUTH_INVALID_HEADER: {
54
55
  status: 401,
55
56
  message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
@@ -104,6 +105,17 @@ var StatusError = class extends Error {
104
105
  }
105
106
  };
106
107
 
108
+ // src/helpers/bucket.ts
109
+ import FileSystem from "bucket/fs";
110
+ function bucket(root) {
111
+ if (!root) return null;
112
+ if (typeof root === "string") return FileSystem(root);
113
+ if (typeof root.file === "function") return root;
114
+ throw new Error(
115
+ "Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
116
+ );
117
+ }
118
+
107
119
  // src/helpers/createId.ts
108
120
  var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
109
121
  var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
@@ -146,6 +158,16 @@ function createId(source, size = 16) {
146
158
  }
147
159
 
148
160
  // src/helpers/upload.ts
161
+ function resolveUploads(up) {
162
+ if (!up) return null;
163
+ if (typeof up === "object" && "bucket" in up) {
164
+ const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
165
+ if (maxSize != null) parseBytes(maxSize);
166
+ if (minSize != null) parseBytes(minSize);
167
+ return { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
168
+ }
169
+ return { bucket: bucket(up) };
170
+ }
149
171
  function parseBytes(value) {
150
172
  if (typeof value === "number") return value;
151
173
  const units = {
@@ -164,8 +186,8 @@ function getExt(filename) {
164
186
  return filename.slice(i).toLowerCase();
165
187
  }
166
188
  async function saveFileToBucket(originalName, data, bucket2, contentType) {
167
- const ext2 = getExt(originalName);
168
- const id = `${createId()}${ext2}`;
189
+ const ext = getExt(originalName);
190
+ const id = `${createId()}${ext}`;
169
191
  const file2 = bucket2.file(id);
170
192
  await file2.write(data, { type: contentType });
171
193
  return {
@@ -188,10 +210,10 @@ function validateFile(originalName, data, contentType, limits) {
188
210
  );
189
211
  }
190
212
  if (fileType2 && fileType2.length > 0) {
191
- const ext2 = getExt(originalName);
213
+ const ext = getExt(originalName);
192
214
  const mime = contentType.toLowerCase();
193
215
  const allowed = fileType2.some(
194
- (t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
216
+ (t) => t.toLowerCase() === mime || t.toLowerCase() === ext
195
217
  );
196
218
  if (!allowed) {
197
219
  throw new Error(
@@ -331,11 +353,11 @@ function isProbablyText(buffer) {
331
353
  return true;
332
354
  }
333
355
  var extByMime = {};
334
- for (const ext2 in mimes_default) extByMime[mimes_default[ext2]] = ext2;
356
+ for (const ext in mimes_default) extByMime[mimes_default[ext]] = ext;
335
357
  function extFromType(type2) {
336
358
  const base = (type2 || "").split(";")[0].trim().toLowerCase();
337
- const ext2 = extByMime[base];
338
- if (ext2) return `.${ext2}`;
359
+ const ext = extByMime[base];
360
+ if (ext) return `.${ext}`;
339
361
  const sub = base.split("/")[1];
340
362
  return sub && /^[a-z0-9]+$/.test(sub) ? `.${sub}` : ".bin";
341
363
  }
@@ -748,20 +770,20 @@ var encodeExt = (name) => encodeURIComponent(name).replace(
748
770
  );
749
771
  function disposition(name) {
750
772
  if (!name) return "attachment";
751
- const clean = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
752
- if (!clean) return "attachment";
753
- const ascii = clean.replace(/[^\x20-\x7e]/g, "?");
773
+ const clean2 = name.replace(/[\r\n]/g, "").split(/[\\/]/).pop() || "";
774
+ if (!clean2) return "attachment";
775
+ const ascii = clean2.replace(/[^\x20-\x7e]/g, "?");
754
776
  const value = `attachment; filename="${ascii.replace(/["\\]/g, "\\$&")}"`;
755
- if (clean === ascii) return value;
756
- return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
777
+ if (clean2 === ascii) return value;
778
+ return `${value}; filename*=UTF-8''${encodeExt(clean2)}`;
757
779
  }
758
780
 
759
781
  // src/helpers/fileType.ts
760
782
  function fileType(file2) {
761
783
  if (file2.type) return file2.type;
762
784
  const name = file2.path || file2.name || "";
763
- const ext2 = name.split(".").pop()?.toLowerCase();
764
- return ext2 ? mimes_default[ext2] : void 0;
785
+ const ext = name.split(".").pop()?.toLowerCase();
786
+ return ext ? mimes_default[ext] : void 0;
765
787
  }
766
788
 
767
789
  // src/helpers/isHtml.ts
@@ -795,8 +817,8 @@ var Reply = class {
795
817
  return this;
796
818
  }
797
819
  download(name) {
798
- const ext2 = name?.split(".").pop();
799
- if (ext2 && !this.res.headers.get("content-type")) this.type(ext2);
820
+ const ext = name?.split(".").pop();
821
+ if (ext && !this.res.headers.get("content-type")) this.type(ext);
800
822
  return this.headers("content-disposition", disposition(name));
801
823
  }
802
824
  headers(key, value) {
@@ -854,10 +876,10 @@ var Reply = class {
854
876
  if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
855
877
  try {
856
878
  const fs = await import("fs");
857
- const ext2 = path.split(".").pop();
879
+ const ext = path.split(".").pop();
858
880
  await fs.promises.access(path);
859
881
  const stream = fs.createReadStream(path);
860
- return this.type(ext2).send(stream);
882
+ return this.type(ext).send(stream);
861
883
  } catch (error) {
862
884
  if (error.code === "ENOENT" || error.code === "EISDIR") {
863
885
  return this.status(404).send();
@@ -1010,8 +1032,8 @@ var validateToken = (authorization) => {
1010
1032
  return id;
1011
1033
  };
1012
1034
  function findSessionId(ctx) {
1013
- const strategy = ctx.options.auth?.strategy;
1014
- if (strategy?.includes("token") && ctx.headers.authorization) {
1035
+ if (ctx.options.auth?.strategy.includes("token")) {
1036
+ if (!ctx.headers.authorization) return;
1015
1037
  return validateToken(ctx.headers.authorization);
1016
1038
  }
1017
1039
  return ctx.cookies.session || void 0;
@@ -1019,28 +1041,33 @@ function findSessionId(ctx) {
1019
1041
 
1020
1042
  // src/middle/session.ts
1021
1043
  var loaded = /* @__PURE__ */ new WeakMap();
1022
- function jwtSession() {
1044
+ function noSession(error) {
1023
1045
  const target = {};
1024
1046
  return new Proxy(target, {
1025
1047
  get(target2, key) {
1026
1048
  if (typeof key === "symbol" || key === "then") return target2[key];
1027
- throw ServerError_default.SESSION_JWT({ key: String(key) });
1049
+ throw error(String(key));
1028
1050
  },
1029
1051
  set(target2, key, value) {
1030
1052
  if (typeof key === "symbol") {
1031
1053
  target2[key] = value;
1032
1054
  return true;
1033
1055
  }
1034
- throw ServerError_default.SESSION_JWT({ key: String(key) });
1056
+ throw error(String(key));
1035
1057
  }
1036
1058
  });
1037
1059
  }
1038
1060
  async function session(ctx) {
1039
- if (ctx.options.auth?.strategy.includes("jwt")) {
1040
- ctx.session = jwtSession();
1061
+ const strategy = ctx.options.auth?.strategy;
1062
+ if (strategy?.includes("jwt")) {
1063
+ ctx.session = noSession((key) => ServerError_default.SESSION_JWT({ key }));
1041
1064
  return;
1042
1065
  }
1043
1066
  const id = findSessionId(ctx);
1067
+ if (!id && strategy?.includes("token")) {
1068
+ ctx.session = noSession((key) => ServerError_default.SESSION_GUEST({ key }));
1069
+ return;
1070
+ }
1044
1071
  ctx.session = id && await ctx.options.sessions.get(id) || {};
1045
1072
  loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
1046
1073
  }
@@ -1050,6 +1077,10 @@ async function finishLogin(ctx, input, opts = {}) {
1050
1077
  const settings = ctx.options.auth;
1051
1078
  const { strategy, onLogin, onUser, onToken } = settings;
1052
1079
  const key = String(input.key);
1080
+ if (!strategy.includes("jwt") && !loaded.has(ctx)) {
1081
+ ctx.session = {};
1082
+ loaded.set(ctx, { id: void 0, data: "{}" });
1083
+ }
1053
1084
  const auth2 = {
1054
1085
  user: key,
1055
1086
  provider: input.provider,
@@ -1613,17 +1644,6 @@ function parseAuthOptions(auth2) {
1613
1644
  };
1614
1645
  }
1615
1646
 
1616
- // src/helpers/bucket.ts
1617
- import FileSystem from "bucket/fs";
1618
- function bucket(root) {
1619
- if (!root) return null;
1620
- if (typeof root === "string") return FileSystem(root);
1621
- if (typeof root.file === "function") return root;
1622
- throw new Error(
1623
- "Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
1624
- );
1625
- }
1626
-
1627
1647
  // src/helpers/color.ts
1628
1648
  var map = {
1629
1649
  reset: 0,
@@ -1863,19 +1883,7 @@ function config(options = {}) {
1863
1883
  }
1864
1884
  const publicDir = options.public || env2.PUBLIC;
1865
1885
  settings.public = publicDir ? bucket(publicDir) : null;
1866
- const up = options.uploads;
1867
- if (!up) {
1868
- settings.uploads = null;
1869
- } else if (typeof up === "object" && "bucket" in up) {
1870
- const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
1871
- if (maxSize != null) parseBytes(maxSize);
1872
- if (minSize != null) parseBytes(minSize);
1873
- settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
1874
- } else {
1875
- settings.uploads = { bucket: bucket(up) };
1876
- }
1877
- const favicon2 = options.favicon || env2.FAVICON;
1878
- if (favicon2) settings.favicon = favicon2;
1886
+ settings.uploads = resolveUploads(options.uploads);
1879
1887
  const production = env2.NODE_ENV === "production";
1880
1888
  const defaulted = options.sessions == null;
1881
1889
  settings.sessionsDefault = defaulted;
@@ -1886,7 +1894,7 @@ function config(options = {}) {
1886
1894
  if (!settings.auth.users) {
1887
1895
  if (production) {
1888
1896
  throw new Error(
1889
- "Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('users:') }."
1897
+ "Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('user:') }."
1890
1898
  );
1891
1899
  }
1892
1900
  settings.auth.users = toStore(/* @__PURE__ */ new Map());
@@ -1903,9 +1911,10 @@ function config(options = {}) {
1903
1911
  );
1904
1912
  }
1905
1913
  if (options.openapi) {
1906
- if (options.openapi === true) {
1907
- settings.openapi = {};
1908
- }
1914
+ const o = options.openapi;
1915
+ if (o === true) settings.openapi = { path: "/openapi.json" };
1916
+ else if (typeof o === "string") settings.openapi = { path: o };
1917
+ else settings.openapi = { path: "/openapi.json", ...o };
1909
1918
  }
1910
1919
  settings.onError = options.onError || ((error) => {
1911
1920
  return new Response(error.message || "Server Error", {
@@ -1924,9 +1933,8 @@ function config(options = {}) {
1924
1933
  const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
1925
1934
  log.message("cors", origin);
1926
1935
  }
1927
- if (settings.favicon) log.message("favicon", loc(settings.favicon));
1928
1936
  if (settings.cache !== void 0) log.message("cache", loc(options.cache));
1929
- if (settings.openapi) log.message("openapi", settings.openapi.path || "/docs");
1937
+ if (settings.openapi) log.message("openapi", settings.openapi.path);
1930
1938
  return settings;
1931
1939
  }
1932
1940
 
@@ -2106,13 +2114,11 @@ async function parseResponse(out, ctx) {
2106
2114
  out.headers.set("Server-Timing", ctx.time.headers());
2107
2115
  }
2108
2116
  const prev = loaded.get(ctx);
2109
- const jwt = ctx.options.auth?.strategy.includes("jwt");
2110
- const data = jwt ? "{}" : JSON.stringify(ctx.session ?? {});
2111
- if (!jwt && data !== (prev?.data ?? "{}")) {
2117
+ if (prev && JSON.stringify(ctx.session ?? {}) !== prev.data) {
2112
2118
  if (ctx.options.sessionsDefault && ctx.platform.production) {
2113
2119
  warnDefault();
2114
2120
  }
2115
- let id = prev?.id;
2121
+ let id = prev.id;
2116
2122
  if (!id) {
2117
2123
  id = createId();
2118
2124
  out.headers.append(
@@ -2128,11 +2134,6 @@ async function parseResponse(out, ctx) {
2128
2134
  }
2129
2135
  ctx.options.sessions.set(id, ctx.session);
2130
2136
  }
2131
- if (ctx?.res?.headers) {
2132
- for (const key in ctx.res.headers) {
2133
- out.headers[key] = ctx.res.headers[key];
2134
- }
2135
- }
2136
2137
  return out;
2137
2138
  }
2138
2139
 
@@ -2465,14 +2466,14 @@ async function getJwtUser(ctx) {
2465
2466
  return exposed;
2466
2467
  }
2467
2468
  async function getAuthSession(ctx) {
2468
- let session2 = ctx.session;
2469
- if (!session2) {
2470
- const id = findSessionId(ctx);
2471
- if (!id) return;
2472
- session2 = await ctx.options.sessions.get(id) ?? void 0;
2469
+ if (loaded.has(ctx)) {
2470
+ const session3 = ctx.session;
2471
+ return session3?.user ? session3 : void 0;
2473
2472
  }
2474
- if (!session2?.user) return;
2475
- return session2;
2473
+ const id = findSessionId(ctx);
2474
+ if (!id) return;
2475
+ const session2 = await ctx.options.sessions.get(id);
2476
+ return session2?.user ? session2 : void 0;
2476
2477
  }
2477
2478
  async function getUser(ctx) {
2478
2479
  if (!ctx.options.auth) return;
@@ -2524,31 +2525,32 @@ function auth(app) {
2524
2525
  app.use(async function middle(ctx) {
2525
2526
  ctx.user = await getUser(ctx);
2526
2527
  });
2527
- app.post("/auth/logout", logout);
2528
+ const spec = { schema: { tags: "auth" } };
2529
+ app.post("/auth/logout", spec, logout);
2528
2530
  const enabled = app.settings.auth.providers;
2529
2531
  for (const name of oauth2) {
2530
2532
  if (!enabled.includes(name)) continue;
2531
2533
  const key = name.toUpperCase();
2532
2534
  if (!env[`${key}_ID`]) throw new Error(`${key}_ID not defined`);
2533
2535
  if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
2534
- app.get(`/auth/login/${name}`, providers_default[name].login);
2535
- app.get(`/auth/callback/${name}`, providers_default[name].callback);
2536
- app.post(`/auth/verify/${name}`, providers_default[name].verify);
2536
+ app.get(`/auth/login/${name}`, spec, providers_default[name].login);
2537
+ app.get(`/auth/callback/${name}`, spec, providers_default[name].callback);
2538
+ app.post(`/auth/verify/${name}`, spec, providers_default[name].verify);
2537
2539
  }
2538
2540
  if (enabled.includes("apple")) {
2539
2541
  const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
2540
2542
  for (const key of keys) {
2541
2543
  if (!env[key]) throw new Error(`${key} not defined`);
2542
2544
  }
2543
- app.get("/auth/login/apple", providers_default.apple.login);
2544
- app.post("/auth/callback/apple", providers_default.apple.callback);
2545
- app.post("/auth/verify/apple", providers_default.apple.verify);
2545
+ app.get("/auth/login/apple", spec, providers_default.apple.login);
2546
+ app.post("/auth/callback/apple", spec, providers_default.apple.callback);
2547
+ app.post("/auth/verify/apple", spec, providers_default.apple.verify);
2546
2548
  }
2547
2549
  if (enabled.includes("email")) {
2548
- app.post("/auth/register/email", providers_default.email.register);
2549
- app.post("/auth/login/email", providers_default.email.login);
2550
- app.put("/auth/password/email", providers_default.email.password);
2551
- app.put("/auth/reset/email", providers_default.email.reset);
2550
+ app.post("/auth/register/email", spec, providers_default.email.register);
2551
+ app.post("/auth/login/email", spec, providers_default.email.login);
2552
+ app.put("/auth/password/email", spec, providers_default.email.password);
2553
+ app.put("/auth/reset/email", spec, providers_default.email.reset);
2552
2554
  }
2553
2555
  }
2554
2556
 
@@ -2587,8 +2589,8 @@ async function assets(ctx) {
2587
2589
  const info = file2.info?.bind(file2);
2588
2590
  const meta = info ? await info() : null;
2589
2591
  if (info ? !meta : !await file2.exists()) return;
2590
- const ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
2591
- const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
2592
+ const ext = ctx.url.pathname.split(".").pop()?.toLowerCase();
2593
+ const ctype = ext && mimes_default[ext] || meta?.type || ext;
2592
2594
  const headers2 = { "cache-control": CACHE_CONTROL };
2593
2595
  let tag;
2594
2596
  if (meta) {
@@ -2623,46 +2625,8 @@ async function assets(ctx) {
2623
2625
  }
2624
2626
  }
2625
2627
 
2626
- // src/middle/favicon.ts
2627
- var CACHE_CONTROL2 = "public, max-age=86400";
2628
- var ext = (name) => name.split(".").pop() || "ico";
2629
- async function loadFavicon(fav) {
2630
- try {
2631
- const type2 = ext(typeof fav === "string" ? fav : fav?.name);
2632
- const bytes = typeof fav === "string" ? await (await import("fs/promises")).readFile(fav) : Buffer.from(await fav.bytes());
2633
- return { bytes, type: type2, etag: etag(bytes) };
2634
- } catch {
2635
- return null;
2636
- }
2637
- }
2638
- async function favicon(ctx) {
2639
- const fav = ctx.options.favicon;
2640
- if (!fav) return;
2641
- if (ctx.app.faviconCache === void 0) {
2642
- ctx.app.faviconCache = await loadFavicon(fav);
2643
- }
2644
- const entry = ctx.app.faviconCache;
2645
- if (!entry) return 204;
2646
- const headers2 = { "cache-control": CACHE_CONTROL2, etag: entry.etag };
2647
- if (ctx.headers["if-none-match"] === entry.etag) {
2648
- return status(304).headers(headers2).send();
2649
- }
2650
- return type(entry.type).headers(headers2).send(entry.bytes);
2651
- }
2652
-
2653
2628
  // src/middle/openapi.ts
2654
2629
  import * as fsp from "fs/promises";
2655
- var entities = {
2656
- "&": "&amp;",
2657
- "<": "&lt;",
2658
- ">": "&gt;",
2659
- '"': "&quot;"
2660
- };
2661
- var encode = (str = "") => {
2662
- if (typeof str === "number") str = String(str);
2663
- if (typeof str !== "string") return "";
2664
- return str.replace(/[&<>"]/g, (tag) => entities[tag]);
2665
- };
2666
2630
  var getConfig = (options = {}) => {
2667
2631
  const config2 = { ...options };
2668
2632
  if (config2.tags) {
@@ -2676,6 +2640,25 @@ var getConfig = (options = {}) => {
2676
2640
  }
2677
2641
  return config2;
2678
2642
  };
2643
+ var clean = ({ $schema, ...schema }) => schema;
2644
+ async function toJsonSchema(schema) {
2645
+ try {
2646
+ if (typeof schema?.toJsonSchema === "function") {
2647
+ return clean(schema.toJsonSchema());
2648
+ }
2649
+ const vendor = schema?.["~standard"]?.vendor;
2650
+ if (vendor === "zod") {
2651
+ const mod = await import("zod");
2652
+ return clean((mod.toJSONSchema ?? mod.z.toJSONSchema)(schema));
2653
+ }
2654
+ if (vendor === "valibot") {
2655
+ const mod = await import("@valibot/to-json-schema");
2656
+ return clean(mod.toJsonSchema(schema));
2657
+ }
2658
+ } catch {
2659
+ }
2660
+ return zodToSchema(schema);
2661
+ }
2679
2662
  function zodToSchema(schema) {
2680
2663
  const type2 = schema?.def?.type || "string";
2681
2664
  if (type2 === "object") {
@@ -2698,48 +2681,31 @@ function zodToSchema(schema) {
2698
2681
  return { type: type2 };
2699
2682
  }
2700
2683
  var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
2701
- var getTag = (name, fn) => {
2702
- const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
2703
- if (!found) return "";
2704
- return encode(found.replace(name, "").trim());
2705
- };
2706
- var getDescription = (fn) => getTag("@description", fn) || "";
2707
- var getReturn = (fn) => getTag("@returns", fn) || "OK";
2708
- var generateOpenApiPaths = (handlers) => {
2684
+ var generateOpenApiPaths = async (handlers, specPath) => {
2709
2685
  const paths = {};
2710
2686
  for (const [method, routes] of Object.entries(handlers)) {
2711
2687
  for (const route of routes) {
2712
2688
  const path = route.path;
2713
- const fn = route.fns.find((p) => typeof p === "function");
2714
2689
  const meta = route.options ?? {};
2715
2690
  const config2 = getConfig(route.options?.schema);
2716
- if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
2691
+ if (typeof path !== "string" || path === "*" || path === specPath) {
2717
2692
  continue;
2718
2693
  }
2694
+ if (route.options?.schema === false) continue;
2719
2695
  const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
2720
2696
  if (!paths[normalizedPath]) {
2721
2697
  paths[normalizedPath] = {};
2722
2698
  }
2723
- const getTitle = (fn2) => {
2724
- if (!fn2.name) return null;
2725
- const wrongNames = ["default"];
2726
- if (wrongNames.includes(fn2.name)) return null;
2727
- if (fn2.name.length <= 3) return null;
2728
- if (fn2.name.includes("_")) return fn2.name.replace(/_/g, " ");
2729
- const name = fn2.name.split(/(?=[A-Z])/).join(" ").toLowerCase();
2730
- return name[0].toUpperCase() + name.slice(1);
2731
- };
2732
2699
  let requestBody;
2733
2700
  if (meta?.body) {
2734
- const schema = zodToSchema(meta.body);
2701
+ const schema = await toJsonSchema(meta.body);
2735
2702
  requestBody = { content: { "application/json": { schema } } };
2736
2703
  }
2737
2704
  let responses;
2738
2705
  if (meta?.response) {
2739
- const schema = zodToSchema(meta.response);
2740
- const description = getReturn(fn);
2706
+ const schema = await toJsonSchema(meta.response);
2741
2707
  responses = {
2742
- 200: { description, content: { "application/json": { schema } } }
2708
+ 200: { description: "OK", content: { "application/json": { schema } } }
2743
2709
  };
2744
2710
  }
2745
2711
  const parameters = [];
@@ -2754,18 +2720,20 @@ var generateOpenApiPaths = (handlers) => {
2754
2720
  });
2755
2721
  });
2756
2722
  if (meta?.query) {
2757
- Object.entries(meta.query).map(([key, value]) => ({
2758
- name: key,
2759
- in: "query",
2760
- required: false,
2761
- schema: { type: typeof value },
2762
- example: value
2763
- }));
2723
+ const schema = await toJsonSchema(meta.query);
2724
+ for (const [name, prop] of Object.entries(schema.properties ?? {})) {
2725
+ parameters.push({
2726
+ name,
2727
+ in: "query",
2728
+ required: schema.required?.includes(name) ?? false,
2729
+ schema: prop
2730
+ });
2731
+ }
2764
2732
  }
2765
2733
  paths[normalizedPath][method] = {
2766
2734
  tags: config2.tags,
2767
- summary: config2.title || getTag("@title", fn) || `${method.toUpperCase()} ${normalizedPath}`,
2768
- description: config2.description || getTitle(fn) || getDescription(fn),
2735
+ summary: config2.title,
2736
+ description: config2.description,
2769
2737
  requestBody,
2770
2738
  parameters,
2771
2739
  responses
@@ -2776,34 +2744,21 @@ var generateOpenApiPaths = (handlers) => {
2776
2744
  };
2777
2745
  var openapi_default = async (ctx) => {
2778
2746
  const pkg = await pkgProm;
2747
+ const { title, description, version } = ctx.options.openapi ?? {};
2779
2748
  const domain = pkg.homepage || ctx.url.origin;
2780
- const openApi = {
2749
+ return {
2781
2750
  openapi: "3.0.0",
2782
2751
  info: {
2783
- title: pkg.name || "API Documentation",
2784
- version: pkg.version || "1.0.0",
2785
- description: pkg.description || ""
2752
+ title: title || pkg.name || "API Documentation",
2753
+ version: version || pkg.version || "1.0.0",
2754
+ description: description ?? (pkg.description || "")
2786
2755
  },
2787
2756
  servers: domain ? [{ url: domain }] : [],
2788
- paths: generateOpenApiPaths(ctx.app.handlers)
2757
+ paths: await generateOpenApiPaths(
2758
+ ctx.app.handlers,
2759
+ ctx.options.openapi?.path ?? ""
2760
+ )
2789
2761
  };
2790
- const configuration = ctx.options.openapi?.scalar || {};
2791
- return `
2792
- <!doctype html>
2793
- <html>
2794
- <head>
2795
- <title>API Reference</title>
2796
- <meta charset="utf-8" />
2797
- <meta
2798
- name="viewport"
2799
- content="width=device-width, initial-scale=1" />
2800
- <style>.open-api-client-button {display: none!important;}</style>
2801
- </head>
2802
- <body>
2803
- <script id="api-reference" type="application/json" data-configuration="${encode(JSON.stringify(configuration))}">${JSON.stringify(openApi, null, 2)}</script>
2804
- <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
2805
- </body>
2806
- </html> `;
2807
2762
  };
2808
2763
 
2809
2764
  // src/middle/preflight.ts
@@ -3038,7 +2993,7 @@ function isValidMethod(method) {
3038
2993
 
3039
2994
  // src/context/node.ts
3040
2995
  var chunkArray = (arr) => arr.length > 2 ? [[arr[0], arr[1]], ...chunkArray(arr.slice(2))] : [arr];
3041
- async function createNode(req, app) {
2996
+ async function createNode(req, app, signal = new AbortController().signal) {
3042
2997
  const init = performance.now();
3043
2998
  const method = req.method?.toLowerCase() || "get";
3044
2999
  if (!isValidMethod(method)) {
@@ -3072,6 +3027,7 @@ async function createNode(req, app) {
3072
3027
  body: void 0,
3073
3028
  headers: headers2,
3074
3029
  cookies: cookies2,
3030
+ signal,
3075
3031
  session: {},
3076
3032
  init,
3077
3033
  app,
@@ -3112,6 +3068,7 @@ async function createWinter(req, app, server2) {
3112
3068
  body: void 0,
3113
3069
  headers: headers2,
3114
3070
  cookies: cookies2,
3071
+ signal: req.signal,
3115
3072
  session: {},
3116
3073
  init,
3117
3074
  app,
@@ -3149,7 +3106,11 @@ var Node = async (app) => {
3149
3106
  const http = await import("http");
3150
3107
  const server2 = http.createServer(
3151
3108
  async (request, response) => {
3152
- const ctx = await createNode(request, app);
3109
+ const controller = new AbortController();
3110
+ response.on("close", () => {
3111
+ if (!response.writableFinished) controller.abort();
3112
+ });
3113
+ const ctx = await createNode(request, app, controller.signal);
3153
3114
  if ("error" in ctx) throw ctx.error;
3154
3115
  const out = await handleRequest(app, ctx);
3155
3116
  response.writeHead(out.status || 200, parseHeaders_default(out.headers));
@@ -3231,6 +3192,9 @@ var Router = class _Router {
3231
3192
  options = rest.shift();
3232
3193
  }
3233
3194
  checkParserConflict(options, this.settings?.parser);
3195
+ if (options.uploads !== void 0) {
3196
+ options.uploads = resolveUploads(options.uploads);
3197
+ }
3234
3198
  const base = method === "socket" ? [] : this.middleware;
3235
3199
  const fns = [...base, ...rest].filter((fn) => fn != null);
3236
3200
  this.handlers[method].push({ path, options, fns });
@@ -3337,9 +3301,6 @@ var Server = class extends Router {
3337
3301
  platform;
3338
3302
  sockets;
3339
3303
  websocket;
3340
- // Lazily-loaded favicon bytes, cached per server until restart (see favicon
3341
- // middleware). `undefined` = not loaded yet; `null` = configured but missing.
3342
- faviconCache;
3343
3304
  port;
3344
3305
  constructor(options = {}) {
3345
3306
  super();
@@ -3359,13 +3320,12 @@ var Server = class extends Router {
3359
3320
  app.use(timer);
3360
3321
  if (this.settings.cors) app.use(preflight);
3361
3322
  app.use(assets);
3362
- if (this.settings.favicon) app.get("/favicon.ico", favicon);
3363
3323
  app.use(session);
3364
3324
  if (this.settings.auth) {
3365
3325
  auth(app);
3366
3326
  }
3367
3327
  if (this.settings.openapi) {
3368
- app.get(this.settings.openapi.path || "/docs", openapi_default);
3328
+ app.get(this.settings.openapi.path, openapi_default);
3369
3329
  }
3370
3330
  }
3371
3331
  self() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.42.0",
3
+ "version": "0.44.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",
@@ -13,7 +13,8 @@
13
13
  "server.js"
14
14
  ],
15
15
  "scripts": {
16
- "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
16
+ "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 --external zod --external @valibot/to-json-schema",
17
+ "prepare": "mkdir -p node_modules/@server && ln -sfn ../.. node_modules/@server/next",
17
18
  "start": "bun test --watch",
18
19
  "lint": "npx tsc --noEmit && npx @biomejs/biome lint ./src --skip=lint/suspicious/noExplicitAny --skip=lint/style/noParameterAssign --skip=lint/suspicious/noConfusingVoidType --skip=lint/complexity/noBannedTypes",
19
20
  "test": "npm run test:bun && tsc --noEmit",
@@ -54,7 +55,7 @@
54
55
  "Reply": "docs/4. Reply.md",
55
56
  "Authentication": "docs/5. Authentication.md",
56
57
  "Testing": "docs/6. Testing.md",
57
- "Platforms": "docs/7. Platforms.md",
58
+ "Concepts": "docs/7. Concepts.md",
58
59
  "FAQ": "docs/8. FAQ.md"
59
60
  },
60
61
  "tutorials": "docs/tutorials"
@@ -66,6 +67,7 @@
66
67
  "devDependencies": {
67
68
  "@types/bun": "^1.3.0",
68
69
  "@types/node": "^24.10.0",
70
+ "@valibot/to-json-schema": "^1.7.1",
69
71
  "arktype": "^2.2.3",
70
72
  "bun": "^1.3.13",
71
73
  "check-dts": "^0.8.2",