@termix-ssh/plugin-sdk 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/PLUGIN-GUIDE.md +58 -35
- package/cli/lib/tmxplug.mjs +3 -0
- package/dist/backend.d.ts +37 -3
- package/dist/host/{chunk-NZG6HNQH.js → chunk-7RRTYD6B.js} +10 -3
- package/dist/host/{chunk-DIOJ3KW2.js → chunk-LSWB3ZZV.js} +1 -1
- package/dist/host/{chunk-G346FUE4.js → chunk-RLQBKJRK.js} +20 -12
- package/dist/host/{linked-server-QAKKDBFM.js → linked-server-UCW74T2V.js} +1 -1
- package/dist/host/{rbac-api-5267EMX7.js → rbac-api-6UYMFIFV.js} +1 -1
- package/dist/host/testing-host.js +3 -3
- package/dist/host/types/src/ui/lib/connection-origin.d.ts +4 -5
- package/dist/host/types/src/ui/lib/electron.d.ts +5 -0
- package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +15 -0
- package/dist/host/types/src/ui/sidebar/HostManagerTabs.d.ts +10 -4
- package/dist/host/ui.js +9 -6
- package/dist/host/{use-permissions-2P2TP6GY.js → use-permissions-GYCBY2AU.js} +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +1 -1
- package/dist/testing.d.ts +12 -1
- package/dist/testing.js +25 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.11
|
|
4
|
+
|
|
5
|
+
Plugin API 1.3.0.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `ctx.auth.issueUserToken()` mints a short-lived session for a user, for calling Termix's own API as them from background work. Needs `users:impersonate` and `engine.api` 1.3
|
|
10
|
+
- `ctx.users.getLocale()` reads a user's UI language. Needs `users:read` and `engine.api` 1.3
|
|
11
|
+
|
|
12
|
+
## 1.0.10
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `termix-plugin pack` includes the plugin's `LICENSE` and `NOTICE` files, so third-party notices ship with it
|
|
17
|
+
|
|
3
18
|
## 1.0.9
|
|
4
19
|
|
|
5
20
|
### Added
|
package/PLUGIN-GUIDE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Termix plugin guide
|
|
2
2
|
|
|
3
|
-
SDK 1.0.
|
|
3
|
+
SDK 1.0.11 | Plugin API 1.3.0
|
|
4
4
|
|
|
5
5
|
This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
|
|
6
6
|
`@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
|
|
@@ -90,7 +90,7 @@ npm test # termix-plugin test: Vitest
|
|
|
90
90
|
npm run typecheck # tsc
|
|
91
91
|
npm run validate # termix-plugin validate: manifest, files, migrations, changelog, docs
|
|
92
92
|
npm run migrations # termix-plugin migrations: write migrations after changing tables.ts
|
|
93
|
-
npm run pack # termix-plugin pack: write <id>-<version>.tmxplug
|
|
93
|
+
npm run pack # termix-plugin pack: write <id>-<version>.tmxplug (with LICENSE and NOTICE)
|
|
94
94
|
npm run format # Prettier
|
|
95
95
|
npm run format:check
|
|
96
96
|
```
|
|
@@ -105,7 +105,7 @@ Before you call a change finished:
|
|
|
105
105
|
- [ ] `npm run build` passes
|
|
106
106
|
- [ ] `npm run format:check` passes
|
|
107
107
|
- [ ] `npx termix-plugin migrations --check` passes (you ran `npm run migrations` after changing tables)
|
|
108
|
-
- [ ] New strings are in `locales/en.json`
|
|
108
|
+
- [ ] New strings are in `locales/en.json`, and `plugin.name` / `plugin.description` match the manifest
|
|
109
109
|
- [ ] New routes have an `@openapi` block and a test, including without the permission
|
|
110
110
|
- [ ] New env vars are listed in `env` in the manifest
|
|
111
111
|
- [ ] `docs/` and the "About this plugin" section of `AGENTS.md` still match what the plugin does
|
|
@@ -225,6 +225,8 @@ Bundled as ESM for Node 22+ into `dist/backend.js`. Backend imports of your own
|
|
|
225
225
|
| `audit.record` | none | Write your own audit line |
|
|
226
226
|
| `sync` | none | Sync your tables to a linked desktop |
|
|
227
227
|
| `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
|
|
228
|
+
| `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
|
|
229
|
+
| `users.getLocale` | `users:read` | A user's UI language |
|
|
228
230
|
| `system` | `system:tls` | The server's HTTPS certificate |
|
|
229
231
|
| `plugins.list()` | `plugins:read` | Which plugins are installed |
|
|
230
232
|
| `asUser(userId, fn)` | always audited | Run background work as a user |
|
|
@@ -237,6 +239,13 @@ In a route the actor is the signed-in user, set by Termix. `ctx.currentActor()`
|
|
|
237
239
|
work with no request (a timer, an event) uses `ctx.asUser(userId, fn)`, which is audited and still applies
|
|
238
240
|
that user's permissions.
|
|
239
241
|
|
|
242
|
+
Background work that must call Termix's own HTTP API (the Termix CLI, say) asks for a session with
|
|
243
|
+
`ctx.auth.issueUserToken(userId, { purpose, ttlSeconds })` and sends it as `Authorization: Bearer <token>`.
|
|
244
|
+
It needs `users:impersonate` and `engine.api` `^1.3`, lasts 300 seconds by default and 900 at most, and is
|
|
245
|
+
audited as `plugin_user_token_issued`. The user's permissions apply, and it is refused on routes that change
|
|
246
|
+
their password, second factors, API keys or linked logins. `ctx.users.getLocale(userId)` gives the user's
|
|
247
|
+
UI language, like `zh_CN`, or null, for writing to them in it.
|
|
248
|
+
|
|
240
249
|
### Routes
|
|
241
250
|
|
|
242
251
|
Routes live at `/plugin-api/<id>/`, sockets at `/plugin-ws/<id>/`, on the main Termix port. You never need
|
|
@@ -484,7 +493,7 @@ your own host settings, under `host.pluginSettings[<id>]`.
|
|
|
484
493
|
|
|
485
494
|
Slots you can fill include `terminal.toolbar`, `terminal.toolbarStatus`, `terminal.sidePanel`,
|
|
486
495
|
`terminal.overlay`, `dashboard.counters`, `dashboard.hostMetrics`, `admin.userTabs`, `hosts.importMenu`,
|
|
487
|
-
`hosts.panel`, `credentials.badges`, `shell.overlay` and `tab.menu`. See
|
|
496
|
+
`hosts.panel`, `credentials.badges`, `shell.overlay`, `shell.rightEdge` and `tab.menu`. See
|
|
488
497
|
https://docs.termix.site/develop/more-extension-points.
|
|
489
498
|
|
|
490
499
|
## Looking native
|
|
@@ -538,6 +547,20 @@ Plurals use `_one` and `_other` with `t("hosts", { count })`. Other languages go
|
|
|
538
547
|
`locales/translated/<code>.json` (like `de_DE.json`) with the same keys. Keep `{{placeholders}}` as they are.
|
|
539
548
|
Untranslated strings show in English.
|
|
540
549
|
|
|
550
|
+
Termix shows your plugin's name and description in the plugin manager, the setup picker and settings.
|
|
551
|
+
Put them in `locales/en.json` as `plugin.name` and `plugin.description`, with the same text as `name` and
|
|
552
|
+
`description` in the manifest, so they get translated too. Without them, the English manifest text shows in
|
|
553
|
+
every language.
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
{
|
|
557
|
+
"plugin": {
|
|
558
|
+
"name": "Host Metrics",
|
|
559
|
+
"description": "Live CPU, memory, disk, network and GPU stats."
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
541
564
|
## Docs
|
|
542
565
|
|
|
543
566
|
`docs/index.md` is the first page: what the plugin does in a sentence or two, how to turn it on, how to use
|
|
@@ -740,37 +763,37 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
|
|
|
740
763
|
|
|
741
764
|
## Capabilities
|
|
742
765
|
|
|
743
|
-
| Capability | Risk | What it allows
|
|
744
|
-
| -------------------- | -------- |
|
|
745
|
-
| `credentials:read` | critical | Read stored secrets for hosts the user can reach
|
|
746
|
-
| `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks
|
|
747
|
-
| `ssh:connect` | high | Run any command on hosts, with the user's access
|
|
748
|
-
| `process:spawn` | high | Run programs on the Termix server
|
|
749
|
-
| `users:write` | high | Add users and change their roles
|
|
750
|
-
| `users:impersonate` | high |
|
|
751
|
-
| `db:core-refs` | high | Query core users, roles and hosts tables (read only)
|
|
752
|
-
| `auth:provide` | high | Add login methods, second factors, SSH auth types
|
|
753
|
-
| `device:serial` | high | Open serial and USB ports on the server
|
|
754
|
-
| `notify:hub` | high | Receive every alert (the alerts hub only)
|
|
755
|
-
| `hosts:write` | medium | Add and edit hosts
|
|
756
|
-
| `credentials:use` | medium | Connect with saved credentials without seeing them
|
|
757
|
-
| `credentials:write` | medium | Save new credentials
|
|
758
|
-
| `network:outbound` | medium | Outbound HTTP through `ctx.fetch`
|
|
759
|
-
| `network:serve` | medium | HTTP routes and sockets
|
|
760
|
-
| `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN
|
|
761
|
-
| `users:read` | medium | Usernames and
|
|
762
|
-
| `events:core` | medium | Listen to core events
|
|
763
|
-
| `notify:send` | medium | Send alerts
|
|
764
|
-
| `audit:read` | medium | Read the audit log
|
|
765
|
-
| `desktop:window` | medium | Open windows in the desktop app
|
|
766
|
-
| `hosts:read` | low | Read hosts the user can see
|
|
767
|
-
| `db:own` | low | Your own tables
|
|
768
|
-
| `kv:own` | low | Small key value state
|
|
769
|
-
| `files:own` | low | Your own folder on disk
|
|
770
|
-
| `secrets:own` | low | Your own encrypted secrets
|
|
771
|
-
| `settings:read-core` | low | Read Termix server settings
|
|
772
|
-
| `plugins:read` | low | See installed plugins
|
|
773
|
-
| `ui:surface` | low | Add tabs, panels and settings
|
|
766
|
+
| Capability | Risk | What it allows |
|
|
767
|
+
| -------------------- | -------- | ------------------------------------------------------- |
|
|
768
|
+
| `credentials:read` | critical | Read stored secrets for hosts the user can reach |
|
|
769
|
+
| `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
|
|
770
|
+
| `ssh:connect` | high | Run any command on hosts, with the user's access |
|
|
771
|
+
| `process:spawn` | high | Run programs on the Termix server |
|
|
772
|
+
| `users:write` | high | Add users and change their roles |
|
|
773
|
+
| `users:impersonate` | high | Act as another user, or get a session for them, audited |
|
|
774
|
+
| `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
|
|
775
|
+
| `auth:provide` | high | Add login methods, second factors, SSH auth types |
|
|
776
|
+
| `device:serial` | high | Open serial and USB ports on the server |
|
|
777
|
+
| `notify:hub` | high | Receive every alert (the alerts hub only) |
|
|
778
|
+
| `hosts:write` | medium | Add and edit hosts |
|
|
779
|
+
| `credentials:use` | medium | Connect with saved credentials without seeing them |
|
|
780
|
+
| `credentials:write` | medium | Save new credentials |
|
|
781
|
+
| `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
|
|
782
|
+
| `network:serve` | medium | HTTP routes and sockets |
|
|
783
|
+
| `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
|
|
784
|
+
| `users:read` | medium | Usernames, roles and UI language |
|
|
785
|
+
| `events:core` | medium | Listen to core events |
|
|
786
|
+
| `notify:send` | medium | Send alerts |
|
|
787
|
+
| `audit:read` | medium | Read the audit log |
|
|
788
|
+
| `desktop:window` | medium | Open windows in the desktop app |
|
|
789
|
+
| `hosts:read` | low | Read hosts the user can see |
|
|
790
|
+
| `db:own` | low | Your own tables |
|
|
791
|
+
| `kv:own` | low | Small key value state |
|
|
792
|
+
| `files:own` | low | Your own folder on disk |
|
|
793
|
+
| `secrets:own` | low | Your own encrypted secrets |
|
|
794
|
+
| `settings:read-core` | low | Read Termix server settings |
|
|
795
|
+
| `plugins:read` | low | See installed plugins |
|
|
796
|
+
| `ui:surface` | low | Add tabs, panels and settings |
|
|
774
797
|
|
|
775
798
|
An update that adds a capability waits for an admin to approve it.
|
|
776
799
|
|
package/cli/lib/tmxplug.mjs
CHANGED
package/dist/backend.d.ts
CHANGED
|
@@ -1223,6 +1223,18 @@ export interface PluginSecondFactor {
|
|
|
1223
1223
|
/** Removes the user's enrolment. Called by the admin reset. */
|
|
1224
1224
|
reset?: (userId: string) => Promise<void>;
|
|
1225
1225
|
}
|
|
1226
|
+
export interface PluginUserTokenOptions {
|
|
1227
|
+
/** Why the token exists, kept in the audit log and on the session. */
|
|
1228
|
+
purpose: string;
|
|
1229
|
+
/** Seconds until it expires. Defaults to 300 and is capped at 900. */
|
|
1230
|
+
ttlSeconds?: number;
|
|
1231
|
+
}
|
|
1232
|
+
export interface PluginUserToken {
|
|
1233
|
+
/** Send as `Authorization: Bearer <token>` to core's own API. */
|
|
1234
|
+
token: string;
|
|
1235
|
+
/** ISO time the session ends. */
|
|
1236
|
+
expiresAt: string;
|
|
1237
|
+
}
|
|
1226
1238
|
/**
|
|
1227
1239
|
* Login methods, second factors and SSH auth types. Every register call needs
|
|
1228
1240
|
* auth:provide and a matching contributes.auth entry, and is undone on
|
|
@@ -1278,6 +1290,22 @@ export interface PluginAuth {
|
|
|
1278
1290
|
* delete a provider people still sign in with.
|
|
1279
1291
|
*/
|
|
1280
1292
|
countLinkedUsers: (provider: string) => Promise<number>;
|
|
1293
|
+
/**
|
|
1294
|
+
* A short-lived session for an existing user, for calling core's own HTTP
|
|
1295
|
+
* API as them from work with no request behind it, like a scheduled task
|
|
1296
|
+
* driving the Termix CLI. The user's RBAC applies unchanged. Needs
|
|
1297
|
+
* users:impersonate, not auth:provide, and every call is audited.
|
|
1298
|
+
*
|
|
1299
|
+
* The session is marked with the plugin and purpose. It cannot change the
|
|
1300
|
+
* user's password, second factors, API keys or linked logins, so it can
|
|
1301
|
+
* never outlive itself.
|
|
1302
|
+
*/
|
|
1303
|
+
issueUserToken: (userId: string, options: PluginUserTokenOptions) => Promise<PluginUserToken>;
|
|
1304
|
+
}
|
|
1305
|
+
/** What a plugin may learn about a user. Needs users:read. */
|
|
1306
|
+
export interface PluginUsers {
|
|
1307
|
+
/** The user's UI language, like "zh_CN", or null when they never chose one. */
|
|
1308
|
+
getLocale: (userId: string) => Promise<string | null>;
|
|
1281
1309
|
}
|
|
1282
1310
|
export interface PluginOpenIsolatedWindowRequest {
|
|
1283
1311
|
/** http(s) only. Refused when it points anywhere else. */
|
|
@@ -1738,8 +1766,13 @@ export interface PluginContext {
|
|
|
1738
1766
|
readonly hosts: PluginHosts;
|
|
1739
1767
|
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
|
|
1740
1768
|
readonly ssh: PluginSsh;
|
|
1741
|
-
/**
|
|
1769
|
+
/**
|
|
1770
|
+
* Login methods, second factors and SSH auth types. Needs auth:provide,
|
|
1771
|
+
* except issueUserToken, which needs users:impersonate.
|
|
1772
|
+
*/
|
|
1742
1773
|
readonly auth: PluginAuth;
|
|
1774
|
+
/** What a plugin may learn about a user. Needs users:read. */
|
|
1775
|
+
readonly users: PluginUsers;
|
|
1743
1776
|
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
|
1744
1777
|
readonly desktop: PluginDesktop;
|
|
1745
1778
|
/** Host protocol credentials and the user's saved SSH keys. See PluginCredentials. */
|
|
@@ -1763,8 +1796,9 @@ export interface PluginContext {
|
|
|
1763
1796
|
* no request behind it. Always audited, and every core API inside still
|
|
1764
1797
|
* applies that user's RBAC.
|
|
1765
1798
|
*
|
|
1766
|
-
* Needs users:impersonate. That and a `userId` option
|
|
1767
|
-
* secrets.getShared are the only ways a plugin can
|
|
1799
|
+
* Needs users:impersonate. That, auth.issueUserToken and a `userId` option
|
|
1800
|
+
* on services.get or secrets.getShared are the only ways a plugin can act
|
|
1801
|
+
* as a user it names.
|
|
1768
1802
|
*/
|
|
1769
1803
|
asUser: <T>(userId: string, fn: () => Promise<T> | T) => Promise<T>;
|
|
1770
1804
|
/** The acting user for the current call, when there is one. */
|
|
@@ -13,6 +13,12 @@ function isElectron() {
|
|
|
13
13
|
const isElectronProp = win.electronAPI?.isElectron === true;
|
|
14
14
|
return hasISElectron || hasElectronAPI || isElectronProp;
|
|
15
15
|
}
|
|
16
|
+
var DEFAULT_LOCAL_BACKEND_PORT = 30001;
|
|
17
|
+
function localBackendPort() {
|
|
18
|
+
if (typeof window === "undefined") return DEFAULT_LOCAL_BACKEND_PORT;
|
|
19
|
+
const port = window.electronAPI?.backendPort;
|
|
20
|
+
return typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_LOCAL_BACKEND_PORT;
|
|
21
|
+
}
|
|
16
22
|
|
|
17
23
|
// src/ui/lib/error-message.ts
|
|
18
24
|
function getErrorMessage(error, fallback = "Unknown error") {
|
|
@@ -5038,7 +5044,7 @@ async function updateRole(roleId, roleData) {
|
|
|
5038
5044
|
try {
|
|
5039
5045
|
const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
|
|
5040
5046
|
if (roleData.permissions) {
|
|
5041
|
-
const { notifyPermissionsChanged } = await import("./use-permissions-
|
|
5047
|
+
const { notifyPermissionsChanged } = await import("./use-permissions-GYCBY2AU.js");
|
|
5042
5048
|
notifyPermissionsChanged();
|
|
5043
5049
|
}
|
|
5044
5050
|
return response.data;
|
|
@@ -5575,7 +5581,7 @@ function getApiUrl(path, defaultPort) {
|
|
|
5575
5581
|
const devMode = isDev();
|
|
5576
5582
|
const electronMode = isElectron();
|
|
5577
5583
|
if (electronMode) {
|
|
5578
|
-
return `http://localhost:${
|
|
5584
|
+
return `http://localhost:${localBackendPort()}${path}`;
|
|
5579
5585
|
} else if (devMode) {
|
|
5580
5586
|
if (!define_import_meta_env_default.VITE_API_HOST) {
|
|
5581
5587
|
return `/__termix_api/${defaultPort}${path}`;
|
|
@@ -5595,7 +5601,7 @@ function createRemoteOriginApiInstance(path) {
|
|
|
5595
5601
|
});
|
|
5596
5602
|
instance.interceptors.request.use(
|
|
5597
5603
|
async (config) => {
|
|
5598
|
-
const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-
|
|
5604
|
+
const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-UCW74T2V.js");
|
|
5599
5605
|
const linked = await getLinkedSession2();
|
|
5600
5606
|
config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
|
|
5601
5607
|
if (config.headers.set) {
|
|
@@ -5958,6 +5964,7 @@ export {
|
|
|
5958
5964
|
getPluginRecord,
|
|
5959
5965
|
knownPluginIds,
|
|
5960
5966
|
isElectron,
|
|
5967
|
+
localBackendPort,
|
|
5961
5968
|
createRegistry,
|
|
5962
5969
|
byOrderThenId,
|
|
5963
5970
|
manifestDeclares,
|
|
@@ -21,7 +21,7 @@ var inFlight = null;
|
|
|
21
21
|
function load() {
|
|
22
22
|
if (cache) return Promise.resolve(cache);
|
|
23
23
|
if (inFlight) return inFlight;
|
|
24
|
-
inFlight = import("./rbac-api-
|
|
24
|
+
inFlight = import("./rbac-api-6UYMFIFV.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
|
|
25
25
|
cache = {
|
|
26
26
|
permissions: result.permissions ?? [],
|
|
27
27
|
isAdmin: !!result.isAdmin
|
|
@@ -2,7 +2,7 @@ const __termixNoGlob = () => ({});
|
|
|
2
2
|
import {
|
|
3
3
|
hasPermission,
|
|
4
4
|
usePermissions
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-LSWB3ZZV.js";
|
|
6
6
|
import {
|
|
7
7
|
PRESETS,
|
|
8
8
|
SYNC_CHANGED_EVENT,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
handleApiError,
|
|
26
26
|
isElectron,
|
|
27
27
|
knownPluginIds,
|
|
28
|
+
localBackendPort,
|
|
28
29
|
logActivity,
|
|
29
30
|
manifestDeclares,
|
|
30
31
|
markUserAuthenticated,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
usePluginStore,
|
|
40
41
|
useTabTypes,
|
|
41
42
|
websocketAuthProtocols
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-7RRTYD6B.js";
|
|
43
44
|
import {
|
|
44
45
|
__export,
|
|
45
46
|
define_import_meta_env_default
|
|
@@ -49078,6 +49079,17 @@ function scopeHostCallback(fn, pluginId) {
|
|
|
49078
49079
|
|
|
49079
49080
|
// src/ui/plugin-host/scope.tsx
|
|
49080
49081
|
import { useTranslation } from "react-i18next";
|
|
49082
|
+
|
|
49083
|
+
// src/ui/lib/plugin-i18n.ts
|
|
49084
|
+
function pluginKey(pluginId, key) {
|
|
49085
|
+
if (!key) return key;
|
|
49086
|
+
return key.includes(":") ? key : `${pluginId}:${key}`;
|
|
49087
|
+
}
|
|
49088
|
+
function pluginName(t, plugin) {
|
|
49089
|
+
return t(pluginKey(plugin.id, "plugin.name"), { defaultValue: plugin.name });
|
|
49090
|
+
}
|
|
49091
|
+
|
|
49092
|
+
// src/ui/plugin-host/scope.tsx
|
|
49081
49093
|
import { jsx as jsx29, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
49082
49094
|
var PluginScopeContext = createContext6(null);
|
|
49083
49095
|
function usePluginScope() {
|
|
@@ -49088,7 +49100,8 @@ function CrashNotice({
|
|
|
49088
49100
|
onRetry
|
|
49089
49101
|
}) {
|
|
49090
49102
|
const { t } = useTranslation();
|
|
49091
|
-
const
|
|
49103
|
+
const summary = getPluginRecord(pluginId)?.summary;
|
|
49104
|
+
const name = summary ? pluginName(t, summary) : pluginId;
|
|
49092
49105
|
return /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center justify-center gap-2 p-4 text-center text-sm text-muted-foreground", children: [
|
|
49093
49106
|
/* @__PURE__ */ jsx29("span", { children: t("plugins.runtime.crashed", { name }) }),
|
|
49094
49107
|
/* @__PURE__ */ jsx29(
|
|
@@ -53579,7 +53592,7 @@ async function pluginWsUrl(pluginId, path2, options2 = {}) {
|
|
|
53579
53592
|
if (isElectron()) {
|
|
53580
53593
|
return buildOriginWsUrl({
|
|
53581
53594
|
origin: options2.origin ?? "local",
|
|
53582
|
-
localPort:
|
|
53595
|
+
localPort: localBackendPort(),
|
|
53583
53596
|
localPath: route,
|
|
53584
53597
|
remotePath: route
|
|
53585
53598
|
});
|
|
@@ -54432,12 +54445,6 @@ function onRemoteServerChange(listener) {
|
|
|
54432
54445
|
return () => window.removeEventListener(SYNC_CHANGED_EVENT, handler);
|
|
54433
54446
|
}
|
|
54434
54447
|
|
|
54435
|
-
// src/ui/lib/plugin-i18n.ts
|
|
54436
|
-
function pluginKey(pluginId, key) {
|
|
54437
|
-
if (!key) return key;
|
|
54438
|
-
return key.includes(":") ? key : `${pluginId}:${key}`;
|
|
54439
|
-
}
|
|
54440
|
-
|
|
54441
54448
|
// src/ui/plugin-host/app.ts
|
|
54442
54449
|
var ONBOARDING_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
54443
54450
|
function createDocsApi(manifest) {
|
|
@@ -54973,7 +54980,7 @@ function PluginViewPlaceholder({
|
|
|
54973
54980
|
usePluginStore();
|
|
54974
54981
|
const { has } = usePermissions();
|
|
54975
54982
|
const { status, owner } = unregisteredViewStatus(kind, viewId);
|
|
54976
|
-
const name = owner
|
|
54983
|
+
const name = owner ? pluginName(t, owner.summary) : viewId;
|
|
54977
54984
|
if (status === "loading") {
|
|
54978
54985
|
return /* @__PURE__ */ jsx39(
|
|
54979
54986
|
"div",
|
|
@@ -55101,6 +55108,8 @@ export {
|
|
|
55101
55108
|
setShellHosts,
|
|
55102
55109
|
notifyShellReady,
|
|
55103
55110
|
resetShellBridge,
|
|
55111
|
+
pluginKey,
|
|
55112
|
+
pluginName,
|
|
55104
55113
|
usePluginScope,
|
|
55105
55114
|
DocsLink,
|
|
55106
55115
|
PANEL,
|
|
@@ -55177,7 +55186,6 @@ export {
|
|
|
55177
55186
|
installPluginHostBridge,
|
|
55178
55187
|
setRemoteServerUrlForTesting,
|
|
55179
55188
|
remoteServerUrl,
|
|
55180
|
-
pluginKey,
|
|
55181
55189
|
createPluginApp,
|
|
55182
55190
|
ActionSlot,
|
|
55183
55191
|
ComponentSlot,
|
|
@@ -40,16 +40,16 @@ import {
|
|
|
40
40
|
setShellCallbacks,
|
|
41
41
|
setShellHosts,
|
|
42
42
|
setShellLayoutProvider
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-RLQBKJRK.js";
|
|
44
44
|
import {
|
|
45
45
|
setPermissionsForTesting
|
|
46
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-LSWB3ZZV.js";
|
|
47
47
|
import {
|
|
48
48
|
getTabType,
|
|
49
49
|
listTabTypes,
|
|
50
50
|
setFrontendState,
|
|
51
51
|
setPluginSummaries
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-7RRTYD6B.js";
|
|
53
53
|
import "./chunk-YC2MAKSP.js";
|
|
54
54
|
|
|
55
55
|
// src/ui/plugin-host/testing-host.tsx
|
|
@@ -14,11 +14,10 @@ interface OriginResolvableHost {
|
|
|
14
14
|
* the desktop-wide default.
|
|
15
15
|
*
|
|
16
16
|
* `defaultRemote` makes a host left on Default resolve to "remote" instead of
|
|
17
|
-
* following the desktop-wide setting. Remote desktop passes it
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* that isn't there -- see Termix-SSH/Support#1240.
|
|
17
|
+
* following the desktop-wide setting. Remote desktop passes it when this
|
|
18
|
+
* desktop is set to use an external guacd, so a host is not moved onto a
|
|
19
|
+
* guacd that isn't there (see Termix-SSH/Support#1240). With the built-in
|
|
20
|
+
* engine it runs locally like SSH.
|
|
22
21
|
*/
|
|
23
22
|
export declare function resolveConnectionOrigin(host: OriginResolvableHost, options?: {
|
|
24
23
|
defaultRemote?: boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TFunction } from "i18next";
|
|
1
2
|
/**
|
|
2
3
|
* Where a plugin's i18n keys resolve from.
|
|
3
4
|
*
|
|
@@ -9,3 +10,17 @@
|
|
|
9
10
|
*/
|
|
10
11
|
export declare function pluginKey(pluginId: string, key: string): string;
|
|
11
12
|
export declare function pluginKey(pluginId: string, key: string | undefined): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* A plugin's name in the UI language: "plugin.name" from its own locales,
|
|
15
|
+
* else the manifest's. A plugin whose strings are not loaded, like one only
|
|
16
|
+
* listed in a registry, shows the manifest name.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pluginName(t: TFunction, plugin: {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}): string;
|
|
22
|
+
/** Same as pluginName, for "plugin.description". */
|
|
23
|
+
export declare function pluginDescription(t: TFunction, plugin: {
|
|
24
|
+
id: string;
|
|
25
|
+
description?: string | null;
|
|
26
|
+
}): string;
|
|
@@ -34,7 +34,7 @@ export interface HostEditorSectionDef {
|
|
|
34
34
|
pluginId?: string;
|
|
35
35
|
group: "top" | "ssh";
|
|
36
36
|
labelKey: string;
|
|
37
|
-
/**
|
|
37
|
+
/** Shown when labelKey has no translation. */
|
|
38
38
|
label?: string;
|
|
39
39
|
icon?: ComponentType<{
|
|
40
40
|
className?: string;
|
|
@@ -59,7 +59,13 @@ export declare function isSshGroupTab(id: string): boolean;
|
|
|
59
59
|
* The main strip. With `protocols`, registered tabs are filtered by their
|
|
60
60
|
* `visible`; without, every registered top tab is listed.
|
|
61
61
|
*/
|
|
62
|
-
export declare function makeHostTabs(t: (key: string
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
export declare function makeHostTabs(t: (key: string, options?: {
|
|
63
|
+
defaultValue?: string;
|
|
64
|
+
}) => string, protocols?: Record<string, boolean>, defaultsMode?: boolean): HostTab[];
|
|
65
|
+
export declare function makeHostSshSubTabs(t: (key: string, options?: {
|
|
66
|
+
defaultValue?: string;
|
|
67
|
+
}) => string, protocols?: Record<string, boolean>, defaultsMode?: boolean): HostTab[];
|
|
68
|
+
export declare function makeCredentialTabs(t: (key: string, options?: {
|
|
69
|
+
defaultValue?: string;
|
|
70
|
+
}) => string): CredentialTab[];
|
|
65
71
|
export { TabStrip } from "../components/tab-strip";
|
package/dist/host/ui.js
CHANGED
|
@@ -120,6 +120,7 @@ import {
|
|
|
120
120
|
isRedactedSecret,
|
|
121
121
|
normalizeLanguageCode,
|
|
122
122
|
pluginKey,
|
|
123
|
+
pluginName,
|
|
123
124
|
pluginWsUrl,
|
|
124
125
|
registerHostEditorSection,
|
|
125
126
|
rememberLoginLanguage,
|
|
@@ -139,8 +140,8 @@ import {
|
|
|
139
140
|
useSurfaceKind,
|
|
140
141
|
useTheme,
|
|
141
142
|
verifySecondFactor
|
|
142
|
-
} from "./chunk-
|
|
143
|
-
import "./chunk-
|
|
143
|
+
} from "./chunk-RLQBKJRK.js";
|
|
144
|
+
import "./chunk-LSWB3ZZV.js";
|
|
144
145
|
import {
|
|
145
146
|
TabProvider,
|
|
146
147
|
buildOriginWsUrl,
|
|
@@ -179,7 +180,7 @@ import {
|
|
|
179
180
|
useTabs,
|
|
180
181
|
useTabsSafe,
|
|
181
182
|
verifyPasswordResetCode
|
|
182
|
-
} from "./chunk-
|
|
183
|
+
} from "./chunk-7RRTYD6B.js";
|
|
183
184
|
import "./chunk-YC2MAKSP.js";
|
|
184
185
|
|
|
185
186
|
// src/ui/components/alert.tsx
|
|
@@ -3852,11 +3853,13 @@ function HostPluginSection({
|
|
|
3852
3853
|
return /* @__PURE__ */ jsxs24(
|
|
3853
3854
|
SectionCard,
|
|
3854
3855
|
{
|
|
3855
|
-
title: plugin
|
|
3856
|
+
title: pluginName(t2, plugin),
|
|
3856
3857
|
icon: /* @__PURE__ */ jsx36(PluginIcon, { name: plugin.icon, className: "size-3.5" }),
|
|
3857
3858
|
action: /* @__PURE__ */ jsx36(DocsLink, { href: plugin.docs }),
|
|
3858
3859
|
children: [
|
|
3859
|
-
!running && /* @__PURE__ */ jsx36("div", { className: "my-3 border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning", children: t2("settings.featureUnavailable", {
|
|
3860
|
+
!running && /* @__PURE__ */ jsx36("div", { className: "my-3 border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning", children: t2("settings.featureUnavailable", {
|
|
3861
|
+
name: pluginName(t2, plugin)
|
|
3862
|
+
}) }),
|
|
3860
3863
|
enableKey && /* @__PURE__ */ jsx36(
|
|
3861
3864
|
SettingsFieldRow,
|
|
3862
3865
|
{
|
|
@@ -4164,7 +4167,7 @@ function syncHostFeatureTabs(plugins) {
|
|
|
4164
4167
|
pluginId: plugin.id,
|
|
4165
4168
|
group: placement?.group ?? host.editorGroup ?? "top",
|
|
4166
4169
|
order: placement?.order ?? host.editorOrder ?? 100,
|
|
4167
|
-
labelKey: plugin.name,
|
|
4170
|
+
labelKey: pluginKey(plugin.id, "plugin.name"),
|
|
4168
4171
|
label: plugin.name,
|
|
4169
4172
|
// Nothing but the plugin's host settings, so it works for defaults too.
|
|
4170
4173
|
defaults: ownSections.has(plugin.id) ? "only" : true,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
19
19
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
20
20
|
* version by the server.
|
|
21
21
|
*/
|
|
22
|
-
export declare const PLUGIN_API_VERSION = "1.
|
|
22
|
+
export declare const PLUGIN_API_VERSION = "1.3.0";
|
|
23
23
|
/** The API major, the default engine.api for a new plugin. */
|
|
24
24
|
export declare const SUPPORTED_PLUGIN_API_VERSION: string;
|
|
25
25
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
package/dist/manifest.js
CHANGED
|
@@ -21,7 +21,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
|
|
|
21
21
|
* "^1.2" needs 1.2 or later). engine.termix is checked against the core
|
|
22
22
|
* version by the server.
|
|
23
23
|
*/
|
|
24
|
-
export const PLUGIN_API_VERSION = "1.
|
|
24
|
+
export const PLUGIN_API_VERSION = "1.3.0";
|
|
25
25
|
/** The API major, the default engine.api for a new plugin. */
|
|
26
26
|
export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
|
|
27
27
|
/** Whether this build's plugin API satisfies a manifest's engine.api. */
|
package/dist/testing.d.ts
CHANGED
|
@@ -112,6 +112,8 @@ export interface FakeContextOptions {
|
|
|
112
112
|
linkedUsers?: Record<string, number>;
|
|
113
113
|
/** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
|
|
114
114
|
loginAttemptLimit?: number;
|
|
115
|
+
/** What ctx.users.getLocale answers, by user id. A missing user answers null. */
|
|
116
|
+
locales?: Record<string, string>;
|
|
115
117
|
}
|
|
116
118
|
export interface FakeProcessOptions {
|
|
117
119
|
run?: (file: string, args: readonly string[], options?: PluginProcessOptions) => PluginProcessHandle | Promise<PluginProcessHandle>;
|
|
@@ -147,6 +149,12 @@ export interface FakeAuthRegistrations {
|
|
|
147
149
|
}>;
|
|
148
150
|
/** Failures recorded through ctx.auth.loginRateLimit, by "<ip>|<key>". */
|
|
149
151
|
loginFailures: Map<string, number>;
|
|
152
|
+
/** Every ctx.auth.issueUserToken call, in order. */
|
|
153
|
+
userTokens: Array<{
|
|
154
|
+
userId: string;
|
|
155
|
+
purpose: string;
|
|
156
|
+
ttlSeconds?: number;
|
|
157
|
+
}>;
|
|
150
158
|
}
|
|
151
159
|
export interface FakePluginContext {
|
|
152
160
|
ctx: PluginContext;
|
|
@@ -363,6 +371,8 @@ export interface MockContextOptions {
|
|
|
363
371
|
linkedUsers?: Record<string, number>;
|
|
364
372
|
/** See FakeContextOptions. */
|
|
365
373
|
loginAttemptLimit?: number;
|
|
374
|
+
/** See FakeContextOptions. */
|
|
375
|
+
locales?: Record<string, string>;
|
|
366
376
|
}
|
|
367
377
|
export interface MockPluginContext extends FakePluginContext {
|
|
368
378
|
/** Capability ids checked during the test, in order, denied ones included. */
|
|
@@ -377,7 +387,8 @@ export interface MockPluginContext extends FakePluginContext {
|
|
|
377
387
|
* topic outside the plugin's own namespace, settings:read-core on
|
|
378
388
|
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
|
379
389
|
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
|
380
|
-
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins,
|
|
390
|
+
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
|
|
391
|
+
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
|
381
392
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
|
382
393
|
*/
|
|
383
394
|
export declare function createMockCtx(options?: MockContextOptions): MockPluginContext;
|
package/dist/testing.js
CHANGED
|
@@ -194,6 +194,7 @@ export function createFakeContext(options = {}) {
|
|
|
194
194
|
completedLogins: [],
|
|
195
195
|
revokedSessions: [],
|
|
196
196
|
loginFailures: new Map(),
|
|
197
|
+
userTokens: [],
|
|
197
198
|
};
|
|
198
199
|
const sshClient = options.sshClient ?? {};
|
|
199
200
|
let actor = options.actor;
|
|
@@ -673,6 +674,17 @@ export function createFakeContext(options = {}) {
|
|
|
673
674
|
},
|
|
674
675
|
},
|
|
675
676
|
countLinkedUsers: async (provider) => options.linkedUsers?.[provider] ?? 0,
|
|
677
|
+
issueUserToken: async (userId, tokenOptions) => {
|
|
678
|
+
auth.userTokens.push({ userId, ...tokenOptions });
|
|
679
|
+
const ttl = Math.min(tokenOptions.ttlSeconds ?? 300, 900);
|
|
680
|
+
return {
|
|
681
|
+
token: `fake-token-${auth.userTokens.length}`,
|
|
682
|
+
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
users: {
|
|
687
|
+
getLocale: async (userId) => options.locales?.[userId] ?? null,
|
|
676
688
|
},
|
|
677
689
|
desktop: {
|
|
678
690
|
openIsolatedWindow: async (request) => {
|
|
@@ -914,7 +926,8 @@ export function createFakeContext(options = {}) {
|
|
|
914
926
|
* topic outside the plugin's own namespace, settings:read-core on
|
|
915
927
|
* ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
|
|
916
928
|
* notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
|
|
917
|
-
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins,
|
|
929
|
+
* ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
|
|
930
|
+
* (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
|
|
918
931
|
* ungated. As the SDK grows a member, add its gate here in the same shape.
|
|
919
932
|
*/
|
|
920
933
|
export function createMockCtx(options = {}) {
|
|
@@ -948,6 +961,7 @@ export function createMockCtx(options = {}) {
|
|
|
948
961
|
baseUrl: options.baseUrl,
|
|
949
962
|
linkedUsers: options.linkedUsers,
|
|
950
963
|
loginAttemptLimit: options.loginAttemptLimit,
|
|
964
|
+
locales: options.locales,
|
|
951
965
|
manifest: {
|
|
952
966
|
capabilities: options.capabilities ?? [],
|
|
953
967
|
...options.manifest,
|
|
@@ -1218,6 +1232,16 @@ export function createMockCtx(options = {}) {
|
|
|
1218
1232
|
require("auth:provide");
|
|
1219
1233
|
return ctx.auth.countLinkedUsers(provider);
|
|
1220
1234
|
},
|
|
1235
|
+
issueUserToken: async (userId, tokenOptions) => {
|
|
1236
|
+
require("users:impersonate");
|
|
1237
|
+
return ctx.auth.issueUserToken(userId, tokenOptions);
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
users: {
|
|
1241
|
+
getLocale: async (userId) => {
|
|
1242
|
+
require("users:read");
|
|
1243
|
+
return ctx.users.getLocale(userId);
|
|
1244
|
+
},
|
|
1221
1245
|
},
|
|
1222
1246
|
desktop: {
|
|
1223
1247
|
openIsolatedWindow: async (request) => {
|
package/package.json
CHANGED