From 380438a8c10af9aced7e9ee2e669ae3ca6757051 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 12 May 2026 09:52:57 -0500 Subject: [PATCH] Fix plugin key route resolution Co-Authored-By: Paperclip --- .../src/__tests__/plugin-routes-authz.test.ts | 40 +++++++++++++++++++ server/src/routes/plugins.ts | 26 +++--------- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts index 52e678b46dc..963d3cd1f15 100644 --- a/server/src/__tests__/plugin-routes-authz.test.ts +++ b/server/src/__tests__/plugin-routes-authz.test.ts @@ -224,6 +224,46 @@ describe.sequential("plugin install and upgrade authz", () => { expect(mockLifecycle.disable).not.toHaveBeenCalled(); }, 20_000); + it("resolves plugin keys without probing the UUID id column for core plugin actions", async () => { + const pluginKey = "paperclipqa.hello-plugin"; + const plugin = { + id: pluginId, + pluginKey, + version: "1.0.0", + status: "ready", + }; + mockRegistry.getById.mockImplementation(() => { + throw new Error("getById should not be called for plugin keys"); + }); + mockRegistry.getByKey.mockResolvedValue(plugin); + mockLifecycle.unload.mockResolvedValue(plugin); + mockLifecycle.enable.mockResolvedValue(plugin); + mockLifecycle.disable.mockResolvedValue(plugin); + + const { app } = await createApp({ + type: "board", + userId: "admin-1", + source: "session", + isInstanceAdmin: true, + companyIds: [companyA], + }); + + const inspectRes = await request(app).get(`/api/plugins/${pluginKey}`); + const disableRes = await request(app).post(`/api/plugins/${pluginKey}/disable`).send({}); + const enableRes = await request(app).post(`/api/plugins/${pluginKey}/enable`).send({}); + const uninstallRes = await request(app).delete(`/api/plugins/${pluginKey}?purge=true`); + + expect(inspectRes.status).toBe(200); + expect(disableRes.status).toBe(200); + expect(enableRes.status).toBe(200); + expect(uninstallRes.status).toBe(200); + expect(mockRegistry.getById).not.toHaveBeenCalled(); + expect(mockRegistry.getByKey).toHaveBeenCalledWith(pluginKey); + expect(mockLifecycle.disable).toHaveBeenCalledWith(pluginId, undefined); + expect(mockLifecycle.enable).toHaveBeenCalledWith(pluginId); + expect(mockLifecycle.unload).toHaveBeenCalledWith(pluginId, true); + }, 20_000); + it("rejects plugin config saves that contain secret refs even for instance admins", async () => { readyPlugin(); diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts index 75a206b154b..443685187c4 100644 --- a/server/src/routes/plugins.ts +++ b/server/src/routes/plugins.ts @@ -199,9 +199,9 @@ function listBundledPluginExamples(): AvailablePluginExample[] { * * Lookup order: * - UUID-like IDs: getById first, then getByKey. - * - Scoped package keys (e.g. "@scope/name"): getByKey only, never getById. - * - Other non-UUID IDs: try getById first (test/memory registries may allow this), - * then fallback to getByKey. Any UUID parse error from getById is ignored. + * - All non-UUID values: getByKey only, never getById. The persisted plugin + * ID column is a PostgreSQL UUID, so probing it with keys such as + * "acme.plugin" raises a database cast error before a key lookup can happen. * * @param registry - The plugin registry service instance * @param pluginId - Either a database UUID or plugin key (manifest id) @@ -212,27 +212,13 @@ async function resolvePlugin( pluginId: string, ) { const isUuid = UUID_REGEX.test(pluginId); - const isScopedPackageKey = pluginId.startsWith("@") || pluginId.includes("/"); - // Scoped package IDs are valid plugin keys but invalid UUIDs. - // Skip getById() entirely to avoid Postgres uuid parse errors. - if (isScopedPackageKey && !isUuid) { + if (!isUuid) { return registry.getByKey(pluginId); } - try { - const byId = await registry.getById(pluginId); - if (byId) return byId; - } catch (error) { - const maybeCode = - typeof error === "object" && error !== null && "code" in error - ? (error as { code?: unknown }).code - : undefined; - // Ignore invalid UUID cast errors and continue with key lookup. - if (maybeCode !== "22P02") { - throw error; - } - } + const byId = await registry.getById(pluginId); + if (byId) return byId; return registry.getByKey(pluginId); }