@dudousxd/nestjs-durable-dashboard 0.37.0 → 0.37.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.
@@ -18,9 +18,11 @@ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> |
18
18
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
19
19
  /**
20
20
  * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per
21
- * session, so a DB round-trip here is cheap. Return `false` to revoke (the cookie is cleared and
22
- * the request denied). Distinct from `session`: that hook reads the host's auth off a fresh
23
- * request, which a console XHR does not carry — this one receives the already-minted session.
21
+ * session, so a DB round-trip here is cheap — this holds even under concurrent traffic: N parallel
22
+ * requests carrying the same past-half-life cookie share ONE call (see `revalidateOnce` in
23
+ * `session-cookie-io.ts`), not N. Return `false` to revoke (the cookie is cleared and the request
24
+ * denied). Distinct from `session`: that hook reads the host's auth off a fresh request, which a
25
+ * console XHR does not carry — this one receives the already-minted session.
24
26
  */
25
27
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
26
28
  type AuthMode = 'session' | 'login';
@@ -418,6 +420,13 @@ declare class DurableApiSessionGuard implements CanActivate {
418
420
  private readonly auth;
419
421
  constructor(auth: ResolvedDashboardAuth | null);
420
422
  canActivate(context: ExecutionContext): Promise<boolean>;
423
+ /**
424
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
425
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
426
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
427
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
428
+ */
429
+ private unauthorized;
421
430
  }
422
431
 
423
432
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -18,9 +18,11 @@ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> |
18
18
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
19
19
  /**
20
20
  * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per
21
- * session, so a DB round-trip here is cheap. Return `false` to revoke (the cookie is cleared and
22
- * the request denied). Distinct from `session`: that hook reads the host's auth off a fresh
23
- * request, which a console XHR does not carry — this one receives the already-minted session.
21
+ * session, so a DB round-trip here is cheap — this holds even under concurrent traffic: N parallel
22
+ * requests carrying the same past-half-life cookie share ONE call (see `revalidateOnce` in
23
+ * `session-cookie-io.ts`), not N. Return `false` to revoke (the cookie is cleared and the request
24
+ * denied). Distinct from `session`: that hook reads the host's auth off a fresh request, which a
25
+ * console XHR does not carry — this one receives the already-minted session.
24
26
  */
25
27
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
26
28
  type AuthMode = 'session' | 'login';
@@ -418,6 +420,13 @@ declare class DurableApiSessionGuard implements CanActivate {
418
420
  private readonly auth;
419
421
  constructor(auth: ResolvedDashboardAuth | null);
420
422
  canActivate(context: ExecutionContext): Promise<boolean>;
423
+ /**
424
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
425
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
426
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
427
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
428
+ */
429
+ private unauthorized;
421
430
  }
422
431
 
423
432
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -1014,6 +1014,20 @@ function readSessionFromRequest(auth, request) {
1014
1014
  });
1015
1015
  }
1016
1016
  __name(readSessionFromRequest, "readSessionFromRequest");
1017
+ var inFlightRevalidations = /* @__PURE__ */ new Map();
1018
+ function revalidateOnce(auth, user) {
1019
+ const revalidate = auth.revalidate;
1020
+ if (!revalidate) return Promise.resolve(true);
1021
+ const key = `${auth.secret}:${user.id}`;
1022
+ const inFlight = inFlightRevalidations.get(key);
1023
+ if (inFlight) return inFlight;
1024
+ const call = Promise.resolve().then(() => revalidate(user)).catch(() => false).finally(() => {
1025
+ inFlightRevalidations.delete(key);
1026
+ });
1027
+ inFlightRevalidations.set(key, call);
1028
+ return call;
1029
+ }
1030
+ __name(revalidateOnce, "revalidateOnce");
1017
1031
  async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
1018
1032
  if (now - session.iat <= auth.ttlMs / 2) return true;
1019
1033
  const user = {
@@ -1023,20 +1037,13 @@ async function maybeRenewSession(auth, session, request, response, now = Date.no
1023
1037
  } : {},
1024
1038
  roles: session.roles
1025
1039
  };
1026
- if (auth.revalidate) {
1027
- let allowed;
1028
- try {
1029
- allowed = await auth.revalidate(user);
1030
- } catch {
1031
- allowed = false;
1032
- }
1033
- if (!allowed) {
1034
- clearSessionCookie({
1035
- request,
1036
- response
1037
- });
1038
- return false;
1039
- }
1040
+ const allowed = await revalidateOnce(auth, user);
1041
+ if (!allowed) {
1042
+ clearSessionCookie({
1043
+ request,
1044
+ response
1045
+ });
1046
+ return false;
1040
1047
  }
1041
1048
  issueSessionCookie(user, {
1042
1049
  auth,
@@ -1260,7 +1267,7 @@ var DurableAuthController = class _DurableAuthController {
1260
1267
  // the developer-controlled `basePath`.
1261
1268
  loginPage() {
1262
1269
  const auth = this.requireAuth();
1263
- if (!auth.login) throw new NotFoundException3();
1270
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1264
1271
  return renderLoginPage(this.basePath);
1265
1272
  }
1266
1273
  // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for
@@ -1269,7 +1276,7 @@ var DurableAuthController = class _DurableAuthController {
1269
1276
  // (unknown user, wrong password, throwing hook) — no user-enumeration.
1270
1277
  async login(body, request, response) {
1271
1278
  const auth = this.requireAuth();
1272
- if (!auth.login) throw new NotFoundException3();
1279
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1273
1280
  if (!isString(body?.username) || !isString(body?.password)) {
1274
1281
  throw new BadRequestException("Body must include string `username` and `password`.");
1275
1282
  }
@@ -1292,7 +1299,7 @@ var DurableAuthController = class _DurableAuthController {
1292
1299
  // guard decides, and no credential ever reaches this library.
1293
1300
  async session(request, response) {
1294
1301
  const auth = this.requireAuth();
1295
- if (!auth.session) throw new NotFoundException3();
1302
+ if (!auth.modes.includes("session")) throw new NotFoundException3();
1296
1303
  const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1297
1304
  if (!user) throw new UnauthorizedException();
1298
1305
  issueSessionCookie(user, {
@@ -1310,7 +1317,7 @@ var DurableAuthController = class _DurableAuthController {
1310
1317
  request,
1311
1318
  response
1312
1319
  });
1313
- redirectRaw(response, this.auth?.login ? `${this.basePath}/login` : this.basePath);
1320
+ redirectRaw(response, this.auth?.modes.includes("login") ? `${this.basePath}/login` : this.basePath);
1314
1321
  }
1315
1322
  requireAuth() {
1316
1323
  if (!this.auth) throw new NotFoundException3();
@@ -1453,7 +1460,7 @@ var DurableUiSessionGuard = class {
1453
1460
  * asserting away) the nullability.
1454
1461
  */
1455
1462
  denyUnauthenticated(auth, request) {
1456
- if (!auth.login) throw new DashboardSessionRequiredException(this.basePath);
1463
+ if (!auth.modes.includes("login")) throw new DashboardSessionRequiredException(this.basePath);
1457
1464
  const returnTo = encodeURIComponent(originalRequestUrl(request));
1458
1465
  throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1459
1466
  }
@@ -1478,15 +1485,29 @@ var DurableApiSessionGuard = class {
1478
1485
  }
1479
1486
  async canActivate(context) {
1480
1487
  if (!this.auth) return true;
1488
+ const auth = this.auth;
1481
1489
  const http = context.switchToHttp();
1482
1490
  const request = http.getRequest();
1483
- const session = readSessionFromRequest(this.auth, request);
1484
- if (!session) throw new UnauthorizedException2();
1485
- if (!await maybeRenewSession(this.auth, session, request, http.getResponse())) {
1486
- throw new UnauthorizedException2();
1491
+ const session = readSessionFromRequest(auth, request);
1492
+ if (!session) throw this.unauthorized(auth);
1493
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1494
+ throw this.unauthorized(auth);
1487
1495
  }
1488
1496
  return true;
1489
1497
  }
1498
+ /**
1499
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
1500
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
1501
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
1502
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
1503
+ */
1504
+ unauthorized(auth) {
1505
+ return new UnauthorizedException2({
1506
+ auth: {
1507
+ modes: auth.modes
1508
+ }
1509
+ });
1510
+ }
1490
1511
  };
1491
1512
  DurableApiSessionGuard = _ts_decorate7([
1492
1513
  Injectable2(),