@dudousxd/nestjs-agent-dashboard 0.9.1 → 0.10.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.
@@ -261,6 +261,39 @@ function clearSessionCookie(context) {
261
261
  appendSetCookie(context.response, cookie);
262
262
  }
263
263
  __name(clearSessionCookie, "clearSessionCookie");
264
+ async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
265
+ if (now - session.iat <= auth.ttlMs / 2) return true;
266
+ const user = {
267
+ id: session.sub,
268
+ ...session.name !== void 0 ? {
269
+ name: session.name
270
+ } : {},
271
+ roles: session.roles
272
+ };
273
+ if (auth.revalidate) {
274
+ let allowed;
275
+ try {
276
+ allowed = await auth.revalidate(user);
277
+ } catch {
278
+ allowed = false;
279
+ }
280
+ if (!allowed) {
281
+ clearSessionCookie({
282
+ request,
283
+ response
284
+ });
285
+ return false;
286
+ }
287
+ }
288
+ issueSessionCookie(user, {
289
+ auth,
290
+ request,
291
+ response,
292
+ now
293
+ });
294
+ return true;
295
+ }
296
+ __name(maybeRenewSession, "maybeRenewSession");
264
297
 
265
298
  // src/server/auth/dashboard-auth.guard.ts
266
299
  function _ts_decorate(decorators, target, key, desc) {
@@ -288,14 +321,16 @@ var DashboardAuthGuard = class {
288
321
  constructor(auth) {
289
322
  this.auth = auth;
290
323
  }
291
- canActivate(context) {
324
+ async canActivate(context) {
292
325
  if (!this.auth) return true;
293
326
  const http = context.switchToHttp();
294
327
  const request = http.getRequest();
295
328
  const session = this.verifyRequestSession(request);
296
329
  if (!session) throw new UnauthorizedException();
297
330
  attachSession(request, session);
298
- this.maybeRenew(http.getResponse(), request, session);
331
+ if (!await maybeRenewSession(this.auth, session, request, http.getResponse())) {
332
+ throw new UnauthorizedException();
333
+ }
299
334
  return true;
300
335
  }
301
336
  verifyRequestSession(request) {
@@ -307,28 +342,6 @@ var DashboardAuthGuard = class {
307
342
  secret: auth.secret
308
343
  });
309
344
  }
310
- /**
311
- * Sliding renewal: a valid cookie past 50% of its TTL is transparently re-issued on the
312
- * response, so an active user never gets logged out mid-session.
313
- */
314
- maybeRenew(response, request, session) {
315
- const auth = this.auth;
316
- if (!auth) return;
317
- const now = Date.now();
318
- if (now - session.iat <= auth.ttlMs / 2) return;
319
- issueSessionCookie({
320
- id: session.sub,
321
- ...session.name !== void 0 ? {
322
- name: session.name
323
- } : {},
324
- roles: session.roles
325
- }, {
326
- auth,
327
- request,
328
- response,
329
- now
330
- });
331
- }
332
345
  };
333
346
  DashboardAuthGuard = _ts_decorate([
334
347
  Injectable(),
@@ -1117,24 +1130,21 @@ AgentApiController = _ts_decorate3([
1117
1130
  ], AgentApiController);
1118
1131
 
1119
1132
  // src/server/agent-dashboard-auth.controller.ts
1120
- import { Body as Body2, Controller as Controller2, Get as Get2, Header, Inject as Inject4, Logger, NotFoundException, Optional as Optional4, Post as Post2, Query as Query2, Req as Req2, Res } from "@nestjs/common";
1133
+ import { Body as Body2, Controller as Controller2, Get as Get2, Header, HttpCode as HttpCode2, Inject as Inject4, Logger, NotFoundException, Optional as Optional4, Post as Post2, Query as Query2, Req as Req2, Res, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
1121
1134
 
1122
1135
  // src/server/auth/login-page.ts
1123
1136
  function escapeHtml(value) {
1124
1137
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1125
1138
  }
1126
1139
  __name(escapeHtml, "escapeHtml");
1127
- function renderLoginPage(options) {
1128
- const action = escapeHtml(options.actionUrl);
1129
- const returnTo = escapeHtml(options.returnTo);
1130
- const errorBanner = options.error === true ? '<p role="alert" class="error">Invalid username or password.</p>' : "";
1140
+ function pageShell(content, title) {
1131
1141
  return `<!doctype html>
1132
1142
  <html lang="en">
1133
1143
  <head>
1134
1144
  <meta charset="utf-8" />
1135
1145
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1136
1146
  <meta name="robots" content="noindex, nofollow" />
1137
- <title>Sign in \u2014 AI Gateway</title>
1147
+ <title>${title}</title>
1138
1148
  <style>
1139
1149
  :root { color-scheme: dark; }
1140
1150
  * { box-sizing: border-box; }
@@ -1165,6 +1175,13 @@ function renderLoginPage(options) {
1165
1175
  font-weight: 600;
1166
1176
  color: #34d399;
1167
1177
  }
1178
+ h1 {
1179
+ margin: 0 0 12px;
1180
+ font-size: 16px;
1181
+ font-weight: 600;
1182
+ color: #f4f4f5;
1183
+ }
1184
+ p { margin: 0 0 16px; color: #a1a1aa; }
1168
1185
  form { display: flex; flex-direction: column; gap: 16px; }
1169
1186
  label { display: flex; flex-direction: column; gap: 6px; }
1170
1187
  .field-label {
@@ -1203,7 +1220,17 @@ function renderLoginPage(options) {
1203
1220
  </head>
1204
1221
  <body>
1205
1222
  <div class="card">
1206
- <p class="brand">AI Gateway</p>
1223
+ ${content}
1224
+ </div>
1225
+ </body>
1226
+ </html>`;
1227
+ }
1228
+ __name(pageShell, "pageShell");
1229
+ function renderLoginPage(options) {
1230
+ const action = escapeHtml(options.actionUrl);
1231
+ const returnTo = escapeHtml(options.returnTo);
1232
+ const errorBanner = options.error === true ? '<p role="alert" class="error">Invalid username or password.</p>' : "";
1233
+ return pageShell(`<p class="brand">AI Gateway</p>
1207
1234
  <form method="post" action="${action}">
1208
1235
  <input type="hidden" name="returnTo" value="${returnTo}" />
1209
1236
  <label>
@@ -1216,12 +1243,15 @@ function renderLoginPage(options) {
1216
1243
  </label>
1217
1244
  ${errorBanner}
1218
1245
  <button type="submit">Sign in</button>
1219
- </form>
1220
- </div>
1221
- </body>
1222
- </html>`;
1246
+ </form>`, "Sign in \u2014 AI Gateway");
1223
1247
  }
1224
1248
  __name(renderLoginPage, "renderLoginPage");
1249
+ function renderSessionRequiredPage() {
1250
+ return pageShell(`<p class="brand">AI Gateway</p>
1251
+ <h1>Open this console from your application</h1>
1252
+ <p>Your session is minted by the host app. Use its console launcher to sign in, then reload.</p>`, "Sign in \u2014 AI Gateway");
1253
+ }
1254
+ __name(renderSessionRequiredPage, "renderSessionRequiredPage");
1225
1255
 
1226
1256
  // src/server/auth/sanitize-return-to.ts
1227
1257
  function sanitizeReturnTo(basePath, returnTo) {
@@ -1267,14 +1297,15 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1267
1297
  auth;
1268
1298
  basePath;
1269
1299
  logger = new Logger(_AgentDashboardAuthController.name);
1270
- /** Warn once (not per request) so a broken hook can't be used to flood the logs. */
1271
- warnedLoginHookThrow = false;
1300
+ /** Warn once PER HOOK KIND (not per request) so a broken hook can't be used to flood the logs. */
1301
+ warnedHooks = /* @__PURE__ */ new Set();
1272
1302
  constructor(auth, basePath) {
1273
1303
  this.auth = auth;
1274
1304
  this.basePath = basePath;
1275
1305
  }
1276
1306
  loginPage(req, res, returnToQuery, error) {
1277
- this.requireAuth();
1307
+ const auth = this.requireAuth();
1308
+ if (!auth.login) throw new NotFoundException();
1278
1309
  const returnTo = sanitizeReturnTo(this.basePath, returnToQuery);
1279
1310
  if (this.currentSession(req)) {
1280
1311
  res.status(302);
@@ -1289,6 +1320,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1289
1320
  }
1290
1321
  async login(body, req, res) {
1291
1322
  const auth = this.requireAuth();
1323
+ if (!auth.login) throw new NotFoundException();
1292
1324
  const returnTo = sanitizeReturnTo(this.basePath, body?.returnTo);
1293
1325
  const username = body?.username;
1294
1326
  const password = body?.password;
@@ -1296,7 +1328,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1296
1328
  this.redirectToLoginError(res, returnTo);
1297
1329
  return "";
1298
1330
  }
1299
- const user = await this.runLoginHook(auth, username.trim(), isString(password) ? password : "");
1331
+ const user = await this.runHook("login", () => auth.login?.(username.trim(), isString(password) ? password : "") ?? null);
1300
1332
  if (!user) {
1301
1333
  this.redirectToLoginError(res, returnTo);
1302
1334
  return "";
@@ -1316,9 +1348,30 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1316
1348
  response: res
1317
1349
  });
1318
1350
  res.status(302);
1319
- res.setHeader("Location", `${this.basePath}/auth/login`);
1351
+ res.setHeader("Location", this.auth && !this.auth.login ? `${this.basePath}/auth/session-required` : `${this.basePath}/auth/login`);
1320
1352
  return "";
1321
1353
  }
1354
+ // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw
1355
+ // request and returns the session user, or `null` to deny. No credential reaches this library.
1356
+ async session(request, response) {
1357
+ const auth = this.requireAuth();
1358
+ if (!auth.session) throw new NotFoundException();
1359
+ const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1360
+ if (!user) throw new UnauthorizedException2();
1361
+ issueSessionCookie(user, {
1362
+ auth,
1363
+ request,
1364
+ response
1365
+ });
1366
+ }
1367
+ // Mode-A-only landing: the host mints the session, so there is nothing to submit here. Serves
1368
+ // as the redirect target `DashboardAuthPageGuard` bounces to instead of `/auth/login`, which
1369
+ // 404s under Mode A (see `loginPage` above).
1370
+ sessionRequiredPage() {
1371
+ const auth = this.requireAuth();
1372
+ if (auth.login) throw new NotFoundException();
1373
+ return renderSessionRequiredPage();
1374
+ }
1322
1375
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
1323
1376
  redirectToLoginError(res, returnTo) {
1324
1377
  res.status(303);
@@ -1334,17 +1387,17 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1334
1387
  }) !== null;
1335
1388
  }
1336
1389
  /**
1337
- * Run the host's `login` hook defensively: a throw (sync or async) is treated as a denial
1338
- * (`null`) and warn-logged ONCE, so a buggy/unreachable hook never 500s the endpoint into a
1339
- * stack-trace leak nor floods the logs.
1390
+ * Run a host hook (`login` or `session`) defensively: a throw (sync or async) is treated as a
1391
+ * denial (`null`) and warn-logged ONCE PER KIND, so a buggy/unreachable hook never 500s the
1392
+ * endpoint into a stack-trace leak nor floods the logs on repeated bad attempts.
1340
1393
  */
1341
- async runLoginHook(auth, username, password) {
1394
+ async runHook(kind, run) {
1342
1395
  try {
1343
- return await auth.login(username, password) ?? null;
1396
+ return await run() ?? null;
1344
1397
  } catch (error) {
1345
- if (!this.warnedLoginHookThrow) {
1346
- this.warnedLoginHookThrow = true;
1347
- this.logger.warn(`AgentDashboardModule dashboardAuth login hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1398
+ if (!this.warnedHooks.has(kind)) {
1399
+ this.warnedHooks.add(kind);
1400
+ this.logger.warn(`AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
1348
1401
  }
1349
1402
  return null;
1350
1403
  }
@@ -1401,6 +1454,28 @@ _ts_decorate4([
1401
1454
  ]),
1402
1455
  _ts_metadata4("design:returntype", String)
1403
1456
  ], AgentDashboardAuthController.prototype, "logout", null);
1457
+ _ts_decorate4([
1458
+ Post2("session"),
1459
+ HttpCode2(204),
1460
+ _ts_param4(0, Req2()),
1461
+ _ts_param4(1, Res({
1462
+ passthrough: true
1463
+ })),
1464
+ _ts_metadata4("design:type", Function),
1465
+ _ts_metadata4("design:paramtypes", [
1466
+ Object,
1467
+ Object
1468
+ ]),
1469
+ _ts_metadata4("design:returntype", Promise)
1470
+ ], AgentDashboardAuthController.prototype, "session", null);
1471
+ _ts_decorate4([
1472
+ Get2("session-required"),
1473
+ Header("Content-Type", "text/html; charset=utf-8"),
1474
+ Header("Cache-Control", "no-store, must-revalidate"),
1475
+ _ts_metadata4("design:type", Function),
1476
+ _ts_metadata4("design:paramtypes", []),
1477
+ _ts_metadata4("design:returntype", String)
1478
+ ], AgentDashboardAuthController.prototype, "sessionRequiredPage", null);
1404
1479
  AgentDashboardAuthController = _ts_decorate4([
1405
1480
  Controller2("auth"),
1406
1481
  _ts_param4(0, Optional4()),
@@ -1513,19 +1588,39 @@ var DashboardAuthPageGuard = class {
1513
1588
  this.auth = auth;
1514
1589
  this.basePath = basePath;
1515
1590
  }
1516
- canActivate(context) {
1591
+ async canActivate(context) {
1517
1592
  if (!this.auth) return true;
1593
+ const auth = this.auth;
1518
1594
  const http = context.switchToHttp();
1519
1595
  const request = http.getRequest();
1520
1596
  const session = this.verifyRequestSession(request);
1521
- if (!session) {
1522
- const returnTo = readOriginalUrl(request);
1523
- throw new DashboardAuthRedirect(`${this.basePath}/auth/login?returnTo=${encodeURIComponent(returnTo)}`);
1524
- }
1597
+ if (!session) this.deny(auth, request);
1525
1598
  attachSession(request, session);
1526
- this.maybeRenew(http.getResponse(), request, session);
1599
+ if (!await maybeRenewSession(auth, session, request, http.getResponse())) {
1600
+ this.deny(auth, request);
1601
+ }
1527
1602
  return true;
1528
1603
  }
1604
+ /**
1605
+ * No valid session: bounce the browser navigation onward. Under Mode B (`login` configured)
1606
+ * that's the built-in login screen, carrying `returnTo` back to the page that was requested.
1607
+ * Under Mode-A-only there is no login screen to redirect to — the host mints the session
1608
+ * itself — so this instead serves the instruction page rather than redirecting into a 404.
1609
+ * Pulled out of `canActivate` so both the "no session at all" and the "revalidate revoked a
1610
+ * renewable session" paths reuse the exact same mode-aware deny logic.
1611
+ *
1612
+ * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early
1613
+ * return narrows the field within that method only — TS doesn't carry it across a method call —
1614
+ * so the caller passes the already-narrowed value instead of this method re-deriving (or
1615
+ * asserting away) the nullability.
1616
+ */
1617
+ deny(auth, request) {
1618
+ if (!auth.login) {
1619
+ throw new DashboardAuthRedirect(`${this.basePath}/auth/session-required`);
1620
+ }
1621
+ const returnTo = readOriginalUrl(request);
1622
+ throw new DashboardAuthRedirect(`${this.basePath}/auth/login?returnTo=${encodeURIComponent(returnTo)}`);
1623
+ }
1529
1624
  verifyRequestSession(request) {
1530
1625
  const auth = this.auth;
1531
1626
  if (!auth) return null;
@@ -1535,28 +1630,6 @@ var DashboardAuthPageGuard = class {
1535
1630
  secret: auth.secret
1536
1631
  });
1537
1632
  }
1538
- /**
1539
- * Sliding renewal: a valid cookie past 50% of its TTL is transparently re-issued on the
1540
- * response, so an active user never gets logged out mid-session.
1541
- */
1542
- maybeRenew(response, request, session) {
1543
- const auth = this.auth;
1544
- if (!auth) return;
1545
- const now = Date.now();
1546
- if (now - session.iat <= auth.ttlMs / 2) return;
1547
- issueSessionCookie({
1548
- id: session.sub,
1549
- ...session.name !== void 0 ? {
1550
- name: session.name
1551
- } : {},
1552
- roles: session.roles
1553
- }, {
1554
- auth,
1555
- request,
1556
- response,
1557
- now
1558
- });
1559
- }
1560
1633
  };
1561
1634
  DashboardAuthPageGuard = _ts_decorate6([
1562
1635
  Injectable4(),
@@ -1715,13 +1788,38 @@ function resolveDashboardAuth(options) {
1715
1788
  if (typeof options.secret !== "string" || options.secret === "") {
1716
1789
  throw new Error("AgentDashboardModule dashboardAuth: `secret` is required and must be a non-empty string (HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.");
1717
1790
  }
1718
- if (typeof options.login !== "function") {
1719
- throw new Error("AgentDashboardModule dashboardAuth: `login` is required (validates submitted credentials for the built-in login screen). Failing closed.");
1791
+ const modes = [];
1792
+ if (options.session !== void 0) {
1793
+ if (typeof options.session !== "function") {
1794
+ throw new Error("AgentDashboardModule dashboardAuth: `session` must be a function.");
1795
+ }
1796
+ modes.push("session");
1797
+ }
1798
+ if (options.login !== void 0) {
1799
+ if (typeof options.login !== "function") {
1800
+ throw new Error("AgentDashboardModule dashboardAuth: `login` must be a function.");
1801
+ }
1802
+ modes.push("login");
1803
+ }
1804
+ if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
1805
+ throw new Error("AgentDashboardModule dashboardAuth: `revalidate` must be a function.");
1806
+ }
1807
+ if (modes.length === 0) {
1808
+ 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.");
1720
1809
  }
1721
1810
  return {
1722
1811
  secret: options.secret,
1723
1812
  ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),
1724
- login: options.login
1813
+ modes,
1814
+ ...options.session !== void 0 ? {
1815
+ session: options.session
1816
+ } : {},
1817
+ ...options.login !== void 0 ? {
1818
+ login: options.login
1819
+ } : {},
1820
+ ...options.revalidate !== void 0 ? {
1821
+ revalidate: options.revalidate
1822
+ } : {}
1725
1823
  };
1726
1824
  }
1727
1825
  __name(resolveDashboardAuth, "resolveDashboardAuth");