Skip to content
Merged

Test #690

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f99277e
feat(measurement-jobs): free-tier card gate (setup mode) + instant ba…
sweetmantech Jun 16, 2026
158a1d4
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 16, 2026
5fa5e3a
fix(songstats-backfill): backoff on 429 + defer instead of churn (cha…
sweetmantech Jun 16, 2026
1e9deac
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 16, 2026
5f45946
refactor(songstats): remove local quota ledger + budget gate (chat#17…
sweetmantech Jun 16, 2026
5472795
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 16, 2026
f24d4c7
feat: POST /api/catalogs (create + materialize from valuation snapsho…
sweetmantech Jun 18, 2026
ace0b01
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 18, 2026
a520a1d
fix: LEFT-join artists in catalog-songs read (materialized tracks wer…
sweetmantech Jun 18, 2026
76b5739
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 18, 2026
709ea0c
feat: add X (Twitter) + LinkedIn to the Composio connector whitelist …
sweetmantech Jun 18, 2026
8a3c3cb
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 18, 2026
66cc2fe
chore: remove unused ALLOWED_ARTIST_CONNECTORS from api (chat#1793) (…
sweetmantech Jun 18, 2026
79c22da
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 18, 2026
6fc10cc
fix: enrich valuation-captured songs (artists + notes) so they render…
sweetmantech Jun 18, 2026
0e42d02
Merge remote-tracking branch 'origin/main' into test
sweetmantech Jun 18, 2026
2fa7029
fix(tasks): let admins fetch any task by id alone (cross-account read…
sweetmantech Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions lib/tasks/__tests__/validateGetTasksQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,41 @@ describe("validateGetTasksQuery", () => {
});
});

it("lets an admin fetch any task by id alone, dropping account scope", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: "worker_admin_acc",
orgId: null,
authToken: "token",
});
vi.mocked(checkIsAdmin).mockResolvedValue(true);

const result = await validateGetTasksQuery(
createMockRequest("http://localhost:3000/api/tasks?id=task_owned_by_someone_else"),
);

expect(checkIsAdmin).toHaveBeenCalledWith("worker_admin_acc");
expect(validateAccountIdOverride).not.toHaveBeenCalled();
// No account_id key -> selectScheduledActions filters by id only (cross-account read).
expect(result).toEqual({ id: "task_owned_by_someone_else" });
});

it("keeps a non-admin id lookup scoped to the authenticated account", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: "regular_acc",
orgId: null,
authToken: "token",
});
vi.mocked(checkIsAdmin).mockResolvedValue(false);

const result = await validateGetTasksQuery(
createMockRequest("http://localhost:3000/api/tasks?id=task_1"),
);

expect(checkIsAdmin).toHaveBeenCalledWith("regular_acc");
expect(validateAccountIdOverride).not.toHaveBeenCalled();
expect(result).toEqual({ id: "task_1", account_id: "regular_acc" });
});

it("returns 403 when non-admin override is denied", async () => {
vi.mocked(validateAuthContext).mockResolvedValue({
accountId: "org_owner_acc",
Expand Down
16 changes: 13 additions & 3 deletions lib/tasks/validateGetTasksQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export const getTasksQuerySchema = z.object({
});

export type GetTasksQuery = z.infer<typeof getTasksQuerySchema>;
export type ValidatedGetTasksQuery = Omit<GetTasksQuery, "account_id"> & { account_id: string };
// account_id is optional: admin callers fetching a single task by `id` are not
// scoped to their own account, so the resolved query may omit account_id entirely.
export type ValidatedGetTasksQuery = Omit<GetTasksQuery, "account_id"> & { account_id?: string };

/**
* Validates get tasks query parameters from a NextRequest.
Expand Down Expand Up @@ -55,7 +57,7 @@ export async function validateGetTasksQuery(
);
}

let targetAccountId = authResult.accountId;
let targetAccountId: string | undefined = authResult.accountId;

if (
validationResult.data.account_id &&
Expand All @@ -76,10 +78,18 @@ export async function validateGetTasksQuery(

targetAccountId = overrideResult.accountId;
}
} else if (validationResult.data.id && !validationResult.data.account_id) {
// Single-task lookup by id: admins may read any task regardless of owner
// (e.g. the background worker fetching a customer's scheduled task config).
// Non-admins stay scoped to their own account.
const isAdmin = await checkIsAdmin(authResult.accountId);
if (isAdmin) {
targetAccountId = undefined;
}
}

return {
...validationResult.data,
account_id: targetAccountId,
...(targetAccountId ? { account_id: targetAccountId } : {}),
};
}
Loading