@termix-ssh/plugin-sdk 1.0.12 → 1.0.13

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 (40) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/PLUGIN-GUIDE.md +91 -59
  3. package/dist/backend.d.ts +404 -0
  4. package/dist/capabilities.js +5 -0
  5. package/dist/db.js +1 -0
  6. package/dist/desktop.d.ts +0 -243
  7. package/dist/frontend.d.ts +23 -2
  8. package/dist/host/{PluginsTab-EV4MSGY6.js → PluginsTab-OXQQ7M4I.js} +511 -178
  9. package/dist/host/{chunk-X4QZKO5G.js → chunk-54KCK6IZ.js} +52 -5
  10. package/dist/host/{chunk-XKA3DBB2.js → chunk-CALNJCHN.js} +114 -54
  11. package/dist/host/{chunk-RUPLD6RF.js → chunk-CN5UBTBP.js} +2 -86
  12. package/dist/host/{chunk-AWR6FYDO.js → chunk-ERGN4YW6.js} +5552 -5550
  13. package/dist/host/{chunk-VEWOYAMZ.js → chunk-T2PZWXFJ.js} +1 -1
  14. package/dist/host/core-classes.json +1 -1
  15. package/dist/host/{linked-server-B4NEG4MA.js → linked-server-5VFO4XQB.js} +1 -1
  16. package/dist/host/{rbac-api-OUO3LUOC.js → rbac-api-5YMBJAO5.js} +1 -1
  17. package/dist/host/testing-host.js +4 -4
  18. package/dist/host/types/src/types/onboarding.d.ts +0 -2
  19. package/dist/host/types/src/ui/api/audit-log-api.d.ts +0 -10
  20. package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
  21. package/dist/host/types/src/ui/api/plugins-api.d.ts +4 -0
  22. package/dist/host/types/src/ui/api/settings-api.d.ts +0 -6
  23. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +0 -28
  24. package/dist/host/types/src/ui/main-axios.d.ts +2 -2
  25. package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
  26. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +5 -6
  27. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +20 -0
  28. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +3 -0
  29. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
  30. package/dist/host/ui.js +14 -205
  31. package/dist/host/{use-permissions-6VSHHUMB.js → use-permissions-ISEDRY3J.js} +1 -1
  32. package/dist/manifest.d.ts +10 -1
  33. package/dist/manifest.js +16 -1
  34. package/dist/ssh-keys.d.ts +29 -0
  35. package/dist/ssh-keys.js +85 -0
  36. package/dist/testing.d.ts +32 -15
  37. package/dist/testing.js +218 -1
  38. package/package.json +5 -1
  39. package/schema/manifest.schema.json +10 -0
  40. package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +0 -30
package/dist/testing.js CHANGED
@@ -124,6 +124,8 @@ export function createFakeContext(options = {}) {
124
124
  const pluginId = options.pluginId ?? "test-plugin";
125
125
  const disposals = [];
126
126
  const emitted = [];
127
+ const hostStatusProviders = [];
128
+ const activityProviders = [];
127
129
  const kv = new Map();
128
130
  const tables = [];
129
131
  const syncEntities = [];
@@ -158,6 +160,7 @@ export function createFakeContext(options = {}) {
158
160
  };
159
161
  const secretStore = new Map();
160
162
  const audits = [];
163
+ const auditSubscribers = new Set();
161
164
  const activities = [];
162
165
  const trackedSessions = [];
163
166
  const interactions = [];
@@ -261,7 +264,7 @@ export function createFakeContext(options = {}) {
261
264
  files: {
262
265
  // A fake path; a test that needs a real folder uses the OS temp dir
263
266
  // itself, not this double.
264
- dataDir: async () => `/tmp/${pluginId}`,
267
+ dataDir: async () => options.dataDir ?? `/tmp/${pluginId}`,
265
268
  },
266
269
  db: options.db
267
270
  ? {
@@ -461,6 +464,27 @@ export function createFakeContext(options = {}) {
461
464
  },
462
465
  disposables,
463
466
  hosts: {
467
+ importMany: async (rows) => {
468
+ const created = rows.map(() => nextHostId++);
469
+ return {
470
+ success: created.length,
471
+ updated: 0,
472
+ skipped: 0,
473
+ failed: 0,
474
+ errors: [],
475
+ createdHostIds: created,
476
+ };
477
+ },
478
+ importSshConfig: async () => ({
479
+ success: 0,
480
+ updated: 0,
481
+ skipped: 0,
482
+ failed: 0,
483
+ errors: [],
484
+ createdHostIds: [],
485
+ }),
486
+ exportMany: async () => ({ hosts: [] }),
487
+ registerImportNormalizer: () => () => { },
464
488
  list: async () => [...hostsById.values()],
465
489
  get: async (hostId) => hostsById.get(hostId) ?? null,
466
490
  checkAccess: async (hostId) => {
@@ -519,7 +543,27 @@ export function createFakeContext(options = {}) {
519
543
  recordActivity: async (hostId, type, hostName) => {
520
544
  activities.push({ hostId, type, hostName });
521
545
  },
546
+ probeTargets: async (filter) => (options.probeTargets ?? []).filter((target) => !filter?.hostIds || filter.hostIds.includes(target.id)),
547
+ sessionActive: (hostId) => options.activeSessions?.includes(hostId) ?? false,
548
+ activity: {
549
+ provide: (provider) => {
550
+ activityProviders.push(provider);
551
+ return () => {
552
+ const at = activityProviders.indexOf(provider);
553
+ if (at >= 0)
554
+ activityProviders.splice(at, 1);
555
+ };
556
+ },
557
+ },
522
558
  status: {
559
+ provide: (provider) => {
560
+ hostStatusProviders.push(provider);
561
+ return () => {
562
+ const at = hostStatusProviders.indexOf(provider);
563
+ if (at >= 0)
564
+ hostStatusProviders.splice(at, 1);
565
+ };
566
+ },
523
567
  get: async (hostId) => hostStatuses.get(hostId) ?? null,
524
568
  check: async (hostId) => hostStatuses.get(hostId) ?? null,
525
569
  reportLogin: (hostId, outcome) => {
@@ -543,11 +587,36 @@ export function createFakeContext(options = {}) {
543
587
  after: (delayMs, fn) => schedule("after", delayMs, fn),
544
588
  },
545
589
  audit: {
590
+ subscribe: (handler) => {
591
+ auditSubscribers.add(handler);
592
+ return () => {
593
+ auditSubscribers.delete(handler);
594
+ };
595
+ },
596
+ list: async (listOptions) => {
597
+ const matching = filterAuditEntries(options.auditEntries ?? [], listOptions?.filters).reverse();
598
+ const offset = listOptions?.offset ?? 0;
599
+ return {
600
+ entries: matching.slice(offset, offset + (listOptions?.limit ?? 50)),
601
+ total: matching.length,
602
+ };
603
+ },
604
+ exportBatches: async function* (filters, batchSize) {
605
+ const matching = filterAuditEntries(options.auditEntries ?? [], filters);
606
+ const size = batchSize ?? 500;
607
+ for (let at = 0; at < matching.length; at += size) {
608
+ yield matching.slice(at, at + size);
609
+ }
610
+ },
546
611
  record: async (entry) => {
547
612
  audits.push({ ...entry });
548
613
  },
549
614
  },
615
+ network: {
616
+ tcpProbe: async (host, port) => options.reachable?.(host, port) ?? false,
617
+ },
550
618
  ssh: {
619
+ probe: async (hostId, probeOptions) => options.reachable?.(hostId, probeOptions?.port) ?? false,
551
620
  connect: async (host, connectOptions) => {
552
621
  sshConnections.push({ host, options: connectOptions });
553
622
  return {
@@ -559,6 +628,16 @@ export function createFakeContext(options = {}) {
559
628
  dispose: () => { },
560
629
  };
561
630
  },
631
+ exec: async (host) => {
632
+ sshConnections.push({ host });
633
+ return {
634
+ stdout: "",
635
+ stderr: "",
636
+ code: 0,
637
+ truncated: false,
638
+ timedOut: false,
639
+ };
640
+ },
562
641
  withConnection: async (host, connectOptions, fn) => {
563
642
  sshConnections.push({ host, pool: connectOptions.pool });
564
643
  return fn(sshClient);
@@ -696,6 +775,7 @@ export function createFakeContext(options = {}) {
696
775
  return { success: true };
697
776
  },
698
777
  available: () => options.desktopAvailable ?? false,
778
+ local: options.desktopLocal ?? unavailableDesktopLocal(),
699
779
  },
700
780
  credentials: {
701
781
  listSshKeys: async () => [
@@ -716,6 +796,12 @@ export function createFakeContext(options = {}) {
716
796
  credentialReads.push({ hostId, protocol });
717
797
  return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
718
798
  },
799
+ publicKey: async (credentialId) => (options.sshKeyCredentials ?? []).find((k) => k.id === credentialId)
800
+ ?.publicKey ?? null,
801
+ resolveKeyRefs: async (key, passphrase) => ({
802
+ key,
803
+ passphrase: passphrase ?? undefined,
804
+ }),
719
805
  registerSecretResolver: (scheme, resolve) => {
720
806
  auth.secretResolvers.set(scheme, resolve);
721
807
  disposals.push(() => {
@@ -863,6 +949,8 @@ export function createFakeContext(options = {}) {
863
949
  return errors;
864
950
  },
865
951
  disposals,
952
+ hostStatusProviders,
953
+ activityProviders,
866
954
  emitted,
867
955
  kv,
868
956
  tables,
@@ -886,6 +974,10 @@ export function createFakeContext(options = {}) {
886
974
  tls,
887
975
  secretStore,
888
976
  audits,
977
+ emitAudit: (entry) => {
978
+ for (const handler of auditSubscribers)
979
+ handler(entry);
980
+ },
889
981
  activities,
890
982
  trackedSessions,
891
983
  interactions,
@@ -930,6 +1022,49 @@ export function createFakeContext(options = {}) {
930
1022
  * (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
931
1023
  * ungated. As the SDK grows a member, add its gate here in the same shape.
932
1024
  */
1025
+ function filterAuditEntries(entries, filters) {
1026
+ return entries.filter((entry) => (filters?.userId === undefined || entry.userId === filters.userId) &&
1027
+ (filters?.action === undefined || entry.action === filters.action) &&
1028
+ (filters?.resourceType === undefined ||
1029
+ entry.resourceType === filters.resourceType) &&
1030
+ (filters?.success === undefined || entry.success === filters.success) &&
1031
+ (filters?.startDate === undefined ||
1032
+ entry.timestamp >= filters.startDate) &&
1033
+ (filters?.endDate === undefined || entry.timestamp <= filters.endDate));
1034
+ }
1035
+ function gateDesktopLocal(target, check) {
1036
+ const wrap = (value) => {
1037
+ if (typeof value === "function") {
1038
+ return (...args) => {
1039
+ check();
1040
+ return value(...args);
1041
+ };
1042
+ }
1043
+ if (value && typeof value === "object") {
1044
+ return new Proxy(value, {
1045
+ get: (obj, key) => wrap(obj[key]),
1046
+ });
1047
+ }
1048
+ return value;
1049
+ };
1050
+ return new Proxy(target, {
1051
+ get: (obj, key) => key === "available"
1052
+ ? obj.available
1053
+ : wrap(obj[key]),
1054
+ });
1055
+ }
1056
+ function unavailableDesktopLocal() {
1057
+ const fail = () => {
1058
+ throw new Error("ctx.desktop.local needs the desktopLocal test option");
1059
+ };
1060
+ const deep = () => new Proxy(() => fail(), { get: () => deep(), apply: () => fail() });
1061
+ return {
1062
+ available: () => false,
1063
+ info: fail,
1064
+ fs: deep(),
1065
+ pty: deep(),
1066
+ };
1067
+ }
933
1068
  export function createMockCtx(options = {}) {
934
1069
  const granted = new Set(options.capabilities ?? []);
935
1070
  const checked = [];
@@ -938,6 +1073,7 @@ export function createMockCtx(options = {}) {
938
1073
  actor: options.actor,
939
1074
  settings: options.settings,
940
1075
  coreSettings: options.coreSettings,
1076
+ dataDir: options.dataDir,
941
1077
  sshClient: options.sshClient,
942
1078
  db: options.db,
943
1079
  router: options.router,
@@ -950,7 +1086,12 @@ export function createMockCtx(options = {}) {
950
1086
  protocolTargets: options.protocolTargets,
951
1087
  sshKeyCredentials: options.sshKeyCredentials,
952
1088
  desktopAvailable: options.desktopAvailable,
1089
+ desktopLocal: options.desktopLocal,
1090
+ auditEntries: options.auditEntries,
953
1091
  hostStatuses: options.hostStatuses,
1092
+ probeTargets: options.probeTargets,
1093
+ activeSessions: options.activeSessions,
1094
+ reachable: options.reachable,
954
1095
  notificationChannels: options.notificationChannels,
955
1096
  fetch: options.fetch,
956
1097
  process: options.process,
@@ -1077,6 +1218,24 @@ export function createMockCtx(options = {}) {
1077
1218
  },
1078
1219
  },
1079
1220
  hosts: {
1221
+ importMany: async (rows, importOptions) => {
1222
+ require("hosts:write");
1223
+ if (importOptions?.credentials?.length)
1224
+ require("credentials:write");
1225
+ return ctx.hosts.importMany(rows, importOptions);
1226
+ },
1227
+ importSshConfig: async (content, importOptions) => {
1228
+ require("hosts:write");
1229
+ return ctx.hosts.importSshConfig(content, importOptions);
1230
+ },
1231
+ exportMany: async (exportOptions) => {
1232
+ require("hosts:export");
1233
+ return ctx.hosts.exportMany(exportOptions);
1234
+ },
1235
+ registerImportNormalizer: (normalizer) => {
1236
+ require("hosts:write");
1237
+ return ctx.hosts.registerImportNormalizer(normalizer);
1238
+ },
1080
1239
  list: async () => {
1081
1240
  require("hosts:read");
1082
1241
  return ctx.hosts.list();
@@ -1125,7 +1284,26 @@ export function createMockCtx(options = {}) {
1125
1284
  require("hosts:read");
1126
1285
  return ctx.hosts.recordActivity(hostId, type, hostName);
1127
1286
  },
1287
+ probeTargets: async (filter) => {
1288
+ require("hosts:read");
1289
+ return ctx.hosts.probeTargets(filter);
1290
+ },
1291
+ sessionActive: (hostId) => {
1292
+ require("hosts:read");
1293
+ return ctx.hosts.sessionActive(hostId);
1294
+ },
1295
+ activity: {
1296
+ provide: (provider) => {
1297
+ require("hosts:activity-provide");
1298
+ return ctx.hosts.activity.provide(provider);
1299
+ },
1300
+ },
1128
1301
  status: {
1302
+ provide: (provider) => {
1303
+ require("hosts:read");
1304
+ require("hosts:status-provide");
1305
+ return ctx.hosts.status.provide(provider);
1306
+ },
1129
1307
  get: async (hostId) => {
1130
1308
  require("hosts:read");
1131
1309
  return ctx.hosts.status.get(hostId);
@@ -1146,11 +1324,20 @@ export function createMockCtx(options = {}) {
1146
1324
  },
1147
1325
  ssh: {
1148
1326
  ...ctx.ssh,
1327
+ probe: async (hostId, probeOptions) => {
1328
+ require("ssh:connect");
1329
+ return ctx.ssh.probe(hostId, probeOptions);
1330
+ },
1149
1331
  connect: async (host, connectOptions) => {
1150
1332
  require("ssh:connect");
1151
1333
  require("credentials:use");
1152
1334
  return ctx.ssh.connect(host, connectOptions);
1153
1335
  },
1336
+ exec: async (host, command, execOptions) => {
1337
+ require("ssh:connect");
1338
+ require("credentials:use");
1339
+ return ctx.ssh.exec(host, command, execOptions);
1340
+ },
1154
1341
  withConnection: async (host, connectOptions, fn) => {
1155
1342
  require("ssh:connect");
1156
1343
  require("credentials:use");
@@ -1243,6 +1430,21 @@ export function createMockCtx(options = {}) {
1243
1430
  return ctx.users.getLocale(userId);
1244
1431
  },
1245
1432
  },
1433
+ audit: {
1434
+ record: (entry) => ctx.audit.record(entry),
1435
+ subscribe: (handler) => {
1436
+ require("audit:read");
1437
+ return ctx.audit.subscribe(handler);
1438
+ },
1439
+ list: async (listOptions) => {
1440
+ require("audit:read");
1441
+ return ctx.audit.list(listOptions);
1442
+ },
1443
+ exportBatches: (filters, batchSize) => {
1444
+ require("audit:read");
1445
+ return ctx.audit.exportBatches(filters, batchSize);
1446
+ },
1447
+ },
1246
1448
  desktop: {
1247
1449
  openIsolatedWindow: async (request) => {
1248
1450
  require("desktop:window");
@@ -1253,6 +1455,7 @@ export function createMockCtx(options = {}) {
1253
1455
  return ctx.desktop.launchExternalClient(request);
1254
1456
  },
1255
1457
  available: () => ctx.desktop.available(),
1458
+ local: gateDesktopLocal(ctx.desktop.local, () => require("desktop:local")),
1256
1459
  },
1257
1460
  credentials: {
1258
1461
  listSshKeys: async () => {
@@ -1271,6 +1474,14 @@ export function createMockCtx(options = {}) {
1271
1474
  }
1272
1475
  return ctx.credentials.resolveHostProtocol(hostId, protocol);
1273
1476
  },
1477
+ publicKey: async (credentialId) => {
1478
+ require("credentials:use");
1479
+ return ctx.credentials.publicKey(credentialId);
1480
+ },
1481
+ resolveKeyRefs: async (key, passphrase) => {
1482
+ require("credentials:read");
1483
+ return ctx.credentials.resolveKeyRefs(key, passphrase);
1484
+ },
1274
1485
  registerSecretResolver: (scheme, resolve) => {
1275
1486
  require("auth:provide");
1276
1487
  ctx.credentials.registerSecretResolver(scheme, resolve);
@@ -1294,6 +1505,12 @@ export function createMockCtx(options = {}) {
1294
1505
  require("network:outbound");
1295
1506
  return ctx.fetch(url, init);
1296
1507
  },
1508
+ network: {
1509
+ tcpProbe: async (host, port, probeOptions) => {
1510
+ require("network:probe");
1511
+ return ctx.network.tcpProbe(host, port, probeOptions);
1512
+ },
1513
+ },
1297
1514
  process: {
1298
1515
  run: async (file, args, runOptions) => {
1299
1516
  require("process:spawn");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
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": {
@@ -85,6 +85,10 @@
85
85
  "types": "./dist/ssh-certs.d.ts",
86
86
  "default": "./dist/ssh-certs.js"
87
87
  },
88
+ "./ssh-keys": {
89
+ "types": "./dist/ssh-keys.d.ts",
90
+ "default": "./dist/ssh-keys.js"
91
+ },
88
92
  "./tsconfig.plugin.json": "./tsconfig.plugin.json",
89
93
  "./schema/manifest.schema.json": "./schema/manifest.schema.json",
90
94
  "./ui": {
@@ -154,6 +154,7 @@
154
154
  "system:tls",
155
155
  "device:serial",
156
156
  "hosts:write",
157
+ "hosts:export",
157
158
  "credentials:use",
158
159
  "credentials:write",
159
160
  "network:outbound",
@@ -163,7 +164,11 @@
163
164
  "events:core",
164
165
  "notify:send",
165
166
  "audit:read",
167
+ "network:probe",
168
+ "hosts:status-provide",
169
+ "hosts:activity-provide",
166
170
  "desktop:window",
171
+ "desktop:local",
167
172
  "users:impersonate",
168
173
  "db:core-refs",
169
174
  "hosts:read",
@@ -1261,6 +1266,11 @@
1261
1266
  "enum": ["mirror", "local", "server"],
1262
1267
  "description": "How a desktop linked to a server treats this plugin: mirror follows the server, local is decided on each desktop, server never runs on a linked desktop. Defaults to mirror."
1263
1268
  },
1269
+ "runtime": {
1270
+ "type": "string",
1271
+ "enum": ["desktop"],
1272
+ "description": "desktop: the plugin only loads on a desktop app's own embedded backend, never on a server. It implies desktop local and may declare desktop:local."
1273
+ },
1264
1274
  "nativeDependencies": {
1265
1275
  "type": "array",
1266
1276
  "minItems": 1,
@@ -1,30 +0,0 @@
1
- export type FieldGroup = "connection" | "notes" | "tags" | "proxy" | "jumpHosts" | "featureFlags" | "advanced";
2
- export interface ExportPayload {
3
- version?: string;
4
- exportedAt?: string;
5
- credentials?: Record<string, unknown>[];
6
- hosts: Record<string, unknown>[];
7
- }
8
- export declare const SECRET_KEYS: string[];
9
- /**
10
- * Keys inside a plugin's JSON host settings that hold secrets, per plugin and
11
- * field, from each manifest's `secretKeys`.
12
- */
13
- export type PluginSecretKeys = Record<string, Record<string, string[]>>;
14
- /** Reads the secret keys the plugins declare in their host settings. */
15
- export declare function pluginSecretKeys(summaries: Array<{
16
- id: string;
17
- contributes?: {
18
- settings?: {
19
- host?: {
20
- fields?: Array<{
21
- key: string;
22
- secretKeys?: string[];
23
- }>;
24
- };
25
- };
26
- } | null;
27
- }>): PluginSecretKeys;
28
- export declare function hostKey(host: Record<string, unknown>): string;
29
- export declare function buildExportPayload(raw: ExportPayload, selected: Set<string> | null, groups: Set<FieldGroup>, withCredentials: boolean, pluginSecrets?: PluginSecretKeys): ExportPayload;
30
- export declare function maskSecrets(payload: ExportPayload, pluginSecrets?: PluginSecretKeys): ExportPayload;