From 6346576a9535e282de2230e1a55dc79837e6bd93 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 8 Jul 2026 16:41:09 -0500 Subject: [PATCH] fix(connectors): stop granting act-as-artist authority off the flat account_artist_ids roster edge (P0b) Interim cut per chat#1860 decision (2026-07-08): connector authorize, execute, disconnect, and the getComposioTools artist branch now require self, org co-membership, workspace ownership, org membership, or RECOUP_ORG staff - never a bare account_artist_ids roster row. checkAccountArtistAccess is unchanged; read/analytics paths keep roster-based access. Co-Authored-By: Claude Fable 5 --- .../__tests__/checkConnectorAuthority.test.ts | 136 ++++++++++++++++++ lib/composio/checkConnectorAuthority.ts | 59 ++++++++ .../validateAuthorizeConnectorRequest.test.ts | 23 ++- ...validateDisconnectConnectorRequest.test.ts | 16 +-- ...idateExecuteConnectorActionRequest.test.ts | 12 +- .../validateAuthorizeConnectorRequest.ts | 6 +- .../validateDisconnectConnectorRequest.ts | 6 +- .../validateExecuteConnectorActionRequest.ts | 6 +- .../__tests__/getComposioTools.test.ts | 18 +-- lib/composio/toolRouter/getComposioTools.ts | 4 +- 10 files changed, 239 insertions(+), 47 deletions(-) create mode 100644 lib/composio/__tests__/checkConnectorAuthority.test.ts create mode 100644 lib/composio/checkConnectorAuthority.ts diff --git a/lib/composio/__tests__/checkConnectorAuthority.test.ts b/lib/composio/__tests__/checkConnectorAuthority.test.ts new file mode 100644 index 000000000..866ca9f41 --- /dev/null +++ b/lib/composio/__tests__/checkConnectorAuthority.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { checkConnectorAuthority } from "../checkConnectorAuthority"; + +import { getAccountOrganizations } from "@/lib/supabase/account_organization_ids/getAccountOrganizations"; +import { selectArtistOrganizationIds } from "@/lib/supabase/artist_organization_ids/selectArtistOrganizationIds"; +import { selectAccountOrganizationIds } from "@/lib/supabase/account_organization_ids/selectAccountOrganizationIds"; +import { selectAccountWorkspaceId } from "@/lib/supabase/account_workspace_ids/selectAccountWorkspaceId"; +import { validateOrganizationAccess } from "@/lib/organizations/validateOrganizationAccess"; +import { selectAccountArtistId } from "@/lib/supabase/account_artist_ids/selectAccountArtistId"; + +vi.mock("@/lib/const", () => ({ + RECOUP_ORG_ID: "recoup-admin-org-id", +})); + +vi.mock("@/lib/supabase/account_organization_ids/getAccountOrganizations", () => ({ + getAccountOrganizations: vi.fn(), +})); + +vi.mock("@/lib/supabase/artist_organization_ids/selectArtistOrganizationIds", () => ({ + selectArtistOrganizationIds: vi.fn(), +})); + +vi.mock("@/lib/supabase/account_organization_ids/selectAccountOrganizationIds", () => ({ + selectAccountOrganizationIds: vi.fn(), +})); + +vi.mock("@/lib/supabase/account_workspace_ids/selectAccountWorkspaceId", () => ({ + selectAccountWorkspaceId: vi.fn(), +})); + +vi.mock("@/lib/organizations/validateOrganizationAccess", () => ({ + validateOrganizationAccess: vi.fn(), +})); + +vi.mock("@/lib/supabase/account_artist_ids/selectAccountArtistId", () => ({ + selectAccountArtistId: vi.fn(), +})); + +describe("checkConnectorAuthority", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getAccountOrganizations).mockResolvedValue([]); + vi.mocked(selectArtistOrganizationIds).mockResolvedValue([]); + vi.mocked(selectAccountOrganizationIds).mockResolvedValue([]); + vi.mocked(selectAccountWorkspaceId).mockResolvedValue(null); + vi.mocked(validateOrganizationAccess).mockResolvedValue(false); + }); + + it("grants self access without any database calls", async () => { + const result = await checkConnectorAuthority("account-123", "account-123"); + + expect(result).toBe(true); + expect(getAccountOrganizations).not.toHaveBeenCalled(); + expect(selectArtistOrganizationIds).not.toHaveBeenCalled(); + }); + + it("denies a roster-only manager (bare account_artist_ids row, no org tie)", async () => { + // Even if a roster row exists, connector authority must never read it. + vi.mocked(selectAccountArtistId).mockResolvedValue({ + id: "aai-1", + artist_id: "artist-456", + pinned: false, + }); + + const result = await checkConnectorAuthority("account-123", "artist-456"); + + expect(result).toBe(false); + expect(selectAccountArtistId).not.toHaveBeenCalled(); + }); + + it("grants access when caller and artist share an organization", async () => { + vi.mocked(selectArtistOrganizationIds).mockResolvedValue([{ organization_id: "org-1" }]); + vi.mocked(selectAccountOrganizationIds).mockResolvedValue([{ organization_id: "org-1" }]); + + const result = await checkConnectorAuthority("account-123", "artist-456"); + + expect(selectArtistOrganizationIds).toHaveBeenCalledWith("artist-456"); + expect(selectAccountOrganizationIds).toHaveBeenCalledWith("account-123", ["org-1"]); + expect(result).toBe(true); + }); + + it("grants RECOUP_ORG staff access to any target", async () => { + vi.mocked(getAccountOrganizations).mockResolvedValue([ + { + id: "aoi-1", + account_id: "admin-account", + organization_id: "recoup-admin-org-id", + updated_at: null, + organization: null, + }, + ]); + + const result = await checkConnectorAuthority("admin-account", "artist-456"); + + expect(result).toBe(true); + expect(selectArtistOrganizationIds).not.toHaveBeenCalled(); + }); + + it("grants access to a workspace the caller owns", async () => { + vi.mocked(selectAccountWorkspaceId).mockResolvedValue({ workspace_id: "workspace-789" }); + + const result = await checkConnectorAuthority("account-123", "workspace-789"); + + expect(selectAccountWorkspaceId).toHaveBeenCalledWith("account-123", "workspace-789"); + expect(result).toBe(true); + }); + + it("grants access to an organization the caller belongs to", async () => { + vi.mocked(validateOrganizationAccess).mockResolvedValue(true); + + const result = await checkConnectorAuthority("account-123", "org-target"); + + expect(validateOrganizationAccess).toHaveBeenCalledWith({ + accountId: "account-123", + organizationId: "org-target", + }); + expect(result).toBe(true); + }); + + it("fails closed when artist org lookup errors", async () => { + vi.mocked(selectArtistOrganizationIds).mockResolvedValue(null); + + const result = await checkConnectorAuthority("account-123", "artist-456"); + + expect(result).toBe(false); + }); + + it("fails closed when account org lookup errors", async () => { + vi.mocked(selectArtistOrganizationIds).mockResolvedValue([{ organization_id: "org-1" }]); + vi.mocked(selectAccountOrganizationIds).mockResolvedValue(null); + + const result = await checkConnectorAuthority("account-123", "artist-456"); + + expect(result).toBe(false); + }); +}); diff --git a/lib/composio/checkConnectorAuthority.ts b/lib/composio/checkConnectorAuthority.ts new file mode 100644 index 000000000..816eb1ec2 --- /dev/null +++ b/lib/composio/checkConnectorAuthority.ts @@ -0,0 +1,59 @@ +import { RECOUP_ORG_ID } from "@/lib/const"; +import { getAccountOrganizations } from "@/lib/supabase/account_organization_ids/getAccountOrganizations"; +import { selectArtistOrganizationIds } from "@/lib/supabase/artist_organization_ids/selectArtistOrganizationIds"; +import { selectAccountOrganizationIds } from "@/lib/supabase/account_organization_ids/selectAccountOrganizationIds"; +import { selectAccountWorkspaceId } from "@/lib/supabase/account_workspace_ids/selectAccountWorkspaceId"; +import { validateOrganizationAccess } from "@/lib/organizations/validateOrganizationAccess"; + +/** + * Check whether a caller has act-as authority over a target account for + * connector operations (authorize, execute, disconnect, tool exposure). + * + * Interim P0b cut (chat#1860): unlike `checkAccountArtistAccess`, this + * NEVER reads the bare `account_artist_ids` roster edge — a roster row + * alone must not grant use of another account's OAuth connectors. + * + * Authority is granted when: + * 1. Target is the caller itself, OR + * 2. Caller is a RECOUP_ORG member (admin bypass), OR + * 3. Caller shares an organization with the target artist, OR + * 4. Target is a workspace the caller owns, OR + * 5. Target is an organization the caller belongs to + * + * Fails closed: any database error results in denied authority. + * + * @param callerAccountId - The authenticated caller's account ID + * @param targetAccountId - The account whose connectors are being used + * @returns true if the caller has connector authority over the target + */ +export async function checkConnectorAuthority( + callerAccountId: string, + targetAccountId: string, +): Promise { + // 1. Self-access + if (targetAccountId === callerAccountId) return true; + + // 2. Admin bypass: RECOUP_ORG staff + const callerOrgs = await getAccountOrganizations({ accountId: callerAccountId }); + if (callerOrgs.some(m => m.organization_id === RECOUP_ORG_ID)) return true; + + // 3. Org co-membership with a target artist + const targetOrgs = await selectArtistOrganizationIds(targetAccountId); + if (targetOrgs?.length) { + const orgIds = targetOrgs.map(o => o.organization_id).filter((id): id is string => Boolean(id)); + if (orgIds.length) { + const sharedOrgs = await selectAccountOrganizationIds(callerAccountId, orgIds); + if (sharedOrgs?.length) return true; + } + } + + // 4. Workspace owned by the caller + const isWorkspace = await selectAccountWorkspaceId(callerAccountId, targetAccountId); + if (isWorkspace) return true; + + // 5. Organization the caller belongs to + return validateOrganizationAccess({ + accountId: callerAccountId, + organizationId: targetAccountId, + }); +} diff --git a/lib/composio/connectors/__tests__/validateAuthorizeConnectorRequest.test.ts b/lib/composio/connectors/__tests__/validateAuthorizeConnectorRequest.test.ts index aa29798de..aff74a7f0 100644 --- a/lib/composio/connectors/__tests__/validateAuthorizeConnectorRequest.test.ts +++ b/lib/composio/connectors/__tests__/validateAuthorizeConnectorRequest.test.ts @@ -3,14 +3,14 @@ import { NextRequest, NextResponse } from "next/server"; import { validateAuthorizeConnectorRequest } from "../validateAuthorizeConnectorRequest"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../../checkConnectorAuthority"; vi.mock("@/lib/auth/validateAuthContext", () => ({ validateAuthContext: vi.fn(), })); -vi.mock("@/lib/auth/checkAccountAccess", () => ({ - checkAccountAccess: vi.fn(), +vi.mock("../../checkConnectorAuthority", () => ({ + checkConnectorAuthority: vi.fn(), })); vi.mock("@/lib/networking/getCorsHeaders", () => ({ @@ -68,7 +68,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "artist" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", @@ -76,7 +76,7 @@ describe("validateAuthorizeConnectorRequest", () => { }); const result = await validateAuthorizeConnectorRequest(request); - expect(checkAccountAccess).toHaveBeenCalledWith(mockAccountId, mockTargetAccountId); + expect(checkConnectorAuthority).toHaveBeenCalledWith(mockAccountId, mockTargetAccountId); expect(result).not.toBeInstanceOf(NextResponse); expect(result).toEqual({ accountId: mockTargetAccountId, @@ -94,7 +94,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "artist" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", @@ -120,7 +120,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "workspace" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", @@ -145,10 +145,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ - hasAccess: true, - entityType: "organization", - }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", @@ -173,7 +170,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: false }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(false); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", @@ -197,7 +194,7 @@ describe("validateAuthorizeConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "artist" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors/authorize", { method: "POST", diff --git a/lib/composio/connectors/__tests__/validateDisconnectConnectorRequest.test.ts b/lib/composio/connectors/__tests__/validateDisconnectConnectorRequest.test.ts index aa7ed6ca5..6ed2663d3 100644 --- a/lib/composio/connectors/__tests__/validateDisconnectConnectorRequest.test.ts +++ b/lib/composio/connectors/__tests__/validateDisconnectConnectorRequest.test.ts @@ -3,15 +3,15 @@ import { NextRequest, NextResponse } from "next/server"; import { validateDisconnectConnectorRequest } from "../validateDisconnectConnectorRequest"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../../checkConnectorAuthority"; import { verifyConnectorOwnership } from "../verifyConnectorOwnership"; vi.mock("@/lib/auth/validateAuthContext", () => ({ validateAuthContext: vi.fn(), })); -vi.mock("@/lib/auth/checkAccountAccess", () => ({ - checkAccountAccess: vi.fn(), +vi.mock("../../checkConnectorAuthority", () => ({ + checkConnectorAuthority: vi.fn(), })); vi.mock("../verifyConnectorOwnership", () => ({ @@ -91,7 +91,7 @@ describe("validateDisconnectConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "artist" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors", { method: "DELETE", @@ -99,7 +99,7 @@ describe("validateDisconnectConnectorRequest", () => { }); const result = await validateDisconnectConnectorRequest(request); - expect(checkAccountAccess).toHaveBeenCalledWith("account-123", mockTargetAccountId); + expect(checkConnectorAuthority).toHaveBeenCalledWith("account-123", mockTargetAccountId); expect(verifyConnectorOwnership).not.toHaveBeenCalled(); expect(result).not.toBeInstanceOf(NextResponse); expect(result).toEqual({ @@ -115,7 +115,7 @@ describe("validateDisconnectConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "workspace" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const request = new NextRequest("http://localhost/api/connectors", { method: "DELETE", @@ -123,7 +123,7 @@ describe("validateDisconnectConnectorRequest", () => { }); const result = await validateDisconnectConnectorRequest(request); - expect(checkAccountAccess).toHaveBeenCalledWith("account-123", mockTargetAccountId); + expect(checkConnectorAuthority).toHaveBeenCalledWith("account-123", mockTargetAccountId); expect(result).not.toBeInstanceOf(NextResponse); }); @@ -134,7 +134,7 @@ describe("validateDisconnectConnectorRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: false }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(false); const request = new NextRequest("http://localhost/api/connectors", { method: "DELETE", diff --git a/lib/composio/connectors/__tests__/validateExecuteConnectorActionRequest.test.ts b/lib/composio/connectors/__tests__/validateExecuteConnectorActionRequest.test.ts index 6c36b78b0..3c73dc271 100644 --- a/lib/composio/connectors/__tests__/validateExecuteConnectorActionRequest.test.ts +++ b/lib/composio/connectors/__tests__/validateExecuteConnectorActionRequest.test.ts @@ -3,14 +3,14 @@ import { NextRequest, NextResponse } from "next/server"; import { validateExecuteConnectorActionRequest } from "../validateExecuteConnectorActionRequest"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../../checkConnectorAuthority"; vi.mock("@/lib/auth/validateAuthContext", () => ({ validateAuthContext: vi.fn(), })); -vi.mock("@/lib/auth/checkAccountAccess", () => ({ - checkAccountAccess: vi.fn(), +vi.mock("../../checkConnectorAuthority", () => ({ + checkConnectorAuthority: vi.fn(), })); vi.mock("@/lib/networking/getCorsHeaders", () => ({ @@ -68,13 +68,13 @@ describe("validateExecuteConnectorActionRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: true, entityType: "artist" }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); const result = await validateExecuteConnectorActionRequest( buildRequest({ ...validBody, account_id: target }), ); - expect(checkAccountAccess).toHaveBeenCalledWith("account-123", target); + expect(checkConnectorAuthority).toHaveBeenCalledWith("account-123", target); expect(result).toEqual({ accountId: target, actionSlug: "GMAIL_FETCH_EMAILS", @@ -89,7 +89,7 @@ describe("validateExecuteConnectorActionRequest", () => { orgId: null, authToken: "test-token", }); - vi.mocked(checkAccountAccess).mockResolvedValue({ hasAccess: false }); + vi.mocked(checkConnectorAuthority).mockResolvedValue(false); const result = await validateExecuteConnectorActionRequest( buildRequest({ ...validBody, account_id: target }), diff --git a/lib/composio/connectors/validateAuthorizeConnectorRequest.ts b/lib/composio/connectors/validateAuthorizeConnectorRequest.ts index db48694c6..553f8f2b0 100644 --- a/lib/composio/connectors/validateAuthorizeConnectorRequest.ts +++ b/lib/composio/connectors/validateAuthorizeConnectorRequest.ts @@ -3,7 +3,7 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; import { validateAuthorizeConnectorBody } from "./validateAuthorizeConnectorBody"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../checkConnectorAuthority"; import { scopedAuthConfigs } from "../toolRouter/scopedAuthConfigs"; /** @@ -55,8 +55,8 @@ export async function validateAuthorizeConnectorRequest( // 3. If account_id is provided, verify access and use that entity if (account_id) { - const accessResult = await checkAccountAccess(accountId, account_id); - if (!accessResult.hasAccess) { + const hasAuthority = await checkConnectorAuthority(accountId, account_id); + if (!hasAuthority) { return NextResponse.json( { error: "Access denied to this account" }, { status: 403, headers }, diff --git a/lib/composio/connectors/validateDisconnectConnectorRequest.ts b/lib/composio/connectors/validateDisconnectConnectorRequest.ts index 5c39de414..f405913a9 100644 --- a/lib/composio/connectors/validateDisconnectConnectorRequest.ts +++ b/lib/composio/connectors/validateDisconnectConnectorRequest.ts @@ -3,7 +3,7 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; import { validateDisconnectConnectorBody } from "./validateDisconnectConnectorBody"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../checkConnectorAuthority"; import { verifyConnectorOwnership } from "./verifyConnectorOwnership"; /** @@ -48,8 +48,8 @@ export async function validateDisconnectConnectorRequest( // 3. Verify access if (account_id) { // Disconnecting for another account - verify access to that account - const accessResult = await checkAccountAccess(accountId, account_id); - if (!accessResult.hasAccess) { + const hasAuthority = await checkConnectorAuthority(accountId, account_id); + if (!hasAuthority) { return NextResponse.json( { error: "Access denied to this account" }, { status: 403, headers }, diff --git a/lib/composio/connectors/validateExecuteConnectorActionRequest.ts b/lib/composio/connectors/validateExecuteConnectorActionRequest.ts index 36e7b8442..2744b4e8e 100644 --- a/lib/composio/connectors/validateExecuteConnectorActionRequest.ts +++ b/lib/composio/connectors/validateExecuteConnectorActionRequest.ts @@ -3,7 +3,7 @@ import { NextResponse } from "next/server"; import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { validateAuthContext } from "@/lib/auth/validateAuthContext"; import { validateExecuteConnectorActionBody } from "./validateExecuteConnectorActionBody"; -import { checkAccountAccess } from "@/lib/auth/checkAccountAccess"; +import { checkConnectorAuthority } from "../checkConnectorAuthority"; /** * Validated params for executing a connector action. @@ -47,8 +47,8 @@ export async function validateExecuteConnectorActionRequest( // 3. If account_id is provided, verify access and use that entity if (account_id) { - const accessResult = await checkAccountAccess(accountId, account_id); - if (!accessResult.hasAccess) { + const hasAuthority = await checkConnectorAuthority(accountId, account_id); + if (!hasAuthority) { return NextResponse.json( { error: "Access denied to this account" }, { status: 403, headers }, diff --git a/lib/composio/toolRouter/__tests__/getComposioTools.test.ts b/lib/composio/toolRouter/__tests__/getComposioTools.test.ts index ae8d5d718..44b194f4f 100644 --- a/lib/composio/toolRouter/__tests__/getComposioTools.test.ts +++ b/lib/composio/toolRouter/__tests__/getComposioTools.test.ts @@ -5,7 +5,7 @@ import { getComposioClient } from "../../client"; import { getCallbackUrl } from "../../getCallbackUrl"; import { getConnectors } from "../../connectors/getConnectors"; import { getSharedAccountConnections } from "../getSharedAccountConnections"; -import { checkAccountArtistAccess } from "@/lib/artists/checkAccountArtistAccess"; +import { checkConnectorAuthority } from "../../checkConnectorAuthority"; vi.mock("../../client", () => ({ getComposioClient: vi.fn() })); vi.mock("../../getCallbackUrl", () => ({ getCallbackUrl: vi.fn() })); @@ -13,8 +13,8 @@ vi.mock("../../connectors/getConnectors", () => ({ getConnectors: vi.fn() })); vi.mock("../getSharedAccountConnections", () => ({ getSharedAccountConnections: vi.fn(), })); -vi.mock("@/lib/artists/checkAccountArtistAccess", () => ({ - checkAccountArtistAccess: vi.fn(), +vi.mock("../../checkConnectorAuthority", () => ({ + checkConnectorAuthority: vi.fn(), })); const mockTool = (name = "mock") => ({ @@ -94,7 +94,7 @@ describe("getComposioTools", () => { }); it("fetches artist's real tools when artistId is in scope", async () => { - vi.mocked(checkAccountArtistAccess).mockResolvedValue(true); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); vi.mocked(getConnectors).mockImplementation(async (id: string) => id === "artist-456" ? [{ slug: "tiktok", name: "TikTok", isConnected: true, connectedAccountId: "ca_tt" }] @@ -129,7 +129,7 @@ describe("getComposioTools", () => { }); it("artist tools win over customer's on toolkit overlap", async () => { - vi.mocked(checkAccountArtistAccess).mockResolvedValue(true); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); vi.mocked(getConnectors).mockImplementation(async (id: string) => { const tiktok = { slug: "tiktok", @@ -179,8 +179,8 @@ describe("getComposioTools", () => { expect(result.GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT).toBe(customerDocs); }); - it("does not fetch artist tools when access is denied", async () => { - vi.mocked(checkAccountArtistAccess).mockResolvedValue(false); + it("does not fetch artist tools for a roster-only caller (authority denied)", async () => { + vi.mocked(checkConnectorAuthority).mockResolvedValue(false); vi.mocked(getConnectors).mockImplementation(async (id: string) => id === "artist-456" ? [{ slug: "tiktok", name: "TikTok", isConnected: true, connectedAccountId: "ca_tt" }] @@ -189,7 +189,7 @@ describe("getComposioTools", () => { await getComposioTools("account-123", "artist-456"); - expect(checkAccountArtistAccess).toHaveBeenCalledWith("account-123", "artist-456"); + expect(checkConnectorAuthority).toHaveBeenCalledWith("account-123", "artist-456"); const artistCalls = mockToolsGet.mock.calls.filter(([id]) => id === "artist-456"); expect(artistCalls).toHaveLength(0); }); @@ -218,7 +218,7 @@ describe("getComposioTools", () => { }); it("still returns meta-tools when an owner fetch rejects", async () => { - vi.mocked(checkAccountArtistAccess).mockResolvedValue(true); + vi.mocked(checkConnectorAuthority).mockResolvedValue(true); vi.mocked(getConnectors).mockImplementation(async (id: string) => id === "artist-456" ? [{ slug: "tiktok", name: "TikTok", isConnected: true, connectedAccountId: "ca_tt" }] diff --git a/lib/composio/toolRouter/getComposioTools.ts b/lib/composio/toolRouter/getComposioTools.ts index 7f74b4c2f..8c2f1aac4 100644 --- a/lib/composio/toolRouter/getComposioTools.ts +++ b/lib/composio/toolRouter/getComposioTools.ts @@ -2,7 +2,7 @@ import type { ToolSet } from "ai"; import { getComposioClient } from "../client"; import { getCallbackUrl } from "../getCallbackUrl"; import { getConnectors } from "../connectors/getConnectors"; -import { checkAccountArtistAccess } from "@/lib/artists/checkAccountArtistAccess"; +import { checkConnectorAuthority } from "../checkConnectorAuthority"; import { getSharedAccountConnections } from "./getSharedAccountConnections"; import { pickValid } from "./pickValid"; import { scopedAuthConfigs } from "./scopedAuthConfigs"; @@ -46,7 +46,7 @@ export async function getComposioTools( try { const effectiveArtistId = - artistId && (await checkAccountArtistAccess(accountId, artistId)) ? artistId : undefined; + artistId && (await checkConnectorAuthority(accountId, artistId)) ? artistId : undefined; const composio = await getComposioClient(); const callbackUrl = getCallbackUrl({ destination: "chat", roomId });