mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: harden plugin api for separate repos, signed updates and generic external login
This commit is contained in:
1 parent
fb9c168b6b
commit
80944301dd
310 files changed
+6577
-4927
No files matched your search
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Where a connection is. Core's SSH pipeline uses the named stages; a plugin
|
||||
* may log its own (such as "guac_ready") and they pass through unchanged.
|
||||
*/
|
||||
export type ConnectionStage =
|
||||
| "dns"
|
||||
| "tcp"
|
||||
@@ -9,29 +13,7 @@ export type ConnectionStage =
|
||||
| "proxy"
|
||||
| "jump"
|
||||
| "validation"
|
||||
| "docker_connecting"
|
||||
| "docker_auth"
|
||||
| "docker_session"
|
||||
| "docker_ready"
|
||||
| "stats_connecting"
|
||||
| "stats_totp"
|
||||
| "stats_polling"
|
||||
| "stats_heartbeat"
|
||||
| "tunnel_connecting"
|
||||
| "tunnel_source"
|
||||
| "tunnel_endpoint"
|
||||
| "tunnel_forwarding"
|
||||
| "tunnel_retry"
|
||||
| "tunnel_connected"
|
||||
| "sftp_connecting"
|
||||
| "sftp_auth"
|
||||
| "sftp_connected"
|
||||
| "guac_token"
|
||||
| "guac_guacd"
|
||||
| "guac_connecting"
|
||||
| "guac_handshake"
|
||||
| "guac_ready"
|
||||
| "guac_disconnected";
|
||||
| (string & {});
|
||||
|
||||
export type LogEntry = {
|
||||
id: string;
|
||||
|
||||
@@ -85,14 +85,6 @@ function sanitizeStringArray(input: unknown): string[] {
|
||||
return input.filter((v): v is string => typeof v === "string");
|
||||
}
|
||||
|
||||
function sanitizeEnumArray<T extends string>(
|
||||
input: unknown,
|
||||
allowed: readonly T[],
|
||||
): T[] {
|
||||
if (!Array.isArray(input)) return [];
|
||||
return input.filter((v): v is T => allowed.includes(v as T));
|
||||
}
|
||||
|
||||
export function sanitizeCredentialSidebarPreferences(
|
||||
input: unknown,
|
||||
): CredentialSidebarPreferences {
|
||||
|
||||
Vendored
+1
-383
@@ -1,383 +1 @@
|
||||
interface ServerConfig {
|
||||
serverUrl?: string;
|
||||
allowInvalidCertificate?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ConnectionTestResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DialogOptions {
|
||||
title?: string;
|
||||
defaultPath?: string;
|
||||
buttonLabel?: string;
|
||||
filters?: Array<{ name: string; extensions: string[] }>;
|
||||
properties?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface DialogResult {
|
||||
canceled: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface LocalDirectoryResult {
|
||||
success: boolean;
|
||||
path: string;
|
||||
parent?: string;
|
||||
entries: LocalFileEntry[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface LocalCollectedFile {
|
||||
path: string;
|
||||
name: string;
|
||||
relativePath: string;
|
||||
size: number;
|
||||
created?: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
export type LocalPathMutationResult =
|
||||
| ({ success: true } & Partial<LocalFileEntry>)
|
||||
| { success: false; error?: string };
|
||||
|
||||
export interface ElectronAPI {
|
||||
getAppVersion: () => Promise<string>;
|
||||
setZoomFactor?: (factor: number) => void;
|
||||
getPlatform: () => Promise<string>;
|
||||
getSetting?: (key: string) => Promise<string | null | undefined>;
|
||||
setSetting?: (key: string, value: string) => Promise<void>;
|
||||
|
||||
getServerConfig: () => Promise<ServerConfig>;
|
||||
saveServerConfig: (config: ServerConfig) => Promise<{ success: boolean }>;
|
||||
testServerConnection: (
|
||||
serverUrl: string,
|
||||
allowInvalidCertificate?: boolean,
|
||||
) => Promise<ConnectionTestResult>;
|
||||
getC2STunnelConfig: () => Promise<unknown[]>;
|
||||
saveC2STunnelConfig: (
|
||||
config: unknown[],
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
checkLocalPortAvailable: (
|
||||
host: string,
|
||||
port: number,
|
||||
) => Promise<{ available: boolean; error?: string }>;
|
||||
getC2STunnelPresetDefaultName: () => Promise<string>;
|
||||
startC2STunnel: (
|
||||
tunnel: unknown,
|
||||
index: number,
|
||||
authToken?: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
tunnelName?: string;
|
||||
error?: string;
|
||||
code?: string;
|
||||
}>;
|
||||
testC2STunnel: (
|
||||
tunnel: unknown,
|
||||
index: number,
|
||||
authToken?: string,
|
||||
) => Promise<{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
code?: string;
|
||||
}>;
|
||||
stopC2STunnel: (
|
||||
tunnelName: string,
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
getC2STunnelStatuses: () => Promise<Record<string, unknown>>;
|
||||
onC2STunnelStatuses?: (
|
||||
callback: (statuses: Record<string, unknown>) => void,
|
||||
) => () => void;
|
||||
startC2SAutoStartTunnels: () => Promise<{
|
||||
success: boolean;
|
||||
started: number;
|
||||
errors: string[];
|
||||
}>;
|
||||
onCloseActiveTab?: (callback: () => void) => () => void;
|
||||
clearSessionCookies: () => Promise<void>;
|
||||
getSessionCookie: (
|
||||
name: string,
|
||||
targetUrl?: string,
|
||||
) => Promise<string | null>;
|
||||
waitForSessionCookie: (
|
||||
name: string,
|
||||
targetUrl?: string,
|
||||
previousValue?: string | null,
|
||||
timeoutMs?: number,
|
||||
) => Promise<{ success: boolean; value?: string; error?: string }>;
|
||||
|
||||
showSaveDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
showOpenDialog: (options: DialogOptions) => Promise<DialogResult>;
|
||||
getLocalHomeDirectory?: () => Promise<string>;
|
||||
listLocalDirectory?: (path?: string) => Promise<LocalDirectoryResult>;
|
||||
statLocalPaths?: (
|
||||
paths: string[],
|
||||
) => Promise<
|
||||
Array<
|
||||
| (LocalFileEntry & { success: true })
|
||||
| { success: false; path: string; error?: string }
|
||||
>
|
||||
>;
|
||||
collectLocalFiles?: (paths: string[]) => Promise<{
|
||||
success: boolean;
|
||||
files: LocalCollectedFile[];
|
||||
truncated?: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
createLocalFolder?: (
|
||||
parentPath: string,
|
||||
folderName: string,
|
||||
) => Promise<LocalPathMutationResult>;
|
||||
renameLocalPath?: (
|
||||
path: string,
|
||||
newName: string,
|
||||
) => Promise<LocalPathMutationResult>;
|
||||
trashLocalPath?: (path: string) => Promise<LocalPathMutationResult>;
|
||||
chmodLocalPath?: (
|
||||
path: string,
|
||||
permissions: string,
|
||||
) => Promise<LocalPathMutationResult>;
|
||||
|
||||
openExternalEditor: (fileData: {
|
||||
fileName: string;
|
||||
content: string;
|
||||
encoding?: "utf8" | "base64";
|
||||
editorPath?: string | null;
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
editId?: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
closeExternalEditor: (editId: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
onExternalEditorSaved?: (
|
||||
callback: (payload: {
|
||||
editId: string;
|
||||
content: string;
|
||||
encoding: "utf8";
|
||||
path: string;
|
||||
}) => void,
|
||||
) => () => void;
|
||||
|
||||
onUpdateAvailable: (callback: () => void) => void;
|
||||
onUpdateDownloaded: (callback: () => void) => void;
|
||||
|
||||
removeAllListeners: (channel: string) => void;
|
||||
isElectron: boolean;
|
||||
isDev: boolean;
|
||||
|
||||
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||
|
||||
createTempFile: (fileData: {
|
||||
fileName: string;
|
||||
content: string;
|
||||
encoding?: "base64" | "utf8";
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
tempId?: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
createTempFolder: (folderData: {
|
||||
folderName: string;
|
||||
files: Array<{
|
||||
relativePath: string;
|
||||
content: string;
|
||||
encoding?: "base64" | "utf8";
|
||||
}>;
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
tempId?: string;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
startDragToDesktop: (dragData: {
|
||||
tempId: string;
|
||||
fileName: string;
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
cleanupTempFile: (tempId: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
startLocalTerminal(dimensions: {
|
||||
cols: number;
|
||||
rows: number;
|
||||
shell?: "default" | "wsl";
|
||||
}): Promise<{ sessionId: string; shell: string }>;
|
||||
readyLocalTerminal(sessionId: string): Promise<boolean>;
|
||||
writeLocalTerminal(sessionId: string, data: string): Promise<boolean>;
|
||||
resizeLocalTerminal(
|
||||
sessionId: string,
|
||||
cols: number,
|
||||
rows: number,
|
||||
): Promise<boolean>;
|
||||
closeLocalTerminal(sessionId: string): Promise<boolean>;
|
||||
onLocalTerminalData(
|
||||
sessionId: string,
|
||||
callback: (data: string) => void,
|
||||
): () => void;
|
||||
onLocalTerminalExit(
|
||||
sessionId: string,
|
||||
callback: (exitCode: number) => void,
|
||||
): () => void;
|
||||
|
||||
/** Local disk browsing for the dual-pane file manager (desktop only). */
|
||||
localFs?: {
|
||||
home: () => Promise<LocalFsResult<LocalFsHomeInfo>>;
|
||||
list: (dirPath: string) => Promise<LocalFsResult<LocalDirectoryListing>>;
|
||||
mkdir: (
|
||||
parentPath: string,
|
||||
name: string,
|
||||
) => Promise<LocalFsResult<{ path: string }>>;
|
||||
createFile: (
|
||||
parentPath: string,
|
||||
name: string,
|
||||
) => Promise<LocalFsResult<{ path: string }>>;
|
||||
rename: (
|
||||
oldPath: string,
|
||||
newName: string,
|
||||
) => Promise<LocalFsResult<{ path: string }>>;
|
||||
trash: (paths: string[]) => Promise<LocalFsResult<LocalTrashResult>>;
|
||||
ensureDir: (
|
||||
dirPath: string,
|
||||
rootPath?: string,
|
||||
) => Promise<LocalFsResult<{ path: string }>>;
|
||||
exists: (paths: string[]) => Promise<LocalFsResult<{ existing: string[] }>>;
|
||||
walk: (paths: string[]) => Promise<LocalFsResult<LocalWalkResult>>;
|
||||
reveal: (targetPath: string) => Promise<LocalFsResult<unknown>>;
|
||||
open: (targetPath: string) => Promise<LocalFsResult<unknown>>;
|
||||
};
|
||||
|
||||
/** Streamed local<->remote transfers driven by the main process. */
|
||||
localTransfer?: {
|
||||
upload: (
|
||||
options: LocalUploadRequest,
|
||||
) => Promise<LocalFsResult<{ bytes: number }>>;
|
||||
download: (
|
||||
options: LocalDownloadRequest,
|
||||
) => Promise<LocalFsResult<{ path: string }>>;
|
||||
cancel: (
|
||||
transferId: string,
|
||||
) => Promise<LocalFsResult<{ cancelled: boolean }>>;
|
||||
onProgress: (
|
||||
callback: (payload: LocalTransferProgress) => void,
|
||||
) => () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export type LocalFsResult<T> =
|
||||
({ success: true } & T) | { success: false; error: string; code?: string };
|
||||
|
||||
export interface LocalFsHomeInfo {
|
||||
home: string;
|
||||
separator: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
export interface LocalFileEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
type: "file" | "directory" | "link" | "other";
|
||||
created?: string;
|
||||
modified?: string;
|
||||
permissions?: string;
|
||||
owner?: string;
|
||||
group?: string;
|
||||
size: number;
|
||||
modifiedTimestamp?: number;
|
||||
linkTarget?: string;
|
||||
hidden: boolean;
|
||||
}
|
||||
|
||||
export interface LocalDirectoryListing {
|
||||
path: string;
|
||||
parent: string | null;
|
||||
entries: LocalFileEntry[];
|
||||
}
|
||||
|
||||
export interface LocalTrashResult {
|
||||
trashed: number;
|
||||
failed: Array<{ path: string; error: string }>;
|
||||
}
|
||||
|
||||
export interface LocalWalkFile {
|
||||
localPath: string;
|
||||
/** Path relative to the drop root; "/"-separated and includes the root's own name. */
|
||||
relativePath: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface LocalWalkResult {
|
||||
files: LocalWalkFile[];
|
||||
emptyDirs: string[];
|
||||
totalBytes: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which Termix backend a transfer talks to. The main process resolves the
|
||||
* actual URL and credentials for the origin; the renderer never supplies them.
|
||||
*/
|
||||
export type LocalTransferOrigin = "local" | "remote";
|
||||
|
||||
export interface LocalUploadRequest {
|
||||
transferId: string;
|
||||
origin: LocalTransferOrigin;
|
||||
fields: Record<string, string>;
|
||||
localPath: string;
|
||||
fileName: string;
|
||||
deviceId?: string;
|
||||
/** The renderer's token for the embedded backend (origin "local"); sent as a Bearer header. */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
export interface LocalDownloadRequest {
|
||||
transferId: string;
|
||||
origin: LocalTransferOrigin;
|
||||
body: Record<string, unknown>;
|
||||
destPath: string;
|
||||
/** Selected download folder; destPath must resolve strictly inside it (code EINVAL otherwise). */
|
||||
rootPath: string;
|
||||
expectedSize?: number;
|
||||
/** Replace an existing file at destPath; otherwise the transfer is refused with code EEXIST. */
|
||||
overwrite?: boolean;
|
||||
deviceId?: string;
|
||||
/** The renderer's token for the embedded backend (origin "local"); sent as a Bearer header. */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
export interface LocalTransferProgress {
|
||||
transferId: string;
|
||||
transferred: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI;
|
||||
IS_ELECTRON: boolean;
|
||||
electronClipboard?: {
|
||||
writeText(text: string): Promise<boolean>;
|
||||
readText(): Promise<string>;
|
||||
};
|
||||
}
|
||||
}
|
||||
export type * from "@termix/plugin-sdk/desktop";
|
||||
Vendored
-225
@@ -1,225 +0,0 @@
|
||||
declare module "guacamole-common-js" {
|
||||
namespace Guacamole {
|
||||
class Client {
|
||||
constructor(tunnel: Tunnel);
|
||||
connect(data?: string): void;
|
||||
disconnect(): void;
|
||||
getDisplay(): Display;
|
||||
sendKeyEvent(pressed: number, keysym: number): void;
|
||||
sendMouseState(state: Mouse.State): void;
|
||||
sendSize(width: number, height: number): void;
|
||||
setClipboard(stream: OutputStream, mimetype: string): void;
|
||||
createClipboardStream(mimetype: string): OutputStream;
|
||||
onstatechange: ((state: number) => void) | null;
|
||||
onerror: ((error: Status) => void) | null;
|
||||
onsync: ((timestamp: number) => void) | null;
|
||||
onclipboard: ((stream: InputStream, mimetype: string) => void) | null;
|
||||
onaudio: ((stream: InputStream, mimetype: string) => void) | null;
|
||||
onfile:
|
||||
| ((stream: InputStream, mimetype: string, filename: string) => void)
|
||||
| null;
|
||||
onfilesystem: ((filesystem: Object, name: string) => void) | null;
|
||||
}
|
||||
|
||||
// Mirrors Guacamole.Object: a named collection of streams. Within this
|
||||
// namespace `Object` refers to this class, not the global one.
|
||||
class Object {
|
||||
static readonly ROOT_STREAM: string;
|
||||
static readonly STREAM_INDEX_MIMETYPE: string;
|
||||
readonly index: number;
|
||||
requestInputStream(
|
||||
name: string,
|
||||
bodyCallback?: (stream: InputStream, mimetype: string) => void,
|
||||
): void;
|
||||
createOutputStream(mimetype: string, name: string): OutputStream;
|
||||
onbody: ((stream: InputStream, mimetype: string) => void) | null;
|
||||
onundefine: (() => void) | null;
|
||||
}
|
||||
|
||||
class AudioPlayer {
|
||||
static getInstance(
|
||||
stream: InputStream,
|
||||
mimetype: string,
|
||||
): AudioPlayer | null;
|
||||
sync(): void;
|
||||
}
|
||||
|
||||
class Display {
|
||||
getElement(): HTMLElement;
|
||||
getWidth(): number;
|
||||
getHeight(): number;
|
||||
scale(scale: number): void;
|
||||
onresize: (() => void) | null;
|
||||
}
|
||||
|
||||
class Tunnel {
|
||||
onerror: ((status: Status) => void) | null;
|
||||
onstatechange: ((state: number) => void) | null;
|
||||
}
|
||||
|
||||
class WebSocketTunnel extends Tunnel {
|
||||
constructor(url: string);
|
||||
}
|
||||
|
||||
class SessionRecording {
|
||||
constructor(source: Blob | Tunnel, refreshInterval?: number);
|
||||
onload: (() => void) | null;
|
||||
onerror: ((message: string) => void) | null;
|
||||
onprogress: ((duration: number, parsedSize: number) => void) | null;
|
||||
onplay: (() => void) | null;
|
||||
onpause: (() => void) | null;
|
||||
onseek:
|
||||
((position: number, current: number, total: number) => void) | null;
|
||||
getDisplay(): Display;
|
||||
getPosition(): number;
|
||||
getDuration(): number;
|
||||
isPlaying(): boolean;
|
||||
play(): void;
|
||||
pause(): void;
|
||||
seek(position: number, callback?: () => void): void;
|
||||
abort(): void;
|
||||
}
|
||||
|
||||
class Mouse {
|
||||
constructor(element: HTMLElement);
|
||||
onmousedown: ((state: Mouse.State) => void) | null;
|
||||
onmouseup: ((state: Mouse.State) => void) | null;
|
||||
onmousemove: ((state: Mouse.State) => void) | null;
|
||||
onmouseout: ((state: Mouse.State) => void) | null;
|
||||
}
|
||||
|
||||
namespace Mouse {
|
||||
class State {
|
||||
constructor(
|
||||
x: number,
|
||||
y: number,
|
||||
left?: boolean,
|
||||
middle?: boolean,
|
||||
right?: boolean,
|
||||
up?: boolean,
|
||||
down?: boolean,
|
||||
);
|
||||
constructor(state: {
|
||||
x: number;
|
||||
y: number;
|
||||
left?: boolean;
|
||||
middle?: boolean;
|
||||
right?: boolean;
|
||||
up?: boolean;
|
||||
down?: boolean;
|
||||
});
|
||||
x: number;
|
||||
y: number;
|
||||
left: boolean;
|
||||
middle: boolean;
|
||||
right: boolean;
|
||||
up: boolean;
|
||||
down: boolean;
|
||||
}
|
||||
|
||||
interface MouseEvent {
|
||||
state: Mouse.State;
|
||||
preventDefault(): void;
|
||||
stopPropagation(): void;
|
||||
}
|
||||
|
||||
class Touchpad {
|
||||
constructor(element: HTMLElement);
|
||||
currentState: Mouse.State;
|
||||
clickTimingThreshold: number;
|
||||
clickMoveThreshold: number;
|
||||
scrollThreshold: number;
|
||||
onEach(
|
||||
types: string[],
|
||||
listener: (event: Mouse.MouseEvent) => void,
|
||||
): void;
|
||||
on(type: string, listener: (event: Mouse.MouseEvent) => void): void;
|
||||
}
|
||||
|
||||
class Touchscreen {
|
||||
constructor(element: HTMLElement);
|
||||
currentState: Mouse.State;
|
||||
clickTimingThreshold: number;
|
||||
clickMoveThreshold: number;
|
||||
scrollThreshold: number;
|
||||
longPressThreshold: number;
|
||||
onEach(
|
||||
types: string[],
|
||||
listener: (event: Mouse.MouseEvent) => void,
|
||||
): void;
|
||||
on(type: string, listener: (event: Mouse.MouseEvent) => void): void;
|
||||
}
|
||||
}
|
||||
|
||||
class Keyboard {
|
||||
constructor(element: Document | HTMLElement);
|
||||
reset(): void;
|
||||
onkeydown: ((keysym: number) => void) | null;
|
||||
onkeyup: ((keysym: number) => void) | null;
|
||||
}
|
||||
|
||||
class Status {
|
||||
code: number;
|
||||
message: string;
|
||||
isError(): boolean;
|
||||
static readonly Code: {
|
||||
SUCCESS: number;
|
||||
UNSUPPORTED: number;
|
||||
SERVER_ERROR: number;
|
||||
SERVER_BUSY: number;
|
||||
UPSTREAM_TIMEOUT: number;
|
||||
UPSTREAM_ERROR: number;
|
||||
RESOURCE_NOT_FOUND: number;
|
||||
RESOURCE_CONFLICT: number;
|
||||
RESOURCE_CLOSED: number;
|
||||
CLIENT_BAD_REQUEST: number;
|
||||
CLIENT_UNAUTHORIZED: number;
|
||||
CLIENT_FORBIDDEN: number;
|
||||
CLIENT_TIMEOUT: number;
|
||||
};
|
||||
}
|
||||
|
||||
class InputStream {
|
||||
onblob: ((data: string) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
sendAck(message: string, code: number): void;
|
||||
}
|
||||
|
||||
class OutputStream {
|
||||
sendBlob(data: string): void;
|
||||
sendEnd(): void;
|
||||
}
|
||||
|
||||
class StringReader {
|
||||
constructor(stream: InputStream);
|
||||
ontext: ((text: string) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
}
|
||||
|
||||
class StringWriter {
|
||||
constructor(stream: OutputStream);
|
||||
sendText(text: string): void;
|
||||
sendEnd(): void;
|
||||
}
|
||||
|
||||
class BlobReader {
|
||||
constructor(stream: InputStream, mimetype: string);
|
||||
getBlob(): Blob;
|
||||
getLength(): number;
|
||||
onprogress: ((length: number) => void) | null;
|
||||
onend: (() => void) | null;
|
||||
}
|
||||
|
||||
class BlobWriter {
|
||||
constructor(stream: OutputStream);
|
||||
sendBlob(blob: Blob): void;
|
||||
sendEnd(): void;
|
||||
onack: ((status: Status) => void) | null;
|
||||
onerror: ((blob: Blob, offset: number, error: Status) => void) | null;
|
||||
onprogress: ((blob: Blob, offset: number) => void) | null;
|
||||
oncomplete: ((blob: Blob) => void) | null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Guacamole;
|
||||
}
|
||||
@@ -32,48 +32,6 @@ export interface HostProtocolAuthInput {
|
||||
fields?: Record<string, string | null>;
|
||||
}
|
||||
|
||||
export type WebEndpointAccess = "direct" | "tunnel";
|
||||
export type WebEndpointRender = "external" | "embedded";
|
||||
|
||||
/** One web UI a host serves, declared in the host's settings. */
|
||||
export interface WebEndpoint {
|
||||
/**
|
||||
* Stable identifier. Must NOT be derived from the port: it keys both the
|
||||
* tunnel name and the tab identity, so editing a port has to leave a live
|
||||
* tunnel findable under the same name.
|
||||
*/
|
||||
id: string;
|
||||
label: string;
|
||||
scheme: "http" | "https";
|
||||
port: number;
|
||||
/** Defaults to "/". Normalized at the storage boundary, never here. */
|
||||
path?: string;
|
||||
access: WebEndpointAccess;
|
||||
render: WebEndpointRender;
|
||||
/**
|
||||
* Direct endpoints only. Allows an invalid TLS certificate for this
|
||||
* endpoint's exact origin. A no-op for tunnel access, whose host component
|
||||
* is loopback and therefore already exempt.
|
||||
*/
|
||||
ignoreCert?: boolean;
|
||||
/**
|
||||
* Tunnel endpoints only. Where the backend binds the forward, exactly as
|
||||
* the server tunnels feature exposes it. Defaults to 127.0.0.1, reachable
|
||||
* only from the machine running the backend. A web deployment runs the
|
||||
* backend on a server, so reaching the forward from a browser needs an
|
||||
* address that machine answers on -- which also exposes the target's web UI
|
||||
* to anyone who can reach the port, with no login in front of it.
|
||||
*/
|
||||
bindHost?: string;
|
||||
/**
|
||||
* Tunnel endpoints only. Which port the forward listens on, as the server
|
||||
* tunnels feature's Source Port does. Left unset the kernel picks a free
|
||||
* one, which is fine when backend and browser share a machine -- but a
|
||||
* container can only publish ports it knows in advance.
|
||||
*/
|
||||
localPort?: number;
|
||||
}
|
||||
|
||||
export interface JumpHost {
|
||||
hostId: number;
|
||||
}
|
||||
@@ -306,29 +264,6 @@ export interface CredentialBackend {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TUNNEL TYPES
|
||||
// ============================================================================
|
||||
|
||||
export type TunnelScope = "s2s" | "c2s";
|
||||
export type TunnelMode = "local" | "remote" | "dynamic";
|
||||
|
||||
// ============================================================================
|
||||
// FILE MANAGER TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface Tab {
|
||||
id: string | number;
|
||||
title: string;
|
||||
fileName: string;
|
||||
content: string;
|
||||
isSSH?: boolean;
|
||||
sshSessionId?: string;
|
||||
filePath?: string;
|
||||
loading?: boolean;
|
||||
dirty?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TERMINAL CONFIGURATION TYPES
|
||||
// ============================================================================
|
||||
@@ -368,7 +303,6 @@ export interface TerminalRefHandle {
|
||||
subscribeOutput?: (listener: (data: string) => void) => () => void;
|
||||
notifyResize?: () => void;
|
||||
refresh?: () => void;
|
||||
openFileManager?: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -427,21 +361,3 @@ export interface CacheEntry<T = unknown> {
|
||||
timestamp: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE EXPORT/IMPORT TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface ExportSummary {
|
||||
sshHostsImported: number;
|
||||
sshCredentialsImported: number;
|
||||
pluginItemsImported: number;
|
||||
credentialUsageImported: number;
|
||||
settingsImported: number;
|
||||
skippedItems: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER TYPES
|
||||
// ============================================================================
|
||||
+21
-45
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
/** 2: the docker and host metrics areas moved to their plugins. */
|
||||
export const UI_PREFERENCES_VERSION = 2;
|
||||
export const UI_PREFERENCES_VERSION = 3;
|
||||
|
||||
/** Bump when onboarding gains steps existing users should be shown again. */
|
||||
export const UI_ONBOARDING_VERSION = 2;
|
||||
@@ -30,16 +30,12 @@ export type UiAreaKey =
|
||||
| "credentialList"
|
||||
| "rail"
|
||||
| "dashboard"
|
||||
| "terminal"
|
||||
| "fileManager"
|
||||
| "hostEditor";
|
||||
|
||||
export type UiDensity = "comfortable" | "compact";
|
||||
export type UiTrayTrigger = "always" | "hover" | "click" | "actionsOnly";
|
||||
export type UiRowActions = "essential" | "full";
|
||||
export type UiEmptyStateVerbosity = "minimal" | "guided";
|
||||
export type UiToolbarDensity = "icon" | "labeled" | "expanded";
|
||||
export type UiFileViewMode = "grid" | "list";
|
||||
export type UiHostEditorMode = "simple" | "full";
|
||||
|
||||
export interface UiChromePreferences {
|
||||
@@ -75,15 +71,6 @@ export interface UiDashboardPreferences {
|
||||
enabledCards: string[];
|
||||
}
|
||||
|
||||
export interface UiTerminalPreferences {
|
||||
toolbarDensity: UiToolbarDensity;
|
||||
}
|
||||
|
||||
export interface UiFileManagerPreferences {
|
||||
viewMode: UiFileViewMode;
|
||||
showHiddenFiles: boolean;
|
||||
}
|
||||
|
||||
export interface UiHostEditorPreferences {
|
||||
mode: UiHostEditorMode;
|
||||
}
|
||||
@@ -94,8 +81,6 @@ export interface UiAreaPreferences {
|
||||
credentialList: UiCredentialListPreferences;
|
||||
rail: UiRailPreferences;
|
||||
dashboard: UiDashboardPreferences;
|
||||
terminal: UiTerminalPreferences;
|
||||
fileManager: UiFileManagerPreferences;
|
||||
hostEditor: UiHostEditorPreferences;
|
||||
}
|
||||
|
||||
@@ -149,10 +134,8 @@ const BALANCED_DASHBOARD_CARDS = [
|
||||
"host_status",
|
||||
"recent_activity",
|
||||
];
|
||||
// Advanced adds service links but leaves network_graph and homepage_preview
|
||||
// off: both are wide, and enabling them by default pushes the dashboard past
|
||||
// the edge of the screen. They stay available in the Add card tray.
|
||||
const ADVANCED_DASHBOARD_CARDS = [...BALANCED_DASHBOARD_CARDS, "service_links"];
|
||||
// Only core's own cards: a plugin's cards are added from the Add card tray.
|
||||
const ADVANCED_DASHBOARD_CARDS = [...BALANCED_DASHBOARD_CARDS];
|
||||
|
||||
export const PRESETS: Record<Exclude<UiPreset, "custom">, UiAreaPreferences> = {
|
||||
simple: {
|
||||
@@ -181,8 +164,6 @@ export const PRESETS: Record<Exclude<UiPreset, "custom">, UiAreaPreferences> = {
|
||||
"host_status",
|
||||
],
|
||||
},
|
||||
terminal: { toolbarDensity: "icon" },
|
||||
fileManager: { viewMode: "grid", showHiddenFiles: false },
|
||||
hostEditor: { mode: "simple" },
|
||||
},
|
||||
balanced: {
|
||||
@@ -202,9 +183,6 @@ export const PRESETS: Record<Exclude<UiPreset, "custom">, UiAreaPreferences> = {
|
||||
credentialList: { density: "comfortable", showTags: true },
|
||||
rail: { hiddenTabs: [] },
|
||||
dashboard: { enabledCards: BALANCED_DASHBOARD_CARDS },
|
||||
terminal: { toolbarDensity: "labeled" },
|
||||
// FileManager.tsx has always defaulted to grid when nothing is stored.
|
||||
fileManager: { viewMode: "grid", showHiddenFiles: false },
|
||||
// 3 is defaultLayoutFromWidgets's own default, i.e. today's behavior.
|
||||
hostEditor: { mode: "full" },
|
||||
},
|
||||
@@ -225,9 +203,6 @@ export const PRESETS: Record<Exclude<UiPreset, "custom">, UiAreaPreferences> = {
|
||||
credentialList: { density: "compact", showTags: true },
|
||||
rail: { hiddenTabs: [] },
|
||||
dashboard: { enabledCards: ADVANCED_DASHBOARD_CARDS },
|
||||
terminal: { toolbarDensity: "expanded" },
|
||||
// List packs more files and metadata per screen than the grid.
|
||||
fileManager: { viewMode: "list", showHiddenFiles: true },
|
||||
hostEditor: { mode: "full" },
|
||||
},
|
||||
};
|
||||
@@ -273,16 +248,6 @@ const AREA_SPECS: {
|
||||
dashboard: {
|
||||
enabledCards: { kind: "stringArray" },
|
||||
},
|
||||
terminal: {
|
||||
toolbarDensity: {
|
||||
kind: "enum",
|
||||
values: ["icon", "labeled", "expanded"],
|
||||
},
|
||||
},
|
||||
fileManager: {
|
||||
viewMode: { kind: "enum", values: ["grid", "list"] },
|
||||
showHiddenFiles: { kind: "bool" },
|
||||
},
|
||||
hostEditor: {
|
||||
mode: { kind: "enum", values: ["simple", "full"] },
|
||||
},
|
||||
@@ -349,11 +314,21 @@ function sanitizePluginArea(input: unknown): Record<string, unknown> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Version 1 stored the docker and host metrics areas under core names. */
|
||||
const LEGACY_PLUGIN_AREAS: Record<string, UiPluginAreaKey> = {
|
||||
docker: "plugin:docker",
|
||||
hostMetrics: "plugin:host-metrics",
|
||||
};
|
||||
/**
|
||||
* Areas that moved into a plugin: before `version` they were stored under
|
||||
* core's name. Version 1 had docker and host metrics, version 2 the terminal
|
||||
* and the file manager.
|
||||
*/
|
||||
const MOVED_PLUGIN_AREAS: Array<{
|
||||
before: number;
|
||||
legacy: string;
|
||||
area: UiPluginAreaKey;
|
||||
}> = [
|
||||
{ before: 2, legacy: "docker", area: "plugin:docker" }, // plugin-id-ok: 2.8 key
|
||||
{ before: 2, legacy: "hostMetrics", area: "plugin:host-metrics" },
|
||||
{ before: 3, legacy: "terminal", area: "plugin:ssh-terminal" },
|
||||
{ before: 3, legacy: "fileManager", area: "plugin:file-manager" },
|
||||
];
|
||||
|
||||
export function sanitizeUiOverrides(
|
||||
input: unknown,
|
||||
@@ -362,9 +337,10 @@ export function sanitizeUiOverrides(
|
||||
const out: Record<string, Record<string, unknown>> = {};
|
||||
if (!input || typeof input !== "object") return out as UiOverrides;
|
||||
|
||||
if (version < 2) {
|
||||
const moves = MOVED_PLUGIN_AREAS.filter((move) => version < move.before);
|
||||
if (moves.length > 0) {
|
||||
const upgraded = { ...(input as Record<string, unknown>) };
|
||||
for (const [legacy, area] of Object.entries(LEGACY_PLUGIN_AREAS)) {
|
||||
for (const { legacy, area } of moves) {
|
||||
if (legacy in upgraded) {
|
||||
upgraded[area] = upgraded[legacy];
|
||||
delete upgraded[legacy];
|
||||
|
||||
+1
-22
@@ -166,12 +166,6 @@ export type Tab = {
|
||||
host?: Host;
|
||||
openedAt: number;
|
||||
restoredSessionId?: string | null;
|
||||
/** Set when this tab joins someone else's live shared session instead of connecting/attaching its own. */
|
||||
joinSharedSessionId?: string | null;
|
||||
joinShareId?: string | null;
|
||||
initialFilePath?: string;
|
||||
/** Directory to open a Files tab into, distinct from initialFilePath (a specific file to open in an editor window). */
|
||||
initialPath?: string;
|
||||
/** Payload owned by the tab's plugin, e.g. which fleet or endpoint it shows. */
|
||||
data?: Record<string, unknown>;
|
||||
/** Present only on a split-screen container tab. Pane tab ids reference live child tabs. */
|
||||
@@ -189,7 +183,6 @@ export type Tab = {
|
||||
notifyResize?: () => void;
|
||||
refresh?: () => void;
|
||||
getApplicationCursorKeysMode?: () => boolean;
|
||||
openFileManager?: () => void;
|
||||
focus?: () => void;
|
||||
} | null>;
|
||||
};
|
||||
@@ -281,9 +274,9 @@ export type WorkspaceTabSnapshot = {
|
||||
hostNameSnapshot?: string | null;
|
||||
label: string;
|
||||
customLabel?: string;
|
||||
/** Read from payloads saved before tabs carried `data`. */
|
||||
initialFilePath?: string;
|
||||
initialPath?: string;
|
||||
/** Read from payloads saved before tabs carried `data`. */
|
||||
fleetId?: number;
|
||||
/** The tab's plugin payload. */
|
||||
data?: Record<string, unknown>;
|
||||
@@ -323,17 +316,3 @@ export type WorkspacePayload = {
|
||||
right: WorkspaceDockState;
|
||||
};
|
||||
};
|
||||
|
||||
const FOLDER_ICONS = [
|
||||
"folder",
|
||||
"server",
|
||||
"cloud",
|
||||
"database",
|
||||
"box",
|
||||
"network",
|
||||
"copy",
|
||||
"settings",
|
||||
"cpu",
|
||||
"globe",
|
||||
] as const;
|
||||
type FolderIconId = (typeof FOLDER_ICONS)[number];
|
||||
Reference in new issue
Block a user