feat(desktop): remote-host mode + OS-store client-cert mTLS (D-1/D-2)
- remote-hosts.ts + DesktopPrefs (remoteHosts/selectedHostId, immutable, back-compat) + tray host-picker. - will-navigate lock is now dynamic to the selected remote origin, fails closed on null (no open-redirect). - select-client-certificate handler: event.preventDefault() first, pick by issuer CN, non-silent missing-cert dialog + cert-less callback -> clean nginx rejection (Chromium sources certs from OS store). Verified: npm run typecheck exit 0; pure-helper assertions pass.
This commit is contained in:
@@ -3,30 +3,92 @@
|
||||
* after its window is hidden (the embedded server must keep running so remote
|
||||
* devices stay connected — the vibe-coding scenario).
|
||||
*
|
||||
* The context menu also hosts the D-1 host-picker (PLAN_NATIVE_TUNNEL /
|
||||
* C-Desktop): "This machine (local)" ↔ each configured remote tunnel host, as a
|
||||
* radio group, plus add/remove. The menu is rebuilt (never mutated) whenever the
|
||||
* host list or selection changes — call buildTrayMenu() and setContextMenu().
|
||||
*
|
||||
* Icon requirement: `iconPath` MUST point at an existing image. new Tray() with a
|
||||
* missing/invalid path can throw on some platforms; that failure surfaces to the
|
||||
* caller rather than being swallowed (a broken install should be visible, not
|
||||
* silently degrade). The caller owns whether to guard startup around it.
|
||||
*/
|
||||
import { Menu, Tray } from 'electron'
|
||||
import type { MenuItemConstructorOptions } from 'electron'
|
||||
import type { RemoteHost } from './types.js'
|
||||
|
||||
const TRAY_TOOLTIP = 'Web Terminal'
|
||||
/** Label for the local (embedded server) entry — selectedHostId === null. */
|
||||
const LOCAL_HOST_LABEL = 'This machine (local)'
|
||||
|
||||
export interface TrayHandlers {
|
||||
show(): void
|
||||
quit(): void
|
||||
/** Switch the active host; null → "This machine (local)". */
|
||||
selectHost(id: string | null): void
|
||||
/** Prompt for + add a new remote host. */
|
||||
addHost(): void
|
||||
/** Remove a configured remote host by id. */
|
||||
removeHost(id: string): void
|
||||
}
|
||||
|
||||
export function createTray(iconPath: string, handlers: TrayHandlers): Tray {
|
||||
const tray = new Tray(iconPath)
|
||||
/** The host-picker state the menu renders (the local entry is implicit). */
|
||||
export interface TrayHostState {
|
||||
readonly remoteHosts: readonly RemoteHost[]
|
||||
/** Selected host id, or null → local. */
|
||||
readonly selectedHostId: string | null
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ label: 'Show Web Terminal', click: handlers.show },
|
||||
/** Build the tray context menu for the given host state (pure construction). */
|
||||
export function buildTrayMenu(state: TrayHostState, handlers: TrayHandlers): Menu {
|
||||
const localItem: MenuItemConstructorOptions = {
|
||||
label: LOCAL_HOST_LABEL,
|
||||
type: 'radio',
|
||||
checked: state.selectedHostId === null,
|
||||
click: () => handlers.selectHost(null),
|
||||
}
|
||||
|
||||
const hostItems: MenuItemConstructorOptions[] = state.remoteHosts.map((host) => ({
|
||||
label: host.name,
|
||||
type: 'radio',
|
||||
checked: state.selectedHostId === host.id,
|
||||
click: () => handlers.selectHost(host.id),
|
||||
}))
|
||||
|
||||
const selectedRemoteId =
|
||||
state.selectedHostId !== null &&
|
||||
state.remoteHosts.some((host) => host.id === state.selectedHostId)
|
||||
? state.selectedHostId
|
||||
: null
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{ label: 'Host', enabled: false },
|
||||
localItem,
|
||||
...hostItems,
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: handlers.quit },
|
||||
])
|
||||
{ label: 'Add remote host…', click: () => handlers.addHost() },
|
||||
{
|
||||
label: 'Remove selected host',
|
||||
enabled: selectedRemoteId !== null,
|
||||
click: () => {
|
||||
if (selectedRemoteId !== null) handlers.removeHost(selectedRemoteId)
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Show Web Terminal', click: () => handlers.show() },
|
||||
{ label: 'Quit', click: () => handlers.quit() },
|
||||
]
|
||||
|
||||
return Menu.buildFromTemplate(template)
|
||||
}
|
||||
|
||||
export function createTray(
|
||||
iconPath: string,
|
||||
state: TrayHostState,
|
||||
handlers: TrayHandlers,
|
||||
): Tray {
|
||||
const tray = new Tray(iconPath)
|
||||
tray.setToolTip(TRAY_TOOLTIP)
|
||||
tray.setContextMenu(menu)
|
||||
tray.setContextMenu(buildTrayMenu(state, handlers))
|
||||
return tray
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user