@dudousxd/nestjs-durable-dashboard 0.34.0 → 0.35.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/dashboard.service.ts","../../src/server/tokens.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/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 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';\nimport { RUN_GATEWAY } from './tokens.js';\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 @Inject(RUN_GATEWAY) 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) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". 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'>,\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","/**\n * Injection token for the bound `RunGateway`, owned by `@dudousxd/nestjs-durable` (the nestjs\n * package). The dashboard package does NOT depend on `@dudousxd/nestjs-durable` — its only\n * relevant peer dep is `@dudousxd/nestjs-durable-core` — so we do NOT import the token from\n * there. Instead we share its well-known token by value, the same pattern\n * `@dudousxd/nestjs-durable`'s own `tokens.ts` uses for `CONTEXT_ACCESSOR` (a cross-lib token\n * owned by `@dudousxd/nestjs-context`).\n *\n * `Symbol.for(key)` uses the global symbol registry, so this resolves to the SAME symbol\n * instance as `packages/nestjs/src/tokens.ts`'s `RUN_GATEWAY` without any import. The key MUST\n * stay byte-identical with that export.\n */\nexport const RUN_GATEWAY = Symbol.for('nestjs-durable:run-gateway');\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 @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 ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\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 /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\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 ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\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 so a throwing `login` hook doesn't spam logs on every failed attempt. */\n private warnedOnHookThrow = false;\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 if (!this.auth) 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 (!isString(body?.username) || !isString(body?.password)) {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const user = await this.runLoginHook(auth, body.username, body.password);\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 // 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 redirectRaw(response, `${this.basePath}/login`);\n }\n\n private requireAuth(): ResolvedDashboardAuth {\n // login()/logout() only reach a real request when dashboardAuth is configured — this is a\n // 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 the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and\n * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods\n * the logs on repeated bad attempts.\n */\n private async runLoginHook(\n auth: ResolvedDashboardAuth,\n username: string,\n password: string,\n ): Promise<DashboardSessionUser | null> {\n try {\n return (await auth.login(username, password)) ?? null;\n } catch (error) {\n if (!this.warnedOnHookThrow) {\n this.warnedOnHookThrow = true;\n this.logger.warn(\n `dashboardAuth login 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 validating 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 * 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). Unlike\n * Telescope's dashboard — a client-rendered SPA that can grow its own login screen — the durable\n * dashboard's bundled React app stays untouched: the login screen here is a small, dependency-free\n * server-rendered HTML page (`GET <basePath>/login`), so gating the SPA shell itself with a real\n * redirect doesn't require rebuilding or extending the Vite bundle.\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 /** Validates submitted username/password; return the session user, or `null` to deny. Thrown\n * errors are treated as a denial (logged once, never surfaced to the client). */\n login: LoginHook;\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 login: LoginHook;\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 a `login` hook — the host learns immediately rather than 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 if (typeof options.login !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.login is required (a login hook).');\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),\n login: options.login,\n };\n}\n","/**\n * The built-in `dashboardAuth` login page (`GET <basePath>/login`). 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 * This sidesteps HTML-escaping entirely: there is no reflected query-param interpolation surface\n * to get wrong.\n *\n * The visual language (dark zinc card, mono type, emerald accent) mirrors\n * `@dudousxd/nestjs-agent`'s dashboard login page so the two consoles feel like one family — only\n * the submit flow differs (this page POSTs JSON via `fetch` and follows the JSON `redirectTo` it\n * gets back, rather than a classic form POST + server redirect), which is why the markup keeps its\n * own `<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 `<!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>Sign in — Durable</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 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 {\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 }\n button:hover:not(:disabled) { 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 <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 </div>\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</body>\n</html>`;\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(): 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 * 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 * Sliding renewal: when a valid cookie is past 50% of its TTL, re-issue a fresh one so an active\n * session never expires mid-use. Appends a new `Set-Cookie` (preserving any others already queued).\n */\nexport function maybeRenewSession(\n auth: ResolvedDashboardAuth,\n session: DashboardSession,\n request: unknown,\n response: unknown,\n): void {\n const now = Date.now();\n if (now - session.iat <= auth.ttlMs / 2) return;\n issueSessionCookie(\n {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n },\n { auth, request, response, now },\n );\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';\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)` is permanent (present regardless of whether\n * `dashboardAuth` is configured): it only ever activates for a `DashboardLoginRedirectException`,\n * which `DurableUiSessionGuard` only throws when it is ALSO stamped on this controller (see\n * `DurableDashboardModule.forRoot`) — so leaving it decorated here is inert, not a behavior change,\n * when `dashboardAuth` is absent.\n */\n@UseFilters(DashboardLoginRedirectFilter)\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 '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 * cookie-session login — no infra, no changes to the bundled React SPA. Unlike\n * `@dudousxd/nestjs-telescope`'s `dashboardAuth` (a client-rendered login screen inside its SPA),\n * the durable dashboard's login page is small, dependency-free, server-rendered HTML served\n * directly by `DurableAuthController` at `GET <basePath>/login` — the SPA bundle itself never\n * changes. A missing/invalid/expired session redirects a page-level request (a full-page\n * navigation to `basePath`) to that login page with `?returnTo=<original url>`; an API request\n * (`apiBasePath`, fetched by the SPA's own JS) gets a plain `401` instead.\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`/`login` shape, and `forRootAsync` /\n * {@link DurableDashboardAsyncOptions.useDashboardAuth} when the `login` hook needs DI (e.g. an\n * `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 `login` hook can reach\n * your DB/services. Returning `undefined` from `useDashboardAuth` leaves the console open, exactly\n * 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 { 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 * a `302` to the built-in login page carrying `?returnTo=<original url>`, via\n * `DashboardLoginRedirectException` (see that file for why a guard can't just call\n * `response.redirect()` and return `false`).\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 canActivate(context: ExecutionContext): boolean {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(this.auth, request);\n if (!session) {\n const returnTo = encodeURIComponent(originalRequestUrl(request));\n throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);\n }\n maybeRenewSession(this.auth, session, request, http.getResponse());\n return true;\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 canActivate(context: ExecutionContext): boolean {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(this.auth, request);\n if (!session) throw new UnauthorizedException();\n maybeRenewSession(this.auth, session, request, http.getResponse());\n return true;\n }\n}\n"],"mappings":"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","names":["STATE_STORE_CANONICAL","WorkflowEngine","collectMetrics","Inject","Injectable","Optional","Observable","RUN_GATEWAY","Symbol","for","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","listRuns","attributes","parseAttrFilters","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","login","ttlMs","renderLoginPage","basePath","loginAction","defaultReturnTo","JSON","stringify","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","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","maybeRenewSession","session","Date","iat","id","sub","name","roles","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","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","warnedOnHookThrow","auth","basePath","loginPage","NotFoundException","renderLoginPage","login","body","request","response","requireAuth","username","password","BadRequestException","user","runLoginHook","UnauthorizedException","message","issueSessionCookie","redirectTo","sanitizeReturnTo","returnTo","logout","clearSessionCookie","redirectRaw","error","warn","Error","String","passthrough","Module","RouterModule","Inject","Injectable","UnauthorizedException","DurableUiSessionGuard","auth","basePath","canActivate","context","http","switchToHttp","request","getRequest","session","readSessionFromRequest","returnTo","encodeURIComponent","originalRequestUrl","DashboardLoginRedirectException","maybeRenewSession","getResponse","DurableApiSessionGuard","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/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/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) — e.g. \"retry\n * every `dead` run tagged `type:mel`\". 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'>,\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 @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 ) {\n return this.dashboard.listRuns({ status, workflow, tag, attributes: parseAttrFilters(attr) });\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 /** Bulk retry/cancel every run matching a filter (status / tag / workflow). */\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 ) {\n return this.dashboard.bulk(\n action === 'cancel' ? 'cancel' : 'retry',\n { status, tag, workflow, attributes: parseAttrFilters(attr) },\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 so a throwing `login` hook doesn't spam logs on every failed attempt. */\n private warnedOnHookThrow = false;\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 if (!this.auth) 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 (!isString(body?.username) || !isString(body?.password)) {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const user = await this.runLoginHook(auth, body.username, body.password);\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 // 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 redirectRaw(response, `${this.basePath}/login`);\n }\n\n private requireAuth(): ResolvedDashboardAuth {\n // login()/logout() only reach a real request when dashboardAuth is configured — this is a\n // 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 the host's `login` hook defensively: a throw is treated as a denial (uniform failure) and\n * warn-logged once, so a buggy hook never 500s the endpoint into a stack-trace leak nor floods\n * the logs on repeated bad attempts.\n */\n private async runLoginHook(\n auth: ResolvedDashboardAuth,\n username: string,\n password: string,\n ): Promise<DashboardSessionUser | null> {\n try {\n return (await auth.login(username, password)) ?? null;\n } catch (error) {\n if (!this.warnedOnHookThrow) {\n this.warnedOnHookThrow = true;\n this.logger.warn(\n `dashboardAuth login 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 validating 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 * 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). Unlike\n * Telescope's dashboard — a client-rendered SPA that can grow its own login screen — the durable\n * dashboard's bundled React app stays untouched: the login screen here is a small, dependency-free\n * server-rendered HTML page (`GET <basePath>/login`), so gating the SPA shell itself with a real\n * redirect doesn't require rebuilding or extending the Vite bundle.\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 /** Validates submitted username/password; return the session user, or `null` to deny. Thrown\n * errors are treated as a denial (logged once, never surfaced to the client). */\n login: LoginHook;\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 login: LoginHook;\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 a `login` hook — the host learns immediately rather than 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 if (typeof options.login !== 'function') {\n throw new Error('DurableDashboardModule: dashboardAuth.login is required (a login hook).');\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl ?? DEFAULT_TTL),\n login: options.login,\n };\n}\n","/**\n * The built-in `dashboardAuth` login page (`GET <basePath>/login`). 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 * This sidesteps HTML-escaping entirely: there is no reflected query-param interpolation surface\n * to get wrong.\n *\n * The visual language (dark zinc card, mono type, emerald accent) mirrors\n * `@dudousxd/nestjs-agent`'s dashboard login page so the two consoles feel like one family — only\n * the submit flow differs (this page POSTs JSON via `fetch` and follows the JSON `redirectTo` it\n * gets back, rather than a classic form POST + server redirect), which is why the markup keeps its\n * own `<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 `<!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>Sign in — Durable</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 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 {\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 }\n button:hover:not(:disabled) { 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 <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 </div>\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</body>\n</html>`;\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(): 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 * 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 * Sliding renewal: when a valid cookie is past 50% of its TTL, re-issue a fresh one so an active\n * session never expires mid-use. Appends a new `Set-Cookie` (preserving any others already queued).\n */\nexport function maybeRenewSession(\n auth: ResolvedDashboardAuth,\n session: DashboardSession,\n request: unknown,\n response: unknown,\n): void {\n const now = Date.now();\n if (now - session.iat <= auth.ttlMs / 2) return;\n issueSessionCookie(\n {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n },\n { auth, request, response, now },\n );\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';\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)` is permanent (present regardless of whether\n * `dashboardAuth` is configured): it only ever activates for a `DashboardLoginRedirectException`,\n * which `DurableUiSessionGuard` only throws when it is ALSO stamped on this controller (see\n * `DurableDashboardModule.forRoot`) — so leaving it decorated here is inert, not a behavior change,\n * when `dashboardAuth` is absent.\n */\n@UseFilters(DashboardLoginRedirectFilter)\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 '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 * cookie-session login — no infra, no changes to the bundled React SPA. Unlike\n * `@dudousxd/nestjs-telescope`'s `dashboardAuth` (a client-rendered login screen inside its SPA),\n * the durable dashboard's login page is small, dependency-free, server-rendered HTML served\n * directly by `DurableAuthController` at `GET <basePath>/login` — the SPA bundle itself never\n * changes. A missing/invalid/expired session redirects a page-level request (a full-page\n * navigation to `basePath`) to that login page with `?returnTo=<original url>`; an API request\n * (`apiBasePath`, fetched by the SPA's own JS) gets a plain `401` instead.\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`/`login` shape, and `forRootAsync` /\n * {@link DurableDashboardAsyncOptions.useDashboardAuth} when the `login` hook needs DI (e.g. an\n * `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 `login` hook can reach\n * your DB/services. Returning `undefined` from `useDashboardAuth` leaves the console open, exactly\n * 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 { 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 * a `302` to the built-in login page carrying `?returnTo=<original url>`, via\n * `DashboardLoginRedirectException` (see that file for why a guard can't just call\n * `response.redirect()` and return `false`).\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 canActivate(context: ExecutionContext): boolean {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(this.auth, request);\n if (!session) {\n const returnTo = encodeURIComponent(originalRequestUrl(request));\n throw new DashboardLoginRedirectException(`${this.basePath}/login?returnTo=${returnTo}`);\n }\n maybeRenewSession(this.auth, session, request, http.getResponse());\n return true;\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 canActivate(context: ExecutionContext): boolean {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest<unknown>();\n const session = readSessionFromRequest(this.auth, request);\n if (!session) throw new UnauthorizedException();\n maybeRenewSession(this.auth, session, request, http.getResponse());\n return true;\n }\n}\n"],"mappings":";;;;AAAA,SAMEA,YAIAC,uBAGAC,gBACAC,sBACK;AACP,SAASC,QAAQC,YAAYC,gBAAgB;AAC7C,SAASC,kBAAkB;;;;;;;;;;;;;;;;;;AAiBpB,IAAMC,mBAAN,MAAMA;SAAAA;;;;;;;;EAGMC;EAEjB,YACmBC,SAC2CC,OAC/BC,QAC7B;SAHiBF,UAAAA;SAC2CC,QAAAA;SAC/BC,SAAAA;AAI7B,SAAKH,mBAAmB,KAAKE,SAAS,KAAKC,SAASC,eAAe,KAAKD,MAAM,IAAIE;EACpF;;;;;;;EAQQC,eAA8D;AACpE,QAAI,CAAC,KAAKJ,SAAS,CAAC,KAAKC,QAAQ;AAC/B,YAAM,IAAII,MACR,qGAAA;IAEJ;AACA,WAAO;MAAEL,OAAO,KAAKA;MAAOC,QAAQ,KAAKA;IAAO;EAClD;;;;;;;EAQA,MAAMK,UAA2B;AAC/B,UAAM,EAAEN,OAAOC,OAAM,IAAK,KAAKG,aAAY;AAC3C,UAAMG,MAAM;AACZ,UAAM,CAACC,SAASC,SAASC,IAAAA,IAAQ,MAAMC,QAAQC,IAAI;MACjDZ,MAAMa,SAAS;QAAEC,QAAQ;QAAWC,OAAOR;MAAI,CAAA;MAC/CP,MAAMa,SAAS;QAAEC,QAAQ;QAAWC,OAAOR;MAAI,CAAA;MAC/CP,MAAMa,SAAS;QAAEC,QAAQ;QAAQC,OAAOR;MAAI,CAAA;KAC7C;AACD,UAAMS,SAAS;MACb;MACA,wBAAwBR,QAAQS,MAAM;MACtC;MACA,wBAAwBR,QAAQQ,MAAM;MACtC;MACA,qBAAqBP,KAAKO,MAAM;;AAIlC,UAAMC,SAAS,MAAMjB,OAAOkB,aAAY;AACxC,QAAID,OAAOD,SAAS,GAAG;AACrBD,aAAOI,KAAK,wCAAA;AACZ,iBAAWC,KAAKH,QAAQ;AACtBF,eAAOI,KAAK,oCAAoCC,EAAEC,KAAK,MAAMD,EAAEE,KAAK,EAAE;MACxE;AACAP,aAAOI,KAAK,yCAAA;AACZ,iBAAWC,KAAKH,QAAQ;AACtBF,eAAOI,KAAK,qCAAqCC,EAAEC,KAAK,MAAMD,EAAEG,YAAYP,MAAM,EAAE;MACtF;IACF;AACA,WAAO,GAAG,KAAKnB,kBAAkB2B,WAAAA,KAAgB,EAAA,GAAKT,OAAOU,KAAK,IAAA,CAAA;;EACpE;EAEAb,SAASc,OAAyC;AAChD,WAAO,KAAK5B,QAAQc,SAASc,KAAAA;EAC/B;;;;EAKAC,WAA4B;AAC1B,WAAO,KAAK7B,QAAQ6B,SAAQ;EAC9B;;;;;EAMA,MAAMT,eAAuC;AAC3C,WAAO,KAAKpB,QAAQoB,aAAY;EAClC;EAEAU,aAAaC,OAA0C;AACrD,WAAO,KAAK/B,QAAQ8B,aAAaC,KAAAA;EACnC;;EAGAC,eAAeD,OAAeE,OAAmD;AAC/E,WAAO,KAAKjC,QAAQgC,eAAeD,OAAOE,KAAAA;EAC5C;;EAGAC,MAAMH,OAA0C;AAC9C,WAAO,KAAK/B,QAAQkC,MAAMH,KAAAA;EAC5B;EAEAI,OAAOJ,OAAeK,MAA4D;AAChF,WAAO,KAAKpC,QAAQmC,OAAOJ,OAAOK,IAAAA;EACpC;;;;;;EAOA,MAAMC,KACJC,QACAC,QACAH,MAC+C;AAC/C,UAAMI,OAAO,MAAM,KAAKxC,QAAQc,SAAS;MAAE,GAAGyB;MAAQvB,OAAO;IAAI,CAAA;AACjE,QAAIyB,UAAU;AACd,eAAWC,KAAKF,MAAM;AACpB,UAAI;AACF,YAAIF,WAAW,QAAS,OAAM,KAAKtC,QAAQkC,MAAMQ,EAAEC,EAAE;YAChD,OAAM,KAAK3C,QAAQmC,OAAOO,EAAEC,IAAIP,IAAAA;AACrCK,mBAAW;MACb,QAAQ;MAER;IACF;AACA,WAAO;MAAEG,SAASJ,KAAKtB;MAAQuB;IAAQ;EACzC;;EAGAI,SAASd,OAA0C;AACjD,WAAO,KAAK/B,QAAQ6C,SAASd,KAAAA;EAC/B;;EAGAe,WAAWf,OAAe;AACxB,WAAO,KAAK/B,QAAQ+C,kBAAkBhB,KAAAA;EACxC;;;;;;EAOA,MAAMiB,eAAeC,OAAeC,MAA0C;AAC5E,WAAO,KAAK7C,aAAY,EAAGH,OAAOiD,OAAOF,OAAOC,IAAAA;EAClD;;EAGA,MAAME,SAASrB,OAAesB,KAA+B;AAC3D,WAAO,KAAKhD,aAAY,EAAGH,OAAOkD,SAASrB,OAAOsB,GAAAA;EACpD;;EAGA,MAAMC,OAAOvB,OAAewB,MAAcC,KAAqC;AAC7E,WAAO,KAAKnD,aAAY,EAAGH,OAAOoD,OAAOvB,OAAOwB,MAAMC,GAAAA;EACxD;;;;;;;EAQAC,UAAU1B,OAAkD;AAC1D,WAAO,IAAI2B,WAAkC,CAACC,eAAAA;AAC5C,YAAMC,MAAM,KAAK5D,QAAQ6D,UAAU9B,OAAO,CAAC+B,UAAUH,WAAWI,KAAK;QAAEC,MAAMF;MAAM,CAAA,CAAA;AACnF,aAAO,MAAMF,IAAAA;IACf,CAAA;EACF;AACF;;;;;;;;;;;;;;;AC3MA,SACEK,MACAC,YACAC,KACAC,QACAC,mBACAC,OACAC,MACAC,OACAC,WACK;;;ACTP,IAAMC,WAAW,oBAAIC,IAAiB;EAAC;EAAM;EAAM;EAAM;EAAO;EAAM;CAAM;AAG5E,SAASC,OAAOC,GAAS;AACvB,MAAIA,MAAM,OAAQ,QAAO;AACzB,MAAIA,MAAM,QAAS,QAAO;AAC1B,MAAIA,MAAM,MAAM,CAACC,OAAOC,MAAMD,OAAOD,CAAAA,CAAAA,EAAK,QAAOC,OAAOD,CAAAA;AACxD,SAAOA;AACT;AALSD;AAYF,SAASI,iBAAiBC,MAAwB;AACvD,MAAI,CAACA,KAAM,QAAOC;AAClB,QAAMC,MAAMC,MAAMC,QAAQJ,IAAAA,IAAQA,OAAO;IAACA;;AAC1C,QAAMK,UAA6B,CAAA;AACnC,aAAWC,SAASJ,KAAK;AACvB,UAAM,CAACK,KAAKC,IAAI,GAAGC,IAAAA,IAAQH,MAAMI,MAAM,GAAA;AACvC,QAAI,CAACH,OAAO,CAACd,SAASkB,IAAIH,EAAAA,KAAsBC,KAAKG,WAAW,EAAG;AACnEP,YAAQQ,KAAK;MAAEN;MAAKC;MAAuBM,OAAOnB,OAAOc,KAAKM,KAAK,GAAA,CAAA;IAAM,CAAA;EAC3E;AACA,SAAOV,QAAQO,SAASP,UAAUJ;AACpC;AAVgBF;;;;;;;;;;;;;;;;;;;;ADAT,IAAMiB,uBAAN,MAAMA;SAAAA;;;;EACX,YAA6BC,WAA6B;SAA7BA,YAAAA;EAA8B;EAG3DC,KACmBC,QACEC,UACLC,KACCC,MACf;AACA,WAAO,KAAKL,UAAUM,SAAS;MAAEJ;MAAQC;MAAUC;MAAKG,YAAYC,iBAAiBH,IAAAA;IAAM,CAAA;EAC7F;;EAKAI,UAAU;AACR,WAAO,KAAKT,UAAUS,QAAO;EAC/B;;EAIAC,UAAU;AACR,WAAO,KAAKV,UAAUW,aAAY;EACpC;;EAIAC,WAAW;AACT,WAAO,KAAKZ,UAAUY,SAAQ;EAChC;EAEA,MACMC,IAAiBC,IAAY;AACjC,UAAMC,SAAS,MAAM,KAAKf,UAAUgB,aAAaF,EAAAA;AACjD,QAAI,CAACC,OAAQ,OAAM,IAAIE,kBAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOC;EACT;;;EAKAG,OAAoBJ,IAAY;AAC9B,WAAO,KAAKd,UAAUmB,UAAUL,EAAAA;EAClC;EAGAM,MAAmBN,IAAY;AAC7B,WAAO,KAAKd,UAAUoB,MAAMN,EAAAA;EAC9B;;EAIAO,eAA4BP,IAAoBQ,MAA0B;AACxE,WAAO,KAAKtB,UAAUqB,eAAeP,IAAIQ,MAAMC,KAAAA;EACjD;;EAIAC,KACmBC,QACAvB,QACHE,KACKD,UACJE,MACMqB,YACrB;AACA,WAAO,KAAK1B,UAAUwB,KACpBC,WAAW,WAAW,WAAW,SACjC;MAAEvB;MAAQE;MAAKD;MAAUI,YAAYC,iBAAiBH,IAAAA;IAAM,GAC5D;MAAEqB,YAAYA,eAAe;IAAO,CAAA;EAExC;EAEA,MACMC,OAAoBb,IAAiCY,YAAqB;AAC9E,UAAME,SAAS,MAAM,KAAK5B,UAAU2B,OAAOb,IAAI;MAAEY,YAAYA,eAAe;IAAO,CAAA;AACnF,QAAI,CAACE,OAAQ,OAAM,IAAIX,kBAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOc;EACT;EAEA,MACMC,SAAsBf,IAAY;AACtC,UAAMc,SAAS,MAAM,KAAK5B,UAAU6B,SAASf,EAAAA;AAC7C,QAAI,CAACc,OAAQ,OAAM,IAAIX,kBAAkB,OAAOH,EAAAA,gCAAkC;AAClF,WAAOc;EACT;;EAGA,MACME,WAAwBhB,IAAY;AACxC,UAAMc,SAAS,MAAM,KAAK5B,UAAU8B,WAAWhB,EAAAA;AAC/C,QAAI,CAACc,OAAQ,OAAM,IAAIX,kBAAkB,OAAOH,EAAAA,YAAc;AAC9D,WAAOc;EACT;;;;;;;EAQA,MACMG,QAAwBC,OAAuBV,MAAe;AAClE,UAAMM,SAAS,MAAM,KAAK5B,UAAUiC,eAAeD,OAAOV,IAAAA;AAC1D,QAAI,CAACM,OAAQ,OAAM,IAAIX,kBAAkB,gCAAgCe,KAAAA,EAAO;AAChF,WAAOJ;EACT;;EAIAM,MAAmBpB,IAA0BqB,KAAa;AACxD,WAAO,KAAKnC,UAAUoC,SAAStB,IAAIqB,GAAAA;EACrC;;EAIAE,OAAoBvB,IAA2BwB,MAAsBhB,MAAe;AAClF,WAAO,KAAKtB,UAAUqC,OAAOvB,IAAIwB,MAAMhB,IAAAA;EACzC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEzIA,SACEiB,qBACAC,QAAAA,OACAC,cAAAA,aACAC,OAAAA,MACAC,UAAAA,SACAC,UACAC,UAAAA,SACAC,QACAC,qBAAAA,oBACAC,YAAAA,WACAC,QAAAA,OACAC,KACAC,KACAC,6BACK;;;ACqBA,IAAMC,iBAAiBC,OAAO,gBAAA;AAErC,IAAMC,cAAc;AACpB,IAAMC,iBAAiB,IAAI,KAAK,KAAK;AACrC,IAAMC,iBAAyC;EAC7CC,GAAG;EACHC,GAAG,KAAK;EACRC,GAAG,KAAK,KAAK;EACbC,GAAG,KAAK,KAAK,KAAK;AACpB;AAGA,SAASC,aAAaC,KAAW;AAC/B,QAAMC,QAAQ,kBAAkBC,KAAKF,IAAIG,KAAI,CAAA;AAC7C,MAAI,CAACF,MAAO,QAAOR;AACnB,QAAMW,OAAOV,eAAeO,MAAM,CAAA,KAAM,EAAA;AACxC,MAAIG,SAASC,OAAW,QAAOZ;AAC/B,SAAOa,OAAOL,MAAM,CAAA,CAAE,IAAIG;AAC5B;AANSL;AAaF,SAASQ,qBACdC,SAAyC;AAEzC,MAAIA,YAAYH,OAAW,QAAO;AAClC,MAAI,OAAOG,QAAQC,WAAW,YAAYD,QAAQC,WAAW,IAAI;AAC/D,UAAM,IAAIC,MACR,2JACE;EAEN;AACA,MAAI,OAAOF,QAAQG,UAAU,YAAY;AACvC,UAAM,IAAID,MAAM,yEAAA;EAClB;AACA,SAAO;IACLD,QAAQD,QAAQC;IAChBG,OAAOb,aAAaS,QAAQR,OAAOR,WAAAA;IACnCmB,OAAOH,QAAQG;EACjB;AACF;AAlBgBJ;;;AC5CT,SAASM,gBAAgBC,UAAgB;AAC9C,QAAMC,cAAc,GAAGD,QAAAA;AACvB,QAAME,kBAAkBF;AACxB,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAiGcG,KAAKC,UAAUH,WAAAA,CAAAA;4BACVE,KAAKC,UAAUF,eAAAA,CAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8C3C;AAnJgBH;;;ACjBhB,SAASM,SAASC,OAAc;AAC9B,SAAO,OAAOA,UAAU,YAAYA,UAAU;AAChD;AAFSD;AAIT,SAASE,YAAYC,SAAgB;AACnC,MAAI,CAACH,SAASG,OAAAA,EAAU,QAAO,CAAC;AAChC,QAAMC,UAAUD,QAAQC;AACxB,SAAOJ,SAASI,OAAAA,IAAWA,UAAU,CAAC;AACxC;AAJSF;AAOT,SAASG,eAAeJ,OAAc;AACpC,MAAI,OAAOA,UAAU,SAAU,QAAOA;AACtC,MAAIK,MAAMC,QAAQN,KAAAA,KAAUA,MAAMO,MAAM,CAACC,SAAS,OAAOA,SAAS,QAAA,GAAW;AAC3E,WAAOR,MAAMS,KAAK,GAAA;EACpB;AACA,SAAOC;AACT;AANSN;AASF,SAASO,iBAAiBT,SAAgB;AAC/C,SAAOE,eAAeH,YAAYC,OAAAA,EAASU,MAAM;AACnD;AAFgBD;AAUT,SAASE,eAAeX,SAAgB;AAC7C,MAAIH,SAASG,OAAAA,KAAYA,QAAQY,aAAa,QAAS,QAAO;AAC9D,QAAMC,YAAYX,eAAeH,YAAYC,OAAAA,EAAS,mBAAA,CAAoB;AAC1E,MAAIa,cAAcL,OAAW,QAAO;AACpC,SAAOK,UACJC,MAAM,GAAA,EACNC,IAAI,CAACC,UAAUA,MAAMC,KAAI,EAAGC,YAAW,CAAA,EACvCC,SAAS,OAAA;AACd;AARgBR;AAWT,SAASS,mBAAmBpB,SAAgB;AACjD,MAAI,CAACH,SAASG,OAAAA,EAAU,QAAO;AAC/B,MAAI,OAAOA,QAAQqB,gBAAgB,YAAYrB,QAAQqB,gBAAgB,IAAI;AACzE,WAAOrB,QAAQqB;EACjB;AACA,MAAI,OAAOrB,QAAQsB,QAAQ,YAAYtB,QAAQsB,QAAQ,GAAI,QAAOtB,QAAQsB;AAC1E,SAAO;AACT;AAPgBF;AAeT,SAASG,iBAAiBC,WAAoBC,UAAgB;AACnE,MAAI,OAAOD,cAAc,SAAU,QAAOC;AAC1C,MAAI,CAACD,UAAUE,WAAW,GAAA,EAAM,QAAOD;AACvC,MAAID,UAAUE,WAAW,IAAA,EAAO,QAAOD;AACvC,MAAID,UAAUL,SAAS,KAAA,EAAQ,QAAOM;AACtC,SAAOD;AACT;AANgBD;;;ACxDhB,SAASI,UAASC,OAAc;AAC9B,SAAO,OAAOA,UAAU,YAAYA,UAAU;AAChD;AAFSD,OAAAA,WAAAA;AAST,SAASE,oBAAoBD,OAAc;AACzC,SACED,UAASC,KAAAA,KACT,OAAOA,MAAME,cAAc,cAC3B,OAAOF,MAAMG,cAAc;AAE/B;AANSF;AAcT,SAASG,sBAAsBC,UAAiB;AAC9C,MAAIJ,oBAAoBI,QAAAA,EAAW,QAAOA;AAC1C,MAAIN,UAASM,QAAAA,KAAaJ,oBAAoBI,SAASC,GAAG,EAAG,QAAOD,SAASC;AAC7E,SAAO;AACT;AAJSF;AAMT,SAASG,mBAAmBF,UAA2B;AACrD,QAAMG,UAAUH,SAASH,UAAU,YAAA;AACnC,MAAIO,MAAMC,QAAQF,OAAAA,EAAU,QAAOA;AACnC,MAAI,OAAOA,YAAY,SAAU,QAAO;IAACA;;AACzC,SAAO,CAAA;AACT;AALSD;AAWF,SAASI,gBAAgBN,UAAmBO,QAAc;AAC/D,QAAMN,MAAMF,sBAAsBC,QAAAA;AAClC,MAAI,CAACC,IAAK;AACVA,MAAIH,UAAU,cAAc;OAAII,mBAAmBD,GAAAA;IAAMM;GAAO;AAClE;AAJgBD;AAWhB,SAASE,sBAAsBb,OAAc;AAC3C,SACED,UAASC,KAAAA,KACT,OAAOA,MAAME,cAAc,cAC3B,OAAOF,MAAMG,cAAc,cAC3B,OAAOH,MAAMc,QAAQ;AAEzB;AAPSD;AAST,SAASE,wBAAwBV,UAAiB;AAChD,MAAIQ,sBAAsBR,QAAAA,EAAW,QAAOA;AAC5C,MAAIN,UAASM,QAAAA,KAAaQ,sBAAsBR,SAASC,GAAG,EAAG,QAAOD,SAASC;AAC/E,SAAO;AACT;AAJSS;AAeF,SAASC,YAAYX,UAAmBY,UAAkBC,SAAS,KAAG;AAC3E,QAAMZ,MAAMS,wBAAwBV,QAAAA;AACpC,MAAI,CAACC,IAAK;AACVA,MAAIa,aAAaD;AACjBZ,MAAIH,UAAU,YAAYc,QAAAA;AAC1BX,MAAIQ,IAAG;AACT;AANgBE;;;ACtET,SAASI,kBAAkBC,QAA0B;AAC1D,QAAMC,UAAkC,CAAC;AACzC,MAAI,OAAOD,WAAW,YAAYA,WAAW,GAAI,QAAOC;AACxD,aAAWC,WAAWF,OAAOG,MAAM,GAAA,GAAM;AACvC,UAAMC,KAAKF,QAAQG,QAAQ,GAAA;AAC3B,QAAID,MAAM,EAAG;AACb,UAAME,OAAOJ,QAAQK,MAAM,GAAGH,EAAAA,EAAII,KAAI;AACtC,QAAIF,SAAS,GAAI;AACjB,QAAIG,WAAWP,QAAQK,MAAMH,KAAK,CAAA,EAAGI,KAAI;AAEzC,QAAIC,SAASC,UAAU,KAAKD,SAASE,WAAW,GAAA,KAAQF,SAASG,SAAS,GAAA,GAAM;AAC9EH,iBAAWA,SAASF,MAAM,GAAG,EAAC;IAChC;AAEA,QAAID,QAAQL,QAAS;AACrB,QAAI;AACFA,cAAQK,IAAAA,IAAQO,mBAAmBJ,QAAAA;IACrC,QAAQ;AACNR,cAAQK,IAAAA,IAAQG;IAClB;EACF;AACA,SAAOR;AACT;AAtBgBF;AAyCT,SAASe,mBAAmBR,MAAcS,OAAeC,SAAyB;AACvF,QAAMC,QAAQ;IAAC,GAAGX,IAAAA,IAAQU,QAAQE,QAAQ,KAAKC,mBAAmBJ,KAAAA,CAAAA;;AAClEE,QAAMG,KAAK,QAAQJ,QAAQK,IAAI,EAAE;AACjCJ,QAAMG,KAAK,UAAA;AACXH,QAAMG,KAAK,cAAA;AACX,MAAIJ,QAAQM,OAAQL,OAAMG,KAAK,QAAA;AAC/B,MAAIJ,QAAQE,OAAO;AACjBD,UAAMG,KAAK,WAAA;AACXH,UAAMG,KAAK,uCAAA;EACb,OAAO;AACLH,UAAMG,KAAK,WAAWJ,QAAQO,aAAa,EAAE;EAC/C;AACA,SAAON,MAAMO,KAAK,IAAA;AACpB;AAbgBV;;;AC9ChB,SAASW,YAAYC,uBAAuB;AAG5C,IAAMC,eAAe;AAoCrB,SAASC,gBAAgBC,OAAa;AACpC,SAAOC,OAAOC,KAAKF,OAAO,MAAA,EAAQG,SAAS,WAAA;AAC7C;AAFSJ;AAIT,SAASK,gBAAgBJ,OAAa;AACpC,SAAOC,OAAOC,KAAKF,OAAO,WAAA,EAAaG,SAAS,MAAA;AAClD;AAFSC;AAIT,SAASC,KAAKC,QAAgBC,SAAe;AAC3C,SAAOC,WAAW,UAAUF,MAAAA,EAAQG,OAAOF,OAAAA,EAASG,OAAO,WAAA;AAC7D;AAFSL;AAUF,SAASM,kBAAkBC,MAA4BC,SAAoB;AAChF,QAAMC,WAAWD,QAAQE,OAAOC,KAAKD,IAAG;AACxC,QAAME,UAA4B;IAChCC,KAAKN,KAAKO;IACV,GAAIP,KAAKQ,SAASC,SAAY;MAAED,MAAMR,KAAKQ;IAAK,IAAI,CAAC;IACrDE,OAAOV,KAAKU,SAAS,CAAA;IACrBC,KAAKT;IACLU,KAAKV,WAAWD,QAAQY;EAC1B;AACA,QAAMC,iBAAiB3B,gBAAgB4B,KAAKC,UAAUX,OAAAA,CAAAA;AACtD,QAAMY,YAAYxB,KAAKQ,QAAQP,QAAQoB,cAAAA;AACvC,SAAO,GAAGA,cAAAA,IAAkBG,SAAAA;AAC9B;AAZgBlB;AAehB,SAASmB,iBAAiB9B,OAAc;AACtC,MAAI,OAAOA,UAAU,YAAYA,UAAU,KAAM,QAAO;AACxD,QAAM+B,SAAkC;IAAE,GAAG/B;EAAM;AACnD,MAAI,OAAO+B,OAAOb,QAAQ,SAAU,QAAO;AAC3C,MAAIa,OAAOX,SAASC,UAAa,OAAOU,OAAOX,SAAS,SAAU,QAAO;AACzE,MAAI,CAACY,MAAMC,QAAQF,OAAOT,KAAK,EAAG,QAAO;AACzC,MAAI,CAACS,OAAOT,MAAMY,MAAM,CAACC,SAAS,OAAOA,SAAS,QAAA,EAAW,QAAO;AACpE,MAAI,OAAOJ,OAAOR,QAAQ,YAAY,CAACa,OAAOC,SAASN,OAAOR,GAAG,EAAG,QAAO;AAC3E,MAAI,OAAOQ,OAAOP,QAAQ,YAAY,CAACY,OAAOC,SAASN,OAAOP,GAAG,EAAG,QAAO;AAC3E,SAAO;AACT;AAVSM;AAiBF,SAASQ,oBACdtC,OACAa,SAAsB;AAEtB,MAAI;AACF,UAAM0B,MAAMvC,MAAMwC,QAAQ,GAAA;AAC1B,QAAID,OAAO,KAAKA,QAAQvC,MAAMyC,SAAS,EAAG,QAAO;AACjD,UAAMf,iBAAiB1B,MAAM0C,MAAM,GAAGH,GAAAA;AACtC,UAAMI,oBAAoB3C,MAAM0C,MAAMH,MAAM,CAAA;AAC5C,UAAMK,oBAAoBvC,KAAKQ,QAAQP,QAAQoB,cAAAA;AAC/C,UAAMmB,WAAW5C,OAAOC,KAAKyC,mBAAmB,WAAA;AAChD,UAAMG,WAAW7C,OAAOC,KAAK0C,mBAAmB,WAAA;AAGhD,QAAIC,SAASJ,WAAWK,SAASL,OAAQ,QAAO;AAChD,QAAI,CAACM,gBAAgBF,UAAUC,QAAAA,EAAW,QAAO;AACjD,UAAME,UAAmBrB,KAAKsB,MAAM7C,gBAAgBsB,cAAAA,CAAAA;AACpD,QAAI,CAACI,iBAAiBkB,OAAAA,EAAU,QAAO;AACvC,UAAMjC,MAAMF,QAAQE,OAAOC,KAAKD,IAAG;AACnC,QAAIA,MAAMiC,QAAQxB,MAAM1B,aAAc,QAAO;AAC7C,UAAMmB,UAA4B;MAChCC,KAAK8B,QAAQ9B;MACb,GAAI8B,QAAQ5B,SAASC,SAAY;QAAED,MAAM4B,QAAQ5B;MAAK,IAAI,CAAC;MAC3DE,OAAO0B,QAAQ1B;MACfC,KAAKyB,QAAQzB;MACbC,KAAKwB,QAAQxB;IACf;AACA,WAAOP;EACT,QAAQ;AACN,WAAO;EACT;AACF;AA/BgBqB;;;AC7ET,IAAMY,sBAAsB;AASnC,IAAMC,cAAc;AAMb,SAASC,mBACdC,MACAC,SAKC;AAED,QAAMC,QAAQC,kBAAkBH,MAAM;IACpCI,QAAQH,QAAQI,KAAKD;IACrBE,OAAOL,QAAQI,KAAKC;IACpB,GAAIL,QAAQM,QAAQC,SAAY;MAAED,KAAKN,QAAQM;IAAI,IAAI,CAAC;EAC1D,CAAA;AACA,QAAME,SAASC,mBAAmBb,qBAAqBK,OAAO;IAC5DS,MAAMb;IACNc,eAAeC,KAAKC,MAAMb,QAAQI,KAAKC,QAAQ,GAAA;IAC/CS,QAAQC,eAAef,QAAQgB,OAAO;EACxC,CAAA;AACAC,kBAAgBjB,QAAQkB,UAAUV,MAAAA;AACpC;AApBgBV;AAuBT,SAASqB,mBAAmBnB,SAAgD;AACjF,QAAMQ,SAASC,mBAAmBb,qBAAqB,IAAI;IACzDc,MAAMb;IACNc,eAAe;IACfG,QAAQC,eAAef,QAAQgB,OAAO;IACtCI,OAAO;EACT,CAAA;AACAH,kBAAgBjB,QAAQkB,UAAUV,MAAAA;AACpC;AARgBW;AAWT,SAASE,uBACdjB,MACAY,SAAgB;AAEhB,QAAMM,cAAcC,kBAAkBC,iBAAiBR,OAAAA,CAAAA,EAAUpB,mBAAAA;AACjE,MAAI0B,gBAAgBf,OAAW,QAAO;AACtC,SAAOkB,oBAAoBH,aAAa;IAAEnB,QAAQC,KAAKD;EAAO,CAAA;AAChE;AAPgBkB;AAaT,SAASK,kBACdtB,MACAuB,SACAX,SACAE,UAAiB;AAEjB,QAAMZ,MAAMsB,KAAKtB,IAAG;AACpB,MAAIA,MAAMqB,QAAQE,OAAOzB,KAAKC,QAAQ,EAAG;AACzCP,qBACE;IACEgC,IAAIH,QAAQI;IACZ,GAAIJ,QAAQK,SAASzB,SAAY;MAAEyB,MAAML,QAAQK;IAAK,IAAI,CAAC;IAC3DC,OAAON,QAAQM;EACjB,GACA;IAAE7B;IAAMY;IAASE;IAAUZ;EAAI,CAAA;AAEnC;AAhBgBoB;;;AC1EhB,SAASQ,YAAYC,oBAAoB;AACzC,SAASC,UAAUC,SAASC,MAAMC,SAASC,WAAW;AACtD,SAASC,qBAAqB;AAC9B,SACEC,cAAAA,aACAC,OAAAA,MACAC,UAAAA,SACAC,UAAAA,SACAC,qBAAAA,oBACAC,SAAAA,QACAC,gBACAC,kBACK;;;ACXP,SAASC,OAAOC,eAAeC,kBAAkB;;;;;;;;AAa1C,IAAMC,kCAAN,cAA8CC,cAAAA;SAAAA;;;;EACnD,YAA4BC,UAAkB;AAC5C,UAAM;MAAEA;IAAS,GAAGC,WAAWC,KAAK,GAAA,KADVF,WAAAA;EAE5B;AACF;AAIO,IAAMG,+BAAN,MAAMA;SAAAA;;;EACXC,MAAMC,WAA4CC,MAA2B;AAC3E,UAAMC,WAAWD,KAAKE,aAAY,EAAGC,YAAW;AAChDC,gBAAYH,UAAUF,UAAUL,QAAQ;EAC1C;AACF;;;;;;;;;;;;;;;;;;;;;;;ADXO,IAAMW,sBAAsBC,OAAO,qBAAA;AAEnC,IAAMC,qBAAqBD,OAAO,oBAAA;AAGzC,IAAME,aAAa;AAGnB,SAASC,SAAAA;AACP,SAAOC,cAAc,IAAIC,IAAI,UAAU,YAAYC,GAAG,CAAA;AACxD;AAFSH;AAIT,IAAMI,gBAAwC;EAC5C,OAAO;EACP,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,SAAS;EACT,UAAU;EACV,QAAQ;AACV;AAeO,IAAMC,sBAAN,MAAMA;SAAAA;;;;;EACMC,MAAMN,OAAAA;EAEvB,YACgDO,UACDC,aAC7C;SAF8CD,WAAAA;SACDC,cAAAA;EAC5C;;;EAOHC,QAAgB;AACd,UAAMC,YAAYC,KAAK,KAAKL,KAAK,YAAA;AACjC,QAAI,CAACM,WAAWF,SAAAA,GAAY;AAC1B,YAAM,IAAIG,mBAAkB,gDAAA;IAC9B;AAGA,UAAMC,OAAOC,aAAaL,WAAW,MAAA,EAAQM,WAC3C,KAAKjB,UAAAA,KACL,KAAK,KAAKQ,QAAQ,GAAG;AAGvB,UAAMU,SAAS,oCAAoC,KAAKV,QAAQ,6BAA6B,KAAKC,WAAW;AAC7G,WAAOM,KAAKI,SAAS,SAAA,IAAaJ,KAAKK,QAAQ,WAAW,GAAGF,MAAAA,SAAe,IAAIA,SAASH;EAC3F;EAIAM,MAAqBC,MAA8B;AACjD,UAAMC,OAAOC,SAASF,IAAAA;AACtB,QAAIC,SAASD,KAAM,OAAM,IAAIR,mBAAAA;AAC7B,UAAMW,OAAOC,QAAQ,KAAKnB,KAAK,QAAA;AAC/B,UAAMoB,YAAYD,QAAQD,MAAMF,IAAAA;AAChC,QAAI,CAACI,UAAUC,WAAWH,OAAOI,GAAAA,KAAQ,CAAChB,WAAWc,SAAAA,GAAY;AAC/D,YAAM,IAAIb,mBAAAA;IACZ;AACA,UAAMgB,OAAOzB,cAAc0B,QAAQR,IAAAA,CAAAA,KAAU;AAC7C,WAAO,IAAIS,eAAehB,aAAaW,SAAAA,GAAY;MAAEG;IAAK,CAAA;EAC5D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AR/DA,SAASG,SAASC,OAAc;AAC9B,SAAO,OAAOA,UAAU;AAC1B;AAFSD;AAcF,IAAME,wBAAN,MAAMA,uBAAAA;SAAAA;;;;;EACMC,SAAS,IAAIC,OAAOF,uBAAsBG,IAAI;;EAEvDC,oBAAoB;EAE5B,YACuDC,MACPC,UAC9C;SAFqDD,OAAAA;SACPC,WAAAA;EAC7C;;;;EAQHC,YAAoB;AAClB,QAAI,CAAC,KAAKF,KAAM,OAAM,IAAIG,mBAAAA;AAC1B,WAAOC,gBAAgB,KAAKH,QAAQ;EACtC;;;;;EAMA,MAEMI,MACIC,MACDC,SACqBC,UACK;AACjC,UAAMR,OAAO,KAAKS,YAAW;AAC7B,QAAI,CAAChB,SAASa,MAAMI,QAAAA,KAAa,CAACjB,SAASa,MAAMK,QAAAA,GAAW;AAC1D,YAAM,IAAIC,oBAAoB,qDAAA;IAChC;AACA,UAAMC,OAAO,MAAM,KAAKC,aAAad,MAAMM,KAAKI,UAAUJ,KAAKK,QAAQ;AACvE,QAAI,CAACE,KAAM,OAAM,IAAIE,sBAAsB;MAAEC,SAAS;IAAgC,CAAA;AACtFC,uBAAmBJ,MAAM;MAAEb;MAAMO;MAASC;IAAS,CAAA;AACnD,WAAO;MAAEU,YAAYC,iBAAiBb,KAAKc,UAAU,KAAKnB,QAAQ;IAAE;EACtE;;;;;EAOAoB,OAAcd,SAAyBC,UAAyB;AAE9Dc,uBAAmB;MAAEf;MAASC;IAAS,CAAA;AACvCe,gBAAYf,UAAU,GAAG,KAAKP,QAAQ,QAAQ;EAChD;EAEQQ,cAAqC;AAG3C,QAAI,CAAC,KAAKT,KAAM,OAAM,IAAIG,mBAAAA;AAC1B,WAAO,KAAKH;EACd;;;;;;EAOA,MAAcc,aACZd,MACAU,UACAC,UACsC;AACtC,QAAI;AACF,aAAQ,MAAMX,KAAKK,MAAMK,UAAUC,QAAAA,KAAc;IACnD,SAASa,OAAO;AACd,UAAI,CAAC,KAAKzB,mBAAmB;AAC3B,aAAKA,oBAAoB;AACzB,aAAKH,OAAO6B,KACV,uDAAuDD,iBAAiBE,QAAQF,MAAMR,UAAUW,OAAOH,KAAAA,CAAAA,EAAQ;MAEnH;AACA,aAAO;IACT;EACF;AACF;;;;;;;;;;;;;;;IApDWI,aAAa;;;;;;;;;;;;;;;;;;;2CAiBqC;;;;;;;;;;;;;;;AU3F7D,OAAO;AACP,SAIEC,cAIK;AACP,SAASC,oBAAoB;;;ACV7B,SAGEC,UAAAA,SACAC,cAAAA,aACAC,yBAAAA,8BACK;;;;;;;;;;;;;;;;;;AAoBA,IAAMC,wBAAN,MAAMA;SAAAA;;;;;EACX,YAC2CC,MACKC,UAC9C;SAFyCD,OAAAA;SACKC,WAAAA;EAC7C;EAEHC,YAAYC,SAAoC;AAC9C,QAAI,CAAC,KAAKH,KAAM,QAAO;AACvB,UAAMI,OAAOD,QAAQE,aAAY;AACjC,UAAMC,UAAUF,KAAKG,WAAU;AAC/B,UAAMC,UAAUC,uBAAuB,KAAKT,MAAMM,OAAAA;AAClD,QAAI,CAACE,SAAS;AACZ,YAAME,WAAWC,mBAAmBC,mBAAmBN,OAAAA,CAAAA;AACvD,YAAM,IAAIO,gCAAgC,GAAG,KAAKZ,QAAQ,mBAAmBS,QAAAA,EAAU;IACzF;AACAI,sBAAkB,KAAKd,MAAMQ,SAASF,SAASF,KAAKW,YAAW,CAAA;AAC/D,WAAO;EACT;AACF;;;;;;;;;;;AAWO,IAAMC,yBAAN,MAAMA;SAAAA;;;;EACX,YAAqDhB,MAAoC;SAApCA,OAAAA;EAAqC;EAE1FE,YAAYC,SAAoC;AAC9C,QAAI,CAAC,KAAKH,KAAM,QAAO;AACvB,UAAMI,OAAOD,QAAQE,aAAY;AACjC,UAAMC,UAAUF,KAAKG,WAAU;AAC/B,UAAMC,UAAUC,uBAAuB,KAAKT,MAAMM,OAAAA;AAClD,QAAI,CAACE,QAAS,OAAM,IAAIS,uBAAAA;AACxBH,sBAAkB,KAAKd,MAAMQ,SAASF,SAASF,KAAKW,YAAW,CAAA;AAC/D,WAAO;EACT;AACF;;;;;;;;;;;;;;;;;;ADjCA,IAAMG,kBAAkB;AAyFxB,SAASC,UAAUC,MAAY;AAC7B,SAAO,IAAIA,KAAKC,QAAQ,cAAc,EAAA,CAAA;AACxC;AAFSF;AAYT,SAASG,YACPC,YACAC,SACAC,YAAgB;AAEhB,QAAMC,SAAS;OAAKF,UAAU;MAACA;QAAW,CAAA;OAASD,cAAc,CAAA;;AACjEI,UAAQC,eAAeV,iBAAiBQ,QAAQD,UAAAA;AAClD;AAPSH;AAkBF,IAAMO,mBAAN,MAAMA,kBAAAA;SAAAA;;;EACX,OAAOC,SAASC,SASE;AAChBT,gBACES,QAAQC,QACRD,QAAQE,sBAAsBC,yBAAyBC,QACvDC,oBAAAA;AAEF,WAAO;MACLC,QAAQR;MACRS,SAAS;WAAKP,QAAQO,WAAW,CAAA;;MACjCC,aAAa;QAACH;;MACdI,WAAW;QACTC;QACAV,QAAQW;QACRR;WACIH,QAAQC,UAAU,CAAA;;MAExBW,SAAS;QAACF;;IACZ;EACF;AACF;;;;AAWO,IAAMG,yBAAN,MAAMA,wBAAAA;SAAAA;;;EACX,OAAOC,QAAQd,UAAmC,CAAC,GAAkB;AACnE,UAAMW,eAAyB;MAC7BI,SAASC;MACTC,UAAUC,qBAAqBlB,QAAQmB,aAAa;IACtD;AACA,WAAON,wBAAuBO,MAAMpB,SAASW,cAAcX,QAAQmB,kBAAkBf,MAAAA;EACvF;EAEA,OAAOiB,aAAarB,SAAsD;AACxE,UAAMW,eAAyB;MAC7BI,SAASC;MACTM,QAAQtB,QAAQsB,UAAU,CAAA;MAC1BC,YAAY,iCAAUC,SACpBN,qBAAqB,MAAMlB,QAAQyB,iBAAgB,GAAID,IAAAA,CAAAA,GAD7C;IAEd;AAIA,WAAOX,wBAAuBO,MAAMpB,SAASW,cAAc,IAAA;EAC7D;;EAGA,OAAeS,MACbpB,SAMAW,cACAT,qBACe;AACf,UAAMwB,WAAWtC,UAAUY,QAAQ0B,YAAY,UAAA;AAC/C,UAAMC,cAAcvC,UAAUY,QAAQ2B,eAAe,GAAGD,QAAAA,MAAc;AACtEnC,gBACES,QAAQC,QACRC,sBAAsB0B,wBAAwBxB,QAC9CyB,mBAAAA;AAEF,WAAO;MACLvB,QAAQO;MACRN,SAAS;WACHP,QAAQO,WAAW,CAAA;;;;QAIvBT,iBAAiBC,SAAS;UACxB,GAAIC,QAAQO,UAAU;YAAEA,SAASP,QAAQO;UAAQ,IAAI,CAAC;UACtD,GAAIP,QAAQC,SAAS;YAAEA,QAAQD,QAAQC;UAAO,IAAI,CAAC;UACnDU;UACAT;QACF,CAAA;QACA4B,aAAa/B,SAAS;UACpB;YAAEV,MAAMqC;YAAUpB,QAAQO;UAAuB;UACjD;YAAExB,MAAMsC;YAAarB,QAAQR;UAAiB;SAC/C;;MAEHU,aAAa;QACXqB;;;;WAII3B,sBAAsB;UAAC6B;YAAyB,CAAA;;MAEtDtB,WAAW;QACT;UAAEM,SAASiB;UAAqBf,UAAUS;QAAS;QACnD;UAAEX,SAASkB;UAAoBhB,UAAUU;QAAY;QACrDhB;QACAiB;;;WAGI5B,QAAQC,UAAU,CAAA;;;MAGxBW,SAAS;QAACd;;IACZ;EACF;AACF;;;;","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","listRuns","attributes","parseAttrFilters","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","login","ttlMs","renderLoginPage","basePath","loginAction","defaultReturnTo","JSON","stringify","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","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","maybeRenewSession","session","Date","iat","id","sub","name","roles","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","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","warnedOnHookThrow","auth","basePath","loginPage","NotFoundException","renderLoginPage","login","body","request","response","requireAuth","username","password","BadRequestException","user","runLoginHook","UnauthorizedException","message","issueSessionCookie","redirectTo","sanitizeReturnTo","returnTo","logout","clearSessionCookie","redirectRaw","error","warn","Error","String","passthrough","Module","RouterModule","Inject","Injectable","UnauthorizedException","DurableUiSessionGuard","auth","basePath","canActivate","context","http","switchToHttp","request","getRequest","session","readSessionFromRequest","returnTo","encodeURIComponent","originalRequestUrl","DashboardLoginRedirectException","maybeRenewSession","getResponse","DurableApiSessionGuard","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"]}