@dudousxd/nestjs-agent-dashboard 0.10.1 → 0.12.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.
@@ -214,6 +214,44 @@ type LoginHook = (username: string, password: string) => Promise<DashboardSessio
214
214
  * receives the already-minted session.
215
215
  */
216
216
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
217
+ /** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */
218
+ interface UnauthenticatedPageContext {
219
+ /**
220
+ * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`
221
+ * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.
222
+ * Cast it to whatever your app actually runs on.
223
+ */
224
+ request: unknown;
225
+ /**
226
+ * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,
227
+ * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the
228
+ * library falls back to its own built-in page rather than leaving the request hanging.
229
+ */
230
+ response: unknown;
231
+ /** Where this console is mounted (e.g. `/ai-gateway`) — useful for a "back to the console" link. */
232
+ basePath: string;
233
+ }
234
+ /**
235
+ * Host-owned page for an unauthenticated navigation to the console.
236
+ *
237
+ * The built-in page cannot know who hosts the console, so it can only say "open this console from
238
+ * your application" in the abstract — it can't name the launcher, link to it, or look like the rest
239
+ * of the host's product. This hook hands the whole response to the host instead: render through
240
+ * your own template engine, your own Inertia page, whatever.
241
+ *
242
+ * Served at `<basePath>/auth/session-required`, which is where `DashboardAuthPageGuard` already
243
+ * redirects a denied Mode-A navigation today — this hook changes what that page CONTAINS, not the
244
+ * flow that reaches it.
245
+ *
246
+ * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login
247
+ * UI uses Mode A plus this hook, and posts to `<basePath>/auth/session` from its own page — the
248
+ * mint endpoint is the supported primitive for that.
249
+ *
250
+ * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook
251
+ * that throws, or that returns without writing, falls back to the built-in page — it cannot let
252
+ * anyone in.
253
+ */
254
+ type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;
217
255
  /** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
218
256
  type AuthMode = 'session' | 'login';
219
257
  /**
@@ -245,6 +283,9 @@ interface DashboardAuthOptions {
245
283
  /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
246
284
  * mint a session, only revoke one already minted by `session`/`login`. */
247
285
  revalidate?: RevalidateHook;
286
+ /** Renders the host's own page for an unauthenticated navigation, in place of the built-in
287
+ * "open this console from your application" card; see `UnauthenticatedPageHook`. */
288
+ unauthenticatedPage?: UnauthenticatedPageHook;
248
289
  }
249
290
  /** Resolved, validated `dashboardAuth` config used by the guards/controller. */
250
291
  interface ResolvedDashboardAuth {
@@ -254,6 +295,7 @@ interface ResolvedDashboardAuth {
254
295
  session?: SessionHook;
255
296
  login?: LoginHook;
256
297
  revalidate?: RevalidateHook;
298
+ unauthenticatedPage?: UnauthenticatedPageHook;
257
299
  }
258
300
  /**
259
301
  * Validate + resolve `dashboardAuth`. Returns `null` when unconfigured (behavior unchanged — the
@@ -306,7 +348,7 @@ declare class AgentDashboardAuthController {
306
348
  login(body: LoginFormBody, req: unknown, res: AuthPageResponse): Promise<string>;
307
349
  logout(req: unknown, res: AuthPageResponse): string;
308
350
  session(request: unknown, response: unknown): Promise<void>;
309
- sessionRequiredPage(): string;
351
+ sessionRequiredPage(req: AuthPageRequest, res: unknown): Promise<void>;
310
352
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
311
353
  private redirectToLoginError;
312
354
  private currentSession;
@@ -316,6 +358,11 @@ declare class AgentDashboardAuthController {
316
358
  * endpoint into a stack-trace leak nor floods the logs on repeated bad attempts.
317
359
  */
318
360
  private runHook;
361
+ /**
362
+ * Warn once per hook kind for the life of the process, not once per request: a broken hook fires
363
+ * on every denied navigation and would otherwise flood the logs.
364
+ */
365
+ private warnHookOnce;
319
366
  private requireAuth;
320
367
  }
321
368
 
@@ -589,4 +636,4 @@ declare const DASHBOARD_APPROVAL_ACTOR_REF: unique symbol;
589
636
  */
590
637
  declare const DASHBOARD_AUTH: unique symbol;
591
638
 
592
- export { AGENT_ACTOR_DIRECTORY, AGENT_ACTOR_RESOLVER, AGENT_APPROVAL_PORT, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, type AgentDashboardAsyncOptions, AgentDashboardAuthController, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, type AuthMode, DASHBOARD_API_PATH, DASHBOARD_APPROVAL_ACTOR_REF, DASHBOARD_AUTH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type LiveAgentEvent, type LoginHook, type ReliabilityOverview, type ResolvedDashboardAuth, type RevalidateHook, type SessionHook, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type WithActorLabel, agentDashboardMountPaths, parsePriceInput, resolveDashboardAuth };
639
+ export { AGENT_ACTOR_DIRECTORY, AGENT_ACTOR_RESOLVER, AGENT_APPROVAL_PORT, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, type AgentDashboardAsyncOptions, AgentDashboardAuthController, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, type AuthMode, DASHBOARD_API_PATH, DASHBOARD_APPROVAL_ACTOR_REF, DASHBOARD_AUTH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type LiveAgentEvent, type LoginHook, type ReliabilityOverview, type ResolvedDashboardAuth, type RevalidateHook, type SessionHook, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type UnauthenticatedPageContext, type UnauthenticatedPageHook, type WithActorLabel, agentDashboardMountPaths, parsePriceInput, resolveDashboardAuth };
@@ -214,6 +214,44 @@ type LoginHook = (username: string, password: string) => Promise<DashboardSessio
214
214
  * receives the already-minted session.
215
215
  */
216
216
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
217
+ /** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */
218
+ interface UnauthenticatedPageContext {
219
+ /**
220
+ * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`
221
+ * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.
222
+ * Cast it to whatever your app actually runs on.
223
+ */
224
+ request: unknown;
225
+ /**
226
+ * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,
227
+ * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the
228
+ * library falls back to its own built-in page rather than leaving the request hanging.
229
+ */
230
+ response: unknown;
231
+ /** Where this console is mounted (e.g. `/ai-gateway`) — useful for a "back to the console" link. */
232
+ basePath: string;
233
+ }
234
+ /**
235
+ * Host-owned page for an unauthenticated navigation to the console.
236
+ *
237
+ * The built-in page cannot know who hosts the console, so it can only say "open this console from
238
+ * your application" in the abstract — it can't name the launcher, link to it, or look like the rest
239
+ * of the host's product. This hook hands the whole response to the host instead: render through
240
+ * your own template engine, your own Inertia page, whatever.
241
+ *
242
+ * Served at `<basePath>/auth/session-required`, which is where `DashboardAuthPageGuard` already
243
+ * redirects a denied Mode-A navigation today — this hook changes what that page CONTAINS, not the
244
+ * flow that reaches it.
245
+ *
246
+ * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login
247
+ * UI uses Mode A plus this hook, and posts to `<basePath>/auth/session` from its own page — the
248
+ * mint endpoint is the supported primitive for that.
249
+ *
250
+ * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook
251
+ * that throws, or that returns without writing, falls back to the built-in page — it cannot let
252
+ * anyone in.
253
+ */
254
+ type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;
217
255
  /** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
218
256
  type AuthMode = 'session' | 'login';
219
257
  /**
@@ -245,6 +283,9 @@ interface DashboardAuthOptions {
245
283
  /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
246
284
  * mint a session, only revoke one already minted by `session`/`login`. */
247
285
  revalidate?: RevalidateHook;
286
+ /** Renders the host's own page for an unauthenticated navigation, in place of the built-in
287
+ * "open this console from your application" card; see `UnauthenticatedPageHook`. */
288
+ unauthenticatedPage?: UnauthenticatedPageHook;
248
289
  }
249
290
  /** Resolved, validated `dashboardAuth` config used by the guards/controller. */
250
291
  interface ResolvedDashboardAuth {
@@ -254,6 +295,7 @@ interface ResolvedDashboardAuth {
254
295
  session?: SessionHook;
255
296
  login?: LoginHook;
256
297
  revalidate?: RevalidateHook;
298
+ unauthenticatedPage?: UnauthenticatedPageHook;
257
299
  }
258
300
  /**
259
301
  * Validate + resolve `dashboardAuth`. Returns `null` when unconfigured (behavior unchanged — the
@@ -306,7 +348,7 @@ declare class AgentDashboardAuthController {
306
348
  login(body: LoginFormBody, req: unknown, res: AuthPageResponse): Promise<string>;
307
349
  logout(req: unknown, res: AuthPageResponse): string;
308
350
  session(request: unknown, response: unknown): Promise<void>;
309
- sessionRequiredPage(): string;
351
+ sessionRequiredPage(req: AuthPageRequest, res: unknown): Promise<void>;
310
352
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
311
353
  private redirectToLoginError;
312
354
  private currentSession;
@@ -316,6 +358,11 @@ declare class AgentDashboardAuthController {
316
358
  * endpoint into a stack-trace leak nor floods the logs on repeated bad attempts.
317
359
  */
318
360
  private runHook;
361
+ /**
362
+ * Warn once per hook kind for the life of the process, not once per request: a broken hook fires
363
+ * on every denied navigation and would otherwise flood the logs.
364
+ */
365
+ private warnHookOnce;
319
366
  private requireAuth;
320
367
  }
321
368
 
@@ -589,4 +636,4 @@ declare const DASHBOARD_APPROVAL_ACTOR_REF: unique symbol;
589
636
  */
590
637
  declare const DASHBOARD_AUTH: unique symbol;
591
638
 
592
- export { AGENT_ACTOR_DIRECTORY, AGENT_ACTOR_RESOLVER, AGENT_APPROVAL_PORT, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, type AgentDashboardAsyncOptions, AgentDashboardAuthController, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, type AuthMode, DASHBOARD_API_PATH, DASHBOARD_APPROVAL_ACTOR_REF, DASHBOARD_AUTH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type LiveAgentEvent, type LoginHook, type ReliabilityOverview, type ResolvedDashboardAuth, type RevalidateHook, type SessionHook, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type WithActorLabel, agentDashboardMountPaths, parsePriceInput, resolveDashboardAuth };
639
+ export { AGENT_ACTOR_DIRECTORY, AGENT_ACTOR_RESOLVER, AGENT_APPROVAL_PORT, AGENT_GOVERNANCE_QUERIES, AGENT_PRICING_STORE, type ActorSpendRowWithLabel, AgentApiController, AgentApiModule, type AgentDashboardAsyncOptions, AgentDashboardAuthController, AgentDashboardModule, type AgentDashboardMountPathsOptions, type AgentDashboardOptions, AgentUiController, type AuthMode, DASHBOARD_API_PATH, DASHBOARD_APPROVAL_ACTOR_REF, DASHBOARD_AUTH, DASHBOARD_BASE_PATH, type DashboardAuthOptions, DashboardService, type LiveAgentEvent, type LoginHook, type ReliabilityOverview, type ResolvedDashboardAuth, type RevalidateHook, type SessionHook, type SpendOverview, type ThreadActivityRowWithLabel, type ThreadSpendRowWithLabel, type UnauthenticatedPageContext, type UnauthenticatedPageHook, type WithActorLabel, agentDashboardMountPaths, parsePriceInput, resolveDashboardAuth };
@@ -147,6 +147,20 @@ function resolveRawResponse(response) {
147
147
  return null;
148
148
  }
149
149
  __name(resolveRawResponse, "resolveRawResponse");
150
+ function responseAlreadyWritten(response) {
151
+ const raw = resolveRawResponse(response);
152
+ return raw?.headersSent === true;
153
+ }
154
+ __name(responseAlreadyWritten, "responseAlreadyWritten");
155
+ function sendHtml(response, status, html) {
156
+ const raw = resolveRawResponse(response);
157
+ if (!raw) return;
158
+ raw.statusCode = status;
159
+ raw.setHeader("content-type", "text/html; charset=utf-8");
160
+ raw.setHeader("cache-control", "no-store, must-revalidate");
161
+ raw.end(html);
162
+ }
163
+ __name(sendHtml, "sendHtml");
150
164
  function sendRedirect(response, status, location) {
151
165
  const raw = resolveRawResponse(response);
152
166
  if (!raw) return;
@@ -1384,10 +1398,31 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1384
1398
  // Mode-A-only landing: the host mints the session, so there is nothing to submit here. Serves
1385
1399
  // as the redirect target `DashboardAuthPageGuard` bounces to instead of `/auth/login`, which
1386
1400
  // 404s under Mode A (see `loginPage` above).
1387
- sessionRequiredPage() {
1401
+ //
1402
+ // Non-passthrough `@Res()` (unlike its sibling routes) because `dashboardAuth.unauthenticatedPage`
1403
+ // hands the response to the HOST, which writes and ends it itself — there is no return value for
1404
+ // Nest to send, and letting Nest also send one would double-write. The `Content-Type` /
1405
+ // `Cache-Control` this route used to carry as decorators move into `sendHtml` for the built-in
1406
+ // path; the host owns its own headers on the hook path.
1407
+ async sessionRequiredPage(req, res) {
1388
1408
  const auth = this.requireAuth();
1389
1409
  if (auth.modes.includes("login")) throw new NotFoundException();
1390
- return renderSessionRequiredPage();
1410
+ const hook = auth.unauthenticatedPage;
1411
+ if (hook) {
1412
+ try {
1413
+ await hook({
1414
+ request: req,
1415
+ response: res,
1416
+ basePath: this.basePath
1417
+ });
1418
+ if (responseAlreadyWritten(res)) return;
1419
+ this.warnHookOnce("unauthenticatedPage", "dashboardAuth.unauthenticatedPage returned without writing a response; falling back to the built-in page. The hook must write AND end the response it is given.");
1420
+ } catch (error) {
1421
+ this.warnHookOnce("unauthenticatedPage", `dashboardAuth.unauthenticatedPage threw; falling back to the built-in page. ${String(error)}`);
1422
+ if (responseAlreadyWritten(res)) return;
1423
+ }
1424
+ }
1425
+ sendHtml(res, 200, renderSessionRequiredPage());
1391
1426
  }
1392
1427
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
1393
1428
  redirectToLoginError(res, returnTo) {
@@ -1412,13 +1447,19 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1412
1447
  try {
1413
1448
  return await run() ?? null;
1414
1449
  } catch (error) {
1415
- if (!this.warnedHooks.has(kind)) {
1416
- this.warnedHooks.add(kind);
1417
- this.logger.warn(`AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1418
- }
1450
+ this.warnHookOnce(kind, `AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1419
1451
  return null;
1420
1452
  }
1421
1453
  }
1454
+ /**
1455
+ * Warn once per hook kind for the life of the process, not once per request: a broken hook fires
1456
+ * on every denied navigation and would otherwise flood the logs.
1457
+ */
1458
+ warnHookOnce(kind, message) {
1459
+ if (this.warnedHooks.has(kind)) return;
1460
+ this.warnedHooks.add(kind);
1461
+ this.logger.warn(message);
1462
+ }
1422
1463
  requireAuth() {
1423
1464
  if (!this.auth) throw new NotFoundException();
1424
1465
  return this.auth;
@@ -1487,11 +1528,14 @@ _ts_decorate4([
1487
1528
  ], AgentDashboardAuthController.prototype, "session", null);
1488
1529
  _ts_decorate4([
1489
1530
  Get2("session-required"),
1490
- Header("Content-Type", "text/html; charset=utf-8"),
1491
- Header("Cache-Control", "no-store, must-revalidate"),
1531
+ _ts_param4(0, Req2()),
1532
+ _ts_param4(1, Res()),
1492
1533
  _ts_metadata4("design:type", Function),
1493
- _ts_metadata4("design:paramtypes", []),
1494
- _ts_metadata4("design:returntype", String)
1534
+ _ts_metadata4("design:paramtypes", [
1535
+ typeof AuthPageRequest === "undefined" ? Object : AuthPageRequest,
1536
+ Object
1537
+ ]),
1538
+ _ts_metadata4("design:returntype", Promise)
1495
1539
  ], AgentDashboardAuthController.prototype, "sessionRequiredPage", null);
1496
1540
  AgentDashboardAuthController = _ts_decorate4([
1497
1541
  Controller2("auth"),
@@ -1821,6 +1865,9 @@ function resolveDashboardAuth(options) {
1821
1865
  if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
1822
1866
  throw new Error("AgentDashboardModule dashboardAuth: `revalidate` must be a function.");
1823
1867
  }
1868
+ if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
1869
+ throw new Error("AgentDashboardModule dashboardAuth: `unauthenticatedPage` must be a function.");
1870
+ }
1824
1871
  if (modes.length === 0) {
1825
1872
  throw new Error("AgentDashboardModule dashboardAuth: at least one of `session` (the host mints the session from its own auth) or `login` (the built-in login screen) is required \u2014 otherwise the cookie can never be minted. Failing closed.");
1826
1873
  }
@@ -1836,6 +1883,9 @@ function resolveDashboardAuth(options) {
1836
1883
  } : {},
1837
1884
  ...options.revalidate !== void 0 ? {
1838
1885
  revalidate: options.revalidate
1886
+ } : {},
1887
+ ...options.unauthenticatedPage !== void 0 ? {
1888
+ unauthenticatedPage: options.unauthenticatedPage
1839
1889
  } : {}
1840
1890
  };
1841
1891
  }