@dudousxd/nestjs-durable-dashboard 0.41.0 → 0.42.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/client/durable-client.d.ts +35 -4
- package/dist/client/durable-client.d.ts.map +1 -1
- package/dist/client/durable-client.js +40 -4
- package/dist/client/durable-client.js.map +1 -1
- package/dist/client/run-origin.d.ts +16 -3
- package/dist/client/run-origin.d.ts.map +1 -1
- package/dist/client/run-origin.js +22 -9
- package/dist/client/run-origin.js.map +1 -1
- package/dist/client/summarize-health.d.ts +11 -0
- package/dist/client/summarize-health.d.ts.map +1 -1
- package/dist/client/summarize-health.js +24 -0
- package/dist/client/summarize-health.js.map +1 -1
- package/dist/server/index.cjs +99 -8
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +27 -3
- package/dist/server/index.d.ts +27 -3
- package/dist/server/index.js +99 -8
- package/dist/server/index.js.map +1 -1
- package/dist/spa/assets/bench-nv8gmn-b.js +2 -0
- package/dist/spa/assets/{index-DqmPvOju.js → index-BcYcSsB3.js} +1 -1
- package/dist/spa/assets/index-CTKdGcMe.js +80 -0
- package/dist/spa/assets/{preview-B1H9t8eN.js → preview-GbzTsAP8.js} +1 -1
- package/dist/spa/bench.html +14 -0
- package/dist/spa/index.html +2 -2
- package/dist/spa/preview.html +2 -2
- package/package.json +3 -2
- package/dist/spa/assets/index-Ho8MqZ0t.js +0 -80
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/dashboard.service.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/durable-auth.controller.ts","../../src/server/auth/dashboard-auth-config.ts","../../src/server/auth/login-page.ts","../../src/server/auth/request.ts","../../src/server/auth/response.ts","../../src/server/auth/cookie-header.ts","../../src/server/auth/session-cookie.ts","../../src/server/auth/session-cookie-io.ts","../../src/server/durable-ui.controller.ts","../../src/server/auth/login-redirect.exception.ts","../../src/server/auth/session-required.exception.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-session.guard.ts"],"sourcesContent":["import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunDetail,\n RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type UpdateResult,\n WorkflowEngine,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\n\n// Single source of truth: core owns `RunDetail` (`run-gateway.ts`) — re-exported here (not\n// re-declared) so this package's existing public API (`import type { RunDetail } from\n// '@dudousxd/nestjs-durable-dashboard'`) keeps working unchanged.\nexport type { RunDetail };\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow / namespace /\n * origin) — e.g. \"retry every `dead` run tagged `type:mel`\". The filter is the same one the run\n * list uses, so a bulk action can never reach wider than the list it was launched from — but note\n * an ABSENT predicate still means \"every value on that axis\" (an omitted `namespace` is every\n * tenant), which is what makes passing the operator's full filter through mandatory, not optional.\n * Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes' | 'namespace' | 'origin'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n /**\n * The run list, filtered by whatever the operator asked for. Every predicate is OPTIONAL and an\n * omitted one means \"don't narrow on this axis\" — in particular `namespace`, whose absence keeps\n * the historical, deliberate behaviour that read paths are NOT namespace-scoped (core,\n * `WorkflowRun.namespace`): the console shows every tenant's runs until an operator chooses one.\n * A blank param (`?namespace=`, from a cleared filter box) is the same as an absent one; passing\n * `''` through would be an exact match on a tenant nobody has, i.e. a silently empty console.\n */\n @Get('runs')\n runs(\n @Query('status') status?: RunStatus,\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n @Query('namespace') namespace?: string,\n @Query('origin') origin?: string,\n ) {\n return this.dashboard.listRuns({\n status,\n workflow,\n tag,\n attributes: parseAttrFilters(attr),\n namespace: namespace || undefined,\n origin: origin || undefined,\n });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /**\n * Bulk retry/cancel every run matching a filter (status / tag / workflow / namespace / origin).\n * The filter takes the SAME params as `GET runs` and is parsed identically, so \"retry all\" acts on\n * exactly the set the operator was looking at rather than a wider one.\n */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n @Query('namespace') namespace?: string,\n @Query('origin') origin?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n {\n status,\n tag,\n workflow,\n attributes: parseAttrFilters(attr),\n namespace: namespace || undefined,\n origin: origin || undefined,\n },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import {\n BadRequestException,\n Body,\n Controller,\n Get,\n Header,\n HttpCode,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Post,\n Req,\n Res,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './auth/dashboard-auth-config.js';\nimport { renderLoginPage } from './auth/login-page.js';\nimport { sanitizeReturnTo } from './auth/request.js';\nimport { redirectRaw } from './auth/response.js';\nimport { clearSessionCookie, issueSessionCookie } from './auth/session-cookie-io.js';\nimport type { DashboardSessionUser } from './auth/session-cookie.js';\nimport { DASHBOARD_BASE_PATH } from './durable-ui.controller.js';\n\ninterface LoginBody {\n username?: unknown;\n password?: unknown;\n returnTo?: unknown;\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\n/**\n * Mints/clears the `dashboardAuth` session cookie and serves the built-in login page. Mounted\n * under `basePath` alongside `DurableUiController` (see `DurableDashboardModule.forRoot`), but on\n * a SEPARATE controller so it is never itself behind `DurableUiSessionGuard` — these endpoints\n * CREATE the session that guard checks for. `@Optional()` on both injections: this controller is\n * only registered when `dashboardAuth` might be configured (a static `forRoot({ dashboardAuth })`\n * or any `forRootAsync`), but the resolved value can still be `null` at runtime for the async case\n * — every handler below re-checks it and 404s rather than assuming it is set.\n */\n@Controller()\nexport class DurableAuthController {\n private readonly logger = new Logger(DurableAuthController.name);\n /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */\n private readonly warnedHooks = new Set<string>();\n\n constructor(\n @Optional() @Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null,\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n ) {}\n\n // The login page reads `returnTo`/`error` from `location.search` client-side (see\n // `login-page.ts`), so this response never varies per request and needs no templating beyond\n // the developer-controlled `basePath`.\n @Get('login')\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n loginPage(): string {\n const auth = this.requireAuth();\n if (!auth.modes.includes('login')) throw new NotFoundException();\n return renderLoginPage(this.basePath);\n }\n\n // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for\n // why: it keeps the login page working with no host-configured body parser beyond the\n // JSON one Nest wires in by default on both Express and Fastify). Uniform `401` on any failure\n // (unknown user, wrong password, throwing hook) — no user-enumeration.\n @Post('login')\n @HttpCode(200)\n async login(\n @Body() body: LoginBody,\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<{ redirectTo: string }> {\n const auth = this.requireAuth();\n if (!auth.modes.includes('login')) throw new NotFoundException();\n if (!isString(body?.username) || !isString(body?.password)) {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const { username, password } = body;\n const user = await this.runHook('login', () => auth.login?.(username, password) ?? null);\n if (!user) throw new UnauthorizedException({ message: 'Invalid username or password.' });\n issueSessionCookie(user, { auth, request, response });\n return { redirectTo: sanitizeReturnTo(body.returnTo, this.basePath) };\n }\n\n // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw\n // request and returns the session user, or `null` to deny. This is the SSO path — the host's own\n // guard decides, and no credential ever reaches this library.\n @Post('session')\n @HttpCode(204)\n async session(\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<void> {\n const auth = this.requireAuth();\n // Mode A not configured => the endpoint does not exist for this host.\n if (!auth.modes.includes('session')) throw new NotFoundException();\n const user = await this.runHook('session', () => auth.session?.(request) ?? null);\n if (!user) throw new UnauthorizedException();\n issueSessionCookie(user, { auth, request, response });\n }\n\n // Plain `GET` (not `POST`): logging out only ever destroys the CALLER's own session, so it's\n // safe/idempotent from the caller's perspective — a simple `<a href>` (no JS, no CSRF token)\n // works. Non-passthrough `@Res()`: this handler owns the response outright (clears the cookie,\n // then redirects), so Nest must not also try to send a return value afterwards.\n @Get('logout')\n logout(@Req() request: unknown, @Res() response: unknown): void {\n // Best-effort: even without dashboardAuth configured, clearing is harmless.\n clearSessionCookie({ request, response });\n // Only bounce to the login page when Mode B is actually configured — under Mode A (only), that\n // page 404s (see loginPage()); redirecting to basePath instead lands back on the guarded UI,\n // which renders the Mode-A session-required instruction page.\n redirectRaw(\n response,\n this.auth?.modes.includes('login') ? `${this.basePath}/login` : this.basePath,\n );\n }\n\n private requireAuth(): ResolvedDashboardAuth {\n // login()/session()/logout() only reach a real request when dashboardAuth is configured — this\n // is a defensive guard (matching loginPage()'s 404), not a reachable runtime path in practice.\n if (!this.auth) throw new NotFoundException();\n return this.auth;\n }\n\n /**\n * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform\n * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a\n * stack-trace leak nor floods the logs on repeated bad attempts.\n */\n private async runHook(\n kind: string,\n run: () => Promise<DashboardSessionUser | null> | DashboardSessionUser | null,\n ): Promise<DashboardSessionUser | null> {\n try {\n return (await run()) ?? null;\n } catch (error) {\n if (!this.warnedHooks.has(kind)) {\n this.warnedHooks.add(kind);\n this.logger.warn(\n `dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return null;\n }\n }\n}\n","import type { DashboardSessionUser } from './session-cookie.js';\n\n/** Host hook for Mode A — validates the host's own auth on the raw request. */\nexport type SessionHook = (\n request: unknown,\n) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;\n\n/** Host hook for Mode B — validates submitted credentials from the built-in login page. */\nexport type LoginHook = (\n username: string,\n password: string,\n) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;\n\n/**\n * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per\n * session, so a DB round-trip here is cheap — this holds even under concurrent traffic: N parallel\n * requests carrying the same past-half-life cookie share ONE call (see `revalidateOnce` in\n * `session-cookie-io.ts`), not N. Return `false` to revoke (the cookie is cleared and the request\n * denied). Distinct from `session`: that hook reads the host's auth off a fresh request, which a\n * console XHR does not carry — this one receives the already-minted session.\n */\nexport type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;\n\n/** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */\nexport interface UnauthenticatedPageContext {\n /**\n * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`\n * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.\n * Cast it to whatever your app actually runs on.\n */\n request: unknown;\n /**\n * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,\n * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the\n * library falls back to its own built-in page rather than leaving the request hanging.\n */\n response: unknown;\n /** Where this console is mounted (e.g. `/durable`) — useful for a \"back to the console\" link. */\n basePath: string;\n}\n\n/**\n * Host-owned page for an unauthenticated full-page navigation to the console.\n *\n * The built-in page cannot know who hosts the console, so it can only say \"open this console from\n * your application\" in the abstract — it can't name the launcher, link to it, or look like the rest\n * of the host's product. This hook hands the whole response to the host instead: render through\n * your own template engine, your own Inertia page, whatever. The page is served AT the console's\n * own URL, so `/durable` stays `/durable`.\n *\n * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login\n * UI uses Mode A plus this hook, and posts to `<basePath>/session` from its own page — the mint\n * endpoint is the supported primitive for that.\n *\n * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook\n * that throws, or that returns without writing, falls back to the built-in page — it cannot let\n * anyone in.\n */\nexport type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;\n\nexport type AuthMode = 'session' | 'login';\n\n/**\n * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates\n * BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON\n * API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s\n * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Two ways to\n * mint that cookie: Mode A (`session`) — the host frontend, already carrying its own auth, POSTs\n * to `<basePath>/session` and the host hook decides; or Mode B (`login`) — the built-in,\n * dependency-free server-rendered login page (`GET <basePath>/login`). At least one of the two is\n * required so an un-mintable gate is a boot error, not a silently-open (or silently-stuck) console.\n */\nexport interface DashboardAuthOptions {\n /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */\n secret: string;\n /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */\n ttl?: string;\n /** Mode A: validates the host's own auth on the raw request POSTed to `<basePath>/session`;\n * return the session user, or `null` to deny. Thrown errors are treated as a denial (logged\n * once, never surfaced to the client). */\n session?: SessionHook;\n /** Mode B: validates submitted username/password from the built-in login page; return the\n * session user, or `null` to deny. Thrown errors are treated as a denial (logged once, never\n * surfaced to the client). */\n login?: LoginHook;\n /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot\n * mint a session, only revoke one already minted by `session`/`login`. */\n revalidate?: RevalidateHook;\n /** Renders the host's own page for an unauthenticated navigation, in place of the built-in\n * \"open this console from your application\" card; see `UnauthenticatedPageHook`. */\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\n/** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */\nexport interface ResolvedDashboardAuth {\n secret: string;\n ttlMs: number;\n modes: AuthMode[];\n session?: SessionHook;\n login?: LoginHook;\n revalidate?: RevalidateHook;\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\n/** DI token carrying the resolved `dashboardAuth` config (`ResolvedDashboardAuth | null`). */\nexport const DASHBOARD_AUTH = Symbol('DASHBOARD_AUTH');\n\nconst DEFAULT_TTL = '8h';\nconst DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;\nconst DURATION_UNITS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/** Parse a `'<number><s|m|h|d>'` duration to ms; falls back to the 8h default on a bad value. */\nfunction durationToMs(ttl: string): number {\n const match = /^(\\d+)([smhd])$/.exec(ttl.trim());\n if (!match) return DEFAULT_TTL_MS;\n const unit = DURATION_UNITS[match[2] ?? ''];\n if (unit === undefined) return DEFAULT_TTL_MS;\n return Number(match[1]) * unit;\n}\n\n/**\n * Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's\n * unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a\n * secret, or missing both a `session` and a `login` hook — the host learns immediately rather than\n * shipping an un-mintable gate.\n */\nexport function resolveDashboardAuth(\n options: DashboardAuthOptions | undefined,\n): ResolvedDashboardAuth | null {\n if (options === undefined) return null;\n if (typeof options.secret !== 'string' || options.secret === '') {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth.secret is required and must be a non-empty string ' +\n '(HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.',\n );\n }\n const modes: AuthMode[] = [];\n if (options.session !== undefined) {\n if (typeof options.session !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.session must be a function.');\n }\n modes.push('session');\n }\n if (options.login !== undefined) {\n if (typeof options.login !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.login must be a function.');\n }\n modes.push('login');\n }\n if (options.revalidate !== undefined && typeof options.revalidate !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.revalidate must be a function.');\n }\n if (\n options.unauthenticatedPage !== undefined &&\n typeof options.unauthenticatedPage !== 'function'\n ) {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth.unauthenticatedPage must be a function.',\n );\n }\n if (modes.length === 0) {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth needs at least one of `session` or `login` ' +\n '(otherwise the cookie can never be minted). Failing closed.',\n );\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),\n modes,\n ...(options.session !== undefined ? { session: options.session } : {}),\n ...(options.login !== undefined ? { login: options.login } : {}),\n ...(options.revalidate !== undefined ? { revalidate: options.revalidate } : {}),\n ...(options.unauthenticatedPage !== undefined\n ? { unauthenticatedPage: options.unauthenticatedPage }\n : {}),\n };\n}\n","/**\n * Shared HTML shell for the `dashboardAuth` server-rendered pages (the Mode B login page below and\n * `renderSessionRequiredPage`'s Mode A instruction page) — same dark zinc card, mono type, emerald\n * accent as `@dudousxd/nestjs-agent`'s dashboard login page, so every auth-adjacent page in this\n * family looks like one product. `content` is placed inside the `.card` div verbatim, so a caller\n * needing its own `<script>` (see `renderLoginPage`) can simply include it in `content` — a\n * `<script>` renders identically whether it's the div's last child or a body-level sibling.\n *\n * `content` is always caller-controlled static markup, never user input reflected into the page,\n * so this sidesteps HTML-escaping entirely.\n */\nfunction page(content: string, title = 'Durable'): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<meta name=\"robots\" content=\"noindex, nofollow\" />\n<title>${title}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #09090b;\n color: #e4e4e7;\n font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n padding: 16px;\n }\n .card {\n width: 100%;\n max-width: 384px;\n border: 1px solid #27272a;\n background: #18181b;\n border-radius: 8px;\n padding: 32px;\n box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5);\n }\n .brand {\n margin: 0 0 24px;\n text-align: center;\n font-size: 18px;\n font-weight: 600;\n color: #34d399;\n }\n h1 {\n margin: 0 0 12px;\n font-size: 16px;\n font-weight: 600;\n color: #f4f4f5;\n }\n p { margin: 0 0 16px; color: #a1a1aa; }\n form { display: flex; flex-direction: column; gap: 16px; }\n label { display: flex; flex-direction: column; gap: 6px; }\n .field-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #71717a;\n }\n input {\n border-radius: 4px;\n border: 1px solid #3f3f46;\n background: #09090b;\n color: #f4f4f5;\n padding: 8px 12px;\n font: inherit;\n outline: none;\n }\n input:focus { border-color: rgb(52 211 153 / 0.6); }\n #error {\n display: none;\n margin: 0;\n font-size: 12px;\n color: #fb7185;\n }\n button, .button {\n margin-top: 8px;\n border-radius: 4px;\n border: 1px solid rgb(52 211 153 / 0.4);\n background: rgb(52 211 153 / 0.1);\n color: #6ee7b7;\n padding: 8px 12px;\n font: inherit;\n font-size: 12px;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n cursor: pointer;\n display: inline-block;\n text-align: center;\n text-decoration: none;\n box-sizing: border-box;\n }\n button:hover:not(:disabled), .button:hover { background: rgb(52 211 153 / 0.2); }\n button:disabled { opacity: 0.6; cursor: default; }\n</style>\n</head>\n<body>\n <div class=\"card\">\n ${content}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * The built-in `dashboardAuth` login page (`GET <basePath>/login`, Mode B). Deliberately a small,\n * dependency-free, hand-authored HTML page — NOT part of the bundled React SPA — so gating the\n * dashboard shell doesn't require rebuilding or extending the Vite bundle with a new route/screen.\n *\n * `returnTo` and any error state are read CLIENT-SIDE from `location.search` (never server-echoed\n * into the HTML), so this function's only per-request-ish input is `basePath` — a developer-\n * controlled config value, not user input — and the page body is otherwise a static template.\n *\n * The submit flow (this page POSTs JSON via `fetch` and follows the JSON `redirectTo` it gets\n * back, rather than a classic form POST + server redirect) is why the markup keeps its own\n * `<script>` rather than adopting agent-dashboard's plain `<form method=\"post\">`.\n */\nexport function renderLoginPage(basePath: string): string {\n const loginAction = `${basePath}/login`;\n const defaultReturnTo = basePath;\n return page(\n `<p class=\"brand\">Durable</p>\n <form id=\"login-form\" autocomplete=\"on\">\n <label>\n <span class=\"field-label\">Username</span>\n <input id=\"username\" name=\"username\" type=\"text\" autocomplete=\"username\" required autofocus />\n </label>\n <label>\n <span class=\"field-label\">Password</span>\n <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" />\n </label>\n <p id=\"error\" role=\"alert\">Invalid username or password.</p>\n <button id=\"submit\" type=\"submit\">Sign in</button>\n </form>\n<script>\n(function () {\n var LOGIN_ACTION = ${JSON.stringify(loginAction)};\n var DEFAULT_RETURN_TO = ${JSON.stringify(defaultReturnTo)};\n var params = new URLSearchParams(window.location.search);\n var errorBox = document.getElementById('error');\n if (params.get('error')) errorBox.style.display = 'block';\n\n function sameOriginReturnTo(value) {\n if (typeof value !== 'string' || value === '') return DEFAULT_RETURN_TO;\n if (value.charAt(0) !== '/' || value.charAt(1) === '/' || value.indexOf('://') !== -1) {\n return DEFAULT_RETURN_TO;\n }\n return value;\n }\n\n var form = document.getElementById('login-form');\n var submitButton = document.getElementById('submit');\n form.addEventListener('submit', function (event) {\n event.preventDefault();\n submitButton.disabled = true;\n errorBox.style.display = 'none';\n var returnTo = sameOriginReturnTo(params.get('returnTo'));\n fetch(LOGIN_ACTION, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n credentials: 'same-origin',\n body: JSON.stringify({\n username: document.getElementById('username').value,\n password: document.getElementById('password').value,\n returnTo: returnTo,\n }),\n })\n .then(function (response) {\n if (!response.ok) throw new Error('unauthorized');\n return response.json();\n })\n .then(function (data) {\n window.location.href = data.redirectTo || DEFAULT_RETURN_TO;\n })\n .catch(function () {\n errorBox.style.display = 'block';\n submitButton.disabled = false;\n });\n });\n})();\n</script>`,\n 'Sign in — Durable',\n );\n}\n\n/**\n * Mode-A-only shell (`GET <basePath>/login` never renders under Mode A — this is what the page\n * guard serves in its place, see `session-required.exception.ts`): there is no form here because\n * the host mints the session. Mirrors `renderLoginPage`'s markup so the two pages look like one\n * product. `basePath` drives the Retry link's `href` — a plain `<a>` back to the guarded mount\n * (which re-runs the check once the host has minted a session), keeping this page free of any\n * inline `<script>`, unlike `renderLoginPage`'s fetch-driven form.\n */\nexport function renderSessionRequiredPage(basePath: string): string {\n return page(\n `<h1>Open this console from your application</h1>\n <p>Your session is minted by the host app. Use its console launcher to sign in, then reload.</p>\n <a class=\"button\" href=\"${basePath}\">Retry</a>`,\n );\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readHeaders(request: unknown): Record<string, unknown> {\n if (!isRecord(request)) return {};\n const headers = request.headers;\n return isRecord(headers) ? headers : {};\n}\n\n/** Coerce a single header (string or string[]) to a string, else undefined. */\nfunction headerToString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value) && value.every((part) => typeof part === 'string')) {\n return value.join(',');\n }\n return undefined;\n}\n\n/** Read the raw `Cookie` request header (Express and Fastify both expose `headers`). */\nexport function readCookieHeader(request: unknown): string | undefined {\n return headerToString(readHeaders(request).cookie);\n}\n\n/**\n * Decide whether the cookie should carry `Secure`. True when the request is https directly\n * (`request.protocol === 'https'`) OR a proxy forwarded https (`x-forwarded-proto` includes\n * 'https'). Defensive structural reads — never throws, defaults to insecure (works on plain http\n * in dev).\n */\nexport function isHttpsRequest(request: unknown): boolean {\n if (isRecord(request) && request.protocol === 'https') return true;\n const forwarded = headerToString(readHeaders(request)['x-forwarded-proto']);\n if (forwarded === undefined) return false;\n return forwarded\n .split(',')\n .map((proto) => proto.trim().toLowerCase())\n .includes('https');\n}\n\n/** The path + query a browser navigated to (Express's `originalUrl`, else the generic `url`). */\nexport function originalRequestUrl(request: unknown): string {\n if (!isRecord(request)) return '/';\n if (typeof request.originalUrl === 'string' && request.originalUrl !== '') {\n return request.originalUrl;\n }\n if (typeof request.url === 'string' && request.url !== '') return request.url;\n return '/';\n}\n\n/**\n * Guard a client-supplied redirect target against an open redirect: it must be a same-origin,\n * root-relative path (`/durable/foo`), never a protocol-relative (`//evil.com`) or absolute\n * (`https://evil.com`) URL. Falls back to `fallback` (typically the dashboard's own `basePath`)\n * for anything else, including a missing/non-string value.\n */\nexport function sanitizeReturnTo(candidate: unknown, fallback: string): string {\n if (typeof candidate !== 'string') return fallback;\n if (!candidate.startsWith('/')) return fallback;\n if (candidate.startsWith('//')) return fallback;\n if (candidate.includes('://')) return fallback;\n return candidate;\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\ninterface RawCookieResponse {\n getHeader(name: string): number | string | string[] | undefined;\n setHeader(name: string, value: string | string[]): unknown;\n}\n\nfunction isRawCookieResponse(value: unknown): value is RawCookieResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function'\n );\n}\n\n/**\n * Resolve the writable Node `ServerResponse` from a platform response. Express' response IS the\n * Node response; Fastify's reply wraps it on `.raw`. Returns the outer response when it already\n * exposes get/setHeader (Express), otherwise the unwrapped `.raw` (Fastify) — so a single write\n * path serves both without a platform-specific dependency.\n */\nfunction resolveCookieResponse(response: unknown): RawCookieResponse | null {\n if (isRawCookieResponse(response)) return response;\n if (isRecord(response) && isRawCookieResponse(response.raw)) return response.raw;\n return null;\n}\n\nfunction existingSetCookies(response: RawCookieResponse): string[] {\n const current = response.getHeader('set-cookie');\n if (Array.isArray(current)) return current;\n if (typeof current === 'string') return [current];\n return [];\n}\n\n/**\n * Append a `Set-Cookie` header to the response WITHOUT clobbering any cookies already queued by\n * the host. Platform-agnostic raw write — no-ops gracefully if the response can't be unwrapped.\n */\nexport function appendSetCookie(response: unknown, cookie: string): void {\n const raw = resolveCookieResponse(response);\n if (!raw) return;\n raw.setHeader('set-cookie', [...existingSetCookies(raw), cookie]);\n}\n\ninterface RawRedirectResponse extends RawCookieResponse {\n statusCode: number;\n end(chunk?: string): unknown;\n}\n\nfunction isRawRedirectResponse(value: unknown): value is RawRedirectResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function' &&\n typeof value.end === 'function'\n );\n}\n\nfunction resolveRedirectResponse(response: unknown): RawRedirectResponse | null {\n if (isRawRedirectResponse(response)) return response;\n if (isRecord(response) && isRawRedirectResponse(response.raw)) return response.raw;\n return null;\n}\n\n/**\n * Write a redirect directly on the raw response and END it. Used from the `DashboardLoginRedirect`\n * exception filter (guards can't use `@Redirect()` — that decorator only applies to a controller\n * handler's return value) and by the manual, non-passthrough logout handler. Bypassing Nest's\n * normal \"send the handler's return value\" pipeline this way is only safe when NOTHING downstream\n * (a filter, or Nest itself) will also try to write to the same response — both call sites here\n * are the terminal step of their request (an exception filter, or a `@Res()` non-passthrough\n * handler), so there is no second writer to race.\n */\nexport function redirectRaw(response: unknown, location: string, status = 302): void {\n const raw = resolveRedirectResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('location', location);\n raw.end();\n}\n\n/**\n * Did something already write to this response?\n *\n * Used to decide whether a host's `unauthenticatedPage` hook actually produced a page. A hook that\n * returns without writing (an early `return`, a forgotten `await`, a template that resolved to\n * nothing) would otherwise leave the request hanging forever — the browser spins until it times\n * out, with no error anywhere. Checking this lets the caller fall back to the built-in page.\n *\n * Reads the Node `ServerResponse.headersSent` through the same Express/Fastify unwrapping as the\n * writers above; Fastify's own `reply.sent` is not consulted because `.raw.headersSent` is true in\n * every case that matters here (the reply has been flushed to the socket).\n */\nexport function responseAlreadyWritten(response: unknown): boolean {\n const raw = resolveRedirectResponse(response) as\n | (RawRedirectResponse & { headersSent?: boolean })\n | null;\n return raw?.headersSent === true;\n}\n\n/**\n * Write a full HTML page directly on the raw response and END it. Used by\n * `DashboardSessionRequiredFilter` (the Mode-A-only instruction page a guard renders in place of a\n * login redirect that no longer exists) — same raw-response bypass, and the same reason it's safe:\n * an exception filter is the terminal step of the request, so there's no second writer to race.\n */\nexport function writeHtmlRaw(response: unknown, html: string, status = 200): void {\n const raw = resolveRedirectResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('content-type', 'text/html; charset=utf-8');\n // Same as the auth controller's `@Header('Cache-Control', ...)` on the sibling login page: this\n // page reflects live session state, so it must never be served stale from a cache.\n raw.setHeader('cache-control', 'no-store, must-revalidate');\n raw.end(html);\n}\n","/**\n * Parse a raw `Cookie` request header into a name→value map. Dependency-free so it works on raw\n * Express AND Fastify requests without `cookie-parser`. Values are URL-decoded; malformed\n * segments are skipped, never thrown.\n */\nexport function parseCookieHeader(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (typeof header !== 'string' || header === '') return cookies;\n for (const segment of header.split(';')) {\n const eq = segment.indexOf('=');\n if (eq <= 0) continue;\n const name = segment.slice(0, eq).trim();\n if (name === '') continue;\n let rawValue = segment.slice(eq + 1).trim();\n // Strip a single pair of wrapping double-quotes (RFC 6265 quoted form).\n if (rawValue.length >= 2 && rawValue.startsWith('\"') && rawValue.endsWith('\"')) {\n rawValue = rawValue.slice(1, -1);\n }\n // First occurrence wins; don't clobber an earlier value with a later dup.\n if (name in cookies) continue;\n try {\n cookies[name] = decodeURIComponent(rawValue);\n } catch {\n cookies[name] = rawValue;\n }\n }\n return cookies;\n}\n\nexport interface SetCookieOptions {\n /** Cookie `Path`. The dashboard scopes its session cookie to `/` — see `session-cookie-io.ts`\n * for why (the UI mount and the JSON API mount can be configured to live at unrelated paths). */\n path: string;\n /** Cookie lifetime in seconds. Ignored (and forced to 0) when `clear` is set. */\n maxAgeSeconds: number;\n /** Add `Secure` when the request is https. */\n secure: boolean;\n /** Clear the cookie: empty value + `Max-Age=0` (and `Expires` in the past). */\n clear?: boolean;\n}\n\n/**\n * Serialize a `Set-Cookie` header value. Always `HttpOnly` + `SameSite=Lax` (Lax blocks\n * cross-site `POST`s carrying the cookie — CSRF coverage for the login/logout endpoints).\n * Platform-agnostic: just a string.\n */\nexport function serializeSetCookie(name: string, value: string, options: SetCookieOptions): string {\n const parts = [`${name}=${options.clear ? '' : encodeURIComponent(value)}`];\n parts.push(`Path=${options.path}`);\n parts.push('HttpOnly');\n parts.push('SameSite=Lax');\n if (options.secure) parts.push('Secure');\n if (options.clear) {\n parts.push('Max-Age=0');\n parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');\n } else {\n parts.push(`Max-Age=${options.maxAgeSeconds}`);\n }\n return parts.join('; ');\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Clock-skew grace applied to `exp` so a marginally-late clock doesn't bounce a valid cookie. */\nconst EXP_GRACE_MS = 30_000;\n\n/** The validated dashboard session attached to a request once its cookie verifies. */\nexport interface DashboardSession {\n /** Stable user id (the session user's `id`). */\n sub: string;\n /** Optional display name. */\n name?: string;\n /** Free-form role strings; the dashboard does not interpret them. */\n roles: string[];\n /** Issued-at, epoch milliseconds. */\n iat: number;\n /** Expiry, epoch milliseconds. */\n exp: number;\n}\n\n/** The session user the host `login` hook returns to mint a cookie. */\nexport interface DashboardSessionUser {\n id: string;\n name?: string;\n roles?: string[];\n}\n\nexport interface SignOptions {\n secret: string;\n ttlMs: number;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nexport interface VerifyOptions {\n secret: string;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nfunction base64urlEncode(value: string): string {\n return Buffer.from(value, 'utf8').toString('base64url');\n}\n\nfunction base64urlDecode(value: string): string {\n return Buffer.from(value, 'base64url').toString('utf8');\n}\n\nfunction hmac(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('base64url');\n}\n\n/**\n * Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless\n * HMAC-SHA256, no store, `node:crypto` only — no JWT dependency. Mirrors\n * `@dudousxd/nestjs-telescope`'s `dashboardAuth` cookie mechanism so the behavior is familiar\n * across the aviary consoles.\n */\nexport function signSessionCookie(user: DashboardSessionUser, options: SignOptions): string {\n const issuedAt = options.now ?? Date.now();\n const session: DashboardSession = {\n sub: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n iat: issuedAt,\n exp: issuedAt + options.ttlMs,\n };\n const encodedPayload = base64urlEncode(JSON.stringify(session));\n const signature = hmac(options.secret, encodedPayload);\n return `${encodedPayload}.${signature}`;\n}\n\n/** Type guard for the decoded payload shape — defends against tampered/legacy payloads. */\nfunction isSessionPayload(value: unknown): value is DashboardSession {\n if (typeof value !== 'object' || value === null) return false;\n const record: Record<string, unknown> = { ...value };\n if (typeof record.sub !== 'string') return false;\n if (record.name !== undefined && typeof record.name !== 'string') return false;\n if (!Array.isArray(record.roles)) return false;\n if (!record.roles.every((role) => typeof role === 'string')) return false;\n if (typeof record.iat !== 'number' || !Number.isFinite(record.iat)) return false;\n if (typeof record.exp !== 'number' || !Number.isFinite(record.exp)) return false;\n return true;\n}\n\n/**\n * Verify a cookie value and return the session, or `null` for anything that's tampered,\n * malformed, or expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER\n * throws — any parse failure yields `null`.\n */\nexport function verifySessionCookie(\n value: string,\n options: VerifyOptions,\n): DashboardSession | null {\n try {\n const dot = value.indexOf('.');\n if (dot <= 0 || dot === value.length - 1) return null;\n const encodedPayload = value.slice(0, dot);\n const providedSignature = value.slice(dot + 1);\n const expectedSignature = hmac(options.secret, encodedPayload);\n const provided = Buffer.from(providedSignature, 'base64url');\n const expected = Buffer.from(expectedSignature, 'base64url');\n // timingSafeEqual throws on a length mismatch — guard it so a wrong-length (tampered)\n // signature returns null instead of throwing.\n if (provided.length !== expected.length) return null;\n if (!timingSafeEqual(provided, expected)) return null;\n const decoded: unknown = JSON.parse(base64urlDecode(encodedPayload));\n if (!isSessionPayload(decoded)) return null;\n const now = options.now ?? Date.now();\n if (now > decoded.exp + EXP_GRACE_MS) return null;\n const session: DashboardSession = {\n sub: decoded.sub,\n ...(decoded.name !== undefined ? { name: decoded.name } : {}),\n roles: decoded.roles,\n iat: decoded.iat,\n exp: decoded.exp,\n };\n return session;\n } catch {\n return null;\n }\n}\n","import { parseCookieHeader, serializeSetCookie } from './cookie-header.js';\nimport type { ResolvedDashboardAuth } from './dashboard-auth-config.js';\nimport { isHttpsRequest, readCookieHeader } from './request.js';\nimport { appendSetCookie } from './response.js';\nimport {\n type DashboardSession,\n type DashboardSessionUser,\n signSessionCookie,\n verifySessionCookie,\n} from './session-cookie.js';\n\n/** Cookie name carrying the signed dashboard session. */\nexport const SESSION_COOKIE_NAME = 'durable_dashboard_session';\n\n/**\n * `Path=/`, deliberately not scoped to `basePath`/`apiBasePath`: the two mounts are independently\n * configurable (`DurableDashboardModule.forRoot({ basePath, apiBasePath })`) and can live at\n * unrelated paths (e.g. `/durable` UI + `/api/durable` API) — a cookie scoped to either one alone\n * would not reach the other. The cookie is `HttpOnly` + signed + short-lived, so the broader scope\n * is a reasonable trade for correctness across any mount configuration.\n */\nconst COOKIE_PATH = '/';\n\n/**\n * Sign a fresh session for `user` and append it as a `Set-Cookie` on the response, `Secure` when\n * the request is https.\n */\nexport function issueSessionCookie(\n user: DashboardSessionUser,\n context: {\n auth: ResolvedDashboardAuth;\n request: unknown;\n response: unknown;\n now?: number;\n },\n): void {\n const value = signSessionCookie(user, {\n secret: context.auth.secret,\n ttlMs: context.auth.ttlMs,\n ...(context.now !== undefined ? { now: context.now } : {}),\n });\n const cookie = serializeSetCookie(SESSION_COOKIE_NAME, value, {\n path: COOKIE_PATH,\n maxAgeSeconds: Math.floor(context.auth.ttlMs / 1000),\n secure: isHttpsRequest(context.request),\n });\n appendSetCookie(context.response, cookie);\n}\n\n/** Append a cookie-clearing `Set-Cookie` (Max-Age=0). */\nexport function clearSessionCookie(context: { request: unknown; response: unknown }): void {\n const cookie = serializeSetCookie(SESSION_COOKIE_NAME, '', {\n path: COOKIE_PATH,\n maxAgeSeconds: 0,\n secure: isHttpsRequest(context.request),\n clear: true,\n });\n appendSetCookie(context.response, cookie);\n}\n\n/** Read + verify the session cookie on `request`, or `null` when absent/tampered/expired. */\nexport function readSessionFromRequest(\n auth: ResolvedDashboardAuth,\n request: unknown,\n): DashboardSession | null {\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return null;\n return verifySessionCookie(cookieValue, { secret: auth.secret });\n}\n\n/**\n * In-flight `revalidate` calls, keyed by `<secret>:<sub>` (the secret disambiguates two\n * `dashboardAuth` configs that happen to share a process, e.g. tests) — joined, not repeated, by\n * every concurrent renewal of the SAME session. Entries are removed the moment their call settles\n * (`finally`, below), so this only ever holds calls genuinely in flight right now: it can't grow\n * without bound, and a throwing hook can't wedge a session's future renewals.\n */\nconst inFlightRevalidations = new Map<string, Promise<boolean>>();\n\n/**\n * Run `auth.revalidate` for `user`, joining an already-in-flight call for the same session instead\n * of starting a second one. This is what makes the `RevalidateHook` doc's \"runs at most once per\n * `ttl/2` per session\" true under real traffic: a single console page load fires several parallel\n * API calls, and without this every one of them carrying the same past-half-life cookie would\n * independently call the host's hook before the first renewal's fresh cookie lands. Every caller\n * still awaits the SAME outcome and applies it to its OWN request/response — only the host round\n * trip is shared.\n */\nfunction revalidateOnce(auth: ResolvedDashboardAuth, user: DashboardSessionUser): Promise<boolean> {\n const revalidate = auth.revalidate;\n if (!revalidate) return Promise.resolve(true);\n const key = `${auth.secret}:${user.id}`;\n const inFlight = inFlightRevalidations.get(key);\n if (inFlight) return inFlight;\n const call = Promise.resolve()\n .then(() => revalidate(user))\n // Fail closed: a throwing hook revokes rather than silently extending the session.\n .catch(() => false)\n .finally(() => {\n // Cleared on BOTH outcomes (the `.catch` above means this only ever sees a resolution, never\n // a rejection) so a later, non-concurrent renewal cycle calls the hook fresh rather than\n // reusing a stale result — and so the map never accumulates settled entries.\n inFlightRevalidations.delete(key);\n });\n inFlightRevalidations.set(key, call);\n return call;\n}\n\n/**\n * Sliding renewal + revalidation. When a valid cookie is past 50% of its TTL, re-issue a fresh one\n * so an active session never expires mid-use — but first give the host's `revalidate` hook a say,\n * so a deactivated or demoted user loses access instead of riding a self-renewing cookie forever.\n * Concurrent renewals of the same session share one `revalidate` call (see `revalidateOnce`).\n *\n * Returns `false` when the session was revoked (the clearing `Set-Cookie` is already queued and the\n * caller must deny the request); `true` otherwise, including when no renewal was due.\n */\nexport async function maybeRenewSession(\n auth: ResolvedDashboardAuth,\n session: DashboardSession,\n request: unknown,\n response: unknown,\n now: number = Date.now(),\n): Promise<boolean> {\n if (now - session.iat <= auth.ttlMs / 2) return true;\n const user: DashboardSessionUser = {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n };\n const allowed = await revalidateOnce(auth, user);\n if (!allowed) {\n clearSessionCookie({ request, response });\n return false;\n }\n issueSessionCookie(user, { auth, request, response, now });\n return true;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n UseFilters,\n} from '@nestjs/common';\nimport { DashboardLoginRedirectFilter } from './auth/login-redirect.exception.js';\nimport { DashboardSessionRequiredFilter } from './auth/session-required.exception.js';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n *\n * `@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)` is permanent (present\n * regardless of whether `dashboardAuth` is configured): each only ever activates for its own\n * exception type, which `DurableUiSessionGuard` only throws when it is ALSO stamped on this\n * controller (see `DurableDashboardModule.forRoot`) — so leaving them decorated here is inert, not\n * a behavior change, when `dashboardAuth` is absent. The guard picks between the two exceptions\n * based on which mode is configured (Mode B => redirect to the login page; Mode A only => render\n * the session-required instruction page in place, since there is no login page to redirect to).\n */\n@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n","import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';\nimport { Catch, HttpException, HttpStatus } from '@nestjs/common';\nimport { redirectRaw } from './response.js';\n\n/**\n * Thrown by `DurableUiSessionGuard` when a page-level (full-page navigation) request has no valid\n * `dashboardAuth` session — carries the login URL to redirect to. Guards can't use Nest's\n * `@Redirect()` decorator (that only applies to a controller handler's return value), so the\n * guard throws this and a dedicated, controller-scoped `DashboardLoginRedirectFilter` (see below)\n * turns it into a real `302`. This is the ONLY safe way to redirect from a guard: writing directly\n * to the response inside `canActivate` and then returning `false` risks a double-write (Nest still\n * tries to send a `ForbiddenException` response afterwards) — routing through the normal\n * exception-filter pipeline instead guarantees exactly one writer finalizes the response.\n */\nexport class DashboardLoginRedirectException extends HttpException {\n constructor(public readonly location: string) {\n super({ location }, HttpStatus.FOUND);\n }\n}\n\n/** Turns a `DashboardLoginRedirectException` into a real `302 Location: <location>` response. */\n@Catch(DashboardLoginRedirectException)\nexport class DashboardLoginRedirectFilter implements ExceptionFilter {\n catch(exception: DashboardLoginRedirectException, host: ArgumentsHost): void {\n const response = host.switchToHttp().getResponse();\n redirectRaw(response, exception.location);\n }\n}\n","import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';\nimport { Catch, HttpException, HttpStatus, Inject, Logger, Optional } from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './dashboard-auth-config.js';\nimport { renderSessionRequiredPage } from './login-page.js';\nimport { responseAlreadyWritten, writeHtmlRaw } from './response.js';\n\n/**\n * Thrown by `DurableUiSessionGuard` when a page-level (full-page navigation) request has no valid\n * `dashboardAuth` session AND Mode B (`login`) isn't configured — there is no login page to\n * redirect to (see `DashboardLoginRedirectException`, its Mode-B sibling). Carries `basePath` so\n * the filter below can render the Mode-A instruction page in its place. Same reasoning as the\n * login redirect for why a guard can't just write the response directly and return `false`: a\n * dedicated, controller-scoped `DashboardSessionRequiredFilter` (see below) is the only writer,\n * guaranteeing no double-write race.\n */\nexport class DashboardSessionRequiredException extends HttpException {\n constructor(public readonly basePath: string) {\n super({ basePath }, HttpStatus.UNAUTHORIZED);\n }\n}\n\n/**\n * Renders the Mode-A page for a denied navigation: the host's own `unauthenticatedPage` when one is\n * configured, otherwise the built-in instruction card.\n *\n * `@Optional()` on the injection is load-bearing. This filter is decorated onto\n * `DurableUiController` unconditionally (see that controller's docblock — it is inert when\n * `dashboardAuth` is absent, because the guard that throws its exception is only stamped on when\n * auth IS configured), so it must still construct in a module where `DASHBOARD_AUTH` was never\n * provided.\n */\n@Catch(DashboardSessionRequiredException)\nexport class DashboardSessionRequiredFilter implements ExceptionFilter {\n private readonly logger = new Logger(DashboardSessionRequiredFilter.name);\n private warned = false;\n\n constructor(\n @Optional()\n @Inject(DASHBOARD_AUTH)\n private readonly auth: ResolvedDashboardAuth | null = null,\n ) {}\n\n async catch(exception: DashboardSessionRequiredException, host: ArgumentsHost): Promise<void> {\n const http = host.switchToHttp();\n const response = http.getResponse();\n const hook = this.auth?.unauthenticatedPage;\n\n if (hook) {\n try {\n await hook({ request: http.getRequest(), response, basePath: exception.basePath });\n // The hook owns the response; if it wrote one, we are done.\n if (responseAlreadyWritten(response)) return;\n // It did not write. Falling through to the built-in page is the only outcome that neither\n // hangs the request forever nor invents a page the host did not ask for — but it is almost\n // certainly a bug in the hook, so say so.\n this.warnOnce(\n 'dashboardAuth.unauthenticatedPage returned without writing a response; falling back to ' +\n 'the built-in page. The hook must write AND end the response it is given.',\n );\n } catch (error) {\n // A broken host page must not turn a denial into a 500, and must never open the console.\n this.warnOnce(\n `dashboardAuth.unauthenticatedPage threw; falling back to the built-in page. ${String(error)}`,\n );\n // It may have written headers before throwing, in which case there is nothing left to do:\n // writing again would raise ERR_HTTP_HEADERS_SENT on top of the original failure.\n if (responseAlreadyWritten(response)) return;\n }\n }\n\n writeHtmlRaw(response, renderSessionRequiredPage(exception.basePath), HttpStatus.UNAUTHORIZED);\n }\n\n /** Once per process, not once per request: a broken hook fires on every denied navigation. */\n private warnOnce(message: string): void {\n if (this.warned) return;\n this.warned = true;\n this.logger.warn(message);\n }\n}\n","import 'reflect-metadata';\nimport {\n type CanActivate,\n type DynamicModule,\n type InjectionToken,\n Module,\n type OptionalFactoryDependency,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport {\n DASHBOARD_AUTH,\n type DashboardAuthOptions,\n resolveDashboardAuth,\n} from './auth/dashboard-auth-config.js';\nimport { DashboardLoginRedirectFilter } from './auth/login-redirect.exception.js';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport { DurableAuthController } from './durable-auth.controller.js';\nimport { DurableApiSessionGuard, DurableUiSessionGuard } from './durable-session.guard.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. A drift spec imports the\n * real constant (via the resolvable `'@nestjs/common/constants.js'` subpath) and asserts this literal\n * stays byte-identical to it. Mirrors `@dudousxd/nestjs-agent-dashboard`'s `agent-dashboard.module.ts`.\n */\nconst GUARDS_METADATA = '__guards__';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n /**\n * Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at\n * `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key\n * — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior\n * call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the\n * host fronts them another way, e.g. a global guard or reverse-proxy auth).\n *\n * A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n *\n * Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch\n * from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like\n * any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page\n * navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so\n * there is no custom header to read: only whatever ambient credential the browser sends by default\n * (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header\n * will 403 the page shell while the API calls it makes right after would have passed — put session\n * auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the\n * shell itself gated.\n */\n guards?: Type<CanActivate>[];\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.\n */\n imports?: DynamicModule['imports'];\n /**\n * Gate the console (the SPA at `basePath` AND its JSON API at `apiBasePath`) behind a built-in,\n * signed session cookie — no infra required. Two ways to mint it:\n *\n * - **Mode A (`session`, recommended)**: your host app already has its own auth (SSO/OIDC/\n * whatever) — the host frontend, carrying that auth, POSTs to `<basePath>/session`, your\n * `session` hook validates the raw request and returns the session user, and this library mints\n * its cookie from that. No credential this library understands ever exists; you keep your own\n * identity provider as the source of truth. A page-level request with no valid session (and only\n * Mode A configured) gets a small server-rendered instruction page telling the visitor to sign in\n * through your host app, since there is no login page to redirect to.\n * - **Mode B (`login`, standalone fallback)**: no host frontend/IdP to lean on — the durable\n * dashboard serves its own small, dependency-free, server-rendered login page (`GET\n * <basePath>/login`, handled by `DurableAuthController`) and your `login` hook validates\n * submitted username/password. A missing/invalid/expired session redirects a page-level request\n * to that login page with `?returnTo=<original url>`.\n *\n * Either mode can be used alone, or both together (e.g. Mode A for normal use, Mode B as a\n * break-glass fallback) — at least one is required, or `forRoot`/`forRootAsync` throws at boot\n * (an un-mintable gate is a boot error, not a silently-open or silently-stuck console). An API\n * request (`apiBasePath`, fetched by the SPA's own JS) always gets a plain `401` on a missing\n * session, regardless of mode — the caller reads the status code, not HTML.\n *\n * A third, optional hook — **`revalidate`** — re-checks an already-minted session rather than\n * minting one: once a session cookie is past 50% of its TTL, the guard slides it forward with a\n * fresh one, and if `revalidate` is set it's consulted first, so a deactivated or demoted\n * operator loses access instead of riding a self-renewing cookie indefinitely. It only runs on\n * that renewal path, not on every request, so revocation has **latency, not immediacy**: an\n * operator revoked right after a renewal keeps their existing cookie — and therefore console\n * access — for up to `ttl/2` (4 hours at the default 8h TTL) before the next renewal check catches\n * it. Do not rely on `revalidate` for a same-second cutoff; front the mount with your own guard\n * (see {@link guards}) if you need that.\n *\n * Auth here is mount-level and role-agnostic: this option (and the guards/controller it wires up)\n * has no notion of control-plane vs tenant role (see the durable dashboard's topology note in\n * `dashboard.service.ts`) — nothing role-specific changes based on who logs in.\n *\n * Composes with {@link guards}: when both are set, a request must pass the built-in session check\n * AND every guard in `guards` — the SAME logical-AND semantics `@UseGuards` already gives multiple\n * guards, since the built-in guard is simply prepended to whatever `guards` you pass. Omit to leave\n * the console open (today's behavior, unchanged byte-for-byte) — front it with `guards`, a global\n * guard, or a reverse proxy instead.\n *\n * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`session`/`login`/`revalidate` shape,\n * and `forRootAsync` / {@link DurableDashboardAsyncOptions.useDashboardAuth} when a hook needs DI\n * (e.g. an `EntityManager` to look up an admin user).\n */\n dashboardAuth?: DashboardAuthOptions;\n}\n\n/**\n * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the\n * router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`\n * already has), but `dashboardAuth` is built by an injected factory, so its `session`/`login` hooks\n * can reach your DB/services — e.g. Mode A's `session` hook calling out to the same auth service\n * your host app already uses to validate the raw request. Returning `undefined` from\n * `useDashboardAuth` leaves the console open, exactly like omitting `dashboardAuth` from `forRoot`.\n */\nexport interface DurableDashboardAsyncOptions {\n basePath?: string;\n apiBasePath?: string;\n guards?: Type<CanActivate>[];\n imports?: DynamicModule['imports'];\n /** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n /** Build the `dashboardAuth` config from injected deps (or `undefined` to leave the console open). */\n useDashboardAuth: (\n ...deps: any[]\n ) => DashboardAuthOptions | undefined | Promise<DashboardAuthOptions | undefined>;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/**\n * Stamp (or clear) `@UseGuards`-equivalent metadata on a dashboard controller — REPLACE, not\n * append, across repeated calls (a second `forRoot(...)` overwrites whatever a prior call\n * stamped). `builtIn`, when given, is always prepended FIRST — it's the `dashboardAuth` session\n * guard for that controller, which must run before any host `guards` (fail the session check\n * before ever invoking host auth). Passing neither reproduces today's behavior byte-for-byte\n * (an empty array — the controller unguarded).\n */\nfunction stampGuards(\n hostGuards: Type<CanActivate>[] | undefined,\n builtIn: Type<CanActivate> | undefined,\n controller: Type,\n): void {\n const merged = [...(builtIn ? [builtIn] : []), ...(hostGuards ?? [])];\n Reflect.defineMetadata(GUARDS_METADATA, merged, controller);\n}\n\n/**\n * Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:\n * guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper\n * — must carry the guard classes as providers plus the host's `imports` that resolve their\n * dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with \"Nest can't\n * resolve dependencies ... in the DurableApiModule context\" even when the host passed the right\n * `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).\n */\n@Module({})\nexport class DurableApiModule {\n static register(options: {\n imports?: DynamicModule['imports'];\n guards?: Type<CanActivate>[];\n /** Provider for `DASHBOARD_AUTH` — a `useValue` (`forRoot`) or a `useFactory` (`forRootAsync`). */\n authProvider: Provider;\n /** Whether the built-in `DurableApiSessionGuard` should be stamped on `DurableApiController`\n * (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's\n * resolved at runtime and is a no-op if it ends up unconfigured). */\n authMayBeConfigured: boolean;\n }): DynamicModule {\n stampGuards(\n options.guards,\n options.authMayBeConfigured ? DurableApiSessionGuard : undefined,\n DurableApiController,\n );\n return {\n module: DurableApiModule,\n imports: [...(options.imports ?? [])],\n controllers: [DurableApiController],\n providers: [\n DashboardService,\n options.authProvider,\n DurableApiSessionGuard,\n ...(options.guards ?? []),\n ],\n exports: [DashboardService],\n };\n }\n}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` (or `forRootAsync`\n * when `dashboardAuth`'s `login` hook needs injected services) alongside `DurableModule` (global),\n * so it resolves the engine and store. Front the routes with the first-class `guards` option\n * (plus `imports` for the guards' own dependencies) — see {@link DurableDashboardOptions.guards} —\n * or with the built-in `dashboardAuth` cookie login, or both.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const authProvider: Provider = {\n provide: DASHBOARD_AUTH,\n useValue: resolveDashboardAuth(options.dashboardAuth),\n };\n return DurableDashboardModule.build(options, authProvider, options.dashboardAuth !== undefined);\n }\n\n static forRootAsync(options: DurableDashboardAsyncOptions): DynamicModule {\n const authProvider: Provider = {\n provide: DASHBOARD_AUTH,\n inject: options.inject ?? [],\n useFactory: async (...deps: any[]) =>\n resolveDashboardAuth(await options.useDashboardAuth(...deps)),\n };\n // Always true: whether the resolved config ends up configured is only known at runtime, so the\n // guards/auth-controller wiring must always be present — `DurableUiSessionGuard`/\n // `DurableApiSessionGuard` are already no-ops when `DASHBOARD_AUTH` resolves to `null`.\n return DurableDashboardModule.build(options, authProvider, true);\n }\n\n /** Shared wiring: static routing + the API module, with `dashboardAuth` supplied by `authProvider`. */\n private static build(\n options: {\n basePath?: string;\n apiBasePath?: string;\n guards?: Type<CanActivate>[];\n imports?: DynamicModule['imports'];\n },\n authProvider: Provider,\n authMayBeConfigured: boolean,\n ): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n stampGuards(\n options.guards,\n authMayBeConfigured ? DurableUiSessionGuard : undefined,\n DurableUiController,\n );\n return {\n module: DurableDashboardModule,\n imports: [\n ...(options.imports ?? []),\n // Guards + host imports must reach the API controller's HOST module — enhancers resolve\n // from their controller's own module, never from a parent (see DurableApiModule.register).\n // Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.\n DurableApiModule.register({\n ...(options.imports ? { imports: options.imports } : {}),\n ...(options.guards ? { guards: options.guards } : {}),\n authProvider,\n authMayBeConfigured,\n }),\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI + auth controllers below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [\n DurableUiController,\n // The auth controller (login page + logout) only exists when dashboardAuth might be\n // configured — mounting it unconditionally would add a new `/login` route even for hosts\n // that never set `dashboardAuth`, breaking \"absent -> today's behavior byte-for-byte\".\n ...(authMayBeConfigured ? [DurableAuthController] : []),\n ],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n authProvider,\n DurableUiSessionGuard,\n // DurableUiController (and DurableAuthController, when present) are hosted HERE, so their\n // guards DI-instantiate from this module.\n ...(options.guards ?? []),\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './auth/dashboard-auth-config.js';\nimport { DashboardLoginRedirectException } from './auth/login-redirect.exception.js';\nimport { originalRequestUrl } from './auth/request.js';\nimport { maybeRenewSession, readSessionFromRequest } from './auth/session-cookie-io.js';\nimport { DashboardSessionRequiredException } from './auth/session-required.exception.js';\nimport { DASHBOARD_BASE_PATH } from './durable-ui.controller.js';\n\n/**\n * Gates `DurableUiController` (the SPA shell + assets, a full-page browser navigation) on a valid\n * `dashboardAuth` session cookie. A no-op (always `true`) when `dashboardAuth` was not configured\n * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session,\n * when Mode B (`login`) is configured, gets a `302` to the built-in login page carrying\n * `?returnTo=<original url>`, via `DashboardLoginRedirectException` (see that file for why a guard\n * can't just call `response.redirect()` and return `false`). When only Mode A (`session`) is\n * configured there is no login page to redirect to — the host mints the session itself — so it\n * instead throws `DashboardSessionRequiredException`, rendering a small instruction page.\n *\n * Auth is mount-level and role-agnostic: this guard has no notion of control-plane vs tenant\n * (see the durable dashboard's topology note in `dashboard.service.ts`) — it only ever answers\n * \"is there a valid dashboard session\", nothing role-specific changes here.\n */\n@Injectable()\nexport class DurableUiSessionGuard implements CanActivate {\n constructor(\n @Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null,\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const auth = this.auth;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(auth, request);\n if (!session) {\n this.denyUnauthenticated(auth, request);\n }\n if (!(await maybeRenewSession(auth, session, request, http.getResponse()))) {\n this.denyUnauthenticated(auth, request);\n }\n return true;\n }\n\n /**\n * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent\n * one — by design, there is nothing left to distinguish \"never had a session\" from \"had one,\n * then lost it\" once the cookie is cleared, so both get the same Mode-aware treatment.\n *\n * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early\n * return narrows the field within that method only — TS doesn't carry it across a method call\n * — so callers pass the already-narrowed value instead of this method re-deriving (or\n * asserting away) the nullability.\n */\n private denyUnauthenticated(auth: ResolvedDashboardAuth, request: unknown): never {\n if (!auth.modes.includes('login')) throw new DashboardSessionRequiredException(this.basePath);\n const returnTo = encodeURIComponent(originalRequestUrl(request));\n throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);\n }\n}\n\n/**\n * Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session\n * cookie. A no-op (always `true`) when `dashboardAuth` was not configured. Missing/invalid/expired\n * session => a plain `401` (the API is called via `fetch`, never a browser navigation, so a\n * redirect would just fail the request anyway — the caller reads the status code, not HTML).\n *\n * Mount-level and role-agnostic, same as `DurableUiSessionGuard` above.\n */\n@Injectable()\nexport class DurableApiSessionGuard implements CanActivate {\n constructor(@Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const auth = this.auth;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(auth, request);\n if (!session) throw this.unauthorized(auth);\n if (!(await maybeRenewSession(auth, session, request, http.getResponse()))) {\n // Revoked mid-session: same treatment as an absent cookie.\n throw this.unauthorized(auth);\n }\n return true;\n }\n\n /**\n * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s\n * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console\n * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session\n * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.\n */\n private unauthorized(auth: ResolvedDashboardAuth): UnauthorizedException {\n return new UnauthorizedException({ auth: { modes: auth.modes } });\n }\n}\n"],"mappings":"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","names":["RunGateway","STATE_STORE_CANONICAL","WorkflowEngine","collectMetrics","Inject","Injectable","Optional","Observable","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","Body","Controller","Get","Header","NotFoundException","Param","Post","Query","Sse","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","DurableApiController","dashboard","runs","status","workflow","tag","attr","namespace","origin","listRuns","attributes","parseAttrFilters","undefined","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","BadRequestException","Body","Controller","Get","Header","HttpCode","Inject","Logger","NotFoundException","Optional","Post","Req","Res","UnauthorizedException","DASHBOARD_AUTH","Symbol","DEFAULT_TTL","DEFAULT_TTL_MS","DURATION_UNITS","s","m","h","d","durationToMs","ttl","match","exec","trim","unit","undefined","Number","resolveDashboardAuth","options","secret","Error","modes","session","push","login","revalidate","unauthenticatedPage","length","ttlMs","page","content","title","renderLoginPage","basePath","loginAction","defaultReturnTo","JSON","stringify","renderSessionRequiredPage","isRecord","value","readHeaders","request","headers","headerToString","Array","isArray","every","part","join","undefined","readCookieHeader","cookie","isHttpsRequest","protocol","forwarded","split","map","proto","trim","toLowerCase","includes","originalRequestUrl","originalUrl","url","sanitizeReturnTo","candidate","fallback","startsWith","isRecord","value","isRawCookieResponse","getHeader","setHeader","resolveCookieResponse","response","raw","existingSetCookies","current","Array","isArray","appendSetCookie","cookie","isRawRedirectResponse","end","resolveRedirectResponse","redirectRaw","location","status","statusCode","responseAlreadyWritten","headersSent","writeHtmlRaw","html","parseCookieHeader","header","cookies","segment","split","eq","indexOf","name","slice","trim","rawValue","length","startsWith","endsWith","decodeURIComponent","serializeSetCookie","value","options","parts","clear","encodeURIComponent","push","path","secure","maxAgeSeconds","join","createHmac","timingSafeEqual","EXP_GRACE_MS","base64urlEncode","value","Buffer","from","toString","base64urlDecode","hmac","secret","payload","createHmac","update","digest","signSessionCookie","user","options","issuedAt","now","Date","session","sub","id","name","undefined","roles","iat","exp","ttlMs","encodedPayload","JSON","stringify","signature","isSessionPayload","record","Array","isArray","every","role","Number","isFinite","verifySessionCookie","dot","indexOf","length","slice","providedSignature","expectedSignature","provided","expected","timingSafeEqual","decoded","parse","SESSION_COOKIE_NAME","COOKIE_PATH","issueSessionCookie","user","context","value","signSessionCookie","secret","auth","ttlMs","now","undefined","cookie","serializeSetCookie","path","maxAgeSeconds","Math","floor","secure","isHttpsRequest","request","appendSetCookie","response","clearSessionCookie","clear","readSessionFromRequest","cookieValue","parseCookieHeader","readCookieHeader","verifySessionCookie","inFlightRevalidations","Map","revalidateOnce","revalidate","Promise","resolve","key","id","inFlight","get","call","then","catch","finally","delete","set","maybeRenewSession","session","Date","iat","sub","name","roles","allowed","existsSync","readFileSync","basename","extname","join","resolve","sep","fileURLToPath","Controller","Get","Header","Inject","NotFoundException","Param","StreamableFile","UseFilters","Catch","HttpException","HttpStatus","DashboardLoginRedirectException","HttpException","location","HttpStatus","FOUND","DashboardLoginRedirectFilter","catch","exception","host","response","switchToHttp","getResponse","redirectRaw","Catch","HttpException","HttpStatus","Inject","Logger","Optional","DashboardSessionRequiredException","HttpException","basePath","HttpStatus","UNAUTHORIZED","DashboardSessionRequiredFilter","logger","Logger","name","warned","auth","catch","exception","host","http","switchToHttp","response","getResponse","hook","unauthenticatedPage","request","getRequest","responseAlreadyWritten","warnOnce","error","String","writeHtmlRaw","renderSessionRequiredPage","message","warn","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","url","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","isString","value","DurableAuthController","logger","Logger","name","warnedHooks","Set","auth","basePath","loginPage","requireAuth","modes","includes","NotFoundException","renderLoginPage","login","body","request","response","username","password","BadRequestException","user","runHook","UnauthorizedException","message","issueSessionCookie","redirectTo","sanitizeReturnTo","returnTo","session","logout","clearSessionCookie","redirectRaw","kind","run","error","has","add","warn","Error","String","passthrough","Module","RouterModule","Inject","Injectable","UnauthorizedException","DurableUiSessionGuard","auth","basePath","canActivate","context","http","switchToHttp","request","getRequest","session","readSessionFromRequest","denyUnauthenticated","maybeRenewSession","getResponse","modes","includes","DashboardSessionRequiredException","returnTo","encodeURIComponent","originalRequestUrl","DashboardLoginRedirectException","DurableApiSessionGuard","unauthorized","UnauthorizedException","GUARDS_METADATA","normalize","path","replace","stampGuards","hostGuards","builtIn","controller","merged","Reflect","defineMetadata","DurableApiModule","register","options","guards","authMayBeConfigured","DurableApiSessionGuard","undefined","DurableApiController","module","imports","controllers","providers","DashboardService","authProvider","exports","DurableDashboardModule","forRoot","provide","DASHBOARD_AUTH","useValue","resolveDashboardAuth","dashboardAuth","build","forRootAsync","inject","useFactory","deps","useDashboardAuth","basePath","apiBasePath","DurableUiSessionGuard","DurableUiController","RouterModule","DurableAuthController","DASHBOARD_BASE_PATH","DASHBOARD_API_PATH"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/dashboard.service.ts","../../src/server/durable-api.controller.ts","../../src/server/attr-filter.ts","../../src/server/run-list-row.ts","../../src/server/durable-auth.controller.ts","../../src/server/auth/dashboard-auth-config.ts","../../src/server/auth/login-page.ts","../../src/server/auth/request.ts","../../src/server/auth/response.ts","../../src/server/auth/cookie-header.ts","../../src/server/auth/session-cookie.ts","../../src/server/auth/session-cookie-io.ts","../../src/server/durable-ui.controller.ts","../../src/server/auth/login-redirect.exception.ts","../../src/server/auth/session-required.exception.ts","../../src/server/durable-dashboard.module.ts","../../src/server/durable-session.guard.ts"],"sourcesContent":["import {\n type DurableTopology,\n type EngineEvent,\n type GroupHealth,\n type MetricsCollector,\n type RunDetail,\n type RunFacetQuery,\n type RunFacetRow,\n RunGateway,\n type RunListItem,\n type RunQuery,\n type RunResult,\n STATE_STORE_CANONICAL,\n type StateStore,\n type UpdateResult,\n WorkflowEngine,\n collectMetrics,\n} from '@dudousxd/nestjs-durable-core';\nimport { Inject, Injectable, Optional } from '@nestjs/common';\nimport { Observable } from 'rxjs';\n\n// Single source of truth: core owns `RunDetail` (`run-gateway.ts`) — re-exported here (not\n// re-declared) so this package's existing public API (`import type { RunDetail } from\n// '@dudousxd/nestjs-durable-dashboard'`) keeps working unchanged.\nexport type { RunDetail };\n\n/**\n * Read-model and actions backing the control-plane UI. Run-facing ops (list/detail/retry/cancel/\n * continue/retryWithInput/stream/bulk/workerHealth) route through the injected {@link RUN_GATEWAY}\n * port, which the lib binds on both topologies (a store-backed impl on the control plane, a proxy\n * over the transport on a store-less tenant) — so those ops work regardless of whether `store`/\n * `engine` are present (`workerHealth` is scoped to the tenant's own groups over the proxy).\n * Operator-only ops (metrics/deliverWebhook/getEvent/update) still need direct `store`/`engine`\n * access; they stay `@Optional()` and throw a clear error when absent.\n */\n@Injectable()\nexport class DashboardService {\n /** Prometheus counters accumulated from engine events since boot (per process); undefined on a\n * store-less tenant (no engine to accumulate from). */\n private readonly metricsCollector: MetricsCollector | undefined;\n\n constructor(\n private readonly gateway: RunGateway,\n @Optional() @Inject(STATE_STORE_CANONICAL) private readonly store?: StateStore,\n @Optional() private readonly engine?: WorkflowEngine,\n ) {\n // Only the control plane (which has the canonical store) has a FULL engine to accumulate from;\n // on a tenant `this.engine` is a store-less start client with no `subscribe`, so guard on store.\n this.metricsCollector = this.store && this.engine ? collectMetrics(this.engine) : undefined;\n }\n\n /**\n * The operator-only ops need the FULL engine + canonical store, which only the control plane has.\n * On a tenant the `WorkflowEngine` token is a store-less start client that implements none of these\n * methods, so presence of the canonical store is the reliable discriminator — gate on it (never\n * on engine presence, which is truthy on a tenant too).\n */\n private controlPlane(): { store: StateStore; engine: WorkflowEngine } {\n if (!this.store || !this.engine) {\n throw new Error(\n 'This durable dashboard operation requires the control plane (not available on a tenant deployment).',\n );\n }\n return { store: this.store, engine: this.engine };\n }\n\n /**\n * Prometheus-text metrics for a `/metrics` scrape: the event-counters (runs/steps by outcome,\n * per-workflow counts) plus live **backlog gauges** queried at scrape time — `durable_pending_runs`\n * (the dispatch backlog: the key health signal of the dispatch model) and `durable_dead_runs` (DLQ\n * size). Capped per status so the scrape can't load an unbounded result set. Control-plane-only.\n */\n async metrics(): Promise<string> {\n const { store, engine } = this.controlPlane();\n const cap = 10_000;\n const [pending, running, dead] = await Promise.all([\n store.listRuns({ status: 'pending', limit: cap }),\n store.listRuns({ status: 'running', limit: cap }),\n store.listRuns({ status: 'dead', limit: cap }),\n ]);\n const gauges = [\n '# TYPE durable_pending_runs gauge',\n `durable_pending_runs ${pending.length}`,\n '# TYPE durable_running_runs gauge',\n `durable_running_runs ${running.length}`,\n '# TYPE durable_dead_runs gauge',\n `durable_dead_runs ${dead.length}`,\n ];\n // Per-group worker health: backlog vs. live workers. `depth>0 && live==0` is the alert (work\n // piling up with no consumer) — expressible as a Prometheus rule on these two series.\n const health = await engine.workerHealth();\n if (health.length > 0) {\n gauges.push('# TYPE durable_group_queue_depth gauge');\n for (const h of health) {\n gauges.push(`durable_group_queue_depth{group=\"${h.group}\"} ${h.depth}`);\n }\n gauges.push('# TYPE durable_group_live_workers gauge');\n for (const h of health) {\n gauges.push(`durable_group_live_workers{group=\"${h.group}\"} ${h.liveWorkers.length}`);\n }\n }\n return `${this.metricsCollector?.prometheus() ?? ''}${gauges.join('\\n')}\\n`;\n }\n\n listRuns(query: RunQuery): Promise<RunListItem[]> {\n return this.gateway.listRuns(query);\n }\n\n /** `(status, origin)` counts over the same predicates {@link listRuns} pages — what keeps the\n * console's chips exact while its list is bounded to a page. */\n runFacets(query: RunFacetQuery): Promise<RunFacetRow[]> {\n return this.gateway.runFacets(query);\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the dashboard header\n * badge. Routes through the gateway, which knows its own topology (store-backed = operator, proxy =\n * tenant). Works on both topologies (no store/engine needed). */\n topology(): DurableTopology {\n return this.gateway.topology();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. The\n * alert state a row turns red on is `depth > 0 && liveWorkers.length === 0`. Routes through the\n * gateway so it works on BOTH topologies: the control plane returns every group; a tenant proxies\n * to the operator, which scopes the result to the tenant's own `@<tenant>` groups. */\n async workerHealth(): Promise<GroupHealth[]> {\n return this.gateway.workerHealth();\n }\n\n getRunDetail(runId: string): Promise<RunDetail | null> {\n return this.gateway.getRunDetail(runId);\n }\n\n /** Fix-and-replay a dead/failed run with a corrected input — a fresh linked run. Returns its id. */\n retryWithInput(runId: string, input: unknown): Promise<{ runId: string } | null> {\n return this.gateway.retryWithInput(runId, input);\n }\n\n /** Re-run a failed/incomplete run; completed steps replay from their checkpoints. */\n retry(runId: string): Promise<RunResult | null> {\n return this.gateway.retry(runId);\n }\n\n cancel(runId: string, opts?: { compensate?: boolean }): Promise<RunResult | null> {\n return this.gateway.cancel(runId, opts);\n }\n\n /**\n * Apply `retry` or `cancel` to every run matching a filter (status / tag / workflow / namespace /\n * origin) — e.g. \"retry every `dead` run tagged `type:mel`\". The filter is the same one the run\n * list uses, so a bulk action can never reach wider than the list it was launched from — but note\n * an ABSENT predicate still means \"every value on that axis\" (an omitted `namespace` is every\n * tenant), which is what makes passing the operator's full filter through mandatory, not optional.\n * Capped at 500 runs per call; runs that can't be acted on\n * (already terminal) are skipped. Returns how many matched and how many the action applied to.\n */\n async bulk(\n action: 'retry' | 'cancel',\n filter: Pick<RunQuery, 'status' | 'tag' | 'workflow' | 'attributes' | 'namespace' | 'origin'>,\n opts?: { compensate?: boolean },\n ): Promise<{ matched: number; applied: number }> {\n const runs = await this.gateway.listRuns({ ...filter, limit: 500 });\n let applied = 0;\n for (const r of runs) {\n try {\n if (action === 'retry') await this.gateway.retry(r.id);\n else await this.gateway.cancel(r.id, opts);\n applied += 1;\n } catch {\n // Skip a run that can't take the action (e.g. already terminal) — keep going.\n }\n }\n return { matched: runs.length, applied };\n }\n\n /** Resume a run paused at a `ctx.breakpoint` (the \"continue\" button). */\n continue(runId: string): Promise<RunResult | null> {\n return this.gateway.continue(runId);\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n redispatch(runId: string) {\n return this.gateway.redispatchPending(runId);\n }\n\n /**\n * Deliver a `ctx.webhook()` callback: turn an inbound POST (token + body) into the signal the\n * waiting run is parked on. Returns the run result, or `null` if no run waits on that token (a\n * stale/duplicate callback) — a safe no-op the controller maps to 404. Control-plane-only.\n */\n async deliverWebhook(token: string, body: unknown): Promise<RunResult | null> {\n return this.controlPlane().engine.signal(token, body);\n }\n\n /** Side-effect-free read of a value a run published via `ctx.setEvent` (a live query). Control-plane-only. */\n async getEvent(runId: string, key: string): Promise<unknown> {\n return this.controlPlane().engine.getEvent(runId, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the validator may reject it (see UpdateResult). Control-plane-only. */\n async update(runId: string, name: string, arg: unknown): Promise<UpdateResult> {\n return this.controlPlane().engine.update(runId, name, arg);\n }\n\n /**\n * Live stream of a run's lifecycle events for SSE. Backed by `gateway.subscribe`, which — on the\n * control plane — receives events from EVERY instance (so a dashboard-only pod tails a run\n * executing on a worker pod), and on a tenant round-trips over the transport. Either way the\n * gateway already filters to `runId`; the dashboard just wires the events through.\n */\n streamRun(runId: string): Observable<{ data: EngineEvent }> {\n return new Observable<{ data: EngineEvent }>((subscriber) => {\n const off = this.gateway.subscribe(runId, (event) => subscriber.next({ data: event }));\n return () => off();\n });\n }\n}\n","import type { RunStatus } from '@dudousxd/nestjs-durable-core';\nimport {\n Body,\n Controller,\n Get,\n Header,\n NotFoundException,\n Param,\n Post,\n Query,\n Sse,\n} from '@nestjs/common';\nimport { parseAttrFilters } from './attr-filter.js';\nimport { DashboardService } from './dashboard.service.js';\nimport { type RunListRow, toRunListRow } from './run-list-row.js';\n\n/**\n * How the console asks for a page. An absent bound means \"no bound\" — the historical whole-listing\n * behaviour, kept so an existing API consumer is not silently truncated. A non-numeric or negative\n * value is treated as absent rather than coerced to 0, which would return an empty page and read as\n * \"there are no runs\".\n */\nfunction pageBounds(limit?: string, offset?: string): { limit?: number; offset?: number } {\n const bound = (raw?: string): number | undefined => {\n const n = Number(raw);\n return raw !== undefined && raw !== '' && Number.isInteger(n) && n >= 0 ? n : undefined;\n };\n const l = bound(limit);\n const o = bound(offset);\n return { ...(l !== undefined ? { limit: l } : {}), ...(o !== undefined ? { offset: o } : {}) };\n}\n\n/**\n * One `status` narrows to that status; several (a repeated param) match ANY of them, which is how a\n * caller asks for a SET — the console's in-flight sibling query, for one. Kept as one param rather\n * than a second `statuses` param so `?status=running&status=suspended` reads the obvious way and an\n * existing single-value caller is unaffected.\n */\nfunction statusPredicate(status?: RunStatus | RunStatus[]): {\n status?: RunStatus;\n statuses?: RunStatus[];\n} {\n if (Array.isArray(status)) return status.length ? { statuses: status } : {};\n return status ? { status } : {};\n}\n\n/**\n * The origin predicate, which has THREE states rather than two: a named package, the unattributed\n * bucket, or no restriction. `unattributed=true` is its own param instead of a reserved `origin`\n * value because any reserved string is a package name someone can legitimately have, and a console\n * that quietly reinterpreted it would show the wrong runs. An explicit `unattributed` wins over a\n * concurrent `origin`, which is a contradictory request either way.\n */\nfunction originPredicate(origin?: string, unattributed?: string): { origin?: string | null } {\n if (unattributed === 'true' || unattributed === '1') return { origin: null };\n return origin ? { origin } : {};\n}\n\n/** JSON API consumed by the control-plane SPA. Mounted at `apiBasePath` (set by RouterModule). */\n@Controller()\nexport class DurableApiController {\n constructor(private readonly dashboard: DashboardService) {}\n\n /**\n * The run list, filtered by whatever the operator asked for. Every predicate is OPTIONAL and an\n * omitted one means \"don't narrow on this axis\" — in particular `namespace`, whose absence keeps\n * the historical, deliberate behaviour that read paths are NOT namespace-scoped (core,\n * `WorkflowRun.namespace`): the console shows every tenant's runs until an operator chooses one.\n * A blank param (`?namespace=`, from a cleared filter box) is the same as an absent one; passing\n * `''` through would be an exact match on a tenant nobody has, i.e. a silently empty console.\n */\n @Get('runs')\n async runs(\n @Query('status') status?: RunStatus | RunStatus[],\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n @Query('namespace') namespace?: string,\n @Query('origin') origin?: string,\n @Query('unattributed') unattributed?: string,\n @Query('limit') limit?: string,\n @Query('offset') offset?: string,\n ): Promise<RunListRow[]> {\n const runs = await this.dashboard.listRuns({\n ...statusPredicate(status),\n workflow,\n tag,\n attributes: parseAttrFilters(attr),\n namespace: namespace || undefined,\n ...originPredicate(origin, unattributed),\n ...pageBounds(limit, offset),\n });\n // Rows only — the three payload fields are the bulk of a large listing and belong to `runs/:id`.\n return runs.map(toRunListRow);\n }\n\n /**\n * `(status, origin)` counts for the runs matching the same tag/tenant/attribute predicates as\n * `GET runs`. This is what lets that listing be PAGED at all: the page bounds what the console\n * renders, this bounds nothing and keeps its status and origin chips exact.\n *\n * `status`, `origin` and the page bounds are deliberately NOT accepted — they are the axes being\n * counted, so narrowing by them would report the answer back to itself.\n */\n @Get('runs/facets')\n facets(\n @Query('workflow') workflow?: string,\n @Query('tag') tag?: string,\n @Query('attr') attr?: string | string[],\n @Query('namespace') namespace?: string,\n ) {\n return this.dashboard.runFacets({\n workflow,\n tag,\n attributes: parseAttrFilters(attr),\n namespace: namespace || undefined,\n });\n }\n\n /** Prometheus-text metrics (runs/steps by outcome, per-workflow counts) for a scrape. */\n @Get('metrics')\n @Header('Content-Type', 'text/plain; version=0.0.4')\n metrics() {\n return this.dashboard.metrics();\n }\n\n /** Per-group worker health (queue backlog + live worker heartbeats) for the Workers panel. */\n @Get('workers')\n workers() {\n return this.dashboard.workerHealth();\n }\n\n /** This deployment's durable role (control plane vs tenant) + tenant name — for the header badge. */\n @Get('topology')\n topology() {\n return this.dashboard.topology();\n }\n\n @Get('runs/:id')\n async run(@Param('id') id: string) {\n const detail = await this.dashboard.getRunDetail(id);\n if (!detail) throw new NotFoundException(`run ${id} not found`);\n return detail;\n }\n\n /** Server-Sent Events stream of a run's live lifecycle events — the dashboard tails it instead\n * of polling. Cross-pod when the transport has a control plane (see DashboardService.streamRun). */\n @Sse('runs/:id/stream')\n stream(@Param('id') id: string) {\n return this.dashboard.streamRun(id);\n }\n\n @Post('runs/:id/retry')\n retry(@Param('id') id: string) {\n return this.dashboard.retry(id);\n }\n\n /** Fix-and-replay: re-run a dead/failed run with a corrected input (a fresh linked run). */\n @Post('runs/:id/retry-with-input')\n retryWithInput(@Param('id') id: string, @Body() body: { input: unknown }) {\n return this.dashboard.retryWithInput(id, body?.input);\n }\n\n /**\n * Bulk retry/cancel every run matching a filter (status / tag / workflow / namespace / origin).\n * The filter takes the SAME params as `GET runs` and is parsed identically, so \"retry all\" acts on\n * exactly the set the operator was looking at rather than a wider one.\n */\n @Post('bulk/:action')\n bulk(\n @Param('action') action: 'retry' | 'cancel',\n @Query('status') status?: RunStatus,\n @Query('tag') tag?: string,\n @Query('workflow') workflow?: string,\n @Query('attr') attr?: string | string[],\n @Query('compensate') compensate?: string,\n @Query('namespace') namespace?: string,\n @Query('origin') origin?: string,\n @Query('unattributed') unattributed?: string,\n ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n {\n status,\n tag,\n workflow,\n attributes: parseAttrFilters(attr),\n namespace: namespace || undefined,\n // The unattributed bucket is a scope a console can actually SELECT, so a bulk action launched\n // under it must be narrowed to it. Dropping the param would widen a retry/cancel to every\n // origin — acting on runs the operator was not looking at.\n ...originPredicate(origin, unattributed),\n },\n { compensate: compensate === 'true' },\n );\n }\n\n @Post('runs/:id/cancel')\n async cancel(@Param('id') id: string, @Query('compensate') compensate?: string) {\n const result = await this.dashboard.cancel(id, { compensate: compensate === 'true' });\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n @Post('runs/:id/continue')\n async continue(@Param('id') id: string) {\n const result = await this.dashboard.continue(id);\n if (!result) throw new NotFoundException(`run ${id} is not paused at a breakpoint`);\n return result;\n }\n\n /** Re-dispatch a run's stuck `pending` remote steps — recovery for a lost step dispatch. */\n @Post('runs/:id/redispatch')\n async redispatch(@Param('id') id: string) {\n const result = await this.dashboard.redispatch(id);\n if (!result) throw new NotFoundException(`run ${id} not found`);\n return result;\n }\n\n /**\n * Public callback endpoint for `ctx.webhook()`: a third party POSTs here (the url handed to it),\n * and the body resumes the waiting run. NOTE: this is reachable by external systems — protect it\n * by treating the token as a secret (it embeds runId:seq) and/or fronting it with signature\n * verification in your own middleware.\n */\n @Post('webhooks/:token')\n async webhook(@Param('token') token: string, @Body() body: unknown) {\n const result = await this.dashboard.deliverWebhook(token, body);\n if (!result) throw new NotFoundException(`no run is waiting on webhook ${token}`);\n return result;\n }\n\n /** Live query: the latest value a run published for `key` via `ctx.setEvent` (no side effect). */\n @Get('runs/:id/events/:key')\n event(@Param('id') id: string, @Param('key') key: string) {\n return this.dashboard.getEvent(id, key);\n }\n\n /** Deliver a validated `ctx.onUpdate` to a run; the body is the update argument. */\n @Post('runs/:id/updates/:name')\n update(@Param('id') id: string, @Param('name') name: string, @Body() body: unknown) {\n return this.dashboard.update(id, name, body);\n }\n}\n","import type { AttributeFilter, AttributeOp } from '@dudousxd/nestjs-durable-core';\n\nconst ATTR_OPS = new Set<AttributeOp>(['eq', 'ne', 'gt', 'gte', 'lt', 'lte']);\n\n/** Coerce a query-string value: `true`/`false` → boolean, numeric → number, else the raw string. */\nfunction coerce(v: string): string | number | boolean {\n if (v === 'true') return true;\n if (v === 'false') return false;\n if (v !== '' && !Number.isNaN(Number(v))) return Number(v);\n return v;\n}\n\n/**\n * Parse `attr=key:op:value` query params (repeatable, ANDed) into {@link AttributeFilter}s — e.g.\n * `?attr=amount:gte:200&attr=tier:eq:pro`. `op` must be a known operator; a colon in the value is\n * preserved (only the first two colons delimit). Malformed entries are skipped.\n */\nexport function parseAttrFilters(attr?: string | string[]): AttributeFilter[] | undefined {\n if (!attr) return undefined;\n const raw = Array.isArray(attr) ? attr : [attr];\n const filters: AttributeFilter[] = [];\n for (const entry of raw) {\n const [key, op, ...rest] = entry.split(':');\n if (!key || !ATTR_OPS.has(op as AttributeOp) || rest.length === 0) continue;\n filters.push({ key, op: op as AttributeOp, value: coerce(rest.join(':')) });\n }\n return filters.length ? filters : undefined;\n}\n","import type { RunListItem } from '@dudousxd/nestjs-durable-core';\n\n/**\n * A run as the CONSOLE'S LIST needs it: every field a row or its derived state reads, and none of the\n * payloads only the detail view opens.\n *\n * `input`, `output` and `error` are dropped. On a real control plane they dominate the response —\n * measured against a 9.5k-run deployment, `error` alone (stack traces on every failed run) was 63% of\n * a 12 MB listing — while no list row, and no `deriveRunState` input, ever reads them. The detail\n * endpoint (`GET runs/:id`) still returns the whole run, which is where those three are actually\n * rendered.\n */\nexport type RunListRow = Omit<RunListItem, 'input' | 'output' | 'error'>;\n\n/** Project a gateway run down to {@link RunListRow}. Deletes rather than re-lists the kept fields, so\n * a field added to `WorkflowRun` reaches the console automatically instead of being silently\n * dropped by a projection nobody remembered to update. */\nexport function toRunListRow(run: RunListItem): RunListRow {\n const { input: _input, output: _output, error: _error, ...row } = run;\n return row;\n}\n","import {\n BadRequestException,\n Body,\n Controller,\n Get,\n Header,\n HttpCode,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Post,\n Req,\n Res,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './auth/dashboard-auth-config.js';\nimport { renderLoginPage } from './auth/login-page.js';\nimport { sanitizeReturnTo } from './auth/request.js';\nimport { redirectRaw } from './auth/response.js';\nimport { clearSessionCookie, issueSessionCookie } from './auth/session-cookie-io.js';\nimport type { DashboardSessionUser } from './auth/session-cookie.js';\nimport { DASHBOARD_BASE_PATH } from './durable-ui.controller.js';\n\ninterface LoginBody {\n username?: unknown;\n password?: unknown;\n returnTo?: unknown;\n}\n\nfunction isString(value: unknown): value is string {\n return typeof value === 'string';\n}\n\n/**\n * Mints/clears the `dashboardAuth` session cookie and serves the built-in login page. Mounted\n * under `basePath` alongside `DurableUiController` (see `DurableDashboardModule.forRoot`), but on\n * a SEPARATE controller so it is never itself behind `DurableUiSessionGuard` — these endpoints\n * CREATE the session that guard checks for. `@Optional()` on both injections: this controller is\n * only registered when `dashboardAuth` might be configured (a static `forRoot({ dashboardAuth })`\n * or any `forRootAsync`), but the resolved value can still be `null` at runtime for the async case\n * — every handler below re-checks it and 404s rather than assuming it is set.\n */\n@Controller()\nexport class DurableAuthController {\n private readonly logger = new Logger(DurableAuthController.name);\n /** Warn once per hook kind so a throwing hook doesn't spam logs on every failed attempt. */\n private readonly warnedHooks = new Set<string>();\n\n constructor(\n @Optional() @Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null,\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n ) {}\n\n // The login page reads `returnTo`/`error` from `location.search` client-side (see\n // `login-page.ts`), so this response never varies per request and needs no templating beyond\n // the developer-controlled `basePath`.\n @Get('login')\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n loginPage(): string {\n const auth = this.requireAuth();\n if (!auth.modes.includes('login')) throw new NotFoundException();\n return renderLoginPage(this.basePath);\n }\n\n // Called by the login page's own fetch (JSON, not a classic form POST — see `login-page.ts` for\n // why: it keeps the login page working with no host-configured body parser beyond the\n // JSON one Nest wires in by default on both Express and Fastify). Uniform `401` on any failure\n // (unknown user, wrong password, throwing hook) — no user-enumeration.\n @Post('login')\n @HttpCode(200)\n async login(\n @Body() body: LoginBody,\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<{ redirectTo: string }> {\n const auth = this.requireAuth();\n if (!auth.modes.includes('login')) throw new NotFoundException();\n if (!isString(body?.username) || !isString(body?.password)) {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const { username, password } = body;\n const user = await this.runHook('login', () => auth.login?.(username, password) ?? null);\n if (!user) throw new UnauthorizedException({ message: 'Invalid username or password.' });\n issueSessionCookie(user, { auth, request, response });\n return { redirectTo: sanitizeReturnTo(body.returnTo, this.basePath) };\n }\n\n // Mode A: the host frontend (carrying its own auth) POSTs here; the host hook validates the raw\n // request and returns the session user, or `null` to deny. This is the SSO path — the host's own\n // guard decides, and no credential ever reaches this library.\n @Post('session')\n @HttpCode(204)\n async session(\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<void> {\n const auth = this.requireAuth();\n // Mode A not configured => the endpoint does not exist for this host.\n if (!auth.modes.includes('session')) throw new NotFoundException();\n const user = await this.runHook('session', () => auth.session?.(request) ?? null);\n if (!user) throw new UnauthorizedException();\n issueSessionCookie(user, { auth, request, response });\n }\n\n // Plain `GET` (not `POST`): logging out only ever destroys the CALLER's own session, so it's\n // safe/idempotent from the caller's perspective — a simple `<a href>` (no JS, no CSRF token)\n // works. Non-passthrough `@Res()`: this handler owns the response outright (clears the cookie,\n // then redirects), so Nest must not also try to send a return value afterwards.\n @Get('logout')\n logout(@Req() request: unknown, @Res() response: unknown): void {\n // Best-effort: even without dashboardAuth configured, clearing is harmless.\n clearSessionCookie({ request, response });\n // Only bounce to the login page when Mode B is actually configured — under Mode A (only), that\n // page 404s (see loginPage()); redirecting to basePath instead lands back on the guarded UI,\n // which renders the Mode-A session-required instruction page.\n redirectRaw(\n response,\n this.auth?.modes.includes('login') ? `${this.basePath}/login` : this.basePath,\n );\n }\n\n private requireAuth(): ResolvedDashboardAuth {\n // login()/session()/logout() only reach a real request when dashboardAuth is configured — this\n // is a defensive guard (matching loginPage()'s 404), not a reachable runtime path in practice.\n if (!this.auth) throw new NotFoundException();\n return this.auth;\n }\n\n /**\n * Run a host hook (`login` or `session`) defensively: a throw is treated as a denial (uniform\n * failure) and warn-logged once PER KIND, so a buggy hook never 500s the endpoint into a\n * stack-trace leak nor floods the logs on repeated bad attempts.\n */\n private async runHook(\n kind: string,\n run: () => Promise<DashboardSessionUser | null> | DashboardSessionUser | null,\n ): Promise<DashboardSessionUser | null> {\n try {\n return (await run()) ?? null;\n } catch (error) {\n if (!this.warnedHooks.has(kind)) {\n this.warnedHooks.add(kind);\n this.logger.warn(\n `dashboardAuth ${kind} hook threw; treating as denial. ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n return null;\n }\n }\n}\n","import type { DashboardSessionUser } from './session-cookie.js';\n\n/** Host hook for Mode A — validates the host's own auth on the raw request. */\nexport type SessionHook = (\n request: unknown,\n) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;\n\n/** Host hook for Mode B — validates submitted credentials from the built-in login page. */\nexport type LoginHook = (\n username: string,\n password: string,\n) => Promise<DashboardSessionUser | null> | DashboardSessionUser | null;\n\n/**\n * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per\n * session, so a DB round-trip here is cheap — this holds even under concurrent traffic: N parallel\n * requests carrying the same past-half-life cookie share ONE call (see `revalidateOnce` in\n * `session-cookie-io.ts`), not N. Return `false` to revoke (the cookie is cleared and the request\n * denied). Distinct from `session`: that hook reads the host's auth off a fresh request, which a\n * console XHR does not carry — this one receives the already-minted session.\n */\nexport type RevalidateHook = (session: DashboardSessionUser) => Promise<boolean> | boolean;\n\n/** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */\nexport interface UnauthenticatedPageContext {\n /**\n * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`\n * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.\n * Cast it to whatever your app actually runs on.\n */\n request: unknown;\n /**\n * The platform-native response. The hook OWNS it: it must write AND end it (`res.render(...)`,\n * `res.status(401).send(...)`, an Inertia render, ...). If the hook returns without writing, the\n * library falls back to its own built-in page rather than leaving the request hanging.\n */\n response: unknown;\n /** Where this console is mounted (e.g. `/durable`) — useful for a \"back to the console\" link. */\n basePath: string;\n}\n\n/**\n * Host-owned page for an unauthenticated full-page navigation to the console.\n *\n * The built-in page cannot know who hosts the console, so it can only say \"open this console from\n * your application\" in the abstract — it can't name the launcher, link to it, or look like the rest\n * of the host's product. This hook hands the whole response to the host instead: render through\n * your own template engine, your own Inertia page, whatever. The page is served AT the console's\n * own URL, so `/durable` stays `/durable`.\n *\n * Deliberately NOT a replacement for Mode B's built-in login form. A host that wants its own login\n * UI uses Mode A plus this hook, and posts to `<basePath>/session` from its own page — the mint\n * endpoint is the supported primitive for that.\n *\n * Fail-closed by construction: it only ever runs on a request that has ALREADY been denied. A hook\n * that throws, or that returns without writing, falls back to the built-in page — it cannot let\n * anyone in.\n */\nexport type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;\n\nexport type AuthMode = 'session' | 'login';\n\n/**\n * Author-facing `dashboardAuth` option on `DurableDashboardModule.forRoot`/`forRootAsync`. Gates\n * BOTH the SPA (a full-page navigation redirected to a server-rendered login page) and the JSON\n * API (a plain `401`) behind a signed session cookie, mirroring `@dudousxd/nestjs-telescope`'s\n * `dashboardAuth` mechanism (same HMAC-SHA256 cookie, same fail-closed validation). Two ways to\n * mint that cookie: Mode A (`session`) — the host frontend, already carrying its own auth, POSTs\n * to `<basePath>/session` and the host hook decides; or Mode B (`login`) — the built-in,\n * dependency-free server-rendered login page (`GET <basePath>/login`). At least one of the two is\n * required so an un-mintable gate is a boot error, not a silently-open (or silently-stuck) console.\n */\nexport interface DashboardAuthOptions {\n /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */\n secret: string;\n /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */\n ttl?: string;\n /** Mode A: validates the host's own auth on the raw request POSTed to `<basePath>/session`;\n * return the session user, or `null` to deny. Thrown errors are treated as a denial (logged\n * once, never surfaced to the client). */\n session?: SessionHook;\n /** Mode B: validates submitted username/password from the built-in login page; return the\n * session user, or `null` to deny. Thrown errors are treated as a denial (logged once, never\n * surfaced to the client). */\n login?: LoginHook;\n /** Re-checks a live session on sliding renewal; see `RevalidateHook`. Not a mode — it cannot\n * mint a session, only revoke one already minted by `session`/`login`. */\n revalidate?: RevalidateHook;\n /** Renders the host's own page for an unauthenticated navigation, in place of the built-in\n * \"open this console from your application\" card; see `UnauthenticatedPageHook`. */\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\n/** Resolved, validated `dashboardAuth` config shared by the guards, auth controller, and login page. */\nexport interface ResolvedDashboardAuth {\n secret: string;\n ttlMs: number;\n modes: AuthMode[];\n session?: SessionHook;\n login?: LoginHook;\n revalidate?: RevalidateHook;\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\n/** DI token carrying the resolved `dashboardAuth` config (`ResolvedDashboardAuth | null`). */\nexport const DASHBOARD_AUTH = Symbol('DASHBOARD_AUTH');\n\nconst DEFAULT_TTL = '8h';\nconst DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;\nconst DURATION_UNITS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/** Parse a `'<number><s|m|h|d>'` duration to ms; falls back to the 8h default on a bad value. */\nfunction durationToMs(ttl: string): number {\n const match = /^(\\d+)([smhd])$/.exec(ttl.trim());\n if (!match) return DEFAULT_TTL_MS;\n const unit = DURATION_UNITS[match[2] ?? ''];\n if (unit === undefined) return DEFAULT_TTL_MS;\n return Number(match[1]) * unit;\n}\n\n/**\n * Validate + resolve the `dashboardAuth` option. Returns `null` when unconfigured (today's\n * unauthenticated behavior, unchanged). Throws at boot (fail closed) when configured but missing a\n * secret, or missing both a `session` and a `login` hook — the host learns immediately rather than\n * shipping an un-mintable gate.\n */\nexport function resolveDashboardAuth(\n options: DashboardAuthOptions | undefined,\n): ResolvedDashboardAuth | null {\n if (options === undefined) return null;\n if (typeof options.secret !== 'string' || options.secret === '') {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth.secret is required and must be a non-empty string ' +\n '(HMAC-SHA256 signing key, 32+ bytes recommended). Failing closed.',\n );\n }\n const modes: AuthMode[] = [];\n if (options.session !== undefined) {\n if (typeof options.session !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.session must be a function.');\n }\n modes.push('session');\n }\n if (options.login !== undefined) {\n if (typeof options.login !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.login must be a function.');\n }\n modes.push('login');\n }\n if (options.revalidate !== undefined && typeof options.revalidate !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.revalidate must be a function.');\n }\n if (\n options.unauthenticatedPage !== undefined &&\n typeof options.unauthenticatedPage !== 'function'\n ) {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth.unauthenticatedPage must be a function.',\n );\n }\n if (modes.length === 0) {\n throw new Error(\n 'DurableDashboardModule: dashboardAuth needs at least one of `session` or `login` ' +\n '(otherwise the cookie can never be minted). Failing closed.',\n );\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),\n modes,\n ...(options.session !== undefined ? { session: options.session } : {}),\n ...(options.login !== undefined ? { login: options.login } : {}),\n ...(options.revalidate !== undefined ? { revalidate: options.revalidate } : {}),\n ...(options.unauthenticatedPage !== undefined\n ? { unauthenticatedPage: options.unauthenticatedPage }\n : {}),\n };\n}\n","/**\n * Shared HTML shell for the `dashboardAuth` server-rendered pages (the Mode B login page below and\n * `renderSessionRequiredPage`'s Mode A instruction page) — same dark zinc card, mono type, emerald\n * accent as `@dudousxd/nestjs-agent`'s dashboard login page, so every auth-adjacent page in this\n * family looks like one product. `content` is placed inside the `.card` div verbatim, so a caller\n * needing its own `<script>` (see `renderLoginPage`) can simply include it in `content` — a\n * `<script>` renders identically whether it's the div's last child or a body-level sibling.\n *\n * `content` is always caller-controlled static markup, never user input reflected into the page,\n * so this sidesteps HTML-escaping entirely.\n */\nfunction page(content: string, title = 'Durable'): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<meta name=\"robots\" content=\"noindex, nofollow\" />\n<title>${title}</title>\n<style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #09090b;\n color: #e4e4e7;\n font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n padding: 16px;\n }\n .card {\n width: 100%;\n max-width: 384px;\n border: 1px solid #27272a;\n background: #18181b;\n border-radius: 8px;\n padding: 32px;\n box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.5);\n }\n .brand {\n margin: 0 0 24px;\n text-align: center;\n font-size: 18px;\n font-weight: 600;\n color: #34d399;\n }\n h1 {\n margin: 0 0 12px;\n font-size: 16px;\n font-weight: 600;\n color: #f4f4f5;\n }\n p { margin: 0 0 16px; color: #a1a1aa; }\n form { display: flex; flex-direction: column; gap: 16px; }\n label { display: flex; flex-direction: column; gap: 6px; }\n .field-label {\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: #71717a;\n }\n input {\n border-radius: 4px;\n border: 1px solid #3f3f46;\n background: #09090b;\n color: #f4f4f5;\n padding: 8px 12px;\n font: inherit;\n outline: none;\n }\n input:focus { border-color: rgb(52 211 153 / 0.6); }\n #error {\n display: none;\n margin: 0;\n font-size: 12px;\n color: #fb7185;\n }\n button, .button {\n margin-top: 8px;\n border-radius: 4px;\n border: 1px solid rgb(52 211 153 / 0.4);\n background: rgb(52 211 153 / 0.1);\n color: #6ee7b7;\n padding: 8px 12px;\n font: inherit;\n font-size: 12px;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n cursor: pointer;\n display: inline-block;\n text-align: center;\n text-decoration: none;\n box-sizing: border-box;\n }\n button:hover:not(:disabled), .button:hover { background: rgb(52 211 153 / 0.2); }\n button:disabled { opacity: 0.6; cursor: default; }\n</style>\n</head>\n<body>\n <div class=\"card\">\n ${content}\n </div>\n</body>\n</html>`;\n}\n\n/**\n * The built-in `dashboardAuth` login page (`GET <basePath>/login`, Mode B). Deliberately a small,\n * dependency-free, hand-authored HTML page — NOT part of the bundled React SPA — so gating the\n * dashboard shell doesn't require rebuilding or extending the Vite bundle with a new route/screen.\n *\n * `returnTo` and any error state are read CLIENT-SIDE from `location.search` (never server-echoed\n * into the HTML), so this function's only per-request-ish input is `basePath` — a developer-\n * controlled config value, not user input — and the page body is otherwise a static template.\n *\n * The submit flow (this page POSTs JSON via `fetch` and follows the JSON `redirectTo` it gets\n * back, rather than a classic form POST + server redirect) is why the markup keeps its own\n * `<script>` rather than adopting agent-dashboard's plain `<form method=\"post\">`.\n */\nexport function renderLoginPage(basePath: string): string {\n const loginAction = `${basePath}/login`;\n const defaultReturnTo = basePath;\n return page(\n `<p class=\"brand\">Durable</p>\n <form id=\"login-form\" autocomplete=\"on\">\n <label>\n <span class=\"field-label\">Username</span>\n <input id=\"username\" name=\"username\" type=\"text\" autocomplete=\"username\" required autofocus />\n </label>\n <label>\n <span class=\"field-label\">Password</span>\n <input id=\"password\" name=\"password\" type=\"password\" autocomplete=\"current-password\" />\n </label>\n <p id=\"error\" role=\"alert\">Invalid username or password.</p>\n <button id=\"submit\" type=\"submit\">Sign in</button>\n </form>\n<script>\n(function () {\n var LOGIN_ACTION = ${JSON.stringify(loginAction)};\n var DEFAULT_RETURN_TO = ${JSON.stringify(defaultReturnTo)};\n var params = new URLSearchParams(window.location.search);\n var errorBox = document.getElementById('error');\n if (params.get('error')) errorBox.style.display = 'block';\n\n function sameOriginReturnTo(value) {\n if (typeof value !== 'string' || value === '') return DEFAULT_RETURN_TO;\n if (value.charAt(0) !== '/' || value.charAt(1) === '/' || value.indexOf('://') !== -1) {\n return DEFAULT_RETURN_TO;\n }\n return value;\n }\n\n var form = document.getElementById('login-form');\n var submitButton = document.getElementById('submit');\n form.addEventListener('submit', function (event) {\n event.preventDefault();\n submitButton.disabled = true;\n errorBox.style.display = 'none';\n var returnTo = sameOriginReturnTo(params.get('returnTo'));\n fetch(LOGIN_ACTION, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n credentials: 'same-origin',\n body: JSON.stringify({\n username: document.getElementById('username').value,\n password: document.getElementById('password').value,\n returnTo: returnTo,\n }),\n })\n .then(function (response) {\n if (!response.ok) throw new Error('unauthorized');\n return response.json();\n })\n .then(function (data) {\n window.location.href = data.redirectTo || DEFAULT_RETURN_TO;\n })\n .catch(function () {\n errorBox.style.display = 'block';\n submitButton.disabled = false;\n });\n });\n})();\n</script>`,\n 'Sign in — Durable',\n );\n}\n\n/**\n * Mode-A-only shell (`GET <basePath>/login` never renders under Mode A — this is what the page\n * guard serves in its place, see `session-required.exception.ts`): there is no form here because\n * the host mints the session. Mirrors `renderLoginPage`'s markup so the two pages look like one\n * product. `basePath` drives the Retry link's `href` — a plain `<a>` back to the guarded mount\n * (which re-runs the check once the host has minted a session), keeping this page free of any\n * inline `<script>`, unlike `renderLoginPage`'s fetch-driven form.\n */\nexport function renderSessionRequiredPage(basePath: string): string {\n return page(\n `<h1>Open this console from your application</h1>\n <p>Your session is minted by the host app. Use its console launcher to sign in, then reload.</p>\n <a class=\"button\" href=\"${basePath}\">Retry</a>`,\n );\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readHeaders(request: unknown): Record<string, unknown> {\n if (!isRecord(request)) return {};\n const headers = request.headers;\n return isRecord(headers) ? headers : {};\n}\n\n/** Coerce a single header (string or string[]) to a string, else undefined. */\nfunction headerToString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value) && value.every((part) => typeof part === 'string')) {\n return value.join(',');\n }\n return undefined;\n}\n\n/** Read the raw `Cookie` request header (Express and Fastify both expose `headers`). */\nexport function readCookieHeader(request: unknown): string | undefined {\n return headerToString(readHeaders(request).cookie);\n}\n\n/**\n * Decide whether the cookie should carry `Secure`. True when the request is https directly\n * (`request.protocol === 'https'`) OR a proxy forwarded https (`x-forwarded-proto` includes\n * 'https'). Defensive structural reads — never throws, defaults to insecure (works on plain http\n * in dev).\n */\nexport function isHttpsRequest(request: unknown): boolean {\n if (isRecord(request) && request.protocol === 'https') return true;\n const forwarded = headerToString(readHeaders(request)['x-forwarded-proto']);\n if (forwarded === undefined) return false;\n return forwarded\n .split(',')\n .map((proto) => proto.trim().toLowerCase())\n .includes('https');\n}\n\n/** The path + query a browser navigated to (Express's `originalUrl`, else the generic `url`). */\nexport function originalRequestUrl(request: unknown): string {\n if (!isRecord(request)) return '/';\n if (typeof request.originalUrl === 'string' && request.originalUrl !== '') {\n return request.originalUrl;\n }\n if (typeof request.url === 'string' && request.url !== '') return request.url;\n return '/';\n}\n\n/**\n * Guard a client-supplied redirect target against an open redirect: it must be a same-origin,\n * root-relative path (`/durable/foo`), never a protocol-relative (`//evil.com`) or absolute\n * (`https://evil.com`) URL. Falls back to `fallback` (typically the dashboard's own `basePath`)\n * for anything else, including a missing/non-string value.\n */\nexport function sanitizeReturnTo(candidate: unknown, fallback: string): string {\n if (typeof candidate !== 'string') return fallback;\n if (!candidate.startsWith('/')) return fallback;\n if (candidate.startsWith('//')) return fallback;\n if (candidate.includes('://')) return fallback;\n return candidate;\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\ninterface RawCookieResponse {\n getHeader(name: string): number | string | string[] | undefined;\n setHeader(name: string, value: string | string[]): unknown;\n}\n\nfunction isRawCookieResponse(value: unknown): value is RawCookieResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function'\n );\n}\n\n/**\n * Resolve the writable Node `ServerResponse` from a platform response. Express' response IS the\n * Node response; Fastify's reply wraps it on `.raw`. Returns the outer response when it already\n * exposes get/setHeader (Express), otherwise the unwrapped `.raw` (Fastify) — so a single write\n * path serves both without a platform-specific dependency.\n */\nfunction resolveCookieResponse(response: unknown): RawCookieResponse | null {\n if (isRawCookieResponse(response)) return response;\n if (isRecord(response) && isRawCookieResponse(response.raw)) return response.raw;\n return null;\n}\n\nfunction existingSetCookies(response: RawCookieResponse): string[] {\n const current = response.getHeader('set-cookie');\n if (Array.isArray(current)) return current;\n if (typeof current === 'string') return [current];\n return [];\n}\n\n/**\n * Append a `Set-Cookie` header to the response WITHOUT clobbering any cookies already queued by\n * the host. Platform-agnostic raw write — no-ops gracefully if the response can't be unwrapped.\n */\nexport function appendSetCookie(response: unknown, cookie: string): void {\n const raw = resolveCookieResponse(response);\n if (!raw) return;\n raw.setHeader('set-cookie', [...existingSetCookies(raw), cookie]);\n}\n\ninterface RawRedirectResponse extends RawCookieResponse {\n statusCode: number;\n end(chunk?: string): unknown;\n}\n\nfunction isRawRedirectResponse(value: unknown): value is RawRedirectResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function' &&\n typeof value.end === 'function'\n );\n}\n\nfunction resolveRedirectResponse(response: unknown): RawRedirectResponse | null {\n if (isRawRedirectResponse(response)) return response;\n if (isRecord(response) && isRawRedirectResponse(response.raw)) return response.raw;\n return null;\n}\n\n/**\n * Write a redirect directly on the raw response and END it. Used from the `DashboardLoginRedirect`\n * exception filter (guards can't use `@Redirect()` — that decorator only applies to a controller\n * handler's return value) and by the manual, non-passthrough logout handler. Bypassing Nest's\n * normal \"send the handler's return value\" pipeline this way is only safe when NOTHING downstream\n * (a filter, or Nest itself) will also try to write to the same response — both call sites here\n * are the terminal step of their request (an exception filter, or a `@Res()` non-passthrough\n * handler), so there is no second writer to race.\n */\nexport function redirectRaw(response: unknown, location: string, status = 302): void {\n const raw = resolveRedirectResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('location', location);\n raw.end();\n}\n\n/**\n * Did something already write to this response?\n *\n * Used to decide whether a host's `unauthenticatedPage` hook actually produced a page. A hook that\n * returns without writing (an early `return`, a forgotten `await`, a template that resolved to\n * nothing) would otherwise leave the request hanging forever — the browser spins until it times\n * out, with no error anywhere. Checking this lets the caller fall back to the built-in page.\n *\n * Reads the Node `ServerResponse.headersSent` through the same Express/Fastify unwrapping as the\n * writers above; Fastify's own `reply.sent` is not consulted because `.raw.headersSent` is true in\n * every case that matters here (the reply has been flushed to the socket).\n */\nexport function responseAlreadyWritten(response: unknown): boolean {\n const raw = resolveRedirectResponse(response) as\n | (RawRedirectResponse & { headersSent?: boolean })\n | null;\n return raw?.headersSent === true;\n}\n\n/**\n * Write a full HTML page directly on the raw response and END it. Used by\n * `DashboardSessionRequiredFilter` (the Mode-A-only instruction page a guard renders in place of a\n * login redirect that no longer exists) — same raw-response bypass, and the same reason it's safe:\n * an exception filter is the terminal step of the request, so there's no second writer to race.\n */\nexport function writeHtmlRaw(response: unknown, html: string, status = 200): void {\n const raw = resolveRedirectResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('content-type', 'text/html; charset=utf-8');\n // Same as the auth controller's `@Header('Cache-Control', ...)` on the sibling login page: this\n // page reflects live session state, so it must never be served stale from a cache.\n raw.setHeader('cache-control', 'no-store, must-revalidate');\n raw.end(html);\n}\n","/**\n * Parse a raw `Cookie` request header into a name→value map. Dependency-free so it works on raw\n * Express AND Fastify requests without `cookie-parser`. Values are URL-decoded; malformed\n * segments are skipped, never thrown.\n */\nexport function parseCookieHeader(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (typeof header !== 'string' || header === '') return cookies;\n for (const segment of header.split(';')) {\n const eq = segment.indexOf('=');\n if (eq <= 0) continue;\n const name = segment.slice(0, eq).trim();\n if (name === '') continue;\n let rawValue = segment.slice(eq + 1).trim();\n // Strip a single pair of wrapping double-quotes (RFC 6265 quoted form).\n if (rawValue.length >= 2 && rawValue.startsWith('\"') && rawValue.endsWith('\"')) {\n rawValue = rawValue.slice(1, -1);\n }\n // First occurrence wins; don't clobber an earlier value with a later dup.\n if (name in cookies) continue;\n try {\n cookies[name] = decodeURIComponent(rawValue);\n } catch {\n cookies[name] = rawValue;\n }\n }\n return cookies;\n}\n\nexport interface SetCookieOptions {\n /** Cookie `Path`. The dashboard scopes its session cookie to `/` — see `session-cookie-io.ts`\n * for why (the UI mount and the JSON API mount can be configured to live at unrelated paths). */\n path: string;\n /** Cookie lifetime in seconds. Ignored (and forced to 0) when `clear` is set. */\n maxAgeSeconds: number;\n /** Add `Secure` when the request is https. */\n secure: boolean;\n /** Clear the cookie: empty value + `Max-Age=0` (and `Expires` in the past). */\n clear?: boolean;\n}\n\n/**\n * Serialize a `Set-Cookie` header value. Always `HttpOnly` + `SameSite=Lax` (Lax blocks\n * cross-site `POST`s carrying the cookie — CSRF coverage for the login/logout endpoints).\n * Platform-agnostic: just a string.\n */\nexport function serializeSetCookie(name: string, value: string, options: SetCookieOptions): string {\n const parts = [`${name}=${options.clear ? '' : encodeURIComponent(value)}`];\n parts.push(`Path=${options.path}`);\n parts.push('HttpOnly');\n parts.push('SameSite=Lax');\n if (options.secure) parts.push('Secure');\n if (options.clear) {\n parts.push('Max-Age=0');\n parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');\n } else {\n parts.push(`Max-Age=${options.maxAgeSeconds}`);\n }\n return parts.join('; ');\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Clock-skew grace applied to `exp` so a marginally-late clock doesn't bounce a valid cookie. */\nconst EXP_GRACE_MS = 30_000;\n\n/** The validated dashboard session attached to a request once its cookie verifies. */\nexport interface DashboardSession {\n /** Stable user id (the session user's `id`). */\n sub: string;\n /** Optional display name. */\n name?: string;\n /** Free-form role strings; the dashboard does not interpret them. */\n roles: string[];\n /** Issued-at, epoch milliseconds. */\n iat: number;\n /** Expiry, epoch milliseconds. */\n exp: number;\n}\n\n/** The session user the host `login` hook returns to mint a cookie. */\nexport interface DashboardSessionUser {\n id: string;\n name?: string;\n roles?: string[];\n}\n\nexport interface SignOptions {\n secret: string;\n ttlMs: number;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nexport interface VerifyOptions {\n secret: string;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nfunction base64urlEncode(value: string): string {\n return Buffer.from(value, 'utf8').toString('base64url');\n}\n\nfunction base64urlDecode(value: string): string {\n return Buffer.from(value, 'base64url').toString('utf8');\n}\n\nfunction hmac(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('base64url');\n}\n\n/**\n * Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless\n * HMAC-SHA256, no store, `node:crypto` only — no JWT dependency. Mirrors\n * `@dudousxd/nestjs-telescope`'s `dashboardAuth` cookie mechanism so the behavior is familiar\n * across the aviary consoles.\n */\nexport function signSessionCookie(user: DashboardSessionUser, options: SignOptions): string {\n const issuedAt = options.now ?? Date.now();\n const session: DashboardSession = {\n sub: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n iat: issuedAt,\n exp: issuedAt + options.ttlMs,\n };\n const encodedPayload = base64urlEncode(JSON.stringify(session));\n const signature = hmac(options.secret, encodedPayload);\n return `${encodedPayload}.${signature}`;\n}\n\n/** Type guard for the decoded payload shape — defends against tampered/legacy payloads. */\nfunction isSessionPayload(value: unknown): value is DashboardSession {\n if (typeof value !== 'object' || value === null) return false;\n const record: Record<string, unknown> = { ...value };\n if (typeof record.sub !== 'string') return false;\n if (record.name !== undefined && typeof record.name !== 'string') return false;\n if (!Array.isArray(record.roles)) return false;\n if (!record.roles.every((role) => typeof role === 'string')) return false;\n if (typeof record.iat !== 'number' || !Number.isFinite(record.iat)) return false;\n if (typeof record.exp !== 'number' || !Number.isFinite(record.exp)) return false;\n return true;\n}\n\n/**\n * Verify a cookie value and return the session, or `null` for anything that's tampered,\n * malformed, or expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER\n * throws — any parse failure yields `null`.\n */\nexport function verifySessionCookie(\n value: string,\n options: VerifyOptions,\n): DashboardSession | null {\n try {\n const dot = value.indexOf('.');\n if (dot <= 0 || dot === value.length - 1) return null;\n const encodedPayload = value.slice(0, dot);\n const providedSignature = value.slice(dot + 1);\n const expectedSignature = hmac(options.secret, encodedPayload);\n const provided = Buffer.from(providedSignature, 'base64url');\n const expected = Buffer.from(expectedSignature, 'base64url');\n // timingSafeEqual throws on a length mismatch — guard it so a wrong-length (tampered)\n // signature returns null instead of throwing.\n if (provided.length !== expected.length) return null;\n if (!timingSafeEqual(provided, expected)) return null;\n const decoded: unknown = JSON.parse(base64urlDecode(encodedPayload));\n if (!isSessionPayload(decoded)) return null;\n const now = options.now ?? Date.now();\n if (now > decoded.exp + EXP_GRACE_MS) return null;\n const session: DashboardSession = {\n sub: decoded.sub,\n ...(decoded.name !== undefined ? { name: decoded.name } : {}),\n roles: decoded.roles,\n iat: decoded.iat,\n exp: decoded.exp,\n };\n return session;\n } catch {\n return null;\n }\n}\n","import { parseCookieHeader, serializeSetCookie } from './cookie-header.js';\nimport type { ResolvedDashboardAuth } from './dashboard-auth-config.js';\nimport { isHttpsRequest, readCookieHeader } from './request.js';\nimport { appendSetCookie } from './response.js';\nimport {\n type DashboardSession,\n type DashboardSessionUser,\n signSessionCookie,\n verifySessionCookie,\n} from './session-cookie.js';\n\n/** Cookie name carrying the signed dashboard session. */\nexport const SESSION_COOKIE_NAME = 'durable_dashboard_session';\n\n/**\n * `Path=/`, deliberately not scoped to `basePath`/`apiBasePath`: the two mounts are independently\n * configurable (`DurableDashboardModule.forRoot({ basePath, apiBasePath })`) and can live at\n * unrelated paths (e.g. `/durable` UI + `/api/durable` API) — a cookie scoped to either one alone\n * would not reach the other. The cookie is `HttpOnly` + signed + short-lived, so the broader scope\n * is a reasonable trade for correctness across any mount configuration.\n */\nconst COOKIE_PATH = '/';\n\n/**\n * Sign a fresh session for `user` and append it as a `Set-Cookie` on the response, `Secure` when\n * the request is https.\n */\nexport function issueSessionCookie(\n user: DashboardSessionUser,\n context: {\n auth: ResolvedDashboardAuth;\n request: unknown;\n response: unknown;\n now?: number;\n },\n): void {\n const value = signSessionCookie(user, {\n secret: context.auth.secret,\n ttlMs: context.auth.ttlMs,\n ...(context.now !== undefined ? { now: context.now } : {}),\n });\n const cookie = serializeSetCookie(SESSION_COOKIE_NAME, value, {\n path: COOKIE_PATH,\n maxAgeSeconds: Math.floor(context.auth.ttlMs / 1000),\n secure: isHttpsRequest(context.request),\n });\n appendSetCookie(context.response, cookie);\n}\n\n/** Append a cookie-clearing `Set-Cookie` (Max-Age=0). */\nexport function clearSessionCookie(context: { request: unknown; response: unknown }): void {\n const cookie = serializeSetCookie(SESSION_COOKIE_NAME, '', {\n path: COOKIE_PATH,\n maxAgeSeconds: 0,\n secure: isHttpsRequest(context.request),\n clear: true,\n });\n appendSetCookie(context.response, cookie);\n}\n\n/** Read + verify the session cookie on `request`, or `null` when absent/tampered/expired. */\nexport function readSessionFromRequest(\n auth: ResolvedDashboardAuth,\n request: unknown,\n): DashboardSession | null {\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return null;\n return verifySessionCookie(cookieValue, { secret: auth.secret });\n}\n\n/**\n * In-flight `revalidate` calls, keyed by `<secret>:<sub>` (the secret disambiguates two\n * `dashboardAuth` configs that happen to share a process, e.g. tests) — joined, not repeated, by\n * every concurrent renewal of the SAME session. Entries are removed the moment their call settles\n * (`finally`, below), so this only ever holds calls genuinely in flight right now: it can't grow\n * without bound, and a throwing hook can't wedge a session's future renewals.\n */\nconst inFlightRevalidations = new Map<string, Promise<boolean>>();\n\n/**\n * Run `auth.revalidate` for `user`, joining an already-in-flight call for the same session instead\n * of starting a second one. This is what makes the `RevalidateHook` doc's \"runs at most once per\n * `ttl/2` per session\" true under real traffic: a single console page load fires several parallel\n * API calls, and without this every one of them carrying the same past-half-life cookie would\n * independently call the host's hook before the first renewal's fresh cookie lands. Every caller\n * still awaits the SAME outcome and applies it to its OWN request/response — only the host round\n * trip is shared.\n */\nfunction revalidateOnce(auth: ResolvedDashboardAuth, user: DashboardSessionUser): Promise<boolean> {\n const revalidate = auth.revalidate;\n if (!revalidate) return Promise.resolve(true);\n const key = `${auth.secret}:${user.id}`;\n const inFlight = inFlightRevalidations.get(key);\n if (inFlight) return inFlight;\n const call = Promise.resolve()\n .then(() => revalidate(user))\n // Fail closed: a throwing hook revokes rather than silently extending the session.\n .catch(() => false)\n .finally(() => {\n // Cleared on BOTH outcomes (the `.catch` above means this only ever sees a resolution, never\n // a rejection) so a later, non-concurrent renewal cycle calls the hook fresh rather than\n // reusing a stale result — and so the map never accumulates settled entries.\n inFlightRevalidations.delete(key);\n });\n inFlightRevalidations.set(key, call);\n return call;\n}\n\n/**\n * Sliding renewal + revalidation. When a valid cookie is past 50% of its TTL, re-issue a fresh one\n * so an active session never expires mid-use — but first give the host's `revalidate` hook a say,\n * so a deactivated or demoted user loses access instead of riding a self-renewing cookie forever.\n * Concurrent renewals of the same session share one `revalidate` call (see `revalidateOnce`).\n *\n * Returns `false` when the session was revoked (the clearing `Set-Cookie` is already queued and the\n * caller must deny the request); `true` otherwise, including when no renewal was due.\n */\nexport async function maybeRenewSession(\n auth: ResolvedDashboardAuth,\n session: DashboardSession,\n request: unknown,\n response: unknown,\n now: number = Date.now(),\n): Promise<boolean> {\n if (now - session.iat <= auth.ttlMs / 2) return true;\n const user: DashboardSessionUser = {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n };\n const allowed = await revalidateOnce(auth, user);\n if (!allowed) {\n clearSessionCookie({ request, response });\n return false;\n }\n issueSessionCookie(user, { auth, request, response, now });\n return true;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n NotFoundException,\n Param,\n StreamableFile,\n UseFilters,\n} from '@nestjs/common';\nimport { DashboardLoginRedirectFilter } from './auth/login-redirect.exception.js';\nimport { DashboardSessionRequiredFilter } from './auth/session-required.exception.js';\n\n/** DI token carrying the UI mount base (e.g. `/durable`). */\nexport const DASHBOARD_BASE_PATH = Symbol('DASHBOARD_BASE_PATH');\n/** DI token carrying the JSON API base the SPA fetches from (e.g. `/api/durable`). */\nexport const DASHBOARD_API_PATH = Symbol('DASHBOARD_API_PATH');\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/durable';\n\n/** dist/server/durable-ui.controller.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled control-plane SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `DurableDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n *\n * `@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)` is permanent (present\n * regardless of whether `dashboardAuth` is configured): each only ever activates for its own\n * exception type, which `DurableUiSessionGuard` only throws when it is ALSO stamped on this\n * controller (see `DurableDashboardModule.forRoot`) — so leaving them decorated here is inert, not\n * a behavior change, when `dashboardAuth` is absent. The guard picks between the two exceptions\n * based on which mode is configured (Mode B => redirect to the login page; Mode A only => render\n * the session-required instruction page in place, since there is no login page to redirect to).\n */\n@UseFilters(DashboardLoginRedirectFilter, DashboardSessionRequiredFilter)\n@Controller()\nexport class DurableUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(DASHBOARD_API_PATH) private readonly apiBasePath: string,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the\n // classic \"stuck loading after a deploy\"). The hashed assets below are immutable.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Dashboard is not built. Run the package build.');\n }\n // The bundle was built with Vite base `/durable/`; rewrite asset URLs to the configured base so\n // the SPA loads from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n // __DURABLE_BASE__ = where assets load; __DURABLE_API__ = where the SPA fetches the JSON API.\n const inject = `<script>window.__DURABLE_BASE__='${this.basePath}';window.__DURABLE_API__='${this.apiBasePath}';</script>`;\n return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n","import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';\nimport { Catch, HttpException, HttpStatus } from '@nestjs/common';\nimport { redirectRaw } from './response.js';\n\n/**\n * Thrown by `DurableUiSessionGuard` when a page-level (full-page navigation) request has no valid\n * `dashboardAuth` session — carries the login URL to redirect to. Guards can't use Nest's\n * `@Redirect()` decorator (that only applies to a controller handler's return value), so the\n * guard throws this and a dedicated, controller-scoped `DashboardLoginRedirectFilter` (see below)\n * turns it into a real `302`. This is the ONLY safe way to redirect from a guard: writing directly\n * to the response inside `canActivate` and then returning `false` risks a double-write (Nest still\n * tries to send a `ForbiddenException` response afterwards) — routing through the normal\n * exception-filter pipeline instead guarantees exactly one writer finalizes the response.\n */\nexport class DashboardLoginRedirectException extends HttpException {\n constructor(public readonly location: string) {\n super({ location }, HttpStatus.FOUND);\n }\n}\n\n/** Turns a `DashboardLoginRedirectException` into a real `302 Location: <location>` response. */\n@Catch(DashboardLoginRedirectException)\nexport class DashboardLoginRedirectFilter implements ExceptionFilter {\n catch(exception: DashboardLoginRedirectException, host: ArgumentsHost): void {\n const response = host.switchToHttp().getResponse();\n redirectRaw(response, exception.location);\n }\n}\n","import type { ArgumentsHost, ExceptionFilter } from '@nestjs/common';\nimport { Catch, HttpException, HttpStatus, Inject, Logger, Optional } from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './dashboard-auth-config.js';\nimport { renderSessionRequiredPage } from './login-page.js';\nimport { responseAlreadyWritten, writeHtmlRaw } from './response.js';\n\n/**\n * Thrown by `DurableUiSessionGuard` when a page-level (full-page navigation) request has no valid\n * `dashboardAuth` session AND Mode B (`login`) isn't configured — there is no login page to\n * redirect to (see `DashboardLoginRedirectException`, its Mode-B sibling). Carries `basePath` so\n * the filter below can render the Mode-A instruction page in its place. Same reasoning as the\n * login redirect for why a guard can't just write the response directly and return `false`: a\n * dedicated, controller-scoped `DashboardSessionRequiredFilter` (see below) is the only writer,\n * guaranteeing no double-write race.\n */\nexport class DashboardSessionRequiredException extends HttpException {\n constructor(public readonly basePath: string) {\n super({ basePath }, HttpStatus.UNAUTHORIZED);\n }\n}\n\n/**\n * Renders the Mode-A page for a denied navigation: the host's own `unauthenticatedPage` when one is\n * configured, otherwise the built-in instruction card.\n *\n * `@Optional()` on the injection is load-bearing. This filter is decorated onto\n * `DurableUiController` unconditionally (see that controller's docblock — it is inert when\n * `dashboardAuth` is absent, because the guard that throws its exception is only stamped on when\n * auth IS configured), so it must still construct in a module where `DASHBOARD_AUTH` was never\n * provided.\n */\n@Catch(DashboardSessionRequiredException)\nexport class DashboardSessionRequiredFilter implements ExceptionFilter {\n private readonly logger = new Logger(DashboardSessionRequiredFilter.name);\n private warned = false;\n\n constructor(\n @Optional()\n @Inject(DASHBOARD_AUTH)\n private readonly auth: ResolvedDashboardAuth | null = null,\n ) {}\n\n async catch(exception: DashboardSessionRequiredException, host: ArgumentsHost): Promise<void> {\n const http = host.switchToHttp();\n const response = http.getResponse();\n const hook = this.auth?.unauthenticatedPage;\n\n if (hook) {\n try {\n await hook({ request: http.getRequest(), response, basePath: exception.basePath });\n // The hook owns the response; if it wrote one, we are done.\n if (responseAlreadyWritten(response)) return;\n // It did not write. Falling through to the built-in page is the only outcome that neither\n // hangs the request forever nor invents a page the host did not ask for — but it is almost\n // certainly a bug in the hook, so say so.\n this.warnOnce(\n 'dashboardAuth.unauthenticatedPage returned without writing a response; falling back to ' +\n 'the built-in page. The hook must write AND end the response it is given.',\n );\n } catch (error) {\n // A broken host page must not turn a denial into a 500, and must never open the console.\n this.warnOnce(\n `dashboardAuth.unauthenticatedPage threw; falling back to the built-in page. ${String(error)}`,\n );\n // It may have written headers before throwing, in which case there is nothing left to do:\n // writing again would raise ERR_HTTP_HEADERS_SENT on top of the original failure.\n if (responseAlreadyWritten(response)) return;\n }\n }\n\n writeHtmlRaw(response, renderSessionRequiredPage(exception.basePath), HttpStatus.UNAUTHORIZED);\n }\n\n /** Once per process, not once per request: a broken hook fires on every denied navigation. */\n private warnOnce(message: string): void {\n if (this.warned) return;\n this.warned = true;\n this.logger.warn(message);\n }\n}\n","import 'reflect-metadata';\nimport {\n type CanActivate,\n type DynamicModule,\n type InjectionToken,\n Module,\n type OptionalFactoryDependency,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport {\n DASHBOARD_AUTH,\n type DashboardAuthOptions,\n resolveDashboardAuth,\n} from './auth/dashboard-auth-config.js';\nimport { DashboardLoginRedirectFilter } from './auth/login-redirect.exception.js';\nimport { DashboardService } from './dashboard.service.js';\nimport { DurableApiController } from './durable-api.controller.js';\nimport { DurableAuthController } from './durable-auth.controller.js';\nimport { DurableApiSessionGuard, DurableUiSessionGuard } from './durable-session.guard.js';\nimport {\n DASHBOARD_API_PATH,\n DASHBOARD_BASE_PATH,\n DurableUiController,\n} from './durable-ui.controller.js';\n\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. A drift spec imports the\n * real constant (via the resolvable `'@nestjs/common/constants.js'` subpath) and asserts this literal\n * stays byte-identical to it. Mirrors `@dudousxd/nestjs-agent-dashboard`'s `agent-dashboard.module.ts`.\n */\nconst GUARDS_METADATA = '__guards__';\n\nexport interface DurableDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/durable`. This is a page route — keep it out of an\n * `/api` prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/durable` — so the API inherits the app's auth/proxy rules\n * while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n /**\n * Guard classes fronting BOTH dashboard controllers (the SPA at `basePath` and its JSON API at\n * `apiBasePath`). Stamped onto each controller via `@nestjs/common`'s own `@UseGuards` metadata key\n * — REPLACE semantics, so a second `forRoot(...)` call overwrites (not appends to) whatever a prior\n * call stamped, same as re-applying `@UseGuards` by hand. Omit to leave the routes unguarded (the\n * host fronts them another way, e.g. a global guard or reverse-proxy auth).\n *\n * A guard's own DEPENDENCIES resolve from this module's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n *\n * Auth reality for the two mount points: `apiBasePath` is fetched by the SPA's own JS (an XHR/fetch\n * from the same origin), so a guard reading a bearer/`Authorization` header there works exactly like\n * any other API route. `basePath` (the UI shell, `DurableUiController.index()`) is a full-page\n * navigation — the BROWSER issues that GET directly (typing the URL, a bookmark, a link click), so\n * there is no custom header to read: only whatever ambient credential the browser sends by default\n * (a cookie, or nothing) reaches the guard there. A guard that only checks an `Authorization` header\n * will 403 the page shell while the API calls it makes right after would have passed — put session\n * auth behind a cookie (or front the UI mount with your reverse proxy / SSO instead) if you need the\n * shell itself gated.\n */\n guards?: Type<CanActivate>[];\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [JwtAuthGuard]`.\n */\n imports?: DynamicModule['imports'];\n /**\n * Gate the console (the SPA at `basePath` AND its JSON API at `apiBasePath`) behind a built-in,\n * signed session cookie — no infra required. Two ways to mint it:\n *\n * - **Mode A (`session`, recommended)**: your host app already has its own auth (SSO/OIDC/\n * whatever) — the host frontend, carrying that auth, POSTs to `<basePath>/session`, your\n * `session` hook validates the raw request and returns the session user, and this library mints\n * its cookie from that. No credential this library understands ever exists; you keep your own\n * identity provider as the source of truth. A page-level request with no valid session (and only\n * Mode A configured) gets a small server-rendered instruction page telling the visitor to sign in\n * through your host app, since there is no login page to redirect to.\n * - **Mode B (`login`, standalone fallback)**: no host frontend/IdP to lean on — the durable\n * dashboard serves its own small, dependency-free, server-rendered login page (`GET\n * <basePath>/login`, handled by `DurableAuthController`) and your `login` hook validates\n * submitted username/password. A missing/invalid/expired session redirects a page-level request\n * to that login page with `?returnTo=<original url>`.\n *\n * Either mode can be used alone, or both together (e.g. Mode A for normal use, Mode B as a\n * break-glass fallback) — at least one is required, or `forRoot`/`forRootAsync` throws at boot\n * (an un-mintable gate is a boot error, not a silently-open or silently-stuck console). An API\n * request (`apiBasePath`, fetched by the SPA's own JS) always gets a plain `401` on a missing\n * session, regardless of mode — the caller reads the status code, not HTML.\n *\n * A third, optional hook — **`revalidate`** — re-checks an already-minted session rather than\n * minting one: once a session cookie is past 50% of its TTL, the guard slides it forward with a\n * fresh one, and if `revalidate` is set it's consulted first, so a deactivated or demoted\n * operator loses access instead of riding a self-renewing cookie indefinitely. It only runs on\n * that renewal path, not on every request, so revocation has **latency, not immediacy**: an\n * operator revoked right after a renewal keeps their existing cookie — and therefore console\n * access — for up to `ttl/2` (4 hours at the default 8h TTL) before the next renewal check catches\n * it. Do not rely on `revalidate` for a same-second cutoff; front the mount with your own guard\n * (see {@link guards}) if you need that.\n *\n * Auth here is mount-level and role-agnostic: this option (and the guards/controller it wires up)\n * has no notion of control-plane vs tenant role (see the durable dashboard's topology note in\n * `dashboard.service.ts`) — nothing role-specific changes based on who logs in.\n *\n * Composes with {@link guards}: when both are set, a request must pass the built-in session check\n * AND every guard in `guards` — the SAME logical-AND semantics `@UseGuards` already gives multiple\n * guards, since the built-in guard is simply prepended to whatever `guards` you pass. Omit to leave\n * the console open (today's behavior, unchanged byte-for-byte) — front it with `guards`, a global\n * guard, or a reverse proxy instead.\n *\n * See {@link DashboardAuthOptions} for the `secret`/`ttl`/`session`/`login`/`revalidate` shape,\n * and `forRootAsync` / {@link DurableDashboardAsyncOptions.useDashboardAuth} when a hook needs DI\n * (e.g. an `EntityManager` to look up an admin user).\n */\n dashboardAuth?: DashboardAuthOptions;\n}\n\n/**\n * Async variant of {@link DurableDashboardOptions}. The mount paths and `guards` stay static (the\n * router and guard metadata are bound at module-DEFINITION time — the same constraint `forRoot`\n * already has), but `dashboardAuth` is built by an injected factory, so its `session`/`login` hooks\n * can reach your DB/services — e.g. Mode A's `session` hook calling out to the same auth service\n * your host app already uses to validate the raw request. Returning `undefined` from\n * `useDashboardAuth` leaves the console open, exactly like omitting `dashboardAuth` from `forRoot`.\n */\nexport interface DurableDashboardAsyncOptions {\n basePath?: string;\n apiBasePath?: string;\n guards?: Type<CanActivate>[];\n imports?: DynamicModule['imports'];\n /** Providers injected into `useDashboardAuth`, in order. Shared with a `guards` class's own deps. */\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n /** Build the `dashboardAuth` config from injected deps (or `undefined` to leave the console open). */\n useDashboardAuth: (\n ...deps: any[]\n ) => DashboardAuthOptions | undefined | Promise<DashboardAuthOptions | undefined>;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/**\n * Stamp (or clear) `@UseGuards`-equivalent metadata on a dashboard controller — REPLACE, not\n * append, across repeated calls (a second `forRoot(...)` overwrites whatever a prior call\n * stamped). `builtIn`, when given, is always prepended FIRST — it's the `dashboardAuth` session\n * guard for that controller, which must run before any host `guards` (fail the session check\n * before ever invoking host auth). Passing neither reproduces today's behavior byte-for-byte\n * (an empty array — the controller unguarded).\n */\nfunction stampGuards(\n hostGuards: Type<CanActivate>[] | undefined,\n builtIn: Type<CanActivate> | undefined,\n controller: Type,\n): void {\n const merged = [...(builtIn ? [builtIn] : []), ...(hostGuards ?? [])];\n Reflect.defineMetadata(GUARDS_METADATA, merged, controller);\n}\n\n/**\n * Holds the JSON API controller + its read service, mounted on its own path by `forRoot`. Dynamic:\n * guards are DI-instantiated by the CONTROLLER's host module, so this module — not the outer wrapper\n * — must carry the guard classes as providers plus the host's `imports` that resolve their\n * dependencies. A static module here made `guards: [SomeGuardWithDeps]` fail at boot with \"Nest can't\n * resolve dependencies ... in the DurableApiModule context\" even when the host passed the right\n * `imports` to `forRoot` (mirrors `@dudousxd/nestjs-agent-dashboard`'s `AgentApiModule`).\n */\n@Module({})\nexport class DurableApiModule {\n static register(options: {\n imports?: DynamicModule['imports'];\n guards?: Type<CanActivate>[];\n /** Provider for `DASHBOARD_AUTH` — a `useValue` (`forRoot`) or a `useFactory` (`forRootAsync`). */\n authProvider: Provider;\n /** Whether the built-in `DurableApiSessionGuard` should be stamped on `DurableApiController`\n * (true whenever `dashboardAuth` was set, or unconditionally for `forRootAsync`, where it's\n * resolved at runtime and is a no-op if it ends up unconfigured). */\n authMayBeConfigured: boolean;\n }): DynamicModule {\n stampGuards(\n options.guards,\n options.authMayBeConfigured ? DurableApiSessionGuard : undefined,\n DurableApiController,\n );\n return {\n module: DurableApiModule,\n imports: [...(options.imports ?? [])],\n controllers: [DurableApiController],\n providers: [\n DashboardService,\n options.authProvider,\n DurableApiSessionGuard,\n ...(options.guards ?? []),\n ],\n exports: [DashboardService],\n };\n }\n}\n\n/**\n * Mounts the control plane: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `DurableDashboardModule.forRoot(...)` (or `forRootAsync`\n * when `dashboardAuth`'s `login` hook needs injected services) alongside `DurableModule` (global),\n * so it resolves the engine and store. Front the routes with the first-class `guards` option\n * (plus `imports` for the guards' own dependencies) — see {@link DurableDashboardOptions.guards} —\n * or with the built-in `dashboardAuth` cookie login, or both.\n */\n@Module({})\nexport class DurableDashboardModule {\n static forRoot(options: DurableDashboardOptions = {}): DynamicModule {\n const authProvider: Provider = {\n provide: DASHBOARD_AUTH,\n useValue: resolveDashboardAuth(options.dashboardAuth),\n };\n return DurableDashboardModule.build(options, authProvider, options.dashboardAuth !== undefined);\n }\n\n static forRootAsync(options: DurableDashboardAsyncOptions): DynamicModule {\n const authProvider: Provider = {\n provide: DASHBOARD_AUTH,\n inject: options.inject ?? [],\n useFactory: async (...deps: any[]) =>\n resolveDashboardAuth(await options.useDashboardAuth(...deps)),\n };\n // Always true: whether the resolved config ends up configured is only known at runtime, so the\n // guards/auth-controller wiring must always be present — `DurableUiSessionGuard`/\n // `DurableApiSessionGuard` are already no-ops when `DASHBOARD_AUTH` resolves to `null`.\n return DurableDashboardModule.build(options, authProvider, true);\n }\n\n /** Shared wiring: static routing + the API module, with `dashboardAuth` supplied by `authProvider`. */\n private static build(\n options: {\n basePath?: string;\n apiBasePath?: string;\n guards?: Type<CanActivate>[];\n imports?: DynamicModule['imports'];\n },\n authProvider: Provider,\n authMayBeConfigured: boolean,\n ): DynamicModule {\n const basePath = normalize(options.basePath ?? '/durable');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n stampGuards(\n options.guards,\n authMayBeConfigured ? DurableUiSessionGuard : undefined,\n DurableUiController,\n );\n return {\n module: DurableDashboardModule,\n imports: [\n ...(options.imports ?? []),\n // Guards + host imports must reach the API controller's HOST module — enhancers resolve\n // from their controller's own module, never from a parent (see DurableApiModule.register).\n // Spread-only-when-set: exactOptionalPropertyTypes rejects an explicit `undefined`.\n DurableApiModule.register({\n ...(options.imports ? { imports: options.imports } : {}),\n ...(options.guards ? { guards: options.guards } : {}),\n authProvider,\n authMayBeConfigured,\n }),\n RouterModule.register([\n { path: basePath, module: DurableDashboardModule }, // the UI + auth controllers below\n { path: apiBasePath, module: DurableApiModule },\n ]),\n ],\n controllers: [\n DurableUiController,\n // The auth controller (login page + logout) only exists when dashboardAuth might be\n // configured — mounting it unconditionally would add a new `/login` route even for hosts\n // that never set `dashboardAuth`, breaking \"absent -> today's behavior byte-for-byte\".\n ...(authMayBeConfigured ? [DurableAuthController] : []),\n ],\n providers: [\n { provide: DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: DASHBOARD_API_PATH, useValue: apiBasePath },\n authProvider,\n DurableUiSessionGuard,\n // DurableUiController (and DurableAuthController, when present) are hosted HERE, so their\n // guards DI-instantiate from this module.\n ...(options.guards ?? []),\n ],\n // Re-export the API module so its DashboardService reaches importers (e.g. flip's own controllers).\n exports: [DurableApiModule],\n };\n }\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { DASHBOARD_AUTH, type ResolvedDashboardAuth } from './auth/dashboard-auth-config.js';\nimport { DashboardLoginRedirectException } from './auth/login-redirect.exception.js';\nimport { originalRequestUrl } from './auth/request.js';\nimport { maybeRenewSession, readSessionFromRequest } from './auth/session-cookie-io.js';\nimport { DashboardSessionRequiredException } from './auth/session-required.exception.js';\nimport { DASHBOARD_BASE_PATH } from './durable-ui.controller.js';\n\n/**\n * Gates `DurableUiController` (the SPA shell + assets, a full-page browser navigation) on a valid\n * `dashboardAuth` session cookie. A no-op (always `true`) when `dashboardAuth` was not configured\n * — see `DurableDashboardModule.forRoot`'s `dashboardAuth` doc. Missing/invalid/expired session,\n * when Mode B (`login`) is configured, gets a `302` to the built-in login page carrying\n * `?returnTo=<original url>`, via `DashboardLoginRedirectException` (see that file for why a guard\n * can't just call `response.redirect()` and return `false`). When only Mode A (`session`) is\n * configured there is no login page to redirect to — the host mints the session itself — so it\n * instead throws `DashboardSessionRequiredException`, rendering a small instruction page.\n *\n * Auth is mount-level and role-agnostic: this guard has no notion of control-plane vs tenant\n * (see the durable dashboard's topology note in `dashboard.service.ts`) — it only ever answers\n * \"is there a valid dashboard session\", nothing role-specific changes here.\n */\n@Injectable()\nexport class DurableUiSessionGuard implements CanActivate {\n constructor(\n @Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null,\n @Inject(DASHBOARD_BASE_PATH) private readonly basePath: string,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const auth = this.auth;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(auth, request);\n if (!session) {\n this.denyUnauthenticated(auth, request);\n }\n if (!(await maybeRenewSession(auth, session, request, http.getResponse()))) {\n this.denyUnauthenticated(auth, request);\n }\n return true;\n }\n\n /**\n * A revoked session (the host's `revalidate` hook says no) is denied exactly like an absent\n * one — by design, there is nothing left to distinguish \"never had a session\" from \"had one,\n * then lost it\" once the cookie is cleared, so both get the same Mode-aware treatment.\n *\n * Takes `auth` explicitly rather than reading `this.auth`: `canActivate`'s `!this.auth` early\n * return narrows the field within that method only — TS doesn't carry it across a method call\n * — so callers pass the already-narrowed value instead of this method re-deriving (or\n * asserting away) the nullability.\n */\n private denyUnauthenticated(auth: ResolvedDashboardAuth, request: unknown): never {\n if (!auth.modes.includes('login')) throw new DashboardSessionRequiredException(this.basePath);\n const returnTo = encodeURIComponent(originalRequestUrl(request));\n throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);\n }\n}\n\n/**\n * Gates `DurableApiController` (the JSON API the SPA fetches) on a valid `dashboardAuth` session\n * cookie. A no-op (always `true`) when `dashboardAuth` was not configured. Missing/invalid/expired\n * session => a plain `401` (the API is called via `fetch`, never a browser navigation, so a\n * redirect would just fail the request anyway — the caller reads the status code, not HTML).\n *\n * Mount-level and role-agnostic, same as `DurableUiSessionGuard` above.\n */\n@Injectable()\nexport class DurableApiSessionGuard implements CanActivate {\n constructor(@Inject(DASHBOARD_AUTH) private readonly auth: ResolvedDashboardAuth | null) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const auth = this.auth;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(auth, request);\n if (!session) throw this.unauthorized(auth);\n if (!(await maybeRenewSession(auth, session, request, http.getResponse()))) {\n // Revoked mid-session: same treatment as an absent cookie.\n throw this.unauthorized(auth);\n }\n return true;\n }\n\n /**\n * A bare 401 body carrying `{ auth: { modes } }` — mirrors `@dudousxd/nestjs-telescope`'s\n * dashboardAuth 401. The API is fetched (never navigated), so this is the ONLY way the console\n * SPA (`durable-client.ts`) learns which auth surface to send the operator to on a mid-session\n * 401; `DurableUiSessionGuard`'s redirect/session-required exceptions above don't reach it.\n */\n private unauthorized(auth: ResolvedDashboardAuth): UnauthorizedException {\n return new UnauthorizedException({ auth: { modes: auth.modes } });\n }\n}\n"],"mappings":"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","names":["RunGateway","STATE_STORE_CANONICAL","WorkflowEngine","collectMetrics","Inject","Injectable","Optional","Observable","DashboardService","metricsCollector","gateway","store","engine","collectMetrics","undefined","controlPlane","Error","metrics","cap","pending","running","dead","Promise","all","listRuns","status","limit","gauges","length","health","workerHealth","push","h","group","depth","liveWorkers","prometheus","join","query","runFacets","topology","getRunDetail","runId","retryWithInput","input","retry","cancel","opts","bulk","action","filter","runs","applied","r","id","matched","continue","redispatch","redispatchPending","deliverWebhook","token","body","signal","getEvent","key","update","name","arg","streamRun","Observable","subscriber","off","subscribe","event","next","data","Body","Controller","Get","Header","NotFoundException","Param","Post","Query","Sse","ATTR_OPS","Set","coerce","v","Number","isNaN","parseAttrFilters","attr","undefined","raw","Array","isArray","filters","entry","key","op","rest","split","has","length","push","value","join","toRunListRow","run","input","_input","output","_output","error","_error","row","pageBounds","limit","offset","bound","raw","n","Number","undefined","isInteger","l","o","statusPredicate","status","Array","isArray","length","statuses","originPredicate","origin","unattributed","DurableApiController","dashboard","runs","workflow","tag","attr","namespace","listRuns","attributes","parseAttrFilters","map","toRunListRow","facets","runFacets","metrics","workers","workerHealth","topology","run","id","detail","getRunDetail","NotFoundException","stream","streamRun","retry","retryWithInput","body","input","bulk","action","compensate","cancel","result","continue","redispatch","webhook","token","deliverWebhook","event","key","getEvent","update","name","BadRequestException","Body","Controller","Get","Header","HttpCode","Inject","Logger","NotFoundException","Optional","Post","Req","Res","UnauthorizedException","DASHBOARD_AUTH","Symbol","DEFAULT_TTL","DEFAULT_TTL_MS","DURATION_UNITS","s","m","h","d","durationToMs","ttl","match","exec","trim","unit","undefined","Number","resolveDashboardAuth","options","secret","Error","modes","session","push","login","revalidate","unauthenticatedPage","length","ttlMs","page","content","title","renderLoginPage","basePath","loginAction","defaultReturnTo","JSON","stringify","renderSessionRequiredPage","isRecord","value","readHeaders","request","headers","headerToString","Array","isArray","every","part","join","undefined","readCookieHeader","cookie","isHttpsRequest","protocol","forwarded","split","map","proto","trim","toLowerCase","includes","originalRequestUrl","originalUrl","url","sanitizeReturnTo","candidate","fallback","startsWith","isRecord","value","isRawCookieResponse","getHeader","setHeader","resolveCookieResponse","response","raw","existingSetCookies","current","Array","isArray","appendSetCookie","cookie","isRawRedirectResponse","end","resolveRedirectResponse","redirectRaw","location","status","statusCode","responseAlreadyWritten","headersSent","writeHtmlRaw","html","parseCookieHeader","header","cookies","segment","split","eq","indexOf","name","slice","trim","rawValue","length","startsWith","endsWith","decodeURIComponent","serializeSetCookie","value","options","parts","clear","encodeURIComponent","push","path","secure","maxAgeSeconds","join","createHmac","timingSafeEqual","EXP_GRACE_MS","base64urlEncode","value","Buffer","from","toString","base64urlDecode","hmac","secret","payload","createHmac","update","digest","signSessionCookie","user","options","issuedAt","now","Date","session","sub","id","name","undefined","roles","iat","exp","ttlMs","encodedPayload","JSON","stringify","signature","isSessionPayload","record","Array","isArray","every","role","Number","isFinite","verifySessionCookie","dot","indexOf","length","slice","providedSignature","expectedSignature","provided","expected","timingSafeEqual","decoded","parse","SESSION_COOKIE_NAME","COOKIE_PATH","issueSessionCookie","user","context","value","signSessionCookie","secret","auth","ttlMs","now","undefined","cookie","serializeSetCookie","path","maxAgeSeconds","Math","floor","secure","isHttpsRequest","request","appendSetCookie","response","clearSessionCookie","clear","readSessionFromRequest","cookieValue","parseCookieHeader","readCookieHeader","verifySessionCookie","inFlightRevalidations","Map","revalidateOnce","revalidate","Promise","resolve","key","id","inFlight","get","call","then","catch","finally","delete","set","maybeRenewSession","session","Date","iat","sub","name","roles","allowed","existsSync","readFileSync","basename","extname","join","resolve","sep","fileURLToPath","Controller","Get","Header","Inject","NotFoundException","Param","StreamableFile","UseFilters","Catch","HttpException","HttpStatus","DashboardLoginRedirectException","HttpException","location","HttpStatus","FOUND","DashboardLoginRedirectFilter","catch","exception","host","response","switchToHttp","getResponse","redirectRaw","Catch","HttpException","HttpStatus","Inject","Logger","Optional","DashboardSessionRequiredException","HttpException","basePath","HttpStatus","UNAUTHORIZED","DashboardSessionRequiredFilter","logger","Logger","name","warned","auth","catch","exception","host","http","switchToHttp","response","getResponse","hook","unauthenticatedPage","request","getRequest","responseAlreadyWritten","warnOnce","error","String","writeHtmlRaw","renderSessionRequiredPage","message","warn","DASHBOARD_BASE_PATH","Symbol","DASHBOARD_API_PATH","BUILD_BASE","spaDir","fileURLToPath","URL","url","CONTENT_TYPES","DurableUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","isString","value","DurableAuthController","logger","Logger","name","warnedHooks","Set","auth","basePath","loginPage","requireAuth","modes","includes","NotFoundException","renderLoginPage","login","body","request","response","username","password","BadRequestException","user","runHook","UnauthorizedException","message","issueSessionCookie","redirectTo","sanitizeReturnTo","returnTo","session","logout","clearSessionCookie","redirectRaw","kind","run","error","has","add","warn","Error","String","passthrough","Module","RouterModule","Inject","Injectable","UnauthorizedException","DurableUiSessionGuard","auth","basePath","canActivate","context","http","switchToHttp","request","getRequest","session","readSessionFromRequest","denyUnauthenticated","maybeRenewSession","getResponse","modes","includes","DashboardSessionRequiredException","returnTo","encodeURIComponent","originalRequestUrl","DashboardLoginRedirectException","DurableApiSessionGuard","unauthorized","UnauthorizedException","GUARDS_METADATA","normalize","path","replace","stampGuards","hostGuards","builtIn","controller","merged","Reflect","defineMetadata","DurableApiModule","register","options","guards","authMayBeConfigured","DurableApiSessionGuard","undefined","DurableApiController","module","imports","controllers","providers","DashboardService","authProvider","exports","DurableDashboardModule","forRoot","provide","DASHBOARD_AUTH","useValue","resolveDashboardAuth","dashboardAuth","build","forRootAsync","inject","useFactory","deps","useDashboardAuth","basePath","apiBasePath","DurableUiSessionGuard","DurableUiController","RouterModule","DurableAuthController","DASHBOARD_BASE_PATH","DASHBOARD_API_PATH"]}
|