@termix-ssh/plugin-sdk 1.0.15 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/PLUGIN-GUIDE.md +92 -72
  3. package/dist/backend.d.ts +370 -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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.17
4
+
5
+ Plugin API 1.9.0.
6
+
7
+ ### Added
8
+
9
+ - `app.registerCompanion` and `app.toggleCompanion`, so a plugin can add a panel beside any host tab. The user opens, resizes and moves it, and the tab keeps running
10
+ - `NativeView` in `@termix-ssh/plugin-sdk/ui`, which shows a web page in a native view in the desktop app, for sites that do not work in an iframe
11
+
12
+ ## 1.0.16
13
+
14
+ Plugin API 1.8.0.
15
+
16
+ ### Added
17
+
18
+ - `ctx.sync.provide` and the `sync:provide` capability, so a plugin can answer every /sync request
19
+ - `ctx.ssh.transports.provide` and the `ssh:transport` capability, so a plugin can carry SSH connections over its own stream
20
+ - `ctx.hosts.folders.ensure` and `removeEmpty`
21
+
22
+ ### Removed
23
+
24
+ - The sync screens and calls in `@termix-ssh/plugin-sdk/ui`: `LinkServerDialog`, `ProxySettingsFields`, `useSyncStatus`, `refreshSyncStatus`, `setSyncStatus`, `getSyncConflicts`, `getSyncErrors`, `retrySyncErrors`, `settleSyncConflict`, `syncNow`, `unlinkServer`, `updateSyncSettings` and their types. They moved into the sync plugin
25
+
3
26
  ## 1.0.15
4
27
 
5
28
  Plugin API 1.7.0.
package/PLUGIN-GUIDE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Termix plugin guide
2
2
 
3
- SDK 1.0.15 | Plugin API 1.7.0
3
+ SDK 1.0.17 | Plugin API 1.9.0
4
4
 
5
5
  This guide is for anyone, person or AI coding tool, building a Termix plugin. It ships inside
6
6
  `@termix-ssh/plugin-sdk`, so the copy in your `node_modules` always matches the SDK you have installed.
@@ -202,37 +202,40 @@ Bundled as ESM for Node 22+ into `dist/backend.js`. Backend imports of your own
202
202
 
203
203
  ### What ctx has
204
204
 
205
- | Member | Capability | What it does |
206
- | ---------------------- | -------------------------------- | -------------------------------------------------------------- |
207
- | `pluginId`, `manifest` | none | Your id and manifest |
208
- | `log` | none | Log lines tagged with your plugin |
209
- | `db` | `db:own` | Your own tables |
210
- | `kv` | `kv:own` | Small key value state |
211
- | `files.dataDir()` | `files:own` | A folder of your own |
212
- | `settings` | none | Your settings |
213
- | `secrets` | `secrets:own` | Encrypted values per user, `seal`/`unseal` for your own tables |
214
- | `http`, `ws` | `network:serve` | Routes and sockets |
215
- | `rbac` | none | Check the actor's permissions |
216
- | `hosts` | `hosts:read`, `hosts:write` | List, read, create, update and share hosts as the actor |
217
- | `ssh` | `ssh:connect`, `credentials:use` | Connect to hosts through Termix's SSH pipeline |
218
- | `credentials` | varies | The actor's saved keys, and protocol logins |
219
- | `services` | per service | Call other plugins and offer your own |
220
- | `events` | `events:core` for core topics | Emit and listen for events |
221
- | `notify` | `notify:send` | Send alerts to the Alerts inbox |
222
- | `fetch` | `network:outbound` | Outbound HTTP with SSRF protection |
223
- | `process` | `process:spawn` | Run programs on the server |
224
- | `schedule` | none | Timers that stop on disable and never overlap |
225
- | `audit.record` | none | Write your own audit line |
226
- | `sync` | none | Sync your tables to a linked desktop |
227
- | `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
228
- | `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
229
- | `auth.policy` | `auth:policy` | Login rules: idle timeout, session length, second factors |
230
- | `users.getLocale` | `users:read` | A user's UI language |
231
- | `system` | `system:tls` | The server's HTTPS certificate |
232
- | `plugins.list()` | `plugins:read` | Which plugins are installed |
233
- | `asUser(userId, fn)` | always audited | Run background work as a user |
234
- | `currentActor()` | none | The user the current call runs for |
235
- | `disposables.add(fn)` | none | Extra cleanup on disable |
205
+ | Member | Capability | What it does |
206
+ | ---------------------- | -------------------------------- | --------------------------------------------------------------- |
207
+ | `pluginId`, `manifest` | none | Your id and manifest |
208
+ | `log` | none | Log lines tagged with your plugin |
209
+ | `db` | `db:own` | Your own tables |
210
+ | `kv` | `kv:own` | Small key value state |
211
+ | `files.dataDir()` | `files:own` | A folder of your own |
212
+ | `settings` | none | Your settings |
213
+ | `secrets` | `secrets:own` | Encrypted values per user, `seal`/`unseal` for your own tables |
214
+ | `http`, `ws` | `network:serve` | Routes and sockets |
215
+ | `rbac` | none | Check the actor's permissions |
216
+ | `hosts` | `hosts:read`, `hosts:write` | List, read, create, update and share hosts as the actor |
217
+ | `ssh` | `ssh:connect`, `credentials:use` | Connect to hosts through Termix's SSH pipeline |
218
+ | `credentials` | varies | The actor's saved keys, and protocol logins |
219
+ | `services` | per service | Call other plugins and offer your own |
220
+ | `events` | `events:core` for core topics | Emit and listen for events |
221
+ | `notify` | `notify:send` | Send alerts to the Alerts inbox |
222
+ | `fetch` | `network:outbound` | Outbound HTTP with SSRF protection |
223
+ | `process` | `process:spawn` | Run programs on the server |
224
+ | `schedule` | none | Timers that stop on disable and never overlap |
225
+ | `audit.record` | none | Write your own audit line |
226
+ | `sync` | none | Sync your tables to a linked desktop |
227
+ | `auth` | `auth:provide` | Login methods, second factors, SSH auth types |
228
+ | `auth.issueUserToken` | `users:impersonate` | A short-lived session for calling Termix's API as a user |
229
+ | `auth.policy` | `auth:policy` | Login rules: idle timeout, session length, second factors |
230
+ | `sync.provide` | `sync:provide` | Answers all /sync requests: server sync, linked desktops |
231
+ | `ssh.transports` | `ssh:transport` | Carries a host's SSH connection over your own stream |
232
+ | `hosts.folders` | `hosts:write` | Create a folder row with a color and icon, remove it when empty |
233
+ | `users.getLocale` | `users:read` | A user's UI language |
234
+ | `system` | `system:tls` | The server's HTTPS certificate |
235
+ | `plugins.list()` | `plugins:read` | Which plugins are installed |
236
+ | `asUser(userId, fn)` | always audited | Run background work as a user |
237
+ | `currentActor()` | none | The user the current call runs for |
238
+ | `disposables.add(fn)` | none | Extra cleanup on disable |
236
239
 
237
240
  ### The actor
238
241
 
@@ -301,6 +304,9 @@ router.post("/notes", async (req, res) => {
301
304
  - `ctx.rbac.require("manage")` on a single route, `await ctx.rbac.has("manage")` in code. A full id like
302
305
  `hosts.edit` checks a core permission.
303
306
  - Host access: `ctx.hosts.checkAccess(hostId, "connect" | "view" | "edit" | "manage")`.
307
+ - Transports: with `ssh:transport`, `ctx.ssh.transports.provide({ claims(hostId), open({ id, ip, port }) })`
308
+ returns a stream the SSH client uses instead of a direct, proxy or jump host connection. Core also serves
309
+ `/plugin-ws/core/ssh-relay?hostId=` for callers with connect access, so one Termix can relay to another.
304
310
  - Managed hosts: with `hosts:lock`, `ctx.hosts.locks.provide({ isLocked, note, allowManualCreate })` makes
305
311
  the hosts you manage read only for the UI, the API and other plugins (pins and order still work), and
306
312
  `allowManualCreate: () => false` blocks adding hosts by hand. Your own `ctx.hosts` writes still go
@@ -489,6 +495,7 @@ your plugin's error boundary and Suspense.
489
495
  | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
490
496
  | `registerRailItem` | A sidebar rail button |
491
497
  | `registerTab`, `registerPanel` | A tab type or sidebar panel (id in `contributes.tabs` / `panels`) |
498
+ | `registerCompanion`, `toggleCompanion` | A panel beside any host tab, bound to its host and handle |
492
499
  | `registerDashboardCard` | A dashboard card (`contributes.dashboardCards`) |
493
500
  | `registerHostAction` | A host button, palette entry or default connect |
494
501
  | `registerHostEditorSection` | Your own host editor section (host settings get one for free) |
@@ -624,6 +631,9 @@ Build UI from `@termix-ssh/plugin-sdk/ui` so it matches Termix and follows the t
624
631
  exports with props is `ui.api.txt` in the Termix repo
625
632
  (https://github.com/Termix-SSH/Termix/blob/main/packages/plugin-sdk/ui.api.txt).
626
633
 
634
+ - Web pages that break in an iframe: `NativeView` (props `url`, `ignoreCert`, `hidden`, `fallback`) shows the
635
+ page in a native view in the desktop app. It fills its element, hides while a dialog or menu is open and renders
636
+ `fallback` in a browser.
627
637
  - Square: panels, cards and containers have no rounded corners. Small controls may stay rounded.
628
638
  - Flat: borders, not shadows. Use theme tokens like `border-border`, `bg-background`,
629
639
  `text-muted-foreground`, `text-destructive`. One accent, `text-accent-brand`. Don't add colors.
@@ -707,6 +717,14 @@ lists them too). `evaluate` returns `allowRememberDevice`, `maxSessionSeconds`,
707
717
  refuses the login. A user who must have a second factor but has none gets a session that can only set one
708
718
  up, and `/users/me` reports `restriction: "second-factor-setup"`.
709
719
 
720
+ Sync is answered by one plugin. `sync:provide` is high risk: `ctx.sync.provide({ handle })` takes over every
721
+ request under `/sync` (an Express style handler, 404 when nothing provides it), and `ctx.sync.host` hands
722
+ over the neutral parts core keeps: the replica store (`replica`), the change feed and push applier
723
+ (`feed`), the linked server list (`links`, one per local user), desktop accounts (`accounts`), sessions
724
+ (`sessions`), host lookups, plugin mirroring and the proxy settings the desktop app needs. A test passes its
725
+ own `syncHost` to `createMockCtx`. An API key can ask `POST /users/ws-ticket/api-key` for a one minute
726
+ ticket that opens plugin sockets as its user, which is how one server opens sessions through another.
727
+
710
728
  ## Testing
711
729
 
712
730
  ```ts
@@ -893,46 +911,48 @@ To run your own registry instead, see https://docs.termix.site/develop/registrie
893
911
 
894
912
  ## Capabilities
895
913
 
896
- | Capability | Risk | What it allows |
897
- | ------------------------ | -------- | ------------------------------------------------------- |
898
- | `credentials:read` | critical | Read stored secrets for hosts the user can reach |
899
- | `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
900
- | `ssh:connect` | high | Run any command on hosts, with the user's access |
901
- | `process:spawn` | high | Run programs on the Termix server |
902
- | `users:write` | high | Add users and change their roles |
903
- | `users:impersonate` | high | Act as another user, or get a session for them, audited |
904
- | `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
905
- | `auth:provide` | high | Add login methods, second factors, SSH auth types |
906
- | `auth:policy` | high | Set login rules: idle timeout, session length, 2FA |
907
- | `device:serial` | high | Open serial and USB ports on the server |
908
- | `desktop:local` | high | Use the user's own computer, desktop app only |
909
- | `hosts:export` | high | Export hosts with their saved logins |
910
- | `notify:hub` | high | Receive every alert (the alerts hub only) |
911
- | `ui:layout` | high | Replace the whole app layout |
912
- | `hosts:write` | medium | Add and edit hosts |
913
- | `hosts:status-provide` | medium | Be the source of host status dots |
914
- | `hosts:activity-provide` | medium | Be the source of the recent activity list |
915
- | `hosts:lock` | medium | Make hosts it manages read only for everyone else |
916
- | `network:probe` | medium | Check that a host answers on a TCP port |
917
- | `credentials:use` | medium | Connect with saved credentials without seeing them |
918
- | `credentials:write` | medium | Save new credentials |
919
- | `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
920
- | `network:serve` | medium | HTTP routes and sockets |
921
- | `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
922
- | `users:read` | medium | Usernames, roles and UI language |
923
- | `events:core` | medium | Listen to core events |
924
- | `notify:send` | medium | Send alerts |
925
- | `audit:read` | medium | Read the audit log |
926
- | `desktop:window` | medium | Open windows in the desktop app |
927
- | `hosts:read` | low | Read hosts the user can see |
928
- | `db:own` | low | Your own tables |
929
- | `kv:own` | low | Small key value state |
930
- | `files:own` | low | Your own folder on disk |
931
- | `secrets:own` | low | Your own encrypted secrets |
932
- | `settings:read-core` | low | Read Termix server settings |
933
- | `plugins:read` | low | See installed plugins |
934
- | `ui:surface` | low | Add tabs, panels and settings |
935
- | `ui:theme` | low | Add color themes, CSS only |
914
+ | Capability | Risk | What it allows |
915
+ | ------------------------ | -------- | ------------------------------------------------------------- |
916
+ | `credentials:read` | critical | Read stored secrets for hosts the user can reach |
917
+ | `system:tls` | critical | Replace the HTTPS certificate, answer CA domain checks |
918
+ | `ssh:connect` | high | Run any command on hosts, with the user's access |
919
+ | `process:spawn` | high | Run programs on the Termix server |
920
+ | `users:write` | high | Add users and change their roles |
921
+ | `users:impersonate` | high | Act as another user, or get a session for them, audited |
922
+ | `db:core-refs` | high | Query core users, roles and hosts tables (read only) |
923
+ | `auth:provide` | high | Add login methods, second factors, SSH auth types |
924
+ | `auth:policy` | high | Set login rules: idle timeout, session length, 2FA |
925
+ | `sync:provide` | high | Run sync: answer every /sync request |
926
+ | `device:serial` | high | Open serial and USB ports on the server |
927
+ | `desktop:local` | high | Use the user's own computer, desktop app only |
928
+ | `hosts:export` | high | Export hosts with their saved logins |
929
+ | `notify:hub` | high | Receive every alert (the alerts hub only) |
930
+ | `ui:layout` | high | Replace the whole app layout |
931
+ | `hosts:write` | medium | Add and edit hosts |
932
+ | `hosts:status-provide` | medium | Be the source of host status dots |
933
+ | `hosts:activity-provide` | medium | Be the source of the recent activity list |
934
+ | `ssh:transport` | high | Carry SSH connections for hosts it claims over its own stream |
935
+ | `hosts:lock` | medium | Make hosts it manages read only for everyone else |
936
+ | `network:probe` | medium | Check that a host answers on a TCP port |
937
+ | `credentials:use` | medium | Connect with saved credentials without seeing them |
938
+ | `credentials:write` | medium | Save new credentials |
939
+ | `network:outbound` | medium | Outbound HTTP through `ctx.fetch` |
940
+ | `network:serve` | medium | HTTP routes and sockets |
941
+ | `network:broadcast` | medium | Local network broadcasts, like Wake-on-LAN |
942
+ | `users:read` | medium | Usernames, roles and UI language |
943
+ | `events:core` | medium | Listen to core events |
944
+ | `notify:send` | medium | Send alerts |
945
+ | `audit:read` | medium | Read the audit log |
946
+ | `desktop:window` | medium | Open windows in the desktop app |
947
+ | `hosts:read` | low | Read hosts the user can see |
948
+ | `db:own` | low | Your own tables |
949
+ | `kv:own` | low | Small key value state |
950
+ | `files:own` | low | Your own folder on disk |
951
+ | `secrets:own` | low | Your own encrypted secrets |
952
+ | `settings:read-core` | low | Read Termix server settings |
953
+ | `plugins:read` | low | See installed plugins |
954
+ | `ui:surface` | low | Add tabs, panels and settings |
955
+ | `ui:theme` | low | Add color themes, CSS only |
936
956
 
937
957
  An update that adds a capability waits for an admin to approve it.
938
958
 
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
@@ -798,6 +1138,17 @@ export interface PluginHosts {
798
1138
  activity: PluginHostActivityApi;
799
1139
  /** Read only hosts this plugin manages. */
800
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>;
801
1152
  }
802
1153
  export interface PluginHostImportOptions {
803
1154
  /** Update a host that matches on ip, port and username instead of skipping it. */
@@ -1136,7 +1487,26 @@ export interface PluginNetwork {
1136
1487
  timeoutMs?: number;
1137
1488
  }) => Promise<boolean>;
1138
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
+ }
1139
1507
  export interface PluginSsh {
1508
+ /** Other ways to reach hosts, used by every connection to them. */
1509
+ transports: PluginSshTransports;
1140
1510
  /**
1141
1511
  * Whether a TCP port answers through the host's jump host chain. Chains are
1142
1512
  * 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"),