@dudousxd/nestjs-durable-dashboard 0.36.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.
@@ -543,28 +543,51 @@ function resolveDashboardAuth(options) {
543
543
  if (typeof options.secret !== "string" || options.secret === "") {
544
544
  throw new Error("DurableDashboardModule: dashboardAuth.secret is required and must be a non-empty string (HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.");
545
545
  }
546
- if (typeof options.login !== "function") {
547
- throw new Error("DurableDashboardModule: dashboardAuth.login is required (a login hook).");
546
+ const modes = [];
547
+ if (options.session !== void 0) {
548
+ if (typeof options.session !== "function") {
549
+ throw new Error("DurableDashboardModule: dashboardAuth.session must be a function.");
550
+ }
551
+ modes.push("session");
552
+ }
553
+ if (options.login !== void 0) {
554
+ if (typeof options.login !== "function") {
555
+ throw new Error("DurableDashboardModule: dashboardAuth.login must be a function.");
556
+ }
557
+ modes.push("login");
558
+ }
559
+ if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
560
+ throw new Error("DurableDashboardModule: dashboardAuth.revalidate must be a function.");
561
+ }
562
+ if (modes.length === 0) {
563
+ throw new Error("DurableDashboardModule: dashboardAuth needs at least one of `session` or `login` (otherwise the cookie can never be minted). Failing closed.");
548
564
  }
549
565
  return {
550
566
  secret: options.secret,
551
567
  ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
552
- login: options.login
568
+ modes,
569
+ ...options.session !== void 0 ? {
570
+ session: options.session
571
+ } : {},
572
+ ...options.login !== void 0 ? {
573
+ login: options.login
574
+ } : {},
575
+ ...options.revalidate !== void 0 ? {
576
+ revalidate: options.revalidate
577
+ } : {}
553
578
  };
554
579
  }
555
580
  __name(resolveDashboardAuth, "resolveDashboardAuth");
556
581
 
557
582
  // src/server/auth/login-page.ts
558
- function renderLoginPage(basePath) {
559
- const loginAction = `${basePath}/login`;
560
- const defaultReturnTo = basePath;
583
+ function page(content, title = "Durable") {
561
584
  return `<!doctype html>
562
585
  <html lang="en">
563
586
  <head>
564
587
  <meta charset="utf-8" />
565
588
  <meta name="viewport" content="width=device-width, initial-scale=1" />
566
589
  <meta name="robots" content="noindex, nofollow" />
567
- <title>Sign in \u2014 Durable</title>
590
+ <title>${title}</title>
568
591
  <style>
569
592
  :root { color-scheme: dark; }
570
593
  * { box-sizing: border-box; }
@@ -595,6 +618,13 @@ function renderLoginPage(basePath) {
595
618
  font-weight: 600;
596
619
  color: #34d399;
597
620
  }
621
+ h1 {
622
+ margin: 0 0 12px;
623
+ font-size: 16px;
624
+ font-weight: 600;
625
+ color: #f4f4f5;
626
+ }
627
+ p { margin: 0 0 16px; color: #a1a1aa; }
598
628
  form { display: flex; flex-direction: column; gap: 16px; }
599
629
  label { display: flex; flex-direction: column; gap: 6px; }
600
630
  .field-label {
@@ -619,7 +649,7 @@ function renderLoginPage(basePath) {
619
649
  font-size: 12px;
620
650
  color: #fb7185;
621
651
  }
622
- button {
652
+ button, .button {
623
653
  margin-top: 8px;
624
654
  border-radius: 4px;
625
655
  border: 1px solid rgb(52 211 153 / 0.4);
@@ -632,14 +662,27 @@ function renderLoginPage(basePath) {
632
662
  text-transform: uppercase;
633
663
  letter-spacing: 0.05em;
634
664
  cursor: pointer;
665
+ display: inline-block;
666
+ text-align: center;
667
+ text-decoration: none;
668
+ box-sizing: border-box;
635
669
  }
636
- button:hover:not(:disabled) { background: rgb(52 211 153 / 0.2); }
670
+ button:hover:not(:disabled), .button:hover { background: rgb(52 211 153 / 0.2); }
637
671
  button:disabled { opacity: 0.6; cursor: default; }
638
672
  </style>
639
673
  </head>
640
674
  <body>
641
675
  <div class="card">
642
- <p class="brand">Durable</p>
676
+ ${content}
677
+ </div>
678
+ </body>
679
+ </html>`;
680
+ }
681
+ __name(page, "page");
682
+ function renderLoginPage(basePath) {
683
+ const loginAction = `${basePath}/login`;
684
+ const defaultReturnTo = basePath;
685
+ return page(`<p class="brand">Durable</p>
643
686
  <form id="login-form" autocomplete="on">
644
687
  <label>
645
688
  <span class="field-label">Username</span>
@@ -652,7 +695,6 @@ function renderLoginPage(basePath) {
652
695
  <p id="error" role="alert">Invalid username or password.</p>
653
696
  <button id="submit" type="submit">Sign in</button>
654
697
  </form>
655
- </div>
656
698
  <script>
657
699
  (function () {
658
700
  var LOGIN_ACTION = ${JSON.stringify(loginAction)};
@@ -699,11 +741,15 @@ function renderLoginPage(basePath) {
699
741
  });
700
742
  });
701
743
  })();
702
- </script>
703
- </body>
704
- </html>`;
744
+ </script>`, "Sign in \u2014 Durable");
705
745
  }
706
746
  __name(renderLoginPage, "renderLoginPage");
747
+ function renderSessionRequiredPage(basePath) {
748
+ return page(`<h1>Open this console from your application</h1>
749
+ <p>Your session is minted by the host app. Use its console launcher to sign in, then reload.</p>
750
+ <a class="button" href="${basePath}">Retry</a>`);
751
+ }
752
+ __name(renderSessionRequiredPage, "renderSessionRequiredPage");
707
753
 
708
754
  // src/server/auth/request.ts
709
755
  function isRecord(value) {
@@ -804,6 +850,15 @@ function redirectRaw(response, location, status = 302) {
804
850
  raw.end();
805
851
  }
806
852
  __name(redirectRaw, "redirectRaw");
853
+ function writeHtmlRaw(response, html, status = 200) {
854
+ const raw = resolveRedirectResponse(response);
855
+ if (!raw) return;
856
+ raw.statusCode = status;
857
+ raw.setHeader("content-type", "text/html; charset=utf-8");
858
+ raw.setHeader("cache-control", "no-store, must-revalidate");
859
+ raw.end(html);
860
+ }
861
+ __name(writeHtmlRaw, "writeHtmlRaw");
807
862
 
808
863
  // src/server/auth/cookie-header.ts
809
864
  function parseCookieHeader(header) {
@@ -959,21 +1014,44 @@ function readSessionFromRequest(auth, request) {
959
1014
  });
960
1015
  }
961
1016
  __name(readSessionFromRequest, "readSessionFromRequest");
962
- function maybeRenewSession(auth, session, request, response) {
963
- const now = Date.now();
964
- if (now - session.iat <= auth.ttlMs / 2) return;
965
- issueSessionCookie({
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");
1031
+ async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
1032
+ if (now - session.iat <= auth.ttlMs / 2) return true;
1033
+ const user = {
966
1034
  id: session.sub,
967
1035
  ...session.name !== void 0 ? {
968
1036
  name: session.name
969
1037
  } : {},
970
1038
  roles: session.roles
971
- }, {
1039
+ };
1040
+ const allowed = await revalidateOnce(auth, user);
1041
+ if (!allowed) {
1042
+ clearSessionCookie({
1043
+ request,
1044
+ response
1045
+ });
1046
+ return false;
1047
+ }
1048
+ issueSessionCookie(user, {
972
1049
  auth,
973
1050
  request,
974
1051
  response,
975
1052
  now
976
1053
  });
1054
+ return true;
977
1055
  }
978
1056
  __name(maybeRenewSession, "maybeRenewSession");
979
1057
 
@@ -1016,7 +1094,8 @@ DashboardLoginRedirectFilter = _ts_decorate3([
1016
1094
  Catch(DashboardLoginRedirectException)
1017
1095
  ], DashboardLoginRedirectFilter);
1018
1096
 
1019
- // src/server/durable-ui.controller.ts
1097
+ // src/server/auth/session-required.exception.ts
1098
+ import { Catch as Catch2, HttpException as HttpException2, HttpStatus as HttpStatus2 } from "@nestjs/common";
1020
1099
  function _ts_decorate4(decorators, target, key, desc) {
1021
1100
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1022
1101
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1024,6 +1103,38 @@ function _ts_decorate4(decorators, target, key, desc) {
1024
1103
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1025
1104
  }
1026
1105
  __name(_ts_decorate4, "_ts_decorate");
1106
+ var DashboardSessionRequiredException = class extends HttpException2 {
1107
+ static {
1108
+ __name(this, "DashboardSessionRequiredException");
1109
+ }
1110
+ basePath;
1111
+ constructor(basePath) {
1112
+ super({
1113
+ basePath
1114
+ }, HttpStatus2.UNAUTHORIZED), this.basePath = basePath;
1115
+ }
1116
+ };
1117
+ var DashboardSessionRequiredFilter = class {
1118
+ static {
1119
+ __name(this, "DashboardSessionRequiredFilter");
1120
+ }
1121
+ catch(exception, host) {
1122
+ const response = host.switchToHttp().getResponse();
1123
+ writeHtmlRaw(response, renderSessionRequiredPage(exception.basePath), HttpStatus2.UNAUTHORIZED);
1124
+ }
1125
+ };
1126
+ DashboardSessionRequiredFilter = _ts_decorate4([
1127
+ Catch2(DashboardSessionRequiredException)
1128
+ ], DashboardSessionRequiredFilter);
1129
+
1130
+ // src/server/durable-ui.controller.ts
1131
+ function _ts_decorate5(decorators, target, key, desc) {
1132
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1133
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1134
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1135
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1136
+ }
1137
+ __name(_ts_decorate5, "_ts_decorate");
1027
1138
  function _ts_metadata3(k, v) {
1028
1139
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1029
1140
  }
@@ -1086,7 +1197,7 @@ var DurableUiController = class {
1086
1197
  });
1087
1198
  }
1088
1199
  };
1089
- _ts_decorate4([
1200
+ _ts_decorate5([
1090
1201
  Get2(),
1091
1202
  Header2("Content-Type", "text/html; charset=utf-8"),
1092
1203
  Header2("Cache-Control", "no-store, must-revalidate"),
@@ -1094,7 +1205,7 @@ _ts_decorate4([
1094
1205
  _ts_metadata3("design:paramtypes", []),
1095
1206
  _ts_metadata3("design:returntype", String)
1096
1207
  ], DurableUiController.prototype, "index", null);
1097
- _ts_decorate4([
1208
+ _ts_decorate5([
1098
1209
  Get2("assets/:file"),
1099
1210
  Header2("Cache-Control", "public, max-age=31536000, immutable"),
1100
1211
  _ts_param3(0, Param2("file")),
@@ -1104,8 +1215,8 @@ _ts_decorate4([
1104
1215
  ]),
1105
1216
  _ts_metadata3("design:returntype", typeof StreamableFile === "undefined" ? Object : StreamableFile)
1106
1217
  ], DurableUiController.prototype, "asset", null);
1107
- DurableUiController = _ts_decorate4([
1108
- UseFilters(DashboardLoginRedirectFilter),
1218
+ DurableUiController = _ts_decorate5([
1219
+ UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter),
1109
1220
  Controller2(),
1110
1221
  _ts_param3(0, Inject2(DASHBOARD_BASE_PATH)),
1111
1222
  _ts_param3(1, Inject2(DASHBOARD_API_PATH)),
@@ -1117,13 +1228,13 @@ DurableUiController = _ts_decorate4([
1117
1228
  ], DurableUiController);
1118
1229
 
1119
1230
  // src/server/durable-auth.controller.ts
1120
- function _ts_decorate5(decorators, target, key, desc) {
1231
+ function _ts_decorate6(decorators, target, key, desc) {
1121
1232
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1122
1233
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1123
1234
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1124
1235
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1125
1236
  }
1126
- __name(_ts_decorate5, "_ts_decorate");
1237
+ __name(_ts_decorate6, "_ts_decorate");
1127
1238
  function _ts_metadata4(k, v) {
1128
1239
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1129
1240
  }
@@ -1145,8 +1256,8 @@ var DurableAuthController = class _DurableAuthController {
1145
1256
  auth;
1146
1257
  basePath;
1147
1258
  logger = new Logger(_DurableAuthController.name);
1148
- /** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
1149
- warnedOnHookThrow = false;
1259
+ /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
1260
+ warnedHooks = /* @__PURE__ */ new Set();
1150
1261
  constructor(auth, basePath) {
1151
1262
  this.auth = auth;
1152
1263
  this.basePath = basePath;
@@ -1155,7 +1266,8 @@ var DurableAuthController = class _DurableAuthController {
1155
1266
  // `login-page.ts`), so this response never varies per request and needs no templating beyond
1156
1267
  // the developer-controlled `basePath`.
1157
1268
  loginPage() {
1158
- if (!this.auth) throw new NotFoundException3();
1269
+ const auth = this.requireAuth();
1270
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1159
1271
  return renderLoginPage(this.basePath);
1160
1272
  }
1161
1273
  // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for
@@ -1164,10 +1276,12 @@ var DurableAuthController = class _DurableAuthController {
1164
1276
  // (unknown user, wrong password, throwing hook) — no user-enumeration.
1165
1277
  async login(body, request, response) {
1166
1278
  const auth = this.requireAuth();
1279
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1167
1280
  if (!isString(body?.username) || !isString(body?.password)) {
1168
1281
  throw new BadRequestException("Body must include string `username` and `password`.");
1169
1282
  }
1170
- const user = await this.runLoginHook(auth, body.username, body.password);
1283
+ const { username, password } = body;
1284
+ const user = await this.runHook("login", () => auth.login?.(username, password) ?? null);
1171
1285
  if (!user) throw new UnauthorizedException({
1172
1286
  message: "Invalid username or password."
1173
1287
  });
@@ -1180,6 +1294,20 @@ var DurableAuthController = class _DurableAuthController {
1180
1294
  redirectTo: sanitizeReturnTo(body.returnTo, this.basePath)
1181
1295
  };
1182
1296
  }
1297
+ // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw
1298
+ // request and returns the session user, or `null` to deny. This is the SSO path — the host's own
1299
+ // guard decides, and no credential ever reaches this library.
1300
+ async session(request, response) {
1301
+ const auth = this.requireAuth();
1302
+ if (!auth.modes.includes("session")) throw new NotFoundException3();
1303
+ const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1304
+ if (!user) throw new UnauthorizedException();
1305
+ issueSessionCookie(user, {
1306
+ auth,
1307
+ request,
1308
+ response
1309
+ });
1310
+ }
1183
1311
  // Plain `GET` (not `POST`): logging out only ever destroys the CALLER's own session, so it's
1184
1312
  // safe/idempotent from the caller's perspective — a simple `<a href>` (no JS, no CSRF token)
1185
1313
  // works. Non-passthrough `@Res()`: this handler owns the response outright (clears the cookie,
@@ -1189,30 +1317,30 @@ var DurableAuthController = class _DurableAuthController {
1189
1317
  request,
1190
1318
  response
1191
1319
  });
1192
- redirectRaw(response, `${this.basePath}/login`);
1320
+ redirectRaw(response, this.auth?.modes.includes("login") ? `${this.basePath}/login` : this.basePath);
1193
1321
  }
1194
1322
  requireAuth() {
1195
1323
  if (!this.auth) throw new NotFoundException3();
1196
1324
  return this.auth;
1197
1325
  }
1198
1326
  /**
1199
- * Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
1200
- * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
1201
- * the logs on repeated bad attempts.
1327
+ * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
1328
+ * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a
1329
+ * stack-trace leak nor floods the logs on repeated bad attempts.
1202
1330
  */
1203
- async runLoginHook(auth, username, password) {
1331
+ async runHook(kind, run) {
1204
1332
  try {
1205
- return await auth.login(username, password) ?? null;
1333
+ return await run() ?? null;
1206
1334
  } catch (error) {
1207
- if (!this.warnedOnHookThrow) {
1208
- this.warnedOnHookThrow = true;
1209
- this.logger.warn(`dashboardAuth login hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1335
+ if (!this.warnedHooks.has(kind)) {
1336
+ this.warnedHooks.add(kind);
1337
+ this.logger.warn(`dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1210
1338
  }
1211
1339
  return null;
1212
1340
  }
1213
1341
  }
1214
1342
  };
1215
- _ts_decorate5([
1343
+ _ts_decorate6([
1216
1344
  Get3("login"),
1217
1345
  Header3("Content-Type", "text/html; charset=utf-8"),
1218
1346
  Header3("Cache-Control", "no-store, must-revalidate"),
@@ -1220,7 +1348,7 @@ _ts_decorate5([
1220
1348
  _ts_metadata4("design:paramtypes", []),
1221
1349
  _ts_metadata4("design:returntype", String)
1222
1350
  ], DurableAuthController.prototype, "loginPage", null);
1223
- _ts_decorate5([
1351
+ _ts_decorate6([
1224
1352
  Post2("login"),
1225
1353
  HttpCode(200),
1226
1354
  _ts_param4(0, Body2()),
@@ -1236,7 +1364,21 @@ _ts_decorate5([
1236
1364
  ]),
1237
1365
  _ts_metadata4("design:returntype", Promise)
1238
1366
  ], DurableAuthController.prototype, "login", null);
1239
- _ts_decorate5([
1367
+ _ts_decorate6([
1368
+ Post2("session"),
1369
+ HttpCode(204),
1370
+ _ts_param4(0, Req()),
1371
+ _ts_param4(1, Res({
1372
+ passthrough: true
1373
+ })),
1374
+ _ts_metadata4("design:type", Function),
1375
+ _ts_metadata4("design:paramtypes", [
1376
+ Object,
1377
+ Object
1378
+ ]),
1379
+ _ts_metadata4("design:returntype", Promise)
1380
+ ], DurableAuthController.prototype, "session", null);
1381
+ _ts_decorate6([
1240
1382
  Get3("logout"),
1241
1383
  _ts_param4(0, Req()),
1242
1384
  _ts_param4(1, Res()),
@@ -1247,7 +1389,7 @@ _ts_decorate5([
1247
1389
  ]),
1248
1390
  _ts_metadata4("design:returntype", void 0)
1249
1391
  ], DurableAuthController.prototype, "logout", null);
1250
- DurableAuthController = _ts_decorate5([
1392
+ DurableAuthController = _ts_decorate6([
1251
1393
  Controller3(),
1252
1394
  _ts_param4(0, Optional2()),
1253
1395
  _ts_param4(0, Inject3(DASHBOARD_AUTH)),
@@ -1266,13 +1408,13 @@ import { RouterModule } from "@nestjs/core";
1266
1408
 
1267
1409
  // src/server/durable-session.guard.ts
1268
1410
  import { Inject as Inject4, Injectable as Injectable2, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
1269
- function _ts_decorate6(decorators, target, key, desc) {
1411
+ function _ts_decorate7(decorators, target, key, desc) {
1270
1412
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1271
1413
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1272
1414
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1273
1415
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1274
1416
  }
1275
- __name(_ts_decorate6, "_ts_decorate");
1417
+ __name(_ts_decorate7, "_ts_decorate");
1276
1418
  function _ts_metadata5(k, v) {
1277
1419
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1278
1420
  }
@@ -1293,20 +1435,37 @@ var DurableUiSessionGuard = class {
1293
1435
  this.auth = auth;
1294
1436
  this.basePath = basePath;
1295
1437
  }
1296
- canActivate(context) {
1438
+ async canActivate(context) {
1297
1439
  if (!this.auth) return true;
1440
+ const auth = this.auth;
1298
1441
  const http = context.switchToHttp();
1299
1442
  const request = http.getRequest();
1300
- const session = readSessionFromRequest(this.auth, request);
1443
+ const session = readSessionFromRequest(auth, request);
1301
1444
  if (!session) {
1302
- const returnTo = encodeURIComponent(originalRequestUrl(request));
1303
- throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1445
+ this.denyUnauthenticated(auth, request);
1446
+ }
1447
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1448
+ this.denyUnauthenticated(auth, request);
1304
1449
  }
1305
- maybeRenewSession(this.auth, session, request, http.getResponse());
1306
1450
  return true;
1307
1451
  }
1452
+ /**
1453
+ * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent
1454
+ * one — by design, there is nothing left to distinguish "never had a session" from "had one,
1455
+ * then lost it" once the cookie is cleared, so both get the same Mode-aware treatment.
1456
+ *
1457
+ * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
1458
+ * return narrows the field within that method only — TS doesn't carry it across a method call
1459
+ * — so callers pass the already-narrowed value instead of this method re-deriving (or
1460
+ * asserting away) the nullability.
1461
+ */
1462
+ denyUnauthenticated(auth, request) {
1463
+ if (!auth.modes.includes("login")) throw new DashboardSessionRequiredException(this.basePath);
1464
+ const returnTo = encodeURIComponent(originalRequestUrl(request));
1465
+ throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1466
+ }
1308
1467
  };
1309
- DurableUiSessionGuard = _ts_decorate6([
1468
+ DurableUiSessionGuard = _ts_decorate7([
1310
1469
  Injectable2(),
1311
1470
  _ts_param5(0, Inject4(DASHBOARD_AUTH)),
1312
1471
  _ts_param5(1, Inject4(DASHBOARD_BASE_PATH)),
@@ -1324,17 +1483,33 @@ var DurableApiSessionGuard = class {
1324
1483
  constructor(auth) {
1325
1484
  this.auth = auth;
1326
1485
  }
1327
- canActivate(context) {
1486
+ async canActivate(context) {
1328
1487
  if (!this.auth) return true;
1488
+ const auth = this.auth;
1329
1489
  const http = context.switchToHttp();
1330
1490
  const request = http.getRequest();
1331
- const session = readSessionFromRequest(this.auth, request);
1332
- if (!session) throw new UnauthorizedException2();
1333
- maybeRenewSession(this.auth, session, request, http.getResponse());
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);
1495
+ }
1334
1496
  return true;
1335
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
+ }
1336
1511
  };
1337
- DurableApiSessionGuard = _ts_decorate6([
1512
+ DurableApiSessionGuard = _ts_decorate7([
1338
1513
  Injectable2(),
1339
1514
  _ts_param5(0, Inject4(DASHBOARD_AUTH)),
1340
1515
  _ts_metadata5("design:type", Function),
@@ -1344,13 +1519,13 @@ DurableApiSessionGuard = _ts_decorate6([
1344
1519
  ], DurableApiSessionGuard);
1345
1520
 
1346
1521
  // src/server/durable-dashboard.module.ts
1347
- function _ts_decorate7(decorators, target, key, desc) {
1522
+ function _ts_decorate8(decorators, target, key, desc) {
1348
1523
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1349
1524
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1350
1525
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1351
1526
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1352
1527
  }
1353
- __name(_ts_decorate7, "_ts_decorate");
1528
+ __name(_ts_decorate8, "_ts_decorate");
1354
1529
  var GUARDS_METADATA = "__guards__";
1355
1530
  function normalize(path) {
1356
1531
  return `/${path.replace(/^\/+|\/+$/g, "")}`;
@@ -1392,7 +1567,7 @@ var DurableApiModule = class _DurableApiModule {
1392
1567
  };
1393
1568
  }
1394
1569
  };
1395
- DurableApiModule = _ts_decorate7([
1570
+ DurableApiModule = _ts_decorate8([
1396
1571
  Module({})
1397
1572
  ], DurableApiModule);
1398
1573
  var DurableDashboardModule = class _DurableDashboardModule {
@@ -1478,7 +1653,7 @@ var DurableDashboardModule = class _DurableDashboardModule {
1478
1653
  };
1479
1654
  }
1480
1655
  };
1481
- DurableDashboardModule = _ts_decorate7([
1656
+ DurableDashboardModule = _ts_decorate8([
1482
1657
  Module({})
1483
1658
  ], DurableDashboardModule);
1484
1659
  export {