@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.
- package/dist/server/index.cjs +257 -103
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +101 -40
- package/dist/server/index.d.ts +101 -40
- package/dist/server/index.js +212 -58
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.d.cts
CHANGED
|
@@ -12,32 +12,53 @@ interface DashboardSessionUser {
|
|
|
12
12
|
roles?: string[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/** Host hook
|
|
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).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* server-rendered
|
|
25
|
-
*
|
|
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
|
-
/**
|
|
33
|
-
* errors are treated as a denial (logged
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
private
|
|
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
|
|
209
|
-
* warn-logged once, so a buggy hook never 500s the endpoint into a
|
|
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
|
|
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
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
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,
|
|
275
|
-
* {@link DurableDashboardAsyncOptions.useDashboardAuth} when
|
|
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`
|
|
284
|
-
* your DB/services.
|
|
285
|
-
*
|
|
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
|
|
337
|
-
* `DashboardLoginRedirectException` (see that file for why a guard
|
|
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
|
|
374
|
-
* `dashboardAuth` is configured):
|
|
375
|
-
* which `DurableUiSessionGuard` only throws when it is ALSO stamped on this
|
|
376
|
-
* `DurableDashboardModule.forRoot`) — so leaving
|
|
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;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -12,32 +12,53 @@ interface DashboardSessionUser {
|
|
|
12
12
|
roles?: string[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/** Host hook
|
|
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).
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* server-rendered
|
|
25
|
-
*
|
|
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
|
-
/**
|
|
33
|
-
* errors are treated as a denial (logged
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
199
|
-
private
|
|
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
|
|
209
|
-
* warn-logged once, so a buggy hook never 500s the endpoint into a
|
|
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
|
|
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
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
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,
|
|
275
|
-
* {@link DurableDashboardAsyncOptions.useDashboardAuth} when
|
|
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`
|
|
284
|
-
* your DB/services.
|
|
285
|
-
*
|
|
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
|
|
337
|
-
* `DashboardLoginRedirectException` (see that file for why a guard
|
|
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
|
|
374
|
-
* `dashboardAuth` is configured):
|
|
375
|
-
* which `DurableUiSessionGuard` only throws when it is ALSO stamped on this
|
|
376
|
-
* `DurableDashboardModule.forRoot`) — so leaving
|
|
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;
|