Files
OpenTerminal/src/shared/api.ts
T
Bill 35583b2c15 feat(lock): main-window lock screen; harden env gating, sandbox and ssh teardown
Lock screen (main-window overlay, no second window):
- scrypt password verifier in <userData>/lock.json (per-write salt,
  timingSafeEqual); salt/hash/password never leave the main process
- lock now / idle auto-lock / lock at startup, growing failure cooldown,
  lock flags persisted so a quit-and-relaunch cannot bypass the lock
- locked shell and body portals go inert while sessions keep running;
  menu accelerators (reload, DevTools, zoom) are swallowed while locked
- settings gains a Lock tab; all copy in zh-CN/zh-TW/en/ja

Security and stability:
- packaged builds ignore ELECTRON_RENDERER_URL / OT_UPDATE_URL (devEnv)
- renderer preload runs with sandbox: true
- unreadable known_hosts store fails closed instead of being overwritten
- connect-time secrets gated by the bookmark's auth method (connectPromptFor)
- ssh stream teardown is idempotent: PTY_EXIT broadcasts exactly once
- sysinfo polling is refcounted for split panes (forceStopPolling on close)
- session-log index entries are path-contained; settings store writes
  atomically with EPERM/EBUSY retry
- sync-changelog tolerates CRLF checkouts (was a silent no-op)
- retry ssh2 host-key generation (flaky malformed key, ~1/500)

Tests: lock-store + lock-controller suites; transport-death PTY_EXIT e2e;
GitHub Actions CI (typecheck + 10 offline tests + build)
2026-09-24 22:16:43 +08:00

142 lines
6.5 KiB
TypeScript

import type {
AppInfo,
LayoutMeta,
PtyCreateOptions,
PtyCreateResult,
PtyDataEvent,
PtyExitEvent,
SessionOpenResult,
SessionSnapshot
} from './ipc'
import type { AppSettings } from './settings'
import type { ReleaseNote, UpdateState } from './ipc'
import type { LockOperationResult, LockPasswordInput, LockSettingsState } from './ipc'
import type {
HostKeyAction,
HostKeyPromptEvent,
SessionOpenOptions,
SshConnection,
SshConnectionInput
} from './connections'
import type { SysinfoMeta, SysinfoSample } from './sysinfo'
import type { SftpEntry, TransferProgressEvent } from './sftp'
import type { CommandItem, SessionLogMeta } from './commands'
import type { ZmodemDoneEvent, ZmodemOfferEvent, ZmodemResponse } from './ipc'
/**
* The single API surface exposed on `window.api` by the preload script.
* Main process implements the handlers; renderer consumes this contract.
*/
export interface AppApi {
appInfo(): Promise<AppInfo>
// ---- sessions (pty data plane is shared by local and ssh sessions) ----
createPty(opts?: PtyCreateOptions): Promise<PtyCreateResult>
/** open a local or ssh session; resolves once the session is ready to stream */
openSession(opts: SessionOpenOptions): Promise<SessionOpenResult>
/** output a session produced before this renderer subscribed (capped ring buffer) */
getSessionReplay(id: string): Promise<string>
writePty(id: string, data: string): void
resizePty(id: string, cols: number, rows: number): void
killPty(id: string): void
/** subscribe to all session output; returns unsubscribe */
onPtyData(cb: (e: PtyDataEvent) => void): () => void
onPtyExit(cb: (e: PtyExitEvent) => void): () => void
// ---- sysinfo (M3: ssh sessions only; main polls the remote and broadcasts) ----
/** start polling for an ssh session; stop happens automatically on session close */
sysinfoStart(id: string): void
sysinfoStop(id: string): void
onSysinfoMeta(cb: (e: { id: string; meta: SysinfoMeta }) => void): () => void
onSysinfoSample(cb: (e: { id: string; sample: SysinfoSample }) => void): () => void
// ---- sftp (M4: bound to an established ssh session) ----
listRemote(sessionId: string, dir: string): Promise<SftpEntry[]>
mkdirRemote(sessionId: string, dir: string, name: string): Promise<void>
renameRemote(sessionId: string, from: string, to: string): Promise<void>
deleteRemote(sessionId: string, paths: string[]): Promise<void>
/** mode = octal string, e.g. "755" or "0644" */
chmodRemote(sessionId: string, path: string, mode: string): Promise<void>
chownRemote(sessionId: string, path: string, uid: number, gid: number): Promise<void>
/** upload local files into remote dir; returns transferId */
uploadRemote(sessionId: string, localPaths: string[], remoteDir: string): Promise<string>
/** download remote paths into local dir; returns transferId */
downloadRemote(sessionId: string, remotePaths: string[], localDir: string): Promise<string>
cancelTransfer(transferId: string): void
onTransferProgress(cb: (e: TransferProgressEvent) => void): () => void
/** native open dialog; returns picked file paths (empty when cancelled) */
pickFiles(): Promise<string[]>
pickDirectory(): Promise<string>
// ---- command history / library + session logs (M5) ----
/** record one executed command (deduped, newest first, capped) */
recordCommand(cmd: string): Promise<void>
listHistory(): Promise<CommandItem[]>
clearHistory(): Promise<void>
listLibrary(): Promise<CommandItem[]>
saveLibraryItem(item: CommandItem): Promise<CommandItem>
deleteLibraryItem(id: string): Promise<void>
/** session output logging */
logStart(sessionId: string): Promise<SessionLogMeta>
logStop(sessionId: string): Promise<void>
listSessionLogs(): Promise<SessionLogMeta[]>
openLogsDir(): void
/** open a local directory in the OS file manager; false if not a directory */
openDirectory(dir: string): Promise<boolean>
// ---- ssh connections ----
listConnections(): Promise<SshConnection[]>
saveConnection(input: SshConnectionInput): Promise<SshConnection>
deleteConnection(id: string): Promise<void>
onHostKeyPrompt(cb: (e: HostKeyPromptEvent) => void): () => void
respondHostKey(promptId: string, action: HostKeyAction): void
// ---- zmodem (M6: main-process engine over ssh sessions; progress reuses transfer events) ----
onZmodemOffer(cb: (e: ZmodemOfferEvent) => void): () => void
zmodemRespond(resp: ZmodemResponse): void
onZmodemDone(cb: (e: ZmodemDoneEvent) => void): () => void
// ---- settings ----
getSettings(): Promise<AppSettings>
saveSettings(next: Partial<AppSettings>): Promise<AppSettings>
onSettingsChanged(cb: (s: AppSettings) => void): () => void
// ---- lock screen (main-window overlay; main owns the lock state) ----
getLockState(): Promise<LockSettingsState>
/** set or replace the password; verifies currentPassword when one exists */
setLockPassword(input: LockPasswordInput): Promise<LockOperationResult>
/** remove the password; verifies currentPassword when one exists */
clearLockPassword(input: { currentPassword?: string }): Promise<LockOperationResult>
/** answer the lock screen; resets the failure cooldown on success */
unlockLock(input: { password?: string }): Promise<LockOperationResult>
lockNow(): Promise<LockSettingsState>
onLockStateChanged(cb: (state: LockSettingsState) => void): () => void
// ---- fonts ----
listFonts(): Promise<string[]>
// ---- layout templates ----
listLayouts(): Promise<LayoutMeta[]>
getLayout(id: string): Promise<string | null>
/** json = serialized dockview layout (DockviewApi.toJSON()) */
saveLayout(meta: LayoutMeta, json: string): Promise<void>
deleteLayout(id: string): Promise<void>
// ---- updater (domestic feed first, GitHub fallback) ----
updateCheck(): Promise<UpdateState>
/** read the updater's current state without triggering a network check */
getUpdateState(): Promise<UpdateState>
updateDownload(): Promise<void>
updateInstall(): Promise<void>
updateChangelog(): Promise<ReleaseNote[]>
onUpdateState(cb: (s: UpdateState) => void): () => void
// ---- session snapshot (restore last layout + per-pane cwd) ----
getSessionState(): Promise<SessionSnapshot | null>
saveSessionState(snapshot: SessionSnapshot): Promise<SessionSnapshot | null>
/** resolve a cd-style argument against the current cwd (platform-aware) */
resolveCwd(current: string | undefined, arg: string): Promise<string | null>
/** normalize a shell-reported cwd payload (OSC 7 / OSC 9;9) */
reportCwd(payload: string): Promise<string | null>
}