@termix-ssh/plugin-sdk 1.0.2 → 1.0.5

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +202 -0
  3. package/README.md +4 -2
  4. package/cli/commands/build.mjs +44 -19
  5. package/cli/commands/dev.mjs +1 -1
  6. package/cli/commands/migrations.mjs +2 -2
  7. package/cli/commands/openapi.mjs +39 -0
  8. package/cli/commands/validate.mjs +72 -0
  9. package/cli/index.mjs +3 -0
  10. package/cli/lib/css-classes.mjs +37 -0
  11. package/dist/changelog.d.ts +3 -1
  12. package/dist/changelog.js +12 -2
  13. package/dist/db.d.ts +1 -1
  14. package/dist/db.js +1 -1
  15. package/dist/ddl.js +4 -1
  16. package/dist/docs.d.ts +28 -0
  17. package/dist/docs.js +39 -0
  18. package/dist/frontend.d.ts +17 -0
  19. package/dist/frontend.js +8 -0
  20. package/dist/host/{chunk-XKMRKQFN.js → chunk-KOOUKYW3.js} +1 -1
  21. package/dist/host/{chunk-LKYAVM4T.js → chunk-OEOKIKXC.js} +18 -9
  22. package/dist/host/{chunk-SS43S6VI.js → chunk-VETPF5WI.js} +478 -431
  23. package/dist/host/core-classes.json +1 -0
  24. package/dist/host/{linked-server-DW5P3NLZ.js → linked-server-RWBK427T.js} +1 -1
  25. package/dist/host/{rbac-api-JKISXZEP.js → rbac-api-UJGGTQVA.js} +1 -1
  26. package/dist/host/testing-host.js +3 -3
  27. package/dist/host/types/src/types/credential-sidebar-preferences.d.ts +1 -1
  28. package/dist/host/types/src/ui/api/plugins-api.d.ts +6 -0
  29. package/dist/host/types/src/ui/components/docs-link.d.ts +25 -0
  30. package/dist/host/types/src/ui/components/panel-layout.d.ts +3 -1
  31. package/dist/host/types/src/ui/lib/docs.d.ts +10 -0
  32. package/dist/host/types/src/ui/lib/error-message.d.ts +1 -1
  33. package/dist/host/types/src/ui/main-axios.d.ts +1 -2
  34. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -0
  35. package/dist/host/ui.js +18 -8
  36. package/dist/host/{use-permissions-5CZEA3P4.js → use-permissions-P3DC7ZDR.js} +1 -1
  37. package/dist/host-commands.js +1 -1
  38. package/dist/manifest.d.ts +7 -1
  39. package/dist/manifest.js +48 -1
  40. package/dist/openapi.d.ts +43 -0
  41. package/dist/openapi.js +60 -0
  42. package/dist/testing/setup.js +1 -0
  43. package/package.json +22 -1
  44. package/schema/manifest.schema.json +21 -0
package/dist/ddl.js CHANGED
@@ -67,10 +67,13 @@ function columnType(dialect, column, keyed) {
67
67
  // text/json column, so in practice this only catches a primary key.
68
68
  if (keyed && dialect !== "sqlite")
69
69
  return `varchar(${KEY_LENGTH})`;
70
+ // MySQL TEXT stops at 64KB, so free text, json and secrets go long.
71
+ if (dialect === "mysql" && column.type !== "timestamp")
72
+ return "longtext";
70
73
  return "text";
71
74
  }
72
75
  }
73
- /** Column types that are TEXT on MySQL when nothing keys them. */
76
+ /** Column types that are TEXT or LONGTEXT on MySQL when nothing keys them. */
74
77
  const TEXT_ON_MYSQL = new Set([
75
78
  "text",
76
79
  "json",
package/dist/docs.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Docs links and env var entries from a manifest. Kept apart from manifest.ts
3
+ * so the browser can use them without pulling in semver.
4
+ */
5
+ /**
6
+ * A docs link, or null for anything that is not an https URL. Core only ever
7
+ * opens it in a new tab, but a javascript: or file: link should never get there.
8
+ */
9
+ export declare function pluginDocsUrl(value: unknown): string | null;
10
+ /** Joins a page and anchor onto a plugin's docs URL. */
11
+ export declare function pluginDocsPage(base: string, page?: string, anchor?: string): string;
12
+ export declare const ENV_NAME_PATTERN: RegExp;
13
+ export declare const MAX_ENV_DESCRIPTION_LENGTH = 300;
14
+ /**
15
+ * An environment variable a plugin reads. Only shown in docs and on the
16
+ * plugin's page, so people know what they can set.
17
+ */
18
+ export interface PluginEnvVar {
19
+ name: string;
20
+ description: string;
21
+ /** The value used when it is not set, as written in a compose file. */
22
+ default?: string;
23
+ required?: boolean;
24
+ /** Holds a password or key. */
25
+ secret?: boolean;
26
+ }
27
+ /** The usable env entries of a manifest. Bad ones are dropped. */
28
+ export declare function pluginEnvVars(value: unknown): PluginEnvVar[];
package/dist/docs.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Docs links and env var entries from a manifest. Kept apart from manifest.ts
3
+ * so the browser can use them without pulling in semver.
4
+ */
5
+ /**
6
+ * A docs link, or null for anything that is not an https URL. Core only ever
7
+ * opens it in a new tab, but a javascript: or file: link should never get there.
8
+ */
9
+ export function pluginDocsUrl(value) {
10
+ if (typeof value !== "string")
11
+ return null;
12
+ try {
13
+ const url = new URL(value.trim());
14
+ if (url.protocol !== "https:")
15
+ return null;
16
+ return url.toString().replace(/\/$/, "");
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** Joins a page and anchor onto a plugin's docs URL. */
23
+ export function pluginDocsPage(base, page, anchor) {
24
+ const clean = (page ?? "").replace(/^\/+|\/+$/g, "");
25
+ const url = clean ? `${base}/${clean}` : base;
26
+ return anchor ? `${url}#${anchor.replace(/^#/, "")}` : url;
27
+ }
28
+ export const ENV_NAME_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
29
+ export const MAX_ENV_DESCRIPTION_LENGTH = 300;
30
+ /** The usable env entries of a manifest. Bad ones are dropped. */
31
+ export function pluginEnvVars(value) {
32
+ if (!Array.isArray(value))
33
+ return [];
34
+ return value.filter((v) => typeof v === "object" &&
35
+ v !== null &&
36
+ typeof v.name === "string" &&
37
+ ENV_NAME_PATTERN.test(v.name) &&
38
+ typeof v.description === "string");
39
+ }
@@ -816,6 +816,15 @@ export interface DesktopApi {
816
816
  /** Called when the desktop app connects to or leaves a remote server. */
817
817
  onRemoteServerChange: (listener: () => void) => Disposer;
818
818
  }
819
+ /** Links into this plugin's docs, from the manifest's docs field. */
820
+ export interface DocsApi {
821
+ /** The docs link, or null when the manifest has none. */
822
+ readonly url: string | null;
823
+ /** A page of the docs ("setup"), with an optional anchor. */
824
+ page: (page?: string, anchor?: string) => string | null;
825
+ /** Opens a page in a new tab. Does nothing without a docs link. */
826
+ open: (page?: string, anchor?: string) => void;
827
+ }
819
828
  export interface TermixAppInfo {
820
829
  readonly pluginId: string;
821
830
  readonly manifest: PluginManifest;
@@ -913,6 +922,8 @@ export interface TermixApp extends TermixAppInfo {
913
922
  confirm: (options: ConfirmRequest) => Promise<boolean>;
914
923
  /** The desktop app, when the frontend runs in it. */
915
924
  desktop: DesktopApi;
925
+ /** This plugin's docs. Needs plugin API 1.2. */
926
+ docs: DocsApi;
916
927
  /**
917
928
  * Fires after this plugin's settings are saved from the settings screen or
918
929
  * the host editor. Disposed automatically.
@@ -966,6 +977,7 @@ export interface SshAuthTypeInfo {
966
977
  */
967
978
  export interface PluginHostBridge {
968
979
  usePluginId: () => string;
980
+ useDocsUrl: (pluginId: string, page?: string, anchor?: string) => string | null;
969
981
  useTranslation: (pluginId: string) => {
970
982
  t: TranslateFn;
971
983
  language: string;
@@ -1107,6 +1119,11 @@ export interface HostStatusInfo {
1107
1119
  export declare function __setPluginHost(bridge: PluginHostBridge | null): void;
1108
1120
  /** The id of the plugin that registered the component being rendered. */
1109
1121
  export declare function usePluginId(): string;
1122
+ /**
1123
+ * A page of this plugin's docs, or null when the manifest has no docs link.
1124
+ * Pass it to PanelShell's docs prop or use DocsLink. Needs plugin API 1.2.
1125
+ */
1126
+ export declare function useDocsUrl(page?: string, anchor?: string): string | null;
1110
1127
  /**
1111
1128
  * Translation bound to this plugin's namespace. Keys the plugin does not
1112
1129
  * define fall back to core's shared strings (common.*, hosts.*).
package/dist/frontend.js CHANGED
@@ -49,6 +49,14 @@ function requireHost() {
49
49
  export function usePluginId() {
50
50
  return requireHost().usePluginId();
51
51
  }
52
+ /**
53
+ * A page of this plugin's docs, or null when the manifest has no docs link.
54
+ * Pass it to PanelShell's docs prop or use DocsLink. Needs plugin API 1.2.
55
+ */
56
+ export function useDocsUrl(page, anchor) {
57
+ const bridge = requireHost();
58
+ return bridge.useDocsUrl(bridge.usePluginId(), page, anchor);
59
+ }
52
60
  /**
53
61
  * Translation bound to this plugin's namespace. Keys the plugin does not
54
62
  * define fall back to core's shared strings (common.*, hosts.*).
@@ -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-JKISXZEP.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
24
+ inFlight = import("./rbac-api-UJGGTQVA.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
25
25
  cache = {
26
26
  permissions: result.permissions ?? [],
27
27
  isAdmin: !!result.isAdmin
@@ -3801,7 +3801,7 @@ var {
3801
3801
 
3802
3802
  // src/ui/main-axios.ts
3803
3803
  import { toast } from "sonner";
3804
- import i18n from "i18next";
3804
+ import i18n2 from "i18next";
3805
3805
 
3806
3806
  // src/ui/lib/base-path.ts
3807
3807
  function getBasePath() {
@@ -4998,12 +4998,13 @@ async function logActivity(type, hostId, hostName) {
4998
4998
  }
4999
4999
 
5000
5000
  // src/ui/api/rbac-api.ts
5001
+ import i18n from "i18next";
5001
5002
  async function getSharingTarget(hostId, syncId) {
5002
5003
  const api = await getConnectedRemoteApi();
5003
5004
  if (!api || !syncId) return { api: rbacApi, hostId };
5004
5005
  const remoteHostId = await resolveRemoteHostId(syncId);
5005
5006
  if (remoteHostId === null) {
5006
- throw new Error("The synced host does not exist on the remote server");
5007
+ throw new Error(i18n.t("errors.syncedHostMissing"));
5007
5008
  }
5008
5009
  return { api, hostId: remoteHostId };
5009
5010
  }
@@ -5012,7 +5013,7 @@ async function getSharedCopyTarget(hostId, syncId) {
5012
5013
  const api = await getConnectedRemoteApi();
5013
5014
  const remoteHostId = api ? await resolveRemoteHostId(syncId) : null;
5014
5015
  if (!api || remoteHostId === null) {
5015
- throw new Error("The linked server is not reachable");
5016
+ throw new Error(i18n.t("errors.linkedServerUnreachable"));
5016
5017
  }
5017
5018
  return { api, hostId: remoteHostId };
5018
5019
  }
@@ -5037,7 +5038,7 @@ async function updateRole(roleId, roleData) {
5037
5038
  try {
5038
5039
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
5039
5040
  if (roleData.permissions) {
5040
- const { notifyPermissionsChanged } = await import("./use-permissions-5CZEA3P4.js");
5041
+ const { notifyPermissionsChanged } = await import("./use-permissions-P3DC7ZDR.js");
5041
5042
  notifyPermissionsChanged();
5042
5043
  }
5043
5044
  return response.data;
@@ -5266,6 +5267,7 @@ async function getUserPreferences() {
5266
5267
  }
5267
5268
 
5268
5269
  // src/ui/main-axios.ts
5270
+ var TRUST_TOKEN_KEY = "termixTrustToken";
5269
5271
  function getLoggerForService(serviceName) {
5270
5272
  if (serviceName.includes("SSH") || serviceName.includes("ssh")) {
5271
5273
  return sshLogger;
@@ -5292,7 +5294,6 @@ if (isElectron()) {
5292
5294
  if (!localStorage.getItem(key)) {
5293
5295
  electronSettingsCache.set(key, value);
5294
5296
  localStorage.setItem(key, value);
5295
- console.log(`[Electron] Loaded setting ${key} from main process`);
5296
5297
  } else {
5297
5298
  electronSettingsCache.set(key, localStorage.getItem(key));
5298
5299
  }
@@ -5318,7 +5319,6 @@ function setCookie(name, value, days = 7) {
5318
5319
  console.error(`[Electron] Failed to persist setting ${name}:`, err);
5319
5320
  });
5320
5321
  }
5321
- console.log(`[Electron] Set setting: ${name}`);
5322
5322
  } catch (error) {
5323
5323
  console.error(`[Electron] Failed to set setting: ${name}`, error);
5324
5324
  }
@@ -5340,7 +5340,6 @@ function getCookie(name) {
5340
5340
  if (token) {
5341
5341
  electronSettingsCache.set(name, token);
5342
5342
  }
5343
- console.log(`[Electron] Get setting: ${name} = ${token}`);
5344
5343
  return token;
5345
5344
  } catch (error) {
5346
5345
  console.error(`[Electron] Failed to get setting: ${name}`, error);
@@ -5427,6 +5426,15 @@ function createApiInstance(baseURL, serviceName = "API") {
5427
5426
  config.headers["Authorization"] = `Bearer ${jwt}`;
5428
5427
  }
5429
5428
  }
5429
+ const trustToken = localStorage.getItem(TRUST_TOKEN_KEY);
5430
+ const isLogin = config.url?.includes("/users/login") || /\/users\/auth\/[^/]+\/verify$/.test(config.url ?? "");
5431
+ if (trustToken && isLogin) {
5432
+ if (config.headers.set) {
5433
+ config.headers.set("X-Termix-Trust-Token", trustToken);
5434
+ } else {
5435
+ config.headers["X-Termix-Trust-Token"] = trustToken;
5436
+ }
5437
+ }
5430
5438
  }
5431
5439
  if (typeof window !== "undefined" && window.ReactNativeWebView) {
5432
5440
  let platform = "Unknown";
@@ -5538,7 +5546,7 @@ function createApiInstance(baseURL, serviceName = "API") {
5538
5546
  clearClientAuthState();
5539
5547
  if (typeof window !== "undefined") {
5540
5548
  console.warn("Session expired - please log in again");
5541
- toast.warning(i18n.t("errors.sessionExpiredLogin"));
5549
+ toast.warning(i18n2.t("errors.sessionExpiredLogin"));
5542
5550
  window.dispatchEvent(new Event("termix:logout"));
5543
5551
  }
5544
5552
  dbHealthMonitor.reportSessionExpired();
@@ -5587,7 +5595,7 @@ function createRemoteOriginApiInstance(path) {
5587
5595
  });
5588
5596
  instance.interceptors.request.use(
5589
5597
  async (config) => {
5590
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-DW5P3NLZ.js");
5598
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-RWBK427T.js");
5591
5599
  const linked = await getLinkedSession2();
5592
5600
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
5593
5601
  if (config.headers.set) {
@@ -6009,6 +6017,7 @@ export {
6009
6017
  patchOpenTab,
6010
6018
  parseCustomKeybindings,
6011
6019
  getUserPreferences,
6020
+ TRUST_TOKEN_KEY,
6012
6021
  setCookie,
6013
6022
  getCookie,
6014
6023
  markUserAuthenticated,