From ba6627fcfb10040138cba0ba3e5e2691a33e1a44 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 10:04:54 -0700 Subject: [PATCH 1/7] fix(onedrive): align tools with live Graph API docs, add missing endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - wire up search/move/copy/create_share_link tools into block operation switch (were registered but unreachable, would throw 'Invalid OneDrive operation') - fix tools.config.params to remap new canonical subBlock ids to correct tool param names - add onedrive_get_item and onedrive_get_drive_info tools (item metadata, drive quota) — both within existing Files.Read/Files.ReadWrite scope - add missing block inputs/outputs for new operations - alphabetize onedrive registry entries --- apps/sim/blocks/blocks/onedrive.ts | 302 ++++++++++++++++++- apps/sim/tools/onedrive/copy.ts | 97 ++++++ apps/sim/tools/onedrive/create_share_link.ts | 80 +++++ apps/sim/tools/onedrive/get_drive_info.ts | 64 ++++ apps/sim/tools/onedrive/get_item.ts | 84 ++++++ apps/sim/tools/onedrive/index.ts | 12 + apps/sim/tools/onedrive/move.ts | 99 ++++++ apps/sim/tools/onedrive/search.ts | 99 ++++++ apps/sim/tools/onedrive/types.ts | 68 +++++ apps/sim/tools/registry.ts | 12 + 10 files changed, 912 insertions(+), 5 deletions(-) create mode 100644 apps/sim/tools/onedrive/copy.ts create mode 100644 apps/sim/tools/onedrive/create_share_link.ts create mode 100644 apps/sim/tools/onedrive/get_drive_info.ts create mode 100644 apps/sim/tools/onedrive/get_item.ts create mode 100644 apps/sim/tools/onedrive/move.ts create mode 100644 apps/sim/tools/onedrive/search.ts diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index 30f8d82a428..bebd53343e3 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -12,10 +12,10 @@ const logger = createLogger('OneDriveBlock') export const OneDriveBlock: BlockConfig = { type: 'onedrive', name: 'OneDrive', - description: 'Create, upload, download, list, and delete files', + description: 'Create, upload, download, search, move, copy, share, and delete files', authMode: AuthMode.OAuth, longDescription: - 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list files, and delete files or folders.', + 'Integrate OneDrive into the workflow. Can create text and Excel files, upload files, download files, list and search files, move or rename files, copy files, create sharing links, and delete files or folders.', docsLink: 'https://docs.sim.ai/integrations/onedrive', category: 'tools', integrationType: IntegrationType.Documents, @@ -33,6 +33,12 @@ export const OneDriveBlock: BlockConfig = { { label: 'Upload File', id: 'upload' }, { label: 'Download File', id: 'download' }, { label: 'List Files', id: 'list' }, + { label: 'Search Files', id: 'search' }, + { label: 'Get Item Info', id: 'get_item' }, + { label: 'Get Drive Info', id: 'get_drive_info' }, + { label: 'Move/Rename File', id: 'move' }, + { label: 'Copy File', id: 'copy' }, + { label: 'Create Sharing Link', id: 'create_share_link' }, { label: 'Delete File', id: 'delete' }, ], }, @@ -231,14 +237,37 @@ export const OneDriveBlock: BlockConfig = { title: 'Search Query', type: 'short-input', placeholder: 'Search for specific files (e.g., name contains "report")', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, }, { id: 'pageSize', title: 'Results Per Page', type: 'short-input', placeholder: 'Number of results (default: 100, max: 1000)', - condition: { field: 'operation', value: 'list' }, + condition: { field: 'operation', value: ['list', 'search'] }, + }, + // Get Item Info Fields - File Selector (basic mode) + { + id: 'getItemFileSelector', + title: 'Select File or Folder', + type: 'file-selector', + canonicalParamId: 'getItemFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder (leave empty for the drive root)', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'get_item' }, + }, + { + id: 'getItemManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'getItemFileId', + placeholder: 'Enter file or folder ID (leave empty for the drive root)', + mode: 'advanced', + condition: { field: 'operation', value: 'get_item' }, }, // Download File Fields - File Selector (basic mode) { @@ -274,6 +303,170 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Optional: Override the filename', condition: { field: 'operation', value: 'download' }, }, + // Move/Rename File Fields - File Selector (basic mode) + { + id: 'moveFileSelector', + title: 'Select File or Folder to Move', + type: 'file-selector', + canonicalParamId: 'moveFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to move or rename', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'moveFileId', + placeholder: 'Enter file or folder ID to move or rename', + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + required: true, + }, + { + id: 'moveDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'moveDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'moveDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'moveDestinationFolderId', + placeholder: 'Enter destination folder ID (leave empty to only rename)', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'move' }, + }, + { + id: 'newName', + title: 'New Name', + type: 'short-input', + placeholder: 'New name for the file or folder (leave empty to only move)', + condition: { field: 'operation', value: 'move' }, + }, + // Copy File Fields - File Selector (basic mode) + { + id: 'copyFileSelector', + title: 'Select File or Folder to Copy', + type: 'file-selector', + canonicalParamId: 'copyFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to copy', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'copyFileId', + placeholder: 'Enter file or folder ID to copy', + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationFolderSelector', + title: 'Select Destination Folder', + type: 'file-selector', + canonicalParamId: 'copyDestinationFolderId', + serviceId: 'onedrive', + selectorKey: 'onedrive.folders', + requiredScopes: getScopesForService('onedrive'), + mimeType: 'application/vnd.microsoft.graph.folder', + placeholder: 'Select a destination folder', + dependsOn: ['credential'], + mode: 'basic', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'copyDestinationManualFolderId', + title: 'Destination Folder ID', + type: 'short-input', + canonicalParamId: 'copyDestinationFolderId', + placeholder: 'Enter destination folder ID', + dependsOn: ['credential'], + mode: 'advanced', + condition: { field: 'operation', value: 'copy' }, + required: true, + }, + { + id: 'destinationFileName', + title: 'New Name', + type: 'short-input', + placeholder: 'Optional name for the copy (defaults to the original name)', + condition: { field: 'operation', value: 'copy' }, + }, + // Create Sharing Link Fields - File Selector (basic mode) + { + id: 'shareLinkFileSelector', + title: 'Select File or Folder to Share', + type: 'file-selector', + canonicalParamId: 'shareLinkFileId', + serviceId: 'onedrive', + selectorKey: 'onedrive.files', + requiredScopes: getScopesForService('onedrive'), + placeholder: 'Select a file or folder to share', + mode: 'basic', + dependsOn: ['credential'], + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'shareLinkManualFileId', + title: 'File or Folder ID', + type: 'short-input', + canonicalParamId: 'shareLinkFileId', + placeholder: 'Enter file or folder ID to share', + mode: 'advanced', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkType', + title: 'Link Type', + type: 'dropdown', + options: [ + { label: 'View (read-only)', id: 'view' }, + { label: 'Edit (read-write)', id: 'edit' }, + { label: 'Embed', id: 'embed' }, + ], + placeholder: 'Select link type', + condition: { field: 'operation', value: 'create_share_link' }, + required: true, + }, + { + id: 'linkScope', + title: 'Link Scope', + type: 'dropdown', + options: [ + { label: 'Anyone with the link', id: 'anonymous' }, + { label: 'People in my organization', id: 'organization' }, + { label: 'Specific people', id: 'users' }, + ], + placeholder: 'Select who can use the link', + condition: { field: 'operation', value: 'create_share_link' }, + }, // Delete File Fields - File Selector (basic mode) { id: 'deleteFileSelector', @@ -308,6 +501,12 @@ export const OneDriveBlock: BlockConfig = { 'onedrive_create_folder', 'onedrive_download', 'onedrive_list', + 'onedrive_search', + 'onedrive_get_item', + 'onedrive_get_drive_info', + 'onedrive_move', + 'onedrive_copy', + 'onedrive_create_share_link', 'onedrive_delete', ], config: { @@ -322,6 +521,18 @@ export const OneDriveBlock: BlockConfig = { return 'onedrive_download' case 'list': return 'onedrive_list' + case 'search': + return 'onedrive_search' + case 'get_item': + return 'onedrive_get_item' + case 'get_drive_info': + return 'onedrive_get_drive_info' + case 'move': + return 'onedrive_move' + case 'copy': + return 'onedrive_copy' + case 'create_share_link': + return 'onedrive_create_share_link' case 'delete': return 'onedrive_delete' default: @@ -335,9 +546,15 @@ export const OneDriveBlock: BlockConfig = { uploadFolderId, createFolderParentId, listFolderId, + moveDestinationFolderId, + copyDestinationFolderId, // File canonical params (per-operation) downloadFileId, deleteFileId, + moveFileId, + copyFileId, + shareLinkFileId, + getItemFileId, mimeType, values, downloadFileName, @@ -377,6 +594,29 @@ export const OneDriveBlock: BlockConfig = { case 'delete': resolvedFileId = deleteFileId?.trim() || undefined break + case 'move': + resolvedFileId = moveFileId?.trim() || undefined + break + case 'copy': + resolvedFileId = copyFileId?.trim() || undefined + break + case 'create_share_link': + resolvedFileId = shareLinkFileId?.trim() || undefined + break + case 'get_item': + resolvedFileId = getItemFileId?.trim() || undefined + break + } + + // Resolve destinationFolderId based on operation + let resolvedDestinationFolderId: string | undefined + switch (params.operation) { + case 'move': + resolvedDestinationFolderId = moveDestinationFolderId?.trim() || undefined + break + case 'copy': + resolvedDestinationFolderId = copyDestinationFolderId?.trim() || undefined + break } return { @@ -386,6 +626,7 @@ export const OneDriveBlock: BlockConfig = { file: normalizedFile, folderId: resolvedFolderId, fileId: resolvedFileId, + destinationFolderId: resolvedDestinationFolderId, pageSize: rest.pageSize ? Number.parseInt(rest.pageSize as string, 10) : undefined, mimeType: mimeType, ...(downloadFileName && { fileName: downloadFileName }), @@ -411,9 +652,29 @@ export const OneDriveBlock: BlockConfig = { deleteFileId: { type: 'string', description: 'File to delete' }, downloadFileName: { type: 'string', description: 'File name override for download' }, folderName: { type: 'string', description: 'Folder name for create_folder' }, - // List operation inputs + // List / Search operation inputs query: { type: 'string', description: 'Search query' }, pageSize: { type: 'number', description: 'Results per page' }, + // Move operation inputs + moveFileId: { type: 'string', description: 'File or folder to move or rename' }, + moveDestinationFolderId: { type: 'string', description: 'Destination folder for move' }, + newName: { type: 'string', description: 'New name for move/rename' }, + // Copy operation inputs + copyFileId: { type: 'string', description: 'File or folder to copy' }, + copyDestinationFolderId: { type: 'string', description: 'Destination folder for copy' }, + destinationFileName: { type: 'string', description: 'Optional name for the copy' }, + // Create sharing link operation inputs + shareLinkFileId: { type: 'string', description: 'File or folder to share' }, + linkType: { type: 'string', description: 'Type of sharing link: view, edit, or embed' }, + linkScope: { + type: 'string', + description: 'Who can use the link: anonymous, organization, or users', + }, + // Get item operation inputs + getItemFileId: { + type: 'string', + description: 'File or folder to retrieve metadata for (empty for drive root)', + }, }, outputs: { success: { type: 'boolean', description: 'Whether the operation was successful' }, @@ -428,6 +689,37 @@ export const OneDriveBlock: BlockConfig = { description: 'An array of OneDrive file objects, each containing details such as id, name, size, and more.', }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of list/search results, if any', + }, + sourceFileId: { + type: 'string', + description: 'The ID of the file or folder that was copied', + }, + name: { + type: 'string', + description: 'The requested name for the copy, if provided', + }, + monitorUrl: { + type: 'string', + description: 'URL to poll for the status of an asynchronous copy operation', + }, + link: { + type: 'json', + description: 'The created sharing link, including its type, scope, and URL', + }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { + type: 'string', + description: 'The type of drive (e.g., "personal", "business")', + }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner' }, + quota: { + type: 'json', + description: 'Drive storage quota information (total, used, remaining, deleted, state)', + }, }, } diff --git a/apps/sim/tools/onedrive/copy.ts b/apps/sim/tools/onedrive/copy.ts new file mode 100644 index 00000000000..7353ac0fc4b --- /dev/null +++ b/apps/sim/tools/onedrive/copy.ts @@ -0,0 +1,97 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveCopyResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveCopyTool') + +/** + * Microsoft Graph processes driveItem copies asynchronously: a successful request returns + * `202 Accepted` with a `Location` header pointing to a monitor URL, not the copied item itself. + * See https://learn.microsoft.com/en-us/graph/api/driveitem-copy + */ +export const copyTool: ToolConfig = { + id: 'onedrive_copy', + name: 'Copy OneDrive File', + description: 'Copy a file or folder to another location within OneDrive', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to copy', + }, + destinationFolderId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder', + }, + destinationFileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional new name for the copy (defaults to the original name)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/copy`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + parentReference: { id: params.destinationFolderId }, + ...(params.destinationFileName && { name: params.destinationFileName }), + }), + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + if (response.status !== 202) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error?.message || 'Failed to start OneDrive copy') + } + + const monitorUrl = response.headers.get('location') || undefined + + logger.info('OneDrive copy accepted for async processing', { + fileId: params?.fileId, + monitorUrl, + }) + + return { + success: true, + output: { + sourceFileId: params?.fileId || '', + name: params?.destinationFileName, + monitorUrl, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the copy request was accepted' }, + sourceFileId: { type: 'string', description: 'The ID of the file or folder that was copied' }, + name: { type: 'string', description: 'The requested name for the copy, if provided' }, + monitorUrl: { + type: 'string', + description: + 'URL to poll for the status of the asynchronous copy operation (copy completes in the background)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/create_share_link.ts b/apps/sim/tools/onedrive/create_share_link.ts new file mode 100644 index 00000000000..ef87a0cb826 --- /dev/null +++ b/apps/sim/tools/onedrive/create_share_link.ts @@ -0,0 +1,80 @@ +import type { OneDriveShareLinkResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const createShareLinkTool: ToolConfig = { + id: 'onedrive_create_share_link', + name: 'Create OneDrive Sharing Link', + description: 'Create a view or edit sharing link for a OneDrive file or folder', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to share', + }, + linkType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Type of link to create: "view" (read-only) or "edit" (read-write)', + }, + linkScope: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Who can use the link: "anonymous" (anyone), "organization" (tenant members), or "users" (specific people)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}/createLink`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + type: params.linkType || 'view', + ...(params.linkScope && { scope: params.linkScope }), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + link: { + type: data.link?.type, + scope: data.link?.scope, + webUrl: data.link?.webUrl, + webHtml: data.link?.webHtml, + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the sharing link was created successfully' }, + link: { + type: 'object', + description: 'The created sharing link, including its type, scope, and URL', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_drive_info.ts b/apps/sim/tools/onedrive/get_drive_info.ts new file mode 100644 index 00000000000..6d282208282 --- /dev/null +++ b/apps/sim/tools/onedrive/get_drive_info.ts @@ -0,0 +1,64 @@ +import type { OneDriveGetDriveInfoResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getDriveInfoTool: ToolConfig = { + id: 'onedrive_get_drive_info', + name: 'Get OneDrive Info', + description: 'Get information about the OneDrive drive, including storage quota', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + }, + + request: { + url: () => 'https://graph.microsoft.com/v1.0/me/drive', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + driveId: data.id, + driveType: data.driveType, + webUrl: data.webUrl, + owner: data.owner?.user?.displayName ?? null, + quota: { + total: data.quota?.total ?? 0, + used: data.quota?.used ?? 0, + remaining: data.quota?.remaining ?? 0, + deleted: data.quota?.deleted ?? 0, + state: data.quota?.state ?? 'normal', + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the drive info was retrieved' }, + driveId: { type: 'string', description: 'The ID of the drive' }, + driveType: { type: 'string', description: 'The type of drive (e.g., "personal", "business")' }, + webUrl: { type: 'string', description: 'URL to the drive in the browser' }, + owner: { type: 'string', description: 'Display name of the drive owner', optional: true }, + quota: { + type: 'object', + description: 'Storage quota information in bytes (total, used, remaining, deleted, state)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/get_item.ts b/apps/sim/tools/onedrive/get_item.ts new file mode 100644 index 00000000000..59d6ebfa5a0 --- /dev/null +++ b/apps/sim/tools/onedrive/get_item.ts @@ -0,0 +1,84 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveGetItemResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const getItemTool: ToolConfig = { + id: 'onedrive_get_item', + name: 'Get OneDrive Item Metadata', + description: 'Get metadata for a specific OneDrive file or folder by ID, or the drive root', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the file or folder to retrieve (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M"). Leave empty to get the drive root folder', + }, + }, + + request: { + url: (params) => { + const fileId = params.fileId?.trim() + const baseUrl = fileId + ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(fileId)}` + : 'https://graph.microsoft.com/v1.0/me/drive/root' + + const url = new URL(baseUrl) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + ) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data: MicrosoftGraphDriveItem = await response.json() + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + webContentLink: data['@microsoft.graph.downloadUrl'], + size: data.size?.toString() || '0', + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the item metadata was retrieved' }, + file: { + type: 'object', + description: + 'The file or folder metadata, including id, name, webViewLink, size, and timestamps', + }, + }, +} diff --git a/apps/sim/tools/onedrive/index.ts b/apps/sim/tools/onedrive/index.ts index f64a78aaf28..b7b184678bc 100644 --- a/apps/sim/tools/onedrive/index.ts +++ b/apps/sim/tools/onedrive/index.ts @@ -1,11 +1,23 @@ +import { copyTool } from '@/tools/onedrive/copy' import { createFolderTool } from '@/tools/onedrive/create_folder' +import { createShareLinkTool } from '@/tools/onedrive/create_share_link' import { deleteTool } from '@/tools/onedrive/delete' import { downloadTool } from '@/tools/onedrive/download' +import { getDriveInfoTool } from '@/tools/onedrive/get_drive_info' +import { getItemTool } from '@/tools/onedrive/get_item' import { listTool } from '@/tools/onedrive/list' +import { moveTool } from '@/tools/onedrive/move' +import { searchTool } from '@/tools/onedrive/search' import { uploadTool } from '@/tools/onedrive/upload' +export const onedriveCopyTool = copyTool export const onedriveCreateFolderTool = createFolderTool +export const onedriveCreateShareLinkTool = createShareLinkTool export const onedriveDeleteTool = deleteTool export const onedriveDownloadTool = downloadTool +export const onedriveGetDriveInfoTool = getDriveInfoTool +export const onedriveGetItemTool = getItemTool export const onedriveListTool = listTool +export const onedriveMoveTool = moveTool +export const onedriveSearchTool = searchTool export const onedriveUploadTool = uploadTool diff --git a/apps/sim/tools/onedrive/move.ts b/apps/sim/tools/onedrive/move.ts new file mode 100644 index 00000000000..389cf18e15a --- /dev/null +++ b/apps/sim/tools/onedrive/move.ts @@ -0,0 +1,99 @@ +import { createLogger } from '@sim/logger' +import type { OneDriveMoveResponse, OneDriveToolParams } from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('OneDriveMoveTool') + +export const moveTool: ToolConfig = { + id: 'onedrive_move', + name: 'Move or Rename OneDrive File', + description: 'Move a file or folder to a new parent folder, rename it, or both', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + fileId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder to move or rename', + }, + destinationFolderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The ID of the destination parent folder (omit to only rename in place)', + }, + newName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'The new name for the file or folder (omit to only move)', + }, + }, + + request: { + url: (params) => + `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(params.fileId || '')}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => { + if (!params.destinationFolderId && !params.newName) { + throw new Error('Provide a destination folder, a new name, or both') + } + + return { + ...(params.destinationFolderId && { + parentReference: { id: params.destinationFolderId }, + }), + ...(params.newName && { name: params.newName }), + } + }, + }, + + transformResponse: async (response: Response, params?: OneDriveToolParams) => { + const data = await response.json() + + logger.info('Successfully moved/renamed OneDrive item', { + fileId: params?.fileId, + newName: data.name, + }) + + return { + success: true, + output: { + file: { + id: data.id, + name: data.name, + mimeType: data.file?.mimeType || (data.folder ? 'application/folder' : 'unknown'), + webViewLink: data.webUrl, + size: data.size?.toString(), + createdTime: data.createdDateTime, + modifiedTime: data.lastModifiedDateTime, + parents: data.parentReference ? [data.parentReference.id] : [], + }, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the move or rename was successful' }, + file: { + type: 'object', + description: 'The updated file object with its new name and/or parent folder', + }, + }, +} diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts new file mode 100644 index 00000000000..1f5a4cc6bec --- /dev/null +++ b/apps/sim/tools/onedrive/search.ts @@ -0,0 +1,99 @@ +import type { + MicrosoftGraphDriveItem, + OneDriveSearchResponse, + OneDriveToolParams, +} from '@/tools/onedrive/types' +import type { ToolConfig } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'onedrive_search', + name: 'Search OneDrive Files', + description: + 'Search for files and folders across OneDrive by name, metadata, or content (recursive)', + version: '1.0', + + oauth: { + required: true, + provider: 'onedrive', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the OneDrive API', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Search text matched against file name, metadata, and content', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return (e.g., 10, 50, 100)', + }, + }, + + request: { + url: (params) => { + const query = params.query?.trim() + if (!query) { + throw new Error('A search query is required') + } + + const url = new URL( + `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(query)}')` + ) + url.searchParams.append( + '$select', + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + ) + if (params.pageSize) { + url.searchParams.append('$top', Number(params.pageSize).toString()) + } + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + files: (data.value || []).map((item: MicrosoftGraphDriveItem) => ({ + id: item.id, + name: item.name, + mimeType: item.file?.mimeType || (item.folder ? 'application/folder' : 'unknown'), + webViewLink: item.webUrl, + webContentLink: item['@microsoft.graph.downloadUrl'], + size: item.size?.toString() || '0', + createdTime: item.createdDateTime, + modifiedTime: item.lastModifiedDateTime, + parents: item.parentReference ? [item.parentReference.id] : [], + })), + nextPageToken: data['@odata.nextLink'] || undefined, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the search completed successfully' }, + files: { + type: 'array', + description: 'Array of file and folder objects matching the search query', + }, + nextPageToken: { + type: 'string', + description: 'Token for retrieving the next page of results (optional)', + }, + }, +} diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 0f7a67a6212..666d0436d21 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -85,6 +85,60 @@ export interface OneDriveDeleteResponse extends ToolResponse { } } +export interface OneDriveSearchResponse extends ToolResponse { + output: { + files: OneDriveFile[] + nextPageToken?: string + } +} + +export interface OneDriveMoveResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveCopyResponse extends ToolResponse { + output: { + sourceFileId: string + name?: string + monitorUrl?: string + } +} + +export interface OneDriveShareLinkResponse extends ToolResponse { + output: { + link: { + type: string + scope?: string + webUrl: string + webHtml?: string + } + } +} + +export interface OneDriveGetItemResponse extends ToolResponse { + output: { + file: OneDriveFile + } +} + +export interface OneDriveGetDriveInfoResponse extends ToolResponse { + output: { + driveId: string + driveType: string + webUrl: string + owner: string | null + quota: { + total: number + used: number + remaining: number + deleted: number + state: string + } + } +} + export interface OneDriveToolParams { accessToken: string folderSelector?: string @@ -103,6 +157,14 @@ export interface OneDriveToolParams { values?: | (string | number | boolean | null)[][] | Array> + // Move/rename parameters + destinationFolderId?: string + newName?: string + // Copy parameters + destinationFileName?: string + // Sharing link parameters + linkType?: 'view' | 'edit' | 'embed' + linkScope?: 'anonymous' | 'organization' | 'users' } export type OneDriveResponse = @@ -110,3 +172,9 @@ export type OneDriveResponse = | OneDriveDownloadResponse | OneDriveListResponse | OneDriveDeleteResponse + | OneDriveSearchResponse + | OneDriveMoveResponse + | OneDriveCopyResponse + | OneDriveShareLinkResponse + | OneDriveGetItemResponse + | OneDriveGetDriveInfoResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index bb6174de162..93790d0ff96 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2481,10 +2481,16 @@ import { oktaUpdateUserTool, } from '@/tools/okta' import { + onedriveCopyTool, onedriveCreateFolderTool, + onedriveCreateShareLinkTool, onedriveDeleteTool, onedriveDownloadTool, + onedriveGetDriveInfoTool, + onedriveGetItemTool, onedriveListTool, + onedriveMoveTool, + onedriveSearchTool, onedriveUploadTool, } from '@/tools/onedrive' import { @@ -7552,10 +7558,16 @@ export const tools: Record = { shopify_create_fulfillment: shopifyCreateFulfillmentTool, shopify_list_collections: shopifyListCollectionsTool, shopify_get_collection: shopifyGetCollectionTool, + onedrive_copy: onedriveCopyTool, onedrive_create_folder: onedriveCreateFolderTool, + onedrive_create_share_link: onedriveCreateShareLinkTool, onedrive_delete: onedriveDeleteTool, onedrive_download: onedriveDownloadTool, + onedrive_get_drive_info: onedriveGetDriveInfoTool, + onedrive_get_item: onedriveGetItemTool, onedrive_list: onedriveListTool, + onedrive_move: onedriveMoveTool, + onedrive_search: onedriveSearchTool, onedrive_upload: onedriveUploadTool, microsoft_dataverse_associate: dataverseAssociateTool, microsoft_dataverse_create_multiple: dataverseCreateMultipleTool, From 3e3cb9a989e1c2e45be137f4e7582ddf69fb4aa6 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 10:16:26 -0700 Subject: [PATCH 2/7] fix(onedrive): escape single quotes in search query, document embed link type - encodeURIComponent doesn't escape single quotes, breaking the OData string literal for filenames containing an apostrophe - clarify create_share_link's linkType description to include 'embed', which the block UI already exposes as a valid option --- apps/sim/tools/onedrive/create_share_link.ts | 2 +- apps/sim/tools/onedrive/search.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/onedrive/create_share_link.ts b/apps/sim/tools/onedrive/create_share_link.ts index ef87a0cb826..c1c8c508855 100644 --- a/apps/sim/tools/onedrive/create_share_link.ts +++ b/apps/sim/tools/onedrive/create_share_link.ts @@ -29,7 +29,7 @@ export const createShareLinkTool: ToolConfig } const url = new URL( - `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(query)}')` + `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(query).replace(/'/g, '%27')}')` ) url.searchParams.append( '$select', From fb38f2946b1b28d5f51cbd7ca2b736b2357952c5 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 10:30:10 -0700 Subject: [PATCH 3/7] fix(onedrive): add real pagination continuation to search - add pageToken param that follows the @odata.nextLink continuation URL directly, so nextPageToken output is actually consumable instead of a dead end - wire pageToken through the block as an advanced-mode field on the search operation --- apps/sim/blocks/blocks/onedrive.ts | 12 ++++++++++++ apps/sim/tools/onedrive/search.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index bebd53343e3..be41343c35c 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -246,6 +246,14 @@ export const OneDriveBlock: BlockConfig = { placeholder: 'Number of results (default: 100, max: 1000)', condition: { field: 'operation', value: ['list', 'search'] }, }, + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Continuation URL from a previous search response (nextPageToken)', + mode: 'advanced', + condition: { field: 'operation', value: 'search' }, + }, // Get Item Info Fields - File Selector (basic mode) { id: 'getItemFileSelector', @@ -655,6 +663,10 @@ export const OneDriveBlock: BlockConfig = { // List / Search operation inputs query: { type: 'string', description: 'Search query' }, pageSize: { type: 'number', description: 'Results per page' }, + pageToken: { + type: 'string', + description: 'Continuation URL for the next page of search results', + }, // Move operation inputs moveFileId: { type: 'string', description: 'File or folder to move or rename' }, moveDestinationFolderId: { type: 'string', description: 'Destination folder for move' }, diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts index 142f3b71c12..bb132ea31bc 100644 --- a/apps/sim/tools/onedrive/search.ts +++ b/apps/sim/tools/onedrive/search.ts @@ -36,10 +36,22 @@ export const searchTool: ToolConfig visibility: 'user-or-llm', description: 'Maximum number of results to return (e.g., 10, 50, 100)', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, }, request: { url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + return pageToken + } + const query = params.query?.trim() if (!query) { throw new Error('A search query is required') From b6c14aee3f59f7fa6901a15c37af6ba621b19122 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 10:41:32 -0700 Subject: [PATCH 4/7] fix(onedrive): prevent SSRF/token exfiltration via search pageToken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pageToken was used verbatim as the request URL with no host validation, while the Authorization header is always attached — a crafted pageToken pointing at an attacker-controlled host would leak the OAuth access token. Pin the continuation URL to graph.microsoft.com before using it. --- apps/sim/tools/onedrive/search.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts index bb132ea31bc..08ed0c066b0 100644 --- a/apps/sim/tools/onedrive/search.ts +++ b/apps/sim/tools/onedrive/search.ts @@ -49,7 +49,11 @@ export const searchTool: ToolConfig url: (params) => { const pageToken = params.pageToken?.trim() if (pageToken) { - return pageToken + const continuationUrl = new URL(pageToken) + if (continuationUrl.hostname !== 'graph.microsoft.com') { + throw new Error('Invalid page token: must be a Microsoft Graph continuation URL') + } + return continuationUrl.toString() } const query = params.query?.trim() From 8a0c2c9c24213de580809978c7ef516e5796b04c Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:37:14 -0700 Subject: [PATCH 5/7] fix(onedrive): make search query optional for pageToken-only continuation requests --- apps/sim/tools/onedrive/search.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts index 08ed0c066b0..bccdb81f0e5 100644 --- a/apps/sim/tools/onedrive/search.ts +++ b/apps/sim/tools/onedrive/search.ts @@ -26,9 +26,10 @@ export const searchTool: ToolConfig }, query: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', - description: 'Search text matched against file name, metadata, and content', + description: + 'Search text matched against file name, metadata, and content. Not required when paginating with pageToken', }, pageSize: { type: 'number', From 64d380525c7a6f84bca3e22aafe58c751d221341 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 11:44:27 -0700 Subject: [PATCH 6/7] fix(onedrive): fix folder-targeting wiring, OData escaping, and downloadUrl selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - upload/create_folder/list all read params.folderSelector/manualFolderId, but the block only ever sends folderId — folder targeting silently fell back to drive root. Consolidate to a single folderId param matching what the block sends (same pattern already used for destinationFolderId on move/copy) - search's percent-encode-then-replace('%27') "escape" is undone by Graph's standard URL-decode-before-parse, so an apostrophe in a query still breaks the OData string literal; list's $filter had no escaping at all. Both now double literal quotes (the correct OData V4 escape) via a shared escapeODataStringLiteral helper before encoding - get_item/list/search all omitted @microsoft.graph.downloadUrl from $select, so webContentLink was always undefined on their outputs; added it - list gains the same pageToken continuation support search already has, and the block's Page Token field is now shown for both operations --- apps/sim/blocks/blocks/onedrive.ts | 6 ++--- apps/sim/tools/onedrive/create_folder.ts | 10 ++----- apps/sim/tools/onedrive/get_item.ts | 2 +- apps/sim/tools/onedrive/list.ts | 34 +++++++++++++++++------- apps/sim/tools/onedrive/search.ts | 5 ++-- apps/sim/tools/onedrive/types.ts | 3 +-- apps/sim/tools/onedrive/upload.ts | 14 +++------- apps/sim/tools/onedrive/utils.ts | 9 +++++++ 8 files changed, 47 insertions(+), 36 deletions(-) diff --git a/apps/sim/blocks/blocks/onedrive.ts b/apps/sim/blocks/blocks/onedrive.ts index be41343c35c..e87f0212674 100644 --- a/apps/sim/blocks/blocks/onedrive.ts +++ b/apps/sim/blocks/blocks/onedrive.ts @@ -250,9 +250,9 @@ export const OneDriveBlock: BlockConfig = { id: 'pageToken', title: 'Page Token', type: 'short-input', - placeholder: 'Continuation URL from a previous search response (nextPageToken)', + placeholder: 'Continuation URL from a previous response (nextPageToken)', mode: 'advanced', - condition: { field: 'operation', value: 'search' }, + condition: { field: 'operation', value: ['list', 'search'] }, }, // Get Item Info Fields - File Selector (basic mode) { @@ -665,7 +665,7 @@ export const OneDriveBlock: BlockConfig = { pageSize: { type: 'number', description: 'Results per page' }, pageToken: { type: 'string', - description: 'Continuation URL for the next page of search results', + description: 'Continuation URL for the next page of list or search results', }, // Move operation inputs moveFileId: { type: 'string', description: 'File or folder to move or rename' }, diff --git a/apps/sim/tools/onedrive/create_folder.ts b/apps/sim/tools/onedrive/create_folder.ts index 1aa799084b1..3c250ca05d8 100644 --- a/apps/sim/tools/onedrive/create_folder.ts +++ b/apps/sim/tools/onedrive/create_folder.ts @@ -25,25 +25,19 @@ export const createFolderTool: ToolConfig { // Use specific parent folder URL if parentId is provided - const parentFolderId = params.manualFolderId || params.folderSelector + const parentFolderId = params.folderId?.trim() if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}/children` } diff --git a/apps/sim/tools/onedrive/get_item.ts b/apps/sim/tools/onedrive/get_item.ts index 59d6ebfa5a0..ab0a9436b91 100644 --- a/apps/sim/tools/onedrive/get_item.ts +++ b/apps/sim/tools/onedrive/get_item.ts @@ -42,7 +42,7 @@ export const getItemTool: ToolConfig = { @@ -23,18 +24,12 @@ export const listTool: ToolConfig = { visibility: 'hidden', description: 'The access token for the OneDrive API', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to list files from (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'The manually entered folder ID (advanced mode)', - }, query: { type: 'string', required: false, @@ -47,12 +42,28 @@ export const listTool: ToolConfig = { visibility: 'user-or-llm', description: 'Maximum number of files to return (e.g., 10, 50, 100)', }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + "Continuation URL from a previous response's nextPageToken, used to fetch the next page", + }, }, request: { url: (params) => { + const pageToken = params.pageToken?.trim() + if (pageToken) { + const continuationUrl = new URL(pageToken) + if (continuationUrl.hostname !== 'graph.microsoft.com') { + throw new Error('Invalid page token: must be a Microsoft Graph continuation URL') + } + return continuationUrl.toString() + } + // Use specific folder if provided, otherwise use root - const folderId = params.manualFolderId || params.folderSelector + const folderId = params.folderId?.trim() const encodedFolderId = folderId ? encodeURIComponent(folderId) : '' const baseUrl = encodedFolderId ? `https://graph.microsoft.com/v1.0/me/drive/items/${encodedFolderId}/children` @@ -63,12 +74,15 @@ export const listTool: ToolConfig = { // Use Microsoft Graph $select parameter url.searchParams.append( '$select', - 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' ) // Add name filter if query provided if (params.query) { - url.searchParams.append('$filter', `startswith(name,'${params.query}')`) + url.searchParams.append( + '$filter', + `startswith(name,'${escapeODataStringLiteral(params.query)}')` + ) } // Add pagination diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts index bccdb81f0e5..1c2c947fbd3 100644 --- a/apps/sim/tools/onedrive/search.ts +++ b/apps/sim/tools/onedrive/search.ts @@ -3,6 +3,7 @@ import type { OneDriveSearchResponse, OneDriveToolParams, } from '@/tools/onedrive/types' +import { escapeODataStringLiteral } from '@/tools/onedrive/utils' import type { ToolConfig } from '@/tools/types' export const searchTool: ToolConfig = { @@ -63,11 +64,11 @@ export const searchTool: ToolConfig } const url = new URL( - `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(query).replace(/'/g, '%27')}')` + `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(escapeODataStringLiteral(query))}')` ) url.searchParams.append( '$select', - 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference' + 'id,name,file,folder,webUrl,size,createdDateTime,lastModifiedDateTime,parentReference,@microsoft.graph.downloadUrl' ) if (params.pageSize) { url.searchParams.append('$top', Number(params.pageSize).toString()) diff --git a/apps/sim/tools/onedrive/types.ts b/apps/sim/tools/onedrive/types.ts index 666d0436d21..dd3d3e4d464 100644 --- a/apps/sim/tools/onedrive/types.ts +++ b/apps/sim/tools/onedrive/types.ts @@ -141,8 +141,7 @@ export interface OneDriveGetDriveInfoResponse extends ToolResponse { export interface OneDriveToolParams { accessToken: string - folderSelector?: string - manualFolderId?: string + folderId?: string folderName?: string fileId?: string fileName?: string diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 09090e2868e..9478fdca285 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -47,18 +47,12 @@ export const uploadTool: ToolConfig description: 'The MIME type of the file to create (e.g., text/plain for .txt, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet for .xlsx)', }, - folderSelector: { + folderId: { type: 'string', required: false, visibility: 'user-or-llm', description: 'Folder ID to upload the file to (e.g., "01BYE5RZ6QN3ZWBTUFOFD3GSPGOHDJD36M")', }, - manualFolderId: { - type: 'string', - required: false, - visibility: 'hidden', - description: 'Manually entered folder ID (advanced mode)', - }, }, request: { @@ -75,8 +69,8 @@ export const uploadTool: ToolConfig fileName = `${fileName.replace(/\.[^.]*$/, '')}.txt` } - const parentFolderId = params.manualFolderId || params.folderSelector - if (parentFolderId && parentFolderId.trim() !== '') { + const parentFolderId = params.folderId?.trim() + if (parentFolderId) { return `https://graph.microsoft.com/v1.0/me/drive/items/${encodeURIComponent(parentFolderId)}:/${fileName}:/content` } return `https://graph.microsoft.com/v1.0/me/drive/root:/${fileName}:/content` @@ -108,7 +102,7 @@ export const uploadTool: ToolConfig accessToken: params.accessToken, fileName: params.fileName, file: params.file, - folderId: params.manualFolderId || params.folderSelector, + folderId: params.folderId, ...(params.mimeType && { mimeType: params.mimeType }), ...(params.values && { values: params.values }), } diff --git a/apps/sim/tools/onedrive/utils.ts b/apps/sim/tools/onedrive/utils.ts index e1b595f944b..9d3760bef65 100644 --- a/apps/sim/tools/onedrive/utils.ts +++ b/apps/sim/tools/onedrive/utils.ts @@ -1,5 +1,14 @@ import type { OneDriveToolParams } from '@/tools/onedrive/types' +/** + * Escapes a value for embedding in an OData string literal (e.g. search(q='...'), + * $filter startswith(name,'...')). OData V4 escapes a literal single quote by doubling + * it — encodeURIComponent does not touch `'`, so this must run before URL-encoding. + */ +export function escapeODataStringLiteral(value: string): string { + return value.replace(/'/g, "''") +} + export type ExcelCell = string | number | boolean | null export type ExcelArrayValues = ExcelCell[][] export type ExcelObjectValues = Array> From 5584abee589c76674d1187da7a937836c238d045 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 12:05:04 -0700 Subject: [PATCH 7/7] fix(onedrive): reuse shared assertGraphNextPageUrl for page-token validation Hostname-only check let http://graph.microsoft.com/... continuation tokens through, which would send the Bearer token over cleartext. Sibling Graph integrations (sharepoint, microsoft_teams, microsoft_planner, microsoft_ad) already share assertGraphNextPageUrl/getGraphNextPageUrl in tools/sharepoint/utils.ts, which checks the full origin (scheme + host). Reuse it in list/search instead of hand-rolling the check twice. --- apps/sim/tools/onedrive/list.ts | 9 +++------ apps/sim/tools/onedrive/search.ts | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/sim/tools/onedrive/list.ts b/apps/sim/tools/onedrive/list.ts index 906a4c1bd97..6b79fff7de6 100644 --- a/apps/sim/tools/onedrive/list.ts +++ b/apps/sim/tools/onedrive/list.ts @@ -4,6 +4,7 @@ import type { OneDriveToolParams, } from '@/tools/onedrive/types' import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const listTool: ToolConfig = { @@ -55,11 +56,7 @@ export const listTool: ToolConfig = { url: (params) => { const pageToken = params.pageToken?.trim() if (pageToken) { - const continuationUrl = new URL(pageToken) - if (continuationUrl.hostname !== 'graph.microsoft.com') { - throw new Error('Invalid page token: must be a Microsoft Graph continuation URL') - } - return continuationUrl.toString() + return assertGraphNextPageUrl(pageToken) } // Use specific folder if provided, otherwise use root @@ -116,7 +113,7 @@ export const listTool: ToolConfig = { parents: item.parentReference ? [item.parentReference.id] : [], })), // Use the actual @odata.nextLink URL as the continuation token - nextPageToken: data['@odata.nextLink'] || undefined, + nextPageToken: getGraphNextPageUrl(data), }, } }, diff --git a/apps/sim/tools/onedrive/search.ts b/apps/sim/tools/onedrive/search.ts index 1c2c947fbd3..f16850acd03 100644 --- a/apps/sim/tools/onedrive/search.ts +++ b/apps/sim/tools/onedrive/search.ts @@ -4,6 +4,7 @@ import type { OneDriveToolParams, } from '@/tools/onedrive/types' import { escapeODataStringLiteral } from '@/tools/onedrive/utils' +import { assertGraphNextPageUrl, getGraphNextPageUrl } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' export const searchTool: ToolConfig = { @@ -51,11 +52,7 @@ export const searchTool: ToolConfig url: (params) => { const pageToken = params.pageToken?.trim() if (pageToken) { - const continuationUrl = new URL(pageToken) - if (continuationUrl.hostname !== 'graph.microsoft.com') { - throw new Error('Invalid page token: must be a Microsoft Graph continuation URL') - } - return continuationUrl.toString() + return assertGraphNextPageUrl(pageToken) } const query = params.query?.trim() @@ -98,7 +95,7 @@ export const searchTool: ToolConfig modifiedTime: item.lastModifiedDateTime, parents: item.parentReference ? [item.parentReference.id] : [], })), - nextPageToken: data['@odata.nextLink'] || undefined, + nextPageToken: getGraphNextPageUrl(data), }, } },