mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05: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
@@ -279,6 +279,12 @@ it, `group` is a section heading, and `permission` is who may write it: admin
|
||||
fields fall back to `admin.plugins.manage`, and a short name resolves against
|
||||
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.
|
||||
|
||||
`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
|
||||
narrow: a plugin supplies data everywhere else, so it cannot ship its own form
|
||||
@@ -339,8 +345,10 @@ A plugin frontend is a built ESM bundle, `dist/frontend.js`, exporting
|
||||
no imports from `plugins/`; everything a plugin shows goes through the `app`
|
||||
object. `scripts/check-shell-plugin-ids.cjs` (run by lint) fails on a quoted
|
||||
plugin id in `src/ui` outside tests, apart from the entries in
|
||||
`scripts/shell-plugin-id-allowlist.json`, each with a reason (host protocol
|
||||
data like `rdp`, which Phase B moves).
|
||||
`scripts/shell-plugin-id-allowlist.json`, each with a reason (host data like
|
||||
the `docker` runtime name, which Phase B moves). **B14** removed the `rdp`,
|
||||
`vnc` and `telnet` entries: the shell learns those protocols from
|
||||
`app.registerHostProtocol`.
|
||||
|
||||
#### The loader
|
||||
|
||||
@@ -427,6 +435,7 @@ boundary and Suspense.
|
||||
| `registerTab` | A tab type, id in `contributes.tabs`. Options cover persistence, layouts, singletons and host needs |
|
||||
| `registerHostEditorSection` | A host editor tab in the top strip or the SSH group, with `form`, `setField` and `updateForm` |
|
||||
| `registerHostAction` | A connect or open action on a host: sidebar row, palette, dashboard, default connect |
|
||||
| `registerHostProtocol` | A connection protocol next to SSH (**B14**): General tab switch, Hosts panel filter and grouping, Quick Connect, the host's primary type and port |
|
||||
| `registerHostBadge`, `registerHostContextMenuItem` | Host row badges and context menu entries |
|
||||
| `registerPaletteEntry` | A global or per-host command palette entry |
|
||||
| `registerDashboardCard` | A dashboard card, id in `contributes.dashboardCards` |
|
||||
@@ -437,6 +446,7 @@ boundary and Suspense.
|
||||
| `registerSshAuthEditor` | An SSH auth method's editor in the host editor |
|
||||
| `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method and a second factor challenge (plus an optional `enrollment` section) |
|
||||
| `api`, `wsUrl(path)` | axios on `/plugin-api/<id>/` and the plugin's WebSocket URL |
|
||||
| `apiFor(origin)` | The same client for a resolved connection origin: `"remote"` reaches the desktop app's connected server (**B14**) |
|
||||
| `t`, `hasPermission` | The plugin's strings and a permission check, for code outside a component (a toast from `activate`) |
|
||||
| `tabs.open`, `getLayout`, `applyLayout`, `onChange` | Tab control, used by workspaces; `tabs.openRailView` (**B12**) opens a rail view |
|
||||
| `guest`, `info`, `onDispose` | Guest mode flag, plugin info, extra cleanup |
|
||||
@@ -504,6 +514,19 @@ their host info), `terminal.sendToActive(text, { run })` and
|
||||
its terminal tab wrapper populates from the core terminal ref it already
|
||||
holds. The snippets plugin's run/paste flow is the first caller.
|
||||
|
||||
**Host protocols (B14).** `app.registerHostProtocol({ id, settingKey,
|
||||
portKey, defaultPort, titleKey, descriptionKey, icon, quickConnect })` adds a
|
||||
protocol next to SSH. Its switch and port are the plugin's own host settings,
|
||||
so core reads them from `host.pluginSettings[<plugin>]` and never knows the
|
||||
protocol by name: the General tab draws the switch and saves it into the
|
||||
plugin's settings, the Hosts panel filters and groups by it, HostItem counts
|
||||
it, Quick Connect offers it (with a domain field when asked), and a host with
|
||||
SSH off takes the first protocol switched on as its `connectionType` and its
|
||||
port. `hostProtocols(host)` from `@termix/plugin-sdk/frontend` lists what a
|
||||
host has on (`"ssh"` included), for a plugin such as session sharing that
|
||||
offers every protocol without knowing who owns it. Remote desktop registers
|
||||
`rdp`, `vnc` and `telnet`.
|
||||
|
||||
#### View ownership
|
||||
|
||||
Who owns a tab, panel or card comes from manifests, so it is known even for a
|
||||
@@ -547,7 +570,11 @@ connection helpers (`isElectron`, `resolveConnectionOrigin`, `pluginWsUrl`,
|
||||
`hydrateLocalSharedHostAuth`, `useConnectionDefaults`); and a few core APIs
|
||||
(`logActivity`, `getHostPassword`, `patchOpenTab`, `getUserPreferences`,
|
||||
`parseCustomKeybindings`, `setHostAutoTmux`, `getCookie`). The APIs are a
|
||||
stopgap in the right place: D1 turns them into typed bridge members. Publishing its
|
||||
stopgap in the right place: D1 turns them into typed bridge members.
|
||||
**B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the
|
||||
`ConnectionStage` type, for remote desktop's display socket and its remote
|
||||
host lookup. `useConnectionDefaults` now carries terminal defaults only; the
|
||||
RDP half became remote desktop's user settings. Publishing its
|
||||
`.d.ts` for plugins outside this repo is a follow-up for the repo split.
|
||||
|
||||
#### Strings
|
||||
@@ -596,10 +623,12 @@ every enabled plugin that declares `contributes.settings.host`, it looks up
|
||||
`"<id>.hostImportNormalizer"` and, if the plugin registered one, calls it with
|
||||
the raw imported row and writes back whatever it returns (or nothing, for
|
||||
`null`). This is not part of the SDK's typed `ctx` surface - it is a plain
|
||||
`ctx.registry` convention, the same mechanism `remote-desktop.sessions`
|
||||
already uses, just with a name core's own code knows
|
||||
to look for. web-endpoint (`src/backend/host-import.ts`) is the first plugin
|
||||
to register one.
|
||||
`ctx.registry` convention, just with a name core's own code knows to look
|
||||
for. web-endpoint (`src/backend/host-import.ts`) is the first plugin to
|
||||
register one; remote desktop's (**B14**) also reads a Termix export's
|
||||
`pluginSettings` and the flat fields hosts had before 2.9.0. Host exports
|
||||
carry every plugin's host settings (secrets redacted) under `pluginSettings`,
|
||||
which is what makes the round trip work without core naming a plugin.
|
||||
|
||||
**B9**'s terminal provides `sessions.live` (live SSH sessions: look up, end,
|
||||
remove a share's participants, hand a room share's control over, subscribe to
|
||||
@@ -623,8 +652,11 @@ per-chunk itself), `persist(summary)` and `discard()` for a session that
|
||||
recorded nothing. `createFinished(input)` is the second half of the
|
||||
contract, for a caller that already wrote its own recording to disk and only
|
||||
needs the database row: remote-desktop's guacd recordings use it instead of
|
||||
importing the old repository directly, and without the plugin enabled a
|
||||
guacd recording still lands on disk but gets no row. Recording files live
|
||||
importing the old repository directly. **B14** added `enabledFor(hostId)`
|
||||
(version 1.1.0), because guacd has to be told to record before the session
|
||||
exists: remote desktop records an RDP or Telnet session only when the plugin
|
||||
is running and the host's switch is on, and writes the row as the session's
|
||||
user. Recording files live
|
||||
under `ctx.files.dataDir()/session_logs/<user>/<session>.cast`. Retention
|
||||
(`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours.
|
||||
|
||||
@@ -647,8 +679,10 @@ any other, `ctx.services.get(service, { provider })` reaches one, and
|
||||
`ctx.services.providers(service)` lists the running ones. A requirement is
|
||||
satisfied by any compatible provider. `sessions.live` is the first: keyed by
|
||||
session type, ssh-terminal provides `ssh` and remote desktop provides `rdp`,
|
||||
`vnc` and `telnet` (B14), with `createViewerToken(sessionId, readOnly)` for
|
||||
a viewer. A consumer with no actor, such as a public guest route, calls it
|
||||
`vnc` and `telnet` (**B14** delivered them), with
|
||||
`createViewerToken(sessionId, readOnly)` for a viewer. A remote desktop
|
||||
session id is guacd's own connection id, and `ownerEndSession` closes it and
|
||||
every viewer joined to it. A consumer with no actor, such as a public guest route, calls it
|
||||
through `ctx.asUser(<share owner>)`.
|
||||
|
||||
**Session sharing (B12)** is the session-sharing plugin. It provides
|
||||
@@ -954,6 +988,19 @@ dropping a column before its data has moved loses it:
|
||||
A column that is still read anywhere in core is not ready to be dropped. Ship
|
||||
the copy and the switch first, and drop it in a later step if that is safer.
|
||||
|
||||
**Order at boot (B14).** On Postgres and MySQL, core's drizzle migrations run
|
||||
while the database opens, and the copies into `plugin_settings` run later,
|
||||
after the plugins table exists. A real `DROP COLUMN` in step 4 therefore
|
||||
deletes the data before it is copied. So step 4 removes the column from
|
||||
`schema.ts`, the pg/mysql variants and the `db/index.ts` bootstrap, and the
|
||||
generated drizzle migration's `DROP COLUMN` statements are replaced with a
|
||||
comment and `SELECT 1;`, exactly as for an adopted table. The column stays in
|
||||
the database, unused, until 3.0.0 removes it. A copy reads it with raw SQL
|
||||
through `utils/crypto-migration/raw-rows.ts`, whose `selectRows` and
|
||||
`runStatement` work on all three engines: `getDb().all()` exists only on
|
||||
SQLite. Remote desktop's host options and `user_preferences.rdp_defaults` are
|
||||
the first columns handled this way.
|
||||
|
||||
---
|
||||
|
||||
## Manifest v2 reference
|
||||
@@ -1230,6 +1277,8 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.notify.*` | `notify:send` | A6 |
|
||||
| `ctx.auth.*` | `auth:provide` | **A8** |
|
||||
| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** |
|
||||
| `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** |
|
||||
| `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** |
|
||||
| `ctx.audit.record` | none, the actor is the runtime's | **B9** |
|
||||
| `ctx.fetch` | `network:outbound` | B |
|
||||
|
||||
@@ -1364,6 +1413,29 @@ share). Rejects outside the desktop app (`ELECTRON_EMBEDDED` unset, or no
|
||||
shares cookies or storage with the main window or with another isolated
|
||||
window.
|
||||
|
||||
**B14** added `ctx.desktop.launchNativeRdp({ host, port, username, domain })`
|
||||
over the same bridge (a second entry in `BACKEND_REQUEST_HANDLERS`,
|
||||
`launch-native-rdp`), which opens mstsc on Windows with a generated `.rdp`
|
||||
file that never carries the password, and `ctx.desktop.available()`, which
|
||||
says whether the server runs embedded in the desktop app. The renderer's own
|
||||
`openNativeRdp` IPC call is gone; the desktop app's frontend always asks its
|
||||
embedded backend, which is the one that can reach Electron.
|
||||
|
||||
**B14** also added `ctx.credentials.resolveHostProtocol(hostId, protocol)`,
|
||||
the first member behind `credentials:read`. It returns the host's address,
|
||||
jump hosts and the plaintext RDP, VNC or Telnet login the acting user may use,
|
||||
for a plugin that has to hand them to another program (guacd). The logins
|
||||
stay in core next to the host: an owner gets the stored login or the stored
|
||||
credential it points at; a shared recipient gets only core's sharing
|
||||
resolution (the owner's shared snapshot or their own override), never the
|
||||
owner's raw secret; no connect access, or no host, is `null`. Every call is
|
||||
audited as `plugin_credentials_read`, allowed or refused.
|
||||
|
||||
`ctx.hosts.create` (**B14**) hands the created row to every plugin's
|
||||
`hostImportNormalizer`, the same as a bulk import row, so a plugin creating a
|
||||
host can set another plugin's host settings (proxmox marks an imported
|
||||
Windows guest as RDP) without core naming either.
|
||||
|
||||
### The actor
|
||||
|
||||
Never comes from plugin code. It is held in `AsyncLocalStorage` and set two
|
||||
@@ -1888,8 +1960,8 @@ Then, with the app running:
|
||||
|
||||
## Legacy core imports: the debt D1 removes
|
||||
|
||||
The bundled plugins predate the SDK, apart from workspaces (A9) and snippets
|
||||
(B2), which import nothing from core. The others still reach core by relative
|
||||
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
|
||||
(B2) and remote-desktop (B14), which import nothing from core. The others still reach core by relative
|
||||
path (`../../../../src/backend/...`), which an esbuild plugin,
|
||||
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
|
||||
and rewrites to the compiled output path (`../../../backend/backend/...`, or
|
||||
@@ -1916,8 +1988,8 @@ What the lint fence enforces today, in `eslint.config.mjs`:
|
||||
| Core importing a plugin backend | **Error** | 0 | - |
|
||||
| A plugin backend importing frontend code or `@/` | **Error** | 0 | - |
|
||||
| The shell importing plugin code | **Error** | 0 | - |
|
||||
| A plugin frontend importing core through `@/` | Warning | 130 files | D1 |
|
||||
| A plugin importing core by relative path | Warning | 66 files | D1 |
|
||||
| A plugin frontend importing core through `@/` | Warning | 120 files | D1 |
|
||||
| A plugin importing core by relative path | Warning | 63 files | D1 |
|
||||
| A plugin importing another plugin's source | Warning | 3 files | B18 |
|
||||
|
||||
A warning does not fail a build, so the counts are held by
|
||||
@@ -1957,6 +2029,13 @@ Known specifics:
|
||||
command history panel into ssh-terminal, which imports nothing from core.
|
||||
The file manager's terminal window renders the `terminal.view` component
|
||||
instead of importing the core terminal.
|
||||
- Host data columns (`enableDocker`, `enableRdp`, `guacamoleConfig` and so on)
|
||||
stay in core types until Phase B moves them. Only UI branches on them left
|
||||
the shell.
|
||||
- Host data columns (`enableDocker` and so on) stay in core types until
|
||||
Phase B moves them. Only UI branches on them left the shell. **B14** moved
|
||||
remote desktop's (`enableRdp`, `enableVnc`, `enableTelnet`, the three ports,
|
||||
`rdpSecurity`, `rdpIgnoreCert`, `guacamoleConfig`) into its host settings;
|
||||
the protocol logins stay core host fields.
|
||||
- **B14** moved remote desktop onto the SDK: it imports nothing from core,
|
||||
owns no module state, and its guacamole-lite server is built in `activate`
|
||||
with its own signal handlers removed (guacamole-lite installs SIGTERM and
|
||||
SIGINT handlers in its constructor). guacd is always an external service,
|
||||
so there is no `ctx.process`; its address is an admin setting.
|
||||
@@ -161,11 +161,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
ssh-terminal host settings shipped (`ssh-terminal-settings-migration.ts`)
|
||||
and the terminal reads its settings, but core host routes, types, the
|
||||
export/import sample and `HostEditorData` still carry the columns.
|
||||
`enable_terminal_toolbar` also drives the RDP/VNC/Telnet toolbar
|
||||
(`remote-desktop/GuacamoleApp.tsx`), whose host editor switch stays in the
|
||||
General tab, so remote-desktop needs its own host setting and copy first.
|
||||
Then drop all three in lockstep. Owner: remote-desktop's step for its copy,
|
||||
then D0.
|
||||
**B14** gave remote desktop its own `enableToolbar` host setting, copied
|
||||
from `enable_terminal_toolbar`, so nothing else holds the column up. Drop
|
||||
all three the B14 way (see "Order at boot" in ARCHITECTURE.md). Owner: D0.
|
||||
- **B9 (ssh-terminal):** the legacy `settings` rows for the terminal
|
||||
(`terminal_session_timeout_minutes`, `terminal_session_persistence_enabled`,
|
||||
`command_history_enabled`, `touch_input_settings`, `terminal_image_*`) are
|
||||
@@ -255,17 +253,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
section. Owner: whoever designs the 3.0.0 install flow (prebuild-per-platform
|
||||
packaging, or a documented "avoid native code" rule for community plugins).
|
||||
|
||||
- **B12 (session-sharing):** the remote desktop side of sharing has no
|
||||
provider yet. Session sharing reaches rdp, vnc and telnet sessions through
|
||||
`sessions.live` named providers `rdp`, `vnc` and `telnet`, each with
|
||||
`getSession(sessionId)` (id, userId as the session's owner, hostId,
|
||||
hostName, isConnected, createdAt, tabInstanceId) and
|
||||
`createViewerToken(sessionId, readOnly)`, typed in
|
||||
`plugins/session-sharing/src/backend/live.ts`. Until remote-desktop provides
|
||||
them, sharing or presenting an RDP/VNC/Telnet session is refused and those
|
||||
links resolve 404. B14 also drops `createJoinToken`/`getSessionInfo` from
|
||||
its `remote-desktop.sessions` registry entry, which core no longer reads.
|
||||
Owner: B14.
|
||||
- **B12 (session-sharing):** `allow_session_sharing` is still a live `ssh_data`
|
||||
column. The copy into the plugin's host setting shipped
|
||||
(`session-sharing-settings-migration.ts`) and nothing in core reads or
|
||||
@@ -291,9 +278,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
a real behaviour change from the old column default, made once here rather
|
||||
than silently. `ssh-terminal/src/backend/terminal-socket.ts` still reads
|
||||
`resolvedHostData?.enableSessionLogging ?? hostConfig.enableSessionLogging`
|
||||
off the resolved SSH host instead of `ctx.settings.getHost`, and
|
||||
`plugins/remote-desktop/src/backend/routes.ts`'s
|
||||
`host.enableSessionLogging !== false` check, `HostEditorData.ts`,
|
||||
off the resolved SSH host instead of `ctx.settings.getHost` (remote desktop
|
||||
asks `recordings.writer.enabledFor` since B14), and `HostEditorData.ts`,
|
||||
`HostManagerData.ts`, `quick-connect-host.ts` and the export/import sample
|
||||
all still read the column directly. The General tab's checkbox was removed
|
||||
from `HostEditor.tsx` (the schema-driven Plugins group replaces it); the
|
||||
@@ -301,14 +287,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
(`schema.ts`, `db/index.ts`, a drizzle migration per dialect,
|
||||
`schema:generate`) needs every one of those switched first. Owner: a
|
||||
dedicated follow-up step, or D0.
|
||||
- **B13 (session-recording):** `remote-desktop`'s guacd recordings now insert
|
||||
through the optional `recordings.writer` service's `createFinished(input)`
|
||||
(added alongside `open()`/`RecordingSink` for ssh-terminal's incremental
|
||||
writer) instead of importing `createCurrentSessionRecordingRepository`
|
||||
directly. Without the session-recording plugin enabled, a guacd recording
|
||||
is still written to disk but gets no `session_recordings` row and so never
|
||||
appears in the list - a warning is logged. Owner: none needed unless a
|
||||
second consumer of `recordings.writer` appears.
|
||||
- **B13 (session-recording):** `accessId` (the legacy `access_id` column,
|
||||
referencing the now-core-only `host_access` table) is carried in the
|
||||
adopted table's shape as a plain unreferenced integer, since the SDK's
|
||||
@@ -317,6 +295,56 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
in core before this step too). Owner: none needed unless a future step
|
||||
wants to actually use it.
|
||||
|
||||
- **B14 (remote-desktop):** the moved columns stay in the database, unused:
|
||||
`ssh_data.enable_rdp`, `enable_vnc`, `enable_telnet`, `rdp_port`,
|
||||
`vnc_port`, `telnet_port`, `rdp_security`, `rdp_ignore_cert`, `security`,
|
||||
`ignore_cert`, `guacamole_config`, and `user_preferences.rdp_defaults`.
|
||||
Their drizzle migrations are `SELECT 1;` on purpose (the copy runs after
|
||||
drizzle on Postgres and MySQL). Drop them physically in 3.0.0, once every
|
||||
install has booted 2.9.0. The legacy `guac_enabled` and `guac_url` rows in
|
||||
`settings` are left too. Owner: 3.0.0.
|
||||
- **B14, found in B5 (proxmox):** `drizzle/postgres/0036_redundant_ender_wiggin.sql`
|
||||
and `drizzle/mysql/0035_clumsy_leech.sql` really `DROP COLUMN` the four
|
||||
proxmox host columns. On Postgres and MySQL that runs at boot before
|
||||
`runProxmoxSettingsMigration` copies them, so an upgrade there loses every
|
||||
host's Proxmox settings. Since nothing has shipped, rewrite those two files
|
||||
to the `SELECT 1;` pattern (keeping their journal entries). Owner: D0.
|
||||
- **B14, found in B5 to B13:** every earlier host-column copy
|
||||
(`proxmox`, `file-manager`, `tunnels`, `web-endpoint`, `ssh-terminal`,
|
||||
`tmux-monitor`, `session-sharing`, `session-recording` settings migrations)
|
||||
reads the old columns with `getDb().all(sql...)`, which only exists on
|
||||
SQLite. On Postgres and MySQL the call throws, the migration's catch logs a
|
||||
warning and nothing is copied. Switch them to `selectRows` from
|
||||
`utils/crypto-migration/raw-rows.ts`, which B14 added and uses. Owner: D0.
|
||||
- **B14, found in B13:** `session-recording-settings-migration.ts` writes
|
||||
`row.enable_session_logging !== false`, but SQLite returns `0`, so a host
|
||||
that had recording off migrates as on. Its test only uses real booleans.
|
||||
Owner: D0.
|
||||
- **B14 (remote-desktop), for D2:** host payloads carry every plugin's host
|
||||
settings after the sharing sanitizers, so a connect-level recipient now
|
||||
receives remote desktop's `guacamoleConfig`, which can hold an RDP gateway
|
||||
password in plain text. A view-level recipient saw it before B14 too.
|
||||
Decide whether plugin host settings need a per-field sharing level, or make
|
||||
the gateway password its own secret field. Owner: D2.
|
||||
- **B14 (remote-desktop):** core still holds some locale keys that look dead
|
||||
after this step but may be built dynamically: `hosts.guac.noCredential`,
|
||||
`authType*`, `recording*`, `saveHostFirst`, `sharingOptionsAfterSave`,
|
||||
`permissionLevel`, `typeHeader`, `targetHeader`, `permissionHeader`,
|
||||
`hosts.connectRdp/Vnc/Telnet`, `hosts.copy*UrlAction`, `hosts.telnet` and
|
||||
`homepage.connType_*`. Owner: whoever next cleans up `src/ui/locales/en.json`,
|
||||
or D0.
|
||||
- **Pre-existing, found in B14:** `npm run test -w plugins/web-endpoint`
|
||||
fails "answers 503 while the tunnels plugin is not available": the route
|
||||
expects `ctx.services.get` to throw for a missing service, but since B12
|
||||
the runtime and the test doubles hand back an empty handle, so the route
|
||||
calls an undefined `forward` and answers 500. Check `typeof forward` the
|
||||
way remote desktop does. Owner: D0.
|
||||
- **Pre-existing, found in B14:** each plugin's own `typecheck` script
|
||||
(`tsc -p tsconfig.json`) fails with the SDK's `tsconfig.plugin.json`
|
||||
(node16 resolution wants `.js` on relative imports and cannot resolve
|
||||
`@termix/plugin-sdk/ui`), workspaces included. `npm run type-check` covers
|
||||
plugins through `tsconfig.plugins*.json` and passes. Owner: D0.
|
||||
|
||||
## Manual checks after 2.9.0
|
||||
|
||||
- SSH terminal: password auth, key auth (and an encrypted key's passphrase
|
||||
@@ -334,3 +362,15 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
off records nothing, retention prunes an old recording on schedule, a guacd
|
||||
RDP/VNC/Telnet recording still gets a row and plays back, and deleting a
|
||||
user anonymizes their recordings instead of removing them.
|
||||
- Remote desktop: an RDP, a VNC and a Telnet host; a host behind one jump
|
||||
host (tunnels plugin on and off) and behind a chain of two; macOS Screen
|
||||
Sharing over VNC with no username; RDP recording (and none with
|
||||
session-recording off or the host's switch off); file transfer through the
|
||||
RDP drive, drag and drop included; clipboard both ways; sharing an RDP
|
||||
session by link (read-only and read-write) and presenting it in a collab
|
||||
room; opening Windows Remote Desktop from the desktop app on Windows;
|
||||
changing the guacd URL in admin settings without a restart; turning Remote
|
||||
Desktop off in admin (actions disappear, connects answer 403); a 2.8 host
|
||||
that only had `connection_type = rdp` comes up as RDP with SSH off; RDP user
|
||||
defaults apply to a saved host; Quick Connect over RDP and VNC; the desktop
|
||||
app with a host on the remote server and one on This device.
|
||||
@@ -510,6 +510,10 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Registered component id. Required when type is \"custom\"."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "Stored and validated, but not drawn by the generic settings form."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,6 +606,10 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Registered component id. Required when type is \"custom\"."
|
||||
},
|
||||
"hidden": {
|
||||
"type": "boolean",
|
||||
"description": "Stored and validated, but not drawn by the generic settings form."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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