mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the remote-desktop plugin (B14)
This commit is contained in:
1 parent
0af2c94983
commit
14eb558b85
215 files changed
+30029
-3921
No files matched your search
@@ -477,7 +477,6 @@ export interface PluginHostRecord {
|
||||
folder: string | null;
|
||||
jumpHosts?: unknown;
|
||||
enableSsh?: boolean | null;
|
||||
enableRdp?: boolean | null;
|
||||
enableTerminal?: boolean | null;
|
||||
enableFileManager?: boolean | null;
|
||||
enableTunnel?: boolean | null;
|
||||
@@ -487,7 +486,11 @@ export interface PluginHostRecord {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Fields a plugin may set when creating a host it will own or manage. */
|
||||
/**
|
||||
* Fields a plugin may set when creating a host it will own or manage. Fields
|
||||
* that belong to another plugin's host settings (enableRdp, rdpPort) are
|
||||
* handed to that plugin's host import normalizer, as on a bulk import.
|
||||
*/
|
||||
export type PluginHostCreateInput = Partial<
|
||||
Omit<PluginHostRecord, "id" | "userId">
|
||||
> & {
|
||||
@@ -1116,10 +1119,69 @@ export interface PluginOpenIsolatedWindowRequest {
|
||||
* Electron only: rejects when the server is not running embedded in the
|
||||
* desktop app. Needs desktop:window.
|
||||
*/
|
||||
export interface PluginNativeRdpRequest {
|
||||
host: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
export interface PluginDesktop {
|
||||
openIsolatedWindow: (
|
||||
request: PluginOpenIsolatedWindowRequest,
|
||||
) => Promise<{ success: true }>;
|
||||
/**
|
||||
* Opens the operating system's own RDP client (mstsc on Windows) for a
|
||||
* host. The password is never passed; the client asks for it. Windows
|
||||
* desktop app only.
|
||||
*/
|
||||
launchNativeRdp: (
|
||||
request: PluginNativeRdpRequest,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
/** Whether the server runs embedded in the desktop app, so the calls above can work. */
|
||||
available: () => boolean;
|
||||
}
|
||||
|
||||
/** A host protocol whose credentials core keeps next to the host. */
|
||||
export type PluginHostProtocol = "rdp" | "vnc" | "telnet";
|
||||
|
||||
/**
|
||||
* What a plugin needs to hand a host's protocol login to something core
|
||||
* does not run (guacd): the address and the plaintext credentials the acting
|
||||
* user is allowed to use. A shared recipient gets the owner's shared
|
||||
* snapshot or their own override, never the owner's raw secret.
|
||||
*/
|
||||
export interface PluginProtocolTarget {
|
||||
host: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
ip: string;
|
||||
/** The host's SSH port, the old fallback when no protocol port is set. */
|
||||
port: number;
|
||||
ownerUserId: string;
|
||||
jumpHosts: Array<{ hostId: number }>;
|
||||
};
|
||||
/** True when the acting user is not the owner. */
|
||||
shared: boolean;
|
||||
auth: {
|
||||
/** "direct", "credential", or "none" when the user is asked at connect time. */
|
||||
authType: string;
|
||||
username: string;
|
||||
password: string;
|
||||
domain: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Plaintext host credentials, for a plugin that has to hand them to another
|
||||
* program. Needs credentials:read, and every call is audited.
|
||||
*/
|
||||
export interface PluginCredentials {
|
||||
/** Null when the host does not exist or the acting user cannot connect to it. */
|
||||
resolveHostProtocol: (
|
||||
hostId: number,
|
||||
protocol: PluginHostProtocol,
|
||||
) => Promise<PluginProtocolTarget | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1145,7 +1207,7 @@ export interface PluginAudit {
|
||||
* hardware (a serial port, a USB device) is the first caller. Core cannot
|
||||
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
|
||||
* capability here is a declared, reviewable, audited statement of intent
|
||||
* rather than a technical gate, matching credentials:read and process:spawn.
|
||||
* rather than a technical gate, matching process:spawn.
|
||||
*/
|
||||
export interface PluginCapabilities {
|
||||
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
|
||||
@@ -1181,6 +1243,8 @@ export interface PluginContext {
|
||||
readonly auth: PluginAuth;
|
||||
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
||||
readonly desktop: PluginDesktop;
|
||||
/** Plaintext host protocol credentials. Needs credentials:read. */
|
||||
readonly credentials: PluginCredentials;
|
||||
/** Audit lines under the plugin's own action names. */
|
||||
readonly audit: PluginAudit;
|
||||
|
||||
|
||||
@@ -317,6 +317,26 @@ export interface HostActionContribution {
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A connection protocol next to SSH, shown as a switch in the host editor's
|
||||
* General tab and in host filters and Quick Connect. Its switch and port are
|
||||
* this plugin's own host settings (declare both in contributes.settings.host).
|
||||
*/
|
||||
export interface HostProtocolContribution {
|
||||
id: string;
|
||||
/** Boolean host setting that turns the protocol on. */
|
||||
settingKey: string;
|
||||
/** Number host setting holding the port. */
|
||||
portKey?: string;
|
||||
defaultPort: number;
|
||||
titleKey: string;
|
||||
descriptionKey?: string;
|
||||
icon: IconComponent;
|
||||
order?: number;
|
||||
/** Offer it in Quick Connect, optionally with a domain field. */
|
||||
quickConnect?: { showDomain?: boolean };
|
||||
}
|
||||
|
||||
export interface HostBadgeContribution {
|
||||
id: string;
|
||||
when: (host: PluginHostRecord) => boolean;
|
||||
@@ -524,6 +544,7 @@ export interface TermixApp extends TermixAppInfo {
|
||||
section: HostEditorSectionContribution,
|
||||
) => Disposer;
|
||||
registerHostAction: (action: HostActionContribution) => Disposer;
|
||||
registerHostProtocol: (protocol: HostProtocolContribution) => Disposer;
|
||||
registerHostBadge: (badge: HostBadgeContribution) => Disposer;
|
||||
registerHostContextMenuItem: (
|
||||
item: HostContextMenuItemContribution,
|
||||
@@ -574,6 +595,11 @@ export interface TermixApp extends TermixAppInfo {
|
||||
|
||||
/** HTTP client for this plugin's /plugin-api/<id>/ routes. */
|
||||
api: PluginApiClient;
|
||||
/**
|
||||
* The same client for a resolved connection origin: "remote" reaches the
|
||||
* desktop app's connected remote server, anything else is `api`.
|
||||
*/
|
||||
apiFor: (origin?: unknown) => PluginApiClient;
|
||||
/** WebSocket URL and auth subprotocols for /plugin-ws/<id>/<path>. */
|
||||
wsUrl: (
|
||||
path: string,
|
||||
@@ -668,6 +694,7 @@ export interface PluginHostBridge {
|
||||
usePluginComponent: (
|
||||
id: string,
|
||||
) => ComponentType<Record<string, unknown>> | undefined;
|
||||
hostProtocols: (host: PluginHostRecord) => string[];
|
||||
}
|
||||
|
||||
let host: PluginHostBridge | null = null;
|
||||
@@ -778,6 +805,15 @@ export function useSshAuthTypes(): {
|
||||
* next to its own. Same permission and `when` filtering as ComponentSlot;
|
||||
* `context` is what each contribution's `when` sees.
|
||||
*/
|
||||
/**
|
||||
* The connection protocols a host has switched on: "ssh" plus every protocol
|
||||
* a running plugin registered with registerHostProtocol (RDP, VNC). Lets a
|
||||
* plugin offer a protocol without knowing which plugin owns its settings.
|
||||
*/
|
||||
export function hostProtocols(record: PluginHostRecord): string[] {
|
||||
return requireHost().hostProtocols(record);
|
||||
}
|
||||
|
||||
export function useSlotContributions(
|
||||
slotId: string,
|
||||
context?: Record<string, unknown>,
|
||||
|
||||
@@ -224,6 +224,12 @@ export interface PluginSettingsField {
|
||||
group?: string;
|
||||
/** Registered component id. Required when type is "custom". */
|
||||
component?: string;
|
||||
/**
|
||||
* Stored and validated like any field, but not drawn by the generic
|
||||
* settings form, because the plugin edits it in its own UI (a host editor
|
||||
* section).
|
||||
*/
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginHostSettingsContribution {
|
||||
@@ -382,6 +388,7 @@ const ALLOWED_SETTINGS_FIELD = [
|
||||
"permission",
|
||||
"group",
|
||||
"component",
|
||||
"hidden",
|
||||
];
|
||||
|
||||
/** Settings keys are stored as-is, so they stay short and index-safe. */
|
||||
@@ -933,6 +940,9 @@ function validateSettingsFields(
|
||||
if ("permission" in raw) {
|
||||
requireString(raw.permission, `${at}.permission`, errors);
|
||||
}
|
||||
if ("hidden" in raw && typeof raw.hidden !== "boolean") {
|
||||
errors.push(`${at}.hidden must be a boolean`);
|
||||
}
|
||||
|
||||
if (raw.type === "select" || raw.type === "multiselect") {
|
||||
validateSettingsOptions(raw.options, at, errors);
|
||||
|
||||
@@ -35,6 +35,10 @@ import type {
|
||||
} from "./backend.js";
|
||||
import type { SyncEntityRegistration } from "./backend.js";
|
||||
import type { PluginDatabase } from "./backend.js";
|
||||
import type {
|
||||
PluginNativeRdpRequest,
|
||||
PluginProtocolTarget,
|
||||
} from "./backend.js";
|
||||
|
||||
export interface FakeContextOptions {
|
||||
pluginId?: string;
|
||||
@@ -69,6 +73,13 @@ export interface FakeContextOptions {
|
||||
shareableRoles?: PluginShareableRole[];
|
||||
/** Hosts ctx.ssh.resolveHost answers with, secrets included. */
|
||||
sshHosts?: PluginSshHost[];
|
||||
/**
|
||||
* What ctx.credentials.resolveHostProtocol answers, keyed
|
||||
* "<hostId>:<protocol>". A missing key answers null.
|
||||
*/
|
||||
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||
/** What ctx.desktop.available() answers. Defaults to false. */
|
||||
desktopAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface FakeAuthRegistrations {
|
||||
@@ -123,6 +134,10 @@ export interface FakePluginContext {
|
||||
title?: string;
|
||||
ignoreCert?: boolean;
|
||||
}>;
|
||||
/** Every ctx.desktop.launchNativeRdp call, in order. */
|
||||
nativeRdpLaunches: PluginNativeRdpRequest[];
|
||||
/** Every ctx.credentials.resolveHostProtocol call, in order. */
|
||||
credentialReads: Array<{ hostId: number; protocol: string }>;
|
||||
/** Every ctx.audit.record entry, in order. */
|
||||
audits: Array<{ action: string; success: boolean; [key: string]: unknown }>;
|
||||
/** Every ctx.hosts.recordActivity call, in order. */
|
||||
@@ -206,6 +221,8 @@ export function createFakeContext(
|
||||
const sshConnections: FakePluginContext["sshConnections"] = [];
|
||||
const hostShares: FakePluginContext["hostShares"] = [];
|
||||
const desktopWindows: FakePluginContext["desktopWindows"] = [];
|
||||
const nativeRdpLaunches: FakePluginContext["nativeRdpLaunches"] = [];
|
||||
const credentialReads: FakePluginContext["credentialReads"] = [];
|
||||
const audits: FakePluginContext["audits"] = [];
|
||||
const activities: FakePluginContext["activities"] = [];
|
||||
const trackedSessions: number[] = [];
|
||||
@@ -635,6 +652,18 @@ export function createFakeContext(
|
||||
desktopWindows.push(request);
|
||||
return { success: true };
|
||||
},
|
||||
launchNativeRdp: async (request) => {
|
||||
nativeRdpLaunches.push(request);
|
||||
return { success: true };
|
||||
},
|
||||
available: () => options.desktopAvailable ?? false,
|
||||
},
|
||||
|
||||
credentials: {
|
||||
resolveHostProtocol: async (hostId, protocol) => {
|
||||
credentialReads.push({ hostId, protocol });
|
||||
return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
|
||||
},
|
||||
},
|
||||
|
||||
asUser: async (userId, fn) => {
|
||||
@@ -666,6 +695,8 @@ export function createFakeContext(
|
||||
hostShares,
|
||||
auth,
|
||||
desktopWindows,
|
||||
nativeRdpLaunches,
|
||||
credentialReads,
|
||||
audits,
|
||||
activities,
|
||||
trackedSessions,
|
||||
@@ -704,6 +735,10 @@ export interface MockContextOptions {
|
||||
services?: Record<string, object>;
|
||||
/** Hosts ctx.ssh.resolveHost serves. See FakeContextOptions. */
|
||||
sshHosts?: PluginSshHost[];
|
||||
/** What ctx.credentials serves. See FakeContextOptions. */
|
||||
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||
/** What ctx.desktop.available() answers. */
|
||||
desktopAvailable?: boolean;
|
||||
}
|
||||
|
||||
export interface MockPluginContext extends FakePluginContext {
|
||||
@@ -740,6 +775,8 @@ export function createMockCtx(
|
||||
hosts: options.hosts,
|
||||
sshHosts: options.sshHosts,
|
||||
services: options.services,
|
||||
protocolTargets: options.protocolTargets,
|
||||
desktopAvailable: options.desktopAvailable,
|
||||
manifest: {
|
||||
capabilities: options.capabilities ?? [],
|
||||
...options.manifest,
|
||||
@@ -972,6 +1009,18 @@ export function createMockCtx(
|
||||
require("desktop:window");
|
||||
return ctx.desktop.openIsolatedWindow(request);
|
||||
},
|
||||
launchNativeRdp: async (request) => {
|
||||
require("desktop:window");
|
||||
return ctx.desktop.launchNativeRdp(request);
|
||||
},
|
||||
available: () => ctx.desktop.available(),
|
||||
},
|
||||
|
||||
credentials: {
|
||||
resolveHostProtocol: async (hostId, protocol) => {
|
||||
require("credentials:read");
|
||||
return ctx.credentials.resolveHostProtocol(hostId, protocol);
|
||||
},
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
@@ -1180,6 +1229,7 @@ export interface RenderedPluginApp {
|
||||
tabs: () => string[];
|
||||
panels: () => string[];
|
||||
hostActions: () => Array<{ id: string; tabType?: string }>;
|
||||
hostProtocols: () => string[];
|
||||
hostEditorSections: () => string[];
|
||||
dashboardCards: () => string[];
|
||||
settingsComponents: () => string[];
|
||||
|
||||
Reference in new issue
Block a user