@server/next 0.47.1 → 0.48.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 +6 -11
  2. package/index.js +94 -95
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -203,7 +203,7 @@ type RedirectOption = string | ((user: any, ctx: Context) => Awaitable<string>)
203
203
  };
204
204
  type AuthConfig<U = AuthProfile> = {
205
205
  providers: string | readonly string[] | Record<string, string | ProviderOptions>;
206
- strategy?: Strategy | readonly Strategy[];
206
+ strategy?: Strategy;
207
207
  expires?: string;
208
208
  redirect?: RedirectOption;
209
209
  onLogin?: (profile: AuthProfile, ctx: Context) => Awaitable<string | number | undefined>;
@@ -212,7 +212,7 @@ type AuthConfig<U = AuthProfile> = {
212
212
  onLogout?: (id: string, ctx: Context) => Awaitable<void>;
213
213
  };
214
214
  type AuthVerify<U = AuthClaims> = {
215
- verify: string;
215
+ issuer: string;
216
216
  audience: string | readonly string[];
217
217
  cookie?: string;
218
218
  audienceClaim?: string | readonly string[];
@@ -224,11 +224,11 @@ type AuthInstance = {
224
224
  user?: (ctx: Context) => Awaitable<any>;
225
225
  };
226
226
  type AuthFunction<U = any> = (ctx: Context<any>) => Awaitable<U>;
227
- type AuthOption = string | AuthFunction | AuthConfig<any> | AuthVerify<any> | AuthInstance | readonly AuthOption[];
228
- type UserOf<A> = A extends readonly (infer M)[] ? UserOf<M> : A extends (...args: any[]) => infer R ? NonNullable<Awaited<R>> : A extends {
227
+ type AuthOption = string | AuthFunction | AuthConfig<any> | AuthVerify<any> | AuthInstance;
228
+ type UserOf<A> = A extends (...args: any[]) => infer R ? NonNullable<Awaited<R>> : A extends {
229
229
  getUser: (...args: any[]) => infer R;
230
230
  } ? NonNullable<Awaited<R>> : A extends {
231
- verify: any;
231
+ issuer: any;
232
232
  } ? AuthClaims : A extends {
233
233
  providers: any;
234
234
  } ? AuthProfile : A extends string ? AuthProfile : never;
@@ -237,7 +237,7 @@ type AuthEntry = {
237
237
  user: (ctx: Context) => Promise<any>;
238
238
  routes?: (app: Server) => void;
239
239
  };
240
- type AuthSettings = AuthEntry[];
240
+ type AuthSettings = AuthEntry;
241
241
  type LogLevel = "info";
242
242
  type Logger = {
243
243
  level?: LogLevel;
@@ -596,11 +596,6 @@ declare function server<U>(options: Omit<Options, "auth"> & {
596
596
  }): Server<{
597
597
  user: NonNullable<Awaited<U>>;
598
598
  }>;
599
- declare function server<A extends readonly AuthOption[]>(options: Omit<Options, "auth"> & {
600
- auth: A;
601
- }): Server<{
602
- user: UserOf<A[number]>;
603
- }>;
604
599
  declare function server<C extends ContextTypes = {}>(options?: Options): Server<C>;
605
600
 
606
601
  export { type AuthClaims, type AuthConfig, type AuthEntry, type AuthFunction, type AuthInstance, type AuthMeta, type AuthOption, type AuthProfile, type AuthSettings, type AuthVerify, type BasicValue, type Body, type BodyMode, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type ContextExtension, type ContextTypes, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type ProviderOptions, type RedirectOption, type Route, type RouteOptions, type RouteSchema, type RouterMethod, type SchemaOutput, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, TypedServerError as ServerError, type Settings, type StandardIssue, type StandardSchemaV1, type Strategy, type Time, type UploadOptions, type UploadedFile, type UserOf, ValidationError, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
package/index.js CHANGED
@@ -1016,29 +1016,28 @@ async function verifyJwt(token, secret) {
1016
1016
  var NAME = "session";
1017
1017
  var inCookie = (s) => s === "session" || s === "cookie";
1018
1018
  var isSigned = (s) => s === "cookie" || s === "jwt";
1019
- var UNITS2 = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 };
1020
1019
  function seconds(expires) {
1021
- const match = /^(\d+)([smhdw])$/.exec(expires);
1022
- if (!match) throw new Error(`Invalid \`expires\`: "${expires}"`);
1023
- return Number(match[1]) * UNITS2[match[2]];
1020
+ const ms = parse(expires);
1021
+ if (!ms) throw new Error(`Invalid \`expires\`: "${expires}"`);
1022
+ return Math.round(ms / 1e3);
1024
1023
  }
1025
1024
  var bearer = (ctx) => {
1026
1025
  const header = ctx.headers.authorization;
1027
1026
  if (!header) return;
1028
1027
  const [type2, token] = header.trim().split(" ");
1029
- if (type2?.toLowerCase() !== "bearer" || !token) {
1030
- throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1031
- }
1028
+ if (type2?.toLowerCase() !== "bearer") return;
1029
+ if (!token) throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1032
1030
  return token;
1033
1031
  };
1034
- async function read(ctx, strategies) {
1035
- for (const strategy of strategies) {
1036
- const token = inCookie(strategy) ? ctx.cookies[NAME] : bearer(ctx);
1037
- if (!token) continue;
1038
- const payload = await verifyJwt(token, ctx.options.secrets);
1039
- if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
1040
- return { payload, strategy };
1032
+ async function read(ctx, strategy) {
1033
+ const token = inCookie(strategy) ? ctx.cookies[NAME] : bearer(ctx);
1034
+ if (!token) return;
1035
+ const payload = await verifyJwt(token, ctx.options.secrets);
1036
+ if (!payload) {
1037
+ if (inCookie(strategy)) return;
1038
+ throw ServerError_default.AUTH_INVALID_TOKEN();
1041
1039
  }
1040
+ return payload;
1042
1041
  }
1043
1042
  var meta = (payload, strategy) => ({
1044
1043
  issuedAt: new Date(payload.iat * 1e3),
@@ -1395,31 +1394,28 @@ function parseProviders(given) {
1395
1394
  var target = async (where, fallback, user, ctx) => typeof where === "function" ? where(user, ctx) : where ?? fallback;
1396
1395
  function entry(config2) {
1397
1396
  const list = parseProviders(config2.providers);
1398
- const strategies = Array.isArray(config2.strategy) ? config2.strategy : [config2.strategy ?? "session"];
1399
- for (const one of strategies) {
1400
- if (!["session", "cookie", "token", "jwt"].includes(one)) {
1401
- throw new Error(
1402
- `Unknown strategy "${one}"; it takes 'session', 'cookie', 'token' or 'jwt'.`
1403
- );
1404
- }
1397
+ const strategy = config2.strategy ?? "session";
1398
+ if (!["session", "cookie", "token", "jwt"].includes(strategy)) {
1399
+ throw new Error(
1400
+ `Unknown strategy "${strategy}"; it takes 'session', 'cookie', 'token' or 'jwt'.`
1401
+ );
1405
1402
  }
1406
1403
  const expires = config2.expires ?? "30d";
1404
+ seconds(expires);
1407
1405
  const { onLogin, getUser, toPublicUser, onLogout } = config2;
1408
1406
  if (onLogin && !getUser) {
1409
1407
  throw new Error("`onLogin` needs a `getUser`: something has to resolve the id it returns.");
1410
1408
  }
1411
- for (const strategy of strategies) {
1412
- if (isSigned(strategy)) {
1413
- if (getUser && !toPublicUser) {
1414
- throw new Error(
1415
- `The \`${strategy}\` strategy signs the user into the credential, so it needs a \`toPublicUser\` to say what goes in. Signing the whole row would publish whatever else is on it.`
1416
- );
1417
- }
1418
- } else if (!getUser) {
1409
+ if (isSigned(strategy)) {
1410
+ if (getUser && !toPublicUser) {
1419
1411
  throw new Error(
1420
- `The \`${strategy}\` strategy puts an id in the credential, so it needs a \`getUser\` to resolve it. With no database, use \`cookie\` or \`jwt\`.`
1412
+ `The \`${strategy}\` strategy signs the user into the credential, so it needs a \`toPublicUser\` to say what goes in. Signing the whole row would publish whatever else is on it.`
1421
1413
  );
1422
1414
  }
1415
+ } else if (!getUser) {
1416
+ throw new Error(
1417
+ `The \`${strategy}\` strategy puts an id in the credential, so it needs a \`getUser\` to resolve it. With no database, use \`cookie\` or \`jwt\`.`
1418
+ );
1423
1419
  }
1424
1420
  const publicProfile = ({ id, email, name, avatar }) => ({
1425
1421
  id,
@@ -1431,19 +1427,28 @@ function entry(config2) {
1431
1427
  const loginTo = typeof config2.redirect === "object" ? redirects.login : config2.redirect;
1432
1428
  const finish = async (ctx, profile) => {
1433
1429
  const payload = getUser ? await (async () => {
1434
- const id = await onLogin(profile, ctx);
1430
+ let id;
1431
+ try {
1432
+ id = await onLogin(profile, ctx);
1433
+ } catch (error) {
1434
+ error.expose = true;
1435
+ throw error;
1436
+ }
1435
1437
  if (id === void 0 || id === null) {
1436
1438
  throw new Error("`onLogin` must return the id the credential points at");
1437
1439
  }
1438
- if (!isSigned(strategies[0])) return { sub: String(id) };
1440
+ if (!isSigned(strategy)) return { sub: String(id) };
1439
1441
  const user2 = await getUser(String(id), ctx);
1442
+ if (user2 === void 0 || user2 === null) {
1443
+ throw new Error(`getUser returned nothing for the id "${id}" that onLogin just returned`);
1444
+ }
1440
1445
  return { user: await toPublicUser(user2) };
1441
1446
  })() : { user: publicProfile(profile) };
1442
1447
  const signed = { ...payload, provider: profile.provider };
1443
1448
  const token = await issue(ctx, signed, expires);
1444
1449
  const user = signed.user ?? await getUser(signed.sub, ctx);
1445
1450
  const to = await target(loginTo, "/", user, ctx);
1446
- if (inCookie(strategies[0])) {
1451
+ if (inCookie(strategy)) {
1447
1452
  return cookies("session", {
1448
1453
  value: token,
1449
1454
  path: "/",
@@ -1458,9 +1463,8 @@ function entry(config2) {
1458
1463
  return {
1459
1464
  name: "flow",
1460
1465
  async user(ctx) {
1461
- const found = await read(ctx, strategies);
1462
- if (!found) return;
1463
- const { payload, strategy } = found;
1466
+ const payload = await read(ctx, strategy);
1467
+ if (!payload) return;
1464
1468
  ctx.auth = meta(payload, strategy);
1465
1469
  if (payload.user) return payload.user;
1466
1470
  if (!payload.sub) return;
@@ -1495,8 +1499,10 @@ function entry(config2) {
1495
1499
  res = await finish(ctx, profile);
1496
1500
  } catch (error) {
1497
1501
  const to = await target(redirects.error, "/", null, ctx);
1498
- const message = error.message;
1499
- res = redirect(`${to}?error=${encodeURIComponent(message)}`);
1502
+ let message = "Could not sign you in";
1503
+ if (error?.expose) message = error.message;
1504
+ else console.error(`[server:auth] ${name} callback failed:`, error);
1505
+ res = await redirect(`${to}?error=${encodeURIComponent(message)}`);
1500
1506
  }
1501
1507
  res.headers.append(
1502
1508
  "set-cookie",
@@ -1507,12 +1513,10 @@ function entry(config2) {
1507
1513
  app.get(`/auth/callback/${name}`, SPEC, callback);
1508
1514
  }
1509
1515
  app.post("/auth/logout", SPEC, async (ctx) => {
1510
- const found = await read(ctx, strategies).catch(() => void 0);
1511
- if (onLogout && found?.payload.sub) {
1512
- await onLogout(found.payload.sub, ctx);
1513
- }
1516
+ const payload = await read(ctx, strategy).catch(() => void 0);
1517
+ if (onLogout && payload?.sub) await onLogout(payload.sub, ctx);
1514
1518
  const to = await target(redirects.logout, "/", null, ctx);
1515
- if (!inCookie(strategies[0])) return status(204);
1519
+ if (!inCookie(strategy)) return status(204);
1516
1520
  return cookies("session", { value: null }).redirect(to);
1517
1521
  });
1518
1522
  }
@@ -1566,17 +1570,16 @@ var bearer2 = (ctx) => {
1566
1570
  const header = ctx.headers.authorization;
1567
1571
  if (!header) return;
1568
1572
  const [type2, token] = header.trim().split(" ");
1569
- if (type2?.toLowerCase() !== "bearer" || !token) {
1570
- throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1571
- }
1573
+ if (type2?.toLowerCase() !== "bearer") return;
1574
+ if (!token) throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
1572
1575
  return token;
1573
1576
  };
1574
1577
  function entry2(options) {
1575
- const { verify: issuer, audience } = options;
1578
+ const { issuer, audience } = options;
1576
1579
  const claimNames = options.audienceClaim ? Array.isArray(options.audienceClaim) ? options.audienceClaim : [options.audienceClaim] : ["aud"];
1577
1580
  if (!audience) {
1578
1581
  throw new Error(
1579
- "`verify` needs an `audience`: one issuer serves many applications, and without it a token minted for another one is accepted here."
1582
+ "`issuer` needs an `audience`: one issuer serves many applications, and without it a token minted for another one is accepted here."
1580
1583
  );
1581
1584
  }
1582
1585
  const allowed = Array.isArray(audience) ? audience : [audience];
@@ -1585,7 +1588,13 @@ function entry2(options) {
1585
1588
  async user(ctx) {
1586
1589
  const token = options.cookie ? ctx.cookies[options.cookie] : bearer2(ctx);
1587
1590
  if (!token) return;
1588
- const claims2 = await check(token, issuer, allowed, claimNames);
1591
+ let claims2;
1592
+ try {
1593
+ claims2 = await check(token, issuer, allowed, claimNames);
1594
+ } catch (error) {
1595
+ if (options.cookie) return;
1596
+ throw error;
1597
+ }
1589
1598
  ctx.auth = {
1590
1599
  issuedAt: new Date((claims2.iat ?? 0) * 1e3),
1591
1600
  expiresAt: claims2.exp ? new Date(claims2.exp * 1e3) : void 0,
@@ -1668,8 +1677,12 @@ var vendors_default = VENDORS;
1668
1677
  // src/auth/parse.ts
1669
1678
  function parseAuth(auth2) {
1670
1679
  if (!auth2) return null;
1671
- const list = Array.isArray(auth2) ? auth2 : [auth2];
1672
- return list.flatMap((one) => toEntry(one));
1680
+ if (Array.isArray(auth2)) {
1681
+ throw new Error(
1682
+ "`auth` takes one method. For several login options, list them under `providers` instead: auth: { providers: ['github', 'google'], ... }."
1683
+ );
1684
+ }
1685
+ return toEntry(auth2);
1673
1686
  }
1674
1687
  function vendorEntry(strategy, name) {
1675
1688
  const vendor = vendors_default[name];
@@ -1697,7 +1710,7 @@ function vendorEntry(strategy, name) {
1697
1710
  );
1698
1711
  }
1699
1712
  return entry2({
1700
- verify: issuer,
1713
+ issuer,
1701
1714
  audience,
1702
1715
  ...vendor.claim ? { audienceClaim: vendor.claim } : {},
1703
1716
  ...strategy === "cookie" ? { cookie: vendor.cookie } : {}
@@ -1711,15 +1724,15 @@ function toEntry(auth2) {
1711
1724
  `Invalid auth "${auth2}": the string form is "<strategy>:<name>", like "cookie:github" to log people in, or "jwt:clerk" to check a token a vendor issued.`
1712
1725
  );
1713
1726
  }
1714
- if (vendors_default[name]) return [vendorEntry(strategy, name)];
1715
- return [entry({ strategy, providers: name })];
1727
+ if (vendors_default[name]) return vendorEntry(strategy, name);
1728
+ return entry({ strategy, providers: name });
1716
1729
  }
1717
1730
  if (typeof auth2 === "function") {
1718
- return [{ name: "function", user: async (ctx) => auth2(ctx) }];
1731
+ return { name: "function", user: async (ctx) => auth2(ctx) };
1719
1732
  }
1720
1733
  if (auth2 && typeof auth2 === "object") {
1721
- if ("verify" in auth2) return [entry2(auth2)];
1722
- if ("providers" in auth2) return [entry(auth2)];
1734
+ if ("issuer" in auth2) return entry2(auth2);
1735
+ if ("providers" in auth2) return entry(auth2);
1723
1736
  if ("handler" in auth2) {
1724
1737
  const instance = auth2;
1725
1738
  const path = (instance.path ?? "/api/auth").replace(/\/$/, "");
@@ -1733,24 +1746,22 @@ function toEntry(auth2) {
1733
1746
  ...ctx.body ? { duplex: "half" } : {}
1734
1747
  })
1735
1748
  );
1736
- return [
1737
- {
1738
- name: `instance:${path}`,
1739
- user: async (ctx) => instance.user?.(ctx),
1740
- routes: (app) => {
1741
- const wildcard = `${path}/*`;
1742
- app.get(wildcard, raw, forward);
1743
- app.post(wildcard, raw, forward);
1744
- app.put(wildcard, raw, forward);
1745
- app.patch(wildcard, raw, forward);
1746
- app.delete(wildcard, raw, forward);
1747
- }
1749
+ return {
1750
+ name: `instance:${path}`,
1751
+ user: async (ctx) => instance.user?.(ctx),
1752
+ routes: (app) => {
1753
+ const wildcard = `${path}/*`;
1754
+ app.get(wildcard, raw, forward);
1755
+ app.post(wildcard, raw, forward);
1756
+ app.put(wildcard, raw, forward);
1757
+ app.patch(wildcard, raw, forward);
1758
+ app.delete(wildcard, raw, forward);
1748
1759
  }
1749
- ];
1760
+ };
1750
1761
  }
1751
1762
  }
1752
1763
  throw new Error(
1753
- "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ verify, audience }`, a library instance, or an array of those."
1764
+ "Invalid `auth`: it takes a string, a function, `{ providers }`, `{ issuer, audience }`, a library instance, or an array of those."
1754
1765
  );
1755
1766
  }
1756
1767
 
@@ -1816,16 +1827,16 @@ var STATUS_TEXT = {
1816
1827
  502: "Bad Gateway",
1817
1828
  503: "Service Unavailable"
1818
1829
  };
1819
- var UNITS3 = ["b", "kb", "mb", "gb", "tb"];
1830
+ var UNITS2 = ["b", "kb", "mb", "gb", "tb"];
1820
1831
  function formatBytes(bytes) {
1821
1832
  if (!bytes || bytes < 0) return "0b";
1822
1833
  const i = Math.min(
1823
1834
  Math.floor(Math.log(bytes) / Math.log(1024)),
1824
- UNITS3.length - 1
1835
+ UNITS2.length - 1
1825
1836
  );
1826
1837
  const value = bytes / 1024 ** i;
1827
1838
  const rounded = i === 0 ? Math.round(value) : Math.round(value * 10) / 10;
1828
- return `${rounded}${UNITS3[i]}`;
1839
+ return `${rounded}${UNITS2[i]}`;
1829
1840
  }
1830
1841
  var SCOPE_COLORS = {
1831
1842
  start: "green",
@@ -2014,10 +2025,10 @@ function config(options = {}) {
2014
2025
  options.auth || env2.AUTH || null
2015
2026
  );
2016
2027
  }
2017
- if (settings.auth?.some((one) => one.name === "flow") && settings.secrets[0].startsWith("unsafe-")) {
2018
- console.warn(
2019
- "[server:auth] auth with no SECRETS set: credentials are signed with a random per-process secret, so they break on restart and across instances. Set the SECRETS environment variable (or the `secrets` option)."
2020
- );
2028
+ if (settings.auth?.name === "flow" && settings.secrets[0].startsWith("unsafe-")) {
2029
+ const message = "Auth needs a stable secret: credentials are signed with it, and the random per-process fallback breaks them on restart and across instances. Set the SECRETS environment variable (or the `secrets` option).";
2030
+ if (env2.NODE_ENV === "production") throw new Error(message);
2031
+ console.warn(`[server:auth] ${message}`);
2021
2032
  }
2022
2033
  if (options.openapi) {
2023
2034
  const o = options.openapi;
@@ -2032,10 +2043,7 @@ function config(options = {}) {
2032
2043
  });
2033
2044
  settings.onResponse = options.onResponse;
2034
2045
  const loc = (v) => typeof v === "string" ? v : "enabled";
2035
- if (settings.auth) {
2036
- const names = settings.auth.map((one) => one.name).join(", ");
2037
- log.message("auth", ` enabled`);
2038
- }
2046
+ if (settings.auth) log.message("auth", `${settings.auth.name} enabled`);
2039
2047
  if (settings.public) log.message("public", loc(options.public));
2040
2048
  if (settings.uploads) log.message("uploads", loc(options.uploads));
2041
2049
  if (settings.cors) {
@@ -2412,17 +2420,11 @@ function toWeb(nodeStream) {
2412
2420
 
2413
2421
  // src/auth/index.ts
2414
2422
  function auth(app) {
2415
- const entries = app.settings.auth;
2423
+ const entry3 = app.settings.auth;
2416
2424
  app.use(async function middle(ctx) {
2417
- for (const entry3 of entries) {
2418
- const user = await entry3.user(ctx);
2419
- if (user) {
2420
- ctx.user = user;
2421
- return;
2422
- }
2423
- }
2425
+ ctx.user = await entry3.user(ctx);
2424
2426
  });
2425
- for (const entry3 of entries) entry3.routes?.(app);
2427
+ entry3.routes?.(app);
2426
2428
  }
2427
2429
 
2428
2430
  // src/helpers/parseRange.ts
@@ -2664,10 +2666,7 @@ function timer(ctx) {
2664
2666
  async function socketUser(app, headers2, cookies2) {
2665
2667
  if (!app.settings.auth) return void 0;
2666
2668
  const ctx = { options: app.settings, headers: headers2, cookies: cookies2 };
2667
- for (const entry3 of app.settings.auth) {
2668
- const user = await entry3.user(ctx);
2669
- if (user) return user;
2670
- }
2669
+ return app.settings.auth.user(ctx);
2671
2670
  }
2672
2671
 
2673
2672
  // src/helpers/wsNode.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.47.1",
3
+ "version": "0.48.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",