feat: folder logins for shared hosts, read only plugin sync, full desktop builds

This commit is contained in:
LukeGus committed 2026-10-29 15:54:35 -05:00
1 parent b32203d19b
commit a0e13f9eeb
33 files changed
+15245 -14

No files matched your search

+2
View File
@@ -13,6 +13,8 @@ Plugin API 1.13.0.
- `ctx.sync.host.entities.idsOf` maps sync ids to the local ids of the user's own rows of one type
- Rail items take `useStatus` for a small ok, warning or error dot on the icon
- `shell.editHost(hostId)` opens the editor of a saved host
- A sync entity can be read only with `load`, `write` and `erase`, so a plugin can sync rows a user does not own, like a shared library, down to linked desktops
- `getFolderAuthOverride` and `setFolderAuthOverride` in `@termix-ssh/plugin-sdk/ui` read and set a user's own login for the shared hosts in a folder
### Fixed
+17
View File
@@ -495,6 +495,23 @@ Other storage: `ctx.kv` for a few values, `ctx.settings` for settings, `ctx.secr
`ctx.files.dataDir()` for files. To sync a table to a linked desktop, use `ctx.sync.registerEntity` and
`contributes.syncEntities`. The entity name is part of the protocol and never changes.
A registered table syncs the rows each user owns, both ways. For data you keep for the whole server (a shared
library, rows other users share), register a read only entity with `table: null`, `readOnly: true` and `load`,
`write` and `erase` (Termix 26.10.6+). `load(userId)` returns the rows that user should get, each with a
`syncId`. The desktop keeps them with `write` and drops them with `erase`, and never pushes them back:
```ts
ctx.sync.registerEntity({
type: "sharedIcons",
table: null,
readOnly: true,
order: 70,
load: (userId) => iconsVisibleTo(userId),
write: (userId, row) => storeCopy(userId, row),
erase: (userId, syncId) => dropCopy(userId, syncId),
});
```
## Frontend
```tsx
+17 -1
View File
@@ -177,8 +177,24 @@ export interface SyncWriteEvent {
export interface SyncEntityRegistration {
/** Stable wire name. Never change it: records on both sides match on it. */
type: string;
/** The table, as returned by ctx.db.define. */
/** The table, as returned by ctx.db.define. Null for an entity with `load`. */
table: unknown;
/**
* Server to desktop only: a desktop never pushes these rows. Required with
* `load`.
*/
readOnly?: boolean;
/**
* Builds the rows the user gets instead of reading the rows they own in
* `table`, for data kept for the whole server (a shared library, rows other
* users share with them). Rows are on the wire: plaintext, each with a
* syncId. Needs `readOnly: true`, `write` and `erase`.
*/
load?: (userId: string) => Promise<SyncRow[]>;
/** Keeps a copy of an inbound row on the desktop, for an entity with `load`. */
write?: (userId: string, wire: SyncRow) => Promise<void>;
/** Drops the desktop copy with this syncId, for an entity with `load`. */
erase?: (userId: string, syncId: string) => Promise<void>;
/** Column holding the owning user id. Defaults to "userId". */
userColumn?: string;
/** Fields stored encrypted with the owner's data key. Plaintext on the wire. */
+3 -1
View File
@@ -403,6 +403,7 @@ getDeviceId (value): () => string
getErrorMessage (value): (error: unknown, fallback?: string) => string
getExternalAutoProvision (value): () => Promise<{ enabled: boolean; }>
getFolderAccess (value): (folder: string) => Promise<{ rules: FolderAccessRule[]; }>
getFolderAuthOverride (value): (folder: string, protocol: string, sharedCopySyncId?: string) => Promise<{ folder: string; protocol: string; credentialId: number; }>
getFolderDefaultsId (value): (name: string) => Promise<number>
getHostAccess (value): (hostId: number, syncId?: string) => Promise<{ accessList: AccessRecord[]; isOwner?: boolean; }>
getHostAuthOverride (value): (hostId: number, protocol: string, sharedCopySyncId?: string) => Promise<{ protocol: string; credentialId: number; }>
@@ -526,7 +527,7 @@ resolveHostTabType (value): (host: Host, preferredType?: TabType) => TabType
resolveRemoteHostId (value): (syncId: string) => Promise<number>
revokeAllUserSessions (value): (userId: string) => Promise<{ success: boolean; message: string; }>
revokeCredentialAccess (value): (credentialId: number, accessId: number) => Promise<void>
revokeFolderAccess (value): (ruleId: number) => Promise<void>
revokeFolderAccess (value): (ruleId: number, options?: { fromHosts?: boolean; }) => Promise<void>
revokeHostAccess (value): (hostId: number, accessId: number, syncId?: string) => Promise<{ success: boolean; }>
revokeSession (value): (sessionId: string) => Promise<{ success: boolean; message: string; }>
rightDockableIds (value): () => string[]
@@ -544,6 +545,7 @@ saveHostTags (value): (tags: string[]) => Promise<string[]>
saveUserPreferences (value): (prefs: Partial<UserPreferences>) => Promise<void>
setAllPluginChannels (value): (channel: PluginChannel) => Promise<{ changed: string[]; }>
setDeveloperMode (value): (enabled: boolean) => Promise<void>
setFolderAuthOverride (value): (folder: string, protocol: string, credentialId: number, sharedCopySyncId?: string) => Promise<{ success: boolean; folder: string; protocol: string; credentialId: number; }>
setHostAuthOverride (value): (hostId: number, protocol: string, credentialId: number, sharedCopySyncId?: string) => Promise<{ success: boolean; protocol: string; credentialId: number; }>
setNotificationPrivateEndpoints (value): (hosts: string[]) => Promise<string[]>
setPluginOptions (value): (pluginId: string, options: { autoUpdate?: boolean; pinned?: boolean; channel?: PluginChannel; }) => Promise<{ autoUpdate: boolean; pinnedVersion: string; channel: PluginChannel; }>