@server/next 0.45.1 → 0.46.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 +2 -2
  2. package/index.js +37 -12
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -255,7 +255,7 @@ type OnError = (error: Error, ctx: Context) => Response | Promise<Response>;
255
255
  type OnResponse = (response: Response, ctx: Context) => Response | void | Promise<Response | void>;
256
256
  type Options = {
257
257
  port?: number;
258
- secret?: string;
258
+ secrets?: string | string[];
259
259
  public?: string | Bucket;
260
260
  uploads?: string | Bucket | UploadOptions;
261
261
  cors?: CorsOptions;
@@ -275,7 +275,7 @@ type Options = {
275
275
  };
276
276
  type Settings = {
277
277
  port: number;
278
- secret: string;
278
+ secrets: string[];
279
279
  public?: Bucket;
280
280
  uploads?: ({
281
281
  bucket: Bucket;
package/index.js CHANGED
@@ -1040,13 +1040,17 @@ async function verifyJwt(token, secret) {
1040
1040
  return null;
1041
1041
  }
1042
1042
  if (header?.alg !== "HS256") return null;
1043
- const key = await hmacKey(secret);
1044
- const ok = await crypto.subtle.verify(
1045
- "HMAC",
1046
- key,
1047
- unb64url(sig),
1048
- enc.encode(`${head}.${body}`)
1049
- );
1043
+ let ok = false;
1044
+ for (const candidate of Array.isArray(secret) ? secret : [secret]) {
1045
+ const key = await hmacKey(candidate);
1046
+ ok = await crypto.subtle.verify(
1047
+ "HMAC",
1048
+ key,
1049
+ unb64url(sig),
1050
+ enc.encode(`${head}.${body}`)
1051
+ );
1052
+ if (ok) break;
1053
+ }
1050
1054
  if (!ok) return null;
1051
1055
  let payload;
1052
1056
  try {
@@ -1102,7 +1106,11 @@ async function finishLogin(ctx, input, opts = {}) {
1102
1106
  provider: input.provider
1103
1107
  };
1104
1108
  assertUser(payload, "onToken");
1105
- const token = await signJwt(payload, ctx.options.secret, 7 * 24 * 60 * 60);
1109
+ const token = await signJwt(
1110
+ payload,
1111
+ ctx.options.secrets[0],
1112
+ 7 * 24 * 60 * 60
1113
+ );
1106
1114
  const exposed = await onUser(payload, ctx);
1107
1115
  assertUser(exposed, "onUser");
1108
1116
  return status(201).json({ ...exposed, token });
@@ -1754,6 +1762,13 @@ function createLogger(level) {
1754
1762
  };
1755
1763
  }
1756
1764
 
1765
+ // src/helpers/secrets.ts
1766
+ function resolveSecrets(option) {
1767
+ const given = option ?? globalThis.env.SECRETS?.split(",");
1768
+ const list = (Array.isArray(given) ? given : [given]).map((one) => one?.trim()).filter(Boolean);
1769
+ return list.length ? list : [`unsafe-${createId()}`];
1770
+ }
1771
+
1757
1772
  // src/helpers/security.ts
1758
1773
  function resolveSecurity(security) {
1759
1774
  const off = security === false;
@@ -1827,13 +1842,23 @@ function config(options = {}) {
1827
1842
  );
1828
1843
  }
1829
1844
  }
1845
+ if (opts.secret !== void 0) {
1846
+ throw new Error(
1847
+ "The `secret` option is now `secrets`, and takes one key or several: `secrets: [current, previous]` signs with the first and verifies with any, so rotating a key no longer signs everyone out."
1848
+ );
1849
+ }
1850
+ if (env2.SECRET && !env2.SECRETS) {
1851
+ throw new Error(
1852
+ "The SECRET environment variable is now SECRETS, a comma-separated list. Rename it, or every token signed with the old key breaks."
1853
+ );
1854
+ }
1830
1855
  const raw = options.log ?? env2.LOG_LEVEL;
1831
1856
  const level = raw === true ? "info" : raw === false ? void 0 : raw;
1832
1857
  const log = createLogger(level);
1833
1858
  const settings = {
1834
1859
  // `env.PORT` is a string, so coerce it: `settings.port` is a number
1835
1860
  port: options.port || Number(env2.PORT) || 3e3,
1836
- secret: options.secret || env2.SECRET || `unsafe-${createId()}`,
1861
+ secrets: resolveSecrets(options.secrets),
1837
1862
  log,
1838
1863
  // How request bodies are read: parsed into ctx.body by default; `raw` keeps
1839
1864
  // the Buffer, `stream` hands the handler the unread web ReadableStream.
@@ -1906,9 +1931,9 @@ function config(options = {}) {
1906
1931
  settings.auth.sessions = toStoreExpiring(/* @__PURE__ */ new Map(), "1w");
1907
1932
  }
1908
1933
  }
1909
- if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
1934
+ if (settings.auth?.strategy.includes("jwt") && settings.secrets[0].startsWith("unsafe-")) {
1910
1935
  console.warn(
1911
- "[server:auth] jwt strategy with no SECRET set: tokens are signed with a random per-process secret, so they break on restart and across instances. Set the SECRET environment variable (or the `secret` option)."
1936
+ "[server:auth] jwt strategy with no SECRETS set: tokens 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)."
1912
1937
  );
1913
1938
  }
1914
1939
  if (options.openapi) {
@@ -2373,7 +2398,7 @@ async function getJwtUser(ctx) {
2373
2398
  if (type2?.toLowerCase() !== "bearer" || !token) {
2374
2399
  throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
2375
2400
  }
2376
- const payload = await verifyJwt(token, ctx.options.secret);
2401
+ const payload = await verifyJwt(token, ctx.options.secrets);
2377
2402
  if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
2378
2403
  const { iat, exp, ...claims } = payload;
2379
2404
  if (!claims.id || !claims.email) throw ServerError_default.AUTH_INVALID_TOKEN();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.45.1",
3
+ "version": "0.46.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",