@termix-ssh/plugin-sdk 1.0.13 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/PLUGIN-GUIDE.md +104 -69
- package/dist/backend.d.ts +500 -1
- package/dist/backend.js +7 -0
- package/dist/capabilities.js +4 -0
- package/dist/frontend.d.ts +45 -0
- package/dist/host/{PluginsTab-OXQQ7M4I.js → PluginsTab-YAGYRBIE.js} +3 -3
- package/dist/host/{chunk-T2PZWXFJ.js → chunk-2MU6WNSX.js} +1 -1
- package/dist/host/{chunk-54KCK6IZ.js → chunk-446UEDSK.js} +6 -2
- package/dist/host/{chunk-CN5UBTBP.js → chunk-7LEJ3HYF.js} +49 -3
- package/dist/host/{chunk-CALNJCHN.js → chunk-J2AILBXM.js} +561 -400
- package/dist/host/{chunk-ERGN4YW6.js → chunk-LQKK2JJA.js} +5 -1
- package/dist/host/core-classes.json +1 -1
- package/dist/host/{linked-server-5VFO4XQB.js → linked-server-OUF3POT7.js} +5 -3
- package/dist/host/{rbac-api-5YMBJAO5.js → rbac-api-LZVENREG.js} +1 -1
- package/dist/host/testing-host.js +4 -4
- package/dist/host/types/src/types/index.d.ts +10 -0
- package/dist/host/types/src/types/onboarding.d.ts +0 -1
- package/dist/host/types/src/types/ui-types.d.ts +3 -1
- package/dist/host/types/src/ui/api/auth-methods-api.d.ts +2 -0
- package/dist/host/types/src/ui/api/settings-api.d.ts +6 -0
- package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +6 -0
- package/dist/host/types/src/ui/auth/Auth.d.ts +1 -0
- package/dist/host/types/src/ui/lib/linked-server.d.ts +2 -0
- package/dist/host/types/src/ui/main-axios.d.ts +2 -2
- package/dist/host/types/src/ui/plugin-host/NativeView.d.ts +19 -0
- package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +4 -8
- package/dist/host/types/src/ui/shell/CompanionFrame.d.ts +11 -0
- package/dist/host/types/src/ui/shell/companions.d.ts +63 -0
- package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +4 -1
- package/dist/host/ui.js +171 -820
- package/dist/host/{use-permissions-ISEDRY3J.js → use-permissions-BYCDS5PD.js} +1 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +1 -1
- package/dist/testing.d.ts +26 -2
- package/dist/testing.js +89 -1
- package/dist/vitest-preset.js +23 -0
- package/package.json +1 -1
- package/schema/manifest.schema.json +4 -0
- package/dist/host/types/src/ui/api/sync-api.d.ts +0 -93
- package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +0 -9
- package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +0 -4
- package/dist/host/types/src/ui/lib/sync-status.d.ts +0 -6
- package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +0 -12
- package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +0 -15
- package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +0 -3
package/dist/backend.d.ts
CHANGED
|
@@ -201,6 +201,346 @@ export interface SyncEntityRegistration {
|
|
|
201
201
|
*/
|
|
202
202
|
export interface PluginSync {
|
|
203
203
|
registerEntity: (entity: SyncEntityRegistration) => void;
|
|
204
|
+
/** Needs sync:provide. Takes over /sync/v2 and /sync/link. Removed on deactivate. */
|
|
205
|
+
provide: (provider: PluginSyncProvider) => () => void;
|
|
206
|
+
/** Needs sync:provide. The replica store, change feed and link registry. */
|
|
207
|
+
readonly host: PluginSyncHost;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The plugin that owns server and desktop sync. Core forwards every request
|
|
211
|
+
* under /sync to `handle` (an Express style handler) and answers 404 when no
|
|
212
|
+
* plugin provides it. Call `next()` to let core answer instead.
|
|
213
|
+
*/
|
|
214
|
+
export interface PluginSyncProvider {
|
|
215
|
+
handle: (req: unknown, res: unknown, next: (error?: unknown) => void) => void | Promise<void>;
|
|
216
|
+
}
|
|
217
|
+
/** A kind of data that syncs, as the sync provider sees it. */
|
|
218
|
+
export interface SyncEntityInfo {
|
|
219
|
+
type: string;
|
|
220
|
+
owner: string;
|
|
221
|
+
order: number;
|
|
222
|
+
readOnly: boolean;
|
|
223
|
+
/** Whether the row would show in a load even if it exists. */
|
|
224
|
+
covers?: (syncId: string) => boolean;
|
|
225
|
+
}
|
|
226
|
+
/** A row ready for the wire, with its content hash. */
|
|
227
|
+
export interface SyncWireRow {
|
|
228
|
+
syncId: string;
|
|
229
|
+
wire: SyncRow;
|
|
230
|
+
hash: string;
|
|
231
|
+
}
|
|
232
|
+
export interface SyncRecordInfo {
|
|
233
|
+
entityType: string;
|
|
234
|
+
syncId: string;
|
|
235
|
+
revision: number;
|
|
236
|
+
hash: string | null;
|
|
237
|
+
deleted: boolean;
|
|
238
|
+
error: string | null;
|
|
239
|
+
errorHash: string | null;
|
|
240
|
+
seq: number;
|
|
241
|
+
}
|
|
242
|
+
export interface SyncConflictInfo {
|
|
243
|
+
id: number;
|
|
244
|
+
entityType: string;
|
|
245
|
+
syncId: string;
|
|
246
|
+
localRow: string;
|
|
247
|
+
createdAt: string;
|
|
248
|
+
}
|
|
249
|
+
export interface SyncAccountInfo {
|
|
250
|
+
id: string;
|
|
251
|
+
username: string;
|
|
252
|
+
isAdmin: boolean;
|
|
253
|
+
active: boolean;
|
|
254
|
+
}
|
|
255
|
+
export interface SyncProxyHeader {
|
|
256
|
+
name: string;
|
|
257
|
+
value: string;
|
|
258
|
+
}
|
|
259
|
+
export interface SyncBasicAuth {
|
|
260
|
+
username: string;
|
|
261
|
+
password: string;
|
|
262
|
+
}
|
|
263
|
+
/** Extra per server connection options. Kept open so new ones need no migration. */
|
|
264
|
+
export type SyncLinkOptions = Record<string, unknown>;
|
|
265
|
+
/** One server a desktop is linked to, with a replica of its data. */
|
|
266
|
+
export interface SyncLinkInfo {
|
|
267
|
+
id: number;
|
|
268
|
+
/** The local user the replica belongs to. */
|
|
269
|
+
userId: string;
|
|
270
|
+
serverUrl: string;
|
|
271
|
+
serverName: string | null;
|
|
272
|
+
serverVersion: string | null;
|
|
273
|
+
sessionToken: string | null;
|
|
274
|
+
customHeaders: SyncProxyHeader[];
|
|
275
|
+
basicAuth: SyncBasicAuth | null;
|
|
276
|
+
allowInvalidCertificate: boolean;
|
|
277
|
+
remoteUserId: string | null;
|
|
278
|
+
remoteUsername: string | null;
|
|
279
|
+
account: {
|
|
280
|
+
remoteUserId?: string;
|
|
281
|
+
username?: string;
|
|
282
|
+
isAdmin?: boolean;
|
|
283
|
+
roles?: string[];
|
|
284
|
+
permissions?: string[];
|
|
285
|
+
} | null;
|
|
286
|
+
disabledTypes: string[];
|
|
287
|
+
/** Extra connection options for this server, kept open for new ones. */
|
|
288
|
+
options: SyncLinkOptions;
|
|
289
|
+
knownTypes: string[];
|
|
290
|
+
cursor: number;
|
|
291
|
+
status: "idle" | "syncing" | "offline" | "error" | "signed_out";
|
|
292
|
+
lastError: string | null;
|
|
293
|
+
linkedAt: string;
|
|
294
|
+
lastSyncAt: string | null;
|
|
295
|
+
}
|
|
296
|
+
export interface SyncLinkCreate {
|
|
297
|
+
userId: string;
|
|
298
|
+
serverUrl: string;
|
|
299
|
+
serverName?: string | null;
|
|
300
|
+
serverVersion?: string | null;
|
|
301
|
+
sessionToken: string;
|
|
302
|
+
customHeaders?: SyncProxyHeader[];
|
|
303
|
+
basicAuth?: SyncBasicAuth | null;
|
|
304
|
+
allowInvalidCertificate?: boolean;
|
|
305
|
+
remoteUserId?: string | null;
|
|
306
|
+
remoteUsername?: string | null;
|
|
307
|
+
}
|
|
308
|
+
export type SyncLinkPatch = Partial<Pick<SyncLinkInfo, "serverName" | "serverVersion" | "sessionToken" | "customHeaders" | "basicAuth" | "allowInvalidCertificate" | "remoteUserId" | "remoteUsername" | "account" | "disabledTypes" | "options" | "knownTypes" | "cursor" | "status" | "lastError" | "lastSyncAt">>;
|
|
309
|
+
export type SyncRemoteErrorKind = "offline" | "tls" | "proxy" | "basic_auth" | "signed_out" | "server";
|
|
310
|
+
export interface SyncRemoteError extends Error {
|
|
311
|
+
readonly kind: SyncRemoteErrorKind;
|
|
312
|
+
readonly status?: number;
|
|
313
|
+
}
|
|
314
|
+
export interface SyncRemoteTarget {
|
|
315
|
+
serverUrl: string;
|
|
316
|
+
sessionToken?: string | null;
|
|
317
|
+
customHeaders?: SyncProxyHeader[];
|
|
318
|
+
basicAuth?: SyncBasicAuth | null;
|
|
319
|
+
allowInvalidCertificate?: boolean;
|
|
320
|
+
}
|
|
321
|
+
export interface SyncRemoteRequest {
|
|
322
|
+
method?: string;
|
|
323
|
+
body?: unknown;
|
|
324
|
+
timeoutMs?: number;
|
|
325
|
+
signal?: AbortSignal;
|
|
326
|
+
}
|
|
327
|
+
export interface SyncPushOp {
|
|
328
|
+
entityType: string;
|
|
329
|
+
syncId: string;
|
|
330
|
+
baseRevision: number;
|
|
331
|
+
op: "upsert" | "delete";
|
|
332
|
+
row?: SyncRow;
|
|
333
|
+
}
|
|
334
|
+
export type SyncPushResult = {
|
|
335
|
+
syncId: string;
|
|
336
|
+
entityType: string;
|
|
337
|
+
status: "ok";
|
|
338
|
+
revision: number;
|
|
339
|
+
} | {
|
|
340
|
+
syncId: string;
|
|
341
|
+
entityType: string;
|
|
342
|
+
status: "conflict";
|
|
343
|
+
revision: number;
|
|
344
|
+
deleted: boolean;
|
|
345
|
+
row: SyncRow | null;
|
|
346
|
+
} | {
|
|
347
|
+
syncId: string;
|
|
348
|
+
entityType: string;
|
|
349
|
+
status: "rejected";
|
|
350
|
+
reason: string;
|
|
351
|
+
};
|
|
352
|
+
export interface SyncRemotePlugin {
|
|
353
|
+
id: string;
|
|
354
|
+
name: string;
|
|
355
|
+
version: string;
|
|
356
|
+
source: string;
|
|
357
|
+
artifact: boolean;
|
|
358
|
+
enabled: boolean;
|
|
359
|
+
active: boolean;
|
|
360
|
+
desktop: string;
|
|
361
|
+
syncEntities: string[];
|
|
362
|
+
granted: string[];
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* What the sync provider gets from core: the replica store, the change feed,
|
|
366
|
+
* the linked server registry, sessions and a few helpers. Core keeps all of
|
|
367
|
+
* it neutral; the provider decides when and how to use it. Needs sync:provide.
|
|
368
|
+
*/
|
|
369
|
+
export interface PluginSyncHost {
|
|
370
|
+
/** True on the desktop app's own backend. */
|
|
371
|
+
readonly embedded: boolean;
|
|
372
|
+
info: {
|
|
373
|
+
appName: () => Promise<string>;
|
|
374
|
+
version: () => string;
|
|
375
|
+
};
|
|
376
|
+
entities: {
|
|
377
|
+
list: () => SyncEntityInfo[];
|
|
378
|
+
get: (type: string) => SyncEntityInfo | undefined;
|
|
379
|
+
/** Registers core's own entities once. */
|
|
380
|
+
ensureCore: () => Promise<void>;
|
|
381
|
+
onChanged: (listener: () => void) => () => void;
|
|
382
|
+
onLocalWrite: (listener: () => void) => () => void;
|
|
383
|
+
};
|
|
384
|
+
/** Rows and bookkeeping of one local user's replica. */
|
|
385
|
+
replica: {
|
|
386
|
+
resolvers: (userId: string, onMissing?: () => void) => unknown;
|
|
387
|
+
loadRows: (type: string, userId: string, resolvers: unknown) => Promise<{
|
|
388
|
+
rows: Map<string, SyncWireRow>;
|
|
389
|
+
failed: Set<string>;
|
|
390
|
+
}>;
|
|
391
|
+
loadRow: (type: string, userId: string, syncId: string, resolvers: unknown) => Promise<SyncWireRow | null>;
|
|
392
|
+
writeRow: (type: string, userId: string, wire: SyncRow, resolvers: unknown) => Promise<void>;
|
|
393
|
+
deleteRow: (type: string, userId: string, syncId: string) => Promise<void>;
|
|
394
|
+
orderSelfReferences: (type: string, rows: SyncRow[]) => SyncRow[];
|
|
395
|
+
counts: (userId: string) => Promise<Record<string, number>>;
|
|
396
|
+
wipe: (userId: string) => Promise<void>;
|
|
397
|
+
regenerateSyncIds: (userId: string) => Promise<void>;
|
|
398
|
+
removeSharedCopies: (userId: string) => Promise<void>;
|
|
399
|
+
records: {
|
|
400
|
+
list: (userId: string) => Promise<Map<string, SyncRecordInfo>>;
|
|
401
|
+
put: (userId: string, type: string, syncId: string, values: {
|
|
402
|
+
revision: number;
|
|
403
|
+
hash: string | null;
|
|
404
|
+
deleted: boolean;
|
|
405
|
+
}) => Promise<void>;
|
|
406
|
+
setError: (userId: string, type: string, syncId: string, reason: string, hash: string) => Promise<void>;
|
|
407
|
+
clearErrors: (userId: string) => Promise<void>;
|
|
408
|
+
listErrors: (userId: string) => Promise<SyncRecordInfo[]>;
|
|
409
|
+
deleteForUser: (userId: string) => Promise<void>;
|
|
410
|
+
key: (type: string, syncId: string) => string;
|
|
411
|
+
};
|
|
412
|
+
conflicts: {
|
|
413
|
+
save: (userId: string, type: string, syncId: string, row: SyncRow, revision: number) => Promise<void>;
|
|
414
|
+
list: (userId: string) => Promise<SyncConflictInfo[]>;
|
|
415
|
+
get: (userId: string, id: number) => Promise<SyncConflictInfo | null>;
|
|
416
|
+
delete: (userId: string, id: number) => Promise<void>;
|
|
417
|
+
};
|
|
418
|
+
};
|
|
419
|
+
/** The server side: what changed for an account and what a desktop pushed. */
|
|
420
|
+
feed: {
|
|
421
|
+
withUserLock: <T>(userId: string, run: () => Promise<T>) => Promise<T>;
|
|
422
|
+
changes: (userId: string, cursor: number, limit: number, types?: string[]) => Promise<{
|
|
423
|
+
records: Array<{
|
|
424
|
+
seq: number;
|
|
425
|
+
entityType: string;
|
|
426
|
+
syncId: string;
|
|
427
|
+
revision: number;
|
|
428
|
+
deleted: boolean;
|
|
429
|
+
row: SyncRow | null;
|
|
430
|
+
}>;
|
|
431
|
+
hasMore: boolean;
|
|
432
|
+
nextCursor: number;
|
|
433
|
+
entityTypes: Array<{
|
|
434
|
+
type: string;
|
|
435
|
+
owner: string;
|
|
436
|
+
order: number;
|
|
437
|
+
readOnly: boolean;
|
|
438
|
+
}>;
|
|
439
|
+
}>;
|
|
440
|
+
parsePushOps: (body: unknown) => Promise<SyncPushOp[] | null>;
|
|
441
|
+
applyPush: (userId: string, ops: SyncPushOp[]) => Promise<SyncPushResult[]>;
|
|
442
|
+
/** Streams a change notice to the account's open event streams. */
|
|
443
|
+
listen: (userId: string, res: unknown) => Promise<() => void>;
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* The linked server registry, kept in core so it is there before plugins.
|
|
447
|
+
* One link per local user. Without a user, the active account is meant.
|
|
448
|
+
*/
|
|
449
|
+
links: {
|
|
450
|
+
list: () => Promise<SyncLinkInfo[]>;
|
|
451
|
+
get: (userId?: string) => Promise<SyncLinkInfo | null>;
|
|
452
|
+
create: (link: SyncLinkCreate) => Promise<SyncLinkInfo>;
|
|
453
|
+
update: (patch: SyncLinkPatch, userId?: string) => Promise<SyncLinkInfo | null>;
|
|
454
|
+
remove: (userId?: string) => Promise<void>;
|
|
455
|
+
};
|
|
456
|
+
/** Desktop accounts: the local user and the replica user of each server. */
|
|
457
|
+
accounts: {
|
|
458
|
+
list: () => Promise<SyncAccountInfo[]>;
|
|
459
|
+
/** A new empty account, ready to be linked to a server. */
|
|
460
|
+
create: () => Promise<string>;
|
|
461
|
+
/** Which account the app signs in as from now on. */
|
|
462
|
+
setActive: (userId: string) => Promise<void>;
|
|
463
|
+
activeId: () => Promise<string | null>;
|
|
464
|
+
/** Deletes an account and all its data. The last one cannot go. */
|
|
465
|
+
remove: (userId: string) => Promise<void>;
|
|
466
|
+
};
|
|
467
|
+
http: {
|
|
468
|
+
json: <T>(target: SyncRemoteTarget, pathname: string, request?: SyncRemoteRequest) => Promise<T>;
|
|
469
|
+
fetch: (target: SyncRemoteTarget, pathname: string, request?: SyncRemoteRequest) => Promise<{
|
|
470
|
+
ok: boolean;
|
|
471
|
+
status: number;
|
|
472
|
+
body: AsyncIterable<Uint8Array> | null;
|
|
473
|
+
}>;
|
|
474
|
+
normalizeServerUrl: (input: string) => string | null;
|
|
475
|
+
isRemoteError: (error: unknown) => error is SyncRemoteError;
|
|
476
|
+
};
|
|
477
|
+
sessions: {
|
|
478
|
+
/** A long lived session for a linked desktop. Shows in the account's sessions. */
|
|
479
|
+
issueDesktop: (userId: string, options: {
|
|
480
|
+
days: number;
|
|
481
|
+
deviceInfo: string;
|
|
482
|
+
}) => Promise<string>;
|
|
483
|
+
revoke: (sessionId: string) => Promise<void>;
|
|
484
|
+
find: (sessionId: string) => Promise<{
|
|
485
|
+
expiresAt: string;
|
|
486
|
+
deviceInfo: string | null;
|
|
487
|
+
} | null>;
|
|
488
|
+
findUser: (userId: string) => Promise<{
|
|
489
|
+
id: string;
|
|
490
|
+
username: string;
|
|
491
|
+
} | null>;
|
|
492
|
+
/** The user and session behind a token, or null when it is not valid. */
|
|
493
|
+
verify: (token: string) => Promise<{
|
|
494
|
+
userId: string;
|
|
495
|
+
sessionId?: string;
|
|
496
|
+
} | null>;
|
|
497
|
+
/** Sets the sign in cookie on a response, like a login does. A name sets another cookie. */
|
|
498
|
+
setCookie: (req: unknown, res: unknown, value: string, options?: {
|
|
499
|
+
name?: string;
|
|
500
|
+
days?: number;
|
|
501
|
+
}) => void;
|
|
502
|
+
/** Removes the sign in cookie, without ending the session behind it. */
|
|
503
|
+
clearCookie: (req: unknown, res: unknown, name?: string) => void;
|
|
504
|
+
/** Express middleware that signs the request in and sets userId and sessionId. */
|
|
505
|
+
middleware: () => unknown;
|
|
506
|
+
/** Express middleware that lets a request through only on a desktop. */
|
|
507
|
+
desktopOnly: () => unknown;
|
|
508
|
+
};
|
|
509
|
+
hosts: {
|
|
510
|
+
findForSync: (userId: string, syncId: string) => Promise<{
|
|
511
|
+
id: number;
|
|
512
|
+
name: string | null;
|
|
513
|
+
} | null>;
|
|
514
|
+
statusBySyncId: (userId: string) => Promise<Record<string, unknown>>;
|
|
515
|
+
};
|
|
516
|
+
plugins: {
|
|
517
|
+
listForDesktop: () => Promise<{
|
|
518
|
+
plugins: SyncRemotePlugin[];
|
|
519
|
+
registryKeys: unknown;
|
|
520
|
+
}>;
|
|
521
|
+
/** Sends a plugin archive; false when there is no such plugin to share. */
|
|
522
|
+
sendPackage: (pluginId: string, res: unknown) => Promise<boolean>;
|
|
523
|
+
mirror: (link: SyncLinkInfo) => Promise<void>;
|
|
524
|
+
lastRemote: () => SyncRemotePlugin[];
|
|
525
|
+
localNames: () => Map<string, string>;
|
|
526
|
+
};
|
|
527
|
+
desktop: {
|
|
528
|
+
/** Tells the app's main process how to reach the linked origin. */
|
|
529
|
+
applyProxy: (link: SyncLinkInfo | null) => Promise<void>;
|
|
530
|
+
deviceName: () => string;
|
|
531
|
+
};
|
|
532
|
+
alerts: {
|
|
533
|
+
send: (alert: {
|
|
534
|
+
title: string;
|
|
535
|
+
body: string;
|
|
536
|
+
severity: "info" | "warning" | "error";
|
|
537
|
+
category: string;
|
|
538
|
+
dedupeKey: string;
|
|
539
|
+
link?: {
|
|
540
|
+
tab: string;
|
|
541
|
+
};
|
|
542
|
+
}) => Promise<void>;
|
|
543
|
+
};
|
|
204
544
|
}
|
|
205
545
|
/**
|
|
206
546
|
* A plain key/value hand-off between plugins. `provide` and `revoke` only take
|
|
@@ -591,11 +931,40 @@ export interface PluginHostCreateInput {
|
|
|
591
931
|
sshOptions?: HostSshOptions | null;
|
|
592
932
|
/** Host settings by plugin id, the shape a Termix export carries. */
|
|
593
933
|
pluginSettings?: Record<string, Record<string, unknown>>;
|
|
934
|
+
/**
|
|
935
|
+
* Each plugin's own host switch (its manifest enableKey), by plugin id.
|
|
936
|
+
* A plugin that is not running is skipped and logged; one with no switch
|
|
937
|
+
* is refused.
|
|
938
|
+
*/
|
|
939
|
+
pluginEnable?: Record<string, boolean>;
|
|
940
|
+
/**
|
|
941
|
+
* Secrets, stored encrypted with the owner's data key and never returned
|
|
942
|
+
* in a PluginHostRecord. An empty string or null clears the stored value.
|
|
943
|
+
*/
|
|
944
|
+
password?: string | null;
|
|
945
|
+
key?: string | null;
|
|
946
|
+
keyPassword?: string | null;
|
|
947
|
+
keyType?: string | null;
|
|
948
|
+
sudoPassword?: string | null;
|
|
949
|
+
/**
|
|
950
|
+
* Logins for plugin protocols (contributes.protocols), keyed by protocol
|
|
951
|
+
* id. null removes that protocol's login. Unknown protocol ids are skipped.
|
|
952
|
+
*/
|
|
953
|
+
protocolAuth?: Record<string, PluginHostProtocolAuthInput | null>;
|
|
954
|
+
}
|
|
955
|
+
/** One protocol login on a host write. Absent keys keep what is stored. */
|
|
956
|
+
export interface PluginHostProtocolAuthInput {
|
|
957
|
+
authType?: "direct" | "credential" | "none";
|
|
958
|
+
credentialId?: number | null;
|
|
959
|
+
username?: string | null;
|
|
960
|
+
password?: string | null;
|
|
961
|
+
/** The protocol's declared credentialFields, secret ones included. */
|
|
962
|
+
fields?: Record<string, string>;
|
|
594
963
|
}
|
|
595
964
|
/** Fields a plugin may change on a host the acting user owns. */
|
|
596
965
|
export type PluginHostUpdateInput = Partial<PluginHostCreateInput>;
|
|
597
966
|
/** Every key PluginHostCreateInput and PluginHostUpdateInput accept. */
|
|
598
|
-
export declare const PLUGIN_HOST_INPUT_KEYS: readonly ["name", "ip", "port", "username", "authType", "credentialId", "overrideCredentialUsername", "connectionType", "tags", "folder", "pin", "notes", "jumpHosts", "enableSsh", "sshPort", "statusCheckEnabled", "statusCheckInterval", "forceKeyboardInteractive", "sshOptions", "pluginSettings"];
|
|
967
|
+
export declare const PLUGIN_HOST_INPUT_KEYS: readonly ["name", "ip", "port", "username", "authType", "credentialId", "overrideCredentialUsername", "connectionType", "tags", "folder", "pin", "notes", "jumpHosts", "enableSsh", "sshPort", "statusCheckEnabled", "statusCheckInterval", "forceKeyboardInteractive", "sshOptions", "pluginSettings", "pluginEnable", "password", "key", "keyPassword", "keyType", "sudoPassword", "protocolAuth"];
|
|
599
968
|
/** The same shape canAccessHost returns internally, without secrets. */
|
|
600
969
|
export interface PluginHostAccess {
|
|
601
970
|
hasAccess: boolean;
|
|
@@ -624,6 +993,31 @@ export interface PluginShareableRole {
|
|
|
624
993
|
name: string;
|
|
625
994
|
displayName: string | null;
|
|
626
995
|
}
|
|
996
|
+
/** One grant in ctx.hosts.setShares. */
|
|
997
|
+
export interface PluginHostShareGrant {
|
|
998
|
+
target: PluginShareTarget;
|
|
999
|
+
permissionLevel: PluginHostShareLevel;
|
|
1000
|
+
/** Omit for a grant that never expires. */
|
|
1001
|
+
durationHours?: number;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Hosts a plugin manages itself, for example from a config file. Core refuses
|
|
1005
|
+
* edits, deletes and share changes on a locked host from the UI, the API and
|
|
1006
|
+
* other plugins, and marks it in host reads so a layout can show it read only.
|
|
1007
|
+
* Pins and order stay with the user. Only the plugin that locks a host may
|
|
1008
|
+
* still write it.
|
|
1009
|
+
*/
|
|
1010
|
+
export interface PluginHostLockProvider {
|
|
1011
|
+
isLocked: (hostId: number) => boolean;
|
|
1012
|
+
/** A short line shown under the managed note, or null. */
|
|
1013
|
+
note: (hostId: number) => string | null;
|
|
1014
|
+
/** False blocks adding hosts by hand (editor, import, other plugins). */
|
|
1015
|
+
allowManualCreate: () => boolean;
|
|
1016
|
+
}
|
|
1017
|
+
export interface PluginHostLocksApi {
|
|
1018
|
+
/** Registers this plugin's locks. Needs hosts:lock. Removed on deactivate. */
|
|
1019
|
+
provide: (provider: PluginHostLockProvider) => () => void;
|
|
1020
|
+
}
|
|
627
1021
|
/**
|
|
628
1022
|
* Hosts the acting user can see, and the RBAC + sharing operations a plugin
|
|
629
1023
|
* needs to build a feature (like fleets) on top of hosts it does not own.
|
|
@@ -675,6 +1069,12 @@ export interface PluginHosts {
|
|
|
675
1069
|
* Refuses a host the caller does not hold "manage" on.
|
|
676
1070
|
*/
|
|
677
1071
|
share: (hostId: number, targets: PluginShareTarget[], permissionLevel: PluginHostShareLevel, durationHours?: number) => Promise<PluginHostShareResult>;
|
|
1072
|
+
/**
|
|
1073
|
+
* Replaces every grant on a host the acting user owns with this list.
|
|
1074
|
+
* Grants that stay keep their row and counters, missing ones are revoked.
|
|
1075
|
+
* Needs hosts:write.
|
|
1076
|
+
*/
|
|
1077
|
+
setShares: (hostId: number, grants: PluginHostShareGrant[]) => Promise<PluginHostShareResult>;
|
|
678
1078
|
/** Users the caller may pick as a share target, for building a picker UI. */
|
|
679
1079
|
listUsers: () => Promise<PluginShareableUser[]>;
|
|
680
1080
|
/** Non-system roles the caller may pick as a share target. */
|
|
@@ -736,6 +1136,19 @@ export interface PluginHosts {
|
|
|
736
1136
|
registerImportNormalizer: (normalizer: PluginHostImportNormalizer) => () => void;
|
|
737
1137
|
/** The recent activity list on the dashboard. */
|
|
738
1138
|
activity: PluginHostActivityApi;
|
|
1139
|
+
/** Read only hosts this plugin manages. */
|
|
1140
|
+
locks: PluginHostLocksApi;
|
|
1141
|
+
/** Folder rows for the acting user. Needs hosts:write. */
|
|
1142
|
+
folders: PluginHostFoldersApi;
|
|
1143
|
+
}
|
|
1144
|
+
export interface PluginHostFoldersApi {
|
|
1145
|
+
/** Creates the folder row if missing and sets its color and icon. */
|
|
1146
|
+
ensure: (name: string, style?: {
|
|
1147
|
+
color?: string | null;
|
|
1148
|
+
icon?: string | null;
|
|
1149
|
+
}) => Promise<void>;
|
|
1150
|
+
/** Removes the folder row and subfolders once no host is left in them. */
|
|
1151
|
+
removeEmpty: (name: string) => Promise<boolean>;
|
|
739
1152
|
}
|
|
740
1153
|
export interface PluginHostImportOptions {
|
|
741
1154
|
/** Update a host that matches on ip, port and username instead of skipping it. */
|
|
@@ -1074,7 +1487,26 @@ export interface PluginNetwork {
|
|
|
1074
1487
|
timeoutMs?: number;
|
|
1075
1488
|
}) => Promise<boolean>;
|
|
1076
1489
|
}
|
|
1490
|
+
/**
|
|
1491
|
+
* A way to reach some hosts other than a plain TCP connection. `claims` is
|
|
1492
|
+
* asked for every connection, so it must be quick. `open` returns a stream
|
|
1493
|
+
* the SSH client runs over, like a relayed socket.
|
|
1494
|
+
*/
|
|
1495
|
+
export interface PluginSshTransportProvider {
|
|
1496
|
+
claims: (hostId: number) => boolean;
|
|
1497
|
+
open: (host: {
|
|
1498
|
+
id: number;
|
|
1499
|
+
ip: string;
|
|
1500
|
+
port: number;
|
|
1501
|
+
}) => Promise<import("node:stream").Duplex>;
|
|
1502
|
+
}
|
|
1503
|
+
export interface PluginSshTransports {
|
|
1504
|
+
/** Needs ssh:transport. Removed on deactivate. */
|
|
1505
|
+
provide: (provider: PluginSshTransportProvider) => () => void;
|
|
1506
|
+
}
|
|
1077
1507
|
export interface PluginSsh {
|
|
1508
|
+
/** Other ways to reach hosts, used by every connection to them. */
|
|
1509
|
+
transports: PluginSshTransports;
|
|
1078
1510
|
/**
|
|
1079
1511
|
* Whether a TCP port answers through the host's jump host chain. Chains are
|
|
1080
1512
|
* shared and closed when idle. The acting user must be able to connect to
|
|
@@ -1416,6 +1848,52 @@ export interface PluginSecondFactor {
|
|
|
1416
1848
|
/** Removes the user's enrolment. Called by the admin reset. */
|
|
1417
1849
|
reset?: (userId: string) => Promise<void>;
|
|
1418
1850
|
}
|
|
1851
|
+
/** A login core is about to turn into a session. */
|
|
1852
|
+
export interface PluginAuthPolicyInput {
|
|
1853
|
+
userId: string;
|
|
1854
|
+
/** Login method id, `"proxy"` for a trusted proxy login. */
|
|
1855
|
+
methodId: string;
|
|
1856
|
+
/** The method signs in through an outside identity provider (SSO, LDAP). */
|
|
1857
|
+
external: boolean;
|
|
1858
|
+
deviceType: "web" | "desktop" | "mobile";
|
|
1859
|
+
/** The device passed a second factor before and was remembered. */
|
|
1860
|
+
trustedDevice: boolean;
|
|
1861
|
+
/** The method itself proved a second factor, like a verified passkey. */
|
|
1862
|
+
mfaSatisfied: boolean;
|
|
1863
|
+
/** Every second factor core has, and whether this user set it up. */
|
|
1864
|
+
secondFactors: {
|
|
1865
|
+
id: string;
|
|
1866
|
+
pluginId: string;
|
|
1867
|
+
enrolled: boolean;
|
|
1868
|
+
}[];
|
|
1869
|
+
}
|
|
1870
|
+
export interface PluginAuthPolicyDecision {
|
|
1871
|
+
/** False turns remember me off and ignores remembered devices. */
|
|
1872
|
+
allowRememberDevice?: boolean;
|
|
1873
|
+
/** Caps how long the session lasts. */
|
|
1874
|
+
maxSessionSeconds?: number;
|
|
1875
|
+
/** Ends the session after this many seconds without a request. */
|
|
1876
|
+
idleTimeoutSeconds?: number;
|
|
1877
|
+
/**
|
|
1878
|
+
* Requires a second factor even for users who have none: they get a
|
|
1879
|
+
* session that can only set one up. `"new-device"` skips it on a
|
|
1880
|
+
* remembered device.
|
|
1881
|
+
*/
|
|
1882
|
+
secondFactor?: "always" | "new-device";
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Login rules from a plugin. Core asks every provider and the strictest
|
|
1886
|
+
* answer wins. A provider that throws refuses the login.
|
|
1887
|
+
*/
|
|
1888
|
+
export interface PluginAuthPolicyProvider {
|
|
1889
|
+
evaluate: (input: PluginAuthPolicyInput) => PluginAuthPolicyDecision | Promise<PluginAuthPolicyDecision>;
|
|
1890
|
+
/** Whether the login page should offer remember me. */
|
|
1891
|
+
allowRememberDevice?: () => boolean;
|
|
1892
|
+
}
|
|
1893
|
+
export interface PluginAuthPolicyApi {
|
|
1894
|
+
/** Needs auth:policy. Removed on deactivate. */
|
|
1895
|
+
provide: (provider: PluginAuthPolicyProvider) => () => void;
|
|
1896
|
+
}
|
|
1419
1897
|
export interface PluginUserTokenOptions {
|
|
1420
1898
|
/** Why the token exists, kept in the audit log and on the session. */
|
|
1421
1899
|
purpose: string;
|
|
@@ -1494,6 +1972,13 @@ export interface PluginAuth {
|
|
|
1494
1972
|
* never outlive itself.
|
|
1495
1973
|
*/
|
|
1496
1974
|
issueUserToken: (userId: string, options: PluginUserTokenOptions) => Promise<PluginUserToken>;
|
|
1975
|
+
/** Login rules: idle timeout, session length, remember me, second factors. */
|
|
1976
|
+
policy: PluginAuthPolicyApi;
|
|
1977
|
+
/** Second factors from every running plugin. Needs no capability. */
|
|
1978
|
+
listSecondFactors: () => {
|
|
1979
|
+
id: string;
|
|
1980
|
+
pluginId: string;
|
|
1981
|
+
}[];
|
|
1497
1982
|
}
|
|
1498
1983
|
/** What a plugin may learn about a user. Needs users:read. */
|
|
1499
1984
|
export interface PluginUsers {
|
|
@@ -1727,6 +2212,14 @@ export interface PluginSshKeyCredential {
|
|
|
1727
2212
|
/** "<type> <base64>", derived from the private key when none was stored. Null when it cannot be. */
|
|
1728
2213
|
publicKey: string | null;
|
|
1729
2214
|
}
|
|
2215
|
+
/** A saved credential without its secrets. */
|
|
2216
|
+
export interface PluginCredentialSummary {
|
|
2217
|
+
id: number;
|
|
2218
|
+
name: string;
|
|
2219
|
+
username: string | null;
|
|
2220
|
+
authType: string;
|
|
2221
|
+
folder: string | null;
|
|
2222
|
+
}
|
|
1730
2223
|
/** A key pair to save as a new credential for the acting user. */
|
|
1731
2224
|
export interface PluginSshKeyCredentialInput {
|
|
1732
2225
|
name: string;
|
|
@@ -1747,6 +2240,12 @@ export interface PluginCredentials {
|
|
|
1747
2240
|
* Needs credentials:use and the core credentials.view permission. Audited.
|
|
1748
2241
|
*/
|
|
1749
2242
|
listSshKeys: () => Promise<PluginSshKeyCredential[]>;
|
|
2243
|
+
/**
|
|
2244
|
+
* The acting user's own credentials by name, with no secret material, so a
|
|
2245
|
+
* plugin can point a host at one. Needs credentials:use and the core
|
|
2246
|
+
* credentials.view permission. Audited.
|
|
2247
|
+
*/
|
|
2248
|
+
list: () => Promise<PluginCredentialSummary[]>;
|
|
1750
2249
|
/**
|
|
1751
2250
|
* Saves a key pair as a new encrypted credential owned by the acting user
|
|
1752
2251
|
* and returns its id. Needs credentials:write, the core credentials.create
|
package/dist/backend.js
CHANGED
|
@@ -65,6 +65,13 @@ export const PLUGIN_HOST_INPUT_KEYS = [
|
|
|
65
65
|
"forceKeyboardInteractive",
|
|
66
66
|
"sshOptions",
|
|
67
67
|
"pluginSettings",
|
|
68
|
+
"pluginEnable",
|
|
69
|
+
"password",
|
|
70
|
+
"key",
|
|
71
|
+
"keyPassword",
|
|
72
|
+
"keyType",
|
|
73
|
+
"sudoPassword",
|
|
74
|
+
"protocolAuth",
|
|
68
75
|
];
|
|
69
76
|
/** Thrown by ctx.ssh.startInteraction when no provider can start it. */
|
|
70
77
|
export class PluginSshInteractionError extends Error {
|
package/dist/capabilities.js
CHANGED
|
@@ -33,6 +33,9 @@ export const CAPABILITY_CATALOG = [
|
|
|
33
33
|
entry("users:impersonate", "high"),
|
|
34
34
|
entry("db:core-refs", "high"),
|
|
35
35
|
entry("auth:provide", "high"),
|
|
36
|
+
entry("auth:policy", "high"),
|
|
37
|
+
entry("sync:provide", "high"),
|
|
38
|
+
entry("ssh:transport", "high"),
|
|
36
39
|
entry("device:serial", "high"),
|
|
37
40
|
entry("notify:hub", "high"),
|
|
38
41
|
entry("ui:layout", "high"),
|
|
@@ -46,6 +49,7 @@ export const CAPABILITY_CATALOG = [
|
|
|
46
49
|
entry("network:probe", "medium"),
|
|
47
50
|
entry("hosts:status-provide", "medium"),
|
|
48
51
|
entry("hosts:activity-provide", "medium"),
|
|
52
|
+
entry("hosts:lock", "medium"),
|
|
49
53
|
entry("users:read", "medium"),
|
|
50
54
|
entry("events:core", "medium"),
|
|
51
55
|
entry("notify:send", "medium"),
|
package/dist/frontend.d.ts
CHANGED
|
@@ -773,6 +773,43 @@ export interface SettingsComponentProps {
|
|
|
773
773
|
setValue: (key: string, value: unknown) => void;
|
|
774
774
|
running: boolean;
|
|
775
775
|
}
|
|
776
|
+
/** What a companion starts with. Read from the plugin's own settings. */
|
|
777
|
+
export interface CompanionDefaults {
|
|
778
|
+
/** Open by itself the first time a matching tab shows. */
|
|
779
|
+
open?: boolean;
|
|
780
|
+
side?: "left" | "right" | "bottom";
|
|
781
|
+
/** Pixels of width, or height at the bottom. */
|
|
782
|
+
size?: number;
|
|
783
|
+
/** Show the edge button that opens and closes it. Default true. */
|
|
784
|
+
edgeButton?: boolean;
|
|
785
|
+
}
|
|
786
|
+
export interface CompanionProps {
|
|
787
|
+
/** The tab it sits beside. */
|
|
788
|
+
tab: PluginTabRecord;
|
|
789
|
+
host?: PluginHostRecord;
|
|
790
|
+
/** The host in the shape the connection APIs take. */
|
|
791
|
+
sshHost?: Record<string, unknown>;
|
|
792
|
+
/** The tab's surface handle (see TabHandle), read at call time. */
|
|
793
|
+
getHandle: () => TabHandle | null;
|
|
794
|
+
side: "left" | "right" | "bottom";
|
|
795
|
+
/** False while the tab is in the background or the panel is closed. */
|
|
796
|
+
isVisible: boolean;
|
|
797
|
+
close: () => void;
|
|
798
|
+
setSide: (side: "left" | "right" | "bottom") => void;
|
|
799
|
+
}
|
|
800
|
+
export interface CompanionOptions {
|
|
801
|
+
icon?: IconComponent;
|
|
802
|
+
titleKey?: string;
|
|
803
|
+
order?: number;
|
|
804
|
+
/** Tab types it applies to. Any tab with a host when empty. */
|
|
805
|
+
tabTypes?: string[];
|
|
806
|
+
when?: (tab: PluginTabRecord, host?: PluginHostRecord) => boolean;
|
|
807
|
+
/**
|
|
808
|
+
* A hook returning the starting state. It runs in the plugin's scope, so it
|
|
809
|
+
* can read the plugin's settings.
|
|
810
|
+
*/
|
|
811
|
+
useDefaults?: () => CompanionDefaults;
|
|
812
|
+
}
|
|
776
813
|
export type ActionHandler = (...args: never[]) => unknown;
|
|
777
814
|
export interface ActionSlotDefinition {
|
|
778
815
|
id: string;
|
|
@@ -1001,6 +1038,14 @@ export interface TermixApp extends TermixAppInfo {
|
|
|
1001
1038
|
registerRailItem: (item: RailItemContribution) => Disposer;
|
|
1002
1039
|
registerPanel: (id: string, component: ComponentType<PanelProps>, options?: PanelOptions) => Disposer;
|
|
1003
1040
|
registerTab: (type: string, component: ComponentType<TabProps>, options?: TabOptions) => Disposer;
|
|
1041
|
+
/**
|
|
1042
|
+
* A panel that sits beside any host tab the options match (a terminal, a
|
|
1043
|
+
* desktop, ...), bound to that tab's host. The user opens, resizes and moves
|
|
1044
|
+
* it; the tab keeps running underneath.
|
|
1045
|
+
*/
|
|
1046
|
+
registerCompanion: (id: string, component: ComponentType<CompanionProps>, options?: CompanionOptions) => Disposer;
|
|
1047
|
+
/** Opens a companion on a tab, or closes it when it is showing. */
|
|
1048
|
+
toggleCompanion: (tab: PluginTabRecord, id?: string) => void;
|
|
1004
1049
|
registerHostEditorSection: (section: HostEditorSectionContribution) => Disposer;
|
|
1005
1050
|
registerHostAction: (action: HostActionContribution) => Disposer;
|
|
1006
1051
|
registerHostProtocol: (protocol: HostProtocolContribution) => Disposer;
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
sortPlugins,
|
|
30
30
|
uploadEntry,
|
|
31
31
|
youtubeEmbedUrl
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-446UEDSK.js";
|
|
33
33
|
import {
|
|
34
34
|
ArrowDownWideNarrow,
|
|
35
35
|
BookOpen,
|
|
@@ -107,8 +107,8 @@ import {
|
|
|
107
107
|
updatePlugin,
|
|
108
108
|
uploadPlugin,
|
|
109
109
|
useConfirm
|
|
110
|
-
} from "./chunk-
|
|
111
|
-
import "./chunk-
|
|
110
|
+
} from "./chunk-LQKK2JJA.js";
|
|
111
|
+
import "./chunk-7LEJ3HYF.js";
|
|
112
112
|
import {
|
|
113
113
|
__toESM,
|
|
114
114
|
define_import_meta_env_default,
|