{"version":3,"file":"companion.mjs","names":[],"sources":["../src/companion/main.ts"],"sourcesContent":["/**\n * Standalone Electron main for the desktop Live2D pet.\n *\n * The DSH host runs inside a Desktop `utilityProcess`, where `BrowserWindow`\n * is unavailable. This file is therefore launched by the host as its own\n * Electron main process (separate from the hosting DSH app) and is the only\n * place that can create the transparent, always-on-top pet window.\n *\n * Launch contract (argv, produced by `companionArgv` in `src/host/desktop-pet.ts`):\n *   <electron> <this-file> --pet-url=<url> --width=<n> --height=<n>\n *     [--x=<n>] [--y=<n>]\n *     [--renderer-header-name=<n>] [--renderer-header-value=<v>] [--carrier-origin=<o>]\n *\n * The window loads `/avatar/pet` from the DSH web server (via `--pet-url`).\n * `--carrier-origin` plus the renderer header let this window declare itself\n * as renderer traffic so the Desktop WebServer fence does not 403 it.\n */\nimport { app, BrowserWindow } from 'electron'\nimport { appendFileSync, mkdirSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\n\nconst MIN_WIDTH = 180\nconst MAX_WIDTH = 480\nconst RATIO = 250 / 180\nconst PET_PATHNAME = '/avatar/pet'\nconst USER_DATA_SUBDIR = 'dsh-live2d-avatar-pet'\nconst READY_MARKER = '__DSH_PET_READY__'\nconst FAIL_MARKER = '__DSH_PET_FAIL__'\n\n// Keep the companion's storage in the standard roaming app-data dir instead of\n// TEMP. The DSH session scopes TEMP to a locked temp dir, and Chromium's network\n// sandbox cannot grant access to a cache created there, which makes the window\n// fail to come up. Roaming is writable and sandbox-friendly.\nconst APP_DATA_ROOT = join(app.getPath('appData'), USER_DATA_SUBDIR)\nconst LOG_PATH = join(APP_DATA_ROOT, 'companion.log')\n\nfunction log(message: string): void {\n  try {\n    mkdirSync(dirname(LOG_PATH), { recursive: true })\n    appendFileSync(LOG_PATH, `[${new Date().toISOString()}] ${message}\\n`)\n  } catch { /* non-fatal */ }\n}\n\nprocess.on('uncaughtException', (error) => log(`uncaughtException: ${error instanceof Error ? error.stack : String(error)}`))\nprocess.on('unhandledRejection', (error) => log(`unhandledRejection: ${String(error)}`))\n\nfunction emit(marker: string): void {\n  process.stdout.write(`${marker}\\n`)\n}\n\ninterface Launch {\n  petUrl: string\n  width: number\n  height: number\n  x?: number\n  y?: number\n  rendererHeaderName?: string\n  rendererHeaderValue?: string\n  carrierOrigin?: string\n}\n\nfunction readArg(name: string): string | undefined {\n  const prefix = `--${name}=`\n  const argument = process.argv.find((value) => value.startsWith(prefix))\n  return argument === undefined ? undefined : argument.slice(prefix.length)\n}\n\nfunction finiteNumber(name: string, fallback: number, min: number, max: number): number {\n  const raw = readArg(name)\n  if (raw === undefined) return fallback\n  const value = Number(raw)\n  return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback\n}\n\nfunction parseLaunch(): Launch {\n  return {\n    petUrl: readArg('pet-url') ?? '',\n    width: finiteNumber('width', 300, MIN_WIDTH, MAX_WIDTH),\n    height: finiteNumber('height', Math.round(300 * RATIO), Math.round(MIN_WIDTH * RATIO), Math.round(MAX_WIDTH * RATIO)),\n    x: readArg('x') === undefined ? undefined : Number(readArg('x')),\n    y: readArg('y') === undefined ? undefined : Number(readArg('y')),\n    rendererHeaderName: readArg('renderer-header-name'),\n    rendererHeaderValue: readArg('renderer-header-value'),\n    carrierOrigin: readArg('carrier-origin'),\n  }\n}\n\n/** Pair an `http(s):` origin with its `ws(s):` counterpart for the carrier fence. */\nfunction pairedWebSocketOrigin(raw: string): string | undefined {\n  try {\n    const url = new URL(raw)\n    if (url.protocol === 'http:') url.protocol = 'ws:'\n    else if (url.protocol === 'https:') url.protocol = 'wss:'\n    else return undefined\n    return url.origin\n  } catch {\n    return undefined\n  }\n}\n\nfunction createWindow(launch: Launch): void {\n  const window = new BrowserWindow({\n    width: launch.width,\n    height: launch.height,\n    ...(launch.x !== undefined && Number.isFinite(launch.x) ? { x: Math.round(launch.x) } : {}),\n    ...(launch.y !== undefined && Number.isFinite(launch.y) ? { y: Math.round(launch.y) } : {}),\n    minWidth: MIN_WIDTH,\n    minHeight: Math.round(MIN_WIDTH * RATIO),\n    maxWidth: MAX_WIDTH,\n    maxHeight: Math.round(MAX_WIDTH * RATIO),\n    transparent: true,\n    frame: false,\n    hasShadow: false,\n    alwaysOnTop: true,\n    skipTaskbar: true,\n    resizable: true,\n    maximizable: false,\n    fullscreenable: false,\n    backgroundColor: '#00000000',\n    title: 'Avatar Pet',\n    webPreferences: {\n      contextIsolation: true,\n      nodeIntegration: false,\n      sandbox: true,\n      webSecurity: true,\n    },\n  })\n\n  window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))\n  window.webContents.on('will-navigate', (event, target) => {\n    try {\n      const destination = new URL(target)\n      const origin = new URL(launch.petUrl).origin\n      if (destination.origin !== origin || destination.pathname !== PET_PATHNAME) event.preventDefault()\n    } catch {\n      event.preventDefault()\n    }\n  })\n\n  if (launch.rendererHeaderName && launch.rendererHeaderValue && launch.carrierOrigin) {\n    const headerName = launch.rendererHeaderName\n    const headerValue = launch.rendererHeaderValue\n    const carrierOrigin = launch.carrierOrigin\n    const wsOrigin = pairedWebSocketOrigin(carrierOrigin)\n    const headerNameLower = headerName.toLowerCase()\n    const session = window.webContents.session\n    session.webRequest.onBeforeSendHeaders(\n      { urls: ['<all_urls>'] },\n      (details, callback) => {\n        const requestHeaders: Record<string, string> = { ...(details.requestHeaders ?? {}) }\n        for (const key of Object.keys(requestHeaders)) {\n          if (key.toLowerCase() === headerNameLower) delete requestHeaders[key]\n        }\n        try {\n          const origin = new URL(details.url ?? '').origin\n          if (origin === carrierOrigin || origin === wsOrigin) requestHeaders[headerName] = headerValue\n        } catch { /* ignore malformed URL */ }\n        callback({ requestHeaders })\n      },\n    )\n  }\n\n  window.on('closed', () => app.quit())\n  window.once('ready-to-show', () => window.show())\n  log(`window created for ${launch.petUrl}`)\n  // Signal the host only once the pet page has actually loaded and the window\n  // is shown, so the host does not report \"available\" for a window that never\n  // rendered (which would detach the in-page pet and leave nothing on screen).\n  window.webContents.once('did-finish-load', () => {\n    window.show()\n    log('pet page loaded, emitting READY')\n    emit(READY_MARKER)\n  })\n  window.webContents.once('did-fail-load', (_event, code, description, url, isMainFrame) => {\n    if (isMainFrame) {\n      log(`main frame failed to load: code=${code} desc=${description} url=${url}`)\n      emit(FAIL_MARKER)\n    }\n  })\n  void window.webContents.loadURL(launch.petUrl).catch((error) => {\n    log(`loadURL rejected: ${error instanceof Error ? error.message : String(error)}`)\n    emit(FAIL_MARKER)\n  })\n}\n\n// Keep storage in the standard roaming app-data dir (see APP_DATA_ROOT note).\ntry {\n  app.setPath('userData', APP_DATA_ROOT)\n} catch { /* keep Electron defaults */ }\n\napp.whenReady().then(() => {\n  const launch = parseLaunch()\n  if (!launch.petUrl) {\n    emit(FAIL_MARKER)\n    app.quit()\n    return\n  }\n  createWindow(launch)\n})\n\napp.on('window-all-closed', () => app.quit())\n"],"mappings":"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"}