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