fix(sync): let a linked desktop enable mirrored plugins it does not trust on its own (#1632)

* fix(sync): let a linked desktop enable mirrored plugins it does not trust on its own

* fix(sync): switch off an untrusted mirrored plugin when the server updates it

* fix: require local consent for mirrored plugin activation

* docs: describe consent fields on the plugin state endpoint
This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-10-14 09:11:55 +08:00
1 parent 9b0af07607
commit b3b08b4ff2
17 files changed
+515 -36

No files matched your search

+14 -10
View File
@@ -8,7 +8,7 @@ import { capabilityRisk, orderByRisk, type PluginEntry } from "./plugin-model";
export interface ConsentRequest {
plugin: PluginEntry;
mode: "install" | "update" | "upload";
mode: "install" | "update" | "upload" | "enable";
version: string;
/** What the prompt lists: all of them to install, only the new ones to update. */
capabilities: string[];
@@ -45,17 +45,19 @@ export function PluginConsentPrompt({
onCancel={busy ? undefined : onCancel}
icon={<PluginIconBox name={plugin.icon} size="sm" />}
title={
mode === "update"
? t("plugins.manager.consent.updateTitle", { name: plugin.name })
: mode === "upload"
? t("plugins.manager.consent.uploadTitle", { name: plugin.name })
: t("plugins.manager.consent.installTitle", { name: plugin.name })
mode === "enable"
? t("plugins.manager.consent.enableTitle", { name: plugin.name })
: mode === "update"
? t("plugins.manager.consent.updateTitle", { name: plugin.name })
: mode === "upload"
? t("plugins.manager.consent.uploadTitle", { name: plugin.name })
: t("plugins.manager.consent.installTitle", { name: plugin.name })
}
description={
<Facts>
{plugin.author && <span className="truncate">{plugin.author}</span>}
<span className="shrink-0">
{mode !== "install" && plugin.version
{(mode === "update" || mode === "upload") && plugin.version
? t("plugins.manager.versionChange", {
from: plugin.version,
to: version,
@@ -75,9 +77,11 @@ export function PluginConsentPrompt({
disabled={busy}
className="border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10"
>
{mode === "update"
? t("plugins.manager.consent.agreeUpdate")
: t("plugins.manager.install")}
{mode === "enable"
? t("plugins.manager.enable")
: mode === "update"
? t("plugins.manager.consent.agreeUpdate")
: t("plugins.manager.install")}
</Button>
</>
}
+1 -1
View File
@@ -177,7 +177,7 @@ export function PluginDetail({
<Button
variant="outline"
size="sm"
disabled={busy || locked}
disabled={busy || plugin.managedByServer}
onClick={() => void manager.toggle(plugin)}
>
{plugin.enabled
+2 -2
View File
@@ -793,7 +793,7 @@ function InstalledCard({
<Button
variant="outline"
size="xs"
disabled={busy || locked}
disabled={busy || plugin.managedByServer}
onClick={() => void manager.toggle(plugin)}
>
{plugin.enabled
@@ -907,7 +907,7 @@ function InstalledRowActions({
<Button
variant="outline"
size="xs"
disabled={busy || locked}
disabled={busy || plugin.managedByServer}
onClick={() => void manager.toggle(plugin)}
>
{plugin.enabled
+5
View File
@@ -69,6 +69,8 @@ export interface PluginEntry {
/** The registry it is listed in or was installed from. */
registryId: string | null;
registryName: string | null;
/** A linked server decides whether it runs here. */
managedByServer: boolean;
}
export type PluginSource =
@@ -165,6 +167,7 @@ export function mergePlugins(
unverified: pluginSource(summary) === "unverified",
registryId: entry?.registryId ?? summary.registryId ?? null,
registryName: entry?.registryName ?? null,
managedByServer: summary.managedByServer ?? false,
});
}
@@ -208,6 +211,7 @@ export function mergePlugins(
unverified: false,
registryId: entry.registryId ?? "official",
registryName: entry.registryName ?? null,
managedByServer: false,
});
}
@@ -248,6 +252,7 @@ export function uploadEntry(preview: PluginUploadPreview): PluginEntry {
unverified: true,
registryId: null,
registryName: null,
managedByServer: false,
};
}
+36 -3
View File
@@ -342,18 +342,42 @@ export function usePluginsManager() {
[failToast],
);
const doEnable = useCallback(
async (request: ConsentRequest) =>
run(request.plugin.id, async () => {
try {
const result = await setPluginState(request.plugin.id, true, {
version: request.version,
capabilities: request.capabilities,
});
if (result.state === "failed" || result.state === "blocked") {
toast.error(
t("plugins.manager.toast.failedToStart", {
name: request.plugin.name,
}),
);
}
} catch (error) {
failToast(error, "plugins.manager.errors.state", request.plugin.name);
}
}),
[run, t, failToast],
);
const confirmConsent = useCallback(async () => {
if (!consent) return;
const request = consent;
setConsent(null);
if (request.mode === "upload" && request.uploadToken) {
if (request.mode === "enable") {
await doEnable(request);
} else if (request.mode === "upload" && request.uploadToken) {
await doInstallUpload(request);
} else if (request.mode === "install") {
await doInstall(request.plugin, request.version, request.capabilities);
} else {
await doUpdate(request.plugin, request.version, request.capabilities);
}
}, [consent, doInstall, doUpdate, doInstallUpload]);
}, [consent, doInstall, doUpdate, doInstallUpload, doEnable]);
const nameOf = useCallback(
(id: string) => plugins.find((p) => p.id === id)?.name ?? id,
@@ -402,6 +426,15 @@ export function usePluginsManager() {
failToast(error, "plugins.manager.errors.state", plugin.name);
return;
}
if (enable && registry?.managedByServer) {
setConsent({
plugin,
mode: "enable",
version: plugin.version!,
capabilities: plugin.capabilities,
});
return;
}
await run(plugin.id, async () => {
try {
const result = await setPluginState(plugin.id, enable);
@@ -415,7 +448,7 @@ export function usePluginsManager() {
}
});
},
[confirm, t, run, failToast, nameOf],
[confirm, t, run, failToast, nameOf, registry?.managedByServer],
);
const retry = useCallback(