@server/next 0.32.0 → 0.34.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 -12
  2. package/index.js +64 -58
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -104,17 +104,16 @@ type AuthUser<T = Record<string, any>> = T & {
104
104
  strategy: Strategy;
105
105
  email: string;
106
106
  };
107
- type ProviderString = Provider | `${Provider}|${Provider}`;
108
- type AuthOption = `${Strategy}:${Provider | ProviderString}` | {
109
- provider: Provider | ProviderString | Provider[];
107
+ type AuthOption = `${Strategy}:${Provider}` | {
110
108
  strategy: Strategy;
109
+ providers: Provider | Provider[];
111
110
  session?: KVStore;
112
111
  store?: KVStore;
113
112
  redirect?: string;
114
113
  cleanUser?: <T = AuthUser>(user: T) => T | Promise<T>;
115
114
  };
116
115
  type AuthSettings = {
117
- provider: Provider[];
116
+ providers: Provider[];
118
117
  strategy: Strategy;
119
118
  store: KVStore;
120
119
  session: KVStore;
@@ -130,9 +129,20 @@ type Logger = {
130
129
  };
131
130
  type SecurityOptions = {
132
131
  trustProxy?: boolean;
132
+ frameguard?: boolean | string;
133
+ noSniff?: boolean;
134
+ referrerPolicy?: boolean | string;
135
+ hsts?: boolean | string;
136
+ xssProtection?: boolean;
137
+ csp?: boolean | string;
138
+ coop?: boolean | string;
139
+ corp?: boolean | string;
140
+ permissionsPolicy?: string;
133
141
  };
134
142
  type SecuritySettings = {
135
143
  trustProxy: boolean;
144
+ headers: Record<string, string>;
145
+ hsts: string | null;
136
146
  };
137
147
  type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
138
148
  type Options = {
@@ -151,7 +161,7 @@ type Options = {
151
161
  onError?: OnError;
152
162
  log?: LogLevel | boolean;
153
163
  favicon?: string | Bucket;
154
- security?: SecurityOptions;
164
+ security?: boolean | SecurityOptions;
155
165
  body?: BodyMode;
156
166
  };
157
167
  type Settings = {
@@ -197,11 +207,6 @@ type PathToParams<Path extends string> = ParamsToObject<ExtractPathParams<Path>>
197
207
  type BunEnv = Record<string, string> & {
198
208
  upgrade?: (req: Request) => boolean;
199
209
  };
200
- type EventCallback = (data: Context & SerializableValue) => void;
201
- type Events = Record<string, EventCallback[]> & {
202
- on?: (key: string, cb: (value?: Context & SerializableValue) => void) => void;
203
- trigger?: (key: string, value?: Partial<Context & SerializableValue>) => void;
204
- };
205
210
  type Context<Params extends Record<string, string | undefined> = Record<string, string>, O extends ServerConfig = object> = {
206
211
  method: Method;
207
212
  ip: string;
@@ -224,7 +229,6 @@ type Context<Params extends Record<string, string | undefined> = Record<string,
224
229
  User?: infer U;
225
230
  } ? U extends Record<"User", infer Inner> ? Inner : AuthUser : AuthUser;
226
231
  init: number;
227
- events: Events;
228
232
  req?: Request;
229
233
  res?: Response & {
230
234
  cookies?: Record<string, string>;
@@ -458,4 +462,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
458
462
  }
459
463
  declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
460
464
 
461
- export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type EventCallback, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
465
+ export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type Bucket, type BunEnv, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type InferParamType, type InlineReply, type KVStore, type LimitOptions, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, UploadPipeline, type UploadedFile, cookies, server as default, download, file, headers, json, redirect, router, send, status, type, upload };
package/index.js CHANGED
@@ -54,7 +54,7 @@ ServerError_default.extend({
54
54
  message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
55
55
  },
56
56
  AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
57
- AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
57
+ AUTH_NO_PROVIDER: "No provider passed to the option 'auth.providers'",
58
58
  AUTH_INVALID_PROVIDER: {
59
59
  status: 401,
60
60
  message: "Invalid provider '{provider}', valid ones are: '{valid}'"
@@ -1113,37 +1113,27 @@ function defaultCleanUser(fullUser) {
1113
1113
  const { password: _password, ...user } = fullUser;
1114
1114
  return user;
1115
1115
  }
1116
- var providersKeys = Object.keys(providers_default);
1117
- function getProviders(provider) {
1118
- if (typeof provider === "string") {
1119
- provider = provider.split("|");
1120
- }
1121
- const invalidProvider = provider.find((p) => !providersKeys.includes(p));
1122
- if (invalidProvider) {
1123
- throw new Error(
1124
- `Provider "${invalidProvider}" not found, available ones are "${providersKeys.join('", "')}"`
1125
- );
1126
- }
1127
- return provider;
1128
- }
1116
+ var available = Object.keys(providers_default);
1129
1117
  function parseAuthOptions(auth2, all) {
1130
1118
  if (!auth2) return null;
1131
1119
  if (typeof auth2 === "string") {
1132
- const [strategy2, providerRaw] = auth2.split(":");
1133
- const provider2 = providerRaw && providerRaw.split("|");
1134
- auth2 = { strategy: strategy2, provider: provider2 };
1135
- }
1136
- if (!auth2.strategy) {
1137
- throw new Error("Auth options needs a strategy");
1120
+ const [strategy2, provider] = auth2.split(":");
1121
+ auth2 = { strategy: strategy2, providers: provider ? [provider] : [] };
1138
1122
  }
1139
- if (!auth2.strategy.length) {
1123
+ if (!auth2.strategy?.length) {
1140
1124
  throw new Error("Auth options needs a strategy");
1141
1125
  }
1142
1126
  const strategy = auth2.strategy;
1143
- if (!auth2.provider?.length) {
1127
+ const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
1128
+ if (!list.length) {
1144
1129
  throw new Error("Auth options needs a provider");
1145
1130
  }
1146
- const provider = getProviders(auth2.provider);
1131
+ const invalid = list.find((p) => !available.includes(p));
1132
+ if (invalid) {
1133
+ throw new Error(
1134
+ `Provider "${invalid}" not found, available ones are "${available.join('", "')}"`
1135
+ );
1136
+ }
1147
1137
  const redirect2 = auth2.redirect || defaultRedirect;
1148
1138
  const cleanUser = auth2.cleanUser || defaultCleanUser;
1149
1139
  if (!auth2.store && !all.store) {
@@ -1155,13 +1145,10 @@ function parseAuthOptions(auth2, all) {
1155
1145
  const store = auth2.store || all.store.prefix("user:");
1156
1146
  const session2 = auth2.session || all.store.prefix("auth:");
1157
1147
  return {
1158
- // Base main configuration
1159
1148
  strategy,
1160
- provider,
1161
- // Extra configuration
1149
+ providers: list,
1162
1150
  redirect: redirect2,
1163
1151
  cleanUser,
1164
- // Stores for the auth session and users
1165
1152
  store,
1166
1153
  session: session2
1167
1154
  };
@@ -1371,6 +1358,47 @@ function createLogger(level) {
1371
1358
  };
1372
1359
  }
1373
1360
 
1361
+ // src/helpers/security.ts
1362
+ function resolveSecurity(security) {
1363
+ const off = security === false;
1364
+ const o = security && typeof security === "object" ? security : {};
1365
+ const val = (v, def) => v === false ? null : v === true || v == null ? def : v;
1366
+ const map2 = off ? {} : {
1367
+ "x-frame-options": val(o.frameguard, "SAMEORIGIN"),
1368
+ "x-content-type-options": o.noSniff === false ? null : "nosniff",
1369
+ "referrer-policy": val(
1370
+ o.referrerPolicy,
1371
+ "strict-origin-when-cross-origin"
1372
+ ),
1373
+ "x-xss-protection": o.xssProtection === false ? null : "0",
1374
+ // Opt-in: default off
1375
+ "content-security-policy": val(o.csp, null),
1376
+ "cross-origin-opener-policy": val(o.coop, null),
1377
+ "cross-origin-resource-policy": val(o.corp, null),
1378
+ "permissions-policy": o.permissionsPolicy ?? null
1379
+ };
1380
+ const headers2 = {};
1381
+ for (const key in map2) {
1382
+ const value = map2[key];
1383
+ if (value) headers2[key] = value;
1384
+ }
1385
+ return {
1386
+ trustProxy: o.trustProxy ?? true,
1387
+ headers: headers2,
1388
+ hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
1389
+ };
1390
+ }
1391
+ function applySecurity(res, ctx) {
1392
+ const security = ctx.options.security;
1393
+ if (!security) return;
1394
+ for (const key in security.headers) {
1395
+ if (!res.headers.has(key)) res.headers.set(key, security.headers[key]);
1396
+ }
1397
+ if (security.hsts && ctx.platform.production && !res.headers.has("strict-transport-security")) {
1398
+ res.headers.set("strict-transport-security", security.hsts);
1399
+ }
1400
+ }
1401
+
1374
1402
  // src/helpers/config.ts
1375
1403
  function config(options = {}) {
1376
1404
  const env2 = globalThis.env;
@@ -1384,11 +1412,9 @@ function config(options = {}) {
1384
1412
  // How request bodies are read: parsed into ctx.body by default; `raw` keeps
1385
1413
  // the Buffer, `stream` hands the handler the unread web ReadableStream.
1386
1414
  body: options.body ?? "parse",
1387
- // Trust X-Forwarded-* headers for ctx.ip (on by default; set it to false
1388
- // when clients connect directly so a client can't spoof its IP).
1389
- security: {
1390
- trustProxy: options.security?.trustProxy ?? true
1391
- }
1415
+ // Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
1416
+ // the added headers off; see resolveSecurity for the defaults.
1417
+ security: resolveSecurity(options.security)
1392
1418
  };
1393
1419
  options.cors = options.cors || env2.CORS || null;
1394
1420
  if (options.cors) {
@@ -1452,7 +1478,7 @@ function config(options = {}) {
1452
1478
  });
1453
1479
  const loc = (v) => typeof v === "string" ? v : "enabled";
1454
1480
  if (settings.auth) {
1455
- log.message("auth", `${settings.auth.provider.join(", ")} auth enabled`);
1481
+ log.message("auth", `${settings.auth.providers.join(", ")} auth enabled`);
1456
1482
  }
1457
1483
  if (settings.public) log.message("public", loc(options.public));
1458
1484
  if (settings.uploads) log.message("uploads", loc(options.uploads));
@@ -1613,6 +1639,7 @@ async function parseResponse(out, ctx) {
1613
1639
  throw new Error(`Invalid response type ${out}`);
1614
1640
  }
1615
1641
  applyCors(out, ctx);
1642
+ applySecurity(out, ctx);
1616
1643
  if (ctx.time?.times?.length > 1) {
1617
1644
  out.headers.set("Server-Timing", ctx.time.headers());
1618
1645
  }
@@ -1769,6 +1796,7 @@ async function getResponse(app, ctx) {
1769
1796
  } catch (error) {
1770
1797
  const res = await ctx.options.onError(error, ctx);
1771
1798
  applyCors(res, ctx);
1799
+ applySecurity(res, ctx);
1772
1800
  return res;
1773
1801
  }
1774
1802
  }
@@ -2062,10 +2090,10 @@ async function getUser(ctx) {
2062
2090
  valid: options.strategy
2063
2091
  });
2064
2092
  }
2065
- if (!options.provider.includes(auth2.provider)) {
2093
+ if (!options.providers.includes(auth2.provider)) {
2066
2094
  throw ServerError_default.AUTH_INVALID_PROVIDER({
2067
2095
  provider: auth2.provider,
2068
- valid: options.provider
2096
+ valid: options.providers
2069
2097
  });
2070
2098
  }
2071
2099
  const user = await ctx.options.auth.store.get(auth2.user);
@@ -2109,7 +2137,7 @@ function auth(app) {
2109
2137
  ctx.user = await getUser(ctx);
2110
2138
  });
2111
2139
  app.post("/auth/logout", logout);
2112
- const enabled = app.settings.auth.provider;
2140
+ const enabled = app.settings.auth.providers;
2113
2141
  for (const name of oauth2) {
2114
2142
  if (!enabled.includes(name)) continue;
2115
2143
  const key = name.toUpperCase();
@@ -2384,22 +2412,6 @@ function timer(ctx) {
2384
2412
  // src/context/node.ts
2385
2413
  import { TLSSocket } from "tls";
2386
2414
 
2387
- // src/context/createEvents.ts
2388
- function createEvents() {
2389
- const events = {};
2390
- events.on = (name, callback3) => {
2391
- events[name] = events[name] || [];
2392
- events[name].push(callback3);
2393
- };
2394
- events.trigger = (name, data) => {
2395
- if (!events[name]) return;
2396
- for (const cb of events[name]) {
2397
- cb(data);
2398
- }
2399
- };
2400
- return events;
2401
- }
2402
-
2403
2415
  // src/context/isValidMethod.ts
2404
2416
  var methods = [
2405
2417
  "get",
@@ -2443,7 +2455,6 @@ async function createNode(req, app) {
2443
2455
  }),
2444
2456
  getStream: () => toWeb(req)
2445
2457
  };
2446
- const events = createEvents();
2447
2458
  const ctx = {
2448
2459
  options: app.settings,
2449
2460
  platform: app.platform,
@@ -2454,7 +2465,6 @@ async function createNode(req, app) {
2454
2465
  cookies: cookies2,
2455
2466
  session: {},
2456
2467
  init,
2457
- events,
2458
2468
  app,
2459
2469
  ip: clientIp(headers2, {
2460
2470
  remoteAddress: req.socket.remoteAddress || "",
@@ -2485,7 +2495,6 @@ async function createWinter(req, app, server2) {
2485
2495
  getBuffer: async () => Buffer.from(await req.arrayBuffer()),
2486
2496
  getStream: () => req.body ?? void 0
2487
2497
  };
2488
- const events = createEvents();
2489
2498
  const ctx = {
2490
2499
  options: app.settings,
2491
2500
  platform: app.platform,
@@ -2496,7 +2505,6 @@ async function createWinter(req, app, server2) {
2496
2505
  cookies: cookies2,
2497
2506
  session: {},
2498
2507
  init,
2499
- events,
2500
2508
  app,
2501
2509
  ip: clientIp(headers2, {
2502
2510
  remoteAddress: server2?.requestIP?.(req)?.address || "",
@@ -2513,7 +2521,6 @@ var Winter = async (app, request, env2) => {
2513
2521
  Object.assign(globalThis.env, env2);
2514
2522
  const ctx = await createWinter(request, app, env2);
2515
2523
  const res = await handleRequest(app, ctx);
2516
- ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
2517
2524
  return res;
2518
2525
  };
2519
2526
  var Node = async (app) => {
@@ -2545,7 +2552,6 @@ var Netlify = async (app, request, context) => {
2545
2552
  }
2546
2553
  const ctx = await createWinter(request, app);
2547
2554
  const res = await handleRequest(app, ctx);
2548
- ctx.events.trigger("finish", { ...ctx, res, end: performance.now() });
2549
2555
  return res;
2550
2556
  };
2551
2557
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.32.0",
3
+ "version": "0.34.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",