@termix-ssh/plugin-sdk 1.0.12 → 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.
- package/CHANGELOG.md +42 -0
- package/PLUGIN-GUIDE.md +106 -59
- package/dist/backend.d.ts +534 -1
- package/dist/backend.js +7 -0
- package/dist/capabilities.js +7 -0
- package/dist/db.js +1 -0
- package/dist/desktop.d.ts +0 -243
- package/dist/frontend.d.ts +23 -2
- package/dist/host/{PluginsTab-EV4MSGY6.js → PluginsTab-S33WIIGC.js} +511 -178
- package/dist/host/{chunk-AWR6FYDO.js → chunk-2IV3VX7F.js} +5552 -5550
- package/dist/host/{chunk-RUPLD6RF.js → chunk-3L45NNT5.js} +29 -84
- package/dist/host/{chunk-VEWOYAMZ.js → chunk-57DJU2ZA.js} +1 -1
- package/dist/host/{chunk-XKA3DBB2.js → chunk-V44FCJBQ.js} +135 -54
- package/dist/host/{chunk-X4QZKO5G.js → chunk-VFTB2RDP.js} +54 -5
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-B4NEG4MA.js → linked-server-SCJH7DSL.js} +1 -1
- package/dist/host/{rbac-api-OUO3LUOC.js → rbac-api-KA5IPQQL.js} +1 -1
- package/dist/host/testing-host.js +4 -4
- package/dist/host/types/src/types/index.d.ts +10 -0
- package/dist/host/types/src/types/onboarding.d.ts +0 -2
- package/dist/host/types/src/types/ui-types.d.ts +3 -1
- package/dist/host/types/src/ui/api/audit-log-api.d.ts +0 -10
- package/dist/host/types/src/ui/api/auth-methods-api.d.ts +2 -0
- package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
- package/dist/host/types/src/ui/api/plugins-api.d.ts +4 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +6 -6
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +6 -28
- package/dist/host/types/src/ui/main-axios.d.ts +3 -3
- package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +6 -7
- package/dist/host/types/src/ui/plugins/plugin-model.d.ts +20 -0
- package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +3 -0
- package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +4 -1
- package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
- package/dist/host/ui.js +36 -210
- package/dist/host/{use-permissions-6VSHHUMB.js → use-permissions-LLN6TLAL.js} +1 -1
- package/dist/manifest.d.ts +10 -1
- package/dist/manifest.js +16 -1
- package/dist/ssh-keys.d.ts +29 -0
- package/dist/ssh-keys.js +85 -0
- package/dist/testing.d.ts +52 -15
- package/dist/testing.js +279 -2
- package/dist/vitest-preset.js +23 -0
- package/package.json +5 -1
- package/schema/manifest.schema.json +12 -0
- package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +0 -30
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. */
|
|
@@ -696,6 +756,153 @@ export interface PluginHosts {
|
|
|
696
756
|
* Needs hosts:read.
|
|
697
757
|
*/
|
|
698
758
|
status: PluginHostStatusApi;
|
|
759
|
+
/**
|
|
760
|
+
* The hosts the acting user may connect to (their own and shared ones) as a
|
|
761
|
+
* status provider needs them: address, port and jump hosts, no secrets.
|
|
762
|
+
* Pass hostIds to narrow it. Needs hosts:read.
|
|
763
|
+
*/
|
|
764
|
+
probeTargets: (filter?: {
|
|
765
|
+
hostIds?: number[];
|
|
766
|
+
}) => Promise<PluginHostProbeTarget[]>;
|
|
767
|
+
/** Whether a session is open to the host right now. Needs hosts:read. */
|
|
768
|
+
sessionActive: (hostId: number) => boolean;
|
|
769
|
+
/**
|
|
770
|
+
* Imports Termix JSON host rows for the acting user, the same way POST
|
|
771
|
+
* /host/bulk-import does: up to 100 rows, jump hosts remapped by exportId,
|
|
772
|
+
* defaults, protocol logins and plugin settings applied, one save at the end.
|
|
773
|
+
* Needs hosts:write, plus credentials:write when credentials are passed, and
|
|
774
|
+
* the acting user needs the core hosts.create and hosts.edit permissions.
|
|
775
|
+
*/
|
|
776
|
+
importMany: (hosts: Record<string, unknown>[], options?: PluginHostImportOptions) => Promise<PluginHostImportResult>;
|
|
777
|
+
/** Imports the Host entries of an OpenSSH config file. Same rules as importMany. */
|
|
778
|
+
importSshConfig: (content: string, options?: Pick<PluginHostImportOptions, "overwrite" | "atomic">) => Promise<PluginHostImportResult>;
|
|
779
|
+
/**
|
|
780
|
+
* The acting user's own hosts as Termix JSON. Without share the secrets are
|
|
781
|
+
* included, decrypted. With share every secret is left out and credential
|
|
782
|
+
* logins point at a scrubbed credentials list by alias. Needs hosts:export
|
|
783
|
+
* and is always audited.
|
|
784
|
+
*/
|
|
785
|
+
exportMany: (options?: {
|
|
786
|
+
share?: boolean;
|
|
787
|
+
/** Only these of the user's hosts. */
|
|
788
|
+
hostIds?: number[];
|
|
789
|
+
}) => Promise<PluginHostExport>;
|
|
790
|
+
/**
|
|
791
|
+
* Validates and writes this plugin's own host settings from an imported
|
|
792
|
+
* row. Called for every imported row once the host exists. Return the
|
|
793
|
+
* values to write, or null to write nothing. Removed when the plugin
|
|
794
|
+
* deactivates. Needs hosts:write.
|
|
795
|
+
*/
|
|
796
|
+
registerImportNormalizer: (normalizer: PluginHostImportNormalizer) => () => void;
|
|
797
|
+
/** The recent activity list on the dashboard. */
|
|
798
|
+
activity: PluginHostActivityApi;
|
|
799
|
+
/** Read only hosts this plugin manages. */
|
|
800
|
+
locks: PluginHostLocksApi;
|
|
801
|
+
}
|
|
802
|
+
export interface PluginHostImportOptions {
|
|
803
|
+
/** Update a host that matches on ip, port and username instead of skipping it. */
|
|
804
|
+
overwrite?: boolean;
|
|
805
|
+
/**
|
|
806
|
+
* All or nothing: if any row fails, the hosts this call created are deleted
|
|
807
|
+
* and the result says rolledBack. Cannot be combined with overwrite.
|
|
808
|
+
*/
|
|
809
|
+
atomic?: boolean;
|
|
810
|
+
/** Credential placeholders rows can refer to by credentialAlias. */
|
|
811
|
+
credentials?: Array<Record<string, unknown>>;
|
|
812
|
+
}
|
|
813
|
+
export interface PluginHostImportResult {
|
|
814
|
+
success: number;
|
|
815
|
+
updated: number;
|
|
816
|
+
skipped: number;
|
|
817
|
+
failed: number;
|
|
818
|
+
errors: string[];
|
|
819
|
+
createdHostIds: number[];
|
|
820
|
+
rolledBack?: boolean;
|
|
821
|
+
}
|
|
822
|
+
export interface PluginHostExport {
|
|
823
|
+
hosts: Array<Record<string, unknown>>;
|
|
824
|
+
/** Share exports only. */
|
|
825
|
+
version?: string;
|
|
826
|
+
exportedAt?: string;
|
|
827
|
+
credentials?: Array<Record<string, unknown>>;
|
|
828
|
+
}
|
|
829
|
+
export type PluginHostImportNormalizer = (raw: Record<string, unknown>) => Record<string, unknown> | null;
|
|
830
|
+
export interface PluginHostProbeTarget {
|
|
831
|
+
id: number;
|
|
832
|
+
/** The user who owns the host; its jump hosts are theirs. */
|
|
833
|
+
ownerUserId: string;
|
|
834
|
+
ip: string;
|
|
835
|
+
port: number;
|
|
836
|
+
connectionType: string;
|
|
837
|
+
jumpHosts: Array<{
|
|
838
|
+
hostId: number;
|
|
839
|
+
}>;
|
|
840
|
+
statusCheckEnabled: boolean;
|
|
841
|
+
/** Seconds, or null to use the global interval. */
|
|
842
|
+
statusCheckInterval: number | null;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The plugin that owns host status. Core keeps the routes, the sync feed and
|
|
846
|
+
* ctx.hosts.status; this fills them. One provider at a time.
|
|
847
|
+
*/
|
|
848
|
+
export interface PluginHostStatusProvider {
|
|
849
|
+
start: () => void;
|
|
850
|
+
stop: () => void;
|
|
851
|
+
get: (hostId: number) => PluginHostStatusEntry | null;
|
|
852
|
+
/**
|
|
853
|
+
* Every status known, after polling is brought in line with the request:
|
|
854
|
+
* with hostIds, exactly those of the user's hosts; without, all of them.
|
|
855
|
+
*/
|
|
856
|
+
statusesFor: (userId: string, hostIds: number[] | null) => Promise<Map<number, PluginHostStatusEntry>>;
|
|
857
|
+
/** Restarts the user's checks. */
|
|
858
|
+
refresh: (userId: string) => Promise<void>;
|
|
859
|
+
check: (hostId: number) => Promise<PluginHostStatusEntry | null>;
|
|
860
|
+
reportLogin: (hostId: number, outcome: {
|
|
861
|
+
ok: boolean;
|
|
862
|
+
}) => void;
|
|
863
|
+
registerPort: (connectionType: string, resolve: (hostId: number) => number | undefined | Promise<number | undefined>) => () => void;
|
|
864
|
+
/** The global check interval in seconds. */
|
|
865
|
+
getIntervalSeconds: () => Promise<number>;
|
|
866
|
+
setIntervalSeconds: (seconds: number) => Promise<void>;
|
|
867
|
+
}
|
|
868
|
+
export interface PluginActivityEntry {
|
|
869
|
+
id: number;
|
|
870
|
+
userId: string;
|
|
871
|
+
type: string;
|
|
872
|
+
hostId: number;
|
|
873
|
+
hostName: string | null;
|
|
874
|
+
timestamp: string;
|
|
875
|
+
}
|
|
876
|
+
export type PluginRecordActivityResult = {
|
|
877
|
+
status: "logged";
|
|
878
|
+
id: number;
|
|
879
|
+
} | {
|
|
880
|
+
status: "rate_limited";
|
|
881
|
+
} | {
|
|
882
|
+
status: "invalid_type";
|
|
883
|
+
} | {
|
|
884
|
+
status: "denied";
|
|
885
|
+
} | {
|
|
886
|
+
status: "unavailable";
|
|
887
|
+
};
|
|
888
|
+
/** The plugin that stores recent activity. One provider at a time. */
|
|
889
|
+
export interface PluginActivityProvider {
|
|
890
|
+
record: (userId: string, entry: {
|
|
891
|
+
type: string;
|
|
892
|
+
hostId: number;
|
|
893
|
+
hostName: string;
|
|
894
|
+
}) => Promise<PluginRecordActivityResult>;
|
|
895
|
+
list: (userId: string, limit: number) => Promise<PluginActivityEntry[]>;
|
|
896
|
+
deleteByUserId: (userId: string) => Promise<void>;
|
|
897
|
+
deleteByHostIds: (hostIds: number[]) => Promise<void>;
|
|
898
|
+
}
|
|
899
|
+
export interface PluginHostActivityApi {
|
|
900
|
+
/**
|
|
901
|
+
* Becomes the store behind the dashboard's recent activity and
|
|
902
|
+
* ctx.hosts.recordActivity. Needs hosts:activity-provide. Removed on
|
|
903
|
+
* deactivate.
|
|
904
|
+
*/
|
|
905
|
+
provide: (provider: PluginActivityProvider) => () => void;
|
|
699
906
|
}
|
|
700
907
|
/**
|
|
701
908
|
* online: the port answers, a session is open or a login worked. offline: the
|
|
@@ -736,6 +943,11 @@ export interface PluginHostStatusApi {
|
|
|
736
943
|
* back to the host's port. Removed on deactivate.
|
|
737
944
|
*/
|
|
738
945
|
registerPort: (connectionType: string, resolve: (hostId: number) => number | undefined | Promise<number | undefined>) => () => void;
|
|
946
|
+
/**
|
|
947
|
+
* Becomes core's host status. Needs hosts:status-provide. Removed on
|
|
948
|
+
* deactivate, which hands status back to core's built-in checks.
|
|
949
|
+
*/
|
|
950
|
+
provide: (provider: PluginHostStatusProvider) => () => void;
|
|
739
951
|
}
|
|
740
952
|
/**
|
|
741
953
|
* A host as the SSH pipeline reads it: resolved by core, with secrets filled
|
|
@@ -879,6 +1091,23 @@ export interface PluginSshConnectOptions {
|
|
|
879
1091
|
*/
|
|
880
1092
|
sock?: unknown;
|
|
881
1093
|
}
|
|
1094
|
+
export interface PluginSshExecOptions extends PluginSshConnectOptions {
|
|
1095
|
+
/** Time to wait for the command to finish. Default 30s, at most 10 minutes. */
|
|
1096
|
+
timeoutMs?: number;
|
|
1097
|
+
/** Most bytes kept of each of stdout and stderr. Default 256 KB, at most 4 MB. */
|
|
1098
|
+
maxOutputBytes?: number;
|
|
1099
|
+
/** Written to the command's standard input, then closed. */
|
|
1100
|
+
stdin?: string;
|
|
1101
|
+
}
|
|
1102
|
+
export interface PluginSshExecResult {
|
|
1103
|
+
stdout: string;
|
|
1104
|
+
stderr: string;
|
|
1105
|
+
/** The exit code. Null when the command was killed or ran out of time. */
|
|
1106
|
+
code: number | null;
|
|
1107
|
+
/** Output went past maxOutputBytes and the rest was dropped. */
|
|
1108
|
+
truncated: boolean;
|
|
1109
|
+
timedOut: boolean;
|
|
1110
|
+
}
|
|
882
1111
|
export interface PluginSshConnection<Client = unknown> {
|
|
883
1112
|
/** An ssh2 Client, already "ready". */
|
|
884
1113
|
client: Client;
|
|
@@ -897,8 +1126,34 @@ export interface PluginSshConnection<Client = unknown> {
|
|
|
897
1126
|
* audit line per new connection. Connections still open on deactivate are
|
|
898
1127
|
* closed.
|
|
899
1128
|
*/
|
|
1129
|
+
export interface PluginNetwork {
|
|
1130
|
+
/**
|
|
1131
|
+
* Whether a TCP port answers. An SSH banner is read and answered politely;
|
|
1132
|
+
* nothing is ever sent that logs in. Loopback, private and link-local
|
|
1133
|
+
* addresses are allowed, since status checks target the user's own hosts.
|
|
1134
|
+
*/
|
|
1135
|
+
tcpProbe: (host: string, port: number, options?: {
|
|
1136
|
+
timeoutMs?: number;
|
|
1137
|
+
}) => Promise<boolean>;
|
|
1138
|
+
}
|
|
900
1139
|
export interface PluginSsh {
|
|
1140
|
+
/**
|
|
1141
|
+
* Whether a TCP port answers through the host's jump host chain. Chains are
|
|
1142
|
+
* shared and closed when idle. The acting user must be able to connect to
|
|
1143
|
+
* the host. Needs ssh:connect.
|
|
1144
|
+
*/
|
|
1145
|
+
probe: (hostId: number, options?: {
|
|
1146
|
+
port?: number;
|
|
1147
|
+
timeoutMs?: number;
|
|
1148
|
+
}) => Promise<boolean>;
|
|
901
1149
|
connect: <Client = unknown>(host: number | PluginSshHost, options?: PluginSshConnectOptions) => Promise<PluginSshConnection<Client>>;
|
|
1150
|
+
/**
|
|
1151
|
+
* Runs one command on a host with its stored login and closes the
|
|
1152
|
+
* connection. The acting user needs connect access to the host, the same as
|
|
1153
|
+
* connect. Output is capped and the command is stopped at the time limit.
|
|
1154
|
+
* Needs ssh:connect and credentials:use.
|
|
1155
|
+
*/
|
|
1156
|
+
exec: (host: number | PluginSshHost, command: string, options?: PluginSshExecOptions) => Promise<PluginSshExecResult>;
|
|
902
1157
|
/** Borrows a pooled connection. The pool key is `<pool>:<owner>:<address>`. */
|
|
903
1158
|
withConnection: <T, Client = unknown>(host: number | PluginSshHost, options: PluginSshConnectOptions & {
|
|
904
1159
|
pool: string;
|
|
@@ -1223,6 +1478,52 @@ export interface PluginSecondFactor {
|
|
|
1223
1478
|
/** Removes the user's enrolment. Called by the admin reset. */
|
|
1224
1479
|
reset?: (userId: string) => Promise<void>;
|
|
1225
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
|
+
}
|
|
1226
1527
|
export interface PluginUserTokenOptions {
|
|
1227
1528
|
/** Why the token exists, kept in the audit log and on the session. */
|
|
1228
1529
|
purpose: string;
|
|
@@ -1301,6 +1602,13 @@ export interface PluginAuth {
|
|
|
1301
1602
|
* never outlive itself.
|
|
1302
1603
|
*/
|
|
1303
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
|
+
}[];
|
|
1304
1612
|
}
|
|
1305
1613
|
/** What a plugin may learn about a user. Needs users:read. */
|
|
1306
1614
|
export interface PluginUsers {
|
|
@@ -1348,6 +1656,149 @@ export interface PluginDesktop {
|
|
|
1348
1656
|
}>;
|
|
1349
1657
|
/** Whether the server runs embedded in the desktop app, so the calls above can work. */
|
|
1350
1658
|
available: () => boolean;
|
|
1659
|
+
/**
|
|
1660
|
+
* The user's own machine. Only works on the desktop app's embedded backend
|
|
1661
|
+
* and needs desktop:local; every call is audited once per plugin start.
|
|
1662
|
+
*/
|
|
1663
|
+
readonly local: PluginDesktopLocal;
|
|
1664
|
+
}
|
|
1665
|
+
export interface PluginLocalDirEntry {
|
|
1666
|
+
name: string;
|
|
1667
|
+
path: string;
|
|
1668
|
+
type: "file" | "directory" | "link";
|
|
1669
|
+
size: number;
|
|
1670
|
+
modifiedTimestamp?: number;
|
|
1671
|
+
linkTarget?: string;
|
|
1672
|
+
hidden: boolean;
|
|
1673
|
+
}
|
|
1674
|
+
export interface PluginLocalWalkResult {
|
|
1675
|
+
files: Array<{
|
|
1676
|
+
localPath: string;
|
|
1677
|
+
relativePath: string;
|
|
1678
|
+
size: number;
|
|
1679
|
+
}>;
|
|
1680
|
+
emptyDirs: string[];
|
|
1681
|
+
totalBytes: number;
|
|
1682
|
+
}
|
|
1683
|
+
export interface PluginLocalPty {
|
|
1684
|
+
readonly pid: number;
|
|
1685
|
+
/** The shell program that was started. */
|
|
1686
|
+
readonly shell: string;
|
|
1687
|
+
write: (data: string) => void;
|
|
1688
|
+
resize: (cols: number, rows: number) => void;
|
|
1689
|
+
kill: () => void;
|
|
1690
|
+
onData: (handler: (data: string) => void) => () => void;
|
|
1691
|
+
onExit: (handler: (exitCode: number) => void) => () => void;
|
|
1692
|
+
}
|
|
1693
|
+
/** A download being written next to its final name, published when done. */
|
|
1694
|
+
export interface PluginLocalDownload {
|
|
1695
|
+
/** Final path inside the chosen folder, links inside it already refused. */
|
|
1696
|
+
readonly path: string;
|
|
1697
|
+
readonly stream: NodeJS.WritableStream;
|
|
1698
|
+
/** Moves the finished partial onto its final name. */
|
|
1699
|
+
publish: (options?: {
|
|
1700
|
+
overwrite?: boolean;
|
|
1701
|
+
}) => Promise<string>;
|
|
1702
|
+
/** Removes the partial file. Safe to call after publish. */
|
|
1703
|
+
abort: () => Promise<void>;
|
|
1704
|
+
}
|
|
1705
|
+
export interface PluginLocalResponse {
|
|
1706
|
+
readonly status: number;
|
|
1707
|
+
readonly ok: boolean;
|
|
1708
|
+
readonly headers: {
|
|
1709
|
+
get: (name: string) => string | null;
|
|
1710
|
+
};
|
|
1711
|
+
/** A web stream of the body, or null when there is none. */
|
|
1712
|
+
readonly body: AsyncIterable<Uint8Array> | null;
|
|
1713
|
+
text: () => Promise<string>;
|
|
1714
|
+
}
|
|
1715
|
+
export interface PluginDesktopLocal {
|
|
1716
|
+
/** Whether local access is possible: embedded backend and capability granted. */
|
|
1717
|
+
available: () => boolean;
|
|
1718
|
+
/**
|
|
1719
|
+
* Calls this plugin's own HTTP routes on the embedded backend ("local") or
|
|
1720
|
+
* on the server the desktop is linked to ("remote"), as the acting user.
|
|
1721
|
+
* The path is under /plugin-api/<this plugin id>; nothing else is
|
|
1722
|
+
* reachable. The link's proxy headers and certificate rules apply. A body
|
|
1723
|
+
* may be a string, a buffer or a Node stream.
|
|
1724
|
+
*/
|
|
1725
|
+
pluginFetch: (origin: "local" | "remote", subPath: string, init?: {
|
|
1726
|
+
method?: string;
|
|
1727
|
+
headers?: Record<string, string>;
|
|
1728
|
+
body?: string | Buffer | NodeJS.ReadableStream;
|
|
1729
|
+
signal?: AbortSignal;
|
|
1730
|
+
}) => Promise<PluginLocalResponse>;
|
|
1731
|
+
info: () => Promise<{
|
|
1732
|
+
home: string;
|
|
1733
|
+
separator: string;
|
|
1734
|
+
platform: NodeJS.Platform;
|
|
1735
|
+
/** The desktop app's user data folder. */
|
|
1736
|
+
userDataDir: string;
|
|
1737
|
+
/** The port the embedded backend listens on. */
|
|
1738
|
+
backendPort: number;
|
|
1739
|
+
/** The server this desktop is linked to, or null. */
|
|
1740
|
+
link: {
|
|
1741
|
+
serverUrl: string;
|
|
1742
|
+
token: string | null;
|
|
1743
|
+
/**
|
|
1744
|
+
* Headers for a request to the linked server: the session, plus the
|
|
1745
|
+
* proxy headers and basic auth the link was set up with.
|
|
1746
|
+
*/
|
|
1747
|
+
headers: Record<string, string>;
|
|
1748
|
+
/** The link accepts a self-signed certificate. */
|
|
1749
|
+
allowInvalidCertificate: boolean;
|
|
1750
|
+
} | null;
|
|
1751
|
+
}>;
|
|
1752
|
+
/**
|
|
1753
|
+
* GET a JSON route on the linked server with the link's session, proxy
|
|
1754
|
+
* headers and certificate rules. Rejects with an error whose `kind` is
|
|
1755
|
+
* "signed_out" when the server refused the session.
|
|
1756
|
+
*/
|
|
1757
|
+
linkJson: (pathname: string) => Promise<unknown>;
|
|
1758
|
+
readonly fs: {
|
|
1759
|
+
list: (dirPath: string) => Promise<{
|
|
1760
|
+
path: string;
|
|
1761
|
+
parent: string | null;
|
|
1762
|
+
entries: PluginLocalDirEntry[];
|
|
1763
|
+
}>;
|
|
1764
|
+
mkdir: (parentPath: string, name: string) => Promise<string>;
|
|
1765
|
+
createFile: (parentPath: string, name: string) => Promise<string>;
|
|
1766
|
+
/** Renames inside the same folder; never replaces an existing name. */
|
|
1767
|
+
rename: (path: string, newName: string) => Promise<string>;
|
|
1768
|
+
/** Creates a folder; with rootPath it must stay inside that folder. */
|
|
1769
|
+
ensureDir: (dirPath: string, rootPath?: string) => Promise<string>;
|
|
1770
|
+
/** Which of the given paths exist. */
|
|
1771
|
+
exists: (paths: string[]) => Promise<string[]>;
|
|
1772
|
+
walk: (rootPaths: string[]) => Promise<PluginLocalWalkResult>;
|
|
1773
|
+
/** Sends entries to the operating system's trash. */
|
|
1774
|
+
trash: (paths: string[]) => Promise<{
|
|
1775
|
+
trashed: number;
|
|
1776
|
+
failed: Array<{
|
|
1777
|
+
path: string;
|
|
1778
|
+
error: string;
|
|
1779
|
+
}>;
|
|
1780
|
+
}>;
|
|
1781
|
+
reveal: (path: string) => Promise<void>;
|
|
1782
|
+
open: (path: string) => Promise<void>;
|
|
1783
|
+
openRead: (path: string) => Promise<{
|
|
1784
|
+
stream: NodeJS.ReadableStream;
|
|
1785
|
+
size: number;
|
|
1786
|
+
}>;
|
|
1787
|
+
openDownload: (options: {
|
|
1788
|
+
rootPath: string;
|
|
1789
|
+
destPath: string;
|
|
1790
|
+
transferId: string;
|
|
1791
|
+
overwrite?: boolean;
|
|
1792
|
+
}) => Promise<PluginLocalDownload>;
|
|
1793
|
+
};
|
|
1794
|
+
readonly pty: {
|
|
1795
|
+
spawn: (options?: {
|
|
1796
|
+
cols?: number;
|
|
1797
|
+
rows?: number;
|
|
1798
|
+
/** "wsl" on Windows starts WSL instead of PowerShell. */
|
|
1799
|
+
shell?: string;
|
|
1800
|
+
}) => Promise<PluginLocalPty>;
|
|
1801
|
+
};
|
|
1351
1802
|
}
|
|
1352
1803
|
/**
|
|
1353
1804
|
* A host protocol whose login core keeps next to the host: any id a plugin
|
|
@@ -1391,6 +1842,14 @@ export interface PluginSshKeyCredential {
|
|
|
1391
1842
|
/** "<type> <base64>", derived from the private key when none was stored. Null when it cannot be. */
|
|
1392
1843
|
publicKey: string | null;
|
|
1393
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
|
+
}
|
|
1394
1853
|
/** A key pair to save as a new credential for the acting user. */
|
|
1395
1854
|
export interface PluginSshKeyCredentialInput {
|
|
1396
1855
|
name: string;
|
|
@@ -1411,6 +1870,12 @@ export interface PluginCredentials {
|
|
|
1411
1870
|
* Needs credentials:use and the core credentials.view permission. Audited.
|
|
1412
1871
|
*/
|
|
1413
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[]>;
|
|
1414
1879
|
/**
|
|
1415
1880
|
* Saves a key pair as a new encrypted credential owned by the acting user
|
|
1416
1881
|
* and returns its id. Needs credentials:write, the core credentials.create
|
|
@@ -1437,6 +1902,23 @@ export interface PluginCredentials {
|
|
|
1437
1902
|
* that would provide it.
|
|
1438
1903
|
*/
|
|
1439
1904
|
registerSecretResolver: (scheme: string, resolve: (userId: string, reference: string) => Promise<string>) => void;
|
|
1905
|
+
/**
|
|
1906
|
+
* The public key line of an SSH key credential the acting user can see
|
|
1907
|
+
* (their own, with the core credentials.view permission). Derived from the
|
|
1908
|
+
* private key when none is stored, including a key kept behind a secret
|
|
1909
|
+
* reference. Null when the credential is missing, is not a key, or cannot
|
|
1910
|
+
* be read. Needs credentials:use. Audited.
|
|
1911
|
+
*/
|
|
1912
|
+
publicKey: (credentialId: number) => Promise<string | null>;
|
|
1913
|
+
/**
|
|
1914
|
+
* Replaces secret references ("op://...") in a key and its passphrase with
|
|
1915
|
+
* the real values, for the acting user. Plain values pass through. Hands
|
|
1916
|
+
* private key material to the plugin. Needs credentials:read. Audited.
|
|
1917
|
+
*/
|
|
1918
|
+
resolveKeyRefs: (key: string, passphrase?: string | null) => Promise<{
|
|
1919
|
+
key: string;
|
|
1920
|
+
passphrase?: string;
|
|
1921
|
+
}>;
|
|
1440
1922
|
}
|
|
1441
1923
|
/**
|
|
1442
1924
|
* An audit line under an action name of the plugin's choosing (an SSH login),
|
|
@@ -1458,6 +1940,55 @@ export interface PluginAudit {
|
|
|
1458
1940
|
*/
|
|
1459
1941
|
request?: unknown;
|
|
1460
1942
|
}) => Promise<void>;
|
|
1943
|
+
/**
|
|
1944
|
+
* Called for every audit line core or any plugin writes, after it is
|
|
1945
|
+
* accepted. Fire and forget: a slow or throwing handler never delays or
|
|
1946
|
+
* fails the audited action. Lines written before the plugin started are not
|
|
1947
|
+
* replayed; read them with list or export. Returns an unsubscribe function;
|
|
1948
|
+
* it is also removed when the plugin deactivates. Needs audit:read.
|
|
1949
|
+
*/
|
|
1950
|
+
subscribe: (handler: (entry: PluginAuditEntry) => void) => () => void;
|
|
1951
|
+
/** A page of stored lines, newest first. Needs audit:read. */
|
|
1952
|
+
list: (options?: {
|
|
1953
|
+
filters?: PluginAuditFilters;
|
|
1954
|
+
limit?: number;
|
|
1955
|
+
offset?: number;
|
|
1956
|
+
}) => Promise<{
|
|
1957
|
+
entries: PluginAuditEntry[];
|
|
1958
|
+
total: number;
|
|
1959
|
+
}>;
|
|
1960
|
+
/**
|
|
1961
|
+
* Stored lines in time order, in batches, so an export never holds the
|
|
1962
|
+
* whole trail in memory. Needs audit:read.
|
|
1963
|
+
*/
|
|
1964
|
+
exportBatches: (filters?: PluginAuditFilters, batchSize?: number) => AsyncGenerator<PluginAuditEntry[]>;
|
|
1965
|
+
}
|
|
1966
|
+
export interface PluginAuditFilters {
|
|
1967
|
+
userId?: string;
|
|
1968
|
+
action?: string;
|
|
1969
|
+
resourceType?: string;
|
|
1970
|
+
success?: boolean;
|
|
1971
|
+
/** ISO date-time. */
|
|
1972
|
+
startDate?: string;
|
|
1973
|
+
/** ISO date-time. */
|
|
1974
|
+
endDate?: string;
|
|
1975
|
+
}
|
|
1976
|
+
export interface PluginAuditEntry {
|
|
1977
|
+
/** Only on stored lines (list and export). */
|
|
1978
|
+
id?: number;
|
|
1979
|
+
/** ISO date-time. */
|
|
1980
|
+
timestamp: string;
|
|
1981
|
+
userId: string | null;
|
|
1982
|
+
username: string;
|
|
1983
|
+
action: string;
|
|
1984
|
+
resourceType: string;
|
|
1985
|
+
resourceId: string | null;
|
|
1986
|
+
resourceName: string | null;
|
|
1987
|
+
details: string | null;
|
|
1988
|
+
ipAddress: string | null;
|
|
1989
|
+
userAgent: string | null;
|
|
1990
|
+
success: boolean;
|
|
1991
|
+
errorMessage: string | null;
|
|
1461
1992
|
}
|
|
1462
1993
|
/** A notification channel a user set up. Its config never leaves the hub. */
|
|
1463
1994
|
export interface PluginNotificationChannel {
|
|
@@ -1785,6 +2316,8 @@ export interface PluginContext {
|
|
|
1785
2316
|
readonly notify: PluginNotify;
|
|
1786
2317
|
/** Outbound HTTP through core's SSRF guard. Needs network:outbound. */
|
|
1787
2318
|
readonly fetch: PluginFetch;
|
|
2319
|
+
/** Plain reachability checks. Needs network:probe. */
|
|
2320
|
+
readonly network: PluginNetwork;
|
|
1788
2321
|
/** Programs on the Termix server. Needs process:spawn. */
|
|
1789
2322
|
readonly process: PluginProcess;
|
|
1790
2323
|
/** The server certificate. Needs system:tls. */
|
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 {
|
package/dist/capabilities.js
CHANGED
|
@@ -33,20 +33,27 @@ 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"),
|
|
39
40
|
entry("hosts:write", "medium"),
|
|
41
|
+
entry("hosts:export", "high"),
|
|
40
42
|
entry("credentials:use", "medium"),
|
|
41
43
|
entry("credentials:write", "medium"),
|
|
42
44
|
entry("network:outbound", "medium"),
|
|
43
45
|
entry("network:serve", "medium"),
|
|
44
46
|
entry("network:broadcast", "medium"),
|
|
47
|
+
entry("network:probe", "medium"),
|
|
48
|
+
entry("hosts:status-provide", "medium"),
|
|
49
|
+
entry("hosts:activity-provide", "medium"),
|
|
50
|
+
entry("hosts:lock", "medium"),
|
|
45
51
|
entry("users:read", "medium"),
|
|
46
52
|
entry("events:core", "medium"),
|
|
47
53
|
entry("notify:send", "medium"),
|
|
48
54
|
entry("audit:read", "medium"),
|
|
49
55
|
entry("desktop:window", "medium"),
|
|
56
|
+
entry("desktop:local", "high"),
|
|
50
57
|
entry("hosts:read", "low"),
|
|
51
58
|
entry("db:own", "low"),
|
|
52
59
|
entry("kv:own", "low"),
|