@termix-ssh/plugin-sdk 1.0.11 → 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 (130) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/PLUGIN-GUIDE.md +178 -54
  3. package/cli/commands/build.mjs +36 -22
  4. package/cli/commands/pack.mjs +12 -2
  5. package/cli/commands/validate.mjs +63 -3
  6. package/cli/lib/plugin-dir.mjs +11 -0
  7. package/cli/lib/tmxplug.mjs +9 -0
  8. package/dist/backend.d.ts +404 -0
  9. package/dist/capabilities.js +7 -0
  10. package/dist/db.js +1 -0
  11. package/dist/desktop.d.ts +0 -243
  12. package/dist/frontend.d.ts +175 -2
  13. package/dist/frontend.js +7 -0
  14. package/dist/host/PluginsTab-OXQQ7M4I.js +3266 -0
  15. package/dist/host/chunk-54KCK6IZ.js +3167 -0
  16. package/dist/host/chunk-CALNJCHN.js +10435 -0
  17. package/dist/host/{chunk-7RRTYD6B.js → chunk-CN5UBTBP.js} +1412 -14
  18. package/dist/host/{chunk-RLQBKJRK.js → chunk-ERGN4YW6.js} +9204 -11492
  19. package/dist/host/{chunk-LSWB3ZZV.js → chunk-T2PZWXFJ.js} +6 -1
  20. package/dist/host/chunk-YGYBM7SQ.js +58 -0
  21. package/dist/host/core-classes.json +1 -1
  22. package/dist/host/{linked-server-UCW74T2V.js → linked-server-5VFO4XQB.js} +2 -2
  23. package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-5YMBJAO5.js} +2 -2
  24. package/dist/host/testing-host.js +123 -57
  25. package/dist/host/types/src/types/onboarding.d.ts +0 -2
  26. package/dist/host/types/src/types/ui-types.d.ts +2 -1
  27. package/dist/host/types/src/ui/api/audit-log-api.d.ts +36 -0
  28. package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
  29. package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
  30. package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
  31. package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
  32. package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
  33. package/dist/host/types/src/ui/api/plugins-api.d.ts +30 -0
  34. package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
  35. package/dist/host/types/src/ui/api/settings-api.d.ts +0 -6
  36. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +0 -28
  37. package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
  38. package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
  39. package/dist/host/types/src/ui/components/command.d.ts +78 -0
  40. package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
  41. package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
  42. package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
  43. package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
  44. package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
  45. package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
  46. package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
  47. package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
  48. package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
  49. package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
  50. package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
  51. package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
  52. package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
  53. package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
  54. package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
  55. package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +4 -3
  56. package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
  57. package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
  58. package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
  59. package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
  60. package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
  61. package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
  62. package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
  63. package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
  64. package/dist/host/types/src/ui/main-axios.d.ts +2 -2
  65. package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
  66. package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
  67. package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
  68. package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
  69. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +101 -0
  70. package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
  71. package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
  72. package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
  73. package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
  74. package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
  75. package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
  76. package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
  77. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +136 -0
  78. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +78 -0
  79. package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
  80. package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
  81. package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
  82. package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
  83. package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
  84. package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
  85. package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
  86. package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
  87. package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
  88. package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
  89. package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
  90. package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
  91. package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
  92. package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
  93. package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
  94. package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
  95. package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
  96. package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
  97. package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
  98. package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
  99. package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
  100. package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
  101. package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
  102. package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
  103. package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
  104. package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
  105. package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
  106. package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
  107. package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
  108. package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
  109. package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
  110. package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
  111. package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
  112. package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
  113. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
  114. package/dist/host/ui.js +6045 -4055
  115. package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-ISEDRY3J.js} +2 -2
  116. package/dist/manifest.d.ts +50 -1
  117. package/dist/manifest.js +104 -1
  118. package/dist/ssh-keys.d.ts +29 -0
  119. package/dist/ssh-keys.js +85 -0
  120. package/dist/testing/setup.js +1 -0
  121. package/dist/testing.d.ts +32 -15
  122. package/dist/testing.js +218 -1
  123. package/dist/theme-css.d.ts +11 -0
  124. package/dist/theme-css.js +142 -0
  125. package/dist/vitest-preset.d.ts +18 -0
  126. package/dist/vitest-preset.js +14 -3
  127. package/package.json +5 -1
  128. package/schema/manifest.schema.json +121 -8
  129. package/tsconfig.plugin.json +1 -0
  130. package/dist/host/chunk-YC2MAKSP.js +0 -14
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");
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Checks a theme's CSS before core serves it.
3
+ *
4
+ * A theme is colors and styles only, so its file may hold nothing but rules
5
+ * whose every selector starts with the theme's own class (`.nord`,
6
+ * `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
7
+ * Anything that could load from elsewhere or run code is refused: @import,
8
+ * url() to another host, other at-rules, expression() and javascript:.
9
+ */
10
+ /** Every problem with a theme's CSS, empty when it is safe to serve. */
11
+ export declare function validateThemeCss(css: string, themeId: string): string[];
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Checks a theme's CSS before core serves it.
3
+ *
4
+ * A theme is colors and styles only, so its file may hold nothing but rules
5
+ * whose every selector starts with the theme's own class (`.nord`,
6
+ * `:root.nord`, `html.nord ...`), optionally inside @media or @supports.
7
+ * Anything that could load from elsewhere or run code is refused: @import,
8
+ * url() to another host, other at-rules, expression() and javascript:.
9
+ */
10
+ const ALLOWED_BLOCK_AT_RULES = new Set(["media", "supports"]);
11
+ function stripComments(css) {
12
+ return css.replace(/\/\*[\s\S]*?\*\//g, " ");
13
+ }
14
+ /** Splits on commas that are not inside parentheses or brackets. */
15
+ function splitSelectors(prelude) {
16
+ const parts = [];
17
+ let depth = 0;
18
+ let current = "";
19
+ for (const ch of prelude) {
20
+ if (ch === "(" || ch === "[")
21
+ depth += 1;
22
+ if (ch === ")" || ch === "]")
23
+ depth -= 1;
24
+ if (ch === "," && depth === 0) {
25
+ parts.push(current);
26
+ current = "";
27
+ continue;
28
+ }
29
+ current += ch;
30
+ }
31
+ parts.push(current);
32
+ return parts.map((part) => part.trim());
33
+ }
34
+ function withoutStrings(css) {
35
+ return css.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""');
36
+ }
37
+ function escapeRegExp(value) {
38
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
39
+ }
40
+ /** Index of the brace closing the one at `open`, skipping strings. */
41
+ function matchingBrace(css, open) {
42
+ let depth = 0;
43
+ let quote = null;
44
+ for (let i = open; i < css.length; i += 1) {
45
+ const ch = css[i];
46
+ if (quote) {
47
+ if (ch === "\\")
48
+ i += 1;
49
+ else if (ch === quote)
50
+ quote = null;
51
+ continue;
52
+ }
53
+ if (ch === '"' || ch === "'")
54
+ quote = ch;
55
+ else if (ch === "{")
56
+ depth += 1;
57
+ else if (ch === "}") {
58
+ depth -= 1;
59
+ if (depth === 0)
60
+ return i;
61
+ }
62
+ }
63
+ return -1;
64
+ }
65
+ function checkUrls(css, errors) {
66
+ const urls = css.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi);
67
+ for (const match of urls) {
68
+ const target = match[2].trim();
69
+ if (target.startsWith("data:"))
70
+ continue;
71
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//")) {
72
+ errors.push(`url(${target}) points outside the plugin`);
73
+ }
74
+ }
75
+ }
76
+ function checkRules(css, scope, themeId, errors) {
77
+ let index = 0;
78
+ while (index < css.length) {
79
+ const rest = css.slice(index);
80
+ if (rest.trim() === "")
81
+ return;
82
+ const open = css.indexOf("{", index);
83
+ const semicolon = css.indexOf(";", index);
84
+ if (semicolon !== -1 && (open === -1 || semicolon < open)) {
85
+ const statement = css.slice(index, semicolon).trim();
86
+ errors.push(statement.startsWith("@")
87
+ ? `${statement.split(/\s/)[0]} is not allowed in a theme`
88
+ : `Unexpected "${statement}" outside a rule`);
89
+ index = semicolon + 1;
90
+ continue;
91
+ }
92
+ if (open === -1) {
93
+ errors.push(`Unexpected "${rest.trim()}" at the end`);
94
+ return;
95
+ }
96
+ const close = matchingBrace(css, open);
97
+ if (close === -1) {
98
+ errors.push("A rule is missing its closing brace");
99
+ return;
100
+ }
101
+ const prelude = css.slice(index, open).trim();
102
+ const body = css.slice(open + 1, close);
103
+ if (prelude.startsWith("@")) {
104
+ const name = prelude.slice(1).split(/[\s(]/)[0].toLowerCase();
105
+ if (ALLOWED_BLOCK_AT_RULES.has(name)) {
106
+ checkRules(body, scope, themeId, errors);
107
+ }
108
+ else {
109
+ errors.push(`@${name} is not allowed in a theme`);
110
+ }
111
+ }
112
+ else {
113
+ for (const selector of splitSelectors(prelude)) {
114
+ if (!scope.test(selector)) {
115
+ errors.push(`Selector "${selector}" must start with .${themeId}, :root.${themeId} or html.${themeId}`);
116
+ }
117
+ }
118
+ if (withoutStrings(body).includes("{")) {
119
+ errors.push(`Nested rules are not allowed in "${prelude}"`);
120
+ }
121
+ }
122
+ index = close + 1;
123
+ }
124
+ }
125
+ /** Every problem with a theme's CSS, empty when it is safe to serve. */
126
+ export function validateThemeCss(css, themeId) {
127
+ const errors = [];
128
+ const clean = stripComments(css);
129
+ if (/@import\b/i.test(clean))
130
+ errors.push("@import is not allowed in a theme");
131
+ if (/expression\s*\(/i.test(clean))
132
+ errors.push("expression() is not allowed in a theme");
133
+ if (/javascript:/i.test(clean))
134
+ errors.push("javascript: is not allowed in a theme");
135
+ if (/-moz-binding|behavior\s*:/i.test(clean))
136
+ errors.push("Script bindings are not allowed in a theme");
137
+ checkUrls(clean, errors);
138
+ const id = escapeRegExp(themeId);
139
+ const scope = new RegExp(`^(?::root|html)?\\.${id}(?![\\w-])`);
140
+ checkRules(clean, scope, themeId, errors);
141
+ return [...new Set(errors)];
142
+ }
@@ -27,8 +27,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
27
27
  enforce: "pre";
28
28
  load(id: string): string;
29
29
  }[];
30
+ server: {
31
+ fs: {
32
+ allow: string[];
33
+ };
34
+ };
30
35
  resolve: {
31
36
  alias: Record<string, string>;
37
+ dedupe: string[];
32
38
  };
33
39
  test: {
34
40
  globals: boolean;
@@ -40,8 +46,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
40
46
  enforce: "pre";
41
47
  load(id: string): string;
42
48
  }[];
49
+ server: {
50
+ fs: {
51
+ allow: string[];
52
+ };
53
+ };
43
54
  resolve: {
44
55
  alias: Record<string, string>;
56
+ dedupe: string[];
45
57
  };
46
58
  test: {
47
59
  name: string;
@@ -59,8 +71,14 @@ export declare function pluginVitestConfig(configUrl: string, options?: PluginVi
59
71
  enforce: "pre";
60
72
  load(id: string): string;
61
73
  }[];
74
+ server: {
75
+ fs: {
76
+ allow: string[];
77
+ };
78
+ };
62
79
  resolve: {
63
80
  alias: Record<string, string>;
81
+ dedupe: string[];
64
82
  };
65
83
  test: {
66
84
  name: string;
@@ -80,9 +80,18 @@ export function pluginVitestConfig(configUrl, options = {}) {
80
80
  // unexpectedly". Threads don't.
81
81
  const pool = "threads";
82
82
  const plugins = [wasmBinaryPlugin()];
83
+ // A local SDK linked into node_modules sits outside the plugin folder, so
84
+ // Vite must be allowed to read it, and React must come from the plugin.
85
+ const sdkDir = fs.realpathSync(fileURLToPath(new URL("..", import.meta.url)));
86
+ const server = { fs: { allow: [pluginDir, sdkDir] } };
87
+ const resolve = {
88
+ alias,
89
+ dedupe: ["react", "react-dom", "react-i18next", "i18next"],
90
+ };
83
91
  return {
84
92
  plugins,
85
- resolve: { alias },
93
+ server,
94
+ resolve,
86
95
  test: {
87
96
  globals: true,
88
97
  setupFiles: [setupFile],
@@ -90,7 +99,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
90
99
  projects: [
91
100
  {
92
101
  plugins,
93
- resolve: { alias },
102
+ server,
103
+ resolve,
94
104
  test: {
95
105
  name: `${pluginId}:backend`,
96
106
  root: pluginDir,
@@ -103,7 +113,8 @@ export function pluginVitestConfig(configUrl, options = {}) {
103
113
  },
104
114
  {
105
115
  plugins,
106
- resolve: { alias },
116
+ server,
117
+ resolve,
107
118
  test: {
108
119
  name: `${pluginId}:frontend`,
109
120
  root: pluginDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termix-ssh/plugin-sdk",
3
- "version": "1.0.11",
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": {