@dudousxd/nestjs-durable-dashboard 0.36.0 → 0.37.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.
@@ -12,32 +12,53 @@ interface DashboardSessionUser {
12
12
  roles?: string[];
13
13
  }
14
14
 
15
- /** Host hook validating submitted credentials from the built-in login page. */
15
+ /** Host hook for Mode A — validates the host's own auth on the raw request. */
16
+ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
17
+ /** Host hook for Mode B — validates submitted credentials from the built-in login page. */
16
18
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
19
+ /**
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.
24
+ */
25
+ type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
26
+ type AuthMode = 'session' | 'login';
17
27
  /**
18
28
  * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
19
29
  * BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON
20
30
  * API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s
21
- * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Unlike
22
- * Telescope's dashboard — a client-rendered SPA that can grow its own login screen — the durable
23
- * dashboard's bundled React app stays untouched: the login screen here is a small, dependency-free
24
- * server-rendered HTML page (`GET <basePath>/login`), so gating the SPA shell itself with a real
25
- * redirect doesn't require rebuilding or extending the Vite bundle.
31
+ * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Two ways to
32
+ * mint that cookie: Mode A (`session`) — the host frontend, already carrying its own auth, POSTs
33
+ * to `<basePath>/session` and the host hook decides; or Mode B (`login`) — the built-in,
34
+ * dependency-free server-rendered login page (`GET <basePath>/login`). At least one of the two is
35
+ * required so an un-mintable gate is a boot error, not a silently-open (or silently-stuck) console.
26
36
  */
27
37
  interface DashboardAuthOptions {
28
38
  /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */
29
39
  secret: string;
30
40
  /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */
31
41
  ttl?: string;
32
- /** Validates submitted username/password; return the session user, or `null` to deny. Thrown
33
- * errors are treated as a denial (logged once, never surfaced to the client). */
34
- login: LoginHook;
42
+ /** Mode A: validates the host's own auth on the raw request POSTed to `<basePath>/session`;
43
+ * return the session user, or `null` to deny. Thrown errors are treated as a denial (logged
44
+ * once, never surfaced to the client). */
45
+ session?: SessionHook;
46
+ /** Mode B: validates submitted username/password from the built-in login page; return the
47
+ * session user, or `null` to deny. Thrown errors are treated as a denial (logged once, never
48
+ * surfaced to the client). */
49
+ login?: LoginHook;
50
+ /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
51
+ * mint a session, only revoke one already minted by `session`/`login`. */
52
+ revalidate?: RevalidateHook;
35
53
  }
36
54
  /** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
37
55
  interface ResolvedDashboardAuth {
38
56
  secret: string;
39
57
  ttlMs: number;
40
- login: LoginHook;
58
+ modes: AuthMode[];
59
+ session?: SessionHook;
60
+ login?: LoginHook;
61
+ revalidate?: RevalidateHook;
41
62
  }
42
63
 
43
64
  /**
@@ -195,21 +216,22 @@ declare class DurableAuthController {
195
216
  private readonly auth;
196
217
  private readonly basePath;
197
218
  private readonly logger;
198
- /** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
199
- private warnedOnHookThrow;
219
+ /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
220
+ private readonly warnedHooks;
200
221
  constructor(auth: ResolvedDashboardAuth | null, basePath: string);
201
222
  loginPage(): string;
202
223
  login(body: LoginBody, request: unknown, response: unknown): Promise<{
203
224
  redirectTo: string;
204
225
  }>;
226
+ session(request: unknown, response: unknown): Promise<void>;
205
227
  logout(request: unknown, response: unknown): void;
206
228
  private requireAuth;
207
229
  /**
208
- * Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
209
- * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
210
- * the logs on repeated bad attempts.
230
+ * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
231
+ * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a
232
+ * stack-trace leak nor floods the logs on repeated bad attempts.
211
233
  */
212
- private runLoginHook;
234
+ private runHook;
213
235
  }
214
236
 
215
237
  interface DurableDashboardOptions {
@@ -253,13 +275,36 @@ interface DurableDashboardOptions {
253
275
  imports?: DynamicModule['imports'];
254
276
  /**
255
277
  * Gate the console (the SPA at `basePath` AND its JSON API at `apiBasePath`) behind a built-in,
256
- * cookie-session login — no infra, no changes to the bundled React SPA. Unlike
257
- * `@dudousxd/nestjs-telescope`'s `dashboardAuth` (a client-rendered login screen inside its SPA),
258
- * the durable dashboard's login page is small, dependency-free, server-rendered HTML served
259
- * directly by `DurableAuthController` at `GET <basePath>/login` — the SPA bundle itself never
260
- * changes. A missing/invalid/expired session redirects a page-level request (a full-page
261
- * navigation to `basePath`) to that login page with `?returnTo=<original url>`; an API request
262
- * (`apiBasePath`, fetched by the SPA's own JS) gets a plain `401` instead.
278
+ * signed session cookie — no infra required. Two ways to mint it:
279
+ *
280
+ * - **Mode A (`session`, recommended)**: your host app already has its own auth (SSO/OIDC/
281
+ * whatever) — the host frontend, carrying that auth, POSTs to `<basePath>/session`, your
282
+ * `session` hook validates the raw request and returns the session user, and this library mints
283
+ * its cookie from that. No credential this library understands ever exists; you keep your own
284
+ * identity provider as the source of truth. A page-level request with no valid session (and only
285
+ * Mode A configured) gets a small server-rendered instruction page telling the visitor to sign in
286
+ * through your host app, since there is no login page to redirect to.
287
+ * - **Mode B (`login`, standalone fallback)**: no host frontend/IdP to lean on — the durable
288
+ * dashboard serves its own small, dependency-free, server-rendered login page (`GET
289
+ * <basePath>/login`, handled by `DurableAuthController`) and your `login` hook validates
290
+ * submitted username/password. A missing/invalid/expired session redirects a page-level request
291
+ * to that login page with `?returnTo=<original url>`.
292
+ *
293
+ * Either mode can be used alone, or both together (e.g. Mode A for normal use, Mode B as a
294
+ * break-glass fallback) — at least one is required, or `forRoot`/`forRootAsync` throws at boot
295
+ * (an un-mintable gate is a boot error, not a silently-open or silently-stuck console). An API
296
+ * request (`apiBasePath`, fetched by the SPA's own JS) always gets a plain `401` on a missing
297
+ * session, regardless of mode — the caller reads the status code, not HTML.
298
+ *
299
+ * A third, optional hook — **`revalidate`** — re-checks an already-minted session rather than
300
+ * minting one: once a session cookie is past 50% of its TTL, the guard slides it forward with a
301
+ * fresh one, and if `revalidate` is set it's consulted first, so a deactivated or demoted
302
+ * operator loses access instead of riding a self-renewing cookie indefinitely. It only runs on
303
+ * that renewal path, not on every request, so revocation has **latency, not immediacy**: an
304
+ * operator revoked right after a renewal keeps their existing cookie — and therefore console
305
+ * access — for up to `ttl/2` (4 hours at the default 8h TTL) before the next renewal check catches
306
+ * it. Do not rely on `revalidate` for a same-second cutoff; front the mount with your own guard
307
+ * (see {@link guards}) if you need that.
263
308
  *
264
309
  * Auth here is mount-level and role-agnostic: this option (and the guards/controller it wires up)
265
310
  * has no notion of control-plane vs tenant role (see the durable dashboard's topology note in
@@ -271,18 +316,19 @@ interface DurableDashboardOptions {
271
316
  * the console open (today's behavior, unchanged byte-for-byte) — front it with `guards`, a global
272
317
  * guard, or a reverse proxy instead.
273
318
  *
274
- * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`login` shape, and `forRootAsync` /
275
- * {@link DurableDashboardAsyncOptions.useDashboardAuth} when the `login` hook needs DI (e.g. an
276
- * `EntityManager` to look up an admin user).
319
+ * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`session`/`login`/`revalidate` shape,
320
+ * and `forRootAsync` / {@link DurableDashboardAsyncOptions.useDashboardAuth} when a hook needs DI
321
+ * (e.g. an `EntityManager` to look up an admin user).
277
322
  */
278
323
  dashboardAuth?: DashboardAuthOptions;
279
324
  }
280
325
  /**
281
326
  * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
282
327
  * router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`
283
- * already has), but `dashboardAuth` is built by an injected factory, so its `login` hook can reach
284
- * your DB/services. Returning `undefined` from `useDashboardAuth` leaves the console open, exactly
285
- * like omitting `dashboardAuth` from `forRoot`.
328
+ * already has), but `dashboardAuth` is built by an injected factory, so its `session`/`login` hooks
329
+ * can reach your DB/services — e.g. Mode A's `session` hook calling out to the same auth service
330
+ * your host app already uses to validate the raw request. Returning `undefined` from
331
+ * `useDashboardAuth` leaves the console open, exactly like omitting `dashboardAuth` from `forRoot`.
286
332
  */
287
333
  interface DurableDashboardAsyncOptions {
288
334
  basePath?: string;
@@ -332,10 +378,12 @@ declare class DurableDashboardModule {
332
378
  /**
333
379
  * Gates `DurableUiController` (the SPA shell + assets, a full-page browser navigation) on a valid
334
380
  * `dashboardAuth` session cookie. A no-op (always `true`) when `dashboardAuth` was not configured
335
- * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session =>
336
- * a `302` to the built-in login page carrying `?returnTo=<original url>`, via
337
- * `DashboardLoginRedirectException` (see that file for why a guard can't just call
338
- * `response.redirect()` and return `false`).
381
+ * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session,
382
+ * when Mode B (`login`) is configured, gets a `302` to the built-in login page carrying
383
+ * `?returnTo=<original url>`, via `DashboardLoginRedirectException` (see that file for why a guard
384
+ * can't just call `response.redirect()` and return `false`). When only Mode A (`session`) is
385
+ * configured there is no login page to redirect to — the host mints the session itself — so it
386
+ * instead throws `DashboardSessionRequiredException`, rendering a small instruction page.
339
387
  *
340
388
  * Auth is mount-level and role-agnostic: this guard has no notion of control-plane vs tenant
341
389
  * (see the durable dashboard's topology note in `dashboard.service.ts`) — it only ever answers
@@ -345,7 +393,18 @@ declare class DurableUiSessionGuard implements CanActivate {
345
393
  private readonly auth;
346
394
  private readonly basePath;
347
395
  constructor(auth: ResolvedDashboardAuth | null, basePath: string);
348
- canActivate(context: ExecutionContext): boolean;
396
+ canActivate(context: ExecutionContext): Promise<boolean>;
397
+ /**
398
+ * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent
399
+ * one — by design, there is nothing left to distinguish "never had a session" from "had one,
400
+ * then lost it" once the cookie is cleared, so both get the same Mode-aware treatment.
401
+ *
402
+ * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
403
+ * return narrows the field within that method only — TS doesn't carry it across a method call
404
+ * — so callers pass the already-narrowed value instead of this method re-deriving (or
405
+ * asserting away) the nullability.
406
+ */
407
+ private denyUnauthenticated;
349
408
  }
350
409
  /**
351
410
  * Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session
@@ -358,7 +417,7 @@ declare class DurableUiSessionGuard implements CanActivate {
358
417
  declare class DurableApiSessionGuard implements CanActivate {
359
418
  private readonly auth;
360
419
  constructor(auth: ResolvedDashboardAuth | null);
361
- canActivate(context: ExecutionContext): boolean;
420
+ canActivate(context: ExecutionContext): Promise<boolean>;
362
421
  }
363
422
 
364
423
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -370,11 +429,13 @@ declare const DASHBOARD_API_PATH: unique symbol;
370
429
  * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the
371
430
  * controller routes are relative.
372
431
  *
373
- * `@UseFilters(DashboardLoginRedirectFilter)` is permanent (present regardless of whether
374
- * `dashboardAuth` is configured): it only ever activates for a `DashboardLoginRedirectException`,
375
- * which `DurableUiSessionGuard` only throws when it is ALSO stamped on this controller (see
376
- * `DurableDashboardModule.forRoot`) — so leaving it decorated here is inert, not a behavior change,
377
- * when `dashboardAuth` is absent.
432
+ * `@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)` is permanent (present
433
+ * regardless of whether `dashboardAuth` is configured): each only ever activates for its own
434
+ * exception type, which `DurableUiSessionGuard` only throws when it is ALSO stamped on this
435
+ * controller (see `DurableDashboardModule.forRoot`) — so leaving them decorated here is inert, not
436
+ * a behavior change, when `dashboardAuth` is absent. The guard picks between the two exceptions
437
+ * based on which mode is configured (Mode B => redirect to the login page; Mode A only => render
438
+ * the session-required instruction page in place, since there is no login page to redirect to).
378
439
  */
379
440
  declare class DurableUiController {
380
441
  private readonly basePath;
@@ -12,32 +12,53 @@ interface DashboardSessionUser {
12
12
  roles?: string[];
13
13
  }
14
14
 
15
- /** Host hook validating submitted credentials from the built-in login page. */
15
+ /** Host hook for Mode A — validates the host's own auth on the raw request. */
16
+ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
17
+ /** Host hook for Mode B — validates submitted credentials from the built-in login page. */
16
18
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
19
+ /**
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.
24
+ */
25
+ type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
26
+ type AuthMode = 'session' | 'login';
17
27
  /**
18
28
  * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
19
29
  * BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON
20
30
  * API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s
21
- * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Unlike
22
- * Telescope's dashboard — a client-rendered SPA that can grow its own login screen — the durable
23
- * dashboard's bundled React app stays untouched: the login screen here is a small, dependency-free
24
- * server-rendered HTML page (`GET <basePath>/login`), so gating the SPA shell itself with a real
25
- * redirect doesn't require rebuilding or extending the Vite bundle.
31
+ * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Two ways to
32
+ * mint that cookie: Mode A (`session`) — the host frontend, already carrying its own auth, POSTs
33
+ * to `<basePath>/session` and the host hook decides; or Mode B (`login`) — the built-in,
34
+ * dependency-free server-rendered login page (`GET <basePath>/login`). At least one of the two is
35
+ * required so an un-mintable gate is a boot error, not a silently-open (or silently-stuck) console.
26
36
  */
27
37
  interface DashboardAuthOptions {
28
38
  /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */
29
39
  secret: string;
30
40
  /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */
31
41
  ttl?: string;
32
- /** Validates submitted username/password; return the session user, or `null` to deny. Thrown
33
- * errors are treated as a denial (logged once, never surfaced to the client). */
34
- login: LoginHook;
42
+ /** Mode A: validates the host's own auth on the raw request POSTed to `<basePath>/session`;
43
+ * return the session user, or `null` to deny. Thrown errors are treated as a denial (logged
44
+ * once, never surfaced to the client). */
45
+ session?: SessionHook;
46
+ /** Mode B: validates submitted username/password from the built-in login page; return the
47
+ * session user, or `null` to deny. Thrown errors are treated as a denial (logged once, never
48
+ * surfaced to the client). */
49
+ login?: LoginHook;
50
+ /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
51
+ * mint a session, only revoke one already minted by `session`/`login`. */
52
+ revalidate?: RevalidateHook;
35
53
  }
36
54
  /** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
37
55
  interface ResolvedDashboardAuth {
38
56
  secret: string;
39
57
  ttlMs: number;
40
- login: LoginHook;
58
+ modes: AuthMode[];
59
+ session?: SessionHook;
60
+ login?: LoginHook;
61
+ revalidate?: RevalidateHook;
41
62
  }
42
63
 
43
64
  /**
@@ -195,21 +216,22 @@ declare class DurableAuthController {
195
216
  private readonly auth;
196
217
  private readonly basePath;
197
218
  private readonly logger;
198
- /** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
199
- private warnedOnHookThrow;
219
+ /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
220
+ private readonly warnedHooks;
200
221
  constructor(auth: ResolvedDashboardAuth | null, basePath: string);
201
222
  loginPage(): string;
202
223
  login(body: LoginBody, request: unknown, response: unknown): Promise<{
203
224
  redirectTo: string;
204
225
  }>;
226
+ session(request: unknown, response: unknown): Promise<void>;
205
227
  logout(request: unknown, response: unknown): void;
206
228
  private requireAuth;
207
229
  /**
208
- * Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
209
- * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
210
- * the logs on repeated bad attempts.
230
+ * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
231
+ * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a
232
+ * stack-trace leak nor floods the logs on repeated bad attempts.
211
233
  */
212
- private runLoginHook;
234
+ private runHook;
213
235
  }
214
236
 
215
237
  interface DurableDashboardOptions {
@@ -253,13 +275,36 @@ interface DurableDashboardOptions {
253
275
  imports?: DynamicModule['imports'];
254
276
  /**
255
277
  * Gate the console (the SPA at `basePath` AND its JSON API at `apiBasePath`) behind a built-in,
256
- * cookie-session login — no infra, no changes to the bundled React SPA. Unlike
257
- * `@dudousxd/nestjs-telescope`'s `dashboardAuth` (a client-rendered login screen inside its SPA),
258
- * the durable dashboard's login page is small, dependency-free, server-rendered HTML served
259
- * directly by `DurableAuthController` at `GET <basePath>/login` — the SPA bundle itself never
260
- * changes. A missing/invalid/expired session redirects a page-level request (a full-page
261
- * navigation to `basePath`) to that login page with `?returnTo=<original url>`; an API request
262
- * (`apiBasePath`, fetched by the SPA's own JS) gets a plain `401` instead.
278
+ * signed session cookie — no infra required. Two ways to mint it:
279
+ *
280
+ * - **Mode A (`session`, recommended)**: your host app already has its own auth (SSO/OIDC/
281
+ * whatever) — the host frontend, carrying that auth, POSTs to `<basePath>/session`, your
282
+ * `session` hook validates the raw request and returns the session user, and this library mints
283
+ * its cookie from that. No credential this library understands ever exists; you keep your own
284
+ * identity provider as the source of truth. A page-level request with no valid session (and only
285
+ * Mode A configured) gets a small server-rendered instruction page telling the visitor to sign in
286
+ * through your host app, since there is no login page to redirect to.
287
+ * - **Mode B (`login`, standalone fallback)**: no host frontend/IdP to lean on — the durable
288
+ * dashboard serves its own small, dependency-free, server-rendered login page (`GET
289
+ * <basePath>/login`, handled by `DurableAuthController`) and your `login` hook validates
290
+ * submitted username/password. A missing/invalid/expired session redirects a page-level request
291
+ * to that login page with `?returnTo=<original url>`.
292
+ *
293
+ * Either mode can be used alone, or both together (e.g. Mode A for normal use, Mode B as a
294
+ * break-glass fallback) — at least one is required, or `forRoot`/`forRootAsync` throws at boot
295
+ * (an un-mintable gate is a boot error, not a silently-open or silently-stuck console). An API
296
+ * request (`apiBasePath`, fetched by the SPA's own JS) always gets a plain `401` on a missing
297
+ * session, regardless of mode — the caller reads the status code, not HTML.
298
+ *
299
+ * A third, optional hook — **`revalidate`** — re-checks an already-minted session rather than
300
+ * minting one: once a session cookie is past 50% of its TTL, the guard slides it forward with a
301
+ * fresh one, and if `revalidate` is set it's consulted first, so a deactivated or demoted
302
+ * operator loses access instead of riding a self-renewing cookie indefinitely. It only runs on
303
+ * that renewal path, not on every request, so revocation has **latency, not immediacy**: an
304
+ * operator revoked right after a renewal keeps their existing cookie — and therefore console
305
+ * access — for up to `ttl/2` (4 hours at the default 8h TTL) before the next renewal check catches
306
+ * it. Do not rely on `revalidate` for a same-second cutoff; front the mount with your own guard
307
+ * (see {@link guards}) if you need that.
263
308
  *
264
309
  * Auth here is mount-level and role-agnostic: this option (and the guards/controller it wires up)
265
310
  * has no notion of control-plane vs tenant role (see the durable dashboard's topology note in
@@ -271,18 +316,19 @@ interface DurableDashboardOptions {
271
316
  * the console open (today's behavior, unchanged byte-for-byte) — front it with `guards`, a global
272
317
  * guard, or a reverse proxy instead.
273
318
  *
274
- * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`login` shape, and `forRootAsync` /
275
- * {@link DurableDashboardAsyncOptions.useDashboardAuth} when the `login` hook needs DI (e.g. an
276
- * `EntityManager` to look up an admin user).
319
+ * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`session`/`login`/`revalidate` shape,
320
+ * and `forRootAsync` / {@link DurableDashboardAsyncOptions.useDashboardAuth} when a hook needs DI
321
+ * (e.g. an `EntityManager` to look up an admin user).
277
322
  */
278
323
  dashboardAuth?: DashboardAuthOptions;
279
324
  }
280
325
  /**
281
326
  * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
282
327
  * router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`
283
- * already has), but `dashboardAuth` is built by an injected factory, so its `login` hook can reach
284
- * your DB/services. Returning `undefined` from `useDashboardAuth` leaves the console open, exactly
285
- * like omitting `dashboardAuth` from `forRoot`.
328
+ * already has), but `dashboardAuth` is built by an injected factory, so its `session`/`login` hooks
329
+ * can reach your DB/services — e.g. Mode A's `session` hook calling out to the same auth service
330
+ * your host app already uses to validate the raw request. Returning `undefined` from
331
+ * `useDashboardAuth` leaves the console open, exactly like omitting `dashboardAuth` from `forRoot`.
286
332
  */
287
333
  interface DurableDashboardAsyncOptions {
288
334
  basePath?: string;
@@ -332,10 +378,12 @@ declare class DurableDashboardModule {
332
378
  /**
333
379
  * Gates `DurableUiController` (the SPA shell + assets, a full-page browser navigation) on a valid
334
380
  * `dashboardAuth` session cookie. A no-op (always `true`) when `dashboardAuth` was not configured
335
- * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session =>
336
- * a `302` to the built-in login page carrying `?returnTo=<original url>`, via
337
- * `DashboardLoginRedirectException` (see that file for why a guard can't just call
338
- * `response.redirect()` and return `false`).
381
+ * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session,
382
+ * when Mode B (`login`) is configured, gets a `302` to the built-in login page carrying
383
+ * `?returnTo=<original url>`, via `DashboardLoginRedirectException` (see that file for why a guard
384
+ * can't just call `response.redirect()` and return `false`). When only Mode A (`session`) is
385
+ * configured there is no login page to redirect to — the host mints the session itself — so it
386
+ * instead throws `DashboardSessionRequiredException`, rendering a small instruction page.
339
387
  *
340
388
  * Auth is mount-level and role-agnostic: this guard has no notion of control-plane vs tenant
341
389
  * (see the durable dashboard's topology note in `dashboard.service.ts`) — it only ever answers
@@ -345,7 +393,18 @@ declare class DurableUiSessionGuard implements CanActivate {
345
393
  private readonly auth;
346
394
  private readonly basePath;
347
395
  constructor(auth: ResolvedDashboardAuth | null, basePath: string);
348
- canActivate(context: ExecutionContext): boolean;
396
+ canActivate(context: ExecutionContext): Promise<boolean>;
397
+ /**
398
+ * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent
399
+ * one — by design, there is nothing left to distinguish "never had a session" from "had one,
400
+ * then lost it" once the cookie is cleared, so both get the same Mode-aware treatment.
401
+ *
402
+ * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
403
+ * return narrows the field within that method only — TS doesn't carry it across a method call
404
+ * — so callers pass the already-narrowed value instead of this method re-deriving (or
405
+ * asserting away) the nullability.
406
+ */
407
+ private denyUnauthenticated;
349
408
  }
350
409
  /**
351
410
  * Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session
@@ -358,7 +417,7 @@ declare class DurableUiSessionGuard implements CanActivate {
358
417
  declare class DurableApiSessionGuard implements CanActivate {
359
418
  private readonly auth;
360
419
  constructor(auth: ResolvedDashboardAuth | null);
361
- canActivate(context: ExecutionContext): boolean;
420
+ canActivate(context: ExecutionContext): Promise<boolean>;
362
421
  }
363
422
 
364
423
  /** DI token carrying the UI mount base (e.g. `/durable`). */
@@ -370,11 +429,13 @@ declare const DASHBOARD_API_PATH: unique symbol;
370
429
  * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the
371
430
  * controller routes are relative.
372
431
  *
373
- * `@UseFilters(DashboardLoginRedirectFilter)` is permanent (present regardless of whether
374
- * `dashboardAuth` is configured): it only ever activates for a `DashboardLoginRedirectException`,
375
- * which `DurableUiSessionGuard` only throws when it is ALSO stamped on this controller (see
376
- * `DurableDashboardModule.forRoot`) — so leaving it decorated here is inert, not a behavior change,
377
- * when `dashboardAuth` is absent.
432
+ * `@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)` is permanent (present
433
+ * regardless of whether `dashboardAuth` is configured): each only ever activates for its own
434
+ * exception type, which `DurableUiSessionGuard` only throws when it is ALSO stamped on this
435
+ * controller (see `DurableDashboardModule.forRoot`) — so leaving them decorated here is inert, not
436
+ * a behavior change, when `dashboardAuth` is absent. The guard picks between the two exceptions
437
+ * based on which mode is configured (Mode B => redirect to the login page; Mode A only => render
438
+ * the session-required instruction page in place, since there is no login page to redirect to).
378
439
  */
379
440
  declare class DurableUiController {
380
441
  private readonly basePath;