@dudousxd/nestjs-durable-dashboard 0.54.0 → 0.55.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/client/durable-client.d.ts +6 -1
- package/dist/client/durable-client.d.ts.map +1 -1
- package/dist/client/durable-client.js +9 -46
- package/dist/client/durable-client.js.map +1 -1
- package/dist/server/index.cjs +223 -1341
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +101 -388
- package/dist/server/index.d.ts +101 -388
- package/dist/server/index.js +208 -1320
- package/dist/server/index.js.map +1 -1
- package/dist/spa/assets/{bench-Dee6-QN_.js → bench-DCclNvcZ.js} +1 -1
- package/dist/spa/assets/index-B-wtX4GV.js +66 -0
- package/dist/spa/assets/{index-RjxbO7dU.js → index-ChjKRQZx.js} +1 -1
- package/dist/spa/assets/{preview-BscU9n0o.js → preview-CdsjvOrK.js} +1 -1
- package/dist/spa/bench.html +2 -2
- package/dist/spa/index.html +2 -2
- package/dist/spa/preview.html +2 -2
- package/package.json +5 -18
- package/dist/client/console-session.d.ts +0 -63
- package/dist/client/console-session.d.ts.map +0 -1
- package/dist/client/console-session.js +0 -107
- package/dist/client/console-session.js.map +0 -1
- package/dist/react/index.d.ts +0 -4
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -14
- package/dist/react/index.js.map +0 -1
- package/dist/react/open-console-button.d.ts +0 -26
- package/dist/react/open-console-button.d.ts.map +0 -1
- package/dist/react/open-console-button.js +0 -26
- package/dist/react/open-console-button.js.map +0 -1
- package/dist/react/use-open-console.d.ts +0 -40
- package/dist/react/use-open-console.d.ts.map +0 -1
- package/dist/react/use-open-console.js +0 -77
- package/dist/react/use-open-console.js.map +0 -1
- package/dist/spa/assets/index-VXNPW483.js +0 -66
package/dist/server/index.d.cts
CHANGED
|
@@ -5,104 +5,7 @@ import * as rxjs from 'rxjs';
|
|
|
5
5
|
import { Observable } from 'rxjs';
|
|
6
6
|
import * as _dudousxd_nestjs_filter from '@dudousxd/nestjs-filter';
|
|
7
7
|
import { FilterRunner } from '@dudousxd/nestjs-filter';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
/** The session user the host `login` hook returns to mint a cookie. */
|
|
11
|
-
interface DashboardSessionUser {
|
|
12
|
-
id: string;
|
|
13
|
-
name?: string;
|
|
14
|
-
roles?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Host hook for Mode A — validates the host's own auth on the raw request. */
|
|
18
|
-
type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
|
|
19
|
-
/** Host hook for Mode B — validates submitted credentials from the built-in login page. */
|
|
20
|
-
type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
|
|
21
|
-
/**
|
|
22
|
-
* Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per
|
|
23
|
-
* session, so a DB round-trip here is cheap — this holds even under concurrent traffic: N parallel
|
|
24
|
-
* requests carrying the same past-half-life cookie share ONE call (see `revalidateOnce` in
|
|
25
|
-
* `session-cookie-io.ts`), not N. Return `false` to revoke (the cookie is cleared and the request
|
|
26
|
-
* denied). Distinct from `session`: that hook reads the host's auth off a fresh request, which a
|
|
27
|
-
* console XHR does not carry — this one receives the already-minted session.
|
|
28
|
-
*/
|
|
29
|
-
type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
|
|
30
|
-
/** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */
|
|
31
|
-
interface UnauthenticatedPageContext {
|
|
32
|
-
/**
|
|
33
|
-
* The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`
|
|
34
|
-
* for the same reason `SessionHook`'s is: this package refuses to depend on either platform.
|
|
35
|
-
* Cast it to whatever your app actually runs on.
|
|
36
|
-
*/
|
|
37
|
-
request: unknown;
|
|
38
|
-
/**
|
|
39
|
-
* The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,
|
|
40
|
-
* `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the
|
|
41
|
-
* library falls back to its own built-in page rather than leaving the request hanging.
|
|
42
|
-
*/
|
|
43
|
-
response: unknown;
|
|
44
|
-
/** Where this console is mounted (e.g. `/durable`) — useful for a "back to the console" link. */
|
|
45
|
-
basePath: string;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Host-owned page for an unauthenticated full-page navigation to the console.
|
|
49
|
-
*
|
|
50
|
-
* The built-in page cannot know who hosts the console, so it can only say "open this console from
|
|
51
|
-
* your application" in the abstract — it can't name the launcher, link to it, or look like the rest
|
|
52
|
-
* of the host's product. This hook hands the whole response to the host instead: render through
|
|
53
|
-
* your own template engine, your own Inertia page, whatever. The page is served AT the console's
|
|
54
|
-
* own URL, so `/durable` stays `/durable`.
|
|
55
|
-
*
|
|
56
|
-
* Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login
|
|
57
|
-
* UI uses Mode A plus this hook, and posts to `<basePath>/session` from its own page — the mint
|
|
58
|
-
* endpoint is the supported primitive for that.
|
|
59
|
-
*
|
|
60
|
-
* Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook
|
|
61
|
-
* that throws, or that returns without writing, falls back to the built-in page — it cannot let
|
|
62
|
-
* anyone in.
|
|
63
|
-
*/
|
|
64
|
-
type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;
|
|
65
|
-
type AuthMode = 'session' | 'login';
|
|
66
|
-
/**
|
|
67
|
-
* Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
|
|
68
|
-
* BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON
|
|
69
|
-
* API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s
|
|
70
|
-
* `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Two ways to
|
|
71
|
-
* mint that cookie: Mode A (`session`) — the host frontend, already carrying its own auth, POSTs
|
|
72
|
-
* to `<basePath>/session` and the host hook decides; or Mode B (`login`) — the built-in,
|
|
73
|
-
* dependency-free server-rendered login page (`GET <basePath>/login`). At least one of the two is
|
|
74
|
-
* required so an un-mintable gate is a boot error, not a silently-open (or silently-stuck) console.
|
|
75
|
-
*/
|
|
76
|
-
interface DashboardAuthOptions {
|
|
77
|
-
/** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */
|
|
78
|
-
secret: string;
|
|
79
|
-
/** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */
|
|
80
|
-
ttl?: string;
|
|
81
|
-
/** Mode A: validates the host's own auth on the raw request POSTed to `<basePath>/session`;
|
|
82
|
-
* return the session user, or `null` to deny. Thrown errors are treated as a denial (logged
|
|
83
|
-
* once, never surfaced to the client). */
|
|
84
|
-
session?: SessionHook;
|
|
85
|
-
/** Mode B: validates submitted username/password from the built-in login page; return the
|
|
86
|
-
* session user, or `null` to deny. Thrown errors are treated as a denial (logged once, never
|
|
87
|
-
* surfaced to the client). */
|
|
88
|
-
login?: LoginHook;
|
|
89
|
-
/** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
|
|
90
|
-
* mint a session, only revoke one already minted by `session`/`login`. */
|
|
91
|
-
revalidate?: RevalidateHook;
|
|
92
|
-
/** Renders the host's own page for an unauthenticated navigation, in place of the built-in
|
|
93
|
-
* "open this console from your application" card; see `UnauthenticatedPageHook`. */
|
|
94
|
-
unauthenticatedPage?: UnauthenticatedPageHook;
|
|
95
|
-
}
|
|
96
|
-
/** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
|
|
97
|
-
interface ResolvedDashboardAuth {
|
|
98
|
-
secret: string;
|
|
99
|
-
ttlMs: number;
|
|
100
|
-
modes: AuthMode[];
|
|
101
|
-
session?: SessionHook;
|
|
102
|
-
login?: LoginHook;
|
|
103
|
-
revalidate?: RevalidateHook;
|
|
104
|
-
unauthenticatedPage?: UnauthenticatedPageHook;
|
|
105
|
-
}
|
|
8
|
+
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
106
9
|
|
|
107
10
|
/**
|
|
108
11
|
* Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/
|
|
@@ -286,14 +189,20 @@ declare class RunQueryDraft {
|
|
|
286
189
|
}
|
|
287
190
|
|
|
288
191
|
/**
|
|
289
|
-
* JSON API consumed by the control-plane SPA.
|
|
192
|
+
* JSON API consumed by the control-plane SPA. The mounted {@link DurableApiController} sits at
|
|
193
|
+
* `apiBasePath` (set by RouterModule).
|
|
194
|
+
*
|
|
195
|
+
* This base is what a host EXTENDS to serve the API from a controller of its own (with
|
|
196
|
+
* `forRoot({ routes: false, apiBasePath })`): `@Controller('<apiBasePath>') class MyConsoleApi
|
|
197
|
+
* extends DurableApiControllerBase {}`. It carries no guards and no `@nestjs/authorization` mark,
|
|
198
|
+
* so a subclass is protected only by what the host puts on it.
|
|
290
199
|
*
|
|
291
200
|
* The filter interceptor (what resolves `@ApplyFilter` into a query) is bound HERE rather than as
|
|
292
201
|
* the app-wide `APP_INTERCEPTOR` that `FilterModule.forRoot()` registers: a library must not add a
|
|
293
202
|
* global interceptor to its host, and on an app that already has one this would be the second copy,
|
|
294
203
|
* running every filter in the app twice.
|
|
295
204
|
*/
|
|
296
|
-
declare class
|
|
205
|
+
declare class DurableApiControllerBase {
|
|
297
206
|
private readonly dashboard;
|
|
298
207
|
private readonly runner;
|
|
299
208
|
constructor(dashboard: DashboardService, runner: FilterRunner);
|
|
@@ -426,103 +335,13 @@ declare class DurableApiController {
|
|
|
426
335
|
/** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */
|
|
427
336
|
update(id: string, name: string, body: unknown): Promise<_dudousxd_nestjs_durable_core.UpdateResult>;
|
|
428
337
|
}
|
|
429
|
-
|
|
430
|
-
interface LoginBody {
|
|
431
|
-
username?: unknown;
|
|
432
|
-
password?: unknown;
|
|
433
|
-
returnTo?: unknown;
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Mints/clears the `dashboardAuth` session cookie and serves the built-in login page. Mounted
|
|
437
|
-
* under `basePath` alongside `DurableUiController` (see `DurableDashboardModule.forRoot`), but on
|
|
438
|
-
* a SEPARATE controller so it is never itself behind `DurableUiSessionGuard` — these endpoints
|
|
439
|
-
* CREATE the session that guard checks for. `@Optional()` on both injections: this controller is
|
|
440
|
-
* only registered when `dashboardAuth` might be configured (a static `forRoot({ dashboardAuth })`
|
|
441
|
-
* or any `forRootAsync`), but the resolved value can still be `null` at runtime for the async case
|
|
442
|
-
* — every handler below re-checks it and 404s rather than assuming it is set.
|
|
443
|
-
*/
|
|
444
|
-
declare class DurableAuthController {
|
|
445
|
-
private readonly auth;
|
|
446
|
-
private readonly basePath;
|
|
447
|
-
private readonly logger;
|
|
448
|
-
/** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */
|
|
449
|
-
private readonly warnedHooks;
|
|
450
|
-
constructor(auth: ResolvedDashboardAuth | null, basePath: string);
|
|
451
|
-
loginPage(): string;
|
|
452
|
-
login(body: LoginBody, request: unknown, response: unknown): Promise<{
|
|
453
|
-
redirectTo: string;
|
|
454
|
-
}>;
|
|
455
|
-
session(request: unknown, response: unknown): Promise<void>;
|
|
456
|
-
logout(request: unknown, response: unknown): void;
|
|
457
|
-
private requireAuth;
|
|
458
|
-
/**
|
|
459
|
-
* Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform
|
|
460
|
-
* failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a
|
|
461
|
-
* stack-trace leak nor floods the logs on repeated bad attempts.
|
|
462
|
-
*/
|
|
463
|
-
private runHook;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/** The env var the token gate reads when no `token` option is given. */
|
|
467
|
-
declare const DURABLE_DASHBOARD_TOKEN_ENV = "DURABLE_DASHBOARD_TOKEN";
|
|
468
|
-
/** DI token carrying the resolved {@link DashboardGate} (one per dashboard mount). */
|
|
469
|
-
declare const DASHBOARD_GATE: unique symbol;
|
|
470
|
-
/**
|
|
471
|
-
* What the module knows about how the console is protected, captured at `forRoot`/`forRootAsync`
|
|
472
|
-
* time. Everything that can change between module definition and boot (env vars, the async
|
|
473
|
-
* `dashboardAuth` factory) is read later, in {@link resolveDashboardGate}.
|
|
474
|
-
*/
|
|
475
|
-
interface DashboardGateOptions {
|
|
476
|
-
/** Host `guards` were passed (non-empty). */
|
|
477
|
-
hasGuards: boolean;
|
|
478
|
-
/** `allowUnauthenticated` as passed. */
|
|
479
|
-
allowUnauthenticated?: boolean;
|
|
480
|
-
/** `token` as passed (else `DURABLE_DASHBOARD_TOKEN` is read at boot). */
|
|
481
|
-
token?: string;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* How the gate treats every console request:
|
|
485
|
-
*
|
|
486
|
-
* - `delegated`: `guards` or `dashboardAuth` protect the console; the gate passes everything.
|
|
487
|
-
* - `open`: `allowUnauthenticated: true`; the gate passes everything (warned at boot).
|
|
488
|
-
* - `token`: a request must carry the token (`Authorization: Bearer`, `x-durable-token`, or
|
|
489
|
-
* `?token=` on GET/HEAD).
|
|
490
|
-
* - `unconfigured`: nothing was configured outside production; the gate passes everything
|
|
491
|
-
* (warned at boot, because production will deny).
|
|
492
|
-
* - `deny`: nothing was configured and `NODE_ENV=production`; every request is refused.
|
|
493
|
-
*/
|
|
494
|
-
type DashboardGateMode = 'delegated' | 'open' | 'token' | 'unconfigured' | 'deny';
|
|
495
|
-
interface DashboardGate {
|
|
496
|
-
mode: DashboardGateMode;
|
|
497
|
-
/** Set only when `mode === 'token'`. */
|
|
498
|
-
token?: string;
|
|
499
|
-
}
|
|
500
|
-
/**
|
|
501
|
-
* Decide the gate mode. Precedence: `guards`/`dashboardAuth` (they are the gate) > a token > the
|
|
502
|
-
* `allowUnauthenticated` opt-out > the environment default. A token beats `allowUnauthenticated`
|
|
503
|
-
* because a configured secret is never silently ignored.
|
|
504
|
-
*/
|
|
505
|
-
declare function resolveDashboardGate(options: DashboardGateOptions, auth: ResolvedDashboardAuth | null): DashboardGate;
|
|
506
|
-
/** Log the one boot line the resolved gate calls for (nothing for a properly configured console). */
|
|
507
|
-
declare function reportDashboardGate(gate: DashboardGate, options: DashboardGateOptions, logger?: Pick<Logger, 'warn' | 'error'>): void;
|
|
508
|
-
/** The `DASHBOARD_GATE` provider: resolves once, after `DASHBOARD_AUTH`, and logs once. */
|
|
509
|
-
declare function dashboardGateProvider(options: DashboardGateOptions): Provider;
|
|
510
338
|
/**
|
|
511
|
-
*
|
|
512
|
-
* `
|
|
513
|
-
*
|
|
339
|
+
* The JSON API controller `DurableDashboardModule.forRoot(...)` mounts at `apiBasePath`. The module
|
|
340
|
+
* puts the `@nestjs/authorization` open mark on THIS class (and `decorators` on a per-registration
|
|
341
|
+
* subclass of it), never on {@link DurableApiControllerBase}, so a host's own subclass of the base
|
|
342
|
+
* inherits none of them.
|
|
514
343
|
*/
|
|
515
|
-
declare
|
|
516
|
-
/**
|
|
517
|
-
* The fail-closed default gate, stamped on `DurableUiController` and `DurableApiController` (after
|
|
518
|
-
* the built-in session guard, before host `guards`) whenever the module can't prove at definition
|
|
519
|
-
* time that something else protects the console. It never gates `DurableAuthController`, so the
|
|
520
|
-
* login/session/logout routes stay reachable. See {@link DashboardGateMode} for what each mode does.
|
|
521
|
-
*/
|
|
522
|
-
declare class DurableDashboardGateGuard implements CanActivate {
|
|
523
|
-
private readonly gate;
|
|
524
|
-
constructor(gate: DashboardGate);
|
|
525
|
-
canActivate(context: ExecutionContext): boolean;
|
|
344
|
+
declare class DurableApiController extends DurableApiControllerBase {
|
|
526
345
|
}
|
|
527
346
|
|
|
528
347
|
interface DurableDashboardOptions {
|
|
@@ -538,150 +357,76 @@ interface DurableDashboardOptions {
|
|
|
538
357
|
*/
|
|
539
358
|
apiBasePath?: string;
|
|
540
359
|
/**
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* call stamped, same as re-applying `@UseGuards` by hand. A non-empty `guards` counts as the
|
|
545
|
-
* console's authentication, so the fail-closed default gate steps aside (see
|
|
546
|
-
* {@link allowUnauthenticated}). If the host fronts the routes another way (a global guard,
|
|
547
|
-
* reverse-proxy auth), say so with `allowUnauthenticated: true` instead.
|
|
548
|
-
*
|
|
549
|
-
* A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the
|
|
550
|
-
* dashboard module has no application context of its own to pull them from otherwise.
|
|
551
|
-
*
|
|
552
|
-
* Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch
|
|
553
|
-
* from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like
|
|
554
|
-
* any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page
|
|
555
|
-
* navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so
|
|
556
|
-
* there is no custom header to read: only whatever ambient credential the browser sends by default
|
|
557
|
-
* (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header
|
|
558
|
-
* will 403 the page shell while the API calls it makes right after would have passed — put session
|
|
559
|
-
* auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the
|
|
560
|
-
* shell itself gated.
|
|
561
|
-
*/
|
|
562
|
-
guards?: Type<CanActivate>[];
|
|
563
|
-
/**
|
|
564
|
-
* Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class
|
|
565
|
-
* passed to {@link guards} (or any other provider the controllers need reachable). Typically the
|
|
566
|
-
* host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.
|
|
360
|
+
* Extra `imports` merged into the dashboard's dynamic module: the DI resolution path for a guard
|
|
361
|
+
* passed through {@link decorators} (`UseGuards(...)`), whose dependencies resolve from the module
|
|
362
|
+
* that hosts the controller. Typically the host's own auth module, unless it is global.
|
|
567
363
|
*/
|
|
568
364
|
imports?: DynamicModule['imports'];
|
|
569
365
|
/**
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* whatever) — the host frontend, carrying that auth, POSTs to `<basePath>/session`, your
|
|
575
|
-
* `session` hook validates the raw request and returns the session user, and this library mints
|
|
576
|
-
* its cookie from that. No credential this library understands ever exists; you keep your own
|
|
577
|
-
* identity provider as the source of truth. A page-level request with no valid session (and only
|
|
578
|
-
* Mode A configured) gets a small server-rendered instruction page telling the visitor to sign in
|
|
579
|
-
* through your host app, since there is no login page to redirect to.
|
|
580
|
-
* - **Mode B (`login`, standalone fallback)**: no host frontend/IdP to lean on — the durable
|
|
581
|
-
* dashboard serves its own small, dependency-free, server-rendered login page (`GET
|
|
582
|
-
* <basePath>/login`, handled by `DurableAuthController`) and your `login` hook validates
|
|
583
|
-
* submitted username/password. A missing/invalid/expired session redirects a page-level request
|
|
584
|
-
* to that login page with `?returnTo=<original url>`.
|
|
585
|
-
*
|
|
586
|
-
* Either mode can be used alone, or both together (e.g. Mode A for normal use, Mode B as a
|
|
587
|
-
* break-glass fallback) — at least one is required, or `forRoot`/`forRootAsync` throws at boot
|
|
588
|
-
* (an un-mintable gate is a boot error, not a silently-open or silently-stuck console). An API
|
|
589
|
-
* request (`apiBasePath`, fetched by the SPA's own JS) always gets a plain `401` on a missing
|
|
590
|
-
* session, regardless of mode — the caller reads the status code, not HTML.
|
|
366
|
+
* Class decorators applied to BOTH controllers this module mounts (the SPA and its JSON API), the
|
|
367
|
+
* way `@nestjs/mcp`'s own `decorators` option works. Use it to hand the console to the official NestJS tooling, e.g.
|
|
368
|
+
* `[UseGuards(AuthenticationGuard), Can(ConsolePolicy, 'view')]` with `@nestjs/authentication`
|
|
369
|
+
* and `@nestjs/authorization`.
|
|
591
370
|
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
* operator loses access instead of riding a self-renewing cookie indefinitely. It only runs on
|
|
596
|
-
* that renewal path, not on every request, so revocation has **latency, not immediacy**: an
|
|
597
|
-
* operator revoked right after a renewal keeps their existing cookie — and therefore console
|
|
598
|
-
* access — for up to `ttl/2` (4 hours at the default 8h TTL) before the next renewal check catches
|
|
599
|
-
* it. Do not rely on `revalidate` for a same-second cutoff; front the mount with your own guard
|
|
600
|
-
* (see {@link guards}) if you need that.
|
|
371
|
+
* Each registration gets fresh subclasses of the controllers, so decorators never leak into
|
|
372
|
+
* another registration or onto the exported classes. A guard named in `UseGuards(...)` resolves
|
|
373
|
+
* its dependencies from {@link imports} (or a global module).
|
|
601
374
|
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
375
|
+
* A non-empty `decorators` is how a mounted console states its protection. With
|
|
376
|
+
* `NODE_ENV=production`, a mounted console with no (or an empty) `decorators` fails the boot; see
|
|
377
|
+
* {@link DurableDashboardModule}. The module cannot tell a `Can(...)` from a decorator that
|
|
378
|
+
* protects nothing, so passing one is your statement that the console is protected.
|
|
605
379
|
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
* counts as the console's authentication (the fail-closed default gate steps aside). Omit it and
|
|
610
|
-
* configure `guards`, a {@link token}, or {@link allowUnauthenticated} instead.
|
|
611
|
-
*
|
|
612
|
-
* See {@link DashboardAuthOptions} for the `secret`/`ttl`/`session`/`login`/`revalidate` shape,
|
|
613
|
-
* and `forRootAsync` / {@link DurableDashboardAsyncOptions.useDashboardAuth} when a hook needs DI
|
|
614
|
-
* (e.g. an `EntityManager` to look up an admin user).
|
|
380
|
+
* Default: none. Outside production the console is then served open, and the controllers are
|
|
381
|
+
* marked `Can.Anyone()` for `@nestjs/authorization`, whose global guard otherwise refuses every
|
|
382
|
+
* route that declares no `@Can()`. A `@Can()` passed here is still enforced.
|
|
615
383
|
*/
|
|
616
|
-
|
|
384
|
+
decorators?: ClassDecorator[];
|
|
617
385
|
/**
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
386
|
+
* `false` mounts NO controllers: the SPA and its JSON API are not registered,
|
|
387
|
+
* while every provider still is (the run read/action service, the run filter, the SPA's base
|
|
388
|
+
* paths). Serve the console from controllers of your own instead, protected with ordinary Nest
|
|
389
|
+
* decorators, by extending {@link DurableUiControllerBase} and {@link DurableApiControllerBase}:
|
|
621
390
|
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
391
|
+
* ```ts
|
|
392
|
+
* @Controller('ops/durable') @UseGuards(AuthenticationGuard) @Can(ConsolePolicy, 'view')
|
|
393
|
+
* class ConsoleUi extends DurableUiControllerBase {}
|
|
394
|
+
* @Controller('ops/durable/api') @UseGuards(AuthenticationGuard) @Can(ConsolePolicy, 'view')
|
|
395
|
+
* class ConsoleApi extends DurableApiControllerBase {}
|
|
396
|
+
* // in the module that declares them:
|
|
397
|
+
* imports: [DurableDashboardModule.forRoot({ routes: false, basePath: '/ops/durable', apiBasePath: '/ops/durable/api' })]
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* `basePath`/`apiBasePath` still matter here: they are where you mounted the two controllers, and
|
|
401
|
+
* the SPA is told to load its assets from `basePath` and fetch from `apiBasePath`.
|
|
402
|
+
*
|
|
403
|
+
* The production rule does not apply: this module mounts nothing, and the routes are yours,
|
|
404
|
+
* protected by what you put on them like any other route you write. The `*Base` classes carry no
|
|
405
|
+
* guards and no `Can.Anyone()` mark, so under `@nestjs/authorization`'s global guard a subclass
|
|
406
|
+
* without a `@Can()` is denied. `decorators` throws when combined with `routes: false` rather than
|
|
407
|
+
* being silently ignored.
|
|
633
408
|
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
409
|
+
* Default `true`. Named `routes` rather than `path: false` because `basePath`/`apiBasePath` are
|
|
410
|
+
* strings that keep their meaning (where the console lives) whether or not this module mounts it.
|
|
636
411
|
*/
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
|
|
641
|
-
* router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`
|
|
642
|
-
* already has), but `dashboardAuth` is built by an injected factory, so its `session`/`login` hooks
|
|
643
|
-
* can reach your DB/services — e.g. Mode A's `session` hook calling out to the same auth service
|
|
644
|
-
* your host app already uses to validate the raw request. Returning `undefined` from
|
|
645
|
-
* `useDashboardAuth` is exactly like omitting `dashboardAuth` from `forRoot`: the console then
|
|
646
|
-
* falls back to `guards` / the token / `allowUnauthenticated`, and is DENIED in production when
|
|
647
|
-
* none of those is configured.
|
|
648
|
-
*/
|
|
649
|
-
interface DurableDashboardAsyncOptions {
|
|
650
|
-
basePath?: string;
|
|
651
|
-
apiBasePath?: string;
|
|
652
|
-
guards?: Type<CanActivate>[];
|
|
653
|
-
imports?: DynamicModule['imports'];
|
|
654
|
-
/** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */
|
|
655
|
-
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
656
|
-
/** Build the `dashboardAuth` config from injected deps (or `undefined` for no session auth). */
|
|
657
|
-
useDashboardAuth: (...deps: any[]) => DashboardAuthOptions | undefined | Promise<DashboardAuthOptions | undefined>;
|
|
658
|
-
/** See {@link DurableDashboardOptions.allowUnauthenticated}. */
|
|
659
|
-
allowUnauthenticated?: boolean;
|
|
660
|
-
/** See {@link DurableDashboardOptions.token}. */
|
|
661
|
-
token?: string;
|
|
412
|
+
routes?: boolean;
|
|
662
413
|
}
|
|
414
|
+
/** DI token of the boot-time check that refuses an unprotected console in production. */
|
|
415
|
+
declare const DASHBOARD_PROTECTION_CHECK: unique symbol;
|
|
416
|
+
/** The boot error for a mounted console with no `decorators` under `NODE_ENV=production`. */
|
|
417
|
+
declare const UNPROTECTED_CONSOLE_IN_PRODUCTION: string;
|
|
663
418
|
/**
|
|
664
|
-
* Holds the JSON API controller + its read service, mounted on its own path by `forRoot`.
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
* dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with "Nest can't
|
|
668
|
-
* resolve dependencies ... in the DurableApiModule context" even when the host passed the right
|
|
669
|
-
* `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).
|
|
419
|
+
* Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Its own
|
|
420
|
+
* dynamic module because enhancers (a guard from `decorators`) DI-instantiate from their
|
|
421
|
+
* controller's host module, so this module, not the outer wrapper, must carry the host's `imports`.
|
|
670
422
|
*/
|
|
671
423
|
declare class DurableApiModule {
|
|
672
424
|
static register(options: {
|
|
673
425
|
imports?: DynamicModule['imports'];
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
* (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's
|
|
679
|
-
* resolved at runtime and is a no-op if it ends up unconfigured). */
|
|
680
|
-
authMayBeConfigured: boolean;
|
|
681
|
-
/** What the fail-closed gate needs; resolved into `DASHBOARD_GATE` at boot (logged once). */
|
|
682
|
-
gate: DashboardGateOptions;
|
|
683
|
-
/** Whether `DurableDashboardGateGuard` is stamped (false when guards/dashboardAuth are static). */
|
|
684
|
-
stampGate: boolean;
|
|
426
|
+
/** `false`: register the providers only, no controller (see `routes`). */
|
|
427
|
+
routes?: boolean;
|
|
428
|
+
/** See {@link DurableDashboardOptions.decorators}. */
|
|
429
|
+
decorators?: ClassDecorator[];
|
|
685
430
|
}): DynamicModule;
|
|
686
431
|
}
|
|
687
432
|
/**
|
|
@@ -691,66 +436,28 @@ declare class DurableApiModule {
|
|
|
691
436
|
* so it resolves the engine and store. Front the routes with the first-class `guards` option
|
|
692
437
|
* (plus `imports` for the guards' own dependencies) — see {@link DurableDashboardOptions.guards} —
|
|
693
438
|
* or with the built-in `dashboardAuth` cookie login, or both.
|
|
694
|
-
* With none of those (and no `token` / `allowUnauthenticated`), the console fails
|
|
695
|
-
* `NODE_ENV=production` — see {@link DurableDashboardOptions.allowUnauthenticated}.
|
|
439
|
+
* With none of those (and no `decorators` / `token` / `allowUnauthenticated`), the console fails
|
|
440
|
+
* closed when `NODE_ENV=production` — see {@link DurableDashboardOptions.allowUnauthenticated}.
|
|
441
|
+
* `routes: false` mounts nothing and leaves serving the console to the host's own controllers — see
|
|
442
|
+
* {@link DurableDashboardOptions.routes}.
|
|
696
443
|
*/
|
|
697
|
-
declare class DurableDashboardModule {
|
|
698
|
-
static forRoot(options?: DurableDashboardOptions): DynamicModule;
|
|
699
|
-
static forRootAsync(options: DurableDashboardAsyncOptions): DynamicModule;
|
|
700
|
-
/** Shared wiring: static routing + the API module, with `dashboardAuth` supplied by `authProvider`. */
|
|
701
|
-
private static build;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
444
|
/**
|
|
705
|
-
*
|
|
706
|
-
* `
|
|
707
|
-
*
|
|
708
|
-
* when Mode B (`login`) is configured, gets a `302` to the built-in login page carrying
|
|
709
|
-
* `?returnTo=<original url>`, via `DashboardLoginRedirectException` (see that file for why a guard
|
|
710
|
-
* can't just call `response.redirect()` and return `false`). When only Mode A (`session`) is
|
|
711
|
-
* configured there is no login page to redirect to — the host mints the session itself — so it
|
|
712
|
-
* instead throws `DashboardSessionRequiredException`, rendering a small instruction page.
|
|
445
|
+
* Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`
|
|
446
|
+
* (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` alongside
|
|
447
|
+
* `DurableModule` (global), so it resolves the engine and store.
|
|
713
448
|
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
private readonly basePath;
|
|
721
|
-
constructor(auth: ResolvedDashboardAuth | null, basePath: string);
|
|
722
|
-
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
723
|
-
/**
|
|
724
|
-
* A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent
|
|
725
|
-
* one — by design, there is nothing left to distinguish "never had a session" from "had one,
|
|
726
|
-
* then lost it" once the cookie is cleared, so both get the same Mode-aware treatment.
|
|
727
|
-
*
|
|
728
|
-
* Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
|
|
729
|
-
* return narrows the field within that method only — TS doesn't carry it across a method call
|
|
730
|
-
* — so callers pass the already-narrowed value instead of this method re-deriving (or
|
|
731
|
-
* asserting away) the nullability.
|
|
732
|
-
*/
|
|
733
|
-
private denyUnauthenticated;
|
|
734
|
-
}
|
|
735
|
-
/**
|
|
736
|
-
* Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session
|
|
737
|
-
* cookie. A no-op (always `true`) when `dashboardAuth` was not configured. Missing/invalid/expired
|
|
738
|
-
* session => a plain `401` (the API is called via `fetch`, never a browser navigation, so a
|
|
739
|
-
* redirect would just fail the request anyway — the caller reads the status code, not HTML).
|
|
449
|
+
* Protect the console with the first-party NestJS tools (`@nestjs/authentication`,
|
|
450
|
+
* `@nestjs/authorization`), one of two ways:
|
|
451
|
+
*
|
|
452
|
+
* 1. `decorators` on the mounted controllers (see {@link DurableDashboardOptions.decorators});
|
|
453
|
+
* 2. `routes: false`, and mount it yourself by extending `DurableUiControllerBase` /
|
|
454
|
+
* `DurableApiControllerBase` (see {@link DurableDashboardOptions.routes}).
|
|
740
455
|
*
|
|
741
|
-
*
|
|
456
|
+
* With `NODE_ENV=production`, a mounted console with no `decorators` fails the boot with
|
|
457
|
+
* {@link UNPROTECTED_CONSOLE_IN_PRODUCTION}. Outside production it is served open, with a warning.
|
|
742
458
|
*/
|
|
743
|
-
declare class
|
|
744
|
-
|
|
745
|
-
constructor(auth: ResolvedDashboardAuth | null);
|
|
746
|
-
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
747
|
-
/**
|
|
748
|
-
* A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s
|
|
749
|
-
* dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console
|
|
750
|
-
* SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session
|
|
751
|
-
* 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.
|
|
752
|
-
*/
|
|
753
|
-
private unauthorized;
|
|
459
|
+
declare class DurableDashboardModule {
|
|
460
|
+
static forRoot(options?: DurableDashboardOptions): DynamicModule;
|
|
754
461
|
}
|
|
755
462
|
|
|
756
463
|
/** DI token carrying the UI mount base (e.g. `/durable`). */
|
|
@@ -759,18 +466,16 @@ declare const DASHBOARD_BASE_PATH: unique symbol;
|
|
|
759
466
|
declare const DASHBOARD_API_PATH: unique symbol;
|
|
760
467
|
/**
|
|
761
468
|
* Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).
|
|
762
|
-
* The
|
|
763
|
-
*
|
|
469
|
+
* The controller routes are relative: the mounted {@link DurableUiController} gets its path from
|
|
470
|
+
* `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`).
|
|
764
471
|
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
* based on which mode is configured (Mode B => redirect to the login page; Mode A only => render
|
|
771
|
-
* the session-required instruction page in place, since there is no login page to redirect to).
|
|
472
|
+
* This base is what a host EXTENDS to serve the console from a controller of its own (with
|
|
473
|
+
* `forRoot({ routes: false, basePath })`): `@Controller('<basePath>') class MyConsole extends
|
|
474
|
+
* DurableUiControllerBase {}`, protected with the host's own decorators. It carries no guards and
|
|
475
|
+
* no `@nestjs/authorization` mark, so a subclass is protected only by what the host puts on it (and,
|
|
476
|
+
* under `AuthorizationModule`'s global guard, is denied until it declares a `@Can()`).
|
|
772
477
|
*/
|
|
773
|
-
declare class
|
|
478
|
+
declare class DurableUiControllerBase {
|
|
774
479
|
private readonly basePath;
|
|
775
480
|
private readonly apiBasePath;
|
|
776
481
|
private readonly dir;
|
|
@@ -778,5 +483,13 @@ declare class DurableUiController {
|
|
|
778
483
|
index(): string;
|
|
779
484
|
asset(file: string): StreamableFile;
|
|
780
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* The SPA controller `DurableDashboardModule.forRoot(...)` mounts at `basePath`. The module puts
|
|
488
|
+
* the `@nestjs/authorization` open mark on THIS class (and `decorators` on a per-registration
|
|
489
|
+
* subclass of it), never on {@link DurableUiControllerBase}, so a host's own subclass of the base
|
|
490
|
+
* inherits none of them.
|
|
491
|
+
*/
|
|
492
|
+
declare class DurableUiController extends DurableUiControllerBase {
|
|
493
|
+
}
|
|
781
494
|
|
|
782
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH,
|
|
495
|
+
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DASHBOARD_PROTECTION_CHECK, DashboardService, DurableApiController, DurableApiControllerBase, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiControllerBase, UNPROTECTED_CONSOLE_IN_PRODUCTION, type WorkflowLifecyclePage };
|