@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/host/ui.js CHANGED
@@ -61,8 +61,10 @@ import {
61
61
  hostActionsFor,
62
62
  hostBadgesFor,
63
63
  hostEditorSectionList,
64
+ hostEditorToolbarItemsFor,
64
65
  hostMenuItemsFor,
65
66
  hostProtocolFlags,
67
+ hostStatusIndicatorsFor,
66
68
  i18n_default,
67
69
  installPluginHostBridge,
68
70
  isCoreRailView,
@@ -139,8 +141,10 @@ import {
139
141
  useHostBadges,
140
142
  useHostContextMenuItems,
141
143
  useHostEditorSections,
144
+ useHostEditorToolbarItems,
142
145
  useHostProtocols,
143
146
  useHostStatus,
147
+ useHostStatusIndicators,
144
148
  useInstalledPlugins,
145
149
  useIsMobile,
146
150
  useKeybindingActions,
@@ -169,10 +173,10 @@ import {
169
173
  useThemes,
170
174
  verifySecondFactor,
171
175
  withProtocolSettings
172
- } from "./chunk-XKA3DBB2.js";
176
+ } from "./chunk-CALNJCHN.js";
173
177
  import {
174
178
  usePermissions
175
- } from "./chunk-VEWOYAMZ.js";
179
+ } from "./chunk-T2PZWXFJ.js";
176
180
  import {
177
181
  AddButton,
178
182
  Card,
@@ -193,7 +197,7 @@ import {
193
197
  isPrerelease,
194
198
  reportBetaFeedbackUrl,
195
199
  reportCoreIssueUrl
196
- } from "./chunk-X4QZKO5G.js";
200
+ } from "./chunk-54KCK6IZ.js";
197
201
  import {
198
202
  Action,
199
203
  Activity,
@@ -383,7 +387,7 @@ import {
383
387
  useSurfaceKind,
384
388
  useUiPreference,
385
389
  useUiPreferencesContext
386
- } from "./chunk-AWR6FYDO.js";
390
+ } from "./chunk-ERGN4YW6.js";
387
391
  import {
388
392
  CREDENTIAL_ROW_FIELD_KEYS,
389
393
  PRESETS,
@@ -404,7 +408,6 @@ import {
404
408
  assignRoleToUser,
405
409
  authApi,
406
410
  buildOriginWsUrl,
407
- bulkImportSSHHosts,
408
411
  bulkUpdateSSHHosts,
409
412
  changePassword,
410
413
  completePasswordReset,
@@ -426,11 +429,7 @@ import {
426
429
  deleteRole,
427
430
  deleteSSHHost,
428
431
  deleteUser,
429
- deployCredentialToHost,
430
432
  duplicateCredential,
431
- exportAllSSHHosts,
432
- generateKeyPair,
433
- generatePublicKeyFromPrivate,
434
433
  getActiveSessions,
435
434
  getApiKeys,
436
435
  getBackendUrl,
@@ -480,7 +479,6 @@ import {
480
479
  handleApiError,
481
480
  hostApi,
482
481
  hydrateLocalSharedHostAuth,
483
- importSSHConfigHosts,
484
482
  initiatePasswordReset,
485
483
  invalidateHostsAndStatusCaches,
486
484
  invalidateServerStatusCache,
@@ -529,7 +527,6 @@ import {
529
527
  sshHostApi,
530
528
  tabTypeForActivity,
531
529
  unlinkExternalFromPasswordAccount,
532
- updateBranding,
533
530
  updateCredential,
534
531
  updateExternalAutoProvision,
535
532
  updateFolderMetadata,
@@ -547,7 +544,7 @@ import {
547
544
  useTabs,
548
545
  useTabsSafe,
549
546
  verifyPasswordResetCode
550
- } from "./chunk-RUPLD6RF.js";
547
+ } from "./chunk-CN5UBTBP.js";
551
548
  import {
552
549
  define_import_meta_env_default,
553
550
  init_define_import_meta_env
@@ -32199,167 +32196,6 @@ function B2({ asChild: r, children: o }, n) {
32199
32196
  }
32200
32197
  var Te = { position: "absolute", width: "1px", height: "1px", padding: "0", margin: "-1px", overflow: "hidden", clip: "rect(0, 0, 0, 0)", whiteSpace: "nowrap", borderWidth: "0" };
32201
32198
 
32202
- // src/ui/sidebar/host-export-payload.ts
32203
- init_define_import_meta_env();
32204
- var FIELD_GROUP_KEYS = {
32205
- connection: [
32206
- "connectionType",
32207
- "name",
32208
- "ip",
32209
- "port",
32210
- "username",
32211
- "folder",
32212
- "domain",
32213
- "security",
32214
- "ignoreCert"
32215
- ],
32216
- notes: ["notes"],
32217
- tags: ["tags", "pin"],
32218
- proxy: [
32219
- "useSocks5",
32220
- "socks5Host",
32221
- "socks5Port",
32222
- "socks5Username",
32223
- "socks5ProxyChain"
32224
- ],
32225
- jumpHosts: ["jumpHosts"],
32226
- // Every plugin's host settings, enable switches included.
32227
- featureFlags: ["pluginSettings", "forceKeyboardInteractive"],
32228
- advanced: [
32229
- "statusCheckEnabled",
32230
- "statusCheckInterval",
32231
- "terminalConfig",
32232
- "sshOptions"
32233
- ]
32234
- };
32235
- var SECRET_KEYS = [
32236
- "password",
32237
- "key",
32238
- "keyPassword",
32239
- "sudoPassword",
32240
- "socks5Password"
32241
- ];
32242
- var CREDENTIAL_KEYS = [
32243
- ...SECRET_KEYS,
32244
- "authType",
32245
- "keyType",
32246
- "credentialAlias",
32247
- "credentialId",
32248
- "overrideCredentialUsername"
32249
- ];
32250
- var TUPLE_KEYS = ["name", "ip", "port", "username", "connectionType"];
32251
- function pluginSecretKeys(summaries) {
32252
- const result = {};
32253
- for (const summary of summaries) {
32254
- for (const field of summary.contributes?.settings?.host?.fields ?? []) {
32255
- if (!field.secretKeys?.length) continue;
32256
- (result[summary.id] ??= {})[field.key] = field.secretKeys;
32257
- }
32258
- }
32259
- return result;
32260
- }
32261
- var NESTED_SECRET_ARRAYS = [
32262
- { container: "socks5ProxyChain", field: "password" }
32263
- ];
32264
- function mapPluginSecrets(pluginSettings, secrets, replace2) {
32265
- if (!pluginSettings || typeof pluginSettings !== "object") {
32266
- return pluginSettings;
32267
- }
32268
- const perPlugin = {};
32269
- for (const [pluginId, values] of Object.entries(
32270
- pluginSettings
32271
- )) {
32272
- const copy = { ...values };
32273
- for (const [field, keys2] of Object.entries(secrets[pluginId] ?? {})) {
32274
- const blob = copy[field];
32275
- if (!blob || typeof blob !== "object") continue;
32276
- const record = { ...blob };
32277
- for (const key of keys2) {
32278
- if (key in record) record[key] = replace2(record[key]);
32279
- }
32280
- copy[field] = record;
32281
- }
32282
- perPlugin[pluginId] = copy;
32283
- }
32284
- return perPlugin;
32285
- }
32286
- function hostKey(host) {
32287
- return JSON.stringify(TUPLE_KEYS.map((k3) => String(host[k3] ?? "")));
32288
- }
32289
- function buildExportPayload(raw, selected, groups, withCredentials, pluginSecrets = {}) {
32290
- const allowed = /* @__PURE__ */ new Set([
32291
- "exportId",
32292
- ...CREDENTIAL_KEYS,
32293
- ...FIELD_GROUP_KEYS.connection
32294
- ]);
32295
- for (const group of groups) {
32296
- for (const key of FIELD_GROUP_KEYS[group]) allowed.add(key);
32297
- }
32298
- const hosts = (raw.hosts ?? []).filter((host) => selected === null || selected.has(hostKey(host))).map((host) => {
32299
- const shaped = {};
32300
- for (const [key, value] of Object.entries(host)) {
32301
- if (allowed.has(key)) shaped[key] = value;
32302
- }
32303
- if (!withCredentials) {
32304
- shaped.pluginSettings = mapPluginSecrets(
32305
- shaped.pluginSettings,
32306
- pluginSecrets,
32307
- () => null
32308
- );
32309
- for (const { container, field } of NESTED_SECRET_ARRAYS) {
32310
- const arr = shaped[container];
32311
- if (Array.isArray(arr)) {
32312
- shaped[container] = arr.map(
32313
- (entry) => entry && typeof entry === "object" ? { ...entry, [field]: null } : entry
32314
- );
32315
- }
32316
- }
32317
- }
32318
- return shaped;
32319
- });
32320
- const result = { ...raw, hosts };
32321
- if (raw.credentials) {
32322
- const used = new Set(
32323
- hosts.map((host) => host.credentialAlias).filter(Boolean)
32324
- );
32325
- result.credentials = raw.credentials.filter(
32326
- (entry) => used.has(entry.alias)
32327
- );
32328
- }
32329
- return result;
32330
- }
32331
- function maskSecrets(payload, pluginSecrets = {}) {
32332
- return {
32333
- ...payload,
32334
- hosts: payload.hosts.map((host) => {
32335
- const masked = { ...host };
32336
- for (const key of SECRET_KEYS) {
32337
- if (masked[key] !== void 0 && masked[key] !== null && masked[key] !== "") {
32338
- masked[key] = "<included>";
32339
- }
32340
- }
32341
- masked.pluginSettings = mapPluginSecrets(
32342
- masked.pluginSettings,
32343
- pluginSecrets,
32344
- (value) => value ? "<included>" : value
32345
- );
32346
- for (const { container, field } of NESTED_SECRET_ARRAYS) {
32347
- const arr = masked[container];
32348
- if (Array.isArray(arr)) {
32349
- masked[container] = arr.map((entry) => {
32350
- if (entry && typeof entry === "object") {
32351
- const record = entry;
32352
- if (record[field]) return { ...record, [field]: "<included>" };
32353
- }
32354
- return entry;
32355
- });
32356
- }
32357
- }
32358
- return masked;
32359
- })
32360
- };
32361
- }
32362
-
32363
32199
  // src/types/auth-protocols.ts
32364
32200
  init_define_import_meta_env();
32365
32201
  var SSH_AUTH_PROTOCOL = "ssh";
@@ -32428,25 +32264,6 @@ async function getAuditLogActions() {
32428
32264
  handleApiError(error, "fetch audit log actions");
32429
32265
  }
32430
32266
  }
32431
- async function getAuditForwarding() {
32432
- try {
32433
- const response = await authApi.get("/users/audit-forwarding");
32434
- return response.data;
32435
- } catch (error) {
32436
- throw handleApiError(error, "get audit forwarding settings");
32437
- }
32438
- }
32439
- async function updateAuditForwarding(url, token) {
32440
- try {
32441
- const response = await authApi.patch("/users/audit-forwarding", {
32442
- url,
32443
- token
32444
- });
32445
- return response.data;
32446
- } catch (error) {
32447
- throw handleApiError(error, "update audit forwarding settings");
32448
- }
32449
- }
32450
32267
 
32451
32268
  // src/ui/api/host-defaults-api.ts
32452
32269
  init_define_import_meta_env();
@@ -34226,6 +34043,7 @@ export {
34226
34043
  NumberField,
34227
34044
  OPEN_PLUGINS_EVENT,
34228
34045
  PANEL,
34046
+ PRESETS,
34229
34047
  PROMPT_BUTTON,
34230
34048
  PROMPT_DESTRUCTIVE_BUTTON,
34231
34049
  PROMPT_PRIMARY_BUTTON,
@@ -34323,9 +34141,7 @@ export {
34323
34141
  applyUiFont,
34324
34142
  assignRoleToUser,
34325
34143
  beginSplitDrag,
34326
- buildExportPayload,
34327
34144
  buildOriginWsUrl,
34328
- bulkImportSSHHosts,
34329
34145
  bulkUpdateSSHHosts,
34330
34146
  canConfigureUpdates,
34331
34147
  canJoinSplit,
@@ -34354,22 +34170,17 @@ export {
34354
34170
  deleteRole,
34355
34171
  deleteSSHHost,
34356
34172
  deleteUser,
34357
- deployCredentialToHost,
34358
34173
  desktopUpdates,
34359
34174
  docsUrl,
34360
34175
  duplicateCredential,
34361
34176
  enabledHostProtocols,
34362
34177
  endSplitDrag,
34363
- exportAllSSHHosts,
34364
34178
  filterPaletteItems,
34365
34179
  findCoreHostDefault,
34366
34180
  findMatchingKeybinding,
34367
- generateKeyPair,
34368
- generatePublicKeyFromPrivate,
34369
34181
  getActiveSessions,
34370
34182
  getAdaptiveResourceBudget,
34371
34183
  getApiKeys,
34372
- getAuditForwarding,
34373
34184
  getAuditLogActions,
34374
34185
  getAuditLogs,
34375
34186
  getBasePath,
@@ -34438,11 +34249,11 @@ export {
34438
34249
  hasVisibleFields,
34439
34250
  hostActionsFor,
34440
34251
  hostBadgesFor,
34441
- hostKey,
34252
+ hostEditorToolbarItemsFor,
34442
34253
  hostMenuItemsFor,
34443
34254
  hostProtocolFlags,
34255
+ hostStatusIndicatorsFor,
34444
34256
  hydrateLocalSharedHostAuth,
34445
- importSSHConfigHosts,
34446
34257
  invalidateHostsAndStatusCaches,
34447
34258
  invalidateServerStatusCache,
34448
34259
  isCoreRailView,
@@ -34467,7 +34278,6 @@ export {
34467
34278
  mapCredentials,
34468
34279
  markAdaptiveResourceUsed,
34469
34280
  markTabSurfaceUsed,
34470
- maskSecrets,
34471
34281
  moveRailItem,
34472
34282
  moveSplitDrag,
34473
34283
  normalizeLanguageCode,
@@ -34478,7 +34288,6 @@ export {
34478
34288
  parseServerTime,
34479
34289
  pluginKey,
34480
34290
  pluginName,
34481
- pluginSecretKeys,
34482
34291
  pluginWsUrl,
34483
34292
  preloadTabSurface,
34484
34293
  previewHostDefaultsLevel,
@@ -34551,8 +34360,6 @@ export {
34551
34360
  takePendingManageRequest,
34552
34361
  unlinkExternalFromPasswordAccount,
34553
34362
  unlinkServer,
34554
- updateAuditForwarding,
34555
- updateBranding,
34556
34363
  updateCredential,
34557
34364
  updateExternalAutoProvision,
34558
34365
  updateFolderMetadata,
@@ -34588,8 +34395,10 @@ export {
34588
34395
  useHostBadges,
34589
34396
  useHostContextMenuItems,
34590
34397
  useHostEditorSections,
34398
+ useHostEditorToolbarItems,
34591
34399
  useHostProtocols,
34592
34400
  useHostStatus,
34401
+ useHostStatusIndicators,
34593
34402
  useInstalledPlugins,
34594
34403
  useIsDefaultsEditor,
34595
34404
  useIsMobile,
@@ -7,7 +7,7 @@ import {
7
7
  resetPermissionsCache,
8
8
  setPermissionsForTesting,
9
9
  usePermissions
10
- } from "./chunk-VEWOYAMZ.js";
10
+ } from "./chunk-T2PZWXFJ.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.4.0";
23
+ export declare const PLUGIN_API_VERSION = "1.5.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. */
@@ -500,7 +500,16 @@ export interface PluginManifest {
500
500
  nativeDependencies?: string[];
501
501
  /** How a linked desktop treats this plugin. Defaults to "mirror". */
502
502
  desktop?: PluginDesktopMode;
503
+ /**
504
+ * "desktop": the plugin only loads on a desktop app's own embedded backend,
505
+ * never on a server. It implies desktop "local" and may declare desktop:local.
506
+ */
507
+ runtime?: PluginRuntime;
503
508
  }
509
+ export type PluginRuntime = "desktop";
510
+ export declare const PLUGIN_RUNTIMES: readonly PluginRuntime[];
511
+ /** The desktop mode a manifest resolves to. */
512
+ export declare function resolvePluginDesktopMode(m: Pick<PluginManifest, "desktop" | "runtime">): PluginDesktopMode;
504
513
  export declare const DEFAULT_BACKEND_ENTRY = "dist/backend.js";
505
514
  export declare const DEFAULT_FRONTEND_ENTRY = "dist/frontend.js";
506
515
  export declare const DEFAULT_LOCALES_DIR = "locales";
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.4.0";
25
+ export const PLUGIN_API_VERSION = "1.5.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. */
@@ -192,6 +192,11 @@ export const PLUGIN_DESKTOP_MODES = [
192
192
  "local",
193
193
  "server",
194
194
  ];
195
+ export const PLUGIN_RUNTIMES = ["desktop"];
196
+ /** The desktop mode a manifest resolves to. */
197
+ export function resolvePluginDesktopMode(m) {
198
+ return m.desktop ?? (m.runtime === "desktop" ? "local" : "mirror");
199
+ }
195
200
  export const DEFAULT_BACKEND_ENTRY = "dist/backend.js";
196
201
  export const DEFAULT_FRONTEND_ENTRY = "dist/frontend.js";
197
202
  export const DEFAULT_LOCALES_DIR = "locales";
@@ -227,6 +232,7 @@ const ALLOWED_TOP_LEVEL = new Set([
227
232
  "platforms",
228
233
  "nativeDependencies",
229
234
  "desktop",
235
+ "runtime",
230
236
  ]);
231
237
  const ALLOWED_CONTRIBUTES = new Set([
232
238
  "tabs",
@@ -365,6 +371,15 @@ export function validateManifest(manifest) {
365
371
  !PLUGIN_DESKTOP_MODES.includes(m.desktop)) {
366
372
  errors.push(`Field "desktop" must be one of: ${PLUGIN_DESKTOP_MODES.join(", ")}`);
367
373
  }
374
+ if (m.runtime !== undefined &&
375
+ !PLUGIN_RUNTIMES.includes(m.runtime)) {
376
+ errors.push(`Field "runtime" must be one of: ${PLUGIN_RUNTIMES.join(", ")}`);
377
+ }
378
+ if (m.runtime === "desktop" &&
379
+ m.desktop !== undefined &&
380
+ m.desktop !== "local") {
381
+ errors.push(`A "desktop" runtime plugin must use desktop "local"`);
382
+ }
368
383
  validateDependencyMap(m.dependencies, "dependencies", errors);
369
384
  validateDependencyMap(m.optionalDependencies, "optionalDependencies", errors);
370
385
  validateProvides(m.provides, errors);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * SSH key helpers for plugins that make, check or deploy keys: generate a key
3
+ * pair, tell what a key is, derive a public key and check a pair belongs
4
+ * together. Pure functions over ssh2, which the host supplies. Core keeps its
5
+ * own copy for the connect pipeline.
6
+ */
7
+ export type SshKeyKind = "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256";
8
+ export interface GeneratedSshKeyPair {
9
+ privateKey: string;
10
+ publicKey: string;
11
+ keyType: SshKeyKind;
12
+ }
13
+ export interface GenerateSshKeyPairOptions {
14
+ /** RSA only. 2048 to 8192, default 4096. */
15
+ bits?: number;
16
+ passphrase?: string;
17
+ comment?: string;
18
+ }
19
+ /** Makes a new key pair; the public half is one authorized_keys line. */
20
+ export declare function generateSshKeyPair(type: SshKeyKind, options?: GenerateSshKeyPairOptions): GeneratedSshKeyPair;
21
+ /** The key's algorithm, like "ssh-ed25519", or null when it cannot be read. */
22
+ export declare function detectSshKeyType(key: string, passphrase?: string): string | null;
23
+ /** The authorized_keys line for a private key, or null. */
24
+ export declare function publicKeyFromPrivate(privateKey: string, passphrase?: string): string | null;
25
+ /** Whether a public key line belongs to the private key. */
26
+ export declare function validateSshKeyPair(privateKey: string, publicKey: string, passphrase?: string): {
27
+ isValid: boolean;
28
+ error?: string;
29
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SSH key helpers for plugins that make, check or deploy keys: generate a key
3
+ * pair, tell what a key is, derive a public key and check a pair belongs
4
+ * together. Pure functions over ssh2, which the host supplies. Core keeps its
5
+ * own copy for the connect pipeline.
6
+ */
7
+ import ssh2 from "ssh2";
8
+ /** Makes a new key pair; the public half is one authorized_keys line. */
9
+ export function generateSshKeyPair(type, options = {}) {
10
+ const algorithm = type === "ssh-rsa" ? "rsa" : type === "ssh-ed25519" ? "ed25519" : "ecdsa";
11
+ if (type !== "ssh-rsa" &&
12
+ type !== "ssh-ed25519" &&
13
+ type !== "ecdsa-sha2-nistp256") {
14
+ throw new Error(`Unsupported key type: ${String(type)}`);
15
+ }
16
+ const generateOptions = {
17
+ comment: options.comment ?? "",
18
+ };
19
+ if (options.passphrase) {
20
+ generateOptions.passphrase = options.passphrase;
21
+ generateOptions.cipher = "aes256-ctr";
22
+ }
23
+ if (type === "ssh-rsa") {
24
+ const bits = options.bits ?? 4096;
25
+ if (!Number.isInteger(bits) || bits < 2048 || bits > 8192) {
26
+ throw new Error("RSA keys need 2048 to 8192 bits");
27
+ }
28
+ generateOptions.bits = bits;
29
+ }
30
+ if (type === "ecdsa-sha2-nistp256")
31
+ generateOptions.bits = 256;
32
+ const pair = ssh2.utils.generateKeyPairSync(algorithm, generateOptions);
33
+ return {
34
+ privateKey: pair.private,
35
+ publicKey: pair.public.trim(),
36
+ keyType: type,
37
+ };
38
+ }
39
+ function parse(key, passphrase) {
40
+ const parsed = ssh2.utils.parseKey(key.trim(), passphrase || undefined);
41
+ const first = Array.isArray(parsed) ? parsed[0] : parsed;
42
+ if (!first || first instanceof Error) {
43
+ throw first instanceof Error ? first : new Error("Not a readable SSH key");
44
+ }
45
+ return first;
46
+ }
47
+ /** The key's algorithm, like "ssh-ed25519", or null when it cannot be read. */
48
+ export function detectSshKeyType(key, passphrase) {
49
+ try {
50
+ return parse(key, passphrase).type;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /** The authorized_keys line for a private key, or null. */
57
+ export function publicKeyFromPrivate(privateKey, passphrase) {
58
+ try {
59
+ const parsed = parse(privateKey, passphrase);
60
+ return `${parsed.type} ${parsed.getPublicSSH().toString("base64")}`;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** Whether a public key line belongs to the private key. */
67
+ export function validateSshKeyPair(privateKey, publicKey, passphrase) {
68
+ try {
69
+ const priv = parse(privateKey, passphrase);
70
+ const pub = parse(publicKey);
71
+ const same = priv.getPublicSSH().equals(pub.getPublicSSH());
72
+ return same
73
+ ? { isValid: true }
74
+ : {
75
+ isValid: false,
76
+ error: "The public key does not match the private key",
77
+ };
78
+ }
79
+ catch (error) {
80
+ return {
81
+ isValid: false,
82
+ error: error instanceof Error ? error.message : String(error),
83
+ };
84
+ }
85
+ }
package/dist/testing.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * The vitest config these run under comes from @termix-ssh/plugin-sdk/vitest-preset.
9
9
  */
10
- import type { PluginContext } from "./backend.js";
10
+ import type { PluginAuditEntry, PluginContext, PluginDesktopLocal } from "./backend.js";
11
11
  import type { HostActionContribution, TermixApp } from "./frontend.js";
12
12
  import type { PluginManifest } from "./manifest.js";
13
13
  import type { PluginTableDefinition } from "./db.js";
@@ -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, PluginHostStatusEntry, PluginNotification, PluginNotificationChannel, PluginNotifyHub, PluginFetchInit, PluginBinarySpec, PluginTlsCertificateInfo, PluginTlsStatus, PluginSummary, PluginKeyboardInteractiveHandler, PluginProcessHandle, PluginProcessOptions } 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";
29
29
  /** A timer registered through ctx.schedule, run by hand with runScheduled. */
30
30
  export interface FakeScheduledJob {
31
31
  kind: "every" | "after";
@@ -42,6 +42,8 @@ export interface FakeContextOptions {
42
42
  settings?: Record<string, unknown>;
43
43
  /** Seed for the core settings ctx.settings.readCore can reach. */
44
44
  coreSettings?: Record<string, string>;
45
+ /** What ctx.files.dataDir() answers. Defaults to /tmp/<pluginId>. */
46
+ dataDir?: string;
45
47
  /** What ctx.ssh.connect and withConnection hand back as the client. */
46
48
  sshClient?: unknown;
47
49
  /** A real database behind ctx.db, usually createTestDb().database. */
@@ -75,8 +77,18 @@ export interface FakeContextOptions {
75
77
  sshKeyCredentials?: PluginSshKeyCredential[];
76
78
  /** What ctx.desktop.available() answers. Defaults to false. */
77
79
  desktopAvailable?: boolean;
80
+ /** What ctx.desktop.local is. Calls throw when this is not given. */
81
+ desktopLocal?: PluginDesktopLocal;
82
+ /** The stored audit lines ctx.audit.list and exportBatches serve. */
83
+ auditEntries?: PluginAuditEntry[];
78
84
  /** What ctx.hosts.status.get and check answer, by host id. */
79
85
  hostStatuses?: Record<number, PluginHostStatusEntry>;
86
+ /** What ctx.hosts.probeTargets answers. */
87
+ probeTargets?: PluginHostProbeTarget[];
88
+ /** Host ids ctx.hosts.sessionActive answers true for. */
89
+ activeSessions?: number[];
90
+ /** What ctx.network.tcpProbe and ctx.ssh.probe answer. Defaults to false. */
91
+ reachable?: (target: string | number, port?: number) => boolean;
80
92
  /** The acting user's channels, what ctx.notify.channels answers. */
81
93
  notificationChannels?: PluginNotificationChannel[];
82
94
  /**
@@ -164,6 +176,10 @@ export interface FakePluginContext {
164
176
  }) => Promise<Record<string, string>>;
165
177
  /** Everything the plugin registered through ctx.disposables. */
166
178
  disposals: Array<() => void | Promise<void>>;
179
+ /** Providers registered through ctx.hosts.status.provide. */
180
+ hostStatusProviders: PluginHostStatusProvider[];
181
+ /** Every provider passed to ctx.hosts.activity.provide that is still registered. */
182
+ activityProviders: PluginActivityProvider[];
167
183
  /** Topics emitted through ctx.events.emit, in order. */
168
184
  emitted: Array<{
169
185
  topic: string;
@@ -255,6 +271,8 @@ export interface FakePluginContext {
255
271
  challenges: Map<string, string>;
256
272
  renewers: number;
257
273
  };
274
+ /** Calls every ctx.audit.subscribe handler, like a line core just accepted. */
275
+ emitAudit: (entry: PluginAuditEntry) => void;
258
276
  /** Every ctx.audit.record entry, in order. */
259
277
  audits: Array<{
260
278
  action: string;
@@ -321,6 +339,8 @@ export interface MockContextOptions {
321
339
  settings?: Record<string, unknown>;
322
340
  /** Seed for the core settings readCore can reach. */
323
341
  coreSettings?: Record<string, string>;
342
+ /** What ctx.files.dataDir() answers. */
343
+ dataDir?: string;
324
344
  /** Acting user returned by currentActor and used by asUser. */
325
345
  actor?: string;
326
346
  /** What ctx.ssh.connect and withConnection hand back as the client. */
@@ -346,8 +366,18 @@ export interface MockContextOptions {
346
366
  sshKeyCredentials?: PluginSshKeyCredential[];
347
367
  /** What ctx.desktop.available() answers. */
348
368
  desktopAvailable?: boolean;
369
+ /** What ctx.desktop.local is. See FakeContextOptions. */
370
+ desktopLocal?: PluginDesktopLocal;
371
+ /** See FakeContextOptions. */
372
+ auditEntries?: PluginAuditEntry[];
349
373
  /** What ctx.hosts.status answers. See FakeContextOptions. */
350
374
  hostStatuses?: Record<number, PluginHostStatusEntry>;
375
+ /** What ctx.hosts.probeTargets answers. */
376
+ probeTargets?: PluginHostProbeTarget[];
377
+ /** Host ids ctx.hosts.sessionActive answers true for. */
378
+ activeSessions?: number[];
379
+ /** What ctx.network.tcpProbe and ctx.ssh.probe answer. */
380
+ reachable?: (target: string | number, port?: number) => boolean;
351
381
  /** The acting user's channels, what ctx.notify.channels answers. */
352
382
  notificationChannels?: PluginNotificationChannel[];
353
383
  /**
@@ -378,19 +408,6 @@ export interface MockPluginContext extends FakePluginContext {
378
408
  /** Capability ids checked during the test, in order, denied ones included. */
379
409
  checked: string[];
380
410
  }
381
- /**
382
- * A context double that gates on capabilities the way src/backend/plugins/ctx.ts
383
- * does, so a test can prove a plugin fails closed without a running server.
384
- *
385
- * The gates on today's ctx surface: kv:own on every ctx.kv call, db:own on
386
- * ctx.db, network:serve on ctx.http and ctx.ws, events:core on emitting a
387
- * topic outside the plugin's own namespace, settings:read-core on
388
- * ctx.settings.readCore, ssh:connect plus credentials:use on ctx.ssh,
389
- * notify:send on ctx.notify (notify:hub on serve), network:outbound on ctx.fetch, process:spawn on
390
- * ctx.process, system:tls on ctx.system, plugins:read on ctx.plugins, auth:provide on ctx.auth
391
- * (users:impersonate on issueUserToken), and users:read on ctx.users. Reading a plugin's own settings is deliberately
392
- * ungated. As the SDK grows a member, add its gate here in the same shape.
393
- */
394
411
  export declare function createMockCtx(options?: MockContextOptions): MockPluginContext;
395
412
  /** A sqlite handle as createTestDb exposes it. Structural, like better-sqlite3's. */
396
413
  export interface TestSqlite {