@dudousxd/nestjs-agent-dashboard 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -203,14 +203,15 @@ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> |
203
203
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
204
204
  /**
205
205
  * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` PER
206
- * COOKIE GENERATION, per in-flight request — the gate is on the inbound cookie's `iat`, so every
207
- * request still carrying a not-yet-renewed cookie invokes this hook, not just the first: a page
208
- * load that fires N parallel API calls can trigger up to N calls before the refreshed cookie lands
209
- * on the client. Bounded and cheap for typical console traffic, but not a per-session-lifetime
210
- * cap — a slow or expensive check should debounce/cache on its own. Return `false` to revoke (the
211
- * cookie is cleared and the request denied). Distinct from `session`: that hook reads the host's
212
- * auth off a fresh request, which a console XHR does not carry — this one receives the
213
- * already-minted session.
206
+ * COOKIE GENERATION — the gate is on the inbound cookie's `iat`, so every request still carrying a
207
+ * not-yet-renewed cookie would, on its own, invoke this hook again; `maybeRenewSession` de-dupes
208
+ * concurrent calls for the same session (`sub` + `iat`) into a single in-flight call, so a page
209
+ * load that fires N parallel API calls still costs exactly ONE host round-trip, not N. Still not a
210
+ * per-session-lifetime cap — this can run again every `ttl/2` as the session keeps sliding, so a
211
+ * slow or expensive check should still debounce/cache across renewal windows on its own. Return
212
+ * `false` to revoke (the cookie is cleared and the request denied). Distinct from `session`: that
213
+ * hook reads the host's auth off a fresh request, which a console XHR does not carry — this one
214
+ * receives the already-minted session.
214
215
  */
215
216
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
216
217
  /** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
@@ -289,9 +290,10 @@ interface LoginFormBody {
289
290
  * hook decides — or Mode B (`login`), a dependency-free, server-rendered login form (GET/POST) —
290
291
  * the bundled React SPA in this package is a built Vite artifact with no auth-aware UI of its own,
291
292
  * so gating the actual page navigation needs a self-contained flow that doesn't touch it. Each
292
- * mode's routes 404 when that mode isn't configured (`auth.session`/`auth.login` absent), and
293
- * every route 404s outright when `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to
294
- * `null`) — no dangling login page when the feature is off.
293
+ * mode's routes 404 when that mode isn't listed in `auth.modes` — the single source of truth for
294
+ * which mode(s) are configured, not hook-presence truthiness — and every route 404s outright when
295
+ * `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to `null`) — no dangling login page
296
+ * when the feature is off.
295
297
  */
296
298
  declare class AgentDashboardAuthController {
297
299
  private readonly auth;
@@ -203,14 +203,15 @@ type SessionHook = (request: unknown) => Promise<DashboardSessionUser | null> |
203
203
  type LoginHook = (username: string, password: string) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;
204
204
  /**
205
205
  * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` PER
206
- * COOKIE GENERATION, per in-flight request — the gate is on the inbound cookie's `iat`, so every
207
- * request still carrying a not-yet-renewed cookie invokes this hook, not just the first: a page
208
- * load that fires N parallel API calls can trigger up to N calls before the refreshed cookie lands
209
- * on the client. Bounded and cheap for typical console traffic, but not a per-session-lifetime
210
- * cap — a slow or expensive check should debounce/cache on its own. Return `false` to revoke (the
211
- * cookie is cleared and the request denied). Distinct from `session`: that hook reads the host's
212
- * auth off a fresh request, which a console XHR does not carry — this one receives the
213
- * already-minted session.
206
+ * COOKIE GENERATION — the gate is on the inbound cookie's `iat`, so every request still carrying a
207
+ * not-yet-renewed cookie would, on its own, invoke this hook again; `maybeRenewSession` de-dupes
208
+ * concurrent calls for the same session (`sub` + `iat`) into a single in-flight call, so a page
209
+ * load that fires N parallel API calls still costs exactly ONE host round-trip, not N. Still not a
210
+ * per-session-lifetime cap — this can run again every `ttl/2` as the session keeps sliding, so a
211
+ * slow or expensive check should still debounce/cache across renewal windows on its own. Return
212
+ * `false` to revoke (the cookie is cleared and the request denied). Distinct from `session`: that
213
+ * hook reads the host's auth off a fresh request, which a console XHR does not carry — this one
214
+ * receives the already-minted session.
214
215
  */
215
216
  type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;
216
217
  /** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
@@ -289,9 +290,10 @@ interface LoginFormBody {
289
290
  * hook decides — or Mode B (`login`), a dependency-free, server-rendered login form (GET/POST) —
290
291
  * the bundled React SPA in this package is a built Vite artifact with no auth-aware UI of its own,
291
292
  * so gating the actual page navigation needs a self-contained flow that doesn't touch it. Each
292
- * mode's routes 404 when that mode isn't configured (`auth.session`/`auth.login` absent), and
293
- * every route 404s outright when `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to
294
- * `null`) — no dangling login page when the feature is off.
293
+ * mode's routes 404 when that mode isn't listed in `auth.modes` — the single source of truth for
294
+ * which mode(s) are configured, not hook-presence truthiness — and every route 404s outright when
295
+ * `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to `null`) — no dangling login page
296
+ * when the feature is off.
295
297
  */
296
298
  declare class AgentDashboardAuthController {
297
299
  private readonly auth;
@@ -261,6 +261,30 @@ function clearSessionCookie(context) {
261
261
  appendSetCookie(context.response, cookie);
262
262
  }
263
263
  __name(clearSessionCookie, "clearSessionCookie");
264
+ var inFlightRevalidations = /* @__PURE__ */ new Map();
265
+ function revalidationKey(session) {
266
+ return `${session.sub}:${session.iat}`;
267
+ }
268
+ __name(revalidationKey, "revalidationKey");
269
+ function getOrStartRevalidation(auth, session, user) {
270
+ const hook = auth.revalidate;
271
+ if (!hook) return Promise.resolve(true);
272
+ const key = revalidationKey(session);
273
+ const inFlight = inFlightRevalidations.get(key);
274
+ if (inFlight) return inFlight;
275
+ const outcome = (async () => {
276
+ try {
277
+ return await hook(user);
278
+ } catch {
279
+ return false;
280
+ }
281
+ })().finally(() => {
282
+ inFlightRevalidations.delete(key);
283
+ });
284
+ inFlightRevalidations.set(key, outcome);
285
+ return outcome;
286
+ }
287
+ __name(getOrStartRevalidation, "getOrStartRevalidation");
264
288
  async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
265
289
  if (now - session.iat <= auth.ttlMs / 2) return true;
266
290
  const user = {
@@ -270,20 +294,13 @@ async function maybeRenewSession(auth, session, request, response, now = Date.no
270
294
  } : {},
271
295
  roles: session.roles
272
296
  };
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
- }
297
+ const allowed = await getOrStartRevalidation(auth, session, user);
298
+ if (!allowed) {
299
+ clearSessionCookie({
300
+ request,
301
+ response
302
+ });
303
+ return false;
287
304
  }
288
305
  issueSessionCookie(user, {
289
306
  auth,
@@ -1305,7 +1322,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1305
1322
  }
1306
1323
  loginPage(req, res, returnToQuery, error) {
1307
1324
  const auth = this.requireAuth();
1308
- if (!auth.login) throw new NotFoundException();
1325
+ if (!auth.modes.includes("login")) throw new NotFoundException();
1309
1326
  const returnTo = sanitizeReturnTo(this.basePath, returnToQuery);
1310
1327
  if (this.currentSession(req)) {
1311
1328
  res.status(302);
@@ -1320,7 +1337,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1320
1337
  }
1321
1338
  async login(body, req, res) {
1322
1339
  const auth = this.requireAuth();
1323
- if (!auth.login) throw new NotFoundException();
1340
+ if (!auth.modes.includes("login")) throw new NotFoundException();
1324
1341
  const returnTo = sanitizeReturnTo(this.basePath, body?.returnTo);
1325
1342
  const username = body?.username;
1326
1343
  const password = body?.password;
@@ -1348,14 +1365,14 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1348
1365
  response: res
1349
1366
  });
1350
1367
  res.status(302);
1351
- res.setHeader("Location", this.auth && !this.auth.login ? `${this.basePath}/auth/session-required` : `${this.basePath}/auth/login`);
1368
+ res.setHeader("Location", this.auth && !this.auth.modes.includes("login") ? `${this.basePath}/auth/session-required` : `${this.basePath}/auth/login`);
1352
1369
  return "";
1353
1370
  }
1354
1371
  // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw
1355
1372
  // request and returns the session user, or `null` to deny. No credential reaches this library.
1356
1373
  async session(request, response) {
1357
1374
  const auth = this.requireAuth();
1358
- if (!auth.session) throw new NotFoundException();
1375
+ if (!auth.modes.includes("session")) throw new NotFoundException();
1359
1376
  const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1360
1377
  if (!user) throw new UnauthorizedException2();
1361
1378
  issueSessionCookie(user, {
@@ -1369,7 +1386,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
1369
1386
  // 404s under Mode A (see `loginPage` above).
1370
1387
  sessionRequiredPage() {
1371
1388
  const auth = this.requireAuth();
1372
- if (auth.login) throw new NotFoundException();
1389
+ if (auth.modes.includes("login")) throw new NotFoundException();
1373
1390
  return renderSessionRequiredPage();
1374
1391
  }
1375
1392
  /** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
@@ -1615,7 +1632,7 @@ var DashboardAuthPageGuard = class {
1615
1632
  * asserting away) the nullability.
1616
1633
  */
1617
1634
  deny(auth, request) {
1618
- if (!auth.login) {
1635
+ if (!auth.modes.includes("login")) {
1619
1636
  throw new DashboardAuthRedirect(`${this.basePath}/auth/session-required`);
1620
1637
  }
1621
1638
  const returnTo = readOriginalUrl(request);