From 72ea7e75d0c98cc58a49f0e9d7b06752fae7658e Mon Sep 17 00:00:00 2001 From: pradipthaadhi Date: Thu, 7 May 2026 21:32:14 +0700 Subject: [PATCH 1/2] feat(api): enhance task run retrieval with account access control - Updated the GET /api/tasks/runs endpoint to support two modes: - Retrieve mode: returns a single task run when `runId` is provided. - List mode: returns recent task runs for the authorized account when `runId` is omitted. - Added optional query parameters for `account_id` and `limit` to control access and pagination. - Implemented account access validation to ensure users can only retrieve their own task runs, returning a 403 error for unauthorized access. - Updated related validation and test cases to reflect these changes. --- app/api/tasks/runs/route.ts | 13 ++++---- lib/tasks/__tests__/getTaskRunHandler.test.ts | 33 +++++++++++++++++-- .../__tests__/validateGetTaskRunQuery.test.ts | 23 +++++++++++-- lib/tasks/getTaskRunHandler.ts | 11 +++++++ lib/tasks/validateGetTaskRunQuery.ts | 10 +++--- 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/app/api/tasks/runs/route.ts b/app/api/tasks/runs/route.ts index b9c876022..2a2eca18f 100644 --- a/app/api/tasks/runs/route.ts +++ b/app/api/tasks/runs/route.ts @@ -17,14 +17,15 @@ 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): Account override (admin/org-authorized only) + * - 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..0d3fcbf03 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 403 when run does not belong to the authorized account", 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(403); + expect(json.error).toBe("Access denied to this task run"); + }); }); 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..17e4afcde 100644 --- a/lib/tasks/getTaskRunHandler.ts +++ b/lib/tasks/getTaskRunHandler.ts @@ -41,6 +41,17 @@ export async function getTaskRunHandler(request: NextRequest): Promise Date: Fri, 8 May 2026 01:11:56 +0700 Subject: [PATCH 2/2] refactor(api): enhance task run retrieval and error handling - Updated documentation for the `account_id` query parameter to clarify its usage for account scoping. - Changed error response from 403 to 404 when access to a task run is denied, improving clarity for clients. - Modified the validation response to include `accountId` in the retrieve mode, aligning with the updated query structure. - Updated tests to reflect changes in error handling and response structure, ensuring consistency across the API. --- app/api/tasks/runs/route.ts | 3 ++- lib/tasks/__tests__/getTaskRunHandler.test.ts | 6 +++--- lib/tasks/getTaskRunHandler.ts | 4 ++-- lib/tasks/validateGetTaskRunQuery.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/api/tasks/runs/route.ts b/app/api/tasks/runs/route.ts index 2a2eca18f..132811b0a 100644 --- a/app/api/tasks/runs/route.ts +++ b/app/api/tasks/runs/route.ts @@ -24,7 +24,8 @@ export async function OPTIONS() { * * Query parameters: * - runId (optional): The unique identifier of the task run - * - account_id (optional): Account override (admin/org-authorized only) + * - 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. diff --git a/lib/tasks/__tests__/getTaskRunHandler.test.ts b/lib/tasks/__tests__/getTaskRunHandler.test.ts index 0d3fcbf03..ca84ce235 100644 --- a/lib/tasks/__tests__/getTaskRunHandler.test.ts +++ b/lib/tasks/__tests__/getTaskRunHandler.test.ts @@ -103,7 +103,7 @@ describe("getTaskRunHandler", () => { expect(json.error).toBe("API error"); }); - it("returns 403 when run does not belong to the authorized account", async () => { + it("returns 404 when run does not belong to the authorized account (no enumeration)", async () => { vi.mocked(validateGetTaskRunQuery).mockResolvedValue({ mode: "retrieve", runId: "run_123", @@ -114,8 +114,8 @@ describe("getTaskRunHandler", () => { const response = await getTaskRunHandler(createMockRequest()); const json = await response.json(); - expect(response.status).toBe(403); - expect(json.error).toBe("Access denied to this task run"); + expect(response.status).toBe(404); + expect(json.error).toBe("Task run not found"); }); }); diff --git a/lib/tasks/getTaskRunHandler.ts b/lib/tasks/getTaskRunHandler.ts index 17e4afcde..8cd23db05 100644 --- a/lib/tasks/getTaskRunHandler.ts +++ b/lib/tasks/getTaskRunHandler.ts @@ -47,8 +47,8 @@ export async function getTaskRunHandler(request: NextRequest): Promise