From a8ab1d3d946ff9ab19b66c30ec944b7748cff794 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 02:43:33 -0800 Subject: [PATCH] fix(skills): resolve slug references in skillId route param (#19808) The skillId route param handler called getById with the raw path segment. A slug such as inbox-triage then reached the uuid id column, Postgres rejected the cast, and the request returned 500. The handler now resolves the reference with getByRouteRef (UUID, unique slug, or key) and rewrites req.params.skillId to the skill's UUID. An unknown reference returns 404. Fixes PAPERCLIP-BACKEND-6H Co-Authored-By: Devin Foley --- .../__tests__/company-skills-routes.test.ts | 31 +++++++++++++++++++ server/src/routes/company-skills.ts | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/company-skills-routes.test.ts b/server/src/__tests__/company-skills-routes.test.ts index 765c9b2c420..e7df5c3f77c 100644 --- a/server/src/__tests__/company-skills-routes.test.ts +++ b/server/src/__tests__/company-skills-routes.test.ts @@ -45,6 +45,7 @@ const mockCompanySkillService = vi.hoisted(() => ({ deleteSkill: vi.fn(), auditSkill: vi.fn(), getById: vi.fn(), + getByRouteRef: vi.fn(), installUpdate: vi.fn(), resetSkill: vi.fn(), listTestInputs: vi.fn(), @@ -314,6 +315,10 @@ describe("company skill mutation permissions", () => { beforeEach(() => { vi.clearAllMocks(); + mockCompanySkillService.getByRouteRef.mockImplementation(async (companyId: string, ref: string) => { + const skill = await mockCompanySkillService.getById(companyId, ref); + return skill ? { ...skill, id: ref } : skill; + }); mockHeartbeatFactory.mockReturnValue(mockHeartbeatService); mockCaptureRunIdentity.mockResolvedValue({ run: { responsibleUserId: "validated-owner" } }); mockSkillSourceService.sourceForSkill.mockResolvedValue(null); @@ -826,6 +831,32 @@ describe("company skill mutation permissions", () => { expect(mockCompanySkillService.scanProjectWorkspaces).not.toHaveBeenCalled(); }); + it("resolves skill slugs in the skillId param to the canonical skill id", async () => { + const skillId = "22222222-2222-4222-8222-222222222222"; + mockCompanySkillService.getByRouteRef.mockResolvedValue({ id: skillId, slug: "inbox-triage" }); + const app = await createApp(boardActor("board-user", "session")); + + await request(app) + .post("/api/companies/company-1/skills/inbox-triage/audit") + .send({}) + .expect(200); + + expect(mockCompanySkillService.getByRouteRef).toHaveBeenCalledWith("company-1", "inbox-triage"); + expect(mockCompanySkillService.auditSkill).toHaveBeenCalledWith("company-1", skillId); + }); + + it("returns 404 when the skillId param does not match any skill", async () => { + mockCompanySkillService.getByRouteRef.mockResolvedValue(null); + const app = await createApp(boardActor("board-user", "session")); + + await request(app) + .post("/api/companies/company-1/skills/missing-skill/audit") + .send({}) + .expect(404); + + expect(mockCompanySkillService.auditSkill).not.toHaveBeenCalled(); + }); + it("allows board users with skills:create to create, import, install, update, delete, audit, and reset company skills", async () => { const app = await createApp(boardActor("board-user", "session")); diff --git a/server/src/routes/company-skills.ts b/server/src/routes/company-skills.ts index 765dd3b8777..c0226d9434d 100644 --- a/server/src/routes/company-skills.ts +++ b/server/src/routes/company-skills.ts @@ -109,7 +109,9 @@ export function companySkillRoutes(db: Db, options: { pluginWorkerManager?: Plug try { if (req.actor.type === "none") throw unauthorized("Authentication required"); assertCompanyAccess(req, String(req.params.companyId)); - if (!await svc.getById(String(req.params.companyId), id)) throw notFound("Skill not found"); + const skill = await svc.getByRouteRef(String(req.params.companyId), id); + if (!skill) throw notFound("Skill not found"); + req.params.skillId = skill.id; next(); } catch (error) { next(error); } });