@dudousxd/nestjs-durable-dashboard 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,11 +18,48 @@ 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;
28
+ /** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */
29
+ interface UnauthenticatedPageContext {
30
+ /**
31
+ * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`
32
+ * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.
33
+ * Cast it to whatever your app actually runs on.
34
+ */
35
+ request: unknown;
36
+ /**
37
+ * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,
38
+ * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the
39
+ * library falls back to its own built-in page rather than leaving the request hanging.
40
+ */
41
+ response: unknown;
42
+ /** Where this console is mounted (e.g. `/durable`) — useful for a "back to the console" link. */
43
+ basePath: string;
44
+ }
45
+ /**
46
+ * Host-owned page for an unauthenticated full-page navigation to the console.
47
+ *
48
+ * The built-in page cannot know who hosts the console, so it can only say "open this console from
49
+ * your application" in the abstract — it can't name the launcher, link to it, or look like the rest
50
+ * of the host's product. This hook hands the whole response to the host instead: render through
51
+ * your own template engine, your own Inertia page, whatever. The page is served AT the console's
52
+ * own URL, so `/durable` stays `/durable`.
53
+ *
54
+ * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login
55
+ * UI uses Mode A plus this hook, and posts to `<basePath>/session` from its own page — the mint
56
+ * endpoint is the supported primitive for that.
57
+ *
58
+ * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook
59
+ * that throws, or that returns without writing, falls back to the built-in page — it cannot let
60
+ * anyone in.
61
+ */
62
+ type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;
26
63
  type AuthMode = 'session' | 'login';
27
64
  /**
28
65
  * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
@@ -50,6 +87,9 @@ interface DashboardAuthOptions {
50
87
  /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
51
88
  * mint a session, only revoke one already minted by `session`/`login`. */
52
89
  revalidate?: RevalidateHook;
90
+ /** Renders the host's own page for an unauthenticated navigation, in place of the built-in
91
+ * "open this console from your application" card; see `UnauthenticatedPageHook`. */
92
+ unauthenticatedPage?: UnauthenticatedPageHook;
53
93
  }
54
94
  /** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
55
95
  interface ResolvedDashboardAuth {
@@ -59,6 +99,7 @@ interface ResolvedDashboardAuth {
59
99
  session?: SessionHook;
60
100
  login?: LoginHook;
61
101
  revalidate?: RevalidateHook;
102
+ unauthenticatedPage?: UnauthenticatedPageHook;
62
103
  }
63
104
 
64
105
  /**
@@ -418,6 +459,13 @@ declare class DurableApiSessionGuard implements CanActivate {
418
459
  private readonly auth;
419
460
  constructor(auth: ResolvedDashboardAuth | null);
420
461
  canActivate(context: ExecutionContext): Promise<boolean>;
462
+ /**
463
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
464
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
465
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
466
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
467
+ */
468
+ private unauthorized;
421
469
  }
422
470
 
423
471
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -446,4 +494,4 @@ declare class DurableUiController {
446
494
  asset(file: string): StreamableFile;
447
495
  }
448
496
 
449
- export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type DashboardSessionUser, DurableApiController, DurableApiModule, DurableApiSessionGuard, DurableAuthController, type DurableDashboardAsyncOptions, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiSessionGuard };
497
+ export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type DashboardSessionUser, DurableApiController, DurableApiModule, DurableApiSessionGuard, DurableAuthController, type DurableDashboardAsyncOptions, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiSessionGuard, type UnauthenticatedPageContext, type UnauthenticatedPageHook };
@@ -18,11 +18,48 @@ 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;
28
+ /** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */
29
+ interface UnauthenticatedPageContext {
30
+ /**
31
+ * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`
32
+ * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.
33
+ * Cast it to whatever your app actually runs on.
34
+ */
35
+ request: unknown;
36
+ /**
37
+ * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,
38
+ * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the
39
+ * library falls back to its own built-in page rather than leaving the request hanging.
40
+ */
41
+ response: unknown;
42
+ /** Where this console is mounted (e.g. `/durable`) — useful for a "back to the console" link. */
43
+ basePath: string;
44
+ }
45
+ /**
46
+ * Host-owned page for an unauthenticated full-page navigation to the console.
47
+ *
48
+ * The built-in page cannot know who hosts the console, so it can only say "open this console from
49
+ * your application" in the abstract — it can't name the launcher, link to it, or look like the rest
50
+ * of the host's product. This hook hands the whole response to the host instead: render through
51
+ * your own template engine, your own Inertia page, whatever. The page is served AT the console's
52
+ * own URL, so `/durable` stays `/durable`.
53
+ *
54
+ * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login
55
+ * UI uses Mode A plus this hook, and posts to `<basePath>/session` from its own page — the mint
56
+ * endpoint is the supported primitive for that.
57
+ *
58
+ * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook
59
+ * that throws, or that returns without writing, falls back to the built-in page — it cannot let
60
+ * anyone in.
61
+ */
62
+ type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;
26
63
  type AuthMode = 'session' | 'login';
27
64
  /**
28
65
  * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
@@ -50,6 +87,9 @@ interface DashboardAuthOptions {
50
87
  /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
51
88
  * mint a session, only revoke one already minted by `session`/`login`. */
52
89
  revalidate?: RevalidateHook;
90
+ /** Renders the host's own page for an unauthenticated navigation, in place of the built-in
91
+ * "open this console from your application" card; see `UnauthenticatedPageHook`. */
92
+ unauthenticatedPage?: UnauthenticatedPageHook;
53
93
  }
54
94
  /** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
55
95
  interface ResolvedDashboardAuth {
@@ -59,6 +99,7 @@ interface ResolvedDashboardAuth {
59
99
  session?: SessionHook;
60
100
  login?: LoginHook;
61
101
  revalidate?: RevalidateHook;
102
+ unauthenticatedPage?: UnauthenticatedPageHook;
62
103
  }
63
104
 
64
105
  /**
@@ -418,6 +459,13 @@ declare class DurableApiSessionGuard implements CanActivate {
418
459
  private readonly auth;
419
460
  constructor(auth: ResolvedDashboardAuth | null);
420
461
  canActivate(context: ExecutionContext): Promise<boolean>;
462
+ /**
463
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
464
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
465
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
466
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
467
+ */
468
+ private unauthorized;
421
469
  }
422
470
 
423
471
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -446,4 +494,4 @@ declare class DurableUiController {
446
494
  asset(file: string): StreamableFile;
447
495
  }
448
496
 
449
- export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type DashboardSessionUser, DurableApiController, DurableApiModule, DurableApiSessionGuard, DurableAuthController, type DurableDashboardAsyncOptions, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiSessionGuard };
497
+ export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type DashboardSessionUser, DurableApiController, DurableApiModule, DurableApiSessionGuard, DurableAuthController, type DurableDashboardAsyncOptions, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiSessionGuard, type UnauthenticatedPageContext, type UnauthenticatedPageHook };
@@ -518,7 +518,7 @@ DurableApiController = _ts_decorate2([
518
518
  ], DurableApiController);
519
519
 
520
520
  // src/server/durable-auth.controller.ts
521
- import { BadRequestException, Body as Body2, Controller as Controller3, Get as Get3, Header as Header3, HttpCode, Inject as Inject3, Logger, NotFoundException as NotFoundException3, Optional as Optional2, Post as Post2, Req, Res, UnauthorizedException } from "@nestjs/common";
521
+ import { BadRequestException, Body as Body2, Controller as Controller3, Get as Get3, Header as Header3, HttpCode, Inject as Inject4, Logger as Logger2, NotFoundException as NotFoundException3, Optional as Optional3, Post as Post2, Req, Res, UnauthorizedException } from "@nestjs/common";
522
522
 
523
523
  // src/server/auth/dashboard-auth-config.ts
524
524
  var DASHBOARD_AUTH = Symbol("DASHBOARD_AUTH");
@@ -559,6 +559,9 @@ function resolveDashboardAuth(options) {
559
559
  if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
560
560
  throw new Error("DurableDashboardModule: dashboardAuth.revalidate must be a function.");
561
561
  }
562
+ if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
563
+ throw new Error("DurableDashboardModule: dashboardAuth.unauthenticatedPage must be a function.");
564
+ }
562
565
  if (modes.length === 0) {
563
566
  throw new Error("DurableDashboardModule: dashboardAuth needs at least one of `session` or `login` (otherwise the cookie can never be minted). Failing closed.");
564
567
  }
@@ -574,6 +577,9 @@ function resolveDashboardAuth(options) {
574
577
  } : {},
575
578
  ...options.revalidate !== void 0 ? {
576
579
  revalidate: options.revalidate
580
+ } : {},
581
+ ...options.unauthenticatedPage !== void 0 ? {
582
+ unauthenticatedPage: options.unauthenticatedPage
577
583
  } : {}
578
584
  };
579
585
  }
@@ -850,6 +856,11 @@ function redirectRaw(response, location, status = 302) {
850
856
  raw.end();
851
857
  }
852
858
  __name(redirectRaw, "redirectRaw");
859
+ function responseAlreadyWritten(response) {
860
+ const raw = resolveRedirectResponse(response);
861
+ return raw?.headersSent === true;
862
+ }
863
+ __name(responseAlreadyWritten, "responseAlreadyWritten");
853
864
  function writeHtmlRaw(response, html, status = 200) {
854
865
  const raw = resolveRedirectResponse(response);
855
866
  if (!raw) return;
@@ -1014,6 +1025,20 @@ function readSessionFromRequest(auth, request) {
1014
1025
  });
1015
1026
  }
1016
1027
  __name(readSessionFromRequest, "readSessionFromRequest");
1028
+ var inFlightRevalidations = /* @__PURE__ */ new Map();
1029
+ function revalidateOnce(auth, user) {
1030
+ const revalidate = auth.revalidate;
1031
+ if (!revalidate) return Promise.resolve(true);
1032
+ const key = `${auth.secret}:${user.id}`;
1033
+ const inFlight = inFlightRevalidations.get(key);
1034
+ if (inFlight) return inFlight;
1035
+ const call = Promise.resolve().then(() => revalidate(user)).catch(() => false).finally(() => {
1036
+ inFlightRevalidations.delete(key);
1037
+ });
1038
+ inFlightRevalidations.set(key, call);
1039
+ return call;
1040
+ }
1041
+ __name(revalidateOnce, "revalidateOnce");
1017
1042
  async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
1018
1043
  if (now - session.iat <= auth.ttlMs / 2) return true;
1019
1044
  const user = {
@@ -1023,20 +1048,13 @@ async function maybeRenewSession(auth, session, request, response, now = Date.no
1023
1048
  } : {},
1024
1049
  roles: session.roles
1025
1050
  };
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
- }
1051
+ const allowed = await revalidateOnce(auth, user);
1052
+ if (!allowed) {
1053
+ clearSessionCookie({
1054
+ request,
1055
+ response
1056
+ });
1057
+ return false;
1040
1058
  }
1041
1059
  issueSessionCookie(user, {
1042
1060
  auth,
@@ -1052,7 +1070,7 @@ __name(maybeRenewSession, "maybeRenewSession");
1052
1070
  import { existsSync, readFileSync } from "node:fs";
1053
1071
  import { basename, extname, join, resolve, sep } from "node:path";
1054
1072
  import { fileURLToPath } from "node:url";
1055
- import { Controller as Controller2, Get as Get2, Header as Header2, Inject as Inject2, NotFoundException as NotFoundException2, Param as Param2, StreamableFile, UseFilters } from "@nestjs/common";
1073
+ import { Controller as Controller2, Get as Get2, Header as Header2, Inject as Inject3, NotFoundException as NotFoundException2, Param as Param2, StreamableFile, UseFilters } from "@nestjs/common";
1056
1074
 
1057
1075
  // src/server/auth/login-redirect.exception.ts
1058
1076
  import { Catch, HttpException, HttpStatus } from "@nestjs/common";
@@ -1088,7 +1106,7 @@ DashboardLoginRedirectFilter = _ts_decorate3([
1088
1106
  ], DashboardLoginRedirectFilter);
1089
1107
 
1090
1108
  // src/server/auth/session-required.exception.ts
1091
- import { Catch as Catch2, HttpException as HttpException2, HttpStatus as HttpStatus2 } from "@nestjs/common";
1109
+ import { Catch as Catch2, HttpException as HttpException2, HttpStatus as HttpStatus2, Inject as Inject2, Logger, Optional as Optional2 } from "@nestjs/common";
1092
1110
  function _ts_decorate4(decorators, target, key, desc) {
1093
1111
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1094
1112
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1096,6 +1114,16 @@ function _ts_decorate4(decorators, target, key, desc) {
1096
1114
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1097
1115
  }
1098
1116
  __name(_ts_decorate4, "_ts_decorate");
1117
+ function _ts_metadata3(k, v) {
1118
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1119
+ }
1120
+ __name(_ts_metadata3, "_ts_metadata");
1121
+ function _ts_param3(paramIndex, decorator) {
1122
+ return function(target, key) {
1123
+ decorator(target, key, paramIndex);
1124
+ };
1125
+ }
1126
+ __name(_ts_param3, "_ts_param");
1099
1127
  var DashboardSessionRequiredException = class extends HttpException2 {
1100
1128
  static {
1101
1129
  __name(this, "DashboardSessionRequiredException");
@@ -1107,17 +1135,51 @@ var DashboardSessionRequiredException = class extends HttpException2 {
1107
1135
  }, HttpStatus2.UNAUTHORIZED), this.basePath = basePath;
1108
1136
  }
1109
1137
  };
1110
- var DashboardSessionRequiredFilter = class {
1138
+ var DashboardSessionRequiredFilter = class _DashboardSessionRequiredFilter {
1111
1139
  static {
1112
1140
  __name(this, "DashboardSessionRequiredFilter");
1113
1141
  }
1114
- catch(exception, host) {
1115
- const response = host.switchToHttp().getResponse();
1142
+ auth;
1143
+ logger = new Logger(_DashboardSessionRequiredFilter.name);
1144
+ warned = false;
1145
+ constructor(auth = null) {
1146
+ this.auth = auth;
1147
+ }
1148
+ async catch(exception, host) {
1149
+ const http = host.switchToHttp();
1150
+ const response = http.getResponse();
1151
+ const hook = this.auth?.unauthenticatedPage;
1152
+ if (hook) {
1153
+ try {
1154
+ await hook({
1155
+ request: http.getRequest(),
1156
+ response,
1157
+ basePath: exception.basePath
1158
+ });
1159
+ if (responseAlreadyWritten(response)) return;
1160
+ this.warnOnce("dashboardAuth.unauthenticatedPage returned without writing a response; falling back to the built-in page. The hook must write AND end the response it is given.");
1161
+ } catch (error) {
1162
+ this.warnOnce(`dashboardAuth.unauthenticatedPage threw; falling back to the built-in page. ${String(error)}`);
1163
+ if (responseAlreadyWritten(response)) return;
1164
+ }
1165
+ }
1116
1166
  writeHtmlRaw(response, renderSessionRequiredPage(exception.basePath), HttpStatus2.UNAUTHORIZED);
1117
1167
  }
1168
+ /** Once per process, not once per request: a broken hook fires on every denied navigation. */
1169
+ warnOnce(message) {
1170
+ if (this.warned) return;
1171
+ this.warned = true;
1172
+ this.logger.warn(message);
1173
+ }
1118
1174
  };
1119
1175
  DashboardSessionRequiredFilter = _ts_decorate4([
1120
- Catch2(DashboardSessionRequiredException)
1176
+ Catch2(DashboardSessionRequiredException),
1177
+ _ts_param3(0, Optional2()),
1178
+ _ts_param3(0, Inject2(DASHBOARD_AUTH)),
1179
+ _ts_metadata3("design:type", Function),
1180
+ _ts_metadata3("design:paramtypes", [
1181
+ Object
1182
+ ])
1121
1183
  ], DashboardSessionRequiredFilter);
1122
1184
 
1123
1185
  // src/server/durable-ui.controller.ts
@@ -1128,16 +1190,16 @@ function _ts_decorate5(decorators, target, key, desc) {
1128
1190
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1129
1191
  }
1130
1192
  __name(_ts_decorate5, "_ts_decorate");
1131
- function _ts_metadata3(k, v) {
1193
+ function _ts_metadata4(k, v) {
1132
1194
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1133
1195
  }
1134
- __name(_ts_metadata3, "_ts_metadata");
1135
- function _ts_param3(paramIndex, decorator) {
1196
+ __name(_ts_metadata4, "_ts_metadata");
1197
+ function _ts_param4(paramIndex, decorator) {
1136
1198
  return function(target, key) {
1137
1199
  decorator(target, key, paramIndex);
1138
1200
  };
1139
1201
  }
1140
- __name(_ts_param3, "_ts_param");
1202
+ __name(_ts_param4, "_ts_param");
1141
1203
  var DASHBOARD_BASE_PATH = Symbol("DASHBOARD_BASE_PATH");
1142
1204
  var DASHBOARD_API_PATH = Symbol("DASHBOARD_API_PATH");
1143
1205
  var BUILD_BASE = "/durable";
@@ -1194,27 +1256,27 @@ _ts_decorate5([
1194
1256
  Get2(),
1195
1257
  Header2("Content-Type", "text/html; charset=utf-8"),
1196
1258
  Header2("Cache-Control", "no-store, must-revalidate"),
1197
- _ts_metadata3("design:type", Function),
1198
- _ts_metadata3("design:paramtypes", []),
1199
- _ts_metadata3("design:returntype", String)
1259
+ _ts_metadata4("design:type", Function),
1260
+ _ts_metadata4("design:paramtypes", []),
1261
+ _ts_metadata4("design:returntype", String)
1200
1262
  ], DurableUiController.prototype, "index", null);
1201
1263
  _ts_decorate5([
1202
1264
  Get2("assets/:file"),
1203
1265
  Header2("Cache-Control", "public, max-age=31536000, immutable"),
1204
- _ts_param3(0, Param2("file")),
1205
- _ts_metadata3("design:type", Function),
1206
- _ts_metadata3("design:paramtypes", [
1266
+ _ts_param4(0, Param2("file")),
1267
+ _ts_metadata4("design:type", Function),
1268
+ _ts_metadata4("design:paramtypes", [
1207
1269
  String
1208
1270
  ]),
1209
- _ts_metadata3("design:returntype", typeof StreamableFile === "undefined" ? Object : StreamableFile)
1271
+ _ts_metadata4("design:returntype", typeof StreamableFile === "undefined" ? Object : StreamableFile)
1210
1272
  ], DurableUiController.prototype, "asset", null);
1211
1273
  DurableUiController = _ts_decorate5([
1212
1274
  UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter),
1213
1275
  Controller2(),
1214
- _ts_param3(0, Inject2(DASHBOARD_BASE_PATH)),
1215
- _ts_param3(1, Inject2(DASHBOARD_API_PATH)),
1216
- _ts_metadata3("design:type", Function),
1217
- _ts_metadata3("design:paramtypes", [
1276
+ _ts_param4(0, Inject3(DASHBOARD_BASE_PATH)),
1277
+ _ts_param4(1, Inject3(DASHBOARD_API_PATH)),
1278
+ _ts_metadata4("design:type", Function),
1279
+ _ts_metadata4("design:paramtypes", [
1218
1280
  String,
1219
1281
  String
1220
1282
  ])
@@ -1228,16 +1290,16 @@ function _ts_decorate6(decorators, target, key, desc) {
1228
1290
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1229
1291
  }
1230
1292
  __name(_ts_decorate6, "_ts_decorate");
1231
- function _ts_metadata4(k, v) {
1293
+ function _ts_metadata5(k, v) {
1232
1294
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1233
1295
  }
1234
- __name(_ts_metadata4, "_ts_metadata");
1235
- function _ts_param4(paramIndex, decorator) {
1296
+ __name(_ts_metadata5, "_ts_metadata");
1297
+ function _ts_param5(paramIndex, decorator) {
1236
1298
  return function(target, key) {
1237
1299
  decorator(target, key, paramIndex);
1238
1300
  };
1239
1301
  }
1240
- __name(_ts_param4, "_ts_param");
1302
+ __name(_ts_param5, "_ts_param");
1241
1303
  function isString(value) {
1242
1304
  return typeof value === "string";
1243
1305
  }
@@ -1248,7 +1310,7 @@ var DurableAuthController = class _DurableAuthController {
1248
1310
  }
1249
1311
  auth;
1250
1312
  basePath;
1251
- logger = new Logger(_DurableAuthController.name);
1313
+ logger = new Logger2(_DurableAuthController.name);
1252
1314
  /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
1253
1315
  warnedHooks = /* @__PURE__ */ new Set();
1254
1316
  constructor(auth, basePath) {
@@ -1260,7 +1322,7 @@ var DurableAuthController = class _DurableAuthController {
1260
1322
  // the developer-controlled `basePath`.
1261
1323
  loginPage() {
1262
1324
  const auth = this.requireAuth();
1263
- if (!auth.login) throw new NotFoundException3();
1325
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1264
1326
  return renderLoginPage(this.basePath);
1265
1327
  }
1266
1328
  // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for
@@ -1269,7 +1331,7 @@ var DurableAuthController = class _DurableAuthController {
1269
1331
  // (unknown user, wrong password, throwing hook) — no user-enumeration.
1270
1332
  async login(body, request, response) {
1271
1333
  const auth = this.requireAuth();
1272
- if (!auth.login) throw new NotFoundException3();
1334
+ if (!auth.modes.includes("login")) throw new NotFoundException3();
1273
1335
  if (!isString(body?.username) || !isString(body?.password)) {
1274
1336
  throw new BadRequestException("Body must include string `username` and `password`.");
1275
1337
  }
@@ -1292,7 +1354,7 @@ var DurableAuthController = class _DurableAuthController {
1292
1354
  // guard decides, and no credential ever reaches this library.
1293
1355
  async session(request, response) {
1294
1356
  const auth = this.requireAuth();
1295
- if (!auth.session) throw new NotFoundException3();
1357
+ if (!auth.modes.includes("session")) throw new NotFoundException3();
1296
1358
  const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1297
1359
  if (!user) throw new UnauthorizedException();
1298
1360
  issueSessionCookie(user, {
@@ -1310,7 +1372,7 @@ var DurableAuthController = class _DurableAuthController {
1310
1372
  request,
1311
1373
  response
1312
1374
  });
1313
- redirectRaw(response, this.auth?.login ? `${this.basePath}/login` : this.basePath);
1375
+ redirectRaw(response, this.auth?.modes.includes("login") ? `${this.basePath}/login` : this.basePath);
1314
1376
  }
1315
1377
  requireAuth() {
1316
1378
  if (!this.auth) throw new NotFoundException3();
@@ -1337,58 +1399,58 @@ _ts_decorate6([
1337
1399
  Get3("login"),
1338
1400
  Header3("Content-Type", "text/html; charset=utf-8"),
1339
1401
  Header3("Cache-Control", "no-store, must-revalidate"),
1340
- _ts_metadata4("design:type", Function),
1341
- _ts_metadata4("design:paramtypes", []),
1342
- _ts_metadata4("design:returntype", String)
1402
+ _ts_metadata5("design:type", Function),
1403
+ _ts_metadata5("design:paramtypes", []),
1404
+ _ts_metadata5("design:returntype", String)
1343
1405
  ], DurableAuthController.prototype, "loginPage", null);
1344
1406
  _ts_decorate6([
1345
1407
  Post2("login"),
1346
1408
  HttpCode(200),
1347
- _ts_param4(0, Body2()),
1348
- _ts_param4(1, Req()),
1349
- _ts_param4(2, Res({
1409
+ _ts_param5(0, Body2()),
1410
+ _ts_param5(1, Req()),
1411
+ _ts_param5(2, Res({
1350
1412
  passthrough: true
1351
1413
  })),
1352
- _ts_metadata4("design:type", Function),
1353
- _ts_metadata4("design:paramtypes", [
1414
+ _ts_metadata5("design:type", Function),
1415
+ _ts_metadata5("design:paramtypes", [
1354
1416
  typeof LoginBody === "undefined" ? Object : LoginBody,
1355
1417
  Object,
1356
1418
  Object
1357
1419
  ]),
1358
- _ts_metadata4("design:returntype", Promise)
1420
+ _ts_metadata5("design:returntype", Promise)
1359
1421
  ], DurableAuthController.prototype, "login", null);
1360
1422
  _ts_decorate6([
1361
1423
  Post2("session"),
1362
1424
  HttpCode(204),
1363
- _ts_param4(0, Req()),
1364
- _ts_param4(1, Res({
1425
+ _ts_param5(0, Req()),
1426
+ _ts_param5(1, Res({
1365
1427
  passthrough: true
1366
1428
  })),
1367
- _ts_metadata4("design:type", Function),
1368
- _ts_metadata4("design:paramtypes", [
1429
+ _ts_metadata5("design:type", Function),
1430
+ _ts_metadata5("design:paramtypes", [
1369
1431
  Object,
1370
1432
  Object
1371
1433
  ]),
1372
- _ts_metadata4("design:returntype", Promise)
1434
+ _ts_metadata5("design:returntype", Promise)
1373
1435
  ], DurableAuthController.prototype, "session", null);
1374
1436
  _ts_decorate6([
1375
1437
  Get3("logout"),
1376
- _ts_param4(0, Req()),
1377
- _ts_param4(1, Res()),
1378
- _ts_metadata4("design:type", Function),
1379
- _ts_metadata4("design:paramtypes", [
1438
+ _ts_param5(0, Req()),
1439
+ _ts_param5(1, Res()),
1440
+ _ts_metadata5("design:type", Function),
1441
+ _ts_metadata5("design:paramtypes", [
1380
1442
  Object,
1381
1443
  Object
1382
1444
  ]),
1383
- _ts_metadata4("design:returntype", void 0)
1445
+ _ts_metadata5("design:returntype", void 0)
1384
1446
  ], DurableAuthController.prototype, "logout", null);
1385
1447
  DurableAuthController = _ts_decorate6([
1386
1448
  Controller3(),
1387
- _ts_param4(0, Optional2()),
1388
- _ts_param4(0, Inject3(DASHBOARD_AUTH)),
1389
- _ts_param4(1, Inject3(DASHBOARD_BASE_PATH)),
1390
- _ts_metadata4("design:type", Function),
1391
- _ts_metadata4("design:paramtypes", [
1449
+ _ts_param5(0, Optional3()),
1450
+ _ts_param5(0, Inject4(DASHBOARD_AUTH)),
1451
+ _ts_param5(1, Inject4(DASHBOARD_BASE_PATH)),
1452
+ _ts_metadata5("design:type", Function),
1453
+ _ts_metadata5("design:paramtypes", [
1392
1454
  Object,
1393
1455
  String
1394
1456
  ])
@@ -1400,7 +1462,7 @@ import { Module } from "@nestjs/common";
1400
1462
  import { RouterModule } from "@nestjs/core";
1401
1463
 
1402
1464
  // src/server/durable-session.guard.ts
1403
- import { Inject as Inject4, Injectable as Injectable2, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
1465
+ import { Inject as Inject5, Injectable as Injectable2, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
1404
1466
  function _ts_decorate7(decorators, target, key, desc) {
1405
1467
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1406
1468
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1408,16 +1470,16 @@ function _ts_decorate7(decorators, target, key, desc) {
1408
1470
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1409
1471
  }
1410
1472
  __name(_ts_decorate7, "_ts_decorate");
1411
- function _ts_metadata5(k, v) {
1473
+ function _ts_metadata6(k, v) {
1412
1474
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1413
1475
  }
1414
- __name(_ts_metadata5, "_ts_metadata");
1415
- function _ts_param5(paramIndex, decorator) {
1476
+ __name(_ts_metadata6, "_ts_metadata");
1477
+ function _ts_param6(paramIndex, decorator) {
1416
1478
  return function(target, key) {
1417
1479
  decorator(target, key, paramIndex);
1418
1480
  };
1419
1481
  }
1420
- __name(_ts_param5, "_ts_param");
1482
+ __name(_ts_param6, "_ts_param");
1421
1483
  var DurableUiSessionGuard = class {
1422
1484
  static {
1423
1485
  __name(this, "DurableUiSessionGuard");
@@ -1453,17 +1515,17 @@ var DurableUiSessionGuard = class {
1453
1515
  * asserting away) the nullability.
1454
1516
  */
1455
1517
  denyUnauthenticated(auth, request) {
1456
- if (!auth.login) throw new DashboardSessionRequiredException(this.basePath);
1518
+ if (!auth.modes.includes("login")) throw new DashboardSessionRequiredException(this.basePath);
1457
1519
  const returnTo = encodeURIComponent(originalRequestUrl(request));
1458
1520
  throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);
1459
1521
  }
1460
1522
  };
1461
1523
  DurableUiSessionGuard = _ts_decorate7([
1462
1524
  Injectable2(),
1463
- _ts_param5(0, Inject4(DASHBOARD_AUTH)),
1464
- _ts_param5(1, Inject4(DASHBOARD_BASE_PATH)),
1465
- _ts_metadata5("design:type", Function),
1466
- _ts_metadata5("design:paramtypes", [
1525
+ _ts_param6(0, Inject5(DASHBOARD_AUTH)),
1526
+ _ts_param6(1, Inject5(DASHBOARD_BASE_PATH)),
1527
+ _ts_metadata6("design:type", Function),
1528
+ _ts_metadata6("design:paramtypes", [
1467
1529
  Object,
1468
1530
  String
1469
1531
  ])
@@ -1478,21 +1540,35 @@ var DurableApiSessionGuard = class {
1478
1540
  }
1479
1541
  async canActivate(context) {
1480
1542
  if (!this.auth) return true;
1543
+ const auth = this.auth;
1481
1544
  const http = context.switchToHttp();
1482
1545
  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();
1546
+ const session = readSessionFromRequest(auth, request);
1547
+ if (!session) throw this.unauthorized(auth);
1548
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1549
+ throw this.unauthorized(auth);
1487
1550
  }
1488
1551
  return true;
1489
1552
  }
1553
+ /**
1554
+ * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
1555
+ * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
1556
+ * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
1557
+ * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
1558
+ */
1559
+ unauthorized(auth) {
1560
+ return new UnauthorizedException2({
1561
+ auth: {
1562
+ modes: auth.modes
1563
+ }
1564
+ });
1565
+ }
1490
1566
  };
1491
1567
  DurableApiSessionGuard = _ts_decorate7([
1492
1568
  Injectable2(),
1493
- _ts_param5(0, Inject4(DASHBOARD_AUTH)),
1494
- _ts_metadata5("design:type", Function),
1495
- _ts_metadata5("design:paramtypes", [
1569
+ _ts_param6(0, Inject5(DASHBOARD_AUTH)),
1570
+ _ts_metadata6("design:type", Function),
1571
+ _ts_metadata6("design:paramtypes", [
1496
1572
  Object
1497
1573
  ])
1498
1574
  ], DurableApiSessionGuard);