mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
chore: lock the plugin boundary and remove 2.9.0 leftovers
This commit is contained in:
1 parent
6489b62ece
commit
2961297989
399 files changed
+3964
-5627
No files matched your search
@@ -737,19 +737,17 @@ export interface PluginSshHost {
|
||||
}
|
||||
|
||||
/** Picks keepalive and timeout defaults. "plugin" is the generic one. */
|
||||
export type PluginSshPurpose =
|
||||
| "plugin"
|
||||
| "docker"
|
||||
| "docker-console"
|
||||
| "metrics"
|
||||
| "proxmox"
|
||||
| "fleet"
|
||||
| "remote-desktop"
|
||||
| "tunnel"
|
||||
| "file-manager"
|
||||
| "file-transfer"
|
||||
| "terminal"
|
||||
| "tmux";
|
||||
/** What a connection is for, as a free label. Auth providers see it. */
|
||||
export type PluginSshPurpose = string;
|
||||
|
||||
/**
|
||||
* Keepalive, timeout and environment defaults for a kind of connection.
|
||||
* terminal: an interactive shell. session: a long-lived browsing session.
|
||||
* stream: a long transfer or console. forward: port forwarding. background
|
||||
* (the default): short exec work such as polling.
|
||||
*/
|
||||
export type PluginSshProfile =
|
||||
"terminal" | "session" | "stream" | "forward" | "background";
|
||||
|
||||
export type PluginSshAuthOutcome =
|
||||
| { status: "ready" }
|
||||
@@ -868,6 +866,8 @@ export interface PluginSshPromptChannel {
|
||||
|
||||
export interface PluginSshConnectOptions {
|
||||
purpose?: PluginSshPurpose;
|
||||
/** Keepalive and timeout defaults. Defaults to "background". */
|
||||
profile?: PluginSshProfile;
|
||||
/** Overall time to reach "ready". Defaults to 30s. */
|
||||
timeoutMs?: number;
|
||||
/** Without one, the stored password answers password prompts. */
|
||||
@@ -956,6 +956,8 @@ export interface PluginSsh {
|
||||
host: PluginSshHost,
|
||||
options: {
|
||||
purpose?: PluginSshPurpose;
|
||||
/** Keepalive and timeout defaults. Defaults to "background". */
|
||||
profile?: PluginSshProfile;
|
||||
client: unknown;
|
||||
/** Server-side host id when it differs from host.id. */
|
||||
serverHostId?: number;
|
||||
@@ -1079,6 +1081,11 @@ export interface PluginSshAuthProvider {
|
||||
requiresSecret?: boolean;
|
||||
/** Can connect unattended. Defaults to true. */
|
||||
supportsBackground?: boolean;
|
||||
/**
|
||||
* Works for a host that was never saved (nothing in host settings, no host
|
||||
* id to key a cache on), so Quick Connect offers it. Default false.
|
||||
*/
|
||||
quickConnect?: boolean;
|
||||
/** Interaction name its outcomes use, for startInteraction routing. */
|
||||
interaction?: string;
|
||||
connectOptions?: (
|
||||
|
||||
@@ -128,6 +128,11 @@ export interface RailItemContribution {
|
||||
kind?: "panel" | "tab";
|
||||
/** Users may hide it from Appearance > Sidebar > Navigation. Default true. */
|
||||
hideable?: boolean;
|
||||
/**
|
||||
* Stays visible in the Simple interface preset, which hides every other
|
||||
* plugin rail item. For the one or two things a new user reaches for.
|
||||
*/
|
||||
simplePreset?: boolean;
|
||||
/** Can also open as a full-width tab. */
|
||||
promotable?: boolean;
|
||||
/** Can open in the right dock. */
|
||||
@@ -302,6 +307,11 @@ export interface HostActionContribution {
|
||||
copyUrlView?: string;
|
||||
/** Where a host overview (the dashboard's host status list) sends a click. */
|
||||
overview?: boolean;
|
||||
/**
|
||||
* Offered as a button in Quick Connect, next to the default connect button,
|
||||
* for an address that is never saved. Opens tabType.
|
||||
*/
|
||||
quickConnect?: boolean;
|
||||
/**
|
||||
* Position in the host row. Core: Files 20, Tunnel 40, Tmux 70. Connect
|
||||
* actions default to 100 and sit after a separator.
|
||||
@@ -709,6 +719,8 @@ export interface SshAuthTypeInfo {
|
||||
credentialType: boolean;
|
||||
/** Can connect unattended, for polling. */
|
||||
supportsBackground: boolean;
|
||||
/** Offered in Quick Connect, for a host that is never saved. */
|
||||
quickConnect: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -825,6 +837,18 @@ export interface PluginCoreApi {
|
||||
setHostAutoTmux: (hostId: number, autoTmux: boolean) => Promise<void>;
|
||||
/** A browser-side UI preference (a cookie, or the desktop app's store). */
|
||||
getClientPreference: (name: string) => string | undefined;
|
||||
/** Every host the user can see, fetched now rather than from the shell's cache. */
|
||||
listHosts: () => Promise<PluginHostRecord[]>;
|
||||
/** The user's stored credentials, without their secrets. */
|
||||
listCredentials: () => Promise<PluginCredentialSummary[]>;
|
||||
}
|
||||
|
||||
/** A stored credential as a picker needs it. Secrets never leave core. */
|
||||
export interface PluginCredentialSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
username?: string;
|
||||
authType?: string;
|
||||
}
|
||||
|
||||
/** What a recent-activity entry opens and how it is labelled. */
|
||||
@@ -972,6 +996,14 @@ export function getClientPreference(name: string): string | undefined {
|
||||
return requireHost().core.getClientPreference(name);
|
||||
}
|
||||
|
||||
export function listHosts(): Promise<PluginHostRecord[]> {
|
||||
return requireHost().core.listHosts();
|
||||
}
|
||||
|
||||
export function listCredentials(): Promise<PluginCredentialSummary[]> {
|
||||
return requireHost().core.listCredentials();
|
||||
}
|
||||
|
||||
export function usePluginApi(): PluginApiClient {
|
||||
const bridge = requireHost();
|
||||
return bridge.getApi(bridge.usePluginId());
|
||||
|
||||
@@ -62,7 +62,6 @@ export type SystemRoleName = (typeof SYSTEM_ROLE_NAMES)[number];
|
||||
*/
|
||||
export const RESERVED_PERMISSION_PREFIXES = [
|
||||
"hosts",
|
||||
"snippets",
|
||||
"credentials",
|
||||
"admin",
|
||||
] as const;
|
||||
@@ -324,6 +323,23 @@ export interface PluginHttpContribution {
|
||||
* a clash.
|
||||
*/
|
||||
legacyPaths?: string[];
|
||||
/**
|
||||
* Old URLs outside the plugin's namespace that something outside Termix
|
||||
* still sends people to (an identity provider's callback, a published
|
||||
* resolver URL). A request at `from`, or under it, is redirected to `to`
|
||||
* under /plugin-api/<id>/ with the rest of the path and the query kept.
|
||||
* Core routes win a clash.
|
||||
*/
|
||||
legacyRedirects?: PluginLegacyRedirect[];
|
||||
}
|
||||
|
||||
export interface PluginLegacyRedirect {
|
||||
/** An absolute path, e.g. "/users/oidc/callback". */
|
||||
from: string;
|
||||
/** A path in this plugin's router, e.g. "/callback". */
|
||||
to: string;
|
||||
/** 307 (the default) or 308 for a permanent move. */
|
||||
status?: 307 | 308;
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
@@ -869,6 +885,40 @@ function validateUiPresets(value: unknown, errors: string[]): void {
|
||||
}
|
||||
|
||||
const LEGACY_PATH_PATTERN = /^\/[a-z][a-z0-9-]*(\/[A-Za-z0-9_.-]+)+$/;
|
||||
const REDIRECT_PATH_PATTERN = /^(\/[A-Za-z0-9_.-]+)+$/;
|
||||
const RESERVED_PREFIXES = ["/plugin-api", "/plugin-ws", "/plugin-assets"];
|
||||
|
||||
function validateLegacyRedirects(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) return;
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push('Field "contributes.http.legacyRedirects" must be an array');
|
||||
return;
|
||||
}
|
||||
value.forEach((entry, index) => {
|
||||
const at = `contributes.http.legacyRedirects[${index}]`;
|
||||
if (!isPlainObject(entry)) {
|
||||
errors.push(`${at} must be an object`);
|
||||
return;
|
||||
}
|
||||
rejectUnknown(entry, ["from", "to", "status"], at, errors);
|
||||
const { from, to, status } = entry;
|
||||
if (typeof from !== "string" || !REDIRECT_PATH_PATTERN.test(from)) {
|
||||
errors.push(`${at}.from must be an absolute path`);
|
||||
} else if (
|
||||
RESERVED_PREFIXES.some(
|
||||
(prefix) => from === prefix || from.startsWith(`${prefix}/`),
|
||||
)
|
||||
) {
|
||||
errors.push(`${at}.from cannot be under ${from.split("/")[1]}`);
|
||||
}
|
||||
if (typeof to !== "string" || !REDIRECT_PATH_PATTERN.test(to)) {
|
||||
errors.push(`${at}.to must be a path in the plugin's router`);
|
||||
}
|
||||
if (status !== undefined && status !== 307 && status !== 308) {
|
||||
errors.push(`${at}.status must be 307 or 308`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateHttpContribution(
|
||||
value: unknown,
|
||||
@@ -880,7 +930,13 @@ function validateHttpContribution(
|
||||
errors.push('Field "contributes.http" must be an object');
|
||||
return;
|
||||
}
|
||||
rejectUnknown(value, ["legacyPaths"], "contributes.http", errors);
|
||||
rejectUnknown(
|
||||
value,
|
||||
["legacyPaths", "legacyRedirects"],
|
||||
"contributes.http",
|
||||
errors,
|
||||
);
|
||||
validateLegacyRedirects(value.legacyRedirects, errors);
|
||||
const paths = value.legacyPaths;
|
||||
if (paths === undefined) return;
|
||||
if (!Array.isArray(paths)) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
PluginDisposables,
|
||||
PluginLogger,
|
||||
} from "./backend.js";
|
||||
import type { TermixApp } from "./frontend.js";
|
||||
import type { HostActionContribution, TermixApp } from "./frontend.js";
|
||||
import type { PluginManifest } from "./manifest.js";
|
||||
import type { PluginTableDefinition } from "./db.js";
|
||||
import type {
|
||||
@@ -1967,6 +1967,8 @@ export interface RenderedPluginApp {
|
||||
tabs: () => string[];
|
||||
panels: () => string[];
|
||||
hostActions: () => Array<{ id: string; tabType?: string }>;
|
||||
/** This plugin's host actions that apply to `host`, as the shell sees them. */
|
||||
hostActionsFor: (host: unknown) => HostActionContribution[];
|
||||
hostProtocols: () => string[];
|
||||
hostEditorSections: () => string[];
|
||||
dashboardCards: () => string[];
|
||||
|
||||
@@ -112,6 +112,7 @@ const offline = () =>
|
||||
pluginId: "core",
|
||||
credentialType: type === "password" || type === "key",
|
||||
supportsBackground: type !== "none",
|
||||
quickConnect: true,
|
||||
})),
|
||||
loaded: true,
|
||||
}),
|
||||
@@ -138,6 +139,8 @@ const offline = () =>
|
||||
getCustomKeybindings: async () => [],
|
||||
setHostAutoTmux: async () => {},
|
||||
getClientPreference: () => undefined,
|
||||
listHosts: async () => [],
|
||||
listCredentials: async () => [],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ export function pluginVitestConfig(
|
||||
"plugin-host",
|
||||
"testing-host.tsx",
|
||||
),
|
||||
// Core's own modules behind sdk-ui and the test host import each
|
||||
// other this way. Plugin code may not (eslint refuses it).
|
||||
"@/types": path.join(repoRoot, "src", "types"),
|
||||
"@": path.join(repoRoot, "src", "ui"),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user