mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: tailscale add host and copy ip, fix outbound fetch hang and plugin ESM warning
This commit is contained in:
1 parent
c21bca8cf0
commit
fbe736bc20
70 files changed
+1117
-1139
No files matched your search
@@ -254,8 +254,8 @@ handler)` for a socket. Core runs the same middleware in front of every plugin:
|
||||
1. **Auth**, unless the path is listed in `options.public`. A public path is
|
||||
audited when the router is registered and logged on every request, because
|
||||
"this plugin opened a hole in auth" should be findable later. **D2** lists
|
||||
a running plugin's public HTTP paths and public socket paths in the admin
|
||||
plugin dialog (`publicRoutes` on `GET /plugins`), and audits a public
|
||||
a running plugin's public HTTP paths and public socket paths as
|
||||
`publicRoutes` on `GET /plugins` (admins only), and audits a public
|
||||
socket when it is registered. It matches the
|
||||
full path, with `:param` segments allowed, so declaring `/webhook/:token`
|
||||
public cannot open `/webhook/:token/anything`. A trailing `/*` is the one
|
||||
@@ -360,8 +360,22 @@ the plugin's own permissions exactly as `ctx.rbac` does.
|
||||
`hidden: true` (**B14**) keeps a field out of the generic form: it is stored,
|
||||
validated and encrypted like any other, but the plugin edits it in its own
|
||||
UI. Remote desktop's host options are hidden fields, edited in its RDP, VNC
|
||||
and Telnet host editor tabs, so they do not appear a second time in the
|
||||
Plugins tab.
|
||||
and Telnet host editor tabs.
|
||||
|
||||
**Where fields render.** In 2.9.0 the plugin system is invisible to users:
|
||||
there is no plugin list, enable switch or capability dialog, and nothing in
|
||||
the UI says "plugin". That UI arrives with the store in 3.0.0. Settings show
|
||||
up as features instead:
|
||||
|
||||
- `user` fields render as an accordion section named after the plugin in the
|
||||
User Profile panel, and `admin` fields do the same in the Admin Settings
|
||||
panel (`src/ui/settings/FeatureSettingsSections.tsx`). `group` becomes a
|
||||
card inside that section.
|
||||
- `host` fields get a host editor tab of their own, named after the plugin
|
||||
(`src/ui/settings/host-feature-tabs.tsx`). `editorGroup` (`"top"` or
|
||||
`"ssh"`, default `"top"`) and `editorOrder` place it among the other tabs.
|
||||
A plugin that registers its own section with `registerHostEditorSection`
|
||||
gets no generated tab, so it must draw any non-hidden host field itself.
|
||||
|
||||
`type: "custom"` names a component the frontend registered, for the few things
|
||||
a schema cannot express (a device browser, a provider list). It is deliberately
|
||||
@@ -419,8 +433,8 @@ allowlist (`CORE_SETTINGS_ALLOWLIST`) rather than the whole settings table.
|
||||
|
||||
**D0** added three pieces:
|
||||
|
||||
- `onValidate(scope, validator)` checks a save from the settings screen or the
|
||||
host editor before anything is written. The validator gets every value in
|
||||
- `onValidate(scope, validator)` checks a save from the profile or admin
|
||||
panel or the host editor before anything is written. The validator gets every value in
|
||||
the save (and the host id for host scope) and returns field key to message
|
||||
for what is wrong. ssh-terminal checks that its image paths are absolute,
|
||||
step-ca that its CA URL is https and its fingerprint a SHA-256 digest.
|
||||
@@ -645,6 +659,13 @@ answers undefined while its plugin is off.
|
||||
shared session") and replaces `ShellApi.openTerminalTab`, which the shell no
|
||||
longer has.
|
||||
|
||||
`ShellApi.openHostEditor(draft?)` opens the host editor for a new host with
|
||||
a `HostDraft` (name, ip, port, username, authType) filled in, which the user
|
||||
reviews and saves (tailscale's "Add host" on a device). `saveQuickConnect`
|
||||
saves only what core's own quick connect payload carries, so the terminal
|
||||
offers it only on a host with `quickConnectSavable`, which core sets for the
|
||||
password, key, credential, none and agent auth types.
|
||||
|
||||
**Components by id.** `app.registerComponent(id, component)` offers a
|
||||
component to other plugins and core, which render it with `PluginComponent`
|
||||
from `@termix/plugin-sdk/ui` (a fallback while the owner is off) or
|
||||
@@ -766,7 +787,7 @@ mode are still preset-seeded localStorage keys core writes, as in 2.8.
|
||||
origin, as `app.apiFor` but usable in a component. The terminal toolbar's
|
||||
metrics bars use it for a host on the desktop app's remote server, and
|
||||
tunnels merges the remote server's statuses through `app.apiFor("remote")`.
|
||||
- `app.onSettingsChanged(listener)`: fires after the settings screen or host
|
||||
- `app.onSettingsChanged(listener)`: fires after the profile panel, admin panel or host
|
||||
editor saved one of the plugin's settings, instead of polling on focus.
|
||||
- A `tab.menu` slot: entries in a tab's right-click menu, invoked with the
|
||||
tab's surface handle; `when` sees `{ tab, handle }`. Session sharing adds its
|
||||
@@ -1258,8 +1279,8 @@ base types and branches on no plugin type name.
|
||||
in it. Its public routes are `/plugin-api/sso/start` (sends the browser to
|
||||
the provider), `/callback` (GET and POST), `/backchannel-logout` and
|
||||
`/config`; provider management is `/plugin-api/sso/providers`, gated on
|
||||
`sso.manage`, and renders as the custom `providers` field on the plugin's
|
||||
admin settings page. Login state (nonce, PKCE verifier, return address)
|
||||
`sso.manage`, and renders as the custom `providers` field in the plugin's
|
||||
Admin Settings section. Login state (nonce, PKCE verifier, return address)
|
||||
lives in `ctx.kv` for ten minutes.
|
||||
- The redirect URI is `<base>/plugin-api/sso/callback`. Providers that
|
||||
existed before 2.9 keep sending `<base>/users/oidc/callback`, the URI their
|
||||
@@ -1937,6 +1958,8 @@ not do.
|
||||
},
|
||||
],
|
||||
"host": {
|
||||
"editorGroup": "ssh", // host editor strip for the generated tab, default "top"
|
||||
"editorOrder": 30, // position among that strip's tabs
|
||||
"enableKey": "enableExample", // boolean rendered first, gating the rest
|
||||
"enableLabelKey": "k",
|
||||
"fields": [
|
||||
@@ -2696,8 +2719,8 @@ None of the checks in this document stop that. They are not a sandbox.
|
||||
`system:tls`, `ssh:connect`, `process:spawn`, `users:write`,
|
||||
`auth:provide`, `device:serial`) means the plugin can reach your hosts,
|
||||
your users or the server itself. Ask why it needs each one.
|
||||
- **Read the public routes.** Admin > Plugins > Permissions lists every path
|
||||
it serves without login. Each one is attack surface on your server.
|
||||
- **Read the public routes.** `publicRoutes` on `GET /plugins` lists every
|
||||
path it serves without login. Each one is attack surface on your server.
|
||||
- **Check its dependencies.** A plugin's npm dependencies run with the same
|
||||
reach as the plugin.
|
||||
- **Watch the audit log after installing.** Filter on `plugin:<id>` and look
|
||||
|
||||
@@ -54,6 +54,13 @@ export async function build({ cwd }) {
|
||||
},
|
||||
});
|
||||
|
||||
// Without this Node finds the host's typeless package.json, tries backend.js
|
||||
// as CommonJS first and warns before reparsing it as ESM.
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, "package.json"),
|
||||
`${JSON.stringify({ type: "module" }, null, 2)}\n`,
|
||||
);
|
||||
|
||||
const frontendEntry = resolveEntry(cwd, FRONTEND_ENTRIES);
|
||||
if (frontendEntry) {
|
||||
await esbuild.build({
|
||||
|
||||
@@ -793,6 +793,14 @@
|
||||
"enableDefault": {
|
||||
"type": "boolean",
|
||||
"description": "What the enable switch reads before a host saves it. Off by default."
|
||||
},
|
||||
"editorGroup": {
|
||||
"enum": ["top", "ssh"],
|
||||
"description": "Host editor strip the generated tab sits in. Defaults to top."
|
||||
},
|
||||
"editorOrder": {
|
||||
"type": "number",
|
||||
"description": "Position among that strip's tabs, core ones included."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,15 @@ export interface ShellLayout {
|
||||
}
|
||||
|
||||
/** What a plugin may ask of the shell. */
|
||||
/** Fields a plugin can fill in when it opens the host editor. */
|
||||
export interface HostDraft {
|
||||
name?: string;
|
||||
ip?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
authType?: string;
|
||||
}
|
||||
|
||||
export interface ShellApi {
|
||||
openTab: (
|
||||
host: PluginHostRecord | null,
|
||||
@@ -88,6 +97,11 @@ export interface ShellApi {
|
||||
tab: PluginTabRecord,
|
||||
host: PluginHostRecord,
|
||||
) => Promise<void>;
|
||||
/**
|
||||
* Opens the host editor for a new host with these fields filled in. The
|
||||
* user reviews and saves it. Absent in some shells.
|
||||
*/
|
||||
openHostEditor?: (draft?: HostDraft) => void;
|
||||
/** Opens a rail view in the left sidebar. */
|
||||
openRailView: (id: string) => void;
|
||||
/** Closes a rail view wherever it is shown. */
|
||||
|
||||
@@ -271,6 +271,10 @@ export interface PluginHostSettingsContribution {
|
||||
enableLabelKey?: string;
|
||||
/** What the enable switch reads before a host saves it. Off by default. */
|
||||
enableDefault?: boolean;
|
||||
/** Host editor strip the generated tab sits in. Defaults to "top". */
|
||||
editorGroup?: "top" | "ssh";
|
||||
/** Position among that strip's tabs, core ones included. */
|
||||
editorOrder?: number;
|
||||
fields: PluginSettingsField[];
|
||||
}
|
||||
|
||||
@@ -1086,7 +1090,14 @@ function validateSettings(settings: unknown, errors: string[]): void {
|
||||
}
|
||||
rejectUnknown(
|
||||
host,
|
||||
["enableKey", "enableLabelKey", "enableDefault", "fields"],
|
||||
[
|
||||
"enableKey",
|
||||
"enableLabelKey",
|
||||
"enableDefault",
|
||||
"editorGroup",
|
||||
"editorOrder",
|
||||
"fields",
|
||||
],
|
||||
`"${at}"`,
|
||||
errors,
|
||||
);
|
||||
@@ -1104,6 +1115,20 @@ function validateSettings(settings: unknown, errors: string[]): void {
|
||||
if ("enableDefault" in host && typeof host.enableDefault !== "boolean") {
|
||||
errors.push(`${at}.enableDefault must be a boolean`);
|
||||
}
|
||||
if (
|
||||
"editorGroup" in host &&
|
||||
host.editorGroup !== "top" &&
|
||||
host.editorGroup !== "ssh"
|
||||
) {
|
||||
errors.push(`${at}.editorGroup must be "top" or "ssh"`);
|
||||
}
|
||||
if (
|
||||
"editorOrder" in host &&
|
||||
(typeof host.editorOrder !== "number" ||
|
||||
!Number.isFinite(host.editorOrder))
|
||||
) {
|
||||
errors.push(`${at}.editorOrder must be a number`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(host.fields)) {
|
||||
errors.push(`${at}.fields must be an array`);
|
||||
|
||||
Reference in new issue
Block a user