@termix-ssh/plugin-sdk 1.0.13 → 1.0.15

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 (31) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/PLUGIN-GUIDE.md +16 -1
  3. package/dist/backend.d.ts +130 -1
  4. package/dist/backend.js +7 -0
  5. package/dist/capabilities.js +2 -0
  6. package/dist/host/{PluginsTab-OXQQ7M4I.js → PluginsTab-S33WIIGC.js} +3 -3
  7. package/dist/host/{chunk-ERGN4YW6.js → chunk-2IV3VX7F.js} +1 -1
  8. package/dist/host/{chunk-CN5UBTBP.js → chunk-3L45NNT5.js} +31 -2
  9. package/dist/host/{chunk-T2PZWXFJ.js → chunk-57DJU2ZA.js} +1 -1
  10. package/dist/host/{chunk-CALNJCHN.js → chunk-V44FCJBQ.js} +25 -4
  11. package/dist/host/{chunk-54KCK6IZ.js → chunk-VFTB2RDP.js} +4 -2
  12. package/dist/host/{linked-server-5VFO4XQB.js → linked-server-SCJH7DSL.js} +1 -1
  13. package/dist/host/{rbac-api-5YMBJAO5.js → rbac-api-KA5IPQQL.js} +1 -1
  14. package/dist/host/testing-host.js +4 -4
  15. package/dist/host/types/src/types/index.d.ts +10 -0
  16. package/dist/host/types/src/types/ui-types.d.ts +3 -1
  17. package/dist/host/types/src/ui/api/auth-methods-api.d.ts +2 -0
  18. package/dist/host/types/src/ui/api/settings-api.d.ts +6 -0
  19. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +6 -0
  20. package/dist/host/types/src/ui/main-axios.d.ts +2 -2
  21. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +2 -2
  22. package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +4 -1
  23. package/dist/host/ui.js +27 -10
  24. package/dist/host/{use-permissions-ISEDRY3J.js → use-permissions-LLN6TLAL.js} +1 -1
  25. package/dist/manifest.d.ts +1 -1
  26. package/dist/manifest.js +1 -1
  27. package/dist/testing.d.ts +21 -1
  28. package/dist/testing.js +61 -1
  29. package/dist/vitest-preset.js +23 -0
  30. package/package.json +1 -1
  31. package/schema/manifest.schema.json +2 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.15
4
+
5
+ Plugin API 1.7.0.
6
+
7
+ ### Added
8
+
9
+ - `ctx.auth.policy.provide` and the `auth:policy` capability, so a plugin can set an idle timeout, cap session length, turn off remember me and require a second factor
10
+ - `ctx.auth.listSecondFactors` lists the second factors from every running plugin
11
+ - `AuthEnrollmentSections` takes `factorsOnly`
12
+ - `getHttpsSettings` and `updateHttpsSettings` in `@termix-ssh/plugin-sdk/ui`
13
+
14
+ ### Fixed
15
+
16
+ - Plugin frontend tests against a locally linked SDK failed with a second copy of React
17
+
18
+ ## 1.0.14
19
+
20
+ Plugin API 1.6.0.
21
+
22
+ ### Added
23
+
24
+ - `ctx.hosts.locks.provide` and the `hosts:lock` capability, so a plugin can manage hosts that stay read only for everyone else and block adding hosts by hand
25
+ - `ctx.hosts.setShares` to replace every grant on a host in one call
26
+ - `ctx.credentials.list` returns the acting user's credentials by name, without secrets
27
+ - `ctx.hosts.create` and `update` take `password`, `key`, `keyPassword`, `keyType`, `sudoPassword`, `protocolAuth` and `pluginEnable`
28
+
3
29
  ## 1.0.13
4
30
 
5
31
  Plugin API 1.5.0.
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.13 | Plugin API 1.5.0
3
+ SDK 1.0.15 | Plugin API 1.7.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.
@@ -226,6 +226,7 @@ Bundled as ESM for Node 22+ into `dist/backend.js`. Backend imports of your own
226
226
  | `sync` | none | Sync your tables to a linked desktop |
227
227
  | `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
228
228
  | `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
229
+ | `auth.policy` | `auth:policy` | Login rules: idle timeout, session length, second factors |
229
230
  | `users.getLocale` | `users:read` | A user's UI language |
230
231
  | `system` | `system:tls` | The server's HTTPS certificate |
231
232
  | `plugins.list()` | `plugins:read` | Which plugins are installed |
@@ -300,6 +301,11 @@ router.post("/notes", async (req, res) => {
300
301
  - `ctx.rbac.require("manage")` on a single route, `await ctx.rbac.has("manage")` in code. A full id like
301
302
  `hosts.edit` checks a core permission.
302
303
  - Host access: `ctx.hosts.checkAccess(hostId, "connect" | "view" | "edit" | "manage")`.
304
+ - Managed hosts: with `hosts:lock`, `ctx.hosts.locks.provide({ isLocked, note, allowManualCreate })` makes
305
+ the hosts you manage read only for the UI, the API and other plugins (pins and order still work), and
306
+ `allowManualCreate: () => false` blocks adding hosts by hand. Your own `ctx.hosts` writes still go
307
+ through. `ctx.hosts.setShares(hostId, grants)` replaces a host's whole grant list, and
308
+ `ctx.credentials.list()` finds the acting user's credentials by name.
303
309
 
304
310
  ### Errors
305
311
 
@@ -694,6 +700,13 @@ closely. Declare ids in `contributes.auth` and register them with `ctx.auth.regi
694
700
  `registerSecondFactor` or `registerSshAuthProvider`. Connection protocols go in `contributes.protocols`
695
701
  and `app.registerHostProtocol`. Read https://docs.termix.site/develop/auth before starting one.
696
702
 
703
+ Login rules need `auth:policy`. `ctx.auth.policy.provide({ evaluate, allowRememberDevice })` is asked at
704
+ every login with the user, the method, the device and every second factor (`ctx.auth.listSecondFactors()`
705
+ lists them too). `evaluate` returns `allowRememberDevice`, `maxSessionSeconds`, `idleTimeoutSeconds` and
706
+ `secondFactor` (`"always"` or `"new-device"`). The strictest answer from all plugins wins, and a throw
707
+ refuses the login. A user who must have a second factor but has none gets a session that can only set one
708
+ up, and `/users/me` reports `restriction: "second-factor-setup"`.
709
+
697
710
  ## Testing
698
711
 
699
712
  ```ts
@@ -890,6 +903,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
890
903
  | `users:impersonate` | high | Act as another user, or get a session for them, audited |
891
904
  | `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
892
905
  | `auth:provide` | high | Add login methods, second factors, SSH auth types |
906
+ | `auth:policy` | high | Set login rules: idle timeout, session length, 2FA |
893
907
  | `device:serial` | high | Open serial and USB ports on the server |
894
908
  | `desktop:local` | high | Use the user's own computer, desktop app only |
895
909
  | `hosts:export` | high | Export hosts with their saved logins |
@@ -898,6 +912,7 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
898
912
  | `hosts:write` | medium | Add and edit hosts |
899
913
  | `hosts:status-provide` | medium | Be the source of host status dots |
900
914
  | `hosts:activity-provide` | medium | Be the source of the recent activity list |
915
+ | `hosts:lock` | medium | Make hosts it manages read only for everyone else |
901
916
  | `network:probe` | medium | Check that a host answers on a TCP port |
902
917
  | `credentials:use` | medium | Connect with saved credentials without seeing them |
903
918
  | `credentials:write` | medium | Save new credentials |
package/dist/backend.d.ts CHANGED
@@ -591,11 +591,40 @@ export interface PluginHostCreateInput {
591
591
  sshOptions?: HostSshOptions | null;
592
592
  /** Host settings by plugin id, the shape a Termix export carries. */
593
593
  pluginSettings?: Record<string, Record<string, unknown>>;
594
+ /**
595
+ * Each plugin's own host switch (its manifest enableKey), by plugin id.
596
+ * A plugin that is not running is skipped and logged; one with no switch
597
+ * is refused.
598
+ */
599
+ pluginEnable?: Record<string, boolean>;
600
+ /**
601
+ * Secrets, stored encrypted with the owner's data key and never returned
602
+ * in a PluginHostRecord. An empty string or null clears the stored value.
603
+ */
604
+ password?: string | null;
605
+ key?: string | null;
606
+ keyPassword?: string | null;
607
+ keyType?: string | null;
608
+ sudoPassword?: string | null;
609
+ /**
610
+ * Logins for plugin protocols (contributes.protocols), keyed by protocol
611
+ * id. null removes that protocol's login. Unknown protocol ids are skipped.
612
+ */
613
+ protocolAuth?: Record<string, PluginHostProtocolAuthInput | null>;
614
+ }
615
+ /** One protocol login on a host write. Absent keys keep what is stored. */
616
+ export interface PluginHostProtocolAuthInput {
617
+ authType?: "direct" | "credential" | "none";
618
+ credentialId?: number | null;
619
+ username?: string | null;
620
+ password?: string | null;
621
+ /** The protocol's declared credentialFields, secret ones included. */
622
+ fields?: Record<string, string>;
594
623
  }
595
624
  /** Fields a plugin may change on a host the acting user owns. */
596
625
  export type PluginHostUpdateInput = Partial<PluginHostCreateInput>;
597
626
  /** Every key PluginHostCreateInput and PluginHostUpdateInput accept. */
598
- export declare const PLUGIN_HOST_INPUT_KEYS: readonly ["name", "ip", "port", "username", "authType", "credentialId", "overrideCredentialUsername", "connectionType", "tags", "folder", "pin", "notes", "jumpHosts", "enableSsh", "sshPort", "statusCheckEnabled", "statusCheckInterval", "forceKeyboardInteractive", "sshOptions", "pluginSettings"];
627
+ export declare const PLUGIN_HOST_INPUT_KEYS: readonly ["name", "ip", "port", "username", "authType", "credentialId", "overrideCredentialUsername", "connectionType", "tags", "folder", "pin", "notes", "jumpHosts", "enableSsh", "sshPort", "statusCheckEnabled", "statusCheckInterval", "forceKeyboardInteractive", "sshOptions", "pluginSettings", "pluginEnable", "password", "key", "keyPassword", "keyType", "sudoPassword", "protocolAuth"];
599
628
  /** The same shape canAccessHost returns internally, without secrets. */
600
629
  export interface PluginHostAccess {
601
630
  hasAccess: boolean;
@@ -624,6 +653,31 @@ export interface PluginShareableRole {
624
653
  name: string;
625
654
  displayName: string | null;
626
655
  }
656
+ /** One grant in ctx.hosts.setShares. */
657
+ export interface PluginHostShareGrant {
658
+ target: PluginShareTarget;
659
+ permissionLevel: PluginHostShareLevel;
660
+ /** Omit for a grant that never expires. */
661
+ durationHours?: number;
662
+ }
663
+ /**
664
+ * Hosts a plugin manages itself, for example from a config file. Core refuses
665
+ * edits, deletes and share changes on a locked host from the UI, the API and
666
+ * other plugins, and marks it in host reads so a layout can show it read only.
667
+ * Pins and order stay with the user. Only the plugin that locks a host may
668
+ * still write it.
669
+ */
670
+ export interface PluginHostLockProvider {
671
+ isLocked: (hostId: number) => boolean;
672
+ /** A short line shown under the managed note, or null. */
673
+ note: (hostId: number) => string | null;
674
+ /** False blocks adding hosts by hand (editor, import, other plugins). */
675
+ allowManualCreate: () => boolean;
676
+ }
677
+ export interface PluginHostLocksApi {
678
+ /** Registers this plugin's locks. Needs hosts:lock. Removed on deactivate. */
679
+ provide: (provider: PluginHostLockProvider) => () => void;
680
+ }
627
681
  /**
628
682
  * Hosts the acting user can see, and the RBAC + sharing operations a plugin
629
683
  * needs to build a feature (like fleets) on top of hosts it does not own.
@@ -675,6 +729,12 @@ export interface PluginHosts {
675
729
  * Refuses a host the caller does not hold "manage" on.
676
730
  */
677
731
  share: (hostId: number, targets: PluginShareTarget[], permissionLevel: PluginHostShareLevel, durationHours?: number) => Promise<PluginHostShareResult>;
732
+ /**
733
+ * Replaces every grant on a host the acting user owns with this list.
734
+ * Grants that stay keep their row and counters, missing ones are revoked.
735
+ * Needs hosts:write.
736
+ */
737
+ setShares: (hostId: number, grants: PluginHostShareGrant[]) => Promise<PluginHostShareResult>;
678
738
  /** Users the caller may pick as a share target, for building a picker UI. */
679
739
  listUsers: () => Promise<PluginShareableUser[]>;
680
740
  /** Non-system roles the caller may pick as a share target. */
@@ -736,6 +796,8 @@ export interface PluginHosts {
736
796
  registerImportNormalizer: (normalizer: PluginHostImportNormalizer) => () => void;
737
797
  /** The recent activity list on the dashboard. */
738
798
  activity: PluginHostActivityApi;
799
+ /** Read only hosts this plugin manages. */
800
+ locks: PluginHostLocksApi;
739
801
  }
740
802
  export interface PluginHostImportOptions {
741
803
  /** Update a host that matches on ip, port and username instead of skipping it. */
@@ -1416,6 +1478,52 @@ export interface PluginSecondFactor {
1416
1478
  /** Removes the user's enrolment. Called by the admin reset. */
1417
1479
  reset?: (userId: string) => Promise<void>;
1418
1480
  }
1481
+ /** A login core is about to turn into a session. */
1482
+ export interface PluginAuthPolicyInput {
1483
+ userId: string;
1484
+ /** Login method id, `"proxy"` for a trusted proxy login. */
1485
+ methodId: string;
1486
+ /** The method signs in through an outside identity provider (SSO, LDAP). */
1487
+ external: boolean;
1488
+ deviceType: "web" | "desktop" | "mobile";
1489
+ /** The device passed a second factor before and was remembered. */
1490
+ trustedDevice: boolean;
1491
+ /** The method itself proved a second factor, like a verified passkey. */
1492
+ mfaSatisfied: boolean;
1493
+ /** Every second factor core has, and whether this user set it up. */
1494
+ secondFactors: {
1495
+ id: string;
1496
+ pluginId: string;
1497
+ enrolled: boolean;
1498
+ }[];
1499
+ }
1500
+ export interface PluginAuthPolicyDecision {
1501
+ /** False turns remember me off and ignores remembered devices. */
1502
+ allowRememberDevice?: boolean;
1503
+ /** Caps how long the session lasts. */
1504
+ maxSessionSeconds?: number;
1505
+ /** Ends the session after this many seconds without a request. */
1506
+ idleTimeoutSeconds?: number;
1507
+ /**
1508
+ * Requires a second factor even for users who have none: they get a
1509
+ * session that can only set one up. `"new-device"` skips it on a
1510
+ * remembered device.
1511
+ */
1512
+ secondFactor?: "always" | "new-device";
1513
+ }
1514
+ /**
1515
+ * Login rules from a plugin. Core asks every provider and the strictest
1516
+ * answer wins. A provider that throws refuses the login.
1517
+ */
1518
+ export interface PluginAuthPolicyProvider {
1519
+ evaluate: (input: PluginAuthPolicyInput) => PluginAuthPolicyDecision | Promise<PluginAuthPolicyDecision>;
1520
+ /** Whether the login page should offer remember me. */
1521
+ allowRememberDevice?: () => boolean;
1522
+ }
1523
+ export interface PluginAuthPolicyApi {
1524
+ /** Needs auth:policy. Removed on deactivate. */
1525
+ provide: (provider: PluginAuthPolicyProvider) => () => void;
1526
+ }
1419
1527
  export interface PluginUserTokenOptions {
1420
1528
  /** Why the token exists, kept in the audit log and on the session. */
1421
1529
  purpose: string;
@@ -1494,6 +1602,13 @@ export interface PluginAuth {
1494
1602
  * never outlive itself.
1495
1603
  */
1496
1604
  issueUserToken: (userId: string, options: PluginUserTokenOptions) => Promise<PluginUserToken>;
1605
+ /** Login rules: idle timeout, session length, remember me, second factors. */
1606
+ policy: PluginAuthPolicyApi;
1607
+ /** Second factors from every running plugin. Needs no capability. */
1608
+ listSecondFactors: () => {
1609
+ id: string;
1610
+ pluginId: string;
1611
+ }[];
1497
1612
  }
1498
1613
  /** What a plugin may learn about a user. Needs users:read. */
1499
1614
  export interface PluginUsers {
@@ -1727,6 +1842,14 @@ export interface PluginSshKeyCredential {
1727
1842
  /** "<type> <base64>", derived from the private key when none was stored. Null when it cannot be. */
1728
1843
  publicKey: string | null;
1729
1844
  }
1845
+ /** A saved credential without its secrets. */
1846
+ export interface PluginCredentialSummary {
1847
+ id: number;
1848
+ name: string;
1849
+ username: string | null;
1850
+ authType: string;
1851
+ folder: string | null;
1852
+ }
1730
1853
  /** A key pair to save as a new credential for the acting user. */
1731
1854
  export interface PluginSshKeyCredentialInput {
1732
1855
  name: string;
@@ -1747,6 +1870,12 @@ export interface PluginCredentials {
1747
1870
  * Needs credentials:use and the core credentials.view permission. Audited.
1748
1871
  */
1749
1872
  listSshKeys: () => Promise<PluginSshKeyCredential[]>;
1873
+ /**
1874
+ * The acting user's own credentials by name, with no secret material, so a
1875
+ * plugin can point a host at one. Needs credentials:use and the core
1876
+ * credentials.view permission. Audited.
1877
+ */
1878
+ list: () => Promise<PluginCredentialSummary[]>;
1750
1879
  /**
1751
1880
  * Saves a key pair as a new encrypted credential owned by the acting user
1752
1881
  * and returns its id. Needs credentials:write, the core credentials.create
package/dist/backend.js CHANGED
@@ -65,6 +65,13 @@ export const PLUGIN_HOST_INPUT_KEYS = [
65
65
  "forceKeyboardInteractive",
66
66
  "sshOptions",
67
67
  "pluginSettings",
68
+ "pluginEnable",
69
+ "password",
70
+ "key",
71
+ "keyPassword",
72
+ "keyType",
73
+ "sudoPassword",
74
+ "protocolAuth",
68
75
  ];
69
76
  /** Thrown by ctx.ssh.startInteraction when no provider can start it. */
70
77
  export class PluginSshInteractionError extends Error {
@@ -33,6 +33,7 @@ export const CAPABILITY_CATALOG = [
33
33
  entry("users:impersonate", "high"),
34
34
  entry("db:core-refs", "high"),
35
35
  entry("auth:provide", "high"),
36
+ entry("auth:policy", "high"),
36
37
  entry("device:serial", "high"),
37
38
  entry("notify:hub", "high"),
38
39
  entry("ui:layout", "high"),
@@ -46,6 +47,7 @@ export const CAPABILITY_CATALOG = [
46
47
  entry("network:probe", "medium"),
47
48
  entry("hosts:status-provide", "medium"),
48
49
  entry("hosts:activity-provide", "medium"),
50
+ entry("hosts:lock", "medium"),
49
51
  entry("users:read", "medium"),
50
52
  entry("events:core", "medium"),
51
53
  entry("notify:send", "medium"),
@@ -29,7 +29,7 @@ import {
29
29
  sortPlugins,
30
30
  uploadEntry,
31
31
  youtubeEmbedUrl
32
- } from "./chunk-54KCK6IZ.js";
32
+ } from "./chunk-VFTB2RDP.js";
33
33
  import {
34
34
  ArrowDownWideNarrow,
35
35
  BookOpen,
@@ -107,8 +107,8 @@ import {
107
107
  updatePlugin,
108
108
  uploadPlugin,
109
109
  useConfirm
110
- } from "./chunk-ERGN4YW6.js";
111
- import "./chunk-CN5UBTBP.js";
110
+ } from "./chunk-2IV3VX7F.js";
111
+ import "./chunk-3L45NNT5.js";
112
112
  import {
113
113
  __toESM,
114
114
  define_import_meta_env_default,
@@ -4,7 +4,7 @@ import {
4
4
  getPluginRecord,
5
5
  rbacApi,
6
6
  usePluginStore
7
- } from "./chunk-CN5UBTBP.js";
7
+ } from "./chunk-3L45NNT5.js";
8
8
  import {
9
9
  __export,
10
10
  init_define_import_meta_env
@@ -4932,6 +4932,14 @@ async function bulkUpdateSSHHosts(hostIds, updates) {
4932
4932
  handleApiError(error, "bulk update SSH hosts");
4933
4933
  }
4934
4934
  }
4935
+ async function getHostPolicy() {
4936
+ try {
4937
+ const response = await sshHostApi.get("/policy");
4938
+ return response.data;
4939
+ } catch (error) {
4940
+ handleApiError(error, "load host policy");
4941
+ }
4942
+ }
4935
4943
  async function reorderSSHHosts(positions) {
4936
4944
  try {
4937
4945
  const response = await sshHostApi.put("/reorder", { positions });
@@ -5588,6 +5596,24 @@ async function updateSessionTimeout(timeoutHours) {
5588
5596
  handleApiError(error, "update session timeout");
5589
5597
  }
5590
5598
  }
5599
+ async function getHttpsSettings() {
5600
+ try {
5601
+ const response = await authApi.get("/users/https-settings");
5602
+ return response.data;
5603
+ } catch (error) {
5604
+ handleApiError(error, "fetch HTTPS settings");
5605
+ }
5606
+ }
5607
+ async function updateHttpsSettings(forceHttps) {
5608
+ try {
5609
+ const response = await authApi.patch("/users/https-settings", {
5610
+ forceHttps
5611
+ });
5612
+ return response.data;
5613
+ } catch (error) {
5614
+ handleApiError(error, "update HTTPS settings");
5615
+ }
5616
+ }
5591
5617
  async function getBranding() {
5592
5618
  try {
5593
5619
  const response = await authApi.get("/users/branding");
@@ -6290,7 +6316,7 @@ async function updateRole(roleId, roleData) {
6290
6316
  try {
6291
6317
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
6292
6318
  if (roleData.permissions) {
6293
- const { notifyPermissionsChanged } = await import("./use-permissions-ISEDRY3J.js");
6319
+ const { notifyPermissionsChanged } = await import("./use-permissions-LLN6TLAL.js");
6294
6320
  notifyPermissionsChanged();
6295
6321
  }
6296
6322
  return response.data;
@@ -6872,7 +6898,7 @@ function createRemoteOriginApiInstance(path) {
6872
6898
  });
6873
6899
  instance.interceptors.request.use(
6874
6900
  async (config) => {
6875
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-5VFO4XQB.js");
6901
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-SCJH7DSL.js");
6876
6902
  const linked = await getLinkedSession2();
6877
6903
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
6878
6904
  if (config.headers.set) {
@@ -7300,6 +7326,7 @@ export {
7300
7326
  createSSHHost,
7301
7327
  updateSSHHost,
7302
7328
  bulkUpdateSSHHosts,
7329
+ getHostPolicy,
7303
7330
  reorderSSHHosts,
7304
7331
  deleteSSHHost,
7305
7332
  websocketAuthProtocols,
@@ -7328,6 +7355,8 @@ export {
7328
7355
  updateLogLevel,
7329
7356
  getSessionTimeout,
7330
7357
  updateSessionTimeout,
7358
+ getHttpsSettings,
7359
+ updateHttpsSettings,
7331
7360
  getBranding,
7332
7361
  hydrateLocalSharedHostAuth,
7333
7362
  getConnectedRemoteApi,
@@ -26,7 +26,7 @@ var inFlight = null;
26
26
  function load() {
27
27
  if (cache) return Promise.resolve(cache);
28
28
  if (inFlight) return inFlight;
29
- inFlight = import("./rbac-api-5YMBJAO5.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
29
+ inFlight = import("./rbac-api-KA5IPQQL.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
30
30
  cache = {
31
31
  permissions: result.permissions ?? [],
32
32
  isAdmin: !!result.isAdmin
@@ -3,7 +3,7 @@ import {
3
3
  hasPermission,
4
4
  resetPermissionsCache,
5
5
  usePermissions
6
- } from "./chunk-T2PZWXFJ.js";
6
+ } from "./chunk-57DJU2ZA.js";
7
7
  import {
8
8
  Button,
9
9
  Check,
@@ -80,7 +80,7 @@ import {
80
80
  withIconBoundary,
81
81
  withPluginContext,
82
82
  withPluginScope
83
- } from "./chunk-ERGN4YW6.js";
83
+ } from "./chunk-2IV3VX7F.js";
84
84
  import {
85
85
  SYNC_CHANGED_EVENT,
86
86
  TRUST_TOKEN_KEY,
@@ -130,7 +130,7 @@ import {
130
130
  usePluginStore,
131
131
  useTabTypes,
132
132
  websocketAuthProtocols
133
- } from "./chunk-CN5UBTBP.js";
133
+ } from "./chunk-3L45NNT5.js";
134
134
  import {
135
135
  define_import_meta_env_default,
136
136
  init_define_import_meta_env
@@ -268,6 +268,14 @@ async function getLoginMethods() {
268
268
  return [];
269
269
  }
270
270
  }
271
+ async function getRememberDeviceAllowed() {
272
+ try {
273
+ const response = await authApi.get("/users/auth/methods");
274
+ return response.data?.rememberDevice !== false;
275
+ } catch {
276
+ return true;
277
+ }
278
+ }
271
279
  async function submitLoginMethod(methodId, body, instanceId) {
272
280
  try {
273
281
  const response = await authApi.post(
@@ -1447,6 +1455,9 @@ var en_default = {
1447
1455
  deleteFolderConfirm_one: 'Delete "{{name}}" and its {{count}} host? This cannot be undone.',
1448
1456
  deleteFolderConfirm_other: 'Delete "{{name}}" and its {{count}} hosts? This cannot be undone.',
1449
1457
  failedToMoveHosts: "Failed to move hosts",
1458
+ managedBanner: "This host is managed by {{plugin}}. Change it there. You can still pin and reorder it.",
1459
+ managedTooltip: "Managed by {{plugin}}",
1460
+ addBlocked: "Hosts are managed by {{plugin}}, so new hosts can't be added here.",
1450
1461
  failedToReorder: "Failed to reorder",
1451
1462
  expandAll: "Expand all folders",
1452
1463
  collapseAll: "Collapse all folders",
@@ -2043,6 +2054,10 @@ var en_default = {
2043
2054
  title: "Handle sign in",
2044
2055
  consequence: "It can add a login method, a second factor or an SSH sign-in type."
2045
2056
  },
2057
+ "auth:policy": {
2058
+ title: "Set sign in rules",
2059
+ consequence: "It can sign people out when idle, limit session length, turn off remember me and require a second factor."
2060
+ },
2046
2061
  "system:tls": {
2047
2062
  title: "Manage certificates",
2048
2063
  consequence: "It can replace the certificate Termix serves over HTTPS and answer domain checks from certificate authorities."
@@ -2111,6 +2126,10 @@ var en_default = {
2111
2126
  title: "Store recent activity",
2112
2127
  consequence: "Becomes the store for the recent activity list of every user."
2113
2128
  },
2129
+ "hosts:lock": {
2130
+ title: "Manage hosts as read only",
2131
+ consequence: "Hosts it manages can't be edited, deleted or reshared by anyone else."
2132
+ },
2114
2133
  "desktop:window": {
2115
2134
  title: "Open desktop windows",
2116
2135
  consequence: "Only in the Termix desktop app."
@@ -4103,6 +4122,7 @@ function sshHostToHost(h) {
4103
4122
  connectionOrigin: h.connectionOrigin ?? null,
4104
4123
  localOnly: !!h.localOnly,
4105
4124
  sharedCopy: !!h.sharedCopy,
4125
+ ...h.lock ? { lock: h.lock } : {},
4106
4126
  enableSsh: h.enableSsh != null ? h.enableSsh : isSshHost,
4107
4127
  sshPort: h.sshPort ?? (h.connectionType === "ssh" || !h.connectionType ? h.port : 22),
4108
4128
  protocolAuth: h.protocolAuth ?? {},
@@ -10040,7 +10060,7 @@ function hostToSSHHost(h) {
10040
10060
  // src/ui/shell/tabUtils.tsx
10041
10061
  import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
10042
10062
  var PluginsTab = lazy(
10043
- () => import("./PluginsTab-OXQQ7M4I.js").then((m) => ({ default: m.PluginsTab }))
10063
+ () => import("./PluginsTab-S33WIIGC.js").then((m) => ({ default: m.PluginsTab }))
10044
10064
  );
10045
10065
  function preloadTabSurface(type) {
10046
10066
  const loader = getTabType(type)?.preload;
@@ -10225,6 +10245,7 @@ export {
10225
10245
  remScale,
10226
10246
  useIsMobile,
10227
10247
  getLoginMethods,
10248
+ getRememberDeviceAllowed,
10228
10249
  submitLoginMethod,
10229
10250
  startLoginRedirect,
10230
10251
  verifySecondFactor,
@@ -7,10 +7,10 @@ import {
7
7
  Trash,
8
8
  cn,
9
9
  useAreaPreferences
10
- } from "./chunk-ERGN4YW6.js";
10
+ } from "./chunk-2IV3VX7F.js";
11
11
  import {
12
12
  isElectron
13
- } from "./chunk-CN5UBTBP.js";
13
+ } from "./chunk-3L45NNT5.js";
14
14
  import {
15
15
  __commonJS,
16
16
  __toESM,
@@ -2382,6 +2382,7 @@ var CAPABILITY_CATALOG = [
2382
2382
  entry("users:impersonate", "high"),
2383
2383
  entry("db:core-refs", "high"),
2384
2384
  entry("auth:provide", "high"),
2385
+ entry("auth:policy", "high"),
2385
2386
  entry("device:serial", "high"),
2386
2387
  entry("notify:hub", "high"),
2387
2388
  entry("ui:layout", "high"),
@@ -2395,6 +2396,7 @@ var CAPABILITY_CATALOG = [
2395
2396
  entry("network:probe", "medium"),
2396
2397
  entry("hosts:status-provide", "medium"),
2397
2398
  entry("hosts:activity-provide", "medium"),
2399
+ entry("hosts:lock", "medium"),
2398
2400
  entry("users:read", "medium"),
2399
2401
  entry("events:core", "medium"),
2400
2402
  entry("notify:send", "medium"),
@@ -3,7 +3,7 @@ import {
3
3
  SYNC_CHANGED_EVENT,
4
4
  getLinkedSession,
5
5
  notifySyncChanged
6
- } from "./chunk-CN5UBTBP.js";
6
+ } from "./chunk-3L45NNT5.js";
7
7
  import "./chunk-YGYBM7SQ.js";
8
8
  export {
9
9
  SYNC_CHANGED_EVENT,
@@ -23,7 +23,7 @@ import {
23
23
  shareHost,
24
24
  updateHostAccess,
25
25
  updateRole
26
- } from "./chunk-CN5UBTBP.js";
26
+ } from "./chunk-3L45NNT5.js";
27
27
  import "./chunk-YGYBM7SQ.js";
28
28
  export {
29
29
  assignRoleToUser,
@@ -59,10 +59,10 @@ import {
59
59
  trackTabHandle,
60
60
  useShellController,
61
61
  useTabStageStore
62
- } from "./chunk-CALNJCHN.js";
62
+ } from "./chunk-V44FCJBQ.js";
63
63
  import {
64
64
  setPermissionsForTesting
65
- } from "./chunk-T2PZWXFJ.js";
65
+ } from "./chunk-57DJU2ZA.js";
66
66
  import {
67
67
  SurfaceScope,
68
68
  notifyShellReady,
@@ -70,14 +70,14 @@ import {
70
70
  setShellCallbacks,
71
71
  setShellHosts,
72
72
  setShellLayoutProvider
73
- } from "./chunk-ERGN4YW6.js";
73
+ } from "./chunk-2IV3VX7F.js";
74
74
  import {
75
75
  getTabType,
76
76
  listTabTypes,
77
77
  resetPluginStore,
78
78
  setFrontendState,
79
79
  setPluginSummaries
80
- } from "./chunk-CN5UBTBP.js";
80
+ } from "./chunk-3L45NNT5.js";
81
81
  import {
82
82
  init_define_import_meta_env
83
83
  } from "./chunk-YGYBM7SQ.js";
@@ -23,6 +23,12 @@ export interface HostProtocolAuthInput {
23
23
  export interface JumpHost {
24
24
  hostId: number;
25
25
  }
26
+ /** The plugin that manages a host, from ctx.hosts.locks. */
27
+ export interface HostLock {
28
+ pluginId: string;
29
+ pluginName: string;
30
+ note: string | null;
31
+ }
26
32
  export type Host = {
27
33
  id: number;
28
34
  name: string;
@@ -91,6 +97,8 @@ export type Host = {
91
97
  ownerUsername?: string;
92
98
  /** A read-only copy of a host shared with the linked account. */
93
99
  sharedCopy?: boolean;
100
+ /** Set when a plugin manages this host: read only, pins and order still work. */
101
+ lock?: HostLock;
94
102
  /** Desktop only: kept on this device, never synced to the server. */
95
103
  localOnly?: boolean;
96
104
  /** Enabled plugins' host-scope settings, keyed by plugin id. Secrets redacted. */
@@ -261,6 +269,8 @@ export interface AuthenticatedRequest extends Request {
261
269
  sessionId?: string;
262
270
  apiKeyId?: string;
263
271
  actingAdminUserId?: string;
272
+ /** Set while the session may only set up a second factor. */
273
+ sessionRestriction?: string | null;
264
274
  user?: {
265
275
  id: string;
266
276
  username: string;
@@ -1,4 +1,4 @@
1
- import type { HostProtocolAuthSummary, HostSshOptions, HostTerminalConfig, SSHAuthType } from "./index.js";
1
+ import type { HostLock, HostProtocolAuthSummary, HostSshOptions, HostTerminalConfig, SSHAuthType } from "./index.js";
2
2
  import type { HostAuthOverrides } from "./auth-protocols.js";
3
3
  import type { DefaultOverrides } from "./host-defaults.js";
4
4
  import type { QuickConnectLogin } from "@termix-ssh/plugin-sdk/frontend";
@@ -93,6 +93,8 @@ export type Host = {
93
93
  * linked to. It arrives through sync and is managed on the server.
94
94
  */
95
95
  sharedCopy?: boolean;
96
+ /** Set when a plugin manages this host: read only, pins and order still work. */
97
+ lock?: HostLock;
96
98
  /** Desktop only: kept on this device, never synced to the server. */
97
99
  localOnly?: boolean;
98
100
  };
@@ -23,6 +23,8 @@ export type LoginResponse = AuthResponse & {
23
23
  };
24
24
  /** Enabled login methods for the login screen. Public. */
25
25
  export declare function getLoginMethods(): Promise<PublicLoginMethod[]>;
26
+ /** False when the login policy turned remember me off. Public. */
27
+ export declare function getRememberDeviceAllowed(): Promise<boolean>;
26
28
  export declare function submitLoginMethod(methodId: string, body: Record<string, unknown>, instanceId?: string): Promise<LoginResponse>;
27
29
  export declare function startLoginRedirect(methodId: string, params: {
28
30
  instanceId?: string;
@@ -6,6 +6,12 @@ export declare function getSessionTimeout(): Promise<{
6
6
  timeoutHours: number;
7
7
  }>;
8
8
  export declare function updateSessionTimeout(timeoutHours: number): Promise<void>;
9
+ export interface HttpsSettings {
10
+ forceHttps: boolean;
11
+ tlsEnabled: boolean;
12
+ }
13
+ export declare function getHttpsSettings(): Promise<HttpsSettings>;
14
+ export declare function updateHttpsSettings(forceHttps: boolean): Promise<HttpsSettings>;
9
15
  export interface BrandingSettings {
10
16
  appName: string;
11
17
  tagline: string;
@@ -9,6 +9,12 @@ export declare function bulkUpdateSSHHosts(hostIds: number[], updates: Record<st
9
9
  failed: number;
10
10
  errors: string[];
11
11
  }>;
12
+ export interface HostPolicy {
13
+ manualCreate: boolean;
14
+ /** The plugin that manages hosts, when hosts can't be added by hand. */
15
+ blockedBy?: string;
16
+ }
17
+ export declare function getHostPolicy(): Promise<HostPolicy>;
12
18
  export declare function reorderSSHHosts(positions: {
13
19
  id: number;
14
20
  sortOrder: number;
@@ -103,12 +103,12 @@ export declare const appReadyPromise: Promise<void>;
103
103
  */
104
104
  export declare function isLoginRequestUrl(url: string | undefined): boolean;
105
105
  export declare function handleApiError(error: unknown, operation: string): never;
106
- export { getSSHHosts, createSSHHost, updateSSHHost, bulkUpdateSSHHosts, reorderSSHHosts, deleteSSHHost, getSSHHostById, testProxyConnection, } from "./api/ssh-host-management-api";
106
+ export { getSSHHosts, createSSHHost, updateSSHHost, bulkUpdateSSHHosts, reorderSSHHosts, getHostPolicy, type HostPolicy, deleteSSHHost, getSSHHostById, testProxyConnection, } from "./api/ssh-host-management-api";
107
107
  export { getAllServerStatuses, getServerStatusById, refreshServerPolling, getStatusCheckSettings, updateStatusCheckSettings, } from "./api/host-status-api";
108
108
  export { getHostSidebarPreferences, saveHostSidebarPreferences, } from "./api/host-sidebar-preferences-api";
109
109
  export { getCredentialSidebarPreferences, saveCredentialSidebarPreferences, } from "./api/credential-sidebar-preferences-api";
110
110
  export { getUiPreferences, saveUiPreferences } from "./api/ui-preferences-api";
111
- export { getLogLevel, updateLogLevel, getSessionTimeout, updateSessionTimeout, } from "./api/settings-api";
111
+ export { getLogLevel, updateLogLevel, getSessionTimeout, updateSessionTimeout, getHttpsSettings, updateHttpsSettings, type HttpsSettings, } from "./api/settings-api";
112
112
  export declare function registerUser(username: string, password: string): Promise<Record<string, unknown>>;
113
113
  export declare function adminCreateUser(username: string, password: string): Promise<Record<string, unknown>>;
114
114
  export declare function loginUser(username: string, password: string, rememberMe?: boolean): Promise<AuthResponse>;
@@ -107,7 +107,7 @@ export { type AuthOverrideProtocol, SSH_AUTH_PROTOCOL, } from "../../types/auth-
107
107
  export { CREDENTIAL_ROW_FIELD_KEYS, type CredentialDensity, type CredentialRowFields, type CredentialSidebarFilterState, type CredentialSidebarPreferences, type CredentialSortKey, type CredentialTrayTrigger, defaultCredentialRowFields, defaultCredentialSidebarPreferences, sanitizeCredentialSidebarPreferences, } from "../../types/credential-sidebar-preferences";
108
108
  export { CORE_HOST_DEFAULTS, CORE_NAMESPACE, type DefaultOverrides, defaultValuesEqual, findCoreHostDefault, type HostDefaultsLevel, type ResolvedHostDefault, splitDefaultKey, } from "../../types/host-defaults";
109
109
  export { defaultHostRowFields, defaultHostSidebarPreferences, type HostClickBehavior, type HostDensity, type HostGroupKey, type HostRowFields, type HostSidebarFilterState, type HostSidebarPreferences, type HostTrayTrigger, sanitizeHostSidebarPreferences, type SortKey, } from "../../types/host-sidebar-preferences";
110
- export type { HostData, HostProtocolAuthInput, ProxyNode, SSHHost, SSHHostData, } from "../../types";
110
+ export type { HostData, HostLock, HostProtocolAuthInput, ProxyNode, SSHHost, SSHHostData, } from "../../types";
111
111
  export { PRESETS, UI_AREA_KEYS, type UiAreaKey, type UiPreset, } from "../../types/ui-preferences";
112
112
  export type { AdminSection, Credential, DashboardCardId, FontSizeId, Host, HostFolder, SharePermissionLevel, Tab, TabType, ThemeId, ToolsTab, UiFontId, } from "../../types/ui-types";
113
113
  export { type AuditLog, type AuditLogFilters, getAuditLogActions, getAuditLogs, } from "../api/audit-log-api";
@@ -165,7 +165,7 @@ export { tabJumpHotkeyKeys } from "../lib/tab-jump-hotkey";
165
165
  export { ACCENT_PRESET_COLORS, applyAccentColor, applyFontSize, applyUiFont, DASHBOARD_CARDS, FONT_SIZES, UI_FONTS, } from "../lib/theme";
166
166
  export { registerLayout } from "../look/layout-registry";
167
167
  export { useChooseTheme } from "../look/use-look-theme";
168
- export { type AccessRecord, type ActiveSessionInfo, adminCreateUser, adminCreateUserCredential, adminCreateUserHost, adminDeleteUserCredential, adminDeleteUserHost, adminExportUserData, adminGetHostPassword, adminGetUserCredentials, adminGetUserHosts, adminResetUserPassword, adminUpdateUserCredential, adminUpdateUserHost, type ApiKey, assignRoleToUser, bulkUpdateSSHHosts, changePassword, createApiKey, createCredential, type CreatedApiKey, createRole, createSSHHost, deleteAccount, deleteAllHostsInFolder, deleteApiKey, deleteCredential, deleteOpenTab, deleteRole, deleteSSHHost, deleteUser, duplicateCredential, getActiveSessions, getApiKeys, getCredentialDetails, getCredentialSidebarPreferences, getExternalAutoProvision, getHostAccess, getHostAuthOverride, getHostPassword, getHostSidebarPreferences, getLogLevel, getPasswordLoginAllowed, getPasswordResetAllowed, getPermissionsCatalog, getRegistrationAllowed, getSecondFactorAfterExternalLogin, getSessions, getSessionTimeout, getSSHFolders, getSSHHosts, getStatusCheckSettings, getUserInfo, getUserList, getUserRoles, type LinkedAccountInfo, linkExternalToPasswordAccount, logoutUser, makeUserAdmin, type OpenTabRecord, type PermissionCatalogEntry, releaseUrlFrom, removeAdminStatus, removeRoleFromUser, renameCredentialFolder, renameFolder, reorderCredentials, reorderFolders, reorderSSHHosts, resetRecentActivity, revokeAllUserSessions, revokeHostAccess, revokeSession, type Role, saveCredentialSidebarPreferences, saveHostSidebarPreferences, setHostAuthOverride, shareFolder, shareHost, type SSHHostWithStatus, unlinkExternalFromPasswordAccount, updateCredential, updateExternalAutoProvision, updateFolderMetadata, updateHostAccess, updateLogLevel, updatePasswordLoginAllowed, updatePasswordResetAllowed, updateRegistrationAllowed, updateRole, updateSecondFactorAfterExternalLogin, updateSessionTimeout, updateSSHHost, updateStatusCheckSettings, type UserPreferences, type UserRole, } from "../main-axios";
168
+ export { type AccessRecord, type ActiveSessionInfo, adminCreateUser, adminCreateUserCredential, adminCreateUserHost, adminDeleteUserCredential, adminDeleteUserHost, adminExportUserData, adminGetHostPassword, adminGetUserCredentials, adminGetUserHosts, adminResetUserPassword, adminUpdateUserCredential, adminUpdateUserHost, type ApiKey, assignRoleToUser, bulkUpdateSSHHosts, changePassword, createApiKey, createCredential, type CreatedApiKey, createRole, createSSHHost, deleteAccount, deleteAllHostsInFolder, deleteApiKey, deleteCredential, deleteOpenTab, deleteRole, deleteSSHHost, deleteUser, duplicateCredential, getActiveSessions, getApiKeys, getCredentialDetails, getCredentialSidebarPreferences, getExternalAutoProvision, getHostAccess, getHostAuthOverride, getHostPassword, getHostPolicy, type HostPolicy, getHostSidebarPreferences, getHttpsSettings, type HttpsSettings, getLogLevel, getPasswordLoginAllowed, getPasswordResetAllowed, getPermissionsCatalog, getRegistrationAllowed, getSecondFactorAfterExternalLogin, getSessions, getSessionTimeout, getSSHFolders, getSSHHosts, getStatusCheckSettings, getUserInfo, getUserList, getUserRoles, type LinkedAccountInfo, linkExternalToPasswordAccount, logoutUser, makeUserAdmin, type OpenTabRecord, type PermissionCatalogEntry, releaseUrlFrom, removeAdminStatus, removeRoleFromUser, renameCredentialFolder, renameFolder, reorderCredentials, reorderFolders, reorderSSHHosts, resetRecentActivity, revokeAllUserSessions, revokeHostAccess, revokeSession, type Role, saveCredentialSidebarPreferences, saveHostSidebarPreferences, setHostAuthOverride, shareFolder, shareHost, type SSHHostWithStatus, unlinkExternalFromPasswordAccount, updateCredential, updateExternalAutoProvision, updateFolderMetadata, updateHostAccess, updateHttpsSettings, updateLogLevel, updatePasswordLoginAllowed, updatePasswordResetAllowed, updateRegistrationAllowed, updateRole, updateSecondFactorAfterExternalLogin, updateSessionTimeout, updateSSHHost, updateStatusCheckSettings, type UserPreferences, type UserRole, } from "../main-axios";
169
169
  export { MANAGE_REQUEST_EVENT, type ManageMode, type ManageRequest, requestFromLegacyEvent, requestManage, takePendingManageRequest, } from "../manage/manage-requests";
170
170
  export { useSshAuthEditors } from "./auth-registry";
171
171
  export { usePluginStore } from "./plugin-store";
@@ -2,4 +2,7 @@
2
2
  * Enrolment sections login methods and second factors register, such as
3
3
  * adding a passkey or setting up an authenticator app.
4
4
  */
5
- export declare function AuthEnrollmentSections(): import("react").JSX.Element;
5
+ export declare function AuthEnrollmentSections({ factorsOnly, }?: {
6
+ /** Only second factors, for a login policy that requires one. */
7
+ factorsOnly?: boolean;
8
+ }): import("react").JSX.Element;
package/dist/host/ui.js CHANGED
@@ -52,6 +52,7 @@ import {
52
52
  getPollingEnvironmentMultiplier,
53
53
  getPreferredHostAction,
54
54
  getRegisteredDashboardCard,
55
+ getRememberDeviceAllowed,
55
56
  getStatusClasses,
56
57
  getSyncConflicts,
57
58
  getSyncErrors,
@@ -173,10 +174,10 @@ import {
173
174
  useThemes,
174
175
  verifySecondFactor,
175
176
  withProtocolSettings
176
- } from "./chunk-CALNJCHN.js";
177
+ } from "./chunk-V44FCJBQ.js";
177
178
  import {
178
179
  usePermissions
179
- } from "./chunk-T2PZWXFJ.js";
180
+ } from "./chunk-57DJU2ZA.js";
180
181
  import {
181
182
  AddButton,
182
183
  Card,
@@ -197,7 +198,7 @@ import {
197
198
  isPrerelease,
198
199
  reportBetaFeedbackUrl,
199
200
  reportCoreIssueUrl
200
- } from "./chunk-54KCK6IZ.js";
201
+ } from "./chunk-VFTB2RDP.js";
201
202
  import {
202
203
  Action,
203
204
  Activity,
@@ -387,7 +388,7 @@ import {
387
388
  useSurfaceKind,
388
389
  useUiPreference,
389
390
  useUiPreferencesContext
390
- } from "./chunk-ERGN4YW6.js";
391
+ } from "./chunk-2IV3VX7F.js";
391
392
  import {
392
393
  CREDENTIAL_ROW_FIELD_KEYS,
393
394
  PRESETS,
@@ -450,7 +451,9 @@ import {
450
451
  getHostAccess,
451
452
  getHostAuthOverride,
452
453
  getHostPassword,
454
+ getHostPolicy,
453
455
  getHostSidebarPreferences,
456
+ getHttpsSettings,
454
457
  getLogLevel,
455
458
  getPasswordLoginAllowed,
456
459
  getPasswordResetAllowed,
@@ -531,6 +534,7 @@ import {
531
534
  updateExternalAutoProvision,
532
535
  updateFolderMetadata,
533
536
  updateHostAccess,
537
+ updateHttpsSettings,
534
538
  updateLogLevel,
535
539
  updatePasswordLoginAllowed,
536
540
  updatePasswordResetAllowed,
@@ -544,7 +548,7 @@ import {
544
548
  useTabs,
545
549
  useTabsSafe,
546
550
  verifyPasswordResetCode
547
- } from "./chunk-CN5UBTBP.js";
551
+ } from "./chunk-3L45NNT5.js";
548
552
  import {
549
553
  define_import_meta_env_default,
550
554
  init_define_import_meta_env
@@ -4343,6 +4347,7 @@ function Auth({ onLogin }) {
4343
4347
  const [username, setUsername] = useState7("");
4344
4348
  const [password, setPassword] = useState7("");
4345
4349
  const [confirmPassword, setConfirmPassword] = useState7("");
4350
+ const [rememberAllowed, setRememberAllowed] = useState7(true);
4346
4351
  const [rememberMe, setRememberMe] = useState7(() => {
4347
4352
  try {
4348
4353
  return localStorage.getItem("rememberMe") === "true";
@@ -4463,6 +4468,11 @@ function Auth({ onLogin }) {
4463
4468
  getPasswordResetAllowed().then((allowed) => setPasswordResetAllowed(allowed)).catch(() => setPasswordResetAllowed(false));
4464
4469
  void startPreLoginPlugins().catch(() => {
4465
4470
  }).finally(() => setAuthPluginsLoading(false));
4471
+ getRememberDeviceAllowed().then((allowed) => {
4472
+ setRememberAllowed(allowed);
4473
+ if (!allowed) setRememberMe(false);
4474
+ }).catch(() => {
4475
+ });
4466
4476
  getLoginMethods().then((methods) => setAuthMethods(methods)).catch(() => setAuthMethods([])).finally(() => setAuthMethodsLoaded(true));
4467
4477
  }, [localDesktopAuth]);
4468
4478
  useEffect6(() => {
@@ -5358,7 +5368,7 @@ function Auth({ onLogin }) {
5358
5368
  /* @__PURE__ */ jsx28("p", { className: "text-xs text-muted-foreground", children: view === "login" ? t3("auth.loginSubtitle", "") : view === "register" ? t3("auth.registerSubtitle", "") : t3("auth.loginWithExternalDesc") })
5359
5369
  ] }),
5360
5370
  view === "external" && /* @__PURE__ */ jsxs20("div", { className: "flex flex-col gap-4", children: [
5361
- /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
5371
+ rememberAllowed && /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
5362
5372
  /* @__PURE__ */ jsx28(
5363
5373
  Checkbox,
5364
5374
  {
@@ -5409,7 +5419,7 @@ function Auth({ onLogin }) {
5409
5419
  }
5410
5420
  ) }),
5411
5421
  /* @__PURE__ */ jsxs20("div", { className: "flex items-center justify-between", children: [
5412
- /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
5422
+ rememberAllowed ? /* @__PURE__ */ jsxs20("div", { className: "flex items-center gap-2", children: [
5413
5423
  /* @__PURE__ */ jsx28(
5414
5424
  Checkbox,
5415
5425
  {
@@ -5427,7 +5437,7 @@ function Auth({ onLogin }) {
5427
5437
  children: t3("auth.rememberMe")
5428
5438
  }
5429
5439
  )
5430
- ] }),
5440
+ ] }) : /* @__PURE__ */ jsx28("span", {}),
5431
5441
  passwordResetAllowed && /* @__PURE__ */ jsx28(
5432
5442
  "button",
5433
5443
  {
@@ -33898,9 +33908,13 @@ function SplitLayoutMenu({
33898
33908
  init_define_import_meta_env();
33899
33909
  import { useTranslation as useTranslation28 } from "react-i18next";
33900
33910
  import { jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
33901
- function AuthEnrollmentSections() {
33911
+ function AuthEnrollmentSections({
33912
+ factorsOnly = false
33913
+ } = {}) {
33902
33914
  const { t: t3 } = useTranslation28();
33903
- const sections = [...useLoginMethods(), ...useSecondFactors()].filter(
33915
+ const methods = useLoginMethods();
33916
+ const factors = useSecondFactors();
33917
+ const sections = [...factorsOnly ? [] : methods, ...factors].filter(
33904
33918
  (entry) => entry.enrollment
33905
33919
  );
33906
33920
  if (sections.length === 0) return null;
@@ -34205,8 +34219,10 @@ export {
34205
34219
  getHostDefaultsLevel,
34206
34220
  getHostEditorSection,
34207
34221
  getHostPassword,
34222
+ getHostPolicy,
34208
34223
  getHostSidebarPreferences,
34209
34224
  getHostTags,
34225
+ getHttpsSettings,
34210
34226
  getLiveHostStatus,
34211
34227
  getLogLevel,
34212
34228
  getNotificationPrivateEndpoints,
@@ -34364,6 +34380,7 @@ export {
34364
34380
  updateExternalAutoProvision,
34365
34381
  updateFolderMetadata,
34366
34382
  updateHostAccess,
34383
+ updateHttpsSettings,
34367
34384
  updateLogLevel,
34368
34385
  updatePasswordLoginAllowed,
34369
34386
  updatePasswordResetAllowed,
@@ -7,7 +7,7 @@ import {
7
7
  resetPermissionsCache,
8
8
  setPermissionsForTesting,
9
9
  usePermissions
10
- } from "./chunk-T2PZWXFJ.js";
10
+ } from "./chunk-57DJU2ZA.js";
11
11
  import "./chunk-YGYBM7SQ.js";
12
12
  export {
13
13
  hasPermission,
@@ -20,7 +20,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
20
20
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
21
21
  * version by the server.
22
22
  */
23
- export declare const PLUGIN_API_VERSION = "1.5.0";
23
+ export declare const PLUGIN_API_VERSION = "1.7.0";
24
24
  /** The API major, the default engine.api for a new plugin. */
25
25
  export declare const SUPPORTED_PLUGIN_API_VERSION: string;
26
26
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
package/dist/manifest.js CHANGED
@@ -22,7 +22,7 @@ export { ENV_NAME_PATTERN, MAX_ENV_DESCRIPTION_LENGTH, pluginDocsPage, pluginDoc
22
22
  * "^1.2" needs 1.2 or later). engine.termix is checked against the core
23
23
  * version by the server.
24
24
  */
25
- export const PLUGIN_API_VERSION = "1.5.0";
25
+ export const PLUGIN_API_VERSION = "1.7.0";
26
26
  /** The API major, the default engine.api for a new plugin. */
27
27
  export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
28
28
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
package/dist/testing.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface FakeWsRoute {
25
25
  import type { PluginLoginMethod, PluginVerifiedIdentity, PluginSecondFactor, PluginSshAuthProvider, PluginSshConnectOptions, PluginSshHost, PluginHostSummary, PluginShareableUser, PluginShareableRole, PluginSshKeyCredential, PluginSshKeyCredentialInput } from "./backend.js";
26
26
  import type { SyncEntityRegistration } from "./backend.js";
27
27
  import type { PluginDatabase } from "./backend.js";
28
- import type { PluginExternalClientRequest, PluginProtocolTarget, PluginHostProbeTarget, PluginHostStatusEntry, PluginActivityProvider, PluginHostStatusProvider, PluginNotification, PluginNotificationChannel, PluginNotifyHub, PluginFetchInit, PluginBinarySpec, PluginTlsCertificateInfo, PluginTlsStatus, PluginSummary, PluginKeyboardInteractiveHandler, PluginProcessHandle, PluginProcessOptions } from "./backend.js";
28
+ import type { PluginExternalClientRequest, PluginProtocolTarget, PluginHostProbeTarget, PluginHostStatusEntry, PluginActivityProvider, PluginHostLockProvider, PluginAuthPolicyProvider, PluginCredentialSummary, PluginHostShareGrant, PluginHostStatusProvider, PluginNotification, PluginNotificationChannel, PluginNotifyHub, PluginFetchInit, PluginBinarySpec, PluginTlsCertificateInfo, PluginTlsStatus, PluginSummary, PluginKeyboardInteractiveHandler, PluginProcessHandle, PluginProcessOptions } from "./backend.js";
29
29
  /** A timer registered through ctx.schedule, run by hand with runScheduled. */
30
30
  export interface FakeScheduledJob {
31
31
  kind: "every" | "after";
@@ -75,6 +75,8 @@ export interface FakeContextOptions {
75
75
  protocolTargets?: Record<string, PluginProtocolTarget>;
76
76
  /** What ctx.credentials.listSshKeys answers. */
77
77
  sshKeyCredentials?: PluginSshKeyCredential[];
78
+ /** What ctx.credentials.list answers. */
79
+ credentialSummaries?: PluginCredentialSummary[];
78
80
  /** What ctx.desktop.available() answers. Defaults to false. */
79
81
  desktopAvailable?: boolean;
80
82
  /** What ctx.desktop.local is. Calls throw when this is not given. */
@@ -122,6 +124,11 @@ export interface FakeContextOptions {
122
124
  baseUrl?: string;
123
125
  /** What ctx.auth.countLinkedUsers answers, by provider. Defaults to 0. */
124
126
  linkedUsers?: Record<string, number>;
127
+ /** Second factors from other plugins that ctx.auth.listSecondFactors adds. */
128
+ secondFactors?: {
129
+ id: string;
130
+ pluginId: string;
131
+ }[];
125
132
  /** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
126
133
  loginAttemptLimit?: number;
127
134
  /** What ctx.users.getLocale answers, by user id. A missing user answers null. */
@@ -167,6 +174,8 @@ export interface FakeAuthRegistrations {
167
174
  purpose: string;
168
175
  ttlSeconds?: number;
169
176
  }>;
177
+ /** Providers passed to ctx.auth.policy.provide that are still registered. */
178
+ policies: PluginAuthPolicyProvider[];
170
179
  }
171
180
  export interface FakePluginContext {
172
181
  ctx: PluginContext;
@@ -180,6 +189,8 @@ export interface FakePluginContext {
180
189
  hostStatusProviders: PluginHostStatusProvider[];
181
190
  /** Every provider passed to ctx.hosts.activity.provide that is still registered. */
182
191
  activityProviders: PluginActivityProvider[];
192
+ /** Every provider passed to ctx.hosts.locks.provide that is still registered. */
193
+ hostLockProviders: PluginHostLockProvider[];
183
194
  /** Topics emitted through ctx.events.emit, in order. */
184
195
  emitted: Array<{
185
196
  topic: string;
@@ -215,6 +226,11 @@ export interface FakePluginContext {
215
226
  permissionLevel: string;
216
227
  durationHours?: number;
217
228
  }>;
229
+ /** Every ctx.hosts.setShares call, in order. */
230
+ hostShareSets: Array<{
231
+ hostId: number;
232
+ grants: PluginHostShareGrant[];
233
+ }>;
218
234
  /** Everything registered through ctx.auth. */
219
235
  auth: FakeAuthRegistrations;
220
236
  /** Every ctx.desktop.openIsolatedWindow call, in order. */
@@ -364,6 +380,8 @@ export interface MockContextOptions {
364
380
  protocolTargets?: Record<string, PluginProtocolTarget>;
365
381
  /** What ctx.credentials.listSshKeys serves. */
366
382
  sshKeyCredentials?: PluginSshKeyCredential[];
383
+ /** What ctx.credentials.list serves. */
384
+ credentialSummaries?: PluginCredentialSummary[];
367
385
  /** What ctx.desktop.available() answers. */
368
386
  desktopAvailable?: boolean;
369
387
  /** What ctx.desktop.local is. See FakeContextOptions. */
@@ -402,6 +420,8 @@ export interface MockContextOptions {
402
420
  /** See FakeContextOptions. */
403
421
  loginAttemptLimit?: number;
404
422
  /** See FakeContextOptions. */
423
+ secondFactors?: FakeContextOptions["secondFactors"];
424
+ /** See FakeContextOptions. */
405
425
  locales?: Record<string, string>;
406
426
  }
407
427
  export interface MockPluginContext extends FakePluginContext {
package/dist/testing.js CHANGED
@@ -100,7 +100,7 @@ function emptyHostRecord(id, userId) {
100
100
  };
101
101
  }
102
102
  function applyHostInput(record, input) {
103
- const { tags, pluginSettings: _settings, forceKeyboardInteractive: _fki, ...rest } = input;
103
+ const { tags, pluginSettings: _settings, pluginEnable: _pluginEnable, forceKeyboardInteractive: _fki, password: _password, key: _key, keyPassword: _keyPassword, keyType: _keyType, sudoPassword: _sudoPassword, protocolAuth: _protocolAuth, ...rest } = input;
104
104
  const next = { ...record };
105
105
  for (const [key, value] of Object.entries(rest)) {
106
106
  if (value !== undefined)
@@ -126,6 +126,7 @@ export function createFakeContext(options = {}) {
126
126
  const emitted = [];
127
127
  const hostStatusProviders = [];
128
128
  const activityProviders = [];
129
+ const hostLockProviders = [];
129
130
  const kv = new Map();
130
131
  const tables = [];
131
132
  const syncEntities = [];
@@ -138,6 +139,7 @@ export function createFakeContext(options = {}) {
138
139
  const settingsValidators = new Set();
139
140
  const sshConnections = [];
140
141
  const hostShares = [];
142
+ const hostShareSets = [];
141
143
  const desktopWindows = [];
142
144
  const externalClientLaunches = [];
143
145
  const credentialReads = [];
@@ -198,6 +200,7 @@ export function createFakeContext(options = {}) {
198
200
  revokedSessions: [],
199
201
  loginFailures: new Map(),
200
202
  userTokens: [],
203
+ policies: [],
201
204
  };
202
205
  const sshClient = options.sshClient ?? {};
203
206
  let actor = options.actor;
@@ -526,6 +529,10 @@ export function createFakeContext(options = {}) {
526
529
  hostShares.push({ hostId, targets, permissionLevel, durationHours });
527
530
  return { hostId, shared: true };
528
531
  },
532
+ setShares: async (hostId, grants) => {
533
+ hostShareSets.push({ hostId, grants: [...grants] });
534
+ return { hostId, shared: true };
535
+ },
529
536
  listUsers: async () => options.shareableUsers ?? [],
530
537
  listRoles: async () => options.shareableRoles ?? [],
531
538
  trackSession: (hostId) => {
@@ -555,6 +562,16 @@ export function createFakeContext(options = {}) {
555
562
  };
556
563
  },
557
564
  },
565
+ locks: {
566
+ provide: (provider) => {
567
+ hostLockProviders.push(provider);
568
+ return () => {
569
+ const at = hostLockProviders.indexOf(provider);
570
+ if (at >= 0)
571
+ hostLockProviders.splice(at, 1);
572
+ };
573
+ },
574
+ },
558
575
  status: {
559
576
  provide: (provider) => {
560
577
  hostStatusProviders.push(provider);
@@ -761,6 +778,23 @@ export function createFakeContext(options = {}) {
761
778
  expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
762
779
  };
763
780
  },
781
+ policy: {
782
+ provide: (provider) => {
783
+ auth.policies.push(provider);
784
+ return () => {
785
+ const at = auth.policies.indexOf(provider);
786
+ if (at >= 0)
787
+ auth.policies.splice(at, 1);
788
+ };
789
+ },
790
+ },
791
+ listSecondFactors: () => [
792
+ ...auth.secondFactors.map((factor) => ({
793
+ id: factor.id,
794
+ pluginId,
795
+ })),
796
+ ...(options.secondFactors ?? []),
797
+ ],
764
798
  },
765
799
  users: {
766
800
  getLocale: async (userId) => options.locales?.[userId] ?? null,
@@ -778,6 +812,7 @@ export function createFakeContext(options = {}) {
778
812
  local: options.desktopLocal ?? unavailableDesktopLocal(),
779
813
  },
780
814
  credentials: {
815
+ list: async () => [...(options.credentialSummaries ?? [])],
781
816
  listSshKeys: async () => [
782
817
  ...(options.sshKeyCredentials ?? []),
783
818
  ...createdSshKeys.map((key) => ({
@@ -951,6 +986,7 @@ export function createFakeContext(options = {}) {
951
986
  disposals,
952
987
  hostStatusProviders,
953
988
  activityProviders,
989
+ hostLockProviders,
954
990
  emitted,
955
991
  kv,
956
992
  tables,
@@ -961,6 +997,7 @@ export function createFakeContext(options = {}) {
961
997
  coreSettings,
962
998
  sshConnections,
963
999
  hostShares,
1000
+ hostShareSets,
964
1001
  auth,
965
1002
  desktopWindows,
966
1003
  externalClientLaunches,
@@ -1085,6 +1122,7 @@ export function createMockCtx(options = {}) {
1085
1122
  services: options.services,
1086
1123
  protocolTargets: options.protocolTargets,
1087
1124
  sshKeyCredentials: options.sshKeyCredentials,
1125
+ credentialSummaries: options.credentialSummaries,
1088
1126
  desktopAvailable: options.desktopAvailable,
1089
1127
  desktopLocal: options.desktopLocal,
1090
1128
  auditEntries: options.auditEntries,
@@ -1102,6 +1140,7 @@ export function createMockCtx(options = {}) {
1102
1140
  baseUrl: options.baseUrl,
1103
1141
  linkedUsers: options.linkedUsers,
1104
1142
  loginAttemptLimit: options.loginAttemptLimit,
1143
+ secondFactors: options.secondFactors,
1105
1144
  locales: options.locales,
1106
1145
  manifest: {
1107
1146
  capabilities: options.capabilities ?? [],
@@ -1268,6 +1307,10 @@ export function createMockCtx(options = {}) {
1268
1307
  require("hosts:write");
1269
1308
  return ctx.hosts.share(hostId, targets, permissionLevel, durationHours);
1270
1309
  },
1310
+ setShares: async (hostId, grants) => {
1311
+ require("hosts:write");
1312
+ return ctx.hosts.setShares(hostId, grants);
1313
+ },
1271
1314
  listUsers: async () => {
1272
1315
  require("hosts:write");
1273
1316
  return ctx.hosts.listUsers();
@@ -1298,6 +1341,12 @@ export function createMockCtx(options = {}) {
1298
1341
  return ctx.hosts.activity.provide(provider);
1299
1342
  },
1300
1343
  },
1344
+ locks: {
1345
+ provide: (provider) => {
1346
+ require("hosts:lock");
1347
+ return ctx.hosts.locks.provide(provider);
1348
+ },
1349
+ },
1301
1350
  status: {
1302
1351
  provide: (provider) => {
1303
1352
  require("hosts:read");
@@ -1423,6 +1472,13 @@ export function createMockCtx(options = {}) {
1423
1472
  require("users:impersonate");
1424
1473
  return ctx.auth.issueUserToken(userId, tokenOptions);
1425
1474
  },
1475
+ policy: {
1476
+ provide: (provider) => {
1477
+ require("auth:policy");
1478
+ return ctx.auth.policy.provide(provider);
1479
+ },
1480
+ },
1481
+ listSecondFactors: () => ctx.auth.listSecondFactors(),
1426
1482
  },
1427
1483
  users: {
1428
1484
  getLocale: async (userId) => {
@@ -1458,6 +1514,10 @@ export function createMockCtx(options = {}) {
1458
1514
  local: gateDesktopLocal(ctx.desktop.local, () => require("desktop:local")),
1459
1515
  },
1460
1516
  credentials: {
1517
+ list: async () => {
1518
+ require("credentials:use");
1519
+ return ctx.credentials.list();
1520
+ },
1461
1521
  listSshKeys: async () => {
1462
1522
  require("credentials:use");
1463
1523
  return ctx.credentials.listSshKeys();
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * Two projects: tests/backend runs in node, tests/frontend in jsdom.
10
10
  */
11
+ import { createRequire } from "node:module";
11
12
  import fs from "node:fs";
12
13
  import path from "node:path";
13
14
  import { fileURLToPath } from "node:url";
@@ -84,6 +85,28 @@ export function pluginVitestConfig(configUrl, options = {}) {
84
85
  // Vite must be allowed to read it, and React must come from the plugin.
85
86
  const sdkDir = fs.realpathSync(fileURLToPath(new URL("..", import.meta.url)));
86
87
  const server = { fs: { allow: [pluginDir, sdkDir] } };
88
+ // dedupe alone is not enough: react-dom or the testing library loaded from
89
+ // the linked SDK's own node_modules pull in a second React, and hooks then
90
+ // see no renderer.
91
+ if (!sdkDir.startsWith(pluginDir)) {
92
+ const pluginRequire = createRequire(path.join(pluginDir, "package.json"));
93
+ for (const name of [
94
+ "react",
95
+ "react-dom",
96
+ "@testing-library/react",
97
+ "react-i18next",
98
+ "i18next",
99
+ ]) {
100
+ if (alias[name])
101
+ continue;
102
+ try {
103
+ alias[name] = path.dirname(pluginRequire.resolve(`${name}/package.json`));
104
+ }
105
+ catch {
106
+ // Not installed in the plugin: leave it to normal resolution.
107
+ }
108
+ }
109
+ }
87
110
  const resolve = {
88
111
  alias,
89
112
  dedupe: ["react", "react-dom", "react-i18next", "i18next"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "The contract between Termix core and a plugin: types, the capability catalog and the manifest schema.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -151,6 +151,7 @@
151
151
  "process:spawn",
152
152
  "users:write",
153
153
  "auth:provide",
154
+ "auth:policy",
154
155
  "system:tls",
155
156
  "device:serial",
156
157
  "hosts:write",
@@ -167,6 +168,7 @@
167
168
  "network:probe",
168
169
  "hosts:status-provide",
169
170
  "hosts:activity-provide",
171
+ "hosts:lock",
170
172
  "desktop:window",
171
173
  "desktop:local",
172
174
  "users:impersonate",