@dotpress/auth 2.0.0-alpha.4 → 2.0.0-alpha.5

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/dist/index.js CHANGED
@@ -4,6 +4,10 @@ declareContextKey("user", "authPlugin");
4
4
  declareContextKey("auth", "authPlugin");
5
5
 
6
6
  // src/auth.ts
7
+ import {
8
+ createMemoryRateLimitStore,
9
+ parseDuration
10
+ } from "@dotpress/core";
7
11
  import bcrypt from "bcryptjs";
8
12
  import {
9
13
  CompactSign,
@@ -47,43 +51,6 @@ var isCookieAuthenticationAllowed = async (req, isTrustedOrigin) => {
47
51
  return originHost !== void 0 && originHost === (req.header("host") ?? hostOf(req.url));
48
52
  };
49
53
 
50
- // src/rate-limit.ts
51
- var memoryRateLimitStore = (options = {}) => {
52
- const maxKeys = options.maxKeys ?? 1e4;
53
- if (!Number.isInteger(maxKeys) || maxKeys < 1) {
54
- throw new RangeError("Invalid maxKeys: expected a positive integer.");
55
- }
56
- const counters = /* @__PURE__ */ new Map();
57
- const makeRoom = (now) => {
58
- if (counters.size < maxKeys) return;
59
- for (const [key, state] of counters) {
60
- if (state.resetAt.getTime() <= now) counters.delete(key);
61
- }
62
- for (const key of counters.keys()) {
63
- if (counters.size < maxKeys) break;
64
- counters.delete(key);
65
- }
66
- };
67
- return {
68
- async increment(key, windowSeconds) {
69
- const now = Date.now();
70
- const current = counters.get(key);
71
- if (current && current.resetAt.getTime() > now) {
72
- current.count += 1;
73
- return { ...current };
74
- }
75
- counters.delete(key);
76
- makeRoom(now);
77
- const state = { count: 1, resetAt: new Date(now + windowSeconds * 1e3) };
78
- counters.set(key, state);
79
- return { ...state };
80
- },
81
- async reset(key) {
82
- counters.delete(key);
83
- }
84
- };
85
- };
86
-
87
54
  // src/auth.ts
88
55
  var ALGORITHM = "RS256";
89
56
  var ACCESS_TOKEN_TYPE = "at+jwt";
@@ -96,6 +63,67 @@ var AuthError = class extends Error {
96
63
  }
97
64
  code;
98
65
  };
66
+ var internals = /* @__PURE__ */ new WeakMap();
67
+ var internalsOf = (auth) => typeof auth === "object" && auth !== null ? internals.get(auth) : void 0;
68
+ var wholeSeconds = (label, value, min) => {
69
+ const result = Math.round(
70
+ parseDuration(value, `createAuth(): ${label}`) / 1e3
71
+ );
72
+ if (result < min) {
73
+ throw new RangeError(
74
+ `createAuth(): ${label} must be at least ${min} second${min === 1 ? "" : "s"}.`
75
+ );
76
+ }
77
+ return result;
78
+ };
79
+ var assertKnown = (options, known, prefix = "") => {
80
+ if (options === void 0) return;
81
+ if (typeof options !== "object" || options === null) {
82
+ throw new TypeError(
83
+ `createAuth(): ${prefix ? prefix.slice(0, -1) : "options"} must be an object.`
84
+ );
85
+ }
86
+ for (const key of Object.keys(options)) {
87
+ if (!known.includes(key)) {
88
+ throw new TypeError(`createAuth(): unknown option "${prefix}${key}".`);
89
+ }
90
+ }
91
+ };
92
+ var OPTIONS = [
93
+ "keys",
94
+ "dataProvider",
95
+ "accessTokenTTL",
96
+ "session",
97
+ "issuer",
98
+ "audience",
99
+ "cookies",
100
+ "clients",
101
+ "onLoginFailed",
102
+ "onLoginSuccess",
103
+ "loginThrottle",
104
+ "beforeLogin",
105
+ "passwordHashRounds",
106
+ "usernameClaim",
107
+ "refreshTokenReuseInterval",
108
+ "onRefreshTokenReuse",
109
+ "getUserRoles"
110
+ ];
111
+ var KEY_OPTIONS = [
112
+ "privateKey",
113
+ "publicKey",
114
+ "kid",
115
+ "additionalPublicKeys"
116
+ ];
117
+ var SESSION_OPTIONS = [
118
+ "idleTimeout",
119
+ "absoluteTimeout"
120
+ ];
121
+ var CLIENT_OPTIONS = [
122
+ "allowed",
123
+ "required",
124
+ "isUserRoleAllowed",
125
+ "sessions"
126
+ ];
99
127
  var DEFAULT_COOKIE_SETTINGS = {
100
128
  enabled: false,
101
129
  httpOnly: true,
@@ -117,15 +145,15 @@ var defaultUserRoles = (user) => {
117
145
  }
118
146
  return typeof role === "string" ? [role] : [];
119
147
  };
120
- var checkSession = (session, name) => {
121
- for (const key of ["idleTimeout", "absoluteTimeout"]) {
122
- const value = session?.[key];
123
- if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) {
124
- throw new RangeError(
125
- `Invalid ${name}.${key}: expected a positive number of seconds.`
126
- );
127
- }
148
+ var resolveSession = (session, name) => {
149
+ assertKnown(session, SESSION_OPTIONS, `${name}.`);
150
+ const resolved = {};
151
+ for (const key of SESSION_OPTIONS) {
152
+ if (session === void 0 || !(key in session)) continue;
153
+ const value = session[key];
154
+ resolved[key] = value === void 0 ? void 0 : wholeSeconds(`${name}.${key}`, value, 1);
128
155
  }
156
+ return resolved;
129
157
  };
130
158
  var DEFAULT_LOGIN_LIMITS = {
131
159
  usernameAndIp: { max: 5, window: 60 },
@@ -133,21 +161,39 @@ var DEFAULT_LOGIN_LIMITS = {
133
161
  };
134
162
  var resolveLoginThrottle = (input) => {
135
163
  if (input === false) return void 0;
164
+ assertKnown(input, ["store", "limits"], "loginThrottle.");
165
+ assertKnown(
166
+ input?.limits,
167
+ ["usernameAndIp", "username"],
168
+ "loginThrottle.limits."
169
+ );
136
170
  const limit = (name) => {
137
171
  const value = input?.limits?.[name];
138
172
  if (value === false) return void 0;
139
173
  const resolved = value ?? DEFAULT_LOGIN_LIMITS[name];
140
- if (!Number.isInteger(resolved.max) || resolved.max < 1 || !Number.isFinite(resolved.window) || resolved.window <= 0) {
174
+ assertKnown(
175
+ resolved,
176
+ ["max", "window"],
177
+ `loginThrottle.limits.${name}.`
178
+ );
179
+ if (!Number.isInteger(resolved.max) || resolved.max < 1) {
141
180
  throw new RangeError(
142
- `Invalid loginThrottle.limits.${name}: expected a positive integer max and a positive window, in seconds.`
181
+ `createAuth(): loginThrottle.limits.${name}.max must be a positive integer.`
143
182
  );
144
183
  }
145
- return { max: resolved.max, window: resolved.window };
184
+ return {
185
+ max: resolved.max,
186
+ window: wholeSeconds(
187
+ `loginThrottle.limits.${name}.window`,
188
+ resolved.window,
189
+ 1
190
+ )
191
+ };
146
192
  };
147
- const store = input?.store ?? memoryRateLimitStore();
193
+ const store = input?.store ?? createMemoryRateLimitStore();
148
194
  if (typeof store.increment !== "function" || typeof store.reset !== "function") {
149
195
  throw new TypeError(
150
- "Invalid loginThrottle.store: expected increment() and reset() methods."
196
+ "createAuth(): loginThrottle.store must have increment() and reset() methods."
151
197
  );
152
198
  }
153
199
  return {
@@ -159,37 +205,62 @@ var resolveLoginThrottle = (input) => {
159
205
  };
160
206
  };
161
207
  var resolveSettings = (settings) => {
162
- if (!settings?.keys?.secretKey || !settings.keys.publicKey) {
208
+ if (typeof settings !== "object" || settings === null) {
209
+ throw new TypeError("createAuth(): expected an object of options.");
210
+ }
211
+ if ("refreshTokenTTLInDays" in settings) {
163
212
  throw new TypeError(
164
- "createAuth() requires keys.secretKey and keys.publicKey."
213
+ "createAuth(): refreshTokenTTLInDays is replaced by session.idleTimeout."
214
+ );
215
+ }
216
+ if (typeof settings.keys === "object" && settings.keys !== null && "secretKey" in settings.keys) {
217
+ throw new TypeError(
218
+ "createAuth(): keys.secretKey is renamed keys.privateKey."
219
+ );
220
+ }
221
+ assertKnown(settings, OPTIONS);
222
+ assertKnown(settings.keys, KEY_OPTIONS, "keys.");
223
+ for (const [index, entry] of (settings.keys?.additionalPublicKeys ?? []).entries()) {
224
+ if (typeof entry !== "string") {
225
+ assertKnown(
226
+ entry,
227
+ ["publicKey", "kid"],
228
+ `keys.additionalPublicKeys[${index}].`
229
+ );
230
+ }
231
+ }
232
+ assertKnown(settings.clients, CLIENT_OPTIONS, "clients.");
233
+ assertKnown(
234
+ settings.cookies,
235
+ Object.keys(DEFAULT_COOKIE_SETTINGS),
236
+ "cookies."
237
+ );
238
+ if (!settings?.keys?.privateKey || !settings.keys.publicKey) {
239
+ throw new TypeError(
240
+ "createAuth(): keys.privateKey and keys.publicKey are required."
165
241
  );
166
242
  }
167
243
  if (!settings.dataProvider) {
168
- throw new TypeError("createAuth() requires a dataProvider.");
244
+ throw new TypeError("createAuth(): dataProvider is required.");
169
245
  }
170
246
  if (typeof settings.issuer !== "string" || settings.issuer === "") {
171
247
  throw new TypeError(
172
- "createAuth() requires an issuer (iss claim), such as the URL of the API."
248
+ "createAuth(): issuer is required (the iss claim), such as the URL of the API."
173
249
  );
174
250
  }
175
251
  if (settings.audience !== void 0 && settings.audience === "") {
176
- throw new TypeError("Invalid audience: expected a non-empty string.");
252
+ throw new TypeError("createAuth(): audience must be a non-empty string.");
177
253
  }
178
254
  const clients = settings.clients;
179
255
  if (clients && (!Array.isArray(clients.allowed) || clients.allowed.length === 0 || clients.allowed.some((id) => typeof id !== "string" || id === ""))) {
180
256
  throw new TypeError(
181
- "Invalid clients.allowed: expected a non-empty list of client identifiers."
257
+ "createAuth(): clients.allowed must be a non-empty list of client identifiers."
182
258
  );
183
259
  }
184
260
  const rounds = settings.passwordHashRounds;
185
261
  if (rounds !== void 0 && (!Number.isInteger(rounds) || rounds < 4 || rounds > 31)) {
186
262
  throw new RangeError(
187
- "Invalid passwordHashRounds: expected an integer between 4 and 31."
188
- );
189
- }
190
- if ("refreshTokenTTLInDays" in settings) {
191
- throw new TypeError(
192
- "refreshTokenTTLInDays is replaced by session.idleTimeout, in seconds."
263
+ "createAuth(): passwordHashRounds must be an integer between 4 and 31."
193
264
  );
194
265
  }
195
266
  for (const origin of settings.cookies?.trustedOrigins ?? []) {
@@ -200,33 +271,33 @@ var resolveSettings = (settings) => {
200
271
  }
201
272
  if (!valid) {
202
273
  throw new TypeError(
203
- `Invalid cookies.trustedOrigins: "${origin}" is not an origin (expected a scheme and a host, such as https://app.example.com, without path or trailing slash).`
274
+ `createAuth(): cookies.trustedOrigins: "${origin}" is not an origin (expected a scheme and a host, such as https://app.example.com, without path or trailing slash).`
204
275
  );
205
276
  }
206
277
  }
207
- checkSession(settings.session, "session");
208
- for (const [clientId, session] of Object.entries(clients?.sessions ?? {})) {
278
+ const session = resolveSession(settings.session, "session");
279
+ const clientSessions = {};
280
+ for (const [clientId, override] of Object.entries(clients?.sessions ?? {})) {
209
281
  if (!clients?.allowed.includes(clientId)) {
210
282
  throw new TypeError(
211
- `Invalid clients.sessions: "${clientId}" is not in clients.allowed.`
283
+ `createAuth(): clients.sessions: "${clientId}" is not in clients.allowed.`
212
284
  );
213
285
  }
214
- checkSession(session, `clients.sessions.${clientId}`);
215
- }
216
- const reuseInterval = settings.refreshTokenReuseInterval;
217
- if (reuseInterval !== void 0 && (!Number.isFinite(reuseInterval) || reuseInterval < 0)) {
218
- throw new RangeError(
219
- "Invalid refreshTokenReuseInterval: expected a non-negative number of seconds."
286
+ clientSessions[clientId] = resolveSession(
287
+ override,
288
+ `clients.sessions.${clientId}`
220
289
  );
221
290
  }
291
+ const optional = (label, fallback, min) => settings[label] === void 0 ? fallback : wholeSeconds(label, settings[label], min);
222
292
  return {
223
293
  keys: settings.keys,
224
294
  dataProvider: settings.dataProvider,
225
- accessTokenTTL: settings.accessTokenTTL ?? 900,
295
+ accessTokenTTL: optional("accessTokenTTL", 900, 1),
226
296
  session: {
227
- idleTimeout: settings.session?.idleTimeout ?? DEFAULT_IDLE_TIMEOUT,
228
- absoluteTimeout: settings.session?.absoluteTimeout
297
+ idleTimeout: session.idleTimeout ?? DEFAULT_IDLE_TIMEOUT,
298
+ absoluteTimeout: session.absoluteTimeout
229
299
  },
300
+ clientSessions,
230
301
  issuer: settings.issuer,
231
302
  audience: settings.audience ?? settings.issuer,
232
303
  cookies: resolveCookies(settings.cookies),
@@ -237,7 +308,7 @@ var resolveSettings = (settings) => {
237
308
  beforeLogin: settings.beforeLogin,
238
309
  passwordHashRounds: settings.passwordHashRounds ?? 12,
239
310
  usernameClaim: settings.usernameClaim ?? false,
240
- refreshTokenReuseInterval: settings.refreshTokenReuseInterval ?? 10,
311
+ refreshTokenReuseInterval: optional("refreshTokenReuseInterval", 10, 0),
241
312
  onRefreshTokenReuse: settings.onRefreshTokenReuse,
242
313
  getUserRoles: settings.getUserRoles ?? defaultUserRoles
243
314
  };
@@ -252,12 +323,12 @@ var cookieName = (setting, name, base, attributes) => {
252
323
  const lowerName = name.toLowerCase();
253
324
  if (lowerName.startsWith("__host-") && !hostAllowed) {
254
325
  throw new TypeError(
255
- `Invalid cookies.${setting}: a __Host- cookie must be secure, without domain, and with path "/".`
326
+ `createAuth(): cookies.${setting}: a __Host- cookie must be secure, without domain, and with path "/".`
256
327
  );
257
328
  }
258
329
  if (lowerName.startsWith("__secure-") && !secure) {
259
330
  throw new TypeError(
260
- `Invalid cookies.${setting}: a __Secure- cookie must be secure.`
331
+ `createAuth(): cookies.${setting}: a __Secure- cookie must be secure.`
261
332
  );
262
333
  }
263
334
  return name;
@@ -269,9 +340,7 @@ var resolveCookies = (settings = {}) => {
269
340
  }
270
341
  for (const key of ["path", "refreshTokenPath"]) {
271
342
  if (!merged[key].startsWith("/")) {
272
- throw new TypeError(
273
- `Invalid cookies.${key}: expected a path starting with "/".`
274
- );
343
+ throw new TypeError(`createAuth(): cookies.${key} must start with "/".`);
275
344
  }
276
345
  }
277
346
  const { secure, domain } = merged;
@@ -298,14 +367,14 @@ var loadKey = async (kind, load) => {
298
367
  return await load();
299
368
  } catch (error) {
300
369
  throw new TypeError(
301
- `Invalid auth ${kind}: expected an RSA PEM key (${error.message}).`,
370
+ `createAuth(): keys.${kind} must be an RSA PEM key (${error.message}).`,
302
371
  { cause: error }
303
372
  );
304
373
  }
305
374
  };
306
375
  var loadPublicKey = async (kind, pem, kid) => {
307
376
  if (pem.includes("BEGIN RSA PUBLIC KEY")) {
308
- throw new TypeError(`Invalid auth ${kind}: ${PKCS1_PUBLIC_KEY_ERROR}`);
377
+ throw new TypeError(`createAuth(): keys.${kind}: ${PKCS1_PUBLIC_KEY_ERROR}`);
309
378
  }
310
379
  const key = await loadKey(
311
380
  kind,
@@ -316,27 +385,27 @@ var loadPublicKey = async (kind, pem, kid) => {
316
385
  return { kid: kid ?? await calculateJwkThumbprint(jwk), key, jwk };
317
386
  };
318
387
  var importKeys = async ({
319
- secretKey,
388
+ privateKey,
320
389
  publicKey,
321
390
  kid,
322
391
  additionalPublicKeys = []
323
392
  }) => {
324
- if (secretKey.includes("BEGIN RSA PRIVATE KEY")) {
393
+ if (privateKey.includes("BEGIN RSA PRIVATE KEY")) {
325
394
  throw new TypeError(
326
- 'Invalid auth secretKey: PKCS#1 keys ("BEGIN RSA PRIVATE KEY") are not supported, use PKCS#8 ("BEGIN PRIVATE KEY"). Convert it with: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem'
395
+ 'createAuth(): keys.privateKey: PKCS#1 keys ("BEGIN RSA PRIVATE KEY") are not supported, use PKCS#8 ("BEGIN PRIVATE KEY"). Convert it with: openssl pkcs8 -topk8 -nocrypt -in private.pem -out private.pkcs8.pem'
327
396
  );
328
397
  }
329
- const privateKey = await loadKey(
330
- "secretKey",
331
- () => importPKCS8(secretKey, ALGORITHM)
398
+ const signingKey = await loadKey(
399
+ "privateKey",
400
+ () => importPKCS8(privateKey, ALGORITHM)
332
401
  );
333
402
  const active = await loadPublicKey("publicKey", publicKey, kid);
334
403
  try {
335
- const probe = await new CompactSign(new Uint8Array([1])).setProtectedHeader({ alg: ALGORITHM }).sign(privateKey);
404
+ const probe = await new CompactSign(new Uint8Array([1])).setProtectedHeader({ alg: ALGORITHM }).sign(signingKey);
336
405
  await compactVerify(probe, active.key);
337
406
  } catch (error) {
338
407
  throw new TypeError(
339
- "Invalid auth keys: publicKey does not match secretKey.",
408
+ "createAuth(): keys.publicKey does not match keys.privateKey.",
340
409
  { cause: error }
341
410
  );
342
411
  }
@@ -353,11 +422,11 @@ var importKeys = async ({
353
422
  verificationKeys.push(loaded);
354
423
  } else if (existing.jwk.n !== loaded.jwk.n || existing.jwk.e !== loaded.jwk.e) {
355
424
  throw new TypeError(
356
- `Invalid auth keys: kid "${loaded.kid}" is used by several keys.`
425
+ `createAuth(): keys: kid "${loaded.kid}" is used by several keys.`
357
426
  );
358
427
  }
359
428
  }
360
- return { privateKey, kid: active.kid, verificationKeys };
429
+ return { signingKey, kid: active.kid, verificationKeys };
361
430
  };
362
431
  var verifyWithKeys = async (token, verificationKeys, verify) => {
363
432
  const { kid } = decodeProtectedHeader(token);
@@ -379,14 +448,14 @@ var verifyWithKeys = async (token, verificationKeys, verify) => {
379
448
  var BCRYPT_HASH = /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/;
380
449
  var nowInSeconds = () => Math.floor(Date.now() / 1e3);
381
450
  var toSession = (claims, roles) => {
382
- if (typeof claims.sub !== "string" || claims.sub === "") {
451
+ if (typeof claims.sub !== "string" || claims.sub === "" || typeof claims.sid !== "string" || claims.sid === "") {
383
452
  throw new AuthError("INVALID_ACCESS_TOKEN");
384
453
  }
385
454
  return {
386
455
  userId: claims.sub,
387
456
  username: typeof claims.username === "string" ? claims.username : void 0,
388
- tokenId: typeof claims.jti === "string" ? claims.jti : "",
389
- sessionId: typeof claims.sid === "string" ? claims.sid : void 0,
457
+ accessTokenId: typeof claims.jti === "string" ? claims.jti : "",
458
+ sessionId: claims.sid,
390
459
  clientId: typeof claims.client_id === "string" ? claims.client_id : void 0,
391
460
  roles,
392
461
  claims
@@ -411,7 +480,7 @@ var createAuth = (input) => {
411
480
  return dummyHash;
412
481
  };
413
482
  const sessionPolicy = (clientId) => {
414
- const override = clientId === void 0 ? void 0 : settings.clients?.sessions?.[clientId];
483
+ const override = clientId === void 0 ? void 0 : settings.clientSessions[clientId];
415
484
  if (!override) return settings.session;
416
485
  return {
417
486
  idleTimeout: override.idleTimeout ?? settings.session.idleTimeout,
@@ -423,8 +492,8 @@ var createAuth = (input) => {
423
492
  return absoluteTimeout === void 0 ? void 0 : authTime + absoluteTimeout;
424
493
  };
425
494
  const signAccessToken = async (claims) => {
426
- const { privateKey, kid } = await getKeys();
427
- const jwtId = crypto.randomUUID();
495
+ const { signingKey, kid } = await getKeys();
496
+ const accessTokenId = crypto.randomUUID();
428
497
  const issuedAt = nowInSeconds();
429
498
  const expiresAt = Math.min(
430
499
  issuedAt + settings.accessTokenTTL,
@@ -434,17 +503,21 @@ var createAuth = (input) => {
434
503
  // Personal data: written on demand only.
435
504
  ...settings.usernameClaim && claims.username && { username: claims.username },
436
505
  auth_time: claims.auth_time,
437
- ...claims.sid !== void 0 && { sid: claims.sid },
506
+ sid: claims.sid,
438
507
  ...claims.client_id !== void 0 && { client_id: claims.client_id }
439
- }).setProtectedHeader({ alg: ALGORITHM, typ: ACCESS_TOKEN_TYPE, kid }).setSubject(claims.sub).setIssuer(settings.issuer).setAudience(settings.audience).setJti(jwtId).setIssuedAt(issuedAt).setExpirationTime(expiresAt).sign(privateKey);
440
- return { jwtId, accessToken: token, expiresAt: new Date(expiresAt * 1e3) };
508
+ }).setProtectedHeader({ alg: ALGORITHM, typ: ACCESS_TOKEN_TYPE, kid }).setSubject(claims.sub).setIssuer(settings.issuer).setAudience(settings.audience).setJti(accessTokenId).setIssuedAt(issuedAt).setExpirationTime(expiresAt).sign(signingKey);
509
+ return {
510
+ accessTokenId,
511
+ accessToken: token,
512
+ expiresAt: new Date(expiresAt * 1e3)
513
+ };
441
514
  };
442
515
  const generateRefreshToken = async () => {
443
516
  const token = toHex(crypto.getRandomValues(new Uint8Array(128)));
444
517
  return { token, tokenHash: await sha256(token) };
445
518
  };
446
- const issueTokens = async (claims, familyId) => {
447
- const access = await signAccessToken({ ...claims, sid: familyId });
519
+ const issueTokens = async (claims, sessionId) => {
520
+ const access = await signAccessToken({ ...claims, sid: sessionId });
448
521
  const refreshToken = await generateRefreshToken();
449
522
  const end = sessionEnd(claims.auth_time, claims.client_id);
450
523
  const refreshTokenExpiresAt = new Date(
@@ -454,10 +527,10 @@ var createAuth = (input) => {
454
527
  )
455
528
  );
456
529
  await settings.dataProvider.storeRefreshToken({
457
- ...refreshToken,
530
+ tokenHash: refreshToken.tokenHash,
458
531
  userId: claims.sub,
459
- jwtId: access.jwtId,
460
- familyId,
532
+ accessTokenId: access.accessTokenId,
533
+ sessionId,
461
534
  expiresAt: refreshTokenExpiresAt
462
535
  });
463
536
  return {
@@ -473,17 +546,17 @@ var createAuth = (input) => {
473
546
  if (usedAt !== void 0 && Date.now() - usedAt < settings.refreshTokenReuseInterval * 1e3) {
474
547
  return;
475
548
  }
476
- const familyId = stored.familyId ?? void 0;
477
- if (familyId) {
478
- await settings.dataProvider.revokeTokenFamily(familyId);
549
+ const sessionId = stored.sessionId ?? void 0;
550
+ if (sessionId) {
551
+ await settings.dataProvider.revokeSessionTokens(sessionId);
479
552
  } else {
480
- await settings.dataProvider.revokeTokens(stored.userId);
553
+ await settings.dataProvider.revokeUserTokens(stored.userId);
481
554
  }
482
555
  try {
483
556
  await settings.onRefreshTokenReuse?.({
484
557
  userId: stored.userId,
485
- familyId,
486
- jwtId: stored.jwtId
558
+ sessionId,
559
+ accessTokenId: stored.accessTokenId
487
560
  });
488
561
  } catch {
489
562
  }
@@ -507,7 +580,6 @@ var createAuth = (input) => {
507
580
  client_id: clientId
508
581
  };
509
582
  };
510
- const generateAccessToken = async (subject) => signAccessToken(sessionClaims(subject));
511
583
  let routesPath;
512
584
  let cors;
513
585
  const isTrustedOrigin = (req) => (origin) => {
@@ -519,12 +591,12 @@ var createAuth = (input) => {
519
591
  const bindRoutesPath = (path) => {
520
592
  if (typeof path !== "string" || !path.startsWith("/")) {
521
593
  throw new TypeError(
522
- `Invalid routesPath: expected a path starting with "/", received ${JSON.stringify(path)}.`
594
+ `authPlugin(): the routes path must start with "/", received ${JSON.stringify(path)}.`
523
595
  );
524
596
  }
525
597
  if (routesPath !== void 0 && routesPath !== path) {
526
598
  throw new Error(
527
- `This auth service already serves routes under "${routesPath}": create one auth service per app to serve them under "${path}".`
599
+ `authPlugin(): this auth service already serves routes under "${routesPath}": create one auth service per app to serve them under "${path}".`
528
600
  );
529
601
  }
530
602
  routesPath = path;
@@ -539,16 +611,8 @@ var createAuth = (input) => {
539
611
  const { httpOnly, secure, sameSite, path, domain } = settings.cookies;
540
612
  return { httpOnly, secure, sameSite, path, domain };
541
613
  };
542
- return {
543
- settings,
614
+ const auth = {
544
615
  isCookieAuthenticationAllowed: (req) => isCookieAuthenticationAllowed(req, isTrustedOrigin(req)),
545
- init: async (options = {}) => {
546
- if (options.routesPath !== void 0) bindRoutesPath(options.routesPath);
547
- if (options.cors !== void 0) cors = options.cors;
548
- await Promise.all([getKeys(), getDummyHash()]);
549
- },
550
- generateAccessToken,
551
- generateRefreshToken,
552
616
  async generateTokensForUser(subject) {
553
617
  return issueTokens(sessionClaims(subject), crypto.randomUUID());
554
618
  },
@@ -574,7 +638,7 @@ var createAuth = (input) => {
574
638
  );
575
639
  const refreshTokenHash = await sha256(refreshToken);
576
640
  const stored = await settings.dataProvider.findRefreshToken(refreshTokenHash);
577
- if (!userId || !stored || stored.userId !== userId || stored.jwtId !== claims.jti || stored.revokedAt || // Client removed from the configuration: the session ends.
641
+ if (!userId || !stored || stored.userId !== userId || stored.accessTokenId !== claims.jti || stored.revokedAt || // Client removed from the configuration: the session ends.
578
642
  !isClientAccepted(clientId)) {
579
643
  throw new AuthError("INVALID_REFRESH_TOKEN");
580
644
  }
@@ -590,10 +654,10 @@ var createAuth = (input) => {
590
654
  }
591
655
  const user = await settings.dataProvider.findUserById(userId);
592
656
  if (!user) {
593
- if (stored.familyId) {
594
- await settings.dataProvider.revokeTokenFamily(stored.familyId);
657
+ if (stored.sessionId) {
658
+ await settings.dataProvider.revokeSessionTokens(stored.sessionId);
595
659
  } else {
596
- await settings.dataProvider.revokeTokens(userId);
660
+ await settings.dataProvider.revokeUserTokens(userId);
597
661
  }
598
662
  throw new AuthError("INVALID_REFRESH_TOKEN");
599
663
  }
@@ -604,9 +668,9 @@ var createAuth = (input) => {
604
668
  auth_time: authTime,
605
669
  client_id: clientId
606
670
  },
607
- // Tokens stored without family start one, derived from the token
608
- // so that concurrent rotations land in the same family.
609
- stored.familyId || stored.jwtId
671
+ // Tokens stored before sessions (V1) start one, derived from the
672
+ // token so that concurrent rotations land in the same session.
673
+ stored.sessionId || stored.accessTokenId
610
674
  );
611
675
  if (!stored.isUsed) {
612
676
  await settings.dataProvider.markRefreshTokenAsUsed(
@@ -652,19 +716,27 @@ var createAuth = (input) => {
652
716
  await sha256(refreshToken)
653
717
  );
654
718
  if (!stored) return void 0;
655
- const sessionId = stored.familyId || void 0;
719
+ const sessionId = stored.sessionId || void 0;
656
720
  if (scope === "all") {
657
721
  if (stored.revokedAt || stored.isUsed || stored.expiresAt.getTime() <= Date.now()) {
658
722
  return void 0;
659
723
  }
660
- await dataProvider.revokeTokens(stored.userId);
724
+ await dataProvider.revokeUserTokens(stored.userId);
661
725
  } else if (sessionId) {
662
- await dataProvider.revokeTokenFamily(sessionId);
726
+ await dataProvider.revokeSessionTokens(sessionId);
663
727
  } else {
664
- await dataProvider.revokeTokens(stored.userId);
728
+ await dataProvider.revokeUserTokens(stored.userId);
665
729
  }
666
730
  return { userId: stored.userId, sessionId };
667
731
  },
732
+ async revokeUserSessions(userId) {
733
+ if (typeof userId !== "string" || userId === "") {
734
+ throw new TypeError(
735
+ "revokeUserSessions(): userId must be a non-empty string."
736
+ );
737
+ }
738
+ await settings.dataProvider.revokeUserTokens(userId);
739
+ },
668
740
  async getJwks() {
669
741
  const { verificationKeys } = await getKeys();
670
742
  return {
@@ -683,13 +755,13 @@ var createAuth = (input) => {
683
755
  return end === void 0 ? void 0 : new Date(end * 1e3);
684
756
  },
685
757
  getUserRoles: (user) => settings.getUserRoles(user),
686
- setAuthCookies(res, tokens, expiresInSeconds) {
758
+ setAuthCookies(res, tokens) {
687
759
  const cookies = settings.cookies;
688
760
  if (!cookies.enabled) return;
689
761
  const expiresAt = tokens.refreshTokenExpiresAt?.getTime();
690
762
  const options = {
691
763
  ...cookieOptions(),
692
- maxAge: expiresInSeconds ?? (expiresAt === void 0 ? settings.session.idleTimeout : Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3)))
764
+ maxAge: expiresAt === void 0 ? settings.session.idleTimeout : Math.max(0, Math.ceil((expiresAt - Date.now()) / 1e3))
693
765
  };
694
766
  res.setCookie(cookies.refreshTokenName, tokens.refreshToken, {
695
767
  ...options,
@@ -711,7 +783,7 @@ var createAuth = (input) => {
711
783
  res.clearCookie(cookies.accessTokenName, cookieOptions());
712
784
  }
713
785
  },
714
- hashPassword: (password, saltRounds = settings.passwordHashRounds) => generatePasswordHash(password, saltRounds),
786
+ hashPassword: (password) => generatePasswordHash(password, { rounds: settings.passwordHashRounds }),
715
787
  passwordNeedsRehash: (passwordHash) => BCRYPT_HASH.test(passwordHash) && bcrypt.getRounds(passwordHash) !== settings.passwordHashRounds,
716
788
  // No length check: the password was verified, and a longer V1 password
717
789
  // is truncated the same way.
@@ -724,16 +796,39 @@ var createAuth = (input) => {
724
796
  return false;
725
797
  }
726
798
  };
799
+ internals.set(auth, {
800
+ settings,
801
+ init: async (options = {}) => {
802
+ if (options.routesPath !== void 0) bindRoutesPath(options.routesPath);
803
+ if (options.cors !== void 0) cors = options.cors;
804
+ await Promise.all([getKeys(), getDummyHash()]);
805
+ }
806
+ });
807
+ return auth;
727
808
  };
728
809
  var MAX_PASSWORD_BYTES = 72;
729
810
  var isPasswordTooLong = (password) => bcrypt.truncates(password);
730
- var generatePasswordHash = async (password, saltRounds = 12) => {
811
+ var generatePasswordHash = async (password, options = {}) => {
812
+ if (typeof options !== "object" || options === null) {
813
+ throw new TypeError("generatePasswordHash(): options must be an object.");
814
+ }
815
+ for (const key of Object.keys(options)) {
816
+ if (key !== "rounds") {
817
+ throw new TypeError(`generatePasswordHash(): unknown option "${key}".`);
818
+ }
819
+ }
820
+ const { rounds = 12 } = options;
821
+ if (!Number.isInteger(rounds) || rounds < 4 || rounds > 31) {
822
+ throw new RangeError(
823
+ "generatePasswordHash(): rounds must be an integer between 4 and 31."
824
+ );
825
+ }
731
826
  if (isPasswordTooLong(password)) {
732
827
  throw new RangeError(
733
828
  `Password too long: bcrypt reads ${MAX_PASSWORD_BYTES} bytes at most (UTF-8). Check it with isPasswordTooLong().`
734
829
  );
735
830
  }
736
- return bcrypt.hash(password, saltRounds);
831
+ return bcrypt.hash(password, rounds);
737
832
  };
738
833
 
739
834
  // src/middlewares.ts
@@ -749,7 +844,7 @@ var requireAuth = defineMiddleware(
749
844
  );
750
845
  var requireRole = (...roles) => {
751
846
  if (roles.length === 0) {
752
- throw new TypeError("requireRole() expects at least one role.");
847
+ throw new TypeError("requireRole(): expected at least one role.");
753
848
  }
754
849
  return defineMiddleware((ctx) => {
755
850
  if (!ctx.user || !ctx.auth) return unauthorizedError();
@@ -759,7 +854,7 @@ var requireRole = (...roles) => {
759
854
  };
760
855
  var requireClient = (...clientIds) => {
761
856
  if (clientIds.length === 0) {
762
- throw new TypeError("requireClient() expects at least one client.");
857
+ throw new TypeError("requireClient(): expected at least one client.");
763
858
  }
764
859
  return defineMiddleware((ctx) => {
765
860
  if (!ctx.user || !ctx.auth) return unauthorizedError();
@@ -776,6 +871,11 @@ import {
776
871
  tooManyRequestsError,
777
872
  unauthorizedError as unauthorizedError2
778
873
  } from "@dotpress/core";
874
+ var PLUGIN_OPTIONS = [
875
+ "prefix",
876
+ "routes",
877
+ "jwks"
878
+ ];
779
879
  var AUTH_SECURITY_SCHEMES = {
780
880
  bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" }
781
881
  };
@@ -802,7 +902,6 @@ var findAccessToken = (req, cookies) => {
802
902
  }
803
903
  return void 0;
804
904
  };
805
- var extractAccessToken = (req, cookies) => findAccessToken(req, cookies)?.token;
806
905
  var loginThrottleKeys = async (throttle, username, ipAddress) => {
807
906
  const normalized = username.trim().toLowerCase();
808
907
  const { usernameAndIp, username: perUsername } = throttle.limits;
@@ -838,8 +937,7 @@ var refreshErrorResponse = (error, fallback) => {
838
937
  }
839
938
  return internalError(fallback);
840
939
  };
841
- var registerRoutes = (group, auth) => {
842
- const { settings } = auth;
940
+ var registerRoutes = (group, auth, settings) => {
843
941
  const { dataProvider, cookies } = settings;
844
942
  const expiration = (accessTokenExpiresAt, sessionExpiresAt) => ({
845
943
  expiresIn: Math.max(
@@ -944,7 +1042,11 @@ var registerRoutes = (group, auth) => {
944
1042
  if (!passwordValid) {
945
1043
  return loginFailed({ ...event, errorType: "invalid_password" });
946
1044
  }
947
- if (clientId !== void 0 && clients?.isUserRoleAllowed && !clients.isUserRoleAllowed(user.role ?? "", clientId)) {
1045
+ if (clientId !== void 0 && clients?.isUserRoleAllowed && !clients.isUserRoleAllowed({
1046
+ userId: user.userId,
1047
+ role: user.role,
1048
+ clientId
1049
+ })) {
948
1050
  return loginFailed({ ...event, errorType: "invalid_role" });
949
1051
  }
950
1052
  const tokens = await auth.generateTokensForUser({
@@ -1123,9 +1225,9 @@ var registerRoutes = (group, auth) => {
1123
1225
  const revoked = refreshToken ? await auth.revokeSession(refreshToken, { scope }) : void 0;
1124
1226
  if (!revoked && session) {
1125
1227
  if (scope === "all") {
1126
- await dataProvider.revokeTokens(session.userId);
1228
+ await dataProvider.revokeUserTokens(session.userId);
1127
1229
  } else if (session.sessionId) {
1128
- await dataProvider.revokeTokenFamily(session.sessionId);
1230
+ await dataProvider.revokeSessionTokens(session.sessionId);
1129
1231
  }
1130
1232
  } else if (!revoked && scope === "all") {
1131
1233
  return unauthorizedError2();
@@ -1137,13 +1239,28 @@ var registerRoutes = (group, auth) => {
1137
1239
  };
1138
1240
  var normalizePrefix = (prefix) => `/${prefix.split("/").filter(Boolean).join("/")}`;
1139
1241
  var authPlugin = (auth, options = {}) => {
1242
+ const internal = internalsOf(auth);
1243
+ if (!internal) {
1244
+ throw new TypeError(
1245
+ "authPlugin(): expected an auth service created with createAuth()."
1246
+ );
1247
+ }
1248
+ if (typeof options !== "object" || options === null) {
1249
+ throw new TypeError("authPlugin(): options must be an object.");
1250
+ }
1251
+ for (const key of Object.keys(options)) {
1252
+ if (!PLUGIN_OPTIONS.includes(key)) {
1253
+ throw new TypeError(`authPlugin(): unknown option "${key}".`);
1254
+ }
1255
+ }
1256
+ const { settings } = internal;
1140
1257
  const prefix = options.prefix ?? "/auth";
1141
1258
  return definePlugin({
1142
1259
  name: "auth",
1143
1260
  auth,
1144
1261
  securitySchemes: AUTH_SECURITY_SCHEMES,
1145
1262
  async setup(api) {
1146
- await auth.init({
1263
+ await internal.init({
1147
1264
  ...options.routes === false ? {} : { routesPath: api.publicPath(normalizePrefix(prefix)) },
1148
1265
  ...api.cors ? { cors: api.cors } : {}
1149
1266
  });
@@ -1151,8 +1268,7 @@ var authPlugin = (auth, options = {}) => {
1151
1268
  api.decorateContext("auth", () => void 0);
1152
1269
  const rejectedTokens = /* @__PURE__ */ new WeakSet();
1153
1270
  const authenticate = async (ctx) => {
1154
- const { cookies } = auth.settings;
1155
- const found = findAccessToken(ctx.req, cookies);
1271
+ const found = findAccessToken(ctx.req, settings.cookies);
1156
1272
  if (!found) return;
1157
1273
  const { token } = found;
1158
1274
  if (found.source === "cookie" && !await auth.isCookieAuthenticationAllowed(ctx.req)) {
@@ -1164,9 +1280,7 @@ var authPlugin = (auth, options = {}) => {
1164
1280
  }
1165
1281
  try {
1166
1282
  const session = await auth.verifyAccessToken(token);
1167
- const user = await auth.settings.dataProvider.findUserById(
1168
- session.userId
1169
- );
1283
+ const user = await settings.dataProvider.findUserById(session.userId);
1170
1284
  if (!user) {
1171
1285
  if (found.source === "header") rejectedTokens.add(ctx);
1172
1286
  return;
@@ -1197,7 +1311,7 @@ var authPlugin = (auth, options = {}) => {
1197
1311
  };
1198
1312
  });
1199
1313
  if (options.jwks) {
1200
- api.addRoute({
1314
+ api.defineRoute({
1201
1315
  path: "/.well-known/jwks.json",
1202
1316
  meta: { tags: ["auth"], summary: "Public keys of the access tokens" },
1203
1317
  handler: async ({ res }) => {
@@ -1207,7 +1321,7 @@ var authPlugin = (auth, options = {}) => {
1207
1321
  });
1208
1322
  }
1209
1323
  if (options.routes !== false) {
1210
- registerRoutes(api.addRouteGroup(prefix), auth);
1324
+ registerRoutes(api.createGroup(prefix), auth, settings);
1211
1325
  }
1212
1326
  }
1213
1327
  });
@@ -1215,13 +1329,10 @@ var authPlugin = (auth, options = {}) => {
1215
1329
  export {
1216
1330
  AUTH_SECURITY_SCHEMES,
1217
1331
  AuthError,
1218
- MAX_PASSWORD_BYTES,
1219
1332
  authPlugin,
1220
1333
  createAuth,
1221
- extractAccessToken,
1222
1334
  generatePasswordHash,
1223
1335
  isPasswordTooLong,
1224
- memoryRateLimitStore,
1225
1336
  requireAuth,
1226
1337
  requireClient,
1227
1338
  requireRole