@server/next 0.28.15 → 0.29.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.
package/index.d.ts CHANGED
@@ -54,11 +54,13 @@ type CorsSettings = {
54
54
  origin: string | boolean;
55
55
  methods: string;
56
56
  headers: string;
57
+ credentials?: boolean;
57
58
  };
58
59
  type CorsOptions = boolean | string | string[] | {
59
60
  origin?: string | string[];
60
61
  methods?: string | Method[];
61
62
  headers?: string | string[];
63
+ credentials?: boolean;
62
64
  };
63
65
  type BasicValue = string | number | boolean | null;
64
66
  type SerializableValue = BasicValue | {
@@ -106,6 +108,19 @@ type AuthSettings = {
106
108
  cleanUser: <T = AuthUser>(user: T) => T | Promise<T>;
107
109
  redirect: string;
108
110
  };
111
+ type LogLevel = "info";
112
+ type Logger = {
113
+ level?: LogLevel;
114
+ message: (scope: string, message: string) => void;
115
+ start: (url: string) => void;
116
+ request: (ctx: Context, res: Response) => void;
117
+ };
118
+ type SecurityOptions = {
119
+ trustProxy?: boolean;
120
+ };
121
+ type SecuritySettings = {
122
+ trustProxy: boolean;
123
+ };
109
124
  type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
110
125
  type Options = {
111
126
  port?: number;
@@ -122,6 +137,9 @@ type Options = {
122
137
  auth?: AuthOption;
123
138
  openapi?: any;
124
139
  onError?: OnError;
140
+ log?: LogLevel | boolean;
141
+ favicon?: string | Bucket;
142
+ security?: SecurityOptions;
125
143
  };
126
144
  type Settings = {
127
145
  port: number;
@@ -138,6 +156,9 @@ type Settings = {
138
156
  auth?: AuthSettings;
139
157
  openapi?: any;
140
158
  onError?: OnError;
159
+ log: Logger;
160
+ favicon?: string | Bucket;
161
+ security: SecuritySettings;
141
162
  };
142
163
  type Time = {
143
164
  (name: string): void;
@@ -168,8 +189,9 @@ type Events = Record<string, EventCallback[]> & {
168
189
  on?: (key: string, cb: (value?: Context & SerializableValue) => void) => void;
169
190
  trigger?: (key: string, value?: Partial<Context & SerializableValue>) => void;
170
191
  };
171
- type Context<Params extends Record<string, string> = Record<string, string>, O extends ServerConfig = object> = {
192
+ type Context<Params extends Record<string, string | undefined> = Record<string, string>, O extends ServerConfig = object> = {
172
193
  method: Method;
194
+ ip: string;
173
195
  headers: Record<string, string | string[]>;
174
196
  cookies: Record<string, string>;
175
197
  body?: SerializableValue;
@@ -201,7 +223,7 @@ type InlineReply = Response | {
201
223
  headers?: Headers;
202
224
  } | SerializableValue | JSX.Element;
203
225
  type Body = InlineReply;
204
- type Middleware<O extends ServerConfig = object, Params extends Record<string, string> = Record<string, string>> = (ctx: Context<Params, O>) => InlineReply | Promise<InlineReply> | void | Promise<void>;
226
+ 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>;
205
227
 
206
228
  type Variables = Record<string, string | string[]>;
207
229
  type ExtendError = string | {
@@ -426,4 +448,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
426
448
  }
427
449
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
428
450
 
429
- 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 Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, 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 };
451
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
package/index.js CHANGED
@@ -79,6 +79,24 @@ if (typeof process !== "undefined") {
79
79
  Object.assign(globalThis.env, process.env);
80
80
  }
81
81
 
82
+ // src/helpers/clientIp.ts
83
+ var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
84
+ var normalize = (ip) => ip.replace(/^::ffff:/, "");
85
+ function clientIp(headers2, opts = {}) {
86
+ const { remoteAddress = "", trustProxy = false } = opts;
87
+ const cf = first(headers2["cf-connecting-ip"]);
88
+ if (cf) return normalize(cf);
89
+ const nf = first(headers2["x-nf-client-connection-ip"]);
90
+ if (nf) return normalize(nf);
91
+ if (trustProxy) {
92
+ const xff = first(headers2["x-forwarded-for"]);
93
+ if (xff) return normalize(xff.split(",")[0].trim());
94
+ const real = first(headers2["x-real-ip"]);
95
+ if (real) return normalize(real);
96
+ }
97
+ return normalize(remoteAddress);
98
+ }
99
+
82
100
  // src/auth/updateUser.ts
83
101
  async function updateUser(user, auth2, store) {
84
102
  if (auth2.provider === "email") {
@@ -254,10 +272,16 @@ var Reply = class {
254
272
  const isHtml = body.trim().startsWith("<");
255
273
  headers2.set("content-type", isHtml ? "text/html" : "text/plain");
256
274
  }
275
+ if (!headers2.has("content-length")) {
276
+ headers2.set("content-length", String(Buffer.byteLength(body)));
277
+ }
257
278
  return new Response(body, { status: status2, headers: headers2 });
258
279
  }
259
280
  const name = body?.constructor?.name;
260
281
  if (name === "Buffer") {
282
+ if (!headers2.has("content-length")) {
283
+ headers2.set("content-length", String(body.length));
284
+ }
261
285
  return new Response(body, { status: status2, headers: headers2 });
262
286
  }
263
287
  if (typeof body?.getReader === "function") {
@@ -272,7 +296,11 @@ var Reply = class {
272
296
  if (!headers2.get("content-type")) {
273
297
  headers2.set("content-type", "application/json");
274
298
  }
275
- return new Response(JSON.stringify(body), { status: status2, headers: headers2 });
299
+ const payload = JSON.stringify(body);
300
+ if (!headers2.has("content-length")) {
301
+ headers2.set("content-length", String(Buffer.byteLength(payload)));
302
+ }
303
+ return new Response(payload, { status: status2, headers: headers2 });
276
304
  }
277
305
  };
278
306
  var r = () => new Reply();
@@ -398,7 +426,7 @@ function parseAuthOptions(auth2, all) {
398
426
  throw new Error("Auth options needs a strategy");
399
427
  }
400
428
  const strategy = auth2.strategy;
401
- if (!auth2.provider || !auth2.provider.length) {
429
+ if (!auth2.provider?.length) {
402
430
  throw new Error("Auth options needs a provider");
403
431
  }
404
432
  const provider = getProviders(auth2.provider);
@@ -439,7 +467,7 @@ function thinLocalBucket(root) {
439
467
  read: async (name) => {
440
468
  const fullPath = absolute(name);
441
469
  const stats = await fsp.stat(fullPath).catch(() => null);
442
- if (!stats || !stats.isFile()) return null;
470
+ if (!stats?.isFile()) return null;
443
471
  const nodeStream = fs.createReadStream(fullPath);
444
472
  return new ReadableStream({
445
473
  start(controller) {
@@ -544,6 +572,115 @@ function createId(source, size = 16) {
544
572
  return randomId(size);
545
573
  }
546
574
 
575
+ // src/helpers/color.ts
576
+ var map = {
577
+ reset: 0,
578
+ bright: 1,
579
+ dim: 2,
580
+ under: 4,
581
+ blink: 5,
582
+ reverse: 7,
583
+ black: 30,
584
+ red: 31,
585
+ green: 32,
586
+ yellow: 33,
587
+ blue: 34,
588
+ magenta: 35,
589
+ cyan: 36,
590
+ white: 37,
591
+ bgblack: 40,
592
+ bgred: 41,
593
+ bggreen: 42,
594
+ bgyellow: 43,
595
+ bgblue: 44,
596
+ bgmagenta: 45,
597
+ bgcyan: 46,
598
+ bgwhite: 47
599
+ };
600
+ var replace = (k) => {
601
+ if (process.env.NO_COLOR) return "";
602
+ if (!(k in map)) throw new Error(`"{${k}}" is not a valid color`);
603
+ return `\x1B[${map[k]}m`;
604
+ };
605
+ function color(str, ...vals) {
606
+ if (typeof str === "string") {
607
+ return str.replace(/\{(\w+)\}/g, (_m, k) => replace(k)).replace(/\{\/\w*\}/g, () => replace("reset"));
608
+ }
609
+ return color(str[0] + vals.map((v, i) => v + str[i + 1]).join(""));
610
+ }
611
+
612
+ // src/helpers/logger.ts
613
+ var STATUS_TEXT = {
614
+ 200: "OK",
615
+ 201: "Created",
616
+ 202: "Accepted",
617
+ 204: "No Content",
618
+ 301: "Moved Permanently",
619
+ 302: "Found",
620
+ 303: "See Other",
621
+ 304: "Not Modified",
622
+ 307: "Temporary Redirect",
623
+ 308: "Permanent Redirect",
624
+ 400: "Bad Request",
625
+ 401: "Unauthorized",
626
+ 403: "Forbidden",
627
+ 404: "Not Found",
628
+ 405: "Method Not Allowed",
629
+ 409: "Conflict",
630
+ 413: "Payload Too Large",
631
+ 422: "Unprocessable Entity",
632
+ 429: "Too Many Requests",
633
+ 500: "Internal Server Error",
634
+ 502: "Bad Gateway",
635
+ 503: "Service Unavailable"
636
+ };
637
+ var UNITS = ["b", "kb", "mb", "gb", "tb"];
638
+ function formatBytes(bytes) {
639
+ if (!bytes || bytes < 0) return "0b";
640
+ const i = Math.min(
641
+ Math.floor(Math.log(bytes) / Math.log(1024)),
642
+ UNITS.length - 1
643
+ );
644
+ const value = bytes / 1024 ** i;
645
+ const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
646
+ return `${rounded}${UNITS[i]}`;
647
+ }
648
+ var SCOPE_COLORS = {
649
+ start: "green",
650
+ api: "cyan"
651
+ };
652
+ var MODULE_COLOR = "magenta";
653
+ var paint = (name, text) => `${color(`{${name}}`)}${text}${color("{/}")}`;
654
+ function createLogger(level) {
655
+ const enabled = !!level;
656
+ const message = (scope, msg) => {
657
+ if (!enabled) return;
658
+ const c = SCOPE_COLORS[scope] || MODULE_COLOR;
659
+ console.log(paint(c, `[server:${scope}] ${msg}`));
660
+ };
661
+ const request = (ctx, res) => {
662
+ if (!enabled) return;
663
+ const method = ctx.method.toUpperCase();
664
+ const path2 = ctx.url.pathname;
665
+ const reqLen = Number(ctx.headers["content-length"]) || 0;
666
+ const resLen = Number(res.headers.get("content-length")) || 0;
667
+ const status2 = res.status;
668
+ const text = STATUS_TEXT[status2] || "";
669
+ const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
670
+ const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
671
+ let line = `${method} ${path2}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
672
+ const location = res.headers.get("location");
673
+ if (location) line += ` \u2192 ${location}`;
674
+ message("api", line);
675
+ };
676
+ return {
677
+ level,
678
+ message,
679
+ start: (url) => message("start", url),
680
+ request
681
+ };
682
+ }
683
+
547
684
  // src/helpers/upload.ts
548
685
  function parseBytes(value) {
549
686
  if (typeof value === "number") return value;
@@ -608,7 +745,7 @@ var UploadPipeline = class {
608
745
  }
609
746
  if (!this._bucket) {
610
747
  throw new Error(
611
- `No destination configured \u2014 pass a bucket to upload() or call .store()`
748
+ `No destination configured. Pass a bucket to upload() or call .store()`
612
749
  );
613
750
  }
614
751
  return saveFileToBucket(originalName, data, this._bucket, contentType);
@@ -621,9 +758,18 @@ function upload(bucket) {
621
758
  // src/helpers/config.ts
622
759
  function config(options = {}) {
623
760
  const env2 = globalThis.env;
761
+ const raw = options.log ?? env2.LOG_LEVEL;
762
+ const level = raw === true ? "info" : raw === false ? void 0 : raw;
763
+ const log = createLogger(level);
624
764
  const settings = {
625
765
  port: options.port || env2.PORT || 3e3,
626
- secret: options.secret || env2.SECRET || `unsafe-${createId()}`
766
+ secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
767
+ log,
768
+ // Trust X-Forwarded-* headers for ctx.ip (on by default; set it to false
769
+ // when clients connect directly so a client can't spoof its IP).
770
+ security: {
771
+ trustProxy: options.security?.trustProxy ?? true
772
+ }
627
773
  };
628
774
  options.cors = options.cors || env2.CORS || null;
629
775
  if (options.cors) {
@@ -652,6 +798,9 @@ function config(options = {}) {
652
798
  if ("headers" in options.cors) {
653
799
  cors2.headers = Array.isArray(options.cors.headers) ? options.cors.headers.join(",") : options.cors.headers;
654
800
  }
801
+ if (options.cors.credentials) {
802
+ cors2.credentials = true;
803
+ }
655
804
  }
656
805
  if (typeof cors2.origin === "string") {
657
806
  cors2.origin = cors2.origin.toLowerCase();
@@ -661,6 +810,7 @@ function config(options = {}) {
661
810
  settings.views = options.views ? bucket_default(options.views) : null;
662
811
  settings.public = options.public ? bucket_default(options.public) : null;
663
812
  settings.uploads = options.uploads instanceof UploadPipeline ? options.uploads : options.uploads ? bucket_default(options.uploads) : null;
813
+ if (options.favicon) settings.favicon = options.favicon;
664
814
  settings.store = options.store ?? null;
665
815
  settings.cookies = options.cookies ?? null;
666
816
  if (options.session) {
@@ -682,6 +832,20 @@ function config(options = {}) {
682
832
  status: error.status || 500
683
833
  });
684
834
  });
835
+ const loc = (v) => typeof v === "string" ? v : "enabled";
836
+ if (settings.auth) {
837
+ log.message("auth", `${settings.auth.provider.join(", ")} auth enabled`);
838
+ }
839
+ if (settings.public) log.message("public", loc(options.public));
840
+ if (settings.views) log.message("views", loc(options.views));
841
+ if (settings.uploads) log.message("uploads", loc(options.uploads));
842
+ if (settings.session) log.message("session", "enabled");
843
+ if (settings.cors) {
844
+ const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
845
+ log.message("cors", origin);
846
+ }
847
+ if (settings.favicon) log.message("favicon", loc(settings.favicon));
848
+ if (settings.openapi) log.message("openapi", settings.openapi.path || "/docs");
685
849
  return settings;
686
850
  }
687
851
 
@@ -698,6 +862,27 @@ function cors(config2, origin = "") {
698
862
  console.warn(`CORS: Origin "${origin}" not allowed. Allowed "${config2}"`);
699
863
  return null;
700
864
  }
865
+ function applyCors(res, ctx) {
866
+ const settings = ctx.options.cors;
867
+ if (!settings) return;
868
+ const requestOrigin = ctx.headers.origin || "";
869
+ let origin = cors(settings.origin, requestOrigin);
870
+ if (!origin) return;
871
+ if (settings.credentials && origin === "*") {
872
+ if (!requestOrigin) return;
873
+ origin = requestOrigin.toLowerCase();
874
+ }
875
+ res.headers.set("Access-Control-Allow-Origin", origin);
876
+ res.headers.set("Access-Control-Allow-Methods", settings.methods);
877
+ res.headers.set("Access-Control-Allow-Headers", settings.headers);
878
+ if (settings.credentials) {
879
+ res.headers.set("Access-Control-Allow-Credentials", "true");
880
+ }
881
+ if (origin !== "*") res.headers.append("Vary", "Origin");
882
+ if (ctx.method === "options") {
883
+ res.headers.set("Access-Control-Max-Age", "86400");
884
+ }
885
+ }
701
886
 
702
887
  // src/helpers/createCookies.ts
703
888
  var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
@@ -825,7 +1010,12 @@ async function parseResponse(out, ctx) {
825
1010
  }
826
1011
  if (typeof out === "string") {
827
1012
  const type2 = /^\s*</.test(out) ? "text/html" : "text/plain";
828
- out = new Response(out, { headers: { "content-type": type2 } });
1013
+ out = new Response(out, {
1014
+ headers: {
1015
+ "content-type": type2,
1016
+ "content-length": String(Buffer.byteLength(out))
1017
+ }
1018
+ });
829
1019
  }
830
1020
  if (out?.constructor === Object || Array.isArray(out)) {
831
1021
  out = json(out);
@@ -848,17 +1038,7 @@ async function parseResponse(out, ctx) {
848
1038
  if (!(out instanceof Response)) {
849
1039
  throw new Error(`Invalid response type ${out}`);
850
1040
  }
851
- if (ctx.options.cors) {
852
- const origin = cors(ctx.options.cors.origin, ctx.headers.origin);
853
- if (origin) {
854
- out.headers.set("Access-Control-Allow-Origin", origin);
855
- out.headers.set("Access-Control-Allow-Methods", ctx.options.cors.methods);
856
- out.headers.set("Access-Control-Allow-Headers", ctx.options.cors.headers);
857
- if (ctx.options.cors.credentials) {
858
- out.headers.set("Access-Control-Allow-Credentials", "true");
859
- }
860
- }
861
- }
1041
+ applyCors(out, ctx);
862
1042
  if (ctx.time?.times?.length > 1) {
863
1043
  out.headers.set("Server-Timing", ctx.time.headers());
864
1044
  }
@@ -976,6 +1156,11 @@ function validate(ctx, schema) {
976
1156
 
977
1157
  // src/helpers/handleRequest.ts
978
1158
  async function handleRequest(handlers, ctx) {
1159
+ const res = await getResponse(handlers, ctx);
1160
+ if (res) ctx.options.log.request(ctx, res);
1161
+ return res;
1162
+ }
1163
+ async function getResponse(handlers, ctx) {
979
1164
  try {
980
1165
  for (const [method, matcher, ...cbs] of handlers[ctx.method]) {
981
1166
  const match = pathPattern(matcher, ctx.url.pathname || "/");
@@ -995,7 +1180,9 @@ async function handleRequest(handlers, ctx) {
995
1180
  if (ctx.platform.provider === "netlify") return;
996
1181
  throw new ServerError_default("NOT_FOUND", 404, "Not Found");
997
1182
  } catch (error) {
998
- return ctx.options.onError(error, ctx);
1183
+ const res = await ctx.options.onError(error, ctx);
1184
+ applyCors(res, ctx);
1185
+ return res;
999
1186
  }
1000
1187
  }
1001
1188
 
@@ -1342,10 +1529,10 @@ async function verify(password, hash3) {
1342
1529
  // src/auth/findSessionId.ts
1343
1530
  var validateToken = (authorization) => {
1344
1531
  const [type2, id] = authorization.trim().split(" ");
1345
- if (!type2 || type2.toLowerCase() !== "bearer") {
1532
+ if (type2?.toLowerCase() !== "bearer") {
1346
1533
  throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1347
1534
  }
1348
- if (!id || id.length !== 16) {
1535
+ if (id?.length !== 16) {
1349
1536
  throw ServerError_default.AUTH_INVALID_TOKEN();
1350
1537
  }
1351
1538
  return id;
@@ -1452,6 +1639,23 @@ async function assets(ctx) {
1452
1639
  }
1453
1640
  }
1454
1641
 
1642
+ // src/middle/favicon.ts
1643
+ async function favicon(ctx) {
1644
+ if (ctx.method !== "get") return;
1645
+ if (ctx.url.pathname !== "/favicon.ico") return;
1646
+ const fav = ctx.options.favicon;
1647
+ if (fav) {
1648
+ if (typeof fav === "string") return file(fav);
1649
+ const icon = await fav.read("favicon.ico");
1650
+ return icon ? type("ico").send(icon) : 204;
1651
+ }
1652
+ const handled = ctx.app.handlers.get.some(
1653
+ ([method, matcher]) => method !== "*" && pathPattern(matcher, "/favicon.ico")
1654
+ );
1655
+ if (handled) return;
1656
+ return 204;
1657
+ }
1658
+
1455
1659
  // src/middle/openapi.ts
1456
1660
  import * as fsp2 from "fs/promises";
1457
1661
  var entities = {
@@ -1614,6 +1818,17 @@ var openapi_default = async (ctx) => {
1614
1818
  </html> `;
1615
1819
  };
1616
1820
 
1821
+ // src/middle/preflight.ts
1822
+ function preflight(ctx) {
1823
+ if (ctx.method !== "options") return;
1824
+ if (!ctx.headers["access-control-request-method"]) return;
1825
+ const handled = ctx.app.handlers.options.some(
1826
+ ([method, matcher]) => method !== "*" && pathPattern(matcher, ctx.url.pathname)
1827
+ );
1828
+ if (handled) return;
1829
+ return 204;
1830
+ }
1831
+
1617
1832
  // src/middle/NoSession.ts
1618
1833
  var NoSession = class {
1619
1834
  };
@@ -1723,6 +1938,9 @@ async function createNode(req, app) {
1723
1938
  const body2 = [];
1724
1939
  req.on("data", (chunk) => body2.push(chunk)).on("end", () => resolve2(Buffer.concat(body2))).on("error", reject);
1725
1940
  });
1941
+ if (rawBody.length && !headers2["content-length"]) {
1942
+ headers2["content-length"] = String(rawBody.length);
1943
+ }
1726
1944
  const body = rawBody ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
1727
1945
  const events = createEvents();
1728
1946
  return {
@@ -1736,12 +1954,16 @@ async function createNode(req, app) {
1736
1954
  session: {},
1737
1955
  init,
1738
1956
  events,
1739
- app
1957
+ app,
1958
+ ip: clientIp(headers2, {
1959
+ remoteAddress: req.socket.remoteAddress || "",
1960
+ trustProxy: app.settings.security.trustProxy
1961
+ })
1740
1962
  };
1741
1963
  }
1742
1964
 
1743
1965
  // src/context/winter.ts
1744
- async function createWinter(req, app) {
1966
+ async function createWinter(req, app, server2) {
1745
1967
  const init = performance.now();
1746
1968
  const method = req.method.toLowerCase();
1747
1969
  if (!isValidMethod(method)) {
@@ -1757,6 +1979,9 @@ async function createWinter(req, app) {
1757
1979
  (url2) => Object.fromEntries(url2.searchParams.entries())
1758
1980
  );
1759
1981
  const rawBody = Buffer.from(await req.arrayBuffer());
1982
+ if (rawBody.length && !headers2["content-length"]) {
1983
+ headers2["content-length"] = String(rawBody.length);
1984
+ }
1760
1985
  const body = req.body ? await parseBody(rawBody, headers2["content-type"], app.settings.uploads) : void 0;
1761
1986
  const events = createEvents();
1762
1987
  return {
@@ -1770,7 +1995,11 @@ async function createWinter(req, app) {
1770
1995
  session: {},
1771
1996
  init,
1772
1997
  events,
1773
- app
1998
+ app,
1999
+ ip: clientIp(headers2, {
2000
+ remoteAddress: server2?.requestIP?.(req)?.address || "",
2001
+ trustProxy: app.settings.security.trustProxy
2002
+ })
1774
2003
  };
1775
2004
  }
1776
2005
 
@@ -1778,7 +2007,7 @@ async function createWinter(req, app) {
1778
2007
  var Winter = async (app, request, env2) => {
1779
2008
  if (env2?.upgrade(request)) return;
1780
2009
  Object.assign(globalThis.env, env2);
1781
- const ctx = await createWinter(request, app);
2010
+ const ctx = await createWinter(request, app, env2);
1782
2011
  const res = await handleRequest(app.handlers, ctx);
1783
2012
  ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
1784
2013
  return res;
@@ -1796,7 +2025,9 @@ var Node = async (app) => {
1796
2025
  response.write(out.body || "");
1797
2026
  }
1798
2027
  response.end();
1799
- }).listen(app.settings.port);
2028
+ }).listen(app.settings.port, () => {
2029
+ app.settings.log.start(`http://localhost:${app.settings.port}/`);
2030
+ });
1800
2031
  };
1801
2032
  var Netlify = async (app, request, context) => {
1802
2033
  request.context = context;
@@ -1964,9 +2195,13 @@ var Server = class extends Router {
1964
2195
  this.websocket = createWebsocket(this.sockets, this.handlers);
1965
2196
  if (this.platform.runtime === "node") {
1966
2197
  this.node();
2198
+ } else if (this.platform.runtime === "bun") {
2199
+ this.settings.log.start(`http://localhost:${this.settings.port}/`);
1967
2200
  }
1968
2201
  this.use(timer);
2202
+ if (this.settings.cors) this.use(preflight);
1969
2203
  this.use(assets);
2204
+ this.use(favicon);
1970
2205
  this.use(session);
1971
2206
  if (this.settings.auth) {
1972
2207
  auth(this);
package/package.json CHANGED
@@ -1,37 +1,27 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.28.15",
3
+ "version": "0.29.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
- "repository": "https://github.com/franciscop/server-next.git",
7
- "bugs": "https://github.com/franciscop/server-next/issues",
8
- "funding": "https://www.paypal.me/franciscopresencia/19",
6
+ "repository": "github:franciscop/server-next",
9
7
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
8
+ "funding": "https://www.paypal.me/franciscopresencia/19",
10
9
  "license": "UNLICENSED",
11
- "documentation": {
12
- "title": "Server JS - A modern web server for Bun and Node.js",
13
- "home": "./docs/index.html",
14
- "menu": {
15
- "Documentation": "/documentation",
16
- "Github": "https://github.com/franciscop/server-next"
17
- }
18
- },
10
+ "keywords": [
11
+ "server",
12
+ "node",
13
+ "server.js"
14
+ ],
19
15
  "scripts": {
20
16
  "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
21
17
  "start": "bun test --watch",
22
- "lint": "npx @biomejs/biome lint ./src --skip=lint/suspicious/noExplicitAny --skip=lint/style/noParameterAssign --skip=lint/suspicious/noConfusingVoidType",
23
- "types": "npx tsc --noEmit",
18
+ "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",
24
19
  "test": "npm run test:bun && tsc --noEmit",
25
20
  "test:bun": "bun test",
26
21
  "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
27
22
  },
28
- "keywords": [
29
- "server",
30
- "node",
31
- "server.js"
32
- ],
33
- "type": "module",
34
23
  "main": "index.js",
24
+ "type": "module",
35
25
  "types": "index.d.ts",
36
26
  "files": [
37
27
  "index.d.ts",
@@ -49,6 +39,14 @@
49
39
  "default": "./src/jsx/jsx-dev-runtime.js"
50
40
  }
51
41
  },
42
+ "documentation.page": {
43
+ "title": "Server JS - A modern web server for Bun and Node.js",
44
+ "home": "./docs/index.html",
45
+ "menu": {
46
+ "Documentation": "/documentation",
47
+ "Github": "https://github.com/franciscop/server-next"
48
+ }
49
+ },
52
50
  "devDependencies": {
53
51
  "@types/bun": "^1.3.0",
54
52
  "@types/jest": "^30.0.0",
@@ -58,12 +56,8 @@
58
56
  "jest": "^29.7.0",
59
57
  "polystore": "^0.21.1",
60
58
  "tsup": "^8.5.1",
61
- "typescript": "^5.9.3"
62
- },
63
- "engines": {
64
- "node": ">=24.11.0"
59
+ "typescript": "^6.0.2"
65
60
  },
66
- "engineStrict": true,
67
61
  "jest": {
68
62
  "testEnvironment": "jest-environment-node",
69
63
  "transform": {}
package/readme.md CHANGED
@@ -1 +1 @@
1
- # Server JS [![test badge](https://github.com/franciscop/server-next/workflows/tests/badge.svg)](https://github.com/franciscop/server-next/actions)
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)
@@ -12,3 +12,15 @@ export declare namespace JSX {
12
12
  [elem: string]: any;
13
13
  }
14
14
  }
15
+
16
+ declare global {
17
+ namespace JSX {
18
+ interface Element {
19
+ type: any;
20
+ props: any;
21
+ }
22
+ interface IntrinsicElements {
23
+ [elem: string]: any;
24
+ }
25
+ }
26
+ }
@@ -34,7 +34,7 @@ const encode = (str = "") => {
34
34
  return str.replace(/[&<>"']/g, (tag) => ENTITIES[tag]);
35
35
  };
36
36
 
37
- // valid primitives only null, undefined, false, true are all skipped
37
+ // valid primitives only: null, undefined, false, true are all skipped
38
38
  const isValidChild = (child) =>
39
39
  child != null && child !== false && child !== true;
40
40