@server/next 0.35.0 → 0.36.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 +16 -8
  2. package/index.js +142 -70
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -34,11 +34,13 @@ type BodyOption = BodyMode | {
34
34
  mode?: BodyMode;
35
35
  max?: number | string | false;
36
36
  };
37
+ type CacheOption = string | number | false;
37
38
  type RouteOptions = {
38
39
  tags?: string | string[];
39
40
  title?: string;
40
41
  description?: string;
41
42
  body?: BodyOption;
43
+ cache?: CacheOption;
42
44
  };
43
45
  type Route = {
44
46
  path: string;
@@ -188,6 +190,7 @@ type Options = {
188
190
  favicon?: string | BucketFile;
189
191
  security?: boolean | SecurityOptions;
190
192
  body?: BodyOption;
193
+ cache?: CacheOption;
191
194
  };
192
195
  type Settings = {
193
196
  port: number;
@@ -207,6 +210,7 @@ type Settings = {
207
210
  favicon?: string | BucketFile;
208
211
  security: SecuritySettings;
209
212
  body: BodyOption;
213
+ cache?: CacheOption;
210
214
  };
211
215
  type Time = {
212
216
  (name: string): void;
@@ -230,7 +234,9 @@ type ParamsToObject<Params extends string> = {
230
234
  };
231
235
  type PathToParams<Path extends string> = ParamsToObject<ExtractPathParams<Path>>;
232
236
  type BunEnv = Record<string, string> & {
233
- upgrade?: (req: Request) => boolean;
237
+ upgrade?: (req: Request, options?: {
238
+ data?: any;
239
+ }) => boolean;
234
240
  };
235
241
  type Context<Params extends Record<string, string | undefined> = Record<string, string>, O extends ServerConfig = object> = {
236
242
  method: Method;
@@ -343,6 +349,7 @@ declare class Reply {
343
349
  type(type?: string): this;
344
350
  download(name?: string): this;
345
351
  headers(key: string | Record<string, string>, value?: string): this;
352
+ cache(value: CacheOption): this;
346
353
  cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
347
354
  json(body: unknown): Response;
348
355
  redirect(path: string): Response;
@@ -353,6 +360,7 @@ type Params<K extends keyof Reply> = Reply[K] extends (...args: infer A) => any
353
360
  declare const status: (...args: Params<"status">) => Reply;
354
361
  declare const headers: (...args: Params<"headers">) => Reply;
355
362
  declare const type: (...args: Params<"type">) => Reply;
363
+ declare const cache: (...args: Params<"cache">) => Reply;
356
364
  declare const download: (...args: Params<"download">) => Reply;
357
365
  declare const cookies: (...args: Params<"cookies">) => Reply;
358
366
  declare const send: (...args: Params<"send">) => Response;
@@ -407,9 +415,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
407
415
  signal?: AbortSignal | null;
408
416
  window?: null;
409
417
  }) => Promise<Response>;
410
- post: (path: string, body?: string | number | boolean | ArrayBuffer | {
418
+ post: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
411
419
  [key: string]: SerializableValue;
412
- } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
420
+ } | SerializableValue[], options?: {
413
421
  cache?: RequestCache;
414
422
  credentials?: RequestCredentials;
415
423
  headers?: HeadersInit;
@@ -424,9 +432,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
424
432
  signal?: AbortSignal | null;
425
433
  window?: null;
426
434
  }) => Promise<Response>;
427
- put: (path: string, body?: string | number | boolean | ArrayBuffer | {
435
+ put: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
428
436
  [key: string]: SerializableValue;
429
- } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
437
+ } | SerializableValue[], options?: {
430
438
  cache?: RequestCache;
431
439
  credentials?: RequestCredentials;
432
440
  headers?: HeadersInit;
@@ -441,9 +449,9 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
441
449
  signal?: AbortSignal | null;
442
450
  window?: null;
443
451
  }) => Promise<Response>;
444
- patch: (path: string, body?: string | number | boolean | ArrayBuffer | {
452
+ patch: (path: string, body?: string | number | boolean | ArrayBuffer | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams | {
445
453
  [key: string]: SerializableValue;
446
- } | SerializableValue[] | ReadableStream<any> | Blob | ArrayBufferView<ArrayBuffer> | FormData | URLSearchParams, options?: {
454
+ } | SerializableValue[], options?: {
447
455
  cache?: RequestCache;
448
456
  credentials?: RequestCredentials;
449
457
  headers?: HeadersInit;
@@ -492,4 +500,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
492
500
  }
493
501
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
494
502
 
495
- export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, 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 };
503
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type 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, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
package/index.js CHANGED
@@ -683,6 +683,97 @@ async function resolveBody(ctx, body) {
683
683
  return parsed;
684
684
  }
685
685
 
686
+ // src/helpers/createCookies.ts
687
+ var EXPIRED = (/* @__PURE__ */ new Date(0)).toUTCString();
688
+ var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
689
+ parse.millisecond = parse.ms = 1e-3;
690
+ parse.second = parse.sec = parse.s = parse[""] = 1;
691
+ parse.minute = parse.min = parse.m = parse.s * 60;
692
+ parse.hour = parse.hr = parse.h = parse.m * 60;
693
+ parse.day = parse.d = parse.h * 24;
694
+ parse.week = parse.wk = parse.w = parse.d * 7;
695
+ parse.year = parse.yr = parse.y = parse.d * 365.25;
696
+ parse.month = parse.b = parse.y / 12;
697
+ function parse(str) {
698
+ if (str === null || str === void 0) return null;
699
+ if (typeof str === "number") return str;
700
+ if (typeof str !== "string") {
701
+ throw new Error(`Not a string: ${str} (${typeof str})`);
702
+ }
703
+ str = str.toLowerCase().replace(/[,_]/g, "");
704
+ const [_, value, units] = times.exec(str) || [];
705
+ if (!units) return null;
706
+ const unitValue = parse[units] || parse[units.replace(/s$/, "")];
707
+ if (!unitValue) return null;
708
+ const result = unitValue * parseFloat(value);
709
+ return Math.abs(Math.round(result * 1e3));
710
+ }
711
+ function normalizeExpires(expires) {
712
+ if (expires === null || expires === void 0) return void 0;
713
+ if (expires === 0) return EXPIRED;
714
+ if (typeof expires === "string") {
715
+ if (/^[\d._]+\w+$/.test(expires)) {
716
+ return new Date(Date.now() + parse(expires)).toUTCString();
717
+ } else {
718
+ return expires;
719
+ }
720
+ }
721
+ if (typeof expires === "number") {
722
+ return new Date(Date.now() + expires).toUTCString();
723
+ }
724
+ if (expires instanceof Date) {
725
+ return expires.toUTCString();
726
+ }
727
+ return void 0;
728
+ }
729
+ function createCookies(key, val) {
730
+ if (val.value === null) val.expires = EXPIRED;
731
+ const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
732
+ let str = `${key}=${value || ""};Path=${path2 || "/"}`;
733
+ if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
734
+ if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
735
+ if (httpOnly) str += ";HttpOnly";
736
+ if (secure) str += ";Secure";
737
+ if (sameSite) str += `;SameSite=${sameSite}`;
738
+ return str;
739
+ }
740
+
741
+ // src/helpers/etag.ts
742
+ function etag(bytes) {
743
+ let h = 2166136261;
744
+ for (let i = 0; i < bytes.length; i++) {
745
+ h ^= bytes[i];
746
+ h = Math.imul(h, 16777619);
747
+ }
748
+ return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
749
+ }
750
+
751
+ // src/helpers/cache.ts
752
+ function resolveCache(value) {
753
+ if (value === false || value === 0) return "no-store";
754
+ if (typeof value === "number") return `public, max-age=${Math.round(value)}`;
755
+ if (typeof value !== "string") return null;
756
+ const ms = parse(value);
757
+ return ms === null ? null : `public, max-age=${Math.round(ms / 1e3)}`;
758
+ }
759
+ async function applyCache(out, ctx) {
760
+ if (ctx.method !== "get" || out.status !== 200) return out;
761
+ if (!out.headers.has("cache-control")) {
762
+ const value = resolveCache(ctx.options.cache);
763
+ if (value) out.headers.set("cache-control", value);
764
+ }
765
+ if (out.headers.has("etag") || !out.headers.has("content-length")) return out;
766
+ const bytes = new Uint8Array(await out.arrayBuffer());
767
+ const tag = etag(bytes);
768
+ const headers2 = new Headers(out.headers);
769
+ headers2.set("etag", tag);
770
+ if (ctx.headers["if-none-match"] === tag) {
771
+ headers2.delete("content-length");
772
+ return new Response(null, { status: 304, headers: headers2 });
773
+ }
774
+ return new Response(bytes, { status: 200, headers: headers2 });
775
+ }
776
+
686
777
  // src/helpers/clientIp.ts
687
778
  var first = (v) => (Array.isArray(v) ? v[0] : v) || "";
688
779
  var normalize = (ip) => ip.replace(/^::ffff:/, "");
@@ -707,7 +798,7 @@ function isReadableStream(obj) {
707
798
  }
708
799
 
709
800
  // src/reply.ts
710
- var EXPIRED = (/* @__PURE__ */ new Date(0)).toUTCString();
801
+ var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
711
802
  var Reply = class {
712
803
  res;
713
804
  constructor() {
@@ -743,6 +834,11 @@ var Reply = class {
743
834
  this.res.headers.append(key, value);
744
835
  return this;
745
836
  }
837
+ cache(value) {
838
+ const resolved = resolveCache(value);
839
+ if (resolved) this.res.headers.set("cache-control", resolved);
840
+ return this;
841
+ }
746
842
  cookies(key, value) {
747
843
  if (typeof key === "object") {
748
844
  Object.entries(key).map(([key2, value2]) => this.cookies(key2, value2));
@@ -752,7 +848,7 @@ var Reply = class {
752
848
  Object.values(value).map((val) => this.cookies(key, val));
753
849
  return this;
754
850
  }
755
- if (value === null) return this.cookies(key, { expires: EXPIRED });
851
+ if (value === null) return this.cookies(key, { expires: EXPIRED2 });
756
852
  if (typeof value !== "object") return this.cookies(key, { value });
757
853
  return this.headers("set-cookie", createCookies(key, value));
758
854
  }
@@ -822,6 +918,7 @@ var r = () => new Reply();
822
918
  var status = (...args) => r().status(...args);
823
919
  var headers = (...args) => r().headers(...args);
824
920
  var type = (...args) => r().type(...args);
921
+ var cache = (...args) => r().cache(...args);
825
922
  var download = (...args) => r().download(...args);
826
923
  var cookies = (...args) => r().cookies(...args);
827
924
  var send = (...args) => r().send(...args);
@@ -939,61 +1036,6 @@ async function finishLogin(ctx, input) {
939
1036
  throw new Error("Unknown auth type");
940
1037
  }
941
1038
 
942
- // src/helpers/createCookies.ts
943
- var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
944
- var times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
945
- parse.millisecond = parse.ms = 1e-3;
946
- parse.second = parse.sec = parse.s = parse[""] = 1;
947
- parse.minute = parse.min = parse.m = parse.s * 60;
948
- parse.hour = parse.hr = parse.h = parse.m * 60;
949
- parse.day = parse.d = parse.h * 24;
950
- parse.week = parse.wk = parse.w = parse.d * 7;
951
- parse.year = parse.yr = parse.y = parse.d * 365.25;
952
- parse.month = parse.b = parse.y / 12;
953
- function parse(str) {
954
- if (str === null || str === void 0) return null;
955
- if (typeof str === "number") return str;
956
- if (typeof str !== "string") {
957
- throw new Error(`Not a string: ${str} (${typeof str})`);
958
- }
959
- str = str.toLowerCase().replace(/[,_]/g, "");
960
- const [_, value, units] = times.exec(str) || [];
961
- if (!units) return null;
962
- const unitValue = parse[units] || parse[units.replace(/s$/, "")];
963
- if (!unitValue) return null;
964
- const result = unitValue * parseFloat(value);
965
- return Math.abs(Math.round(result * 1e3));
966
- }
967
- function normalizeExpires(expires) {
968
- if (expires === null || expires === void 0) return void 0;
969
- if (expires === 0) return EXPIRED2;
970
- if (typeof expires === "string") {
971
- if (/^[\d._]+\w+$/.test(expires)) {
972
- return new Date(Date.now() + parse(expires)).toUTCString();
973
- } else {
974
- return expires;
975
- }
976
- }
977
- if (typeof expires === "number") {
978
- return new Date(Date.now() + expires).toUTCString();
979
- }
980
- if (expires instanceof Date) {
981
- return expires.toUTCString();
982
- }
983
- return void 0;
984
- }
985
- function createCookies(key, val) {
986
- if (val.value === null) val.expires = EXPIRED2;
987
- const { value, path: path2, expires, maxAge, httpOnly, secure, sameSite } = val;
988
- let str = `${key}=${value || ""};Path=${path2 || "/"}`;
989
- if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
990
- if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
991
- if (httpOnly) str += ";HttpOnly";
992
- if (secure) str += ";Secure";
993
- if (sameSite) str += `;SameSite=${sameSite}`;
994
- return str;
995
- }
996
-
997
1039
  // src/auth/state.ts
998
1040
  var NAME = "oauth_state";
999
1041
  function startState(ctx, crossSite = false) {
@@ -1606,6 +1648,7 @@ function config(options = {}) {
1606
1648
  // the added headers off; see resolveSecurity for the defaults.
1607
1649
  security: resolveSecurity(options.security)
1608
1650
  };
1651
+ if (options.cache !== void 0) settings.cache = options.cache;
1609
1652
  options.cors = options.cors || env2.CORS || null;
1610
1653
  if (options.cors) {
1611
1654
  const cors2 = {
@@ -1683,6 +1726,7 @@ function config(options = {}) {
1683
1726
  log.message("cors", origin);
1684
1727
  }
1685
1728
  if (settings.favicon) log.message("favicon", loc(settings.favicon));
1729
+ if (settings.cache !== void 0) log.message("cache", loc(options.cache));
1686
1730
  if (settings.openapi) log.message("openapi", settings.openapi.path || "/docs");
1687
1731
  return settings;
1688
1732
  }
@@ -1722,23 +1766,14 @@ function applyCors(res, ctx) {
1722
1766
  }
1723
1767
  }
1724
1768
 
1725
- // src/helpers/etag.ts
1726
- function etag(bytes) {
1727
- let h = 2166136261;
1728
- for (let i = 0; i < bytes.length; i++) {
1729
- h ^= bytes[i];
1730
- h = Math.imul(h, 16777619);
1731
- }
1732
- return `"${bytes.length.toString(16)}-${(h >>> 0).toString(16)}"`;
1733
- }
1734
-
1735
1769
  // src/helpers/createWebsocket.ts
1736
1770
  function createWebsocket(sockets, handlers) {
1737
1771
  const run = (event, socket, body) => {
1738
1772
  const routes = handlers.socket?.filter((r2) => r2.path === event) ?? [];
1773
+ const user = socket.user ?? socket.data?.user;
1739
1774
  for (const route of routes) {
1740
1775
  for (const fn of route.fns) {
1741
- fn({ socket, sockets, body });
1776
+ fn({ socket, sockets, body, user });
1742
1777
  }
1743
1778
  }
1744
1779
  };
@@ -1845,6 +1880,7 @@ async function parseResponse(out, ctx) {
1845
1880
  }
1846
1881
  applyCors(out, ctx);
1847
1882
  applySecurity(out, ctx);
1883
+ out = await applyCache(out, ctx);
1848
1884
  if (ctx.time?.times?.length > 1) {
1849
1885
  out.headers.set("Server-Timing", ctx.time.headers());
1850
1886
  }
@@ -2632,6 +2668,13 @@ function timer(ctx) {
2632
2668
  ctx.time = createTime();
2633
2669
  }
2634
2670
 
2671
+ // src/auth/socketUser.ts
2672
+ async function socketUser(app, headers2, cookies2) {
2673
+ if (!app.settings.auth) return void 0;
2674
+ const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
2675
+ return getUser(ctx);
2676
+ }
2677
+
2635
2678
  // src/helpers/wsNode.ts
2636
2679
  var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2637
2680
  var CONTINUATION = 0;
@@ -2666,6 +2709,9 @@ var NodeWebSocket = class {
2666
2709
  fragmentOpcode;
2667
2710
  closed;
2668
2711
  readyState;
2712
+ // The auth user resolved from the upgrade request (see attachWebsocket), or
2713
+ // undefined for an anonymous connection. Read by socket handlers as `ctx.user`.
2714
+ user;
2669
2715
  constructor(socket, handlers) {
2670
2716
  this.socket = socket;
2671
2717
  this.handlers = handlers;
@@ -2764,13 +2810,24 @@ var NodeWebSocket = class {
2764
2810
  };
2765
2811
  async function attachWebsocket(server2, app) {
2766
2812
  const { createHash } = await import("crypto");
2767
- server2.on("upgrade", (req, socket, head) => {
2813
+ server2.on("upgrade", async (req, socket, head) => {
2768
2814
  const key = req.headers["sec-websocket-key"];
2769
2815
  const upgrade = String(req.headers.upgrade || "").toLowerCase();
2770
2816
  if (upgrade !== "websocket" || !key || !app.handlers.socket.length) {
2771
2817
  socket.destroy();
2772
2818
  return;
2773
2819
  }
2820
+ const cookies2 = parseCookies(req.headers.cookie);
2821
+ let user;
2822
+ try {
2823
+ user = await socketUser(app, req.headers, cookies2);
2824
+ } catch {
2825
+ socket.write(
2826
+ "HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"
2827
+ );
2828
+ socket.destroy();
2829
+ return;
2830
+ }
2774
2831
  const accept = createHash("sha1").update(key + GUID).digest("base64");
2775
2832
  socket.write(
2776
2833
  `HTTP/1.1 101 Switching Protocols\r
@@ -2786,6 +2843,7 @@ Sec-WebSocket-Accept: ${accept}\r
2786
2843
  onMessage: (body) => app.websocket.message(ws, body),
2787
2844
  onClose: () => app.websocket.close(ws)
2788
2845
  });
2846
+ ws.user = user;
2789
2847
  app.websocket.open(ws);
2790
2848
  if (head?.length) ws.receive(head);
2791
2849
  socket.on("data", (chunk) => ws.receive(chunk));
@@ -2902,7 +2960,20 @@ async function createWinter(req, app, server2) {
2902
2960
 
2903
2961
  // src/context/handlers.ts
2904
2962
  var Winter = async (app, request, env2) => {
2905
- if (env2?.upgrade(request)) return;
2963
+ if (env2?.upgrade) {
2964
+ const wantsWs = String(request.headers.get("upgrade") || "").toLowerCase() === "websocket";
2965
+ if (wantsWs) {
2966
+ const headers2 = parseHeaders_default(request.headers);
2967
+ const cookies2 = parseCookies(headers2.cookie);
2968
+ let user;
2969
+ try {
2970
+ user = await socketUser(app, headers2, cookies2);
2971
+ } catch {
2972
+ return new Response("Unauthorized", { status: 401 });
2973
+ }
2974
+ if (env2.upgrade(request, { data: { user } })) return;
2975
+ }
2976
+ }
2906
2977
  Object.assign(globalThis.env, env2);
2907
2978
  const ctx = await createWinter(request, app, env2);
2908
2979
  const res = await handleRequest(app, ctx);
@@ -3137,6 +3208,7 @@ function server(options) {
3137
3208
  export {
3138
3209
  Server,
3139
3210
  ServerError_default as ServerError,
3211
+ cache,
3140
3212
  cookies,
3141
3213
  server as default,
3142
3214
  download,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.35.0",
3
+ "version": "0.36.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",