@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/PLUGIN-GUIDE.md +106 -59
  3. package/dist/backend.d.ts +534 -1
  4. package/dist/backend.js +7 -0
  5. package/dist/capabilities.js +7 -0
  6. package/dist/db.js +1 -0
  7. package/dist/desktop.d.ts +0 -243
  8. package/dist/frontend.d.ts +23 -2
  9. package/dist/host/{PluginsTab-EV4MSGY6.js → PluginsTab-S33WIIGC.js} +511 -178
  10. package/dist/host/{chunk-AWR6FYDO.js → chunk-2IV3VX7F.js} +5552 -5550
  11. package/dist/host/{chunk-RUPLD6RF.js → chunk-3L45NNT5.js} +29 -84
  12. package/dist/host/{chunk-VEWOYAMZ.js → chunk-57DJU2ZA.js} +1 -1
  13. package/dist/host/{chunk-XKA3DBB2.js → chunk-V44FCJBQ.js} +135 -54
  14. package/dist/host/{chunk-X4QZKO5G.js → chunk-VFTB2RDP.js} +54 -5
  15. package/dist/host/core-classes.json +1 -1
  16. package/dist/host/{linked-server-B4NEG4MA.js → linked-server-SCJH7DSL.js} +1 -1
  17. package/dist/host/{rbac-api-OUO3LUOC.js → rbac-api-KA5IPQQL.js} +1 -1
  18. package/dist/host/testing-host.js +4 -4
  19. package/dist/host/types/src/types/index.d.ts +10 -0
  20. package/dist/host/types/src/types/onboarding.d.ts +0 -2
  21. package/dist/host/types/src/types/ui-types.d.ts +3 -1
  22. package/dist/host/types/src/ui/api/audit-log-api.d.ts +0 -10
  23. package/dist/host/types/src/ui/api/auth-methods-api.d.ts +2 -0
  24. package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
  25. package/dist/host/types/src/ui/api/plugins-api.d.ts +4 -0
  26. package/dist/host/types/src/ui/api/settings-api.d.ts +6 -6
  27. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +6 -28
  28. package/dist/host/types/src/ui/main-axios.d.ts +3 -3
  29. package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
  30. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +6 -7
  31. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +20 -0
  32. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +3 -0
  33. package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +4 -1
  34. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
  35. package/dist/host/ui.js +36 -210
  36. package/dist/host/{use-permissions-6VSHHUMB.js → use-permissions-LLN6TLAL.js} +1 -1
  37. package/dist/manifest.d.ts +10 -1
  38. package/dist/manifest.js +16 -1
  39. package/dist/ssh-keys.d.ts +29 -0
  40. package/dist/ssh-keys.js +85 -0
  41. package/dist/testing.d.ts +52 -15
  42. package/dist/testing.js +279 -2
  43. package/dist/vitest-preset.js +23 -0
  44. package/package.json +5 -1
  45. package/schema/manifest.schema.json +12 -0
  46. package/dist/host/types/src/ui/sidebar/host-export-payload.d.ts +0 -30
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.7.0";
26
26
  /** The API major, the default engine.api for a new plugin. */
27
27
  export const SUPPORTED_PLUGIN_API_VERSION = String(semver.major(PLUGIN_API_VERSION));
28
28
  /** Whether this build's plugin API satisfies a manifest's engine.api. */
@@ -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, PluginHostLockProvider, PluginAuthPolicyProvider, PluginCredentialSummary, PluginHostShareGrant, PluginHostStatusProvider, PluginNotification, PluginNotificationChannel, PluginNotifyHub, PluginFetchInit, PluginBinarySpec, PluginTlsCertificateInfo, PluginTlsStatus, PluginSummary, PluginKeyboardInteractiveHandler, PluginProcessHandle, PluginProcessOptions } from "./backend.js";
29
29
  /** A timer registered through ctx.schedule, run by hand with runScheduled. */
30
30
  export interface FakeScheduledJob {
31
31
  kind: "every" | "after";
@@ -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. */
@@ -73,10 +75,22 @@ export interface FakeContextOptions {
73
75
  protocolTargets?: Record<string, PluginProtocolTarget>;
74
76
  /** What ctx.credentials.listSshKeys answers. */
75
77
  sshKeyCredentials?: PluginSshKeyCredential[];
78
+ /** What ctx.credentials.list answers. */
79
+ credentialSummaries?: PluginCredentialSummary[];
76
80
  /** What ctx.desktop.available() answers. Defaults to false. */
77
81
  desktopAvailable?: boolean;
82
+ /** What ctx.desktop.local is. Calls throw when this is not given. */
83
+ desktopLocal?: PluginDesktopLocal;
84
+ /** The stored audit lines ctx.audit.list and exportBatches serve. */
85
+ auditEntries?: PluginAuditEntry[];
78
86
  /** What ctx.hosts.status.get and check answer, by host id. */
79
87
  hostStatuses?: Record<number, PluginHostStatusEntry>;
88
+ /** What ctx.hosts.probeTargets answers. */
89
+ probeTargets?: PluginHostProbeTarget[];
90
+ /** Host ids ctx.hosts.sessionActive answers true for. */
91
+ activeSessions?: number[];
92
+ /** What ctx.network.tcpProbe and ctx.ssh.probe answer. Defaults to false. */
93
+ reachable?: (target: string | number, port?: number) => boolean;
80
94
  /** The acting user's channels, what ctx.notify.channels answers. */
81
95
  notificationChannels?: PluginNotificationChannel[];
82
96
  /**
@@ -110,6 +124,11 @@ export interface FakeContextOptions {
110
124
  baseUrl?: string;
111
125
  /** What ctx.auth.countLinkedUsers answers, by provider. Defaults to 0. */
112
126
  linkedUsers?: Record<string, number>;
127
+ /** Second factors from other plugins that ctx.auth.listSecondFactors adds. */
128
+ secondFactors?: {
129
+ id: string;
130
+ pluginId: string;
131
+ }[];
113
132
  /** Failures before ctx.auth.loginRateLimit locks a key. Defaults to 5. */
114
133
  loginAttemptLimit?: number;
115
134
  /** What ctx.users.getLocale answers, by user id. A missing user answers null. */
@@ -155,6 +174,8 @@ export interface FakeAuthRegistrations {
155
174
  purpose: string;
156
175
  ttlSeconds?: number;
157
176
  }>;
177
+ /** Providers passed to ctx.auth.policy.provide that are still registered. */
178
+ policies: PluginAuthPolicyProvider[];
158
179
  }
159
180
  export interface FakePluginContext {
160
181
  ctx: PluginContext;
@@ -164,6 +185,12 @@ export interface FakePluginContext {
164
185
  }) => Promise<Record<string, string>>;
165
186
  /** Everything the plugin registered through ctx.disposables. */
166
187
  disposals: Array<() => void | Promise<void>>;
188
+ /** Providers registered through ctx.hosts.status.provide. */
189
+ hostStatusProviders: PluginHostStatusProvider[];
190
+ /** Every provider passed to ctx.hosts.activity.provide that is still registered. */
191
+ activityProviders: PluginActivityProvider[];
192
+ /** Every provider passed to ctx.hosts.locks.provide that is still registered. */
193
+ hostLockProviders: PluginHostLockProvider[];
167
194
  /** Topics emitted through ctx.events.emit, in order. */
168
195
  emitted: Array<{
169
196
  topic: string;
@@ -199,6 +226,11 @@ export interface FakePluginContext {
199
226
  permissionLevel: string;
200
227
  durationHours?: number;
201
228
  }>;
229
+ /** Every ctx.hosts.setShares call, in order. */
230
+ hostShareSets: Array<{
231
+ hostId: number;
232
+ grants: PluginHostShareGrant[];
233
+ }>;
202
234
  /** Everything registered through ctx.auth. */
203
235
  auth: FakeAuthRegistrations;
204
236
  /** Every ctx.desktop.openIsolatedWindow call, in order. */
@@ -255,6 +287,8 @@ export interface FakePluginContext {
255
287
  challenges: Map<string, string>;
256
288
  renewers: number;
257
289
  };
290
+ /** Calls every ctx.audit.subscribe handler, like a line core just accepted. */
291
+ emitAudit: (entry: PluginAuditEntry) => void;
258
292
  /** Every ctx.audit.record entry, in order. */
259
293
  audits: Array<{
260
294
  action: string;
@@ -321,6 +355,8 @@ export interface MockContextOptions {
321
355
  settings?: Record<string, unknown>;
322
356
  /** Seed for the core settings readCore can reach. */
323
357
  coreSettings?: Record<string, string>;
358
+ /** What ctx.files.dataDir() answers. */
359
+ dataDir?: string;
324
360
  /** Acting user returned by currentActor and used by asUser. */
325
361
  actor?: string;
326
362
  /** What ctx.ssh.connect and withConnection hand back as the client. */
@@ -344,10 +380,22 @@ export interface MockContextOptions {
344
380
  protocolTargets?: Record<string, PluginProtocolTarget>;
345
381
  /** What ctx.credentials.listSshKeys serves. */
346
382
  sshKeyCredentials?: PluginSshKeyCredential[];
383
+ /** What ctx.credentials.list serves. */
384
+ credentialSummaries?: PluginCredentialSummary[];
347
385
  /** What ctx.desktop.available() answers. */
348
386
  desktopAvailable?: boolean;
387
+ /** What ctx.desktop.local is. See FakeContextOptions. */
388
+ desktopLocal?: PluginDesktopLocal;
389
+ /** See FakeContextOptions. */
390
+ auditEntries?: PluginAuditEntry[];
349
391
  /** What ctx.hosts.status answers. See FakeContextOptions. */
350
392
  hostStatuses?: Record<number, PluginHostStatusEntry>;
393
+ /** What ctx.hosts.probeTargets answers. */
394
+ probeTargets?: PluginHostProbeTarget[];
395
+ /** Host ids ctx.hosts.sessionActive answers true for. */
396
+ activeSessions?: number[];
397
+ /** What ctx.network.tcpProbe and ctx.ssh.probe answer. */
398
+ reachable?: (target: string | number, port?: number) => boolean;
351
399
  /** The acting user's channels, what ctx.notify.channels answers. */
352
400
  notificationChannels?: PluginNotificationChannel[];
353
401
  /**
@@ -372,25 +420,14 @@ export interface MockContextOptions {
372
420
  /** See FakeContextOptions. */
373
421
  loginAttemptLimit?: number;
374
422
  /** See FakeContextOptions. */
423
+ secondFactors?: FakeContextOptions["secondFactors"];
424
+ /** See FakeContextOptions. */
375
425
  locales?: Record<string, string>;
376
426
  }
377
427
  export interface MockPluginContext extends FakePluginContext {
378
428
  /** Capability ids checked during the test, in order, denied ones included. */
379
429
  checked: string[];
380
430
  }
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
431
  export declare function createMockCtx(options?: MockContextOptions): MockPluginContext;
395
432
  /** A sqlite handle as createTestDb exposes it. Structural, like better-sqlite3's. */
396
433
  export interface TestSqlite {