{"version":3,"file":"index.mjs","names":[],"sources":["../../forge-devtools/src/server/auth/authCode.ts","../../forge-devtools/src/server/session/DevToolsSession.ts","../../forge-devtools/src/server/trace/TraceMessageBuilder.ts","../../forge-devtools/src/server/trace/devToolsCookie.ts","../../forge-devtools/src/server/trace/TraceDispatcher.ts","../../forge-devtools/src/server/DevToolsServer.ts","../../forge-devtools/src/server/trace/RedisTraceChannel.ts","../../forge-devtools/src/server/setUpForgeDevTools.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\n\nconst CODE_LENGTH = 5\nconst CODE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\nconst CODE_EXPIRY_MS = 300_000\nconst MAX_ATTEMPTS = 3\n\nexport interface PendingCode {\n  readonly code: string\n  readonly expiresAt: number\n  attempts: number\n}\n\nexport type CodeValidationResult = 'valid' | 'invalid' | 'expired' | 'max-attempts'\n\nexport function generateCode(): PendingCode {\n  const bytes = randomBytes(CODE_LENGTH)\n  const code = Array.from(bytes, byte => CODE_CHARSET[byte % CODE_CHARSET.length]).join('')\n\n  return {\n    code,\n    expiresAt: Date.now() + CODE_EXPIRY_MS,\n    attempts: 0,\n  }\n}\n\nexport function validateCode(pending: PendingCode, submitted: string): CodeValidationResult {\n  if (Date.now() > pending.expiresAt) {\n    return 'expired'\n  }\n\n  if (pending.attempts >= MAX_ATTEMPTS) {\n    return 'max-attempts'\n  }\n\n  pending.attempts += 1\n\n  if (submitted.toUpperCase() === pending.code) {\n    return 'valid'\n  }\n\n  if (pending.attempts >= MAX_ATTEMPTS) {\n    return 'max-attempts'\n  }\n\n  return 'invalid'\n}\n","import { randomBytes } from 'node:crypto'\nimport type { WebSocket } from 'ws'\nimport { generateCode, validateCode, type CodeValidationResult, type PendingCode } from '../auth/authCode'\n\nexport type SessionState = 'awaiting-code' | 'authenticated' | 'rejected'\n\nexport default class DevToolsSession {\n  private readonly clientId = randomBytes(2).toString('hex')\n\n  private state: SessionState = 'awaiting-code'\n\n  private pendingCode: PendingCode\n\n  private cookieValue?: string\n\n  constructor(private readonly ws: WebSocket) {\n    this.pendingCode = generateCode()\n  }\n\n  getId(): string {\n    return this.clientId\n  }\n\n  getState(): SessionState {\n    return this.state\n  }\n\n  getCode(): string {\n    return this.pendingCode.code\n  }\n\n  getCodeExpiresIn(): number {\n    return Math.max(0, this.pendingCode.expiresAt - Date.now())\n  }\n\n  getCookieValue(): string | undefined {\n    return this.cookieValue\n  }\n\n  getRemainingAttempts(): number {\n    return 3 - this.pendingCode.attempts\n  }\n\n  authenticateImmediately(): void {\n    this.state = 'authenticated'\n    this.cookieValue = randomBytes(32).toString('hex')\n    this.send({ type: 'auth:success', cookie: this.cookieValue })\n  }\n\n  refreshCode(): void {\n    this.state = 'awaiting-code'\n    this.pendingCode = generateCode()\n    this.sendChallenge()\n  }\n\n  handleAuthCode(code: string): CodeValidationResult {\n    if (this.state !== 'awaiting-code') {\n      return 'max-attempts'\n    }\n\n    const result = validateCode(this.pendingCode, code)\n\n    if (result === 'valid') {\n      this.state = 'authenticated'\n      this.cookieValue = randomBytes(32).toString('hex')\n      this.send({ type: 'auth:success', cookie: this.cookieValue })\n\n      return result\n    }\n\n    if (result === 'expired') {\n      this.send({ type: 'auth:expired' })\n\n      return result\n    }\n\n    if (result === 'max-attempts') {\n      this.send({ type: 'auth:rejected' })\n\n      return result\n    }\n\n    this.send({ type: 'auth:failed', attemptsRemaining: this.getRemainingAttempts() })\n\n    return result\n  }\n\n  sendChallenge(): void {\n    this.send({ type: 'auth:challenge', expiresIn: this.getCodeExpiresIn() })\n  }\n\n  send(message: Record<string, unknown>): void {\n    if (this.ws.readyState === this.ws.OPEN) {\n      this.ws.send(JSON.stringify(message))\n    }\n  }\n\n  close(): void {\n    this.ws.close()\n  }\n}\n","import type { RequestTraceEvent, RequestTraceUnit } from '@ministryofjustice/hmpps-forge/core'\n\ntype RuntimeContextSnapshotTrace = Extract<RequestTraceUnit, { readonly kind: 'context-snapshot' }>\n\ninterface TraceSnapshotMessage {\n  readonly answers: Record<string, unknown>\n  readonly data: Record<string, unknown>\n  readonly reachabilityValidities?: Record<string, unknown>\n  readonly reachability?: unknown\n}\n\ninterface TraceUnitMessage {\n  readonly kind: string\n  readonly durationMs?: number\n  readonly selfDurationMs?: number\n  readonly startedAtMs?: number\n  readonly completedAtMs?: number\n  readonly executionSlices?: readonly { readonly startedAtMs: number; readonly completedAtMs: number }[]\n  readonly nodeId?: string\n  readonly variant?: string\n  readonly name?: string\n  readonly properties?: Record<string, unknown>\n  readonly fields?: Record<string, unknown>\n  readonly snapshot?: TraceSnapshotMessage\n  readonly children?: readonly TraceUnitMessage[]\n}\n\ninterface PhaseMessage {\n  readonly phase: string\n  readonly outcome: string\n  readonly durationMs: number\n  readonly startedAtMs: number\n  readonly completedAtMs?: number\n  readonly units: readonly TraceUnitMessage[]\n}\n\ninterface RouteContextMessage {\n  readonly journeyCode: string\n  readonly journeyTitle: string\n  readonly stepTitle?: string\n  readonly routeTemplatePath: string\n}\n\ninterface TraceReachabilityStepMessage {\n  readonly stepId: string\n  readonly routeTemplatePath: string\n  readonly code?: string\n  readonly declarationIndex: number\n  readonly isEntryPoint: boolean\n  readonly isConditionalEntry: boolean\n  readonly hasValidation: boolean\n  readonly isReachable: boolean\n  readonly isValid: boolean\n  readonly forwardRouteTemplatePaths: readonly string[]\n  readonly declaredForwardRouteTemplatePaths?: readonly string[]\n  readonly predecessorRouteTemplatePaths: readonly string[]\n  readonly tieBreakerPriority?: number\n}\n\ninterface TraceReachabilityMessage {\n  readonly currentStepId?: string\n  readonly steps: readonly TraceReachabilityStepMessage[]\n  readonly defaultEntryRouteTemplatePath?: string\n  readonly frontierRouteTemplatePath?: string\n  readonly canonicalPathRouteTemplatePaths: readonly string[]\n  readonly progressExists: boolean\n  readonly resumeActive: boolean\n  readonly resumeOutcome: 'no-op' | 'redirect'\n  readonly unreachableRedirect: 'entry' | 'frontier'\n}\n\ninterface TraceRequestMessage {\n  readonly params: Record<string, string>\n  readonly query: Record<string, string | string[]>\n  readonly post: Record<string, unknown>\n  readonly state: Record<string, unknown>\n  readonly headers: Record<string, string | string[] | undefined>\n  readonly cookies: Record<string, string | undefined>\n  readonly session: Record<string, unknown>\n}\n\nexport interface TraceMessage extends Record<string, unknown> {\n  readonly type: 'trace'\n  readonly method: string\n  readonly nodeId: string\n  readonly pathname: string\n  readonly trace: {\n    readonly outcome: string\n    readonly durationMs: number\n    readonly startedAtMs: number\n    readonly redirect?: { readonly target: string }\n    readonly error?: { readonly status?: number; readonly message: string; readonly stack?: string }\n    readonly reachability?: TraceReachabilityMessage\n    readonly phases: readonly PhaseMessage[]\n  }\n  readonly route: RouteContextMessage\n  readonly request: TraceRequestMessage\n}\n\nexport default class TraceMessageBuilder {\n  build(event: RequestTraceEvent): TraceMessage {\n    const phases = event.trace.phases.map(phase => ({\n      phase: phase.phase,\n      outcome: event.trace.outcome,\n      durationMs: phase.durationMs ?? 0,\n      startedAtMs: phase.startedAtMs,\n      completedAtMs: phase.completedAtMs,\n      units: phase.units.map(unit => this.buildUnit(unit)),\n    }))\n\n    return {\n      type: 'trace',\n      method: event.snapshot.method,\n      nodeId: event.snapshot.nodeId,\n      pathname: event.snapshot.location.pathname,\n      trace: {\n        outcome: event.trace.outcome,\n        durationMs: event.trace.durationMs ?? 0,\n        startedAtMs: event.trace.startedAtMs,\n        ...(event.trace.redirect ? { redirect: event.trace.redirect } : {}),\n        ...(event.trace.error ? { error: event.trace.error } : {}),\n        ...(event.trace.reachability ? { reachability: event.trace.reachability } : {}),\n        phases,\n      },\n      route: this.buildRouteContext(event),\n      request: {\n        params: event.snapshot.params,\n        query: event.snapshot.query,\n        post: event.snapshot.post,\n        state: event.snapshot.state,\n        headers: event.snapshot.headers,\n        cookies: event.snapshot.cookies,\n        // The core snapshot types the session as `unknown` (adapters vary); the panel treats it as a\n        // record for display, so widen it at this wire boundary.\n        session: event.snapshot.session as Record<string, unknown>,\n      },\n    }\n  }\n\n  private buildUnit(unit: RequestTraceUnit): TraceUnitMessage {\n    if (this.isContextSnapshotTrace(unit)) {\n      return {\n        kind: unit.kind,\n        name: unit.key,\n        snapshot: {\n          answers: unit.answers,\n          data: unit.data,\n          reachabilityValidities: unit.reachabilityValidities,\n          reachability: unit.reachability,\n        },\n        children: [],\n      }\n    }\n\n    const { properties, ...fields } = { ...unit.beginFields, ...unit.completeFields }\n\n    return {\n      kind: unit.kind,\n      durationMs: unit.durationMs,\n      selfDurationMs: unit.selfDurationMs,\n      startedAtMs: unit.startedAtMs,\n      completedAtMs: unit.completedAtMs,\n      executionSlices: unit.executionSlices,\n      nodeId: this.resolveStringField(fields, ['nodeId', 'id', 'stepId', 'currentStepId']),\n      variant: this.resolveStringField(fields, ['variant', 'mode', 'stage', 'blockType', 'outcome']),\n      name: this.resolveStringField(fields, ['name', 'code']),\n      ...(this.isRecord(properties) ? { properties } : {}),\n      ...(Object.keys(fields).length > 0 ? { fields } : {}),\n      children: unit.children.map(child => this.buildUnit(child)),\n    }\n  }\n\n  private isRecord(value: unknown): value is Record<string, unknown> {\n    return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value)\n  }\n\n  private resolveStringField(fields: Record<string, unknown>, names: readonly string[]): string | undefined {\n    const value = names.map(name => fields[name]).find(field => typeof field === 'string')\n\n    return typeof value === 'string' ? value : undefined\n  }\n\n  private buildRouteContext(event: RequestTraceEvent): RouteContextMessage {\n    const { route } = event\n\n    return {\n      journeyCode: route?.journeyCode ?? event.snapshot.nodeId,\n      journeyTitle: route?.journeyTitle ?? event.snapshot.nodeId,\n      stepTitle: route?.stepTitle,\n      routeTemplatePath: route?.routeTemplatePath ?? event.snapshot.location.pathname,\n    }\n  }\n\n  private isContextSnapshotTrace(unit: RequestTraceUnit): unit is RuntimeContextSnapshotTrace {\n    return unit.kind === 'context-snapshot'\n  }\n}\n","import type { RequestTraceEvent } from '@ministryofjustice/hmpps-forge/core'\n\nconst DEVTOOLS_COOKIE_NAME = '__forgeDevtools'\n\n/**\n * Reads the devtools cookie from the snapshot. Checks `cookies` first\n * (present when the app uses cookie-parser), then falls back to parsing\n * the raw `Cookie` header so the devtools works without requiring the\n * host app to install cookie-parser.\n */\nexport function extractDevToolsCookie(snapshot: RequestTraceEvent['snapshot']): string | undefined {\n  const fromParsed = snapshot.cookies?.[DEVTOOLS_COOKIE_NAME]\n\n  if (fromParsed) {\n    return fromParsed\n  }\n\n  const raw = snapshot.headers?.cookie\n\n  if (typeof raw !== 'string') {\n    return undefined\n  }\n\n  const prefix = `${DEVTOOLS_COOKIE_NAME}=`\n  const pair = raw.split(';').find(segment => segment.trimStart().startsWith(prefix))\n\n  return pair?.trimStart().slice(prefix.length)\n}\n","import type { RequestTraceEvent } from '@ministryofjustice/hmpps-forge/core'\nimport type DevToolsSession from '../session/DevToolsSession'\nimport TraceMessageBuilder from './TraceMessageBuilder'\nimport { extractDevToolsCookie } from './devToolsCookie'\n\nexport default class TraceDispatcher {\n  private readonly builder = new TraceMessageBuilder()\n\n  private readonly sessions = new Set<DevToolsSession>()\n\n  addSession(session: DevToolsSession): void {\n    this.sessions.add(session)\n  }\n\n  removeSession(session: DevToolsSession): void {\n    this.sessions.delete(session)\n  }\n\n  closeAll(): void {\n    this.sessions.forEach(session => session.close())\n    this.sessions.clear()\n  }\n\n  onRequestTrace(event: RequestTraceEvent): void {\n    const cookie = extractDevToolsCookie(event.snapshot)\n    const message = this.builder.build(event)\n\n    this.sessions.forEach(session => {\n      if (session.getState() !== 'authenticated') {\n        return\n      }\n\n      if (cookie !== session.getCookieValue()) {\n        return\n      }\n\n      session.send(message)\n    })\n  }\n}\n","import type { IncomingMessage, Server as HttpServer } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { WebSocketServer, type WebSocket } from 'ws'\nimport type { ForgeInstrumentationSink, RequestTraceEvent } from '@ministryofjustice/hmpps-forge/core'\nimport DevToolsSession from './session/DevToolsSession'\nimport TraceDispatcher from './trace/TraceDispatcher'\nimport { extractDevToolsCookie } from './trace/devToolsCookie'\nimport type RedisTraceChannel from './trace/RedisTraceChannel'\n\nexport interface DevToolsServerOptions {\n  readonly path: string\n  readonly logger: { info(message: string): void }\n  readonly noAuth?: boolean\n  readonly redisChannel?: RedisTraceChannel\n}\n\ninterface ClientMessage {\n  readonly type: string\n  readonly code?: string\n}\n\n/**\n * WebSocket server that authenticates devtools clients via a one-time code\n * and delegates trace delivery to a {@link TraceDispatcher}.\n */\nexport default class DevToolsServer implements ForgeInstrumentationSink {\n  private readonly wss: WebSocketServer\n\n  private readonly dispatcher = new TraceDispatcher()\n\n  private readonly logger: DevToolsServerOptions['logger']\n\n  private readonly webSocketPath: string\n\n  private readonly noAuth: boolean\n\n  private readonly redisChannel?: RedisTraceChannel\n\n  private attachedServer?: HttpServer\n\n  private redisStarted = false\n\n  constructor(options: DevToolsServerOptions) {\n    this.logger = options.logger\n    this.webSocketPath = normalizeWebSocketPath(options.path)\n    this.noAuth = options.noAuth ?? false\n    this.redisChannel = options.redisChannel\n\n    this.wss = new WebSocketServer({\n      noServer: true,\n      // Context takeover keeps the deflate window across messages on a\n      // connection, so each trace compresses against earlier ones (~11x\n      // measured); the threshold skips tiny auth/heartbeat frames.\n      perMessageDeflate: {\n        threshold: 1024,\n        serverNoContextTakeover: false,\n      },\n    })\n    this.wss.on('connection', ws => this.handleConnection(ws))\n  }\n\n  attach(server: HttpServer): void {\n    this.detach()\n    this.attachedServer = server\n    server.on('upgrade', this.handleUpgrade)\n    this.startRedisChannel()\n    this.logger.info(`Forge DevTools WebSocket server attached at ${this.webSocketPath}`)\n  }\n\n  detach(): void {\n    this.attachedServer?.off('upgrade', this.handleUpgrade)\n    this.attachedServer = undefined\n  }\n\n  close(): void {\n    this.detach()\n    this.dispatcher.closeAll()\n    this.wss.close()\n    this.redisChannel?.close()\n  }\n\n  onRequestTrace(event: RequestTraceEvent): void {\n    if (!this.redisChannel) {\n      this.dispatcher.onRequestTrace(event)\n\n      return\n    }\n\n    // Redis mode has one delivery path: publish even our own traces and hear\n    // them back through the subscription, like every other pod's.\n    this.redisChannel.publish(event)\n  }\n\n  shouldTrace(snapshot: RequestTraceEvent['snapshot']): boolean {\n    return extractDevToolsCookie(snapshot) !== undefined\n  }\n\n  private async startRedisChannel(): Promise<void> {\n    if (!this.redisChannel || this.redisStarted) {\n      return\n    }\n\n    this.redisStarted = true\n    await this.redisChannel.connect()\n    await this.redisChannel.subscribe(event => this.dispatcher.onRequestTrace(event))\n  }\n\n  private readonly handleUpgrade = (request: IncomingMessage, socket: Duplex, head: Buffer): void => {\n    if (!this.isDevToolsUpgrade(request)) {\n      return\n    }\n\n    this.wss.handleUpgrade(request, socket, head, ws => {\n      this.wss.emit('connection', ws, request)\n    })\n  }\n\n  private isDevToolsUpgrade(request: IncomingMessage): boolean {\n    const url = new URL(request.url ?? '/', 'http://localhost')\n\n    return url.pathname === this.webSocketPath\n  }\n\n  private handleConnection(ws: WebSocket): void {\n    const session = new DevToolsSession(ws)\n    this.dispatcher.addSession(session)\n\n    if (this.noAuth) {\n      session.authenticateImmediately()\n      this.logger.info(`DevTools client ${session.getId()} connected (auth disabled)`)\n    } else {\n      this.logCodeIssued(session)\n      session.sendChallenge()\n    }\n\n    ws.on('message', (raw: Buffer) => {\n      const message = parseMessage(raw)\n\n      if (!message) {\n        return\n      }\n\n      if (message.type === 'heartbeat:ping') {\n        session.send({ type: 'heartbeat:pong' })\n\n        return\n      }\n\n      if (message.type === 'auth:code' && message.code) {\n        const result = session.handleAuthCode(message.code)\n\n        if (result === 'valid') {\n          this.logger.info(`DevTools client ${session.getId()} authenticated`)\n        }\n      }\n\n      if (message.type === 'auth:refresh') {\n        session.refreshCode()\n        this.logCodeIssued(session)\n      }\n    })\n\n    ws.on('close', () => {\n      this.dispatcher.removeSession(session)\n      this.logger.info(`DevTools client ${session.getId()} disconnected`)\n    })\n  }\n\n  private logCodeIssued(session: DevToolsSession): void {\n    const expiresIn = Math.ceil(session.getCodeExpiresIn() / 60_000)\n\n    this.logger.info(\n      `DevTools client ${session.getId()} — auth code: \\x1b[1m${session.getCode()}\\x1b[0m (expires in ${expiresIn}m)`,\n    )\n  }\n}\n\nfunction normalizeWebSocketPath(path: string): string {\n  const withoutTrailingSlash = path.endsWith('/') ? path.slice(0, -1) : path\n  const normalizedBasePath = withoutTrailingSlash.startsWith('/') ? withoutTrailingSlash : `/${withoutTrailingSlash}`\n\n  return `${normalizedBasePath}/ws`\n}\n\nfunction parseMessage(raw: Buffer): ClientMessage | undefined {\n  try {\n    return JSON.parse(raw.toString()) as ClientMessage\n  } catch {\n    return undefined\n  }\n}\n","import { gunzipSync, gzipSync } from 'node:zlib'\nimport type { RequestTraceEvent } from '@ministryofjustice/hmpps-forge/core'\n\nconst TRACE_CHANNEL = '__forgeDevtools:traces'\n\n/**\n * Minimal structural view of a node-redis v4 client. The package takes no\n * dependency on `redis`; any client with this shape (the HMPPS template's)\n * satisfies it, so local-only users never pull `redis` in.\n */\nexport interface DevToolsRedisClient {\n  duplicate(): DevToolsRedisClient\n  connect(): Promise<unknown>\n  publish(channel: string, message: string | Buffer): Promise<unknown>\n  subscribe(channel: string, listener: (message: Buffer) => void, bufferMode: true): Promise<unknown>\n  quit(): Promise<unknown>\n}\n\ninterface RedisChannelLogger {\n  warn(message: string): void\n}\n\n/**\n * Carries request traces between app replicas over Redis pub/sub, so whichever\n * pod holds the panel's websocket sees every pod's traces rather than only the\n * slice that lands on it. Redis being down just means missing traces, so every\n * operation logs and swallows its failure instead of crashing the app.\n */\nexport default class RedisTraceChannel {\n  private readonly publisher: DevToolsRedisClient\n\n  private readonly subscriber: DevToolsRedisClient\n\n  constructor(\n    client: DevToolsRedisClient,\n    private readonly logger: RedisChannelLogger,\n  ) {\n    // A node-redis client in subscriber mode can't issue other commands, so the\n    // caller's client is never used directly - each role gets its own duplicate\n    // whose lifecycle this channel owns.\n    this.publisher = client.duplicate()\n    this.subscriber = client.duplicate()\n  }\n\n  async connect(): Promise<void> {\n    try {\n      await Promise.all([this.publisher.connect(), this.subscriber.connect()])\n    } catch (error) {\n      this.logger.warn(`Forge DevTools Redis channel failed to connect: ${this.describe(error)}`)\n    }\n  }\n\n  async publish(event: RequestTraceEvent): Promise<void> {\n    try {\n      // Sync gzip is acceptable here: traces average ~4 KB gzipped and publish at\n      // human clicking speed, so the event-loop stall is negligible.\n      const payload = gzipSync(JSON.stringify(event))\n      await this.publisher.publish(TRACE_CHANNEL, payload)\n    } catch (error) {\n      this.logger.warn(`Forge DevTools Redis publish failed: ${this.describe(error)}`)\n    }\n  }\n\n  async subscribe(onEvent: (event: RequestTraceEvent) => void): Promise<void> {\n    try {\n      await this.subscriber.subscribe(TRACE_CHANNEL, message => this.deliver(message, onEvent), true)\n    } catch (error) {\n      this.logger.warn(`Forge DevTools Redis subscribe failed: ${this.describe(error)}`)\n    }\n  }\n\n  async close(): Promise<void> {\n    try {\n      await Promise.all([this.publisher.quit(), this.subscriber.quit()])\n    } catch (error) {\n      this.logger.warn(`Forge DevTools Redis channel failed to close: ${this.describe(error)}`)\n    }\n  }\n\n  private deliver(message: Buffer, onEvent: (event: RequestTraceEvent) => void): void {\n    try {\n      const event = JSON.parse(gunzipSync(message).toString()) as RequestTraceEvent\n\n      onEvent(event)\n    } catch (error) {\n      // A malformed or truncated message must never take the listener down.\n      this.logger.warn(`Forge DevTools Redis dropped a malformed trace: ${this.describe(error)}`)\n    }\n  }\n\n  private describe(error: unknown): string {\n    return error instanceof Error ? error.message : String(error)\n  }\n}\n","import DevToolsServer from './DevToolsServer'\nimport RedisTraceChannel, { type DevToolsRedisClient } from './trace/RedisTraceChannel'\n\nconst DEFAULT_PATH = '/__forge-devtools'\n\nexport interface ForgeDevToolsOptions {\n  readonly path?: string\n  readonly logger?: { info(message: string): void; warn(message: string): void }\n  readonly noAuth?: boolean\n  readonly redis?: DevToolsRedisClient\n}\n\n/**\n * Creates the Forge DevTools bridge. Call once at app startup, attach it to\n * the app's HTTP server, and pass it into Forge's instrumentation sinks.\n *\n * Pass a `redis` client to run in multi-instance mode: traces are published to\n * the app's Redis so whichever replica holds the panel's websocket sees every\n * replica's traces. Omit it for today's in-process, single-instance behaviour.\n */\nexport function setUpForgeDevTools(options?: ForgeDevToolsOptions): DevToolsServer {\n  const path = options?.path ?? DEFAULT_PATH\n  const logger = options?.logger ?? console\n  const redisChannel = options?.redis ? new RedisTraceChannel(options.redis, logger) : undefined\n\n  return new DevToolsServer({ path, logger, noAuth: options?.noAuth, redisChannel })\n}\n"],"mappings":"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"}