@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.
@@ -552,7 +552,7 @@ DurableApiController = _ts_decorate2([
552
552
  ], DurableApiController);
553
553
 
554
554
  // src/server/durable-auth.controller.ts
555
- var import_common5 = require("@nestjs/common");
555
+ var import_common6 = require("@nestjs/common");
556
556
 
557
557
  // src/server/auth/dashboard-auth-config.ts
558
558
  var DASHBOARD_AUTH = Symbol("DASHBOARD_AUTH");
@@ -577,28 +577,51 @@ function resolveDashboardAuth(options) {
577
577
  if (typeof options.secret !== "string" || options.secret === "") {
578
578
  throw new Error("DurableDashboardModule: dashboardAuth.secret is required and must be a non-empty string (HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.");
579
579
  }
580
- if (typeof options.login !== "function") {
581
- throw new Error("DurableDashboardModule: dashboardAuth.login is required (a login hook).");
580
+ const modes = [];
581
+ if (options.session !== void 0) {
582
+ if (typeof options.session !== "function") {
583
+ throw new Error("DurableDashboardModule: dashboardAuth.session must be a function.");
584
+ }
585
+ modes.push("session");
586
+ }
587
+ if (options.login !== void 0) {
588
+ if (typeof options.login !== "function") {
589
+ throw new Error("DurableDashboardModule: dashboardAuth.login must be a function.");
590
+ }
591
+ modes.push("login");
592
+ }
593
+ if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
594
+ throw new Error("DurableDashboardModule: dashboardAuth.revalidate must be a function.");
595
+ }
596
+ if (modes.length === 0) {
597
+ throw new Error("DurableDashboardModule: dashboardAuth needs at least one of `session` or `login` (otherwise the cookie can never be minted). Failing closed.");
582
598
  }
583
599
  return {
584
600
  secret: options.secret,
585
601
  ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
586
- login: options.login
602
+ modes,
603
+ ...options.session !== void 0 ? {
604
+ session: options.session
605
+ } : {},
606
+ ...options.login !== void 0 ? {
607
+ login: options.login
608
+ } : {},
609
+ ...options.revalidate !== void 0 ? {
610
+ revalidate: options.revalidate
611
+ } : {}
587
612
  };
588
613
  }
589
614
  __name(resolveDashboardAuth, "resolveDashboardAuth");
590
615
 
591
616
  // src/server/auth/login-page.ts
592
- function renderLoginPage(basePath) {
593
- const loginAction = `${basePath}/login`;
594
- const defaultReturnTo = basePath;
617
+ function page(content, title = "Durable") {
595
618
  return `<!doctype html>
596
619
  <html lang="en">
597
620
  <head>
598
621
  <meta charset="utf-8" />
599
622
  <meta name="viewport" content="width=device-width, initial-scale=1" />
600
623
  <meta name="robots" content="noindex, nofollow" />
601
- <title>Sign in \u2014 Durable</title>
624
+ <title>${title}</title>
602
625
  <style>
603
626
  :root { color-scheme: dark; }
604
627
  * { box-sizing: border-box; }
@@ -629,6 +652,13 @@ function renderLoginPage(basePath) {
629
652
  font-weight: 600;
630
653
  color: #34d399;
631
654
  }
655
+ h1 {
656
+ margin: 0 0 12px;
657
+ font-size: 16px;
658
+ font-weight: 600;
659
+ color: #f4f4f5;
660
+ }
661
+ p { margin: 0 0 16px; color: #a1a1aa; }
632
662
  form { display: flex; flex-direction: column; gap: 16px; }
633
663
  label { display: flex; flex-direction: column; gap: 6px; }
634
664
  .field-label {
@@ -653,7 +683,7 @@ function renderLoginPage(basePath) {
653
683
  font-size: 12px;
654
684
  color: #fb7185;
655
685
  }
656
- button {
686
+ button, .button {
657
687
  margin-top: 8px;
658
688
  border-radius: 4px;
659
689
  border: 1px solid rgb(52 211 153 / 0.4);
@@ -666,14 +696,27 @@ function renderLoginPage(basePath) {
666
696
  text-transform: uppercase;
667
697
  letter-spacing: 0.05em;
668
698
  cursor: pointer;
699
+ display: inline-block;
700
+ text-align: center;
701
+ text-decoration: none;
702
+ box-sizing: border-box;
669
703
  }
670
- button:hover:not(:disabled) { background: rgb(52 211 153 / 0.2); }
704
+ button:hover:not(:disabled), .button:hover { background: rgb(52 211 153 / 0.2); }
671
705
  button:disabled { opacity: 0.6; cursor: default; }
672
706
  </style>
673
707
  </head>
674
708
  <body>
675
709
  <div class="card">
676
- <p class="brand">Durable</p>
710
+ ${content}
711
+ </div>
712
+ </body>
713
+ </html>`;
714
+ }
715
+ __name(page, "page");
716
+ function renderLoginPage(basePath) {
717
+ const loginAction = `${basePath}/login`;
718
+ const defaultReturnTo = basePath;
719
+ return page(`<p class="brand">Durable</p>
677
720
  <form id="login-form" autocomplete="on">
678
721
  <label>
679
722
  <span class="field-label">Username</span>
@@ -686,7 +729,6 @@ function renderLoginPage(basePath) {
686
729
  <p id="error" role="alert">Invalid username or password.</p>
687
730
  <button id="submit" type="submit">Sign in</button>
688
731
  </form>
689
- </div>
690
732
  <script>
691
733
  (function () {
692
734
  var LOGIN_ACTION = ${JSON.stringify(loginAction)};
@@ -733,11 +775,15 @@ function renderLoginPage(basePath) {
733
775
  });
734
776
  });
735
777
  })();
736
- </script>
737
- </body>
738
- </html>`;
778
+ </script>`, "Sign in \u2014 Durable");
739
779
  }
740
780
  __name(renderLoginPage, "renderLoginPage");
781
+ function renderSessionRequiredPage(basePath) {
782
+ return page(`<h1>Open this console from your application</h1>
783
+ <p>Your session is minted by the host app. Use its console launcher to sign in, then reload.</p>
784
+ <a class="button" href="${basePath}">Retry</a>`);
785
+ }
786
+ __name(renderSessionRequiredPage, "renderSessionRequiredPage");
741
787
 
742
788
  // src/server/auth/request.ts
743
789
  function isRecord(value) {
@@ -838,6 +884,15 @@ function redirectRaw(response, location, status = 302) {
838
884
  raw.end();
839
885
  }
840
886
  __name(redirectRaw, "redirectRaw");
887
+ function writeHtmlRaw(response, html, status = 200) {
888
+ const raw = resolveRedirectResponse(response);
889
+ if (!raw) return;
890
+ raw.statusCode = status;
891
+ raw.setHeader("content-type", "text/html; charset=utf-8");
892
+ raw.setHeader("cache-control", "no-store, must-revalidate");
893
+ raw.end(html);
894
+ }
895
+ __name(writeHtmlRaw, "writeHtmlRaw");
841
896
 
842
897
  // src/server/auth/cookie-header.ts
843
898
  function parseCookieHeader(header) {
@@ -993,21 +1048,44 @@ function readSessionFromRequest(auth, request) {
993
1048
  });
994
1049
  }
995
1050
  __name(readSessionFromRequest, "readSessionFromRequest");
996
- function maybeRenewSession(auth, session, request, response) {
997
- const now = Date.now();
998
- if (now - session.iat <= auth.ttlMs / 2) return;
999
- issueSessionCookie({
1051
+ var inFlightRevalidations = /* @__PURE__ */ new Map();
1052
+ function revalidateOnce(auth, user) {
1053
+ const revalidate = auth.revalidate;
1054
+ if (!revalidate) return Promise.resolve(true);
1055
+ const key = `${auth.secret}:${user.id}`;
1056
+ const inFlight = inFlightRevalidations.get(key);
1057
+ if (inFlight) return inFlight;
1058
+ const call = Promise.resolve().then(() => revalidate(user)).catch(() => false).finally(() => {
1059
+ inFlightRevalidations.delete(key);
1060
+ });
1061
+ inFlightRevalidations.set(key, call);
1062
+ return call;
1063
+ }
1064
+ __name(revalidateOnce, "revalidateOnce");
1065
+ async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
1066
+ if (now - session.iat <= auth.ttlMs / 2) return true;
1067
+ const user = {
1000
1068
  id: session.sub,
1001
1069
  ...session.name !== void 0 ? {
1002
1070
  name: session.name
1003
1071
  } : {},
1004
1072
  roles: session.roles
1005
- }, {
1073
+ };
1074
+ const allowed = await revalidateOnce(auth, user);
1075
+ if (!allowed) {
1076
+ clearSessionCookie({
1077
+ request,
1078
+ response
1079
+ });
1080
+ return false;
1081
+ }
1082
+ issueSessionCookie(user, {
1006
1083
  auth,
1007
1084
  request,
1008
1085
  response,
1009
1086
  now
1010
1087
  });
1088
+ return true;
1011
1089
  }
1012
1090
  __name(maybeRenewSession, "maybeRenewSession");
1013
1091
 
@@ -1015,7 +1093,7 @@ __name(maybeRenewSession, "maybeRenewSession");
1015
1093
  var import_node_fs = require("fs");
1016
1094
  var import_node_path = require("path");
1017
1095
  var import_node_url = require("url");
1018
- var import_common4 = require("@nestjs/common");
1096
+ var import_common5 = require("@nestjs/common");
1019
1097
 
1020
1098
  // src/server/auth/login-redirect.exception.ts
1021
1099
  var import_common3 = require("@nestjs/common");
@@ -1050,7 +1128,8 @@ DashboardLoginRedirectFilter = _ts_decorate3([
1050
1128
  (0, import_common3.Catch)(DashboardLoginRedirectException)
1051
1129
  ], DashboardLoginRedirectFilter);
1052
1130
 
1053
- // src/server/durable-ui.controller.ts
1131
+ // src/server/auth/session-required.exception.ts
1132
+ var import_common4 = require("@nestjs/common");
1054
1133
  function _ts_decorate4(decorators, target, key, desc) {
1055
1134
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1056
1135
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1058,6 +1137,38 @@ function _ts_decorate4(decorators, target, key, desc) {
1058
1137
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1059
1138
  }
1060
1139
  __name(_ts_decorate4, "_ts_decorate");
1140
+ var DashboardSessionRequiredException = class extends import_common4.HttpException {
1141
+ static {
1142
+ __name(this, "DashboardSessionRequiredException");
1143
+ }
1144
+ basePath;
1145
+ constructor(basePath) {
1146
+ super({
1147
+ basePath
1148
+ }, import_common4.HttpStatus.UNAUTHORIZED), this.basePath = basePath;
1149
+ }
1150
+ };
1151
+ var DashboardSessionRequiredFilter = class {
1152
+ static {
1153
+ __name(this, "DashboardSessionRequiredFilter");
1154
+ }
1155
+ catch(exception, host) {
1156
+ const response = host.switchToHttp().getResponse();
1157
+ writeHtmlRaw(response, renderSessionRequiredPage(exception.basePath), import_common4.HttpStatus.UNAUTHORIZED);
1158
+ }
1159
+ };
1160
+ DashboardSessionRequiredFilter = _ts_decorate4([
1161
+ (0, import_common4.Catch)(DashboardSessionRequiredException)
1162
+ ], DashboardSessionRequiredFilter);
1163
+
1164
+ // src/server/durable-ui.controller.ts
1165
+ function _ts_decorate5(decorators, target, key, desc) {
1166
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1167
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1168
+ 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;
1169
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1170
+ }
1171
+ __name(_ts_decorate5, "_ts_decorate");
1061
1172
  function _ts_metadata3(k, v) {
1062
1173
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1063
1174
  }
@@ -1100,7 +1211,7 @@ var DurableUiController = class {
1100
1211
  index() {
1101
1212
  const indexPath = (0, import_node_path.join)(this.dir, "index.html");
1102
1213
  if (!(0, import_node_fs.existsSync)(indexPath)) {
1103
- throw new import_common4.NotFoundException("Dashboard is not built. Run the package build.");
1214
+ throw new import_common5.NotFoundException("Dashboard is not built. Run the package build.");
1104
1215
  }
1105
1216
  const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
1106
1217
  const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;
@@ -1108,41 +1219,41 @@ var DurableUiController = class {
1108
1219
  }
1109
1220
  asset(file) {
1110
1221
  const safe = (0, import_node_path.basename)(file);
1111
- if (safe !== file) throw new import_common4.NotFoundException();
1222
+ if (safe !== file) throw new import_common5.NotFoundException();
1112
1223
  const root = (0, import_node_path.resolve)(this.dir, "assets");
1113
1224
  const assetPath = (0, import_node_path.resolve)(root, safe);
1114
1225
  if (!assetPath.startsWith(root + import_node_path.sep) || !(0, import_node_fs.existsSync)(assetPath)) {
1115
- throw new import_common4.NotFoundException();
1226
+ throw new import_common5.NotFoundException();
1116
1227
  }
1117
1228
  const type = CONTENT_TYPES[(0, import_node_path.extname)(safe)] ?? "application/octet-stream";
1118
- return new import_common4.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
1229
+ return new import_common5.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
1119
1230
  type
1120
1231
  });
1121
1232
  }
1122
1233
  };
1123
- _ts_decorate4([
1124
- (0, import_common4.Get)(),
1125
- (0, import_common4.Header)("Content-Type", "text/html; charset=utf-8"),
1126
- (0, import_common4.Header)("Cache-Control", "no-store, must-revalidate"),
1234
+ _ts_decorate5([
1235
+ (0, import_common5.Get)(),
1236
+ (0, import_common5.Header)("Content-Type", "text/html; charset=utf-8"),
1237
+ (0, import_common5.Header)("Cache-Control", "no-store, must-revalidate"),
1127
1238
  _ts_metadata3("design:type", Function),
1128
1239
  _ts_metadata3("design:paramtypes", []),
1129
1240
  _ts_metadata3("design:returntype", String)
1130
1241
  ], DurableUiController.prototype, "index", null);
1131
- _ts_decorate4([
1132
- (0, import_common4.Get)("assets/:file"),
1133
- (0, import_common4.Header)("Cache-Control", "public, max-age=31536000, immutable"),
1134
- _ts_param3(0, (0, import_common4.Param)("file")),
1242
+ _ts_decorate5([
1243
+ (0, import_common5.Get)("assets/:file"),
1244
+ (0, import_common5.Header)("Cache-Control", "public, max-age=31536000, immutable"),
1245
+ _ts_param3(0, (0, import_common5.Param)("file")),
1135
1246
  _ts_metadata3("design:type", Function),
1136
1247
  _ts_metadata3("design:paramtypes", [
1137
1248
  String
1138
1249
  ]),
1139
- _ts_metadata3("design:returntype", typeof import_common4.StreamableFile === "undefined" ? Object : import_common4.StreamableFile)
1250
+ _ts_metadata3("design:returntype", typeof import_common5.StreamableFile === "undefined" ? Object : import_common5.StreamableFile)
1140
1251
  ], DurableUiController.prototype, "asset", null);
1141
- DurableUiController = _ts_decorate4([
1142
- (0, import_common4.UseFilters)(DashboardLoginRedirectFilter),
1143
- (0, import_common4.Controller)(),
1144
- _ts_param3(0, (0, import_common4.Inject)(DASHBOARD_BASE_PATH)),
1145
- _ts_param3(1, (0, import_common4.Inject)(DASHBOARD_API_PATH)),
1252
+ DurableUiController = _ts_decorate5([
1253
+ (0, import_common5.UseFilters)(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter),
1254
+ (0, import_common5.Controller)(),
1255
+ _ts_param3(0, (0, import_common5.Inject)(DASHBOARD_BASE_PATH)),
1256
+ _ts_param3(1, (0, import_common5.Inject)(DASHBOARD_API_PATH)),
1146
1257
  _ts_metadata3("design:type", Function),
1147
1258
  _ts_metadata3("design:paramtypes", [
1148
1259
  String,
@@ -1151,13 +1262,13 @@ DurableUiController = _ts_decorate4([
1151
1262
  ], DurableUiController);
1152
1263
 
1153
1264
  // src/server/durable-auth.controller.ts
1154
- function _ts_decorate5(decorators, target, key, desc) {
1265
+ function _ts_decorate6(decorators, target, key, desc) {
1155
1266
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1156
1267
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1157
1268
  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;
1158
1269
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1159
1270
  }
1160
- __name(_ts_decorate5, "_ts_decorate");
1271
+ __name(_ts_decorate6, "_ts_decorate");
1161
1272
  function _ts_metadata4(k, v) {
1162
1273
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1163
1274
  }
@@ -1178,9 +1289,9 @@ var DurableAuthController = class _DurableAuthController {
1178
1289
  }
1179
1290
  auth;
1180
1291
  basePath;
1181
- logger = new import_common5.Logger(_DurableAuthController.name);
1182
- /** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
1183
- warnedOnHookThrow = false;
1292
+ logger = new import_common6.Logger(_DurableAuthController.name);
1293
+ /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
1294
+ warnedHooks = /* @__PURE__ */ new Set();
1184
1295
  constructor(auth, basePath) {
1185
1296
  this.auth = auth;
1186
1297
  this.basePath = basePath;
@@ -1189,7 +1300,8 @@ var DurableAuthController = class _DurableAuthController {
1189
1300
  // `login-page.ts`), so this response never varies per request and needs no templating beyond
1190
1301
  // the developer-controlled `basePath`.
1191
1302
  loginPage() {
1192
- if (!this.auth) throw new import_common5.NotFoundException();
1303
+ const auth = this.requireAuth();
1304
+ if (!auth.modes.includes("login")) throw new import_common6.NotFoundException();
1193
1305
  return renderLoginPage(this.basePath);
1194
1306
  }
1195
1307
  // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for
@@ -1198,11 +1310,13 @@ var DurableAuthController = class _DurableAuthController {
1198
1310
  // (unknown user, wrong password, throwing hook) — no user-enumeration.
1199
1311
  async login(body, request, response) {
1200
1312
  const auth = this.requireAuth();
1313
+ if (!auth.modes.includes("login")) throw new import_common6.NotFoundException();
1201
1314
  if (!isString(body?.username) || !isString(body?.password)) {
1202
- throw new import_common5.BadRequestException("Body must include string `username` and `password`.");
1315
+ throw new import_common6.BadRequestException("Body must include string `username` and `password`.");
1203
1316
  }
1204
- const user = await this.runLoginHook(auth, body.username, body.password);
1205
- if (!user) throw new import_common5.UnauthorizedException({
1317
+ const { username, password } = body;
1318
+ const user = await this.runHook("login", () => auth.login?.(username, password) ?? null);
1319
+ if (!user) throw new import_common6.UnauthorizedException({
1206
1320
  message: "Invalid username or password."
1207
1321
  });
1208
1322
  issueSessionCookie(user, {
@@ -1214,6 +1328,20 @@ var DurableAuthController = class _DurableAuthController {
1214
1328
  redirectTo: sanitizeReturnTo(body.returnTo, this.basePath)
1215
1329
  };
1216
1330
  }
1331
+ // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw
1332
+ // request and returns the session user, or `null` to deny. This is the SSO path — the host's own
1333
+ // guard decides, and no credential ever reaches this library.
1334
+ async session(request, response) {
1335
+ const auth = this.requireAuth();
1336
+ if (!auth.modes.includes("session")) throw new import_common6.NotFoundException();
1337
+ const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1338
+ if (!user) throw new import_common6.UnauthorizedException();
1339
+ issueSessionCookie(user, {
1340
+ auth,
1341
+ request,
1342
+ response
1343
+ });
1344
+ }
1217
1345
  // Plain `GET` (not `POST`): logging out only ever destroys the CALLER's own session, so it's
1218
1346
  // safe/idempotent from the caller's perspective — a simple `<a href>` (no JS, no CSRF token)
1219
1347
  // works. Non-passthrough `@Res()`: this handler owns the response outright (clears the cookie,
@@ -1223,43 +1351,43 @@ var DurableAuthController = class _DurableAuthController {
1223
1351
  request,
1224
1352
  response
1225
1353
  });
1226
- redirectRaw(response, `${this.basePath}/login`);
1354
+ redirectRaw(response, this.auth?.modes.includes("login") ? `${this.basePath}/login` : this.basePath);
1227
1355
  }
1228
1356
  requireAuth() {
1229
- if (!this.auth) throw new import_common5.NotFoundException();
1357
+ if (!this.auth) throw new import_common6.NotFoundException();
1230
1358
  return this.auth;
1231
1359
  }
1232
1360
  /**
1233
- * Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
1234
- * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
1235
- * the logs on repeated bad attempts.
1361
+ * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
1362
+ * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a
1363
+ * stack-trace leak nor floods the logs on repeated bad attempts.
1236
1364
  */
1237
- async runLoginHook(auth, username, password) {
1365
+ async runHook(kind, run) {
1238
1366
  try {
1239
- return await auth.login(username, password) ?? null;
1367
+ return await run() ?? null;
1240
1368
  } catch (error) {
1241
- if (!this.warnedOnHookThrow) {
1242
- this.warnedOnHookThrow = true;
1243
- this.logger.warn(`dashboardAuth login hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1369
+ if (!this.warnedHooks.has(kind)) {
1370
+ this.warnedHooks.add(kind);
1371
+ this.logger.warn(`dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1244
1372
  }
1245
1373
  return null;
1246
1374
  }
1247
1375
  }
1248
1376
  };
1249
- _ts_decorate5([
1250
- (0, import_common5.Get)("login"),
1251
- (0, import_common5.Header)("Content-Type", "text/html; charset=utf-8"),
1252
- (0, import_common5.Header)("Cache-Control", "no-store, must-revalidate"),
1377
+ _ts_decorate6([
1378
+ (0, import_common6.Get)("login"),
1379
+ (0, import_common6.Header)("Content-Type", "text/html; charset=utf-8"),
1380
+ (0, import_common6.Header)("Cache-Control", "no-store, must-revalidate"),
1253
1381
  _ts_metadata4("design:type", Function),
1254
1382
  _ts_metadata4("design:paramtypes", []),
1255
1383
  _ts_metadata4("design:returntype", String)
1256
1384
  ], DurableAuthController.prototype, "loginPage", null);
1257
- _ts_decorate5([
1258
- (0, import_common5.Post)("login"),
1259
- (0, import_common5.HttpCode)(200),
1260
- _ts_param4(0, (0, import_common5.Body)()),
1261
- _ts_param4(1, (0, import_common5.Req)()),
1262
- _ts_param4(2, (0, import_common5.Res)({
1385
+ _ts_decorate6([
1386
+ (0, import_common6.Post)("login"),
1387
+ (0, import_common6.HttpCode)(200),
1388
+ _ts_param4(0, (0, import_common6.Body)()),
1389
+ _ts_param4(1, (0, import_common6.Req)()),
1390
+ _ts_param4(2, (0, import_common6.Res)({
1263
1391
  passthrough: true
1264
1392
  })),
1265
1393
  _ts_metadata4("design:type", Function),
@@ -1270,10 +1398,24 @@ _ts_decorate5([
1270
1398
  ]),
1271
1399
  _ts_metadata4("design:returntype", Promise)
1272
1400
  ], DurableAuthController.prototype, "login", null);
1273
- _ts_decorate5([
1274
- (0, import_common5.Get)("logout"),
1275
- _ts_param4(0, (0, import_common5.Req)()),
1276
- _ts_param4(1, (0, import_common5.Res)()),
1401
+ _ts_decorate6([
1402
+ (0, import_common6.Post)("session"),
1403
+ (0, import_common6.HttpCode)(204),
1404
+ _ts_param4(0, (0, import_common6.Req)()),
1405
+ _ts_param4(1, (0, import_common6.Res)({
1406
+ passthrough: true
1407
+ })),
1408
+ _ts_metadata4("design:type", Function),
1409
+ _ts_metadata4("design:paramtypes", [
1410
+ Object,
1411
+ Object
1412
+ ]),
1413
+ _ts_metadata4("design:returntype", Promise)
1414
+ ], DurableAuthController.prototype, "session", null);
1415
+ _ts_decorate6([
1416
+ (0, import_common6.Get)("logout"),
1417
+ _ts_param4(0, (0, import_common6.Req)()),
1418
+ _ts_param4(1, (0, import_common6.Res)()),
1277
1419
  _ts_metadata4("design:type", Function),
1278
1420
  _ts_metadata4("design:paramtypes", [
1279
1421
  Object,
@@ -1281,11 +1423,11 @@ _ts_decorate5([
1281
1423
  ]),
1282
1424
  _ts_metadata4("design:returntype", void 0)
1283
1425
  ], DurableAuthController.prototype, "logout", null);
1284
- DurableAuthController = _ts_decorate5([
1285
- (0, import_common5.Controller)(),
1286
- _ts_param4(0, (0, import_common5.Optional)()),
1287
- _ts_param4(0, (0, import_common5.Inject)(DASHBOARD_AUTH)),
1288
- _ts_param4(1, (0, import_common5.Inject)(DASHBOARD_BASE_PATH)),
1426
+ DurableAuthController = _ts_decorate6([
1427
+ (0, import_common6.Controller)(),
1428
+ _ts_param4(0, (0, import_common6.Optional)()),
1429
+ _ts_param4(0, (0, import_common6.Inject)(DASHBOARD_AUTH)),
1430
+ _ts_param4(1, (0, import_common6.Inject)(DASHBOARD_BASE_PATH)),
1289
1431
  _ts_metadata4("design:type", Function),
1290
1432
  _ts_metadata4("design:paramtypes", [
1291
1433
  Object,
@@ -1295,18 +1437,18 @@ DurableAuthController = _ts_decorate5([
1295
1437
 
1296
1438
  // src/server/durable-dashboard.module.ts
1297
1439
  var import_reflect_metadata = require("reflect-metadata");
1298
- var import_common7 = require("@nestjs/common");
1440
+ var import_common8 = require("@nestjs/common");
1299
1441
  var import_core = require("@nestjs/core");
1300
1442
 
1301
1443
  // src/server/durable-session.guard.ts
1302
- var import_common6 = require("@nestjs/common");
1303
- function _ts_decorate6(decorators, target, key, desc) {
1444
+ var import_common7 = require("@nestjs/common");
1445
+ function _ts_decorate7(decorators, target, key, desc) {
1304
1446
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1305
1447
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1306
1448
  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;
1307
1449
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1308
1450
  }
1309
- __name(_ts_decorate6, "_ts_decorate");
1451
+ __name(_ts_decorate7, "_ts_decorate");
1310
1452
  function _ts_metadata5(k, v) {
1311
1453
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1312
1454
  }
@@ -1327,23 +1469,40 @@ var DurableUiSessionGuard = class {
1327
1469
  this.auth = auth;
1328
1470
  this.basePath = basePath;
1329
1471
  }
1330
- canActivate(context) {
1472
+ async canActivate(context) {
1331
1473
  if (!this.auth) return true;
1474
+ const auth = this.auth;
1332
1475
  const http = context.switchToHttp();
1333
1476
  const request = http.getRequest();
1334
- const session = readSessionFromRequest(this.auth, request);
1477
+ const session = readSessionFromRequest(auth, request);
1335
1478
  if (!session) {
1336
- const returnTo = encodeURIComponent(originalRequestUrl(request));
1337
- throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1479
+ this.denyUnauthenticated(auth, request);
1480
+ }
1481
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1482
+ this.denyUnauthenticated(auth, request);
1338
1483
  }
1339
- maybeRenewSession(this.auth, session, request, http.getResponse());
1340
1484
  return true;
1341
1485
  }
1486
+ /**
1487
+ * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent
1488
+ * one — by design, there is nothing left to distinguish "never had a session" from "had one,
1489
+ * then lost it" once the cookie is cleared, so both get the same Mode-aware treatment.
1490
+ *
1491
+ * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
1492
+ * return narrows the field within that method only — TS doesn't carry it across a method call
1493
+ * — so callers pass the already-narrowed value instead of this method re-deriving (or
1494
+ * asserting away) the nullability.
1495
+ */
1496
+ denyUnauthenticated(auth, request) {
1497
+ if (!auth.modes.includes("login")) throw new DashboardSessionRequiredException(this.basePath);
1498
+ const returnTo = encodeURIComponent(originalRequestUrl(request));
1499
+ throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1500
+ }
1342
1501
  };
1343
- DurableUiSessionGuard = _ts_decorate6([
1344
- (0, import_common6.Injectable)(),
1345
- _ts_param5(0, (0, import_common6.Inject)(DASHBOARD_AUTH)),
1346
- _ts_param5(1, (0, import_common6.Inject)(DASHBOARD_BASE_PATH)),
1502
+ DurableUiSessionGuard = _ts_decorate7([
1503
+ (0, import_common7.Injectable)(),
1504
+ _ts_param5(0, (0, import_common7.Inject)(DASHBOARD_AUTH)),
1505
+ _ts_param5(1, (0, import_common7.Inject)(DASHBOARD_BASE_PATH)),
1347
1506
  _ts_metadata5("design:type", Function),
1348
1507
  _ts_metadata5("design:paramtypes", [
1349
1508
  Object,
@@ -1358,19 +1517,35 @@ var DurableApiSessionGuard = class {
1358
1517
  constructor(auth) {
1359
1518
  this.auth = auth;
1360
1519
  }
1361
- canActivate(context) {
1520
+ async canActivate(context) {
1362
1521
  if (!this.auth) return true;
1522
+ const auth = this.auth;
1363
1523
  const http = context.switchToHttp();
1364
1524
  const request = http.getRequest();
1365
- const session = readSessionFromRequest(this.auth, request);
1366
- if (!session) throw new import_common6.UnauthorizedException();
1367
- maybeRenewSession(this.auth, session, request, http.getResponse());
1525
+ const session = readSessionFromRequest(auth, request);
1526
+ if (!session) throw this.unauthorized(auth);
1527
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1528
+ throw this.unauthorized(auth);
1529
+ }
1368
1530
  return true;
1369
1531
  }
1532
+ /**
1533
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
1534
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
1535
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
1536
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
1537
+ */
1538
+ unauthorized(auth) {
1539
+ return new import_common7.UnauthorizedException({
1540
+ auth: {
1541
+ modes: auth.modes
1542
+ }
1543
+ });
1544
+ }
1370
1545
  };
1371
- DurableApiSessionGuard = _ts_decorate6([
1372
- (0, import_common6.Injectable)(),
1373
- _ts_param5(0, (0, import_common6.Inject)(DASHBOARD_AUTH)),
1546
+ DurableApiSessionGuard = _ts_decorate7([
1547
+ (0, import_common7.Injectable)(),
1548
+ _ts_param5(0, (0, import_common7.Inject)(DASHBOARD_AUTH)),
1374
1549
  _ts_metadata5("design:type", Function),
1375
1550
  _ts_metadata5("design:paramtypes", [
1376
1551
  Object
@@ -1378,13 +1553,13 @@ DurableApiSessionGuard = _ts_decorate6([
1378
1553
  ], DurableApiSessionGuard);
1379
1554
 
1380
1555
  // src/server/durable-dashboard.module.ts
1381
- function _ts_decorate7(decorators, target, key, desc) {
1556
+ function _ts_decorate8(decorators, target, key, desc) {
1382
1557
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1383
1558
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1384
1559
  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;
1385
1560
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1386
1561
  }
1387
- __name(_ts_decorate7, "_ts_decorate");
1562
+ __name(_ts_decorate8, "_ts_decorate");
1388
1563
  var GUARDS_METADATA = "__guards__";
1389
1564
  function normalize(path) {
1390
1565
  return `/${path.replace(/^\/+|\/+$/g, "")}`;
@@ -1426,8 +1601,8 @@ var DurableApiModule = class _DurableApiModule {
1426
1601
  };
1427
1602
  }
1428
1603
  };
1429
- DurableApiModule = _ts_decorate7([
1430
- (0, import_common7.Module)({})
1604
+ DurableApiModule = _ts_decorate8([
1605
+ (0, import_common8.Module)({})
1431
1606
  ], DurableApiModule);
1432
1607
  var DurableDashboardModule = class _DurableDashboardModule {
1433
1608
  static {
@@ -1512,8 +1687,8 @@ var DurableDashboardModule = class _DurableDashboardModule {
1512
1687
  };
1513
1688
  }
1514
1689
  };
1515
- DurableDashboardModule = _ts_decorate7([
1516
- (0, import_common7.Module)({})
1690
+ DurableDashboardModule = _ts_decorate8([
1691
+ (0, import_common8.Module)({})
1517
1692
  ], DurableDashboardModule);
1518
1693
  // Annotate the CommonJS export names for ESM import in node:
1519
1694
  0 && (module.exports = {