From 6741f4f165b2e583a53dbbfb6829b96242818b4f Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:01:28 -0400 Subject: [PATCH] refactor(orcad): move the Node host adapters out of orcad-entry.ts (#28530) Fixes main's lint: #28464 left orcad-entry.ts at 301 lines (max 300). No behaviour change. --- src/main/orcad/orcad-entry.ts | 73 +------------------ .../orcad/orcad-forked-child-paths.test.ts | 2 +- src/main/orcad/orcad-host-adapters.ts | 72 ++++++++++++++++++ .../orcad/orcad-wsl-guest-shell-path.test.ts | 2 +- .../daemon-generation-profile-store.ts | 2 +- 5 files changed, 79 insertions(+), 72 deletions(-) create mode 100644 src/main/orcad/orcad-host-adapters.ts diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index f27d0bcb35b..4d7bbe88c77 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -11,16 +11,11 @@ * Chromium proves available at startup. */ import process from 'node:process' -import { describeOrcadSecretProtectionGap } from './orcad-secret-protection-gap' -import { - getAppEnvironment, - setAppEnvironment, - type AppEnvironment -} from '../../shared/app-environment' -import { setSecretStore, type SecretStore } from '../../shared/secret-store' +import { getAppEnvironment } from '../../shared/app-environment' import type { ServeReadiness } from '../server/serve-readiness' -import { resolveOrcadInstallRoot, resolveOrcadPath, resolveUserDataPath } from './orcad-app-paths' -import { getOrcadCliLauncherPath, prepareOrcadCliLauncher } from './orcad-cli-launcher' +import { resolveOrcadInstallRoot, resolveUserDataPath } from './orcad-app-paths' +import { prepareOrcadCliLauncher } from './orcad-cli-launcher' +import { installOrcadHostAdapters, runOrcadQuitHandlers } from './orcad-host-adapters' import { describeOrcadBindExposure, resolveOrcadBindHost } from './orcad-bind-address' import { flushOrcadProfileStoreForShutdown, @@ -43,68 +38,8 @@ import { adoptWslHostLoginEnvironment } from './orcad-wsl-host-login-environment export { parseArgs } -let runOrcadQuitHandlers = (): void => {} let closeOrcadObservability = (): void => {} -function createNodeAppEnvironment(): AppEnvironment { - const quitHandlers: (() => void)[] = [] - // The main signal handler awaits runtime and browser teardown before process.exit. - // Keep will-quit callbacks synchronous, but never let them pre-empt that async barrier. - runOrcadQuitHandlers = (): void => { - const errors: unknown[] = [] - for (const handler of quitHandlers.splice(0)) { - try { - handler() - } catch (error) { - errors.push(error) - } - } - // Why throw: a quit handler that failed may leave a writer running, which keeps the lock. - if (errors.length > 0) { - throw new AggregateError(errors, 'orcad_quit_handlers_failed') - } - } - return { - getPath: resolveOrcadPath, - getAppPath: () => resolveOrcadInstallRoot(), - getVersion: () => process.env.ORCA_VERSION ?? '0.0.0-orcad', - // Why still true: consumers read this as "production build, not a dev checkout" — - // it gates HTTPS-only skill downloads, the real CLI command name, and shell-PATH - // hydration. Answering false to satisfy a path resolver would relax a security - // posture. Layout questions must ask whether the app root is an asar archive - // instead (see parcel-watcher-entry-path.ts). - isPackaged: () => true, - getCliLauncherPath: getOrcadCliLauncherPath, - onWillQuit: (handler) => quitHandlers.push(handler), - exit: (code = 0) => process.exit(code), - // Why []: there are no Chromium processes on this host to measure. - getAppMetrics: () => [] - } -} - -/** - * Why not silently plaintext: `isEncryptionAvailable() === false` already makes every - * caller fall back to unsealed storage, which is a security posture, not a detail. - * `describeProtectionGap()` gives the reason a client can surface. - */ -function createNodeSecretStore(): SecretStore { - return { - isEncryptionAvailable: () => false, - encryptString: () => { - throw new Error('orcad_secret_sealing_unavailable') - }, - decryptString: () => { - throw new Error('orcad_secret_sealing_unavailable') - }, - describeProtectionGap: () => describeOrcadSecretProtectionGap(process.platform) - } -} - -export function installOrcadHostAdapters(): void { - setAppEnvironment(createNodeAppEnvironment()) - setSecretStore(createNodeSecretStore()) -} - export type OrcadOptions = { port?: number json?: boolean diff --git a/src/main/orcad/orcad-forked-child-paths.test.ts b/src/main/orcad/orcad-forked-child-paths.test.ts index 70fe1009744..223daf8137a 100644 --- a/src/main/orcad/orcad-forked-child-paths.test.ts +++ b/src/main/orcad/orcad-forked-child-paths.test.ts @@ -13,7 +13,7 @@ import { resolveForeignSqliteReaderEntryPath } from '../foreign-sqlite-readers/foreign-sqlite-reader-entry-path' import { currentWorkerEntryLayout } from '../worker-thread-entry-path' -import { installOrcadHostAdapters } from './orcad-entry' +import { installOrcadHostAdapters } from './orcad-host-adapters' /** * The composition, not the pieces: orcad's real host adapters against the real resolver diff --git a/src/main/orcad/orcad-host-adapters.ts b/src/main/orcad/orcad-host-adapters.ts new file mode 100644 index 00000000000..92b482566db --- /dev/null +++ b/src/main/orcad/orcad-host-adapters.ts @@ -0,0 +1,72 @@ +/** The Node host's stand-ins for Electron's app environment and secret store. */ +import process from 'node:process' +import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environment' +import { setSecretStore, type SecretStore } from '../../shared/secret-store' +import { resolveOrcadInstallRoot, resolveOrcadPath } from './orcad-app-paths' +import { getOrcadCliLauncherPath } from './orcad-cli-launcher' +import { describeOrcadSecretProtectionGap } from './orcad-secret-protection-gap' + +let quitRegisteredHandlers = (): void => {} + +export function runOrcadQuitHandlers(): void { + quitRegisteredHandlers() +} + +function createNodeAppEnvironment(): AppEnvironment { + const quitHandlers: (() => void)[] = [] + // The main signal handler awaits runtime and browser teardown before process.exit. + // Keep will-quit callbacks synchronous, but never let them pre-empt that async barrier. + quitRegisteredHandlers = (): void => { + const errors: unknown[] = [] + for (const handler of quitHandlers.splice(0)) { + try { + handler() + } catch (error) { + errors.push(error) + } + } + // Why throw: a quit handler that failed may leave a writer running, which keeps the lock. + if (errors.length > 0) { + throw new AggregateError(errors, 'orcad_quit_handlers_failed') + } + } + return { + getPath: resolveOrcadPath, + getAppPath: () => resolveOrcadInstallRoot(), + getVersion: () => process.env.ORCA_VERSION ?? '0.0.0-orcad', + // Why still true: consumers read this as "production build, not a dev checkout" — + // it gates HTTPS-only skill downloads, the real CLI command name, and shell-PATH + // hydration. Answering false to satisfy a path resolver would relax a security + // posture. Layout questions must ask whether the app root is an asar archive + // instead (see parcel-watcher-entry-path.ts). + isPackaged: () => true, + getCliLauncherPath: getOrcadCliLauncherPath, + onWillQuit: (handler) => quitHandlers.push(handler), + exit: (code = 0) => process.exit(code), + // Why []: there are no Chromium processes on this host to measure. + getAppMetrics: () => [] + } +} + +/** + * Why not silently plaintext: `isEncryptionAvailable() === false` already makes every + * caller fall back to unsealed storage, which is a security posture, not a detail. + * `describeProtectionGap()` gives the reason a client can surface. + */ +function createNodeSecretStore(): SecretStore { + return { + isEncryptionAvailable: () => false, + encryptString: () => { + throw new Error('orcad_secret_sealing_unavailable') + }, + decryptString: () => { + throw new Error('orcad_secret_sealing_unavailable') + }, + describeProtectionGap: () => describeOrcadSecretProtectionGap(process.platform) + } +} + +export function installOrcadHostAdapters(): void { + setAppEnvironment(createNodeAppEnvironment()) + setSecretStore(createNodeSecretStore()) +} diff --git a/src/main/orcad/orcad-wsl-guest-shell-path.test.ts b/src/main/orcad/orcad-wsl-guest-shell-path.test.ts index 860eba35a30..d7de7c1d3b3 100644 --- a/src/main/orcad/orcad-wsl-guest-shell-path.test.ts +++ b/src/main/orcad/orcad-wsl-guest-shell-path.test.ts @@ -13,7 +13,7 @@ vi.mock('../startup/hydrate-shell-path', () => hydration) import { getAppEnvironment } from '../../shared/app-environment' import { hydrateShellPathForAgentDetection } from '../ipc/agent-detection-shell-path' -import { installOrcadHostAdapters } from './orcad-entry' +import { installOrcadHostAdapters } from './orcad-host-adapters' describe('orcad PATH hydration on a WSL guest', () => { const platform = process.platform diff --git a/tests/e2e/fixtures/daemon-generation-profile-store.ts b/tests/e2e/fixtures/daemon-generation-profile-store.ts index a709d84bcc8..6d27724a1d7 100644 --- a/tests/e2e/fixtures/daemon-generation-profile-store.ts +++ b/tests/e2e/fixtures/daemon-generation-profile-store.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import { installOrcadHostAdapters } from '../../../src/main/orcad/orcad-entry' +import { installOrcadHostAdapters } from '../../../src/main/orcad/orcad-host-adapters' import type { Store } from '../../../src/main/persistence' import { createProfileStateStore } from '../../../src/main/persistence/profile-state/profile-state-store-factory'