feat: move termix identity into a plugin (C6)

This commit is contained in:
LukeGus committed 2026-09-24 20:50:54 -05:00
1 parent 6e56c90aac
commit 4dbce08379
157 files changed
+20743 -5766

No files matched your search

+32 -1
View File
@@ -1382,11 +1382,42 @@ export interface PluginProtocolTarget {
};
}
/** One of the acting user's saved SSH key credentials, public half only. */
export interface PluginSshKeyCredential {
id: number;
name: string;
username: string | null;
/** "<type> <base64>", derived from the private key when none was stored. Null when it cannot be. */
publicKey: string | null;
}
/** A key pair to save as a new credential for the acting user. */
export interface PluginSshKeyCredentialInput {
name: string;
description?: string;
username?: string | null;
privateKey: string;
publicKey: string;
/** The key's algorithm token, such as "ssh-ed25519". */
keyType: 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 {
/**
* The acting user's own SSH key credentials, without any private material.
* Needs credentials:use and the core credentials.view permission. Audited.
*/
listSshKeys: () => Promise<PluginSshKeyCredential[]>;
/**
* Saves a key pair as a new encrypted credential owned by the acting user
* and returns its id. Needs credentials:write, the core credentials.create
* permission and an unlocked data key. Audited.
*/
createSshKey: (input: PluginSshKeyCredentialInput) => Promise<{ id: number }>;
/** Null when the host does not exist or the acting user cannot connect to it. */
resolveHostProtocol: (
hostId: number,
@@ -1627,7 +1658,7 @@ 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. */
/** Host protocol credentials and the user's saved SSH keys. See PluginCredentials. */
readonly credentials: PluginCredentials;
/** Audit lines under the plugin's own action names. */
readonly audit: PluginAudit;
+1
View File
@@ -49,6 +49,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
entry("hosts:write", "medium"),
entry("credentials:use", "medium"),
entry("credentials:write", "medium"),
entry("network:outbound", "medium"),
entry("network:serve", "medium"),
entry("network:broadcast", "medium"),
+15
View File
@@ -530,6 +530,19 @@ export interface PluginWsTarget {
protocols?: string[];
}
/** Where the frontend runs, for a plugin that behaves differently in the desktop app. */
export interface DesktopApi {
/** True inside the Termix desktop app. */
readonly available: boolean;
/**
* The remote server the desktop app is connected to for sync, or null
* outside the desktop app and while it runs standalone.
*/
remoteServerUrl: () => Promise<string | null>;
/** Called when the desktop app connects to or leaves a remote server. */
onRemoteServerChange: (listener: () => void) => Disposer;
}
export interface TermixAppInfo {
readonly pluginId: string;
readonly manifest: PluginManifest;
@@ -629,6 +642,8 @@ export interface TermixApp extends TermixAppInfo {
options?: { origin?: unknown },
) => Promise<PluginWsTarget | null>;
tabs: TabsApi;
/** The desktop app, when the frontend runs in it. */
desktop: DesktopApi;
/** Registered cleanup, run when the plugin is disabled. */
onDispose: (dispose: Disposer) => void;
}
+38
View File
@@ -58,6 +58,8 @@ import type {
PluginHostShareResult,
PluginShareableUser,
PluginShareableRole,
PluginSshKeyCredential,
PluginSshKeyCredentialInput,
} from "./backend.js";
import type { SyncEntityRegistration } from "./backend.js";
import type { PluginDatabase } from "./backend.js";
@@ -120,6 +122,8 @@ export interface FakeContextOptions {
* "<hostId>:<protocol>". A missing key answers null.
*/
protocolTargets?: Record<string, PluginProtocolTarget>;
/** What ctx.credentials.listSshKeys answers. */
sshKeyCredentials?: PluginSshKeyCredential[];
/** What ctx.desktop.available() answers. Defaults to false. */
desktopAvailable?: boolean;
/** What ctx.hosts.status.get and check answer, by host id. */
@@ -273,6 +277,8 @@ export interface FakePluginContext {
nativeRdpLaunches: PluginNativeRdpRequest[];
/** Every ctx.credentials.resolveHostProtocol call, in order. */
credentialReads: Array<{ hostId: number; protocol: string }>;
/** Every ctx.credentials.createSshKey call, with the id it answered. */
createdSshKeys: Array<PluginSshKeyCredentialInput & { id: number }>;
/** Every ctx.notify.send call, with the actor it ran as. */
notifications: Array<{
actor: string | undefined;
@@ -392,6 +398,7 @@ export function createFakeContext(
const desktopWindows: FakePluginContext["desktopWindows"] = [];
const nativeRdpLaunches: FakePluginContext["nativeRdpLaunches"] = [];
const credentialReads: FakePluginContext["credentialReads"] = [];
const createdSshKeys: FakePluginContext["createdSshKeys"] = [];
const notifications: FakePluginContext["notifications"] = [];
const fetches: FakePluginContext["fetches"] = [];
const processRuns: FakePluginContext["processRuns"] = [];
@@ -972,6 +979,25 @@ export function createFakeContext(
},
credentials: {
listSshKeys: async () => [
...(options.sshKeyCredentials ?? []),
...createdSshKeys.map((key) => ({
id: key.id,
name: key.name,
username: key.username ?? null,
publicKey: key.publicKey,
})),
],
createSshKey: async (input) => {
const id =
Math.max(
0,
...(options.sshKeyCredentials ?? []).map((key) => key.id),
...createdSshKeys.map((key) => key.id),
) + 1;
createdSshKeys.push({ ...input, id });
return { id };
},
resolveHostProtocol: async (hostId, protocol) => {
credentialReads.push({ hostId, protocol });
return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
@@ -1049,6 +1075,7 @@ export function createFakeContext(
desktopWindows,
nativeRdpLaunches,
credentialReads,
createdSshKeys,
notifications,
fetches,
processRuns,
@@ -1105,6 +1132,8 @@ export interface MockContextOptions {
sshHosts?: PluginSshHost[];
/** What ctx.credentials serves. See FakeContextOptions. */
protocolTargets?: Record<string, PluginProtocolTarget>;
/** What ctx.credentials.listSshKeys serves. */
sshKeyCredentials?: PluginSshKeyCredential[];
/** What ctx.desktop.available() answers. */
desktopAvailable?: boolean;
/** What ctx.hosts.status answers. See FakeContextOptions. */
@@ -1164,6 +1193,7 @@ export function createMockCtx(
sshHosts: options.sshHosts,
services: options.services,
protocolTargets: options.protocolTargets,
sshKeyCredentials: options.sshKeyCredentials,
desktopAvailable: options.desktopAvailable,
hostStatuses: options.hostStatuses,
notificationChannels: options.notificationChannels,
@@ -1462,6 +1492,14 @@ export function createMockCtx(
},
credentials: {
listSshKeys: async () => {
require("credentials:use");
return ctx.credentials.listSshKeys();
},
createSshKey: async (input) => {
require("credentials:write");
return ctx.credentials.createSshKey(input);
},
resolveHostProtocol: async (hostId, protocol) => {
require("credentials:read");
return ctx.credentials.resolveHostProtocol(hostId, protocol);