@termix-ssh/plugin-sdk 1.0.15 → 1.0.18

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 (37) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/PLUGIN-GUIDE.md +92 -72
  3. package/dist/backend.d.ts +381 -0
  4. package/dist/capabilities.js +2 -0
  5. package/dist/frontend.d.ts +45 -0
  6. package/dist/host/{PluginsTab-S33WIIGC.js → PluginsTab-YAGYRBIE.js} +3 -3
  7. package/dist/host/{chunk-57DJU2ZA.js → chunk-2MU6WNSX.js} +1 -1
  8. package/dist/host/{chunk-VFTB2RDP.js → chunk-446UEDSK.js} +4 -2
  9. package/dist/host/{chunk-3L45NNT5.js → chunk-7LEJ3HYF.js} +20 -3
  10. package/dist/host/{chunk-V44FCJBQ.js → chunk-J2AILBXM.js} +540 -400
  11. package/dist/host/{chunk-2IV3VX7F.js → chunk-LQKK2JJA.js} +5 -1
  12. package/dist/host/core-classes.json +1 -1
  13. package/dist/host/{linked-server-SCJH7DSL.js → linked-server-OUF3POT7.js} +5 -3
  14. package/dist/host/{rbac-api-KA5IPQQL.js → rbac-api-LZVENREG.js} +1 -1
  15. package/dist/host/testing-host.js +4 -4
  16. package/dist/host/types/src/types/onboarding.d.ts +0 -1
  17. package/dist/host/types/src/ui/auth/Auth.d.ts +1 -0
  18. package/dist/host/types/src/ui/lib/linked-server.d.ts +2 -0
  19. package/dist/host/types/src/ui/plugin-host/NativeView.d.ts +19 -0
  20. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +2 -6
  21. package/dist/host/types/src/ui/shell/CompanionFrame.d.ts +11 -0
  22. package/dist/host/types/src/ui/shell/companions.d.ts +63 -0
  23. package/dist/host/ui.js +149 -815
  24. package/dist/host/{use-permissions-LLN6TLAL.js → use-permissions-BYCDS5PD.js} +1 -1
  25. package/dist/manifest.d.ts +1 -1
  26. package/dist/manifest.js +1 -1
  27. package/dist/testing.d.ts +5 -1
  28. package/dist/testing.js +28 -0
  29. package/package.json +1 -1
  30. package/schema/manifest.schema.json +2 -0
  31. package/dist/host/types/src/ui/api/sync-api.d.ts +0 -93
  32. package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +0 -9
  33. package/dist/host/types/src/ui/hooks/use-sync-status.d.ts +0 -4
  34. package/dist/host/types/src/ui/lib/sync-status.d.ts +0 -6
  35. package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +0 -12
  36. package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +0 -15
  37. package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +0 -3
package/dist/backend.d.ts CHANGED
@@ -201,6 +201,357 @@ 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
+ /** A client certificate for a server that asks for mutual TLS. */
264
+ export interface SyncClientCert {
265
+ /** Base64 of the PFX/P12 file. */
266
+ pfx: string;
267
+ passphrase: string;
268
+ /** The file name, to show which certificate is saved. */
269
+ name: string | null;
270
+ }
271
+ /** Extra per server connection options. Kept open so new ones need no migration. */
272
+ export type SyncLinkOptions = Record<string, unknown>;
273
+ /** One server a desktop is linked to, with a replica of its data. */
274
+ export interface SyncLinkInfo {
275
+ id: number;
276
+ /** The local user the replica belongs to. */
277
+ userId: string;
278
+ serverUrl: string;
279
+ serverName: string | null;
280
+ serverVersion: string | null;
281
+ sessionToken: string | null;
282
+ customHeaders: SyncProxyHeader[];
283
+ basicAuth: SyncBasicAuth | null;
284
+ allowInvalidCertificate: boolean;
285
+ clientCert: SyncClientCert | null;
286
+ remoteUserId: string | null;
287
+ remoteUsername: string | null;
288
+ account: {
289
+ remoteUserId?: string;
290
+ username?: string;
291
+ isAdmin?: boolean;
292
+ roles?: string[];
293
+ permissions?: string[];
294
+ } | null;
295
+ disabledTypes: string[];
296
+ /** Extra connection options for this server, kept open for new ones. */
297
+ options: SyncLinkOptions;
298
+ knownTypes: string[];
299
+ cursor: number;
300
+ status: "idle" | "syncing" | "offline" | "error" | "signed_out";
301
+ lastError: string | null;
302
+ linkedAt: string;
303
+ lastSyncAt: string | null;
304
+ }
305
+ export interface SyncLinkCreate {
306
+ userId: string;
307
+ serverUrl: string;
308
+ serverName?: string | null;
309
+ serverVersion?: string | null;
310
+ sessionToken: string;
311
+ customHeaders?: SyncProxyHeader[];
312
+ basicAuth?: SyncBasicAuth | null;
313
+ allowInvalidCertificate?: boolean;
314
+ clientCert?: SyncClientCert | null;
315
+ remoteUserId?: string | null;
316
+ remoteUsername?: string | null;
317
+ }
318
+ export type SyncLinkPatch = Partial<Pick<SyncLinkInfo, "serverName" | "serverVersion" | "sessionToken" | "customHeaders" | "basicAuth" | "allowInvalidCertificate" | "clientCert" | "remoteUserId" | "remoteUsername" | "account" | "disabledTypes" | "options" | "knownTypes" | "cursor" | "status" | "lastError" | "lastSyncAt">>;
319
+ export type SyncRemoteErrorKind = "offline" | "tls" | "client_cert" | "proxy" | "basic_auth" | "signed_out" | "server";
320
+ export interface SyncRemoteError extends Error {
321
+ readonly kind: SyncRemoteErrorKind;
322
+ readonly status?: number;
323
+ }
324
+ export interface SyncRemoteTarget {
325
+ serverUrl: string;
326
+ sessionToken?: string | null;
327
+ customHeaders?: SyncProxyHeader[];
328
+ basicAuth?: SyncBasicAuth | null;
329
+ allowInvalidCertificate?: boolean;
330
+ clientCert?: SyncClientCert | null;
331
+ }
332
+ export interface SyncRemoteRequest {
333
+ method?: string;
334
+ body?: unknown;
335
+ timeoutMs?: number;
336
+ signal?: AbortSignal;
337
+ }
338
+ export interface SyncPushOp {
339
+ entityType: string;
340
+ syncId: string;
341
+ baseRevision: number;
342
+ op: "upsert" | "delete";
343
+ row?: SyncRow;
344
+ }
345
+ export type SyncPushResult = {
346
+ syncId: string;
347
+ entityType: string;
348
+ status: "ok";
349
+ revision: number;
350
+ } | {
351
+ syncId: string;
352
+ entityType: string;
353
+ status: "conflict";
354
+ revision: number;
355
+ deleted: boolean;
356
+ row: SyncRow | null;
357
+ } | {
358
+ syncId: string;
359
+ entityType: string;
360
+ status: "rejected";
361
+ reason: string;
362
+ };
363
+ export interface SyncRemotePlugin {
364
+ id: string;
365
+ name: string;
366
+ version: string;
367
+ source: string;
368
+ artifact: boolean;
369
+ enabled: boolean;
370
+ active: boolean;
371
+ desktop: string;
372
+ syncEntities: string[];
373
+ granted: string[];
374
+ }
375
+ /**
376
+ * What the sync provider gets from core: the replica store, the change feed,
377
+ * the linked server registry, sessions and a few helpers. Core keeps all of
378
+ * it neutral; the provider decides when and how to use it. Needs sync:provide.
379
+ */
380
+ export interface PluginSyncHost {
381
+ /** True on the desktop app's own backend. */
382
+ readonly embedded: boolean;
383
+ info: {
384
+ appName: () => Promise<string>;
385
+ version: () => string;
386
+ };
387
+ entities: {
388
+ list: () => SyncEntityInfo[];
389
+ get: (type: string) => SyncEntityInfo | undefined;
390
+ /** Registers core's own entities once. */
391
+ ensureCore: () => Promise<void>;
392
+ onChanged: (listener: () => void) => () => void;
393
+ onLocalWrite: (listener: () => void) => () => void;
394
+ };
395
+ /** Rows and bookkeeping of one local user's replica. */
396
+ replica: {
397
+ resolvers: (userId: string, onMissing?: () => void) => unknown;
398
+ loadRows: (type: string, userId: string, resolvers: unknown) => Promise<{
399
+ rows: Map<string, SyncWireRow>;
400
+ failed: Set<string>;
401
+ }>;
402
+ loadRow: (type: string, userId: string, syncId: string, resolvers: unknown) => Promise<SyncWireRow | null>;
403
+ writeRow: (type: string, userId: string, wire: SyncRow, resolvers: unknown) => Promise<void>;
404
+ deleteRow: (type: string, userId: string, syncId: string) => Promise<void>;
405
+ orderSelfReferences: (type: string, rows: SyncRow[]) => SyncRow[];
406
+ counts: (userId: string) => Promise<Record<string, number>>;
407
+ wipe: (userId: string) => Promise<void>;
408
+ regenerateSyncIds: (userId: string) => Promise<void>;
409
+ removeSharedCopies: (userId: string) => Promise<void>;
410
+ records: {
411
+ list: (userId: string) => Promise<Map<string, SyncRecordInfo>>;
412
+ put: (userId: string, type: string, syncId: string, values: {
413
+ revision: number;
414
+ hash: string | null;
415
+ deleted: boolean;
416
+ }) => Promise<void>;
417
+ setError: (userId: string, type: string, syncId: string, reason: string, hash: string) => Promise<void>;
418
+ clearErrors: (userId: string) => Promise<void>;
419
+ listErrors: (userId: string) => Promise<SyncRecordInfo[]>;
420
+ deleteForUser: (userId: string) => Promise<void>;
421
+ key: (type: string, syncId: string) => string;
422
+ };
423
+ conflicts: {
424
+ save: (userId: string, type: string, syncId: string, row: SyncRow, revision: number) => Promise<void>;
425
+ list: (userId: string) => Promise<SyncConflictInfo[]>;
426
+ get: (userId: string, id: number) => Promise<SyncConflictInfo | null>;
427
+ delete: (userId: string, id: number) => Promise<void>;
428
+ };
429
+ };
430
+ /** The server side: what changed for an account and what a desktop pushed. */
431
+ feed: {
432
+ withUserLock: <T>(userId: string, run: () => Promise<T>) => Promise<T>;
433
+ changes: (userId: string, cursor: number, limit: number, types?: string[]) => Promise<{
434
+ records: Array<{
435
+ seq: number;
436
+ entityType: string;
437
+ syncId: string;
438
+ revision: number;
439
+ deleted: boolean;
440
+ row: SyncRow | null;
441
+ }>;
442
+ hasMore: boolean;
443
+ nextCursor: number;
444
+ entityTypes: Array<{
445
+ type: string;
446
+ owner: string;
447
+ order: number;
448
+ readOnly: boolean;
449
+ }>;
450
+ }>;
451
+ parsePushOps: (body: unknown) => Promise<SyncPushOp[] | null>;
452
+ applyPush: (userId: string, ops: SyncPushOp[]) => Promise<SyncPushResult[]>;
453
+ /** Streams a change notice to the account's open event streams. */
454
+ listen: (userId: string, res: unknown) => Promise<() => void>;
455
+ };
456
+ /**
457
+ * The linked server registry, kept in core so it is there before plugins.
458
+ * One link per local user. Without a user, the active account is meant.
459
+ */
460
+ links: {
461
+ list: () => Promise<SyncLinkInfo[]>;
462
+ get: (userId?: string) => Promise<SyncLinkInfo | null>;
463
+ create: (link: SyncLinkCreate) => Promise<SyncLinkInfo>;
464
+ update: (patch: SyncLinkPatch, userId?: string) => Promise<SyncLinkInfo | null>;
465
+ remove: (userId?: string) => Promise<void>;
466
+ };
467
+ /** Desktop accounts: the local user and the replica user of each server. */
468
+ accounts: {
469
+ list: () => Promise<SyncAccountInfo[]>;
470
+ /** A new empty account, ready to be linked to a server. */
471
+ create: () => Promise<string>;
472
+ /** Which account the app signs in as from now on. */
473
+ setActive: (userId: string) => Promise<void>;
474
+ activeId: () => Promise<string | null>;
475
+ /** Deletes an account and all its data. The last one cannot go. */
476
+ remove: (userId: string) => Promise<void>;
477
+ };
478
+ http: {
479
+ json: <T>(target: SyncRemoteTarget, pathname: string, request?: SyncRemoteRequest) => Promise<T>;
480
+ fetch: (target: SyncRemoteTarget, pathname: string, request?: SyncRemoteRequest) => Promise<{
481
+ ok: boolean;
482
+ status: number;
483
+ body: AsyncIterable<Uint8Array> | null;
484
+ }>;
485
+ normalizeServerUrl: (input: string) => string | null;
486
+ isRemoteError: (error: unknown) => error is SyncRemoteError;
487
+ };
488
+ sessions: {
489
+ /** A long lived session for a linked desktop. Shows in the account's sessions. */
490
+ issueDesktop: (userId: string, options: {
491
+ days: number;
492
+ deviceInfo: string;
493
+ }) => Promise<string>;
494
+ revoke: (sessionId: string) => Promise<void>;
495
+ find: (sessionId: string) => Promise<{
496
+ expiresAt: string;
497
+ deviceInfo: string | null;
498
+ } | null>;
499
+ findUser: (userId: string) => Promise<{
500
+ id: string;
501
+ username: string;
502
+ } | null>;
503
+ /** The user and session behind a token, or null when it is not valid. */
504
+ verify: (token: string) => Promise<{
505
+ userId: string;
506
+ sessionId?: string;
507
+ } | null>;
508
+ /** Sets the sign in cookie on a response, like a login does. A name sets another cookie. */
509
+ setCookie: (req: unknown, res: unknown, value: string, options?: {
510
+ name?: string;
511
+ days?: number;
512
+ }) => void;
513
+ /** Removes the sign in cookie, without ending the session behind it. */
514
+ clearCookie: (req: unknown, res: unknown, name?: string) => void;
515
+ /** Express middleware that signs the request in and sets userId and sessionId. */
516
+ middleware: () => unknown;
517
+ /** Express middleware that lets a request through only on a desktop. */
518
+ desktopOnly: () => unknown;
519
+ };
520
+ hosts: {
521
+ findForSync: (userId: string, syncId: string) => Promise<{
522
+ id: number;
523
+ name: string | null;
524
+ } | null>;
525
+ statusBySyncId: (userId: string) => Promise<Record<string, unknown>>;
526
+ };
527
+ plugins: {
528
+ listForDesktop: () => Promise<{
529
+ plugins: SyncRemotePlugin[];
530
+ registryKeys: unknown;
531
+ }>;
532
+ /** Sends a plugin archive; false when there is no such plugin to share. */
533
+ sendPackage: (pluginId: string, res: unknown) => Promise<boolean>;
534
+ mirror: (link: SyncLinkInfo) => Promise<void>;
535
+ lastRemote: () => SyncRemotePlugin[];
536
+ localNames: () => Map<string, string>;
537
+ };
538
+ desktop: {
539
+ /** Tells the app's main process how to reach the linked origin. */
540
+ applyProxy: (link: SyncLinkInfo | null) => Promise<void>;
541
+ deviceName: () => string;
542
+ };
543
+ alerts: {
544
+ send: (alert: {
545
+ title: string;
546
+ body: string;
547
+ severity: "info" | "warning" | "error";
548
+ category: string;
549
+ dedupeKey: string;
550
+ link?: {
551
+ tab: string;
552
+ };
553
+ }) => Promise<void>;
554
+ };
204
555
  }
205
556
  /**
206
557
  * A plain key/value hand-off between plugins. `provide` and `revoke` only take
@@ -798,6 +1149,17 @@ export interface PluginHosts {
798
1149
  activity: PluginHostActivityApi;
799
1150
  /** Read only hosts this plugin manages. */
800
1151
  locks: PluginHostLocksApi;
1152
+ /** Folder rows for the acting user. Needs hosts:write. */
1153
+ folders: PluginHostFoldersApi;
1154
+ }
1155
+ export interface PluginHostFoldersApi {
1156
+ /** Creates the folder row if missing and sets its color and icon. */
1157
+ ensure: (name: string, style?: {
1158
+ color?: string | null;
1159
+ icon?: string | null;
1160
+ }) => Promise<void>;
1161
+ /** Removes the folder row and subfolders once no host is left in them. */
1162
+ removeEmpty: (name: string) => Promise<boolean>;
801
1163
  }
802
1164
  export interface PluginHostImportOptions {
803
1165
  /** Update a host that matches on ip, port and username instead of skipping it. */
@@ -1136,7 +1498,26 @@ export interface PluginNetwork {
1136
1498
  timeoutMs?: number;
1137
1499
  }) => Promise<boolean>;
1138
1500
  }
1501
+ /**
1502
+ * A way to reach some hosts other than a plain TCP connection. `claims` is
1503
+ * asked for every connection, so it must be quick. `open` returns a stream
1504
+ * the SSH client runs over, like a relayed socket.
1505
+ */
1506
+ export interface PluginSshTransportProvider {
1507
+ claims: (hostId: number) => boolean;
1508
+ open: (host: {
1509
+ id: number;
1510
+ ip: string;
1511
+ port: number;
1512
+ }) => Promise<import("node:stream").Duplex>;
1513
+ }
1514
+ export interface PluginSshTransports {
1515
+ /** Needs ssh:transport. Removed on deactivate. */
1516
+ provide: (provider: PluginSshTransportProvider) => () => void;
1517
+ }
1139
1518
  export interface PluginSsh {
1519
+ /** Other ways to reach hosts, used by every connection to them. */
1520
+ transports: PluginSshTransports;
1140
1521
  /**
1141
1522
  * Whether a TCP port answers through the host's jump host chain. Chains are
1142
1523
  * shared and closed when idle. The acting user must be able to connect to
@@ -34,6 +34,8 @@ export const CAPABILITY_CATALOG = [
34
34
  entry("db:core-refs", "high"),
35
35
  entry("auth:provide", "high"),
36
36
  entry("auth:policy", "high"),
37
+ entry("sync:provide", "high"),
38
+ entry("ssh:transport", "high"),
37
39
  entry("device:serial", "high"),
38
40
  entry("notify:hub", "high"),
39
41
  entry("ui:layout", "high"),
@@ -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-VFTB2RDP.js";
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-2IV3VX7F.js";
111
- import "./chunk-3L45NNT5.js";
110
+ } from "./chunk-LQKK2JJA.js";
111
+ import "./chunk-7LEJ3HYF.js";
112
112
  import {
113
113
  __toESM,
114
114
  define_import_meta_env_default,
@@ -26,7 +26,7 @@ var inFlight = null;
26
26
  function load() {
27
27
  if (cache) return Promise.resolve(cache);
28
28
  if (inFlight) return inFlight;
29
- inFlight = import("./rbac-api-KA5IPQQL.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
29
+ inFlight = import("./rbac-api-LZVENREG.js").then(({ getMyPermissions }) => getMyPermissions()).then((result) => {
30
30
  cache = {
31
31
  permissions: result.permissions ?? [],
32
32
  isAdmin: !!result.isAdmin
@@ -7,10 +7,10 @@ import {
7
7
  Trash,
8
8
  cn,
9
9
  useAreaPreferences
10
- } from "./chunk-2IV3VX7F.js";
10
+ } from "./chunk-LQKK2JJA.js";
11
11
  import {
12
12
  isElectron
13
- } from "./chunk-3L45NNT5.js";
13
+ } from "./chunk-7LEJ3HYF.js";
14
14
  import {
15
15
  __commonJS,
16
16
  __toESM,
@@ -2383,6 +2383,8 @@ var CAPABILITY_CATALOG = [
2383
2383
  entry("db:core-refs", "high"),
2384
2384
  entry("auth:provide", "high"),
2385
2385
  entry("auth:policy", "high"),
2386
+ entry("sync:provide", "high"),
2387
+ entry("ssh:transport", "high"),
2386
2388
  entry("device:serial", "high"),
2387
2389
  entry("notify:hub", "high"),
2388
2390
  entry("ui:layout", "high"),
@@ -7,6 +7,7 @@ import {
7
7
 
8
8
  // src/ui/lib/linked-server.ts
9
9
  init_define_import_meta_env();
10
+ import { useEffect as useEffect2, useState as useState2 } from "react";
10
11
 
11
12
  // src/ui/lib/electron.ts
12
13
  init_define_import_meta_env();
@@ -5432,7 +5433,6 @@ init_define_import_meta_env();
5432
5433
  init_define_import_meta_env();
5433
5434
  var LEGACY_SEEN = Object.freeze({
5434
5435
  plugins: 1,
5435
- "desktop-sync": 1,
5436
5436
  preset: 1,
5437
5437
  appearance: 1,
5438
5438
  security: 1
@@ -6316,7 +6316,7 @@ async function updateRole(roleId, roleData) {
6316
6316
  try {
6317
6317
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
6318
6318
  if (roleData.permissions) {
6319
- const { notifyPermissionsChanged } = await import("./use-permissions-LLN6TLAL.js");
6319
+ const { notifyPermissionsChanged } = await import("./use-permissions-BYCDS5PD.js");
6320
6320
  notifyPermissionsChanged();
6321
6321
  }
6322
6322
  return response.data;
@@ -6898,7 +6898,7 @@ function createRemoteOriginApiInstance(path) {
6898
6898
  });
6899
6899
  instance.interceptors.request.use(
6900
6900
  async (config) => {
6901
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-SCJH7DSL.js");
6901
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-OUF3POT7.js");
6902
6902
  const linked = await getLinkedSession2();
6903
6903
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
6904
6904
  if (config.headers.set) {
@@ -7287,6 +7287,22 @@ function notifySyncChanged() {
7287
7287
  cached = null;
7288
7288
  window.dispatchEvent(new CustomEvent(SYNC_CHANGED_EVENT));
7289
7289
  }
7290
+ function useDesktopLinked() {
7291
+ const [linked, setLinked] = useState2(false);
7292
+ useEffect2(() => {
7293
+ let live = true;
7294
+ const read = () => void getLinkedSession().then((session) => {
7295
+ if (live) setLinked(!!session);
7296
+ });
7297
+ read();
7298
+ window.addEventListener(SYNC_CHANGED_EVENT, read);
7299
+ return () => {
7300
+ live = false;
7301
+ window.removeEventListener(SYNC_CHANGED_EVENT, read);
7302
+ };
7303
+ }, []);
7304
+ return linked;
7305
+ }
7290
7306
 
7291
7307
  export {
7292
7308
  getPluginStoreState,
@@ -7332,6 +7348,7 @@ export {
7332
7348
  websocketAuthProtocols,
7333
7349
  getLinkedSession,
7334
7350
  notifySyncChanged,
7351
+ useDesktopLinked,
7335
7352
  resolveConnectionOrigin,
7336
7353
  buildOriginWsUrl,
7337
7354
  getAllServerStatuses,