@dudousxd/nestjs-durable-dashboard 0.30.0 → 0.32.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 +825 -42
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +205 -5
- package/dist/server/index.d.ts +205 -5
- package/dist/server/index.js +807 -27
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/server/index.d.ts
CHANGED
|
@@ -3,7 +3,42 @@ import { RunGateway, StateStore, WorkflowEngine, RunQuery, RunListItem, DurableT
|
|
|
3
3
|
export { RunDetail } from '@dudousxd/nestjs-durable-core';
|
|
4
4
|
import * as rxjs from 'rxjs';
|
|
5
5
|
import { Observable } from 'rxjs';
|
|
6
|
-
import { DynamicModule, StreamableFile } from '@nestjs/common';
|
|
6
|
+
import { DynamicModule, Type, CanActivate, Provider, InjectionToken, OptionalFactoryDependency, ExecutionContext, StreamableFile } from '@nestjs/common';
|
|
7
|
+
|
|
8
|
+
/** The session user the host `login` hook returns to mint a cookie. */
|
|
9
|
+
interface DashboardSessionUser {
|
|
10
|
+
id: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
roles?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Host hook validating submitted credentials from the built-in login page. */
|
|
16
|
+
type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
|
|
17
|
+
/**
|
|
18
|
+
* Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates
|
|
19
|
+
* BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON
|
|
20
|
+
* API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s
|
|
21
|
+
* `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Unlike
|
|
22
|
+
* Telescope's dashboard — a client-rendered SPA that can grow its own login screen — the durable
|
|
23
|
+
* dashboard's bundled React app stays untouched: the login screen here is a small, dependency-free
|
|
24
|
+
* server-rendered HTML page (`GET <basePath>/login`), so gating the SPA shell itself with a real
|
|
25
|
+
* redirect doesn't require rebuilding or extending the Vite bundle.
|
|
26
|
+
*/
|
|
27
|
+
interface DashboardAuthOptions {
|
|
28
|
+
/** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */
|
|
29
|
+
secret: string;
|
|
30
|
+
/** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */
|
|
31
|
+
ttl?: string;
|
|
32
|
+
/** Validates submitted username/password; return the session user, or `null` to deny. Thrown
|
|
33
|
+
* errors are treated as a denial (logged once, never surfaced to the client). */
|
|
34
|
+
login: LoginHook;
|
|
35
|
+
}
|
|
36
|
+
/** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */
|
|
37
|
+
interface ResolvedDashboardAuth {
|
|
38
|
+
secret: string;
|
|
39
|
+
ttlMs: number;
|
|
40
|
+
login: LoginHook;
|
|
41
|
+
}
|
|
7
42
|
|
|
8
43
|
/**
|
|
9
44
|
* Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/
|
|
@@ -142,6 +177,41 @@ declare class DurableApiController {
|
|
|
142
177
|
update(id: string, name: string, body: unknown): Promise<_dudousxd_nestjs_durable_core.UpdateResult>;
|
|
143
178
|
}
|
|
144
179
|
|
|
180
|
+
interface LoginBody {
|
|
181
|
+
username?: unknown;
|
|
182
|
+
password?: unknown;
|
|
183
|
+
returnTo?: unknown;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Mints/clears the `dashboardAuth` session cookie and serves the built-in login page. Mounted
|
|
187
|
+
* under `basePath` alongside `DurableUiController` (see `DurableDashboardModule.forRoot`), but on
|
|
188
|
+
* a SEPARATE controller so it is never itself behind `DurableUiSessionGuard` — these endpoints
|
|
189
|
+
* CREATE the session that guard checks for. `@Optional()` on both injections: this controller is
|
|
190
|
+
* only registered when `dashboardAuth` might be configured (a static `forRoot({ dashboardAuth })`
|
|
191
|
+
* or any `forRootAsync`), but the resolved value can still be `null` at runtime for the async case
|
|
192
|
+
* — every handler below re-checks it and 404s rather than assuming it is set.
|
|
193
|
+
*/
|
|
194
|
+
declare class DurableAuthController {
|
|
195
|
+
private readonly auth;
|
|
196
|
+
private readonly basePath;
|
|
197
|
+
private readonly logger;
|
|
198
|
+
/** Warn once so a throwing `login` hook doesn't spam logs on every failed attempt. */
|
|
199
|
+
private warnedOnHookThrow;
|
|
200
|
+
constructor(auth: ResolvedDashboardAuth | null, basePath: string);
|
|
201
|
+
loginPage(): string;
|
|
202
|
+
login(body: LoginBody, request: unknown, response: unknown): Promise<{
|
|
203
|
+
redirectTo: string;
|
|
204
|
+
}>;
|
|
205
|
+
logout(request: unknown, response: unknown): void;
|
|
206
|
+
private requireAuth;
|
|
207
|
+
/**
|
|
208
|
+
* Run the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and
|
|
209
|
+
* warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods
|
|
210
|
+
* the logs on repeated bad attempts.
|
|
211
|
+
*/
|
|
212
|
+
private runLoginHook;
|
|
213
|
+
}
|
|
214
|
+
|
|
145
215
|
interface DurableDashboardOptions {
|
|
146
216
|
/**
|
|
147
217
|
* Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an
|
|
@@ -154,17 +224,141 @@ interface DurableDashboardOptions {
|
|
|
154
224
|
* while the UI stays at `basePath`.
|
|
155
225
|
*/
|
|
156
226
|
apiBasePath?: string;
|
|
227
|
+
/**
|
|
228
|
+
* Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at
|
|
229
|
+
* `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key
|
|
230
|
+
* — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior
|
|
231
|
+
* call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the
|
|
232
|
+
* host fronts them another way, e.g. a global guard or reverse-proxy auth).
|
|
233
|
+
*
|
|
234
|
+
* A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the
|
|
235
|
+
* dashboard module has no application context of its own to pull them from otherwise.
|
|
236
|
+
*
|
|
237
|
+
* Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch
|
|
238
|
+
* from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like
|
|
239
|
+
* any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page
|
|
240
|
+
* navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so
|
|
241
|
+
* there is no custom header to read: only whatever ambient credential the browser sends by default
|
|
242
|
+
* (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header
|
|
243
|
+
* will 403 the page shell while the API calls it makes right after would have passed — put session
|
|
244
|
+
* auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the
|
|
245
|
+
* shell itself gated.
|
|
246
|
+
*/
|
|
247
|
+
guards?: Type<CanActivate>[];
|
|
248
|
+
/**
|
|
249
|
+
* Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class
|
|
250
|
+
* passed to {@link guards} (or any other provider the controllers need reachable). Typically the
|
|
251
|
+
* host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.
|
|
252
|
+
*/
|
|
253
|
+
imports?: DynamicModule['imports'];
|
|
254
|
+
/**
|
|
255
|
+
* Gate the console (the SPA at `basePath` AND its JSON API at `apiBasePath`) behind a built-in,
|
|
256
|
+
* cookie-session login — no infra, no changes to the bundled React SPA. Unlike
|
|
257
|
+
* `@dudousxd/nestjs-telescope`'s `dashboardAuth` (a client-rendered login screen inside its SPA),
|
|
258
|
+
* the durable dashboard's login page is small, dependency-free, server-rendered HTML served
|
|
259
|
+
* directly by `DurableAuthController` at `GET <basePath>/login` — the SPA bundle itself never
|
|
260
|
+
* changes. A missing/invalid/expired session redirects a page-level request (a full-page
|
|
261
|
+
* navigation to `basePath`) to that login page with `?returnTo=<original url>`; an API request
|
|
262
|
+
* (`apiBasePath`, fetched by the SPA's own JS) gets a plain `401` instead.
|
|
263
|
+
*
|
|
264
|
+
* Auth here is mount-level and role-agnostic: this option (and the guards/controller it wires up)
|
|
265
|
+
* has no notion of control-plane vs tenant role (see the durable dashboard's topology note in
|
|
266
|
+
* `dashboard.service.ts`) — nothing role-specific changes based on who logs in.
|
|
267
|
+
*
|
|
268
|
+
* Composes with {@link guards}: when both are set, a request must pass the built-in session check
|
|
269
|
+
* AND every guard in `guards` — the SAME logical-AND semantics `@UseGuards` already gives multiple
|
|
270
|
+
* guards, since the built-in guard is simply prepended to whatever `guards` you pass. Omit to leave
|
|
271
|
+
* the console open (today's behavior, unchanged byte-for-byte) — front it with `guards`, a global
|
|
272
|
+
* guard, or a reverse proxy instead.
|
|
273
|
+
*
|
|
274
|
+
* See {@link DashboardAuthOptions} for the `secret`/`ttl`/`login` shape, and `forRootAsync` /
|
|
275
|
+
* {@link DurableDashboardAsyncOptions.useDashboardAuth} when the `login` hook needs DI (e.g. an
|
|
276
|
+
* `EntityManager` to look up an admin user).
|
|
277
|
+
*/
|
|
278
|
+
dashboardAuth?: DashboardAuthOptions;
|
|
157
279
|
}
|
|
158
|
-
/**
|
|
280
|
+
/**
|
|
281
|
+
* Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the
|
|
282
|
+
* router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`
|
|
283
|
+
* already has), but `dashboardAuth` is built by an injected factory, so its `login` hook can reach
|
|
284
|
+
* your DB/services. Returning `undefined` from `useDashboardAuth` leaves the console open, exactly
|
|
285
|
+
* like omitting `dashboardAuth` from `forRoot`.
|
|
286
|
+
*/
|
|
287
|
+
interface DurableDashboardAsyncOptions {
|
|
288
|
+
basePath?: string;
|
|
289
|
+
apiBasePath?: string;
|
|
290
|
+
guards?: Type<CanActivate>[];
|
|
291
|
+
imports?: DynamicModule['imports'];
|
|
292
|
+
/** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */
|
|
293
|
+
inject?: Array<InjectionToken | OptionalFactoryDependency>;
|
|
294
|
+
/** Build the `dashboardAuth` config from injected deps (or `undefined` to leave the console open). */
|
|
295
|
+
useDashboardAuth: (...deps: any[]) => DashboardAuthOptions | undefined | Promise<DashboardAuthOptions | undefined>;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:
|
|
299
|
+
* guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper
|
|
300
|
+
* — must carry the guard classes as providers plus the host's `imports` that resolve their
|
|
301
|
+
* dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with "Nest can't
|
|
302
|
+
* resolve dependencies ... in the DurableApiModule context" even when the host passed the right
|
|
303
|
+
* `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).
|
|
304
|
+
*/
|
|
159
305
|
declare class DurableApiModule {
|
|
306
|
+
static register(options: {
|
|
307
|
+
imports?: DynamicModule['imports'];
|
|
308
|
+
guards?: Type<CanActivate>[];
|
|
309
|
+
/** Provider for `DASHBOARD_AUTH` — a `useValue` (`forRoot`) or a `useFactory` (`forRootAsync`). */
|
|
310
|
+
authProvider: Provider;
|
|
311
|
+
/** Whether the built-in `DurableApiSessionGuard` should be stamped on `DurableApiController`
|
|
312
|
+
* (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's
|
|
313
|
+
* resolved at runtime and is a no-op if it ends up unconfigured). */
|
|
314
|
+
authMayBeConfigured: boolean;
|
|
315
|
+
}): DynamicModule;
|
|
160
316
|
}
|
|
161
317
|
/**
|
|
162
318
|
* Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`
|
|
163
|
-
* (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)`
|
|
164
|
-
* `
|
|
319
|
+
* (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` (or `forRootAsync`
|
|
320
|
+
* when `dashboardAuth`'s `login` hook needs injected services) alongside `DurableModule` (global),
|
|
321
|
+
* so it resolves the engine and store. Front the routes with the first-class `guards` option
|
|
322
|
+
* (plus `imports` for the guards' own dependencies) — see {@link DurableDashboardOptions.guards} —
|
|
323
|
+
* or with the built-in `dashboardAuth` cookie login, or both.
|
|
165
324
|
*/
|
|
166
325
|
declare class DurableDashboardModule {
|
|
167
326
|
static forRoot(options?: DurableDashboardOptions): DynamicModule;
|
|
327
|
+
static forRootAsync(options: DurableDashboardAsyncOptions): DynamicModule;
|
|
328
|
+
/** Shared wiring: static routing + the API module, with `dashboardAuth` supplied by `authProvider`. */
|
|
329
|
+
private static build;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Gates `DurableUiController` (the SPA shell + assets, a full-page browser navigation) on a valid
|
|
334
|
+
* `dashboardAuth` session cookie. A no-op (always `true`) when `dashboardAuth` was not configured
|
|
335
|
+
* — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session =>
|
|
336
|
+
* a `302` to the built-in login page carrying `?returnTo=<original url>`, via
|
|
337
|
+
* `DashboardLoginRedirectException` (see that file for why a guard can't just call
|
|
338
|
+
* `response.redirect()` and return `false`).
|
|
339
|
+
*
|
|
340
|
+
* Auth is mount-level and role-agnostic: this guard has no notion of control-plane vs tenant
|
|
341
|
+
* (see the durable dashboard's topology note in `dashboard.service.ts`) — it only ever answers
|
|
342
|
+
* "is there a valid dashboard session", nothing role-specific changes here.
|
|
343
|
+
*/
|
|
344
|
+
declare class DurableUiSessionGuard implements CanActivate {
|
|
345
|
+
private readonly auth;
|
|
346
|
+
private readonly basePath;
|
|
347
|
+
constructor(auth: ResolvedDashboardAuth | null, basePath: string);
|
|
348
|
+
canActivate(context: ExecutionContext): boolean;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session
|
|
352
|
+
* cookie. A no-op (always `true`) when `dashboardAuth` was not configured. Missing/invalid/expired
|
|
353
|
+
* session => a plain `401` (the API is called via `fetch`, never a browser navigation, so a
|
|
354
|
+
* redirect would just fail the request anyway — the caller reads the status code, not HTML).
|
|
355
|
+
*
|
|
356
|
+
* Mount-level and role-agnostic, same as `DurableUiSessionGuard` above.
|
|
357
|
+
*/
|
|
358
|
+
declare class DurableApiSessionGuard implements CanActivate {
|
|
359
|
+
private readonly auth;
|
|
360
|
+
constructor(auth: ResolvedDashboardAuth | null);
|
|
361
|
+
canActivate(context: ExecutionContext): boolean;
|
|
168
362
|
}
|
|
169
363
|
|
|
170
364
|
/** DI token carrying the UI mount base (e.g. `/durable`). */
|
|
@@ -175,6 +369,12 @@ declare const DASHBOARD_API_PATH: unique symbol;
|
|
|
175
369
|
* Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).
|
|
176
370
|
* The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the
|
|
177
371
|
* controller routes are relative.
|
|
372
|
+
*
|
|
373
|
+
* `@UseFilters(DashboardLoginRedirectFilter)` is permanent (present regardless of whether
|
|
374
|
+
* `dashboardAuth` is configured): it only ever activates for a `DashboardLoginRedirectException`,
|
|
375
|
+
* which `DurableUiSessionGuard` only throws when it is ALSO stamped on this controller (see
|
|
376
|
+
* `DurableDashboardModule.forRoot`) — so leaving it decorated here is inert, not a behavior change,
|
|
377
|
+
* when `dashboardAuth` is absent.
|
|
178
378
|
*/
|
|
179
379
|
declare class DurableUiController {
|
|
180
380
|
private readonly basePath;
|
|
@@ -185,4 +385,4 @@ declare class DurableUiController {
|
|
|
185
385
|
asset(file: string): StreamableFile;
|
|
186
386
|
}
|
|
187
387
|
|
|
188
|
-
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, DashboardService, DurableApiController, DurableApiModule, DurableDashboardModule, type DurableDashboardOptions, DurableUiController };
|
|
388
|
+
export { DASHBOARD_API_PATH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type DashboardSessionUser, DurableApiController, DurableApiModule, DurableApiSessionGuard, DurableAuthController, type DurableDashboardAsyncOptions, DurableDashboardModule, type DurableDashboardOptions, DurableUiController, DurableUiSessionGuard };
|