Compare commits

..
Author SHA1 Message Date
LukeGus 1c4fa52de3 fix: remove plugin updates the bundled copy caught up with 2026-11-04 08:11:34 -06:00
3 changed files with 31 additions and 3 deletions

No files matched your search

+1
View File
@@ -20,6 +20,7 @@
- Alerts with no icon no longer squeeze their text to one word per line
- Password fields with the show button are the same height as the other fields next to them
- The Secure your account setup step no longer shows up empty when no passkey or authenticator plugin is on
- An installed plugin update older than the copy in a newer image is removed at startup instead of logging an error every boot
## 26.11.0
+20 -3
View File
@@ -409,6 +409,15 @@ export class PluginLoader {
}
loaded.push(plugin);
} catch (error) {
// A newer image ships a newer copy, so the old update can go.
if (isArtifact && error instanceof SupersededUpdateError) {
await fs.promises.rm(dir, { force: true }).catch(() => {});
await fs.promises.rm(`${dir}.sig`, { force: true }).catch(() => {});
pluginLogger.info(`Removed ${entry.name}: ${error.message}`, {
operation: "plugin_load",
});
continue;
}
this.skippedPlugins.push(
describeSkipped(dir, entry.name, isArtifact, error),
);
@@ -948,6 +957,16 @@ function withTimeout<T>(
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
/** An installed update that the bundled copy has caught up with. */
export class SupersededUpdateError extends Error {
constructor(id: string, version: string, bundledVersion: string) {
super(
`update for "${id}" is ${version}, which is not newer than the bundled ${bundledVersion}`,
);
this.name = "SupersededUpdateError";
}
}
/**
* Throws unless an artifact may replace the bundled plugin with the same id:
* a trusted key signed it and its version is strictly higher, so an old
@@ -971,9 +990,7 @@ export function assertBundledUpdate(
// An admin pinning an exact older release is the one allowed downgrade.
if (pinnedVersion && pinnedVersion === version) return;
if (bundledVersion && !semver.gt(version, bundledVersion)) {
throw new Error(
`update for "${id}" is ${version}, which is not newer than the bundled ${bundledVersion}`,
);
throw new SupersededUpdateError(id, version, bundledVersion);
}
}
@@ -231,6 +231,16 @@ describe("updating a bundled plugin", () => {
expect(loader.get("bundled-one")?.manifest.version).toBe("1.0.0");
});
it("removes an update the bundled copy caught up with", async () => {
const { userDir } = setup();
const file = await writeArtifact(userDir, "bundled-one", "valid", "0.9.0");
const loader = new PluginLoader();
await loader.loadAll();
expect(fs.existsSync(file)).toBe(false);
expect(fs.existsSync(`${file}.sig`)).toBe(false);
expect(loader.skipped()).toEqual([]);
});
it("refuses a downgrade after the bundled copy was unloaded", async () => {
const { userDir } = setup();
const loader = new PluginLoader();