@dudousxd/nestjs-agent-dashboard 0.10.0 → 0.11.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/README.md +24 -0
- package/dist/server/index.cjs +97 -30
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +62 -13
- package/dist/server/index.d.ts +62 -13
- package/dist/server/index.js +97 -30
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
package/dist/server/index.d.cts
CHANGED
|
@@ -203,16 +203,55 @@ 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
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* cap —
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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;
|
|
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>;
|
|
216
255
|
/** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
|
|
217
256
|
type AuthMode = 'session' | 'login';
|
|
218
257
|
/**
|
|
@@ -244,6 +283,9 @@ interface DashboardAuthOptions {
|
|
|
244
283
|
/** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
|
|
245
284
|
* mint a session, only revoke one already minted by `session`/`login`. */
|
|
246
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;
|
|
247
289
|
}
|
|
248
290
|
/** Resolved, validated `dashboardAuth` config used by the guards/controller. */
|
|
249
291
|
interface ResolvedDashboardAuth {
|
|
@@ -253,6 +295,7 @@ interface ResolvedDashboardAuth {
|
|
|
253
295
|
session?: SessionHook;
|
|
254
296
|
login?: LoginHook;
|
|
255
297
|
revalidate?: RevalidateHook;
|
|
298
|
+
unauthenticatedPage?: UnauthenticatedPageHook;
|
|
256
299
|
}
|
|
257
300
|
/**
|
|
258
301
|
* Validate + resolve `dashboardAuth`. Returns `null` when unconfigured (behavior unchanged — the
|
|
@@ -289,9 +332,10 @@ interface LoginFormBody {
|
|
|
289
332
|
* hook decides — or Mode B (`login`), a dependency-free, server-rendered login form (GET/POST) —
|
|
290
333
|
* the bundled React SPA in this package is a built Vite artifact with no auth-aware UI of its own,
|
|
291
334
|
* 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
|
|
293
|
-
*
|
|
294
|
-
* `null`) — no dangling login page
|
|
335
|
+
* mode's routes 404 when that mode isn't listed in `auth.modes` — the single source of truth for
|
|
336
|
+
* which mode(s) are configured, not hook-presence truthiness — and every route 404s outright when
|
|
337
|
+
* `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to `null`) — no dangling login page
|
|
338
|
+
* when the feature is off.
|
|
295
339
|
*/
|
|
296
340
|
declare class AgentDashboardAuthController {
|
|
297
341
|
private readonly auth;
|
|
@@ -304,7 +348,7 @@ declare class AgentDashboardAuthController {
|
|
|
304
348
|
login(body: LoginFormBody, req: unknown, res: AuthPageResponse): Promise<string>;
|
|
305
349
|
logout(req: unknown, res: AuthPageResponse): string;
|
|
306
350
|
session(request: unknown, response: unknown): Promise<void>;
|
|
307
|
-
sessionRequiredPage():
|
|
351
|
+
sessionRequiredPage(req: AuthPageRequest, res: unknown): Promise<void>;
|
|
308
352
|
/** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
|
|
309
353
|
private redirectToLoginError;
|
|
310
354
|
private currentSession;
|
|
@@ -314,6 +358,11 @@ declare class AgentDashboardAuthController {
|
|
|
314
358
|
* endpoint into a stack-trace leak nor floods the logs on repeated bad attempts.
|
|
315
359
|
*/
|
|
316
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;
|
|
317
366
|
private requireAuth;
|
|
318
367
|
}
|
|
319
368
|
|
|
@@ -587,4 +636,4 @@ declare const DASHBOARD_APPROVAL_ACTOR_REF: unique symbol;
|
|
|
587
636
|
*/
|
|
588
637
|
declare const DASHBOARD_AUTH: unique symbol;
|
|
589
638
|
|
|
590
|
-
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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -203,16 +203,55 @@ 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
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* cap —
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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;
|
|
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>;
|
|
216
255
|
/** Which `dashboardAuth` hook(s) a resolved config was given — see `resolveDashboardAuth`. */
|
|
217
256
|
type AuthMode = 'session' | 'login';
|
|
218
257
|
/**
|
|
@@ -244,6 +283,9 @@ interface DashboardAuthOptions {
|
|
|
244
283
|
/** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot
|
|
245
284
|
* mint a session, only revoke one already minted by `session`/`login`. */
|
|
246
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;
|
|
247
289
|
}
|
|
248
290
|
/** Resolved, validated `dashboardAuth` config used by the guards/controller. */
|
|
249
291
|
interface ResolvedDashboardAuth {
|
|
@@ -253,6 +295,7 @@ interface ResolvedDashboardAuth {
|
|
|
253
295
|
session?: SessionHook;
|
|
254
296
|
login?: LoginHook;
|
|
255
297
|
revalidate?: RevalidateHook;
|
|
298
|
+
unauthenticatedPage?: UnauthenticatedPageHook;
|
|
256
299
|
}
|
|
257
300
|
/**
|
|
258
301
|
* Validate + resolve `dashboardAuth`. Returns `null` when unconfigured (behavior unchanged — the
|
|
@@ -289,9 +332,10 @@ interface LoginFormBody {
|
|
|
289
332
|
* hook decides — or Mode B (`login`), a dependency-free, server-rendered login form (GET/POST) —
|
|
290
333
|
* the bundled React SPA in this package is a built Vite artifact with no auth-aware UI of its own,
|
|
291
334
|
* 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
|
|
293
|
-
*
|
|
294
|
-
* `null`) — no dangling login page
|
|
335
|
+
* mode's routes 404 when that mode isn't listed in `auth.modes` — the single source of truth for
|
|
336
|
+
* which mode(s) are configured, not hook-presence truthiness — and every route 404s outright when
|
|
337
|
+
* `dashboardAuth` is unconfigured (`DASHBOARD_AUTH` resolves to `null`) — no dangling login page
|
|
338
|
+
* when the feature is off.
|
|
295
339
|
*/
|
|
296
340
|
declare class AgentDashboardAuthController {
|
|
297
341
|
private readonly auth;
|
|
@@ -304,7 +348,7 @@ declare class AgentDashboardAuthController {
|
|
|
304
348
|
login(body: LoginFormBody, req: unknown, res: AuthPageResponse): Promise<string>;
|
|
305
349
|
logout(req: unknown, res: AuthPageResponse): string;
|
|
306
350
|
session(request: unknown, response: unknown): Promise<void>;
|
|
307
|
-
sessionRequiredPage():
|
|
351
|
+
sessionRequiredPage(req: AuthPageRequest, res: unknown): Promise<void>;
|
|
308
352
|
/** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
|
|
309
353
|
private redirectToLoginError;
|
|
310
354
|
private currentSession;
|
|
@@ -314,6 +358,11 @@ declare class AgentDashboardAuthController {
|
|
|
314
358
|
* endpoint into a stack-trace leak nor floods the logs on repeated bad attempts.
|
|
315
359
|
*/
|
|
316
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;
|
|
317
366
|
private requireAuth;
|
|
318
367
|
}
|
|
319
368
|
|
|
@@ -587,4 +636,4 @@ declare const DASHBOARD_APPROVAL_ACTOR_REF: unique symbol;
|
|
|
587
636
|
*/
|
|
588
637
|
declare const DASHBOARD_AUTH: unique symbol;
|
|
589
638
|
|
|
590
|
-
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 };
|
package/dist/server/index.js
CHANGED
|
@@ -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;
|
|
@@ -261,6 +275,30 @@ function clearSessionCookie(context) {
|
|
|
261
275
|
appendSetCookie(context.response, cookie);
|
|
262
276
|
}
|
|
263
277
|
__name(clearSessionCookie, "clearSessionCookie");
|
|
278
|
+
var inFlightRevalidations = /* @__PURE__ */ new Map();
|
|
279
|
+
function revalidationKey(session) {
|
|
280
|
+
return `${session.sub}:${session.iat}`;
|
|
281
|
+
}
|
|
282
|
+
__name(revalidationKey, "revalidationKey");
|
|
283
|
+
function getOrStartRevalidation(auth, session, user) {
|
|
284
|
+
const hook = auth.revalidate;
|
|
285
|
+
if (!hook) return Promise.resolve(true);
|
|
286
|
+
const key = revalidationKey(session);
|
|
287
|
+
const inFlight = inFlightRevalidations.get(key);
|
|
288
|
+
if (inFlight) return inFlight;
|
|
289
|
+
const outcome = (async () => {
|
|
290
|
+
try {
|
|
291
|
+
return await hook(user);
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
})().finally(() => {
|
|
296
|
+
inFlightRevalidations.delete(key);
|
|
297
|
+
});
|
|
298
|
+
inFlightRevalidations.set(key, outcome);
|
|
299
|
+
return outcome;
|
|
300
|
+
}
|
|
301
|
+
__name(getOrStartRevalidation, "getOrStartRevalidation");
|
|
264
302
|
async function maybeRenewSession(auth, session, request, response, now = Date.now()) {
|
|
265
303
|
if (now - session.iat <= auth.ttlMs / 2) return true;
|
|
266
304
|
const user = {
|
|
@@ -270,20 +308,13 @@ async function maybeRenewSession(auth, session, request, response, now = Date.no
|
|
|
270
308
|
} : {},
|
|
271
309
|
roles: session.roles
|
|
272
310
|
};
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (!allowed) {
|
|
281
|
-
clearSessionCookie({
|
|
282
|
-
request,
|
|
283
|
-
response
|
|
284
|
-
});
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
311
|
+
const allowed = await getOrStartRevalidation(auth, session, user);
|
|
312
|
+
if (!allowed) {
|
|
313
|
+
clearSessionCookie({
|
|
314
|
+
request,
|
|
315
|
+
response
|
|
316
|
+
});
|
|
317
|
+
return false;
|
|
287
318
|
}
|
|
288
319
|
issueSessionCookie(user, {
|
|
289
320
|
auth,
|
|
@@ -1305,7 +1336,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
|
|
|
1305
1336
|
}
|
|
1306
1337
|
loginPage(req, res, returnToQuery, error) {
|
|
1307
1338
|
const auth = this.requireAuth();
|
|
1308
|
-
if (!auth.login) throw new NotFoundException();
|
|
1339
|
+
if (!auth.modes.includes("login")) throw new NotFoundException();
|
|
1309
1340
|
const returnTo = sanitizeReturnTo(this.basePath, returnToQuery);
|
|
1310
1341
|
if (this.currentSession(req)) {
|
|
1311
1342
|
res.status(302);
|
|
@@ -1320,7 +1351,7 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
|
|
|
1320
1351
|
}
|
|
1321
1352
|
async login(body, req, res) {
|
|
1322
1353
|
const auth = this.requireAuth();
|
|
1323
|
-
if (!auth.login) throw new NotFoundException();
|
|
1354
|
+
if (!auth.modes.includes("login")) throw new NotFoundException();
|
|
1324
1355
|
const returnTo = sanitizeReturnTo(this.basePath, body?.returnTo);
|
|
1325
1356
|
const username = body?.username;
|
|
1326
1357
|
const password = body?.password;
|
|
@@ -1348,14 +1379,14 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
|
|
|
1348
1379
|
response: res
|
|
1349
1380
|
});
|
|
1350
1381
|
res.status(302);
|
|
1351
|
-
res.setHeader("Location", this.auth && !this.auth.login ? `${this.basePath}/auth/session-required` : `${this.basePath}/auth/login`);
|
|
1382
|
+
res.setHeader("Location", this.auth && !this.auth.modes.includes("login") ? `${this.basePath}/auth/session-required` : `${this.basePath}/auth/login`);
|
|
1352
1383
|
return "";
|
|
1353
1384
|
}
|
|
1354
1385
|
// Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw
|
|
1355
1386
|
// request and returns the session user, or `null` to deny. No credential reaches this library.
|
|
1356
1387
|
async session(request, response) {
|
|
1357
1388
|
const auth = this.requireAuth();
|
|
1358
|
-
if (!auth.session) throw new NotFoundException();
|
|
1389
|
+
if (!auth.modes.includes("session")) throw new NotFoundException();
|
|
1359
1390
|
const user = await this.runHook("session", () => auth.session?.(request) ?? null);
|
|
1360
1391
|
if (!user) throw new UnauthorizedException2();
|
|
1361
1392
|
issueSessionCookie(user, {
|
|
@@ -1367,10 +1398,31 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
|
|
|
1367
1398
|
// Mode-A-only landing: the host mints the session, so there is nothing to submit here. Serves
|
|
1368
1399
|
// as the redirect target `DashboardAuthPageGuard` bounces to instead of `/auth/login`, which
|
|
1369
1400
|
// 404s under Mode A (see `loginPage` above).
|
|
1370
|
-
|
|
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) {
|
|
1371
1408
|
const auth = this.requireAuth();
|
|
1372
|
-
if (auth.login) throw new NotFoundException();
|
|
1373
|
-
|
|
1409
|
+
if (auth.modes.includes("login")) throw new NotFoundException();
|
|
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());
|
|
1374
1426
|
}
|
|
1375
1427
|
/** Uniform failure: same redirect (generic error flag) whether the user is unknown or the password is wrong. */
|
|
1376
1428
|
redirectToLoginError(res, returnTo) {
|
|
@@ -1395,13 +1447,19 @@ var AgentDashboardAuthController = class _AgentDashboardAuthController {
|
|
|
1395
1447
|
try {
|
|
1396
1448
|
return await run() ?? null;
|
|
1397
1449
|
} catch (error) {
|
|
1398
|
-
|
|
1399
|
-
this.warnedHooks.add(kind);
|
|
1400
|
-
this.logger.warn(`AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
|
|
1401
|
-
}
|
|
1450
|
+
this.warnHookOnce(kind, `AgentDashboardModule dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`);
|
|
1402
1451
|
return null;
|
|
1403
1452
|
}
|
|
1404
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
|
+
}
|
|
1405
1463
|
requireAuth() {
|
|
1406
1464
|
if (!this.auth) throw new NotFoundException();
|
|
1407
1465
|
return this.auth;
|
|
@@ -1470,11 +1528,14 @@ _ts_decorate4([
|
|
|
1470
1528
|
], AgentDashboardAuthController.prototype, "session", null);
|
|
1471
1529
|
_ts_decorate4([
|
|
1472
1530
|
Get2("session-required"),
|
|
1473
|
-
|
|
1474
|
-
|
|
1531
|
+
_ts_param4(0, Req2()),
|
|
1532
|
+
_ts_param4(1, Res()),
|
|
1475
1533
|
_ts_metadata4("design:type", Function),
|
|
1476
|
-
_ts_metadata4("design:paramtypes", [
|
|
1477
|
-
|
|
1534
|
+
_ts_metadata4("design:paramtypes", [
|
|
1535
|
+
typeof AuthPageRequest === "undefined" ? Object : AuthPageRequest,
|
|
1536
|
+
Object
|
|
1537
|
+
]),
|
|
1538
|
+
_ts_metadata4("design:returntype", Promise)
|
|
1478
1539
|
], AgentDashboardAuthController.prototype, "sessionRequiredPage", null);
|
|
1479
1540
|
AgentDashboardAuthController = _ts_decorate4([
|
|
1480
1541
|
Controller2("auth"),
|
|
@@ -1615,7 +1676,7 @@ var DashboardAuthPageGuard = class {
|
|
|
1615
1676
|
* asserting away) the nullability.
|
|
1616
1677
|
*/
|
|
1617
1678
|
deny(auth, request) {
|
|
1618
|
-
if (!auth.login) {
|
|
1679
|
+
if (!auth.modes.includes("login")) {
|
|
1619
1680
|
throw new DashboardAuthRedirect(`${this.basePath}/auth/session-required`);
|
|
1620
1681
|
}
|
|
1621
1682
|
const returnTo = readOriginalUrl(request);
|
|
@@ -1804,6 +1865,9 @@ function resolveDashboardAuth(options) {
|
|
|
1804
1865
|
if (options.revalidate !== void 0 && typeof options.revalidate !== "function") {
|
|
1805
1866
|
throw new Error("AgentDashboardModule dashboardAuth: `revalidate` must be a function.");
|
|
1806
1867
|
}
|
|
1868
|
+
if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
|
|
1869
|
+
throw new Error("AgentDashboardModule dashboardAuth: `unauthenticatedPage` must be a function.");
|
|
1870
|
+
}
|
|
1807
1871
|
if (modes.length === 0) {
|
|
1808
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.");
|
|
1809
1873
|
}
|
|
@@ -1819,6 +1883,9 @@ function resolveDashboardAuth(options) {
|
|
|
1819
1883
|
} : {},
|
|
1820
1884
|
...options.revalidate !== void 0 ? {
|
|
1821
1885
|
revalidate: options.revalidate
|
|
1886
|
+
} : {},
|
|
1887
|
+
...options.unauthenticatedPage !== void 0 ? {
|
|
1888
|
+
unauthenticatedPage: options.unauthenticatedPage
|
|
1822
1889
|
} : {}
|
|
1823
1890
|
};
|
|
1824
1891
|
}
|