fix(server): resolve company context for single-company actors on project refs

A board actor with exactly one membership can resolve non-UUID project
references without a ?companyId= query — shortnames only resolve inside a
company anyway. Keeps the 404 for refs that still cannot resolve, and
updates route-test mocks to model resolveByReference's real behavior
instead of relying on the unreachable getById fall-through.

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Waseem Ilyas 2026-09-10 23:23:56 +00:00
parent ca6a6cacf3
commit 5d66168ba6
4 changed files with 12 additions and 3 deletions

View File

@ -117,6 +117,7 @@ function buildApp(routerFactory: (app: express.Express) => void) {
(req as any).actor = {
type: "board",
userId: "user-1",
companyIds: ["company-1"],
source: "local_implicit",
};
next();
@ -170,6 +171,10 @@ describe.sequential("execution environment route guards", () => {
mockProjectService.createWorkspace.mockReset();
mockProjectService.remove.mockReset();
mockProjectService.resolveByReference.mockReset();
mockProjectService.resolveByReference.mockResolvedValue({
ambiguous: false,
project: { id: "project-1" },
});
mockProjectService.listWorkspaces.mockReset();
mockIssueService.create.mockReset();
mockIssueService.getById.mockReset();

View File

@ -158,7 +158,7 @@ describe("project env routes", () => {
explanation: "Allowed by test mock.",
});
mockGetTelemetryClient.mockReturnValue({ track: vi.fn() });
mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null });
mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: buildProject() });
mockProjectService.createWorkspace.mockResolvedValue(null);
mockProjectService.listWorkspaces.mockResolvedValue([]);
mockEnvironmentService.getById.mockReset();

View File

@ -203,7 +203,7 @@ describe("project workspace host-path floor", () => {
explanation: "Allowed by test mock.",
});
mockGetTelemetryClient.mockReturnValue({ track: vi.fn() });
mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: null });
mockProjectService.resolveByReference.mockResolvedValue({ ambiguous: false, project: buildProject() });
mockProjectService.getById.mockResolvedValue(buildProject());
mockProjectService.create.mockResolvedValue(buildProject());
mockProjectService.createWorkspace.mockResolvedValue(buildWorkspace());

View File

@ -116,7 +116,11 @@ export function projectRoutes(db: Db) {
if (req.actor.type === "agent" && req.actor.companyId) {
return req.actor.companyId;
}
return null;
// A single-company actor (the common self-hosted case) has an unambiguous
// company context without a `?companyId=` query — shortnames only resolve
// inside one company anyway, so require exactly one.
const actorCompanyIds = req.actor.companyIds ?? [];
return actorCompanyIds.length === 1 ? actorCompanyIds[0] : null;
}
async function normalizeProjectReference(req: Request, rawId: string) {