Fix plugin key route resolution

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-12 12:03:15 -05:00
1 parent a8cb1427d1
commit 380438a8c1
2 files changed
+46 -20

No files matched your search

@@ -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();
+6 -20
View File
@@ -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);
}