diff --git a/app/api/tasks/runs/route.ts b/app/api/tasks/runs/route.ts index b9c876022..132811b0a 100644 --- a/app/api/tasks/runs/route.ts +++ b/app/api/tasks/runs/route.ts @@ -17,14 +17,16 @@ export async function OPTIONS() { /** * GET /api/tasks/runs * - * Retrieves the status of a Trigger.dev task run. - * Returns one of three possible statuses: - * - pending: Task is still running - * - complete: Task completed successfully with data - * - failed: Task failed with error message + * Retrieves task runs from Trigger.dev. + * Supports two modes: + * - Retrieve mode: when `runId` is provided, returns a single run in `runs: [run]` + * - List mode: when `runId` is omitted, returns recent runs for the authorized account in `runs: []` * * Query parameters: - * - runId (required): The unique identifier of the task run + * - runId (optional): The unique identifier of the task run + * - account_id (optional): Scope to this account when allowed: admin, org-authorized member, or + * personal key with account_id equal to the authenticated account (self-access). + * - limit (optional): Number of runs to return in list mode (default 20, max 100) * * @param request - The request object containing query parameters. * @returns A NextResponse with task run status. diff --git a/lib/tasks/__tests__/getTaskRunHandler.test.ts b/lib/tasks/__tests__/getTaskRunHandler.test.ts index 9f17fffce..ca84ce235 100644 --- a/lib/tasks/__tests__/getTaskRunHandler.test.ts +++ b/lib/tasks/__tests__/getTaskRunHandler.test.ts @@ -61,7 +61,11 @@ describe("getTaskRunHandler", () => { describe("retrieve mode", () => { it("wraps a single run in { status, runs[] }", async () => { - vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ mode: "retrieve", runId: "run_123" }); + vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ + mode: "retrieve", + runId: "run_123", + accountId: "acc_123", + }); vi.mocked(retrieveTaskRun).mockResolvedValue(mockRun); const response = await getTaskRunHandler(createMockRequest()); @@ -74,7 +78,11 @@ describe("getTaskRunHandler", () => { }); it("returns 404 when run is not found", async () => { - vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ mode: "retrieve", runId: "run_x" }); + vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ + mode: "retrieve", + runId: "run_x", + accountId: "acc_123", + }); vi.mocked(retrieveTaskRun).mockResolvedValue(null); const response = await getTaskRunHandler(createMockRequest()); @@ -82,7 +90,11 @@ describe("getTaskRunHandler", () => { }); it("returns 500 when retrieveTaskRun throws", async () => { - vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ mode: "retrieve", runId: "run_123" }); + vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ + mode: "retrieve", + runId: "run_123", + accountId: "acc_123", + }); vi.mocked(retrieveTaskRun).mockRejectedValue(new Error("API error")); const response = await getTaskRunHandler(createMockRequest()); @@ -90,6 +102,21 @@ describe("getTaskRunHandler", () => { const json = await response.json(); expect(json.error).toBe("API error"); }); + + it("returns 404 when run does not belong to the authorized account (no enumeration)", async () => { + vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ + mode: "retrieve", + runId: "run_123", + accountId: "acc_999", + }); + vi.mocked(retrieveTaskRun).mockResolvedValue(mockRun); + + const response = await getTaskRunHandler(createMockRequest()); + const json = await response.json(); + + expect(response.status).toBe(404); + expect(json.error).toBe("Task run not found"); + }); }); describe("list mode", () => { diff --git a/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts b/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts index f7126175d..830a10be5 100644 --- a/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts +++ b/lib/tasks/__tests__/validateGetTaskRunQuery.test.ts @@ -100,7 +100,7 @@ describe("validateGetTaskRunQuery", () => { const result = await validateGetTaskRunQuery(request); expect(result).not.toBeInstanceOf(NextResponse); - expect(result).toEqual({ mode: "retrieve", runId: "run_abc123" }); + expect(result).toEqual({ mode: "retrieve", runId: "run_abc123", accountId: "acc_123" }); }); it("trims whitespace from runId", async () => { @@ -111,7 +111,7 @@ describe("validateGetTaskRunQuery", () => { const result = await validateGetTaskRunQuery(request); expect(result).not.toBeInstanceOf(NextResponse); - expect(result).toEqual({ mode: "retrieve", runId: "run_abc123" }); + expect(result).toEqual({ mode: "retrieve", runId: "run_abc123", accountId: "acc_123" }); }); it("returns list mode with custom limit", async () => { @@ -210,5 +210,24 @@ describe("validateGetTaskRunQuery", () => { expect(result).not.toBeInstanceOf(NextResponse); expect(result).toEqual({ mode: "list", accountId: "acc_123", limit: 20 }); }); + + it("applies account_id override in retrieve mode for admin", async () => { + vi.mocked(validateAuthContext).mockResolvedValue({ + accountId: "admin_acc", + orgId: null, + authToken: "bearer-token", + }); + vi.mocked(checkIsAdmin).mockResolvedValue(true); + + const request = createMockRequest( + "http://localhost:3000/api/tasks/runs?runId=run_123&account_id=other_acc", + ); + + const result = await validateGetTaskRunQuery(request); + + expect(result).not.toBeInstanceOf(NextResponse); + expect(result).toEqual({ mode: "retrieve", runId: "run_123", accountId: "other_acc" }); + expect(validateAccountIdOverride).not.toHaveBeenCalled(); + }); }); }); diff --git a/lib/tasks/getTaskRunHandler.ts b/lib/tasks/getTaskRunHandler.ts index f988c6b6a..8cd23db05 100644 --- a/lib/tasks/getTaskRunHandler.ts +++ b/lib/tasks/getTaskRunHandler.ts @@ -41,6 +41,17 @@ export async function getTaskRunHandler(request: NextRequest): Promise