@server/next 0.44.1 → 0.45.1

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
@@ -1,4 +1,5 @@
1
1
  import * as http from 'http';
2
+ import { Readable } from 'node:stream';
2
3
  export { default as kv } from 'polystore';
3
4
  export { default as bucket } from 'bucket';
4
5
 
@@ -9,6 +10,7 @@ type LimitOptions = {
9
10
  };
10
11
 
11
12
  type CookieOptions = string | string[] | Cookie | Cookie[] | null;
13
+ type SendBody = SerializableValue | JSX.Element | Uint8Array | ReadableStream | Readable | Response | Reply$1 | BucketFile | Promise<SendBody>;
12
14
  interface ResponseData {
13
15
  headers: Headers;
14
16
  status?: number;
@@ -22,10 +24,10 @@ declare class Reply$1 {
22
24
  headers(key: string | Record<string, string | string[]>, value?: string | string[]): this;
23
25
  cache(value: CacheOption): this;
24
26
  cookies(key: string | Record<string, CookieOptions>, value?: CookieOptions): this;
25
- json(body: unknown): Response;
26
- redirect(path: string): Response;
27
+ json(body: unknown): Promise<Response>;
28
+ redirect(path: string): Promise<Response>;
27
29
  file(path: string | BucketFile): Promise<Response>;
28
- send(body?: string | Buffer | ReadableStream | any): Response;
30
+ send(input?: SendBody): Promise<Response>;
29
31
  }
30
32
  type Params<K extends keyof Reply$1> = Reply$1[K] extends (...args: infer A) => any ? A : never;
31
33
  declare const status: (...args: Params<"status">) => Reply$1;
@@ -34,25 +36,23 @@ declare const type: (...args: Params<"type">) => Reply$1;
34
36
  declare const cache: (...args: Params<"cache">) => Reply$1;
35
37
  declare const download: (...args: Params<"download">) => Reply$1;
36
38
  declare const cookies: (...args: Params<"cookies">) => Reply$1;
37
- declare const send: (...args: Params<"send">) => Response;
38
- declare const json: (...args: Params<"json">) => Response;
39
+ declare const send: (...args: Params<"send">) => Promise<Response>;
40
+ declare const json: (...args: Params<"json">) => Promise<Response>;
39
41
  declare const file: (...args: Params<"file">) => Promise<Response>;
40
- declare const redirect: (...args: Params<"redirect">) => Response;
42
+ declare const redirect: (...args: Params<"redirect">) => Promise<Response>;
41
43
 
42
44
  type Reply = ReturnType<typeof status>;
43
45
  type Method = "get" | "post" | "put" | "patch" | "delete" | "head" | "options" | "socket";
44
46
  type ContextTypes = {
45
- session?: any;
46
47
  user?: any;
47
48
  params?: any;
48
49
  query?: any;
49
50
  body?: any;
50
51
  };
51
52
  type Field<C, K extends keyof ContextTypes, Fallback> = K extends keyof C ? SchemaOutput<C[K], C[K]> : Fallback;
52
- declare namespace JSX {
53
+ declare namespace JSX$1 {
53
54
  interface Element {
54
- type: any;
55
- props: any;
55
+ (): string;
56
56
  }
57
57
  interface IntrinsicElements {
58
58
  [elem: string]: any;
@@ -203,6 +203,7 @@ type AuthOption = `${Strategy}:${Provider}` | {
203
203
  strategy: Strategy;
204
204
  providers?: Provider | Provider[];
205
205
  users?: StoreSource;
206
+ sessions?: StoreSource;
206
207
  redirect?: string;
207
208
  onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
208
209
  onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
@@ -214,6 +215,7 @@ type AuthSettings = {
214
215
  providers: Provider[];
215
216
  strategy: Strategy;
216
217
  users: KVStore;
218
+ sessions: KVStore;
217
219
  onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
218
220
  onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
219
221
  onUser: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
@@ -256,7 +258,6 @@ type Options = {
256
258
  secret?: string;
257
259
  public?: string | Bucket;
258
260
  uploads?: string | Bucket | UploadOptions;
259
- sessions?: StoreSource;
260
261
  cors?: CorsOptions;
261
262
  auth?: AuthOption;
262
263
  openapi?: boolean | string | {
@@ -279,8 +280,6 @@ type Settings = {
279
280
  uploads?: ({
280
281
  bucket: Bucket;
281
282
  } & LimitOptions) | null;
282
- sessions: KVStore;
283
- sessionsDefault?: boolean;
284
283
  cors?: CorsSettings;
285
284
  auth?: AuthSettings;
286
285
  openapi?: {
@@ -339,7 +338,6 @@ type Context<C extends ContextTypes = {}> = {
339
338
  time?: Time;
340
339
  socket?: WebSocket;
341
340
  sockets?: WebSocket[];
342
- session: Field<C, "session", Record<string, any>>;
343
341
  user?: Field<C, "user", Record<string, any>>;
344
342
  init: number;
345
343
  app: Server;
@@ -351,7 +349,7 @@ type Context<C extends ContextTypes = {}> = {
351
349
  type InlineReply = Response | Reply | BucketFile | {
352
350
  body: string;
353
351
  headers?: Headers;
354
- } | SerializableValue | JSX.Element | Buffer | ReadableStream;
352
+ } | SerializableValue | JSX$1.Element | Buffer | ReadableStream;
355
353
  type Body = InlineReply;
356
354
  type Middleware<C extends ContextTypes = {}> = (ctx: Context<C>) => InlineReply | Promise<InlineReply> | void | Promise<void>;
357
355
 
@@ -373,7 +371,7 @@ declare class ServerError extends Error {
373
371
  declare const TypedServerError: typeof ServerError & ServerErrorConstructor;
374
372
 
375
373
  declare global {
376
- var env: Record<string, any>;
374
+ var env: Record<string, string | undefined>;
377
375
  }
378
376
 
379
377
  type Fn<C extends ContextTypes> = (ctx: Context<C>) => ReturnType<Middleware>;
@@ -440,7 +438,7 @@ declare class ValidationError extends StatusError {
440
438
  declare class Server<C extends ContextTypes = {}> extends Router<C> {
441
439
  settings: Settings;
442
440
  platform: Platform;
443
- sockets: any[];
441
+ sockets: WebSocket[];
444
442
  websocket: any;
445
443
  port?: number;
446
444
  constructor(options?: Options);
package/index.js CHANGED
@@ -49,8 +49,6 @@ ServerError_default.extend({
49
49
  status: 400,
50
50
  message: "Missing the OAuth 'code' in the request body"
51
51
  },
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",
54
52
  AUTH_INVALID_HEADER: {
55
53
  status: 401,
56
54
  message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
@@ -799,7 +797,7 @@ function isReadableStream(obj) {
799
797
 
800
798
  // src/reply.ts
801
799
  var EXPIRED2 = (/* @__PURE__ */ new Date(0)).toUTCString();
802
- var Reply = class {
800
+ var Reply = class _Reply {
803
801
  res;
804
802
  constructor() {
805
803
  this.res = {
@@ -870,10 +868,12 @@ var Reply = class {
870
868
  }
871
869
  async file(path) {
872
870
  if (typeof path !== "string") {
873
- if (!await path.exists()) return this.status(404).send();
871
+ if (!await path.exists()) return new Response(null, { status: 404 });
874
872
  return this.type(fileType(path)).send(path.stream());
875
873
  }
876
- if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
874
+ if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) {
875
+ return new Response(null, { status: 404 });
876
+ }
877
877
  try {
878
878
  const fs = await import("fs");
879
879
  const ext = path.split(".").pop();
@@ -882,23 +882,55 @@ var Reply = class {
882
882
  return this.type(ext).send(stream);
883
883
  } catch (error) {
884
884
  if (error.code === "ENOENT" || error.code === "EISDIR") {
885
- return this.status(404).send();
885
+ return new Response(null, { status: 404 });
886
886
  }
887
887
  throw error;
888
888
  }
889
889
  }
890
- send(body = "") {
890
+ // Accepts everything a route can return, so `send(x)` and `return x` agree.
891
+ // Async because a bucket file has to be read before its status is known;
892
+ // routes await whatever they return, so this is invisible in normal use.
893
+ async send(input = "") {
891
894
  const { status: status2 = 200, headers: headers2 } = this.res;
895
+ let body = input;
892
896
  if (status2 === 101 || status2 === 204 || status2 === 205 || status2 === 304) {
893
897
  return new Response(null, { status: status2, headers: headers2 });
894
898
  }
895
899
  if (body === null) body = "";
900
+ if (typeof body?.then === "function") body = await body;
896
901
  if (typeof body === "function") body = body();
897
902
  if (typeof body?.then === "function") {
898
903
  throw new Error(
899
- "send() received a promise, likely an async component. Await it first, or return it from the route, which resolves it for you."
904
+ "Cannot render an async component: components must be synchronous. Await the data before rendering and pass it in as props."
900
905
  );
901
906
  }
907
+ if (body instanceof _Reply) body = await body.send();
908
+ if (body instanceof Response) {
909
+ const merged = new Headers(body.headers);
910
+ for (const [key, value] of headers2) {
911
+ if (key === "set-cookie") continue;
912
+ merged.set(key, value);
913
+ }
914
+ for (const cookie of headers2.getSetCookie?.() ?? []) {
915
+ merged.append("set-cookie", cookie);
916
+ }
917
+ if (body.url && /^(br|gzip)$/.test(merged.get("content-encoding") || "")) {
918
+ merged.delete("content-encoding");
919
+ }
920
+ return new Response(body.body, {
921
+ status: this.res.status ?? body.status,
922
+ headers: merged
923
+ });
924
+ }
925
+ if (body && typeof body.stream === "function" && typeof body.bytes === "function" && typeof body.exists === "function" && typeof body.name === "string") {
926
+ return this.file(body);
927
+ }
928
+ if (body instanceof Blob) {
929
+ if (!headers2.get("content-type") && body.type) {
930
+ headers2.set("content-type", body.type);
931
+ }
932
+ return new Response(body, { status: status2, headers: headers2 });
933
+ }
902
934
  if (typeof body === "string") {
903
935
  if (!headers2.get("content-type")) {
904
936
  headers2.set("content-type", isHtml(body) ? mimes_default.html : mimes_default.text);
@@ -924,6 +956,12 @@ var Reply = class {
924
956
  if (isReadableStream(body)) {
925
957
  return new Response(toWeb(body), { status: status2, headers: headers2 });
926
958
  }
959
+ if (!Array.isArray(body) && body?.[Symbol.iterator]) {
960
+ return new Response(iteratorToReadable(body), { status: status2, headers: headers2 });
961
+ }
962
+ if (body?.[Symbol.asyncIterator]) {
963
+ return new Response(iteratorAsyncToReadable(body), { status: status2, headers: headers2 });
964
+ }
927
965
  if (!headers2.get("content-type")) {
928
966
  headers2.set("content-type", "application/json");
929
967
  }
@@ -1039,48 +1077,11 @@ function findSessionId(ctx) {
1039
1077
  return ctx.cookies.session || void 0;
1040
1078
  }
1041
1079
 
1042
- // src/middle/session.ts
1043
- var loaded = /* @__PURE__ */ new WeakMap();
1044
- function noSession(error) {
1045
- const target = {};
1046
- return new Proxy(target, {
1047
- get(target2, key) {
1048
- if (typeof key === "symbol" || key === "then") return target2[key];
1049
- throw error(String(key));
1050
- },
1051
- set(target2, key, value) {
1052
- if (typeof key === "symbol") {
1053
- target2[key] = value;
1054
- return true;
1055
- }
1056
- throw error(String(key));
1057
- }
1058
- });
1059
- }
1060
- async function session(ctx) {
1061
- const strategy = ctx.options.auth?.strategy;
1062
- if (strategy?.includes("jwt")) {
1063
- ctx.session = noSession((key) => ServerError_default.SESSION_JWT({ key }));
1064
- return;
1065
- }
1066
- const id = findSessionId(ctx);
1067
- if (!id && strategy?.includes("token")) {
1068
- ctx.session = noSession((key) => ServerError_default.SESSION_GUEST({ key }));
1069
- return;
1070
- }
1071
- ctx.session = id && await ctx.options.sessions.get(id) || {};
1072
- loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
1073
- }
1074
-
1075
1080
  // src/auth/finishLogin.ts
1076
1081
  async function finishLogin(ctx, input, opts = {}) {
1077
1082
  const settings = ctx.options.auth;
1078
1083
  const { strategy, onLogin, onUser, onToken } = settings;
1079
1084
  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
- }
1084
1085
  const auth2 = {
1085
1086
  user: key,
1086
1087
  provider: input.provider,
@@ -1106,12 +1107,10 @@ async function finishLogin(ctx, input, opts = {}) {
1106
1107
  assertUser(exposed, "onUser");
1107
1108
  return status(201).json({ ...exposed, token });
1108
1109
  }
1109
- const prev = loaded.get(ctx);
1110
- if (prev?.id) await ctx.options.sessions.del(prev.id);
1110
+ const prev = findSessionId(ctx);
1111
+ if (prev) await settings.sessions.del(prev);
1111
1112
  const id = createId();
1112
- Object.assign(ctx.session, auth2);
1113
- await ctx.options.sessions.set(id, ctx.session);
1114
- loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
1113
+ await settings.sessions.set(id, auth2);
1115
1114
  if (strategy.includes("token")) {
1116
1115
  const exposed = await onUser(user, ctx);
1117
1116
  assertUser(exposed, "onUser");
@@ -1631,6 +1630,7 @@ function parseAuthOptions(auth2) {
1631
1630
  const onUser = auth2.onUser || defaultOnUser;
1632
1631
  const onToken = auth2.onToken || defaultOnUser;
1633
1632
  const users = auth2.users ? toStore(auth2.users) : null;
1633
+ const sessions = auth2.sessions ? toStoreExpiring(auth2.sessions, "1w") : null;
1634
1634
  return {
1635
1635
  strategy,
1636
1636
  providers: list,
@@ -1640,7 +1640,8 @@ function parseAuthOptions(auth2) {
1640
1640
  onUser,
1641
1641
  onToken,
1642
1642
  onLogout,
1643
- users
1643
+ users,
1644
+ sessions
1644
1645
  };
1645
1646
  }
1646
1647
 
@@ -1830,7 +1831,8 @@ function config(options = {}) {
1830
1831
  const level = raw === true ? "info" : raw === false ? void 0 : raw;
1831
1832
  const log = createLogger(level);
1832
1833
  const settings = {
1833
- port: options.port || env2.PORT || 3e3,
1834
+ // `env.PORT` is a string, so coerce it: `settings.port` is a number
1835
+ port: options.port || Number(env2.PORT) || 3e3,
1834
1836
  secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
1835
1837
  log,
1836
1838
  // How request bodies are read: parsed into ctx.body by default; `raw` keeps
@@ -1838,11 +1840,7 @@ function config(options = {}) {
1838
1840
  parser: options.parser ?? "parse",
1839
1841
  // Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
1840
1842
  // the added headers off; see resolveSecurity for the defaults.
1841
- security: resolveSecurity(options.security),
1842
- // Sessions: one record per device, exposed as ctx.session. Anything
1843
- // polystore accepts works; raw sources (a Map, a Redis client) get a 1w
1844
- // expiry, a built store is honored as-is, prefix and expiry included.
1845
- sessions: toStoreExpiring(options.sessions ?? /* @__PURE__ */ new Map(), "1w")
1843
+ security: resolveSecurity(options.security)
1846
1844
  };
1847
1845
  if (options.cache !== void 0) settings.cache = options.cache;
1848
1846
  options.cors = options.cors || env2.CORS || null;
@@ -1885,10 +1883,10 @@ function config(options = {}) {
1885
1883
  settings.public = publicDir ? bucket(publicDir) : null;
1886
1884
  settings.uploads = resolveUploads(options.uploads);
1887
1885
  const production = env2.NODE_ENV === "production";
1888
- const defaulted = options.sessions == null;
1889
- settings.sessionsDefault = defaulted;
1890
1886
  if (options.auth || env2.AUTH) {
1891
- settings.auth = parseAuthOptions(options.auth || env2.AUTH || null);
1887
+ settings.auth = parseAuthOptions(
1888
+ options.auth || env2.AUTH || null
1889
+ );
1892
1890
  }
1893
1891
  if (settings.auth) {
1894
1892
  if (!settings.auth.users) {
@@ -1899,10 +1897,13 @@ function config(options = {}) {
1899
1897
  }
1900
1898
  settings.auth.users = toStore(/* @__PURE__ */ new Map());
1901
1899
  }
1902
- if (production && defaulted && !settings.auth.strategy.includes("jwt")) {
1903
- throw new Error(
1904
- "Auth in production needs a persistent `sessions` store, like sessions: kv(redis).prefix('session:')."
1905
- );
1900
+ if (!settings.auth.sessions && !settings.auth.strategy.includes("jwt")) {
1901
+ if (production) {
1902
+ throw new Error(
1903
+ "Auth in production needs a persistent `sessions` store, like auth: { ..., sessions: kv(redis).prefix('session:') }."
1904
+ );
1905
+ }
1906
+ settings.auth.sessions = toStoreExpiring(/* @__PURE__ */ new Map(), "1w");
1906
1907
  }
1907
1908
  }
1908
1909
  if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
@@ -1928,7 +1929,6 @@ function config(options = {}) {
1928
1929
  }
1929
1930
  if (settings.public) log.message("public", loc(options.public));
1930
1931
  if (settings.uploads) log.message("uploads", loc(options.uploads));
1931
- if (options.sessions) log.message("sessions", "enabled");
1932
1932
  if (settings.cors) {
1933
1933
  const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
1934
1934
  log.message("cors", origin);
@@ -2038,74 +2038,17 @@ function getMachine() {
2038
2038
  }
2039
2039
 
2040
2040
  // src/parseResponse.ts
2041
- var warned = false;
2042
- var warnDefault = () => {
2043
- if (warned) return;
2044
- warned = true;
2045
- console.warn(
2046
- "[server:sessions] Using the default in-memory session store in production: sessions are lost on restart and not shared across instances. Configure one with sessions: kv(redis).prefix('session:')."
2047
- );
2048
- };
2049
2041
  async function parseResponse(out, ctx) {
2050
2042
  if (!out && typeof out !== "string") return null;
2051
2043
  if (typeof out === "function") {
2052
2044
  out = await out(ctx);
2053
- }
2054
- if (out && typeof out.send === "function" && out.res?.headers instanceof Headers) {
2055
- out = out.send();
2056
- }
2057
- if (out instanceof Blob) {
2058
- out = new Response(out, { headers: { "Content-Type": out.type } });
2059
- }
2060
- if (out && typeof out.stream === "function" && typeof out.bytes === "function" && typeof out.exists === "function" && typeof out.name === "string") {
2061
- if (!await out.exists()) {
2062
- out = new Response(null, { status: 404 });
2063
- } else {
2064
- const type2 = fileType(out);
2065
- out = new Response(
2066
- out.stream(),
2067
- type2 ? { headers: { "content-type": type2 } } : void 0
2068
- );
2069
- }
2070
- }
2071
- if (out instanceof ReadableStream) {
2072
- out = new Response(out);
2073
- }
2074
- if (out instanceof Uint8Array) {
2075
- out = new Response(out);
2045
+ if (!out && typeof out !== "string") return null;
2076
2046
  }
2077
2047
  if (typeof out === "number") {
2078
- out = new Response(void 0, { status: out });
2079
- }
2080
- if (typeof out === "string") {
2081
- const type2 = isHtml(out) ? mimes_default.html : mimes_default.text;
2082
- out = new Response(out, {
2083
- headers: {
2084
- "content-type": type2,
2085
- "content-length": String(Buffer.byteLength(out))
2086
- }
2087
- });
2088
- }
2089
- if (out?.constructor === Object || Array.isArray(out)) {
2090
- out = json(out);
2091
- }
2092
- if (out[Symbol.iterator]) {
2093
- out = new Response(iteratorToReadable(out));
2048
+ out = new Response(null, { status: out });
2094
2049
  }
2095
- if (out[Symbol.asyncIterator] && !(out instanceof Response)) {
2096
- out = new Response(iteratorAsyncToReadable(out));
2097
- }
2098
- if (out instanceof Response && out.url && out.body) {
2099
- out = new Response(out.body, {
2100
- status: out.status,
2101
- headers: out.headers
2102
- });
2103
- if (/^(br|gzip)$/.test(out.headers.get("content-encoding") || "")) {
2104
- out.headers.delete("content-encoding");
2105
- }
2106
- }
2107
- if (!(out instanceof Response)) {
2108
- throw new Error(`Invalid response type ${out}`);
2050
+ if (!(out instanceof Response) || out.url) {
2051
+ out = await send(out);
2109
2052
  }
2110
2053
  applyCors(out, ctx);
2111
2054
  applySecurity(out, ctx);
@@ -2113,27 +2056,6 @@ async function parseResponse(out, ctx) {
2113
2056
  if (ctx.time?.times?.length > 1) {
2114
2057
  out.headers.set("Server-Timing", ctx.time.headers());
2115
2058
  }
2116
- const prev = loaded.get(ctx);
2117
- if (prev && JSON.stringify(ctx.session ?? {}) !== prev.data) {
2118
- if (ctx.options.sessionsDefault && ctx.platform.production) {
2119
- warnDefault();
2120
- }
2121
- let id = prev.id;
2122
- if (!id) {
2123
- id = createId();
2124
- out.headers.append(
2125
- "set-cookie",
2126
- createCookies("session", {
2127
- value: id,
2128
- path: "/",
2129
- httpOnly: true,
2130
- secure: ctx.platform.production,
2131
- sameSite: "Lax"
2132
- })
2133
- );
2134
- }
2135
- ctx.options.sessions.set(id, ctx.session);
2136
- }
2137
2059
  return out;
2138
2060
  }
2139
2061
 
@@ -2294,23 +2216,30 @@ import * as crypto2 from "crypto";
2294
2216
  import { getRandomValues } from "crypto";
2295
2217
  import { promisify } from "util";
2296
2218
  async function hash2(password) {
2297
- if ("argon2" in crypto2) {
2298
- const argon23 = promisify(crypto2.argon2);
2299
- const buf = await argon23("argon2id", {
2300
- message: Buffer.from(password),
2301
- nonce: getRandomValues(new Uint8Array(16)),
2302
- parallelism: 4,
2303
- tagLength: 64,
2304
- memory: 65536,
2305
- passes: 3
2219
+ if ("Bun" in globalThis) {
2220
+ return await Bun.password.hash(password, {
2221
+ algorithm: "argon2id",
2222
+ memoryCost: 65536,
2223
+ timeCost: 3
2306
2224
  });
2307
- return buf.toString("base64");
2308
2225
  }
2309
- return await Bun.password.hash(password, {
2310
- algorithm: "argon2id",
2311
- memoryCost: 65536,
2312
- timeCost: 3
2226
+ if (!("argon2" in crypto2)) {
2227
+ throw new Error(
2228
+ "Password hashing needs argon2: run on Bun, or on Node 24+ where node:crypto provides it."
2229
+ );
2230
+ }
2231
+ const nonce = getRandomValues(new Uint8Array(32));
2232
+ const argon23 = promisify(crypto2.argon2);
2233
+ const buf = await argon23("argon2id", {
2234
+ message: Buffer.from(password),
2235
+ nonce,
2236
+ parallelism: 1,
2237
+ tagLength: 32,
2238
+ memory: 65536,
2239
+ passes: 3
2313
2240
  });
2241
+ const b64 = (bytes) => Buffer.from(bytes).toString("base64").replace(/=+$/, "");
2242
+ return `$argon2id$v=19$m=65536,t=3,p=1$${b64(nonce)}$${b64(buf)}`;
2314
2243
  }
2315
2244
 
2316
2245
  // src/helpers/iteratorAsyncToReadable.ts
@@ -2405,16 +2334,6 @@ function toWeb(nodeStream) {
2405
2334
 
2406
2335
  // src/helpers/verify.ts
2407
2336
  import * as crypto3 from "crypto";
2408
- function timingSafeEqual(a, b) {
2409
- const len = Math.max(a.length, b.length);
2410
- let mismatch = a.length ^ b.length;
2411
- for (let i = 0; i < len; i++) {
2412
- const ca = a.charCodeAt(i) || 0;
2413
- const cb = b.charCodeAt(i) || 0;
2414
- mismatch |= ca ^ cb;
2415
- }
2416
- return mismatch === 0;
2417
- }
2418
2337
  async function verify2(password, hash3) {
2419
2338
  if ("Bun" in globalThis) {
2420
2339
  return Bun.password.verify(password, hash3, "argon2id");
@@ -2439,11 +2358,8 @@ async function verify2(password, hash3) {
2439
2358
  },
2440
2359
  (err, derivedKey) => {
2441
2360
  if (err) return reject(err);
2442
- if (derivedKey.length === expected.length && timingSafeEqual(derivedKey, expected)) {
2443
- resolve(true);
2444
- } else {
2445
- resolve(false);
2446
- }
2361
+ if (derivedKey.length !== expected.length) return resolve(false);
2362
+ resolve(crypto3.timingSafeEqual(derivedKey, expected));
2447
2363
  }
2448
2364
  );
2449
2365
  });
@@ -2472,14 +2388,10 @@ async function getJwtUser(ctx) {
2472
2388
  return exposed;
2473
2389
  }
2474
2390
  async function getAuthSession(ctx) {
2475
- if (loaded.has(ctx)) {
2476
- const session3 = ctx.session;
2477
- return session3?.user ? session3 : void 0;
2478
- }
2479
2391
  const id = findSessionId(ctx);
2480
2392
  if (!id) return;
2481
- const session2 = await ctx.options.sessions.get(id);
2482
- return session2?.user ? session2 : void 0;
2393
+ const session = await ctx.options.auth.sessions.get(id);
2394
+ return session?.user ? session : void 0;
2483
2395
  }
2484
2396
  async function getUser(ctx) {
2485
2397
  if (!ctx.options.auth) return;
@@ -2504,10 +2416,8 @@ async function getUser(ctx) {
2504
2416
  async function logout(ctx) {
2505
2417
  const { strategy } = ctx.options.auth;
2506
2418
  if (!strategy.includes("jwt")) {
2507
- const prev = loaded.get(ctx);
2508
- if (prev?.id) await ctx.options.sessions.del(prev.id);
2509
- ctx.session = {};
2510
- loaded.set(ctx, { id: void 0, data: "{}" });
2419
+ const prev = findSessionId(ctx);
2420
+ if (prev) await ctx.options.auth.sessions.del(prev);
2511
2421
  }
2512
2422
  if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
2513
2423
  if (strategy.includes("token") || strategy.includes("jwt")) {
@@ -3034,7 +2944,6 @@ async function createNode(req, app, signal = new AbortController().signal) {
3034
2944
  headers: headers2,
3035
2945
  cookies: cookies2,
3036
2946
  signal,
3037
- session: {},
3038
2947
  init,
3039
2948
  app,
3040
2949
  ip: clientIp(headers2, {
@@ -3075,7 +2984,6 @@ async function createWinter(req, app, server2) {
3075
2984
  headers: headers2,
3076
2985
  cookies: cookies2,
3077
2986
  signal: req.signal,
3078
- session: {},
3079
2987
  init,
3080
2988
  app,
3081
2989
  ip: clientIp(headers2, {
@@ -3326,7 +3234,6 @@ var Server = class extends Router {
3326
3234
  app.use(timer);
3327
3235
  if (this.settings.cors) app.use(preflight);
3328
3236
  app.use(assets);
3329
- app.use(session);
3330
3237
  if (this.settings.auth) {
3331
3238
  auth(app);
3332
3239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.44.1",
3
+ "version": "0.45.1",
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",
@@ -25,7 +25,8 @@
25
25
  "types": "index.d.ts",
26
26
  "files": [
27
27
  "index.d.ts",
28
- "src/jsx/"
28
+ "src/jsx/*.js",
29
+ "src/jsx/jsx-runtime.d.ts"
29
30
  ],
30
31
  "exports": {
31
32
  ".": "./index.js",
@@ -45,6 +46,7 @@
45
46
  "menu": {
46
47
  "Documentation": "/documentation",
47
48
  "Tutorials": "/tutorials",
49
+ "Donate": "https://www.paypal.com/paypalme/franciscopresencia/19",
48
50
  "Github": "https://github.com/franciscop/server-next"
49
51
  },
50
52
  "documentation": {
package/readme.md CHANGED
@@ -15,16 +15,24 @@ export default server({ uploads: './uploads' })
15
15
  .post('/avatar', (ctx) => ctx.body.avatar.path);
16
16
  ```
17
17
 
18
- Key-value stores and file storage come included, so `sessions` work out of the box and `uploads` takes a folder path. For Redis, S3 and the rest, pass the client straight in:
18
+ Key-value stores and file storage come included, so logins work out of the box and `uploads` takes a folder path. For Redis, S3 and the rest, pass the client straight in:
19
19
 
20
20
  ```js
21
- import server, { bucket } from '@server/next';
21
+ import server, { bucket, kv } from '@server/next';
22
22
  import { createClient } from 'redis';
23
23
 
24
- const sessions = createClient({ url });
24
+ const redis = kv(createClient({ url }));
25
25
  const uploads = bucket.S3('my-bucket', { id, key });
26
26
 
27
- export default server({ sessions, uploads });
27
+ export default server({
28
+ uploads,
29
+ auth: {
30
+ strategy: 'cookie',
31
+ providers: ['github'],
32
+ users: redis.prefix('user:'),
33
+ sessions: redis.prefix('session:'),
34
+ },
35
+ });
28
36
  ```
29
37
 
30
38
  See the [full documentation](https://serverjs.io/documentation).