From 4f19d8447c7364e2c4fe370cb5fe5d2b7d0a2072 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:18:49 -0700 Subject: [PATCH 1/6] feat(sharepoint): validate against Graph API and add delete/update/download tools - Fix bugs found validating existing tools against live Graph docs: malformed nested $expand syntax in get_list, reversed site-resolution precedence in create_list, unsanitized field fallback in add_list_items, missing URL encoding in read_page, and an incorrect root/serverRelativeUrl field shape - Fix V1 block gaps vs V2: upload_file required flag, missing required on pageName/listDisplayName, wrong site-picker mimeType, dead subBlock refs - Add 8 new tools within already-granted scopes for CRUD completion: delete_list_item, get_list_item, delete_page, update_page, publish_page, download_file, delete_file, get_drive_item - Add opt-in stripAuthOnRedirect option to secureFetchWithPinnedIP so the SharePoint file-download route doesn't resend the bearer token to the preauthenticated redirect target (default off, no behavior change elsewhere) - Dedupe read-only list-item field sanitization into a shared utils helper --- .../tools/sharepoint/download-file/route.ts | 176 +++++++++++ apps/sim/blocks/blocks/sharepoint.ts | 297 +++++++++++++++--- apps/sim/lib/api/contracts/tools/microsoft.ts | 15 + .../core/security/input-validation.server.ts | 11 +- apps/sim/tools/registry.ts | 16 + apps/sim/tools/sharepoint/add_list_items.ts | 105 ++----- apps/sim/tools/sharepoint/create_list.ts | 2 +- apps/sim/tools/sharepoint/delete_file.ts | 66 ++++ apps/sim/tools/sharepoint/delete_list_item.ts | 88 ++++++ apps/sim/tools/sharepoint/delete_page.ts | 72 +++++ apps/sim/tools/sharepoint/download_file.ts | 59 ++++ apps/sim/tools/sharepoint/get_drive_item.ts | 106 +++++++ apps/sim/tools/sharepoint/get_list.ts | 81 ++--- apps/sim/tools/sharepoint/get_list_item.ts | 96 ++++++ apps/sim/tools/sharepoint/index.ts | 18 +- apps/sim/tools/sharepoint/list_sites.ts | 6 +- apps/sim/tools/sharepoint/publish_page.ts | 72 +++++ apps/sim/tools/sharepoint/read_page.ts | 14 +- apps/sim/tools/sharepoint/types.ts | 132 +++++--- apps/sim/tools/sharepoint/update_list.ts | 53 +--- apps/sim/tools/sharepoint/update_page.ts | 168 ++++++++++ apps/sim/tools/sharepoint/utils.ts | 51 +++ scripts/check-api-validation-contracts.ts | 4 +- 23 files changed, 1452 insertions(+), 256 deletions(-) create mode 100644 apps/sim/app/api/tools/sharepoint/download-file/route.ts create mode 100644 apps/sim/tools/sharepoint/delete_file.ts create mode 100644 apps/sim/tools/sharepoint/delete_list_item.ts create mode 100644 apps/sim/tools/sharepoint/delete_page.ts create mode 100644 apps/sim/tools/sharepoint/download_file.ts create mode 100644 apps/sim/tools/sharepoint/get_drive_item.ts create mode 100644 apps/sim/tools/sharepoint/get_list_item.ts create mode 100644 apps/sim/tools/sharepoint/publish_page.ts create mode 100644 apps/sim/tools/sharepoint/update_page.ts diff --git a/apps/sim/app/api/tools/sharepoint/download-file/route.ts b/apps/sim/app/api/tools/sharepoint/download-file/route.ts new file mode 100644 index 00000000000..6f3bb947770 --- /dev/null +++ b/apps/sim/app/api/tools/sharepoint/download-file/route.ts @@ -0,0 +1,176 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { type NextRequest, NextResponse } from 'next/server' +import { sharepointDownloadFileContract } from '@/lib/api/contracts/tools/microsoft' +import { parseRequest } from '@/lib/api/server' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { generateRequestId } from '@/lib/core/utils/request' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +/** Microsoft Graph API error response structure */ +interface GraphApiError { + error?: { + code?: string + message?: string + } +} + +/** Microsoft Graph API drive item metadata response */ +interface DriveItemMetadata { + id?: string + name?: string + folder?: Record + file?: { + mimeType?: string + } +} + +const logger = createLogger('SharepointDownloadFileAPI') + +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + + if (!authResult.success) { + logger.warn(`[${requestId}] Unauthorized SharePoint download attempt: ${authResult.error}`) + return NextResponse.json( + { + success: false, + error: authResult.error || 'Authentication required', + }, + { status: 401 } + ) + } + + const parsed = await parseRequest(sharepointDownloadFileContract, request, {}) + if (!parsed.success) return parsed.response + const { accessToken, driveId, itemId, fileName } = parsed.data.body + const authHeader = `Bearer ${accessToken}` + + logger.info(`[${requestId}] Getting file metadata from SharePoint`, { driveId, itemId }) + + const metadataUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}` + const metadataUrlValidation = await validateUrlWithDNS(metadataUrl, 'metadataUrl') + if (!metadataUrlValidation.isValid) { + return NextResponse.json( + { success: false, error: metadataUrlValidation.error }, + { status: 400 } + ) + } + + const metadataResponse = await secureFetchWithPinnedIP( + metadataUrl, + metadataUrlValidation.resolvedIP!, + { + headers: { Authorization: authHeader }, + } + ) + + if (!metadataResponse.ok) { + const errorDetails = (await metadataResponse.json().catch(() => ({}))) as GraphApiError + logger.error(`[${requestId}] Failed to get file metadata`, { + status: metadataResponse.status, + error: errorDetails, + }) + return NextResponse.json( + { success: false, error: errorDetails.error?.message || 'Failed to get file metadata' }, + { status: 400 } + ) + } + + const metadata = (await metadataResponse.json()) as DriveItemMetadata + + if (metadata.folder && !metadata.file) { + logger.error(`[${requestId}] Attempted to download a folder`, { + itemId: metadata.id, + itemName: metadata.name, + }) + return NextResponse.json( + { + success: false, + error: `Cannot download folder "${metadata.name}". Please select a file instead.`, + }, + { status: 400 } + ) + } + + const mimeType = metadata.file?.mimeType || 'application/octet-stream' + + logger.info(`[${requestId}] Downloading file from SharePoint`, { driveId, itemId, mimeType }) + + const downloadUrl = `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(itemId)}/content` + const downloadUrlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl') + if (!downloadUrlValidation.isValid) { + return NextResponse.json( + { success: false, error: downloadUrlValidation.error }, + { status: 400 } + ) + } + + const downloadResponse = await secureFetchWithPinnedIP( + downloadUrl, + downloadUrlValidation.resolvedIP!, + { + headers: { Authorization: authHeader }, + // The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth. + stripAuthOnRedirect: true, + } + ) + + if (!downloadResponse.ok) { + const downloadError = (await downloadResponse.json().catch(() => ({}))) as GraphApiError + logger.error(`[${requestId}] Failed to download file`, { + status: downloadResponse.status, + error: downloadError, + }) + return NextResponse.json( + { success: false, error: downloadError.error?.message || 'Failed to download file' }, + { status: 400 } + ) + } + + const arrayBuffer = await downloadResponse.arrayBuffer() + const fileBuffer = Buffer.from(arrayBuffer) + + const resolvedName = fileName || metadata.name || 'download' + + logger.info(`[${requestId}] File downloaded successfully`, { + driveId, + itemId, + name: resolvedName, + size: fileBuffer.length, + mimeType, + }) + + const base64Data = fileBuffer.toString('base64') + + return NextResponse.json({ + success: true, + output: { + file: { + name: resolvedName, + mimeType, + data: base64Data, + size: fileBuffer.length, + }, + }, + }) + } catch (error) { + logger.error(`[${requestId}] Error downloading SharePoint file:`, error) + return NextResponse.json( + { + success: false, + error: getErrorMessage(error, 'Unknown error occurred'), + }, + { status: 500 } + ) + } +}) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 489b45ebcee..84b1ba230fd 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -30,12 +30,20 @@ export const SharepointBlock: BlockConfig = { options: [ { label: 'Create Page', id: 'create_page' }, { label: 'Read Page', id: 'read_page' }, + { label: 'Update Page', id: 'update_page' }, + { label: 'Publish Page', id: 'publish_page' }, + { label: 'Delete Page', id: 'delete_page' }, { label: 'List Sites', id: 'list_sites' }, { label: 'Create List', id: 'create_list' }, { label: 'Read List', id: 'read_list' }, { label: 'Update List', id: 'update_list' }, { label: 'Add List Items', id: 'add_list_items' }, + { label: 'Get List Item', id: 'get_list_item' }, + { label: 'Delete List Item', id: 'delete_list_item' }, { label: 'Upload File', id: 'upload_file' }, + { label: 'Download File', id: 'download_file' }, + { label: 'Get Drive Item', id: 'get_drive_item' }, + { label: 'Delete File', id: 'delete_file' }, ], }, { @@ -65,7 +73,7 @@ export const SharepointBlock: BlockConfig = { serviceId: 'sharepoint', selectorKey: 'sharepoint.sites', requiredScopes: getScopesForService('sharepoint'), - mimeType: 'application/vnd.microsoft.graph.folder', + mimeType: 'application/vnd.microsoft.graph.site', placeholder: 'Select a site', dependsOn: ['credential'], mode: 'basic', @@ -74,11 +82,16 @@ export const SharepointBlock: BlockConfig = { value: [ 'create_page', 'read_page', + 'update_page', + 'publish_page', + 'delete_page', 'list_sites', 'create_list', 'read_list', 'update_list', 'add_list_items', + 'get_list_item', + 'delete_list_item', 'upload_file', ], }, @@ -90,14 +103,37 @@ export const SharepointBlock: BlockConfig = { type: 'short-input', placeholder: 'Name of the page', condition: { field: 'operation', value: ['create_page', 'read_page'] }, + required: { field: 'operation', value: 'create_page' }, + }, + + { + id: 'pageTitle', + title: 'Page Title', + type: 'short-input', + placeholder: 'Optional title (defaults to page name)', + condition: { field: 'operation', value: ['create_page', 'update_page'] }, + mode: 'advanced', + }, + + { + id: 'pageContent', + title: 'Page Content', + type: 'long-input', + placeholder: 'Optional text content for the page', + condition: { field: 'operation', value: ['create_page', 'update_page'] }, + mode: 'advanced', }, { id: 'pageId', title: 'Page ID', type: 'short-input', - placeholder: 'Page ID (alternative to page name)', - condition: { field: 'operation', value: 'read_page' }, + placeholder: 'Page ID', + condition: { + field: 'operation', + value: ['read_page', 'update_page', 'publish_page', 'delete_page'], + }, + required: { field: 'operation', value: ['update_page', 'publish_page', 'delete_page'] }, mode: 'advanced', }, @@ -111,7 +147,14 @@ export const SharepointBlock: BlockConfig = { placeholder: 'Select a list', dependsOn: ['credential', 'siteSelector'], mode: 'basic', - condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, + condition: { + field: 'operation', + value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'], + }, + required: { + field: 'operation', + value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'], + }, }, { id: 'listId', @@ -120,7 +163,14 @@ export const SharepointBlock: BlockConfig = { canonicalParamId: 'listId', placeholder: 'Enter list ID (GUID). Required for Update; optional for Read.', mode: 'advanced', - condition: { field: 'operation', value: ['read_list', 'update_list', 'add_list_items'] }, + condition: { + field: 'operation', + value: ['read_list', 'update_list', 'add_list_items', 'get_list_item', 'delete_list_item'], + }, + required: { + field: 'operation', + value: ['update_list', 'add_list_items', 'get_list_item', 'delete_list_item'], + }, }, { @@ -129,7 +179,11 @@ export const SharepointBlock: BlockConfig = { type: 'short-input', placeholder: 'Enter item ID', canonicalParamId: 'itemId', - condition: { field: 'operation', value: ['update_list'] }, + condition: { + field: 'operation', + value: ['update_list', 'get_list_item', 'delete_list_item'], + }, + required: { field: 'operation', value: ['get_list_item', 'delete_list_item'] }, }, { @@ -138,6 +192,7 @@ export const SharepointBlock: BlockConfig = { type: 'short-input', placeholder: 'Name of the list', condition: { field: 'operation', value: 'create_list' }, + required: { field: 'operation', value: 'create_list' }, }, { @@ -270,11 +325,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, value: [ 'create_page', 'read_page', + 'update_page', + 'publish_page', + 'delete_page', 'list_sites', 'create_list', 'read_list', 'update_list', 'add_list_items', + 'get_list_item', + 'delete_list_item', 'upload_file', ], }, @@ -330,16 +390,29 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, }, }, - // Upload File operation fields + // Upload / Download / Delete File / Get Drive Item operation fields { id: 'driveId', title: 'Document Library ID', type: 'short-input', placeholder: 'Enter document library (drive) ID', canonicalParamId: 'driveId', - condition: { field: 'operation', value: 'upload_file' }, + condition: { + field: 'operation', + value: ['upload_file', 'download_file', 'delete_file', 'get_drive_item'], + }, + required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] }, mode: 'advanced', }, + { + id: 'driveItemId', + title: 'File ID', + type: 'short-input', + placeholder: 'Enter the file (drive item) ID', + canonicalParamId: 'driveItemId', + condition: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] }, + required: { field: 'operation', value: ['download_file', 'delete_file', 'get_drive_item'] }, + }, { id: 'folderPath', title: 'Folder Path', @@ -352,8 +425,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'fileName', title: 'File Name', type: 'short-input', - placeholder: 'Optional: override uploaded file name', - condition: { field: 'operation', value: 'upload_file' }, + placeholder: 'Optional: override uploaded/downloaded file name', + condition: { field: 'operation', value: ['upload_file', 'download_file'] }, mode: 'advanced', required: false, }, @@ -367,7 +440,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, condition: { field: 'operation', value: 'upload_file' }, mode: 'basic', multiple: true, - required: false, + required: true, }, // Variable reference (advanced mode) { @@ -378,19 +451,27 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, placeholder: 'Reference files from previous blocks', condition: { field: 'operation', value: 'upload_file' }, mode: 'advanced', - required: false, + required: true, }, ], tools: { access: [ 'sharepoint_create_page', 'sharepoint_read_page', + 'sharepoint_update_page', + 'sharepoint_publish_page', + 'sharepoint_delete_page', 'sharepoint_list_sites', 'sharepoint_create_list', 'sharepoint_get_list', 'sharepoint_update_list', 'sharepoint_add_list_items', + 'sharepoint_get_list_item', + 'sharepoint_delete_list_item', 'sharepoint_upload_file', + 'sharepoint_download_file', + 'sharepoint_get_drive_item', + 'sharepoint_delete_file', ], config: { tool: (params) => { @@ -399,6 +480,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, return 'sharepoint_create_page' case 'read_page': return 'sharepoint_read_page' + case 'update_page': + return 'sharepoint_update_page' + case 'publish_page': + return 'sharepoint_publish_page' + case 'delete_page': + return 'sharepoint_delete_page' case 'list_sites': return 'sharepoint_list_sites' case 'create_list': @@ -409,8 +496,18 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, return 'sharepoint_update_list' case 'add_list_items': return 'sharepoint_add_list_items' + case 'get_list_item': + return 'sharepoint_get_list_item' + case 'delete_list_item': + return 'sharepoint_delete_list_item' case 'upload_file': return 'sharepoint_upload_file' + case 'download_file': + return 'sharepoint_download_file' + case 'get_drive_item': + return 'sharepoint_get_drive_item' + case 'delete_file': + return 'sharepoint_delete_file' default: throw new Error(`Invalid Sharepoint operation: ${params.operation}`) } @@ -428,6 +525,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, includeItems, files, // canonical param from uploadFiles (basic) or files (advanced) driveId, // canonical param from driveId + driveItemId, // canonical param from driveItemId columnDefinitions, listId, ...others @@ -458,19 +556,16 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, } if (others.operation === 'update_list' || others.operation === 'add_list_items') { - try { - logger.info('SharepointBlock list item param check', { - siteId: effectiveSiteId || undefined, - listId: listId, - listTitle: (others as any)?.listTitle, - itemId: sanitizedItemId, - hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object', - itemFieldKeys: - parsedItemFields && typeof parsedItemFields === 'object' - ? Object.keys(parsedItemFields) - : [], - }) - } catch {} + logger.info('SharepointBlock list item param check', { + siteId: effectiveSiteId || undefined, + listId: listId, + itemId: sanitizedItemId, + hasItemFields: !!parsedItemFields && typeof parsedItemFields === 'object', + itemFieldKeys: + parsedItemFields && typeof parsedItemFields === 'object' + ? Object.keys(parsedItemFields) + : [], + }) } // Handle file upload files parameter using canonical param @@ -478,11 +573,10 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, const baseParams: Record = { oauthCredential, siteId: effectiveSiteId || undefined, - pageSize: others.pageSize ? Number.parseInt(others.pageSize as string, 10) : undefined, - mimeType: mimeType, ...others, ...(listId ? { listId } : {}), ...(driveId ? { driveId } : {}), + ...(driveItemId ? { driveItemId: String(driveItemId).trim() } : {}), itemId: sanitizedItemId, listItemFields: parsedItemFields, includeColumns: coerceBoolean(includeColumns), @@ -511,14 +605,13 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'Column definitions for list creation (JSON array)', }, pageTitle: { type: 'string', description: 'Page title' }, + pageContent: { type: 'string', description: 'Page text content' }, pageId: { type: 'string', description: 'Page ID' }, siteId: { type: 'string', description: 'Site ID' }, - pageSize: { type: 'number', description: 'Results per page' }, listDisplayName: { type: 'string', description: 'List display name' }, listDescription: { type: 'string', description: 'List description' }, listTemplate: { type: 'string', description: 'List template' }, listId: { type: 'string', description: 'List ID' }, - listTitle: { type: 'string', description: 'List title' }, includeColumns: { type: 'boolean', description: 'Include columns in response' }, includeItems: { type: 'boolean', description: 'Include items in response' }, itemId: { type: 'string', description: 'List item ID (canonical param)' }, @@ -527,6 +620,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, type: 'string', description: 'Document library (drive) ID', }, + driveItemId: { type: 'string', description: 'File (drive item) ID (canonical param)' }, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, files: { type: 'array', description: 'Files to upload' }, @@ -537,6 +631,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'An array of SharePoint site objects, each containing details such as id, name, and more.', }, + page: { + type: 'json', + description: 'SharePoint page object (id, name, title, webUrl, pageLayout)', + }, + published: { type: 'boolean', description: 'Whether the page was published' }, + deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' }, list: { type: 'json', description: 'SharePoint list object (id, displayName, name, webUrl, etc.)', @@ -549,6 +649,14 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, type: 'json', description: 'Array of SharePoint list items with fields', }, + driveItem: { + type: 'json', + description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)', + }, + file: { + type: 'json', + description: 'Downloaded file stored in execution files', + }, uploadedFiles: { type: 'json', description: 'Array of uploaded file objects with id, name, webUrl, size', @@ -557,6 +665,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, type: 'number', description: 'Number of files uploaded', }, + itemId: { type: 'string', description: 'ID of the deleted list item or file' }, + pageId: { type: 'string', description: 'ID of the deleted or published page' }, success: { type: 'boolean', description: 'Success status', @@ -571,21 +681,48 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, const SHAREPOINT_V2_TOOL_IDS = [ 'sharepoint_create_page', 'sharepoint_read_page', + 'sharepoint_update_page', + 'sharepoint_publish_page', + 'sharepoint_delete_page', 'sharepoint_list_sites', 'sharepoint_create_list', 'sharepoint_get_list', 'sharepoint_update_list', 'sharepoint_add_list_items', + 'sharepoint_get_list_item', + 'sharepoint_delete_list_item', 'sharepoint_upload_file', + 'sharepoint_download_file', + 'sharepoint_get_drive_item', + 'sharepoint_delete_file', +] as const + +const SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS = [ + 'sharepoint_download_file', + 'sharepoint_delete_file', + 'sharepoint_get_drive_item', ] as const -const SHAREPOINT_V2_SITE_OPERATIONS = Array.from(SHAREPOINT_V2_TOOL_IDS) +const SHAREPOINT_V2_SITE_OPERATIONS = SHAREPOINT_V2_TOOL_IDS.filter( + (id) => !(SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS as readonly string[]).includes(id) +) const SHAREPOINT_V2_LIST_ITEM_OPERATIONS = [ 'sharepoint_update_list', 'sharepoint_add_list_items', ] as const +const SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS = [ + 'sharepoint_get_list_item', + 'sharepoint_delete_list_item', +] as const + +const SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS = [ + 'sharepoint_update_page', + 'sharepoint_publish_page', + 'sharepoint_delete_page', +] as const + export const SharepointV2Block: BlockConfig = { ...SharepointBlock, type: 'sharepoint_v2', @@ -599,12 +736,20 @@ export const SharepointV2Block: BlockConfig = { options: [ { label: 'Create Page', id: 'sharepoint_create_page' }, { label: 'Read Page', id: 'sharepoint_read_page' }, + { label: 'Update Page', id: 'sharepoint_update_page' }, + { label: 'Publish Page', id: 'sharepoint_publish_page' }, + { label: 'Delete Page', id: 'sharepoint_delete_page' }, { label: 'List Sites', id: 'sharepoint_list_sites' }, { label: 'Create List', id: 'sharepoint_create_list' }, { label: 'Read List', id: 'sharepoint_get_list' }, { label: 'Update List Item', id: 'sharepoint_update_list' }, { label: 'Add List Item', id: 'sharepoint_add_list_items' }, + { label: 'Get List Item', id: 'sharepoint_get_list_item' }, + { label: 'Delete List Item', id: 'sharepoint_delete_list_item' }, { label: 'Upload File', id: 'sharepoint_upload_file' }, + { label: 'Download File', id: 'sharepoint_download_file' }, + { label: 'Get Drive Item', id: 'sharepoint_get_drive_item' }, + { label: 'Delete File', id: 'sharepoint_delete_file' }, ], value: () => 'sharepoint_create_page', }, @@ -664,8 +809,12 @@ export const SharepointV2Block: BlockConfig = { id: 'pageId', title: 'Page ID', type: 'short-input', - placeholder: 'Page ID (alternative to page name)', - condition: { field: 'operation', value: 'sharepoint_read_page' }, + placeholder: 'Page ID (alternative to page name for Read Page)', + condition: { + field: 'operation', + value: ['sharepoint_read_page', ...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS], + }, + required: { field: 'operation', value: [...SHAREPOINT_V2_PAGE_MUTATION_OPERATIONS] }, mode: 'advanced', }, { @@ -673,7 +822,10 @@ export const SharepointV2Block: BlockConfig = { title: 'Page Title', type: 'short-input', placeholder: 'Optional title (defaults to page name)', - condition: { field: 'operation', value: 'sharepoint_create_page' }, + condition: { + field: 'operation', + value: ['sharepoint_create_page', 'sharepoint_update_page'], + }, mode: 'advanced', }, { @@ -681,7 +833,10 @@ export const SharepointV2Block: BlockConfig = { title: 'Page Content', type: 'long-input', placeholder: 'Optional text content for the page', - condition: { field: 'operation', value: 'sharepoint_create_page' }, + condition: { + field: 'operation', + value: ['sharepoint_create_page', 'sharepoint_update_page'], + }, mode: 'advanced', }, { @@ -712,9 +867,19 @@ export const SharepointV2Block: BlockConfig = { mode: 'basic', condition: { field: 'operation', - value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS], + value: [ + 'sharepoint_get_list', + ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS, + ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS, + ], + }, + required: { + field: 'operation', + value: [ + ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS, + ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS, + ], }, - required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, }, { id: 'manualListId', @@ -725,9 +890,19 @@ export const SharepointV2Block: BlockConfig = { mode: 'advanced', condition: { field: 'operation', - value: ['sharepoint_get_list', ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS], + value: [ + 'sharepoint_get_list', + ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS, + ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS, + ], + }, + required: { + field: 'operation', + value: [ + ...SHAREPOINT_V2_LIST_ITEM_OPERATIONS, + ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS, + ], }, - required: { field: 'operation', value: [...SHAREPOINT_V2_LIST_ITEM_OPERATIONS] }, }, { id: 'includeColumns', @@ -821,8 +996,14 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, title: 'Item ID', type: 'short-input', placeholder: 'Enter item ID', - condition: { field: 'operation', value: 'sharepoint_update_list' }, - required: { field: 'operation', value: 'sharepoint_update_list' }, + condition: { + field: 'operation', + value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS], + }, + required: { + field: 'operation', + value: ['sharepoint_update_list', ...SHAREPOINT_V2_LIST_ITEM_LOOKUP_OPERATIONS], + }, }, { id: 'listItemFields', @@ -848,9 +1029,21 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, title: 'Document Library ID', type: 'short-input', placeholder: 'Enter document library (drive) ID', - condition: { field: 'operation', value: 'sharepoint_upload_file' }, + condition: { + field: 'operation', + value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS, 'sharepoint_upload_file'], + }, + required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] }, mode: 'advanced', }, + { + id: 'driveItemId', + title: 'File ID', + type: 'short-input', + placeholder: 'Enter the file (drive item) ID', + condition: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] }, + required: { field: 'operation', value: [...SHAREPOINT_V2_DRIVE_ONLY_OPERATIONS] }, + }, { id: 'folderPath', title: 'Folder Path', @@ -864,8 +1057,11 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, id: 'fileName', title: 'File Name', type: 'short-input', - placeholder: 'Optional: override uploaded file name', - condition: { field: 'operation', value: 'sharepoint_upload_file' }, + placeholder: 'Optional: override uploaded/downloaded file name', + condition: { + field: 'operation', + value: ['sharepoint_upload_file', 'sharepoint_download_file'], + }, mode: 'advanced', required: false, }, @@ -919,6 +1115,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, files, maxPages, driveId, + driveItemId, ...rest } = params @@ -950,6 +1147,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, listId: cleanString(listId), itemId: cleanString(itemId), driveId: cleanString(driveId), + driveItemId: cleanString(driveItemId), includeColumns: coerceBoolean(includeColumns), includeItems: coerceBoolean(includeItems), listItemFields: parseJsonObject(listItemFields), @@ -991,6 +1189,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, itemId: { type: 'string', description: 'List item ID' }, listItemFields: { type: 'json', description: 'List item fields' }, driveId: { type: 'string', description: 'Document library (drive) ID' }, + driveItemId: { type: 'string', description: 'File (drive item) ID' }, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, files: { type: 'json', description: 'Files to upload' }, @@ -1017,6 +1216,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'SharePoint page content (content, canvasLayout)', }, totalPages: { type: 'number', description: 'Number of pages returned' }, + published: { type: 'boolean', description: 'Whether the page was published' }, list: { type: 'json', description: 'SharePoint list object (id, displayName, name, webUrl, columns, items)', @@ -1027,6 +1227,15 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, }, item: { type: 'json', description: 'SharePoint list item with fields' }, items: { type: 'json', description: 'Array of SharePoint list items with fields' }, + deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' }, + driveItem: { + type: 'json', + description: 'SharePoint drive item metadata (id, name, size, webUrl, file/folder facet)', + }, + file: { + type: 'json', + description: 'Downloaded file stored in execution files', + }, uploadedFiles: { type: 'json', description: 'Array of uploaded file objects with id, name, webUrl, size', @@ -1041,6 +1250,8 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, type: 'json', description: 'Array of per-file upload errors (name, error, status)', }, + itemId: { type: 'string', description: 'ID of the deleted list item or file' }, + pageId: { type: 'string', description: 'ID of the deleted or published page' }, nextPageUrl: { type: 'string', description: 'Microsoft Graph @odata.nextLink URL for the next page of results', diff --git a/apps/sim/lib/api/contracts/tools/microsoft.ts b/apps/sim/lib/api/contracts/tools/microsoft.ts index be68501cdba..30fe918d9cf 100644 --- a/apps/sim/lib/api/contracts/tools/microsoft.ts +++ b/apps/sim/lib/api/contracts/tools/microsoft.ts @@ -86,6 +86,13 @@ export const sharepointUploadBodySchema = z.object({ files: RawFileInputArraySchema.optional().nullable(), }) +export const sharepointDownloadFileBodySchema = z.object({ + accessToken: accessTokenSchema, + driveId: z.string().min(1, 'Drive ID is required'), + itemId: z.string().min(1, 'Item ID is required'), + fileName: z.string().optional().nullable(), +}) + export const dataverseUploadFileBodySchema = z.object({ accessToken: accessTokenSchema, environmentUrl: z.string().min(1, 'Environment URL is required'), @@ -190,6 +197,13 @@ export const sharepointUploadContract = defineRouteContract({ response: { mode: 'json', schema: toolJsonResponseSchema }, }) +export const sharepointDownloadFileContract = defineRouteContract({ + method: 'POST', + path: '/api/tools/sharepoint/download-file', + body: sharepointDownloadFileBodySchema, + response: { mode: 'json', schema: toolJsonResponseSchema }, +}) + export const dataverseUploadFileContract = defineRouteContract({ method: 'POST', path: '/api/tools/microsoft-dataverse/upload-file', @@ -210,4 +224,5 @@ export type TeamsDeleteChatMessageBody = ContractBody export type OneDriveDownloadBody = ContractBody export type SharepointUploadBody = ContractBody +export type SharepointDownloadFileBody = ContractBody export type DataverseUploadFileBody = z.output diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index a81853c5514..7f81039622c 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -4,6 +4,7 @@ import https from 'https' import type { LookupFunction } from 'net' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { omit } from '@sim/utils/object' import * as ipaddr from 'ipaddr.js' import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici' import { isHosted, isPrivateDatabaseHostsAllowed } from '@/lib/core/config/env-flags' @@ -333,6 +334,8 @@ export interface SecureFetchOptions { maxRedirects?: number maxResponseBytes?: number signal?: AbortSignal + /** Drop the Authorization header when following a redirect, so it is not sent to the redirect target's origin. */ + stripAuthOnRedirect?: boolean } export class SecureFetchHeaders { @@ -500,10 +503,16 @@ export async function secureFetchWithPinnedIP( settledReject(new Error(`Redirect blocked: ${validation.error}`)) return } + const redirectOptions = options.stripAuthOnRedirect + ? { + ...options, + headers: omit(options.headers ?? {}, ['Authorization', 'authorization']), + } + : options return secureFetchWithPinnedIP( redirectUrl, validation.resolvedIP!, - options, + redirectOptions, redirectCount + 1 ) }) diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bb910c4eef..2935858eddb 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -3160,10 +3160,18 @@ import { sharepointAddListItemTool, sharepointCreateListTool, sharepointCreatePageTool, + sharepointDeleteFileTool, + sharepointDeleteListItemTool, + sharepointDeletePageTool, + sharepointDownloadFileTool, + sharepointGetDriveItemTool, + sharepointGetListItemTool, sharepointGetListTool, sharepointListSitesTool, + sharepointPublishPageTool, sharepointReadPageTool, sharepointUpdateListItemTool, + sharepointUpdatePageTool, sharepointUploadFileTool, } from '@/tools/sharepoint' import { @@ -7636,10 +7644,18 @@ export const tools: Record = { sharepoint_add_list_items: sharepointAddListItemTool, sharepoint_create_list: sharepointCreateListTool, sharepoint_create_page: sharepointCreatePageTool, + sharepoint_delete_file: sharepointDeleteFileTool, + sharepoint_delete_list_item: sharepointDeleteListItemTool, + sharepoint_delete_page: sharepointDeletePageTool, + sharepoint_download_file: sharepointDownloadFileTool, + sharepoint_get_drive_item: sharepointGetDriveItemTool, sharepoint_get_list: sharepointGetListTool, + sharepoint_get_list_item: sharepointGetListItemTool, sharepoint_list_sites: sharepointListSitesTool, + sharepoint_publish_page: sharepointPublishPageTool, sharepoint_read_page: sharepointReadPageTool, sharepoint_update_list: sharepointUpdateListItemTool, + sharepoint_update_page: sharepointUpdatePageTool, sharepoint_upload_file: sharepointUploadFileTool, stripe_create_payment_intent: stripeCreatePaymentIntentTool, stripe_retrieve_payment_intent: stripeRetrievePaymentIntentTool, diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts index ab3afdf3bea..1958a0fe9e6 100644 --- a/apps/sim/tools/sharepoint/add_list_items.ts +++ b/apps/sim/tools/sharepoint/add_list_items.ts @@ -1,9 +1,28 @@ -import { createLogger } from '@sim/logger' import type { SharepointAddListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types' -import { optionalTrim } from '@/tools/sharepoint/utils' +import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SharePointAddListItem') +function resolveSanitizedFields( + listItemFields: SharepointToolParams['listItemFields'] +): Record { + if (!listItemFields || Object.keys(listItemFields).length === 0) { + throw new Error('listItemFields must not be empty') + } + + const providedFields = + typeof listItemFields === 'object' && + listItemFields !== null && + 'fields' in (listItemFields as Record) && + Object.keys(listItemFields as Record).length === 1 + ? ((listItemFields as { fields: Record }).fields as Record) + : (listItemFields as Record) + + if (!providedFields || Object.keys(providedFields).length === 0) { + throw new Error('No fields provided to create the SharePoint list item') + } + + return sanitizeListItemFields(providedFields, { action: 'create' }) +} export const addListItemTool: ToolConfig = { id: 'sharepoint_add_list_items', @@ -66,87 +85,23 @@ export const addListItemTool: ToolConfig { - if (!params.listItemFields || Object.keys(params.listItemFields).length === 0) { - throw new Error('listItemFields must not be empty') - } - - const providedFields = - typeof params.listItemFields === 'object' && - params.listItemFields !== null && - 'fields' in (params.listItemFields as Record) && - Object.keys(params.listItemFields as Record).length === 1 - ? ((params.listItemFields as { fields: Record }).fields as Record< - string, - unknown - >) - : (params.listItemFields as Record) - - if (!providedFields || Object.keys(providedFields).length === 0) { - throw new Error('No fields provided to create the SharePoint list item') - } - - const readOnlyFields = new Set([ - 'Id', - 'id', - 'UniqueId', - 'GUID', - 'ContentTypeId', - 'Created', - 'Modified', - 'Author', - 'Editor', - 'CreatedBy', - 'ModifiedBy', - 'AuthorId', - 'EditorId', - '_UIVersionString', - 'Attachments', - 'FileRef', - 'FileDirRef', - 'FileLeafRef', - ]) - - const entries = Object.entries(providedFields) - const creatableEntries = entries.filter(([key]) => !readOnlyFields.has(key)) - - if (creatableEntries.length !== entries.length) { - const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key) - logger.warn('Removed read-only SharePoint fields from create', { - removed, - }) - } - - if (creatableEntries.length === 0) { - const requestedKeys = Object.keys(providedFields) - throw new Error( - `All provided fields are read-only and cannot be set: ${requestedKeys.join(', ')}` - ) - } - - const sanitizedFields = Object.fromEntries(creatableEntries) - - logger.info('Creating SharePoint list item', { - listId: params.listId, - fieldsKeys: Object.keys(sanitizedFields), - }) - - return { - fields: sanitizedFields, - } - }, + body: (params) => ({ + fields: resolveSanitizedFields(params.listItemFields), + }), }, transformResponse: async (response: Response, params) => { - let data: any + let data: Record | undefined try { data = await response.json() } catch { data = undefined } - const itemId: string | undefined = data?.id - const fields: Record | undefined = data?.fields || params?.listItemFields + const itemId = data?.id as string | undefined + const fields = + (data?.fields as Record | undefined) || + (params ? resolveSanitizedFields(params.listItemFields) : undefined) return { success: true, diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts index 007631b207e..1fb20f65180 100644 --- a/apps/sim/tools/sharepoint/create_list.ts +++ b/apps/sim/tools/sharepoint/create_list.ts @@ -70,7 +70,7 @@ export const createListTool: ToolConfig { - const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` }, method: 'POST', diff --git a/apps/sim/tools/sharepoint/delete_file.ts b/apps/sim/tools/sharepoint/delete_file.ts new file mode 100644 index 00000000000..183ac484762 --- /dev/null +++ b/apps/sim/tools/sharepoint/delete_file.ts @@ -0,0 +1,66 @@ +import type { SharepointDeleteFileResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteFileTool: ToolConfig = { + id: 'sharepoint_delete_file', + name: 'Delete SharePoint File', + description: 'Delete a file (or folder) from a SharePoint document library', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + driveId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document library (drive). Example: b!abc123def456', + }, + driveItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file (drive item) to delete', + }, + }, + + request: { + url: (params) => { + const driveId = optionalTrim(params.driveId) + const driveItemId = optionalTrim(params.driveItemId) + if (!driveId) throw new Error('driveId must be provided') + if (!driveItemId) throw new Error('driveItemId must be provided') + return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (_response: Response, params) => { + return { + success: true, + output: { + deleted: true, + itemId: params?.driveItemId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the file was deleted' }, + itemId: { type: 'string', description: 'The ID of the deleted file' }, + }, +} diff --git a/apps/sim/tools/sharepoint/delete_list_item.ts b/apps/sim/tools/sharepoint/delete_list_item.ts new file mode 100644 index 00000000000..b71e6b4e823 --- /dev/null +++ b/apps/sim/tools/sharepoint/delete_list_item.ts @@ -0,0 +1,88 @@ +import type { + SharepointDeleteListItemResponse, + SharepointToolParams, +} from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const deleteListItemTool: ToolConfig< + SharepointToolParams, + SharepointDeleteListItemResponse +> = { + id: 'sharepoint_delete_list_item', + name: 'Delete SharePoint List Item', + description: 'Delete an item from a SharePoint list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the list item to delete. Example: 1, 42, or 123', + }, + }, + + request: { + url: (params) => { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const listId = optionalTrim(params.listId) + const itemId = optionalTrim(params.itemId) + if (!listId) throw new Error('listId must be provided') + if (!itemId) throw new Error('itemId must be provided') + const listSegment = encodeURIComponent(listId) + const itemSegment = encodeURIComponent(itemId) + return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (_response: Response, params) => { + return { + success: true, + output: { + deleted: true, + itemId: params?.itemId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the list item was deleted' }, + itemId: { type: 'string', description: 'The ID of the deleted list item' }, + }, +} diff --git a/apps/sim/tools/sharepoint/delete_page.ts b/apps/sim/tools/sharepoint/delete_page.ts new file mode 100644 index 00000000000..4b5b77872eb --- /dev/null +++ b/apps/sim/tools/sharepoint/delete_page.ts @@ -0,0 +1,72 @@ +import type { SharepointDeletePageResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const deletePageTool: ToolConfig = { + id: 'sharepoint_delete_page', + name: 'Delete SharePoint Page', + description: 'Delete a page from a SharePoint site', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the page to delete. Example: a GUID like 12345678-1234-1234-1234-123456789012', + }, + }, + + request: { + url: (params) => { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const pageId = optionalTrim(params.pageId) + if (!pageId) throw new Error('pageId must be provided') + return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}` + }, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (_response: Response, params) => { + return { + success: true, + output: { + deleted: true, + pageId: params?.pageId ?? '', + }, + } + }, + + outputs: { + deleted: { type: 'boolean', description: 'Whether the page was deleted' }, + pageId: { type: 'string', description: 'The ID of the deleted page' }, + }, +} diff --git a/apps/sim/tools/sharepoint/download_file.ts b/apps/sim/tools/sharepoint/download_file.ts new file mode 100644 index 00000000000..add8673b254 --- /dev/null +++ b/apps/sim/tools/sharepoint/download_file.ts @@ -0,0 +1,59 @@ +import type { SharepointDownloadFileResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import type { ToolConfig } from '@/tools/types' + +export const downloadFileTool: ToolConfig = { + id: 'sharepoint_download_file', + name: 'Download File from SharePoint', + description: 'Download a file from a SharePoint document library', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + driveId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document library (drive). Example: b!abc123def456', + }, + driveItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file (drive item) to download', + }, + fileName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional filename override (e.g., "report.pdf", "data.xlsx")', + }, + }, + + request: { + url: '/api/tools/sharepoint/download-file', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + accessToken: params.accessToken, + driveId: params.driveId, + itemId: params.driveItemId, + fileName: params.fileName, + }), + }, + + outputs: { + file: { type: 'file', description: 'Downloaded file stored in execution files' }, + }, +} diff --git a/apps/sim/tools/sharepoint/get_drive_item.ts b/apps/sim/tools/sharepoint/get_drive_item.ts new file mode 100644 index 00000000000..0b6f689aaf6 --- /dev/null +++ b/apps/sim/tools/sharepoint/get_drive_item.ts @@ -0,0 +1,106 @@ +import type { + SharepointDriveItem, + SharepointGetDriveItemResponse, + SharepointToolParams, +} from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const getDriveItemTool: ToolConfig = { + id: 'sharepoint_get_drive_item', + name: 'Get SharePoint Drive Item', + description: 'Get metadata for a file or folder in a SharePoint document library', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + driveId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the document library (drive). Example: b!abc123def456', + }, + driveItemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the file or folder (drive item) to retrieve', + }, + }, + + request: { + url: (params) => { + const driveId = optionalTrim(params.driveId) + const driveItemId = optionalTrim(params.driveItemId) + if (!driveId) throw new Error('driveId must be provided') + if (!driveItemId) throw new Error('driveItemId must be provided') + return `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/items/${encodeURIComponent(driveItemId)}` + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: Record = await response.json() + + const driveItem: SharepointDriveItem = { + id: data.id as string, + name: data.name as string, + webUrl: data.webUrl as string | undefined, + size: data.size as number | undefined, + createdDateTime: data.createdDateTime as string | undefined, + lastModifiedDateTime: data.lastModifiedDateTime as string | undefined, + file: (data.file as SharepointDriveItem['file']) ?? null, + folder: (data.folder as SharepointDriveItem['folder']) ?? null, + parentReference: (data.parentReference as SharepointDriveItem['parentReference']) ?? null, + } + + return { + success: true, + output: { driveItem }, + } + }, + + outputs: { + driveItem: { + type: 'object', + description: 'Metadata for the SharePoint file or folder', + properties: { + id: { type: 'string', description: 'The unique ID of the drive item' }, + name: { type: 'string', description: 'The name of the file or folder' }, + webUrl: { type: 'string', description: 'The URL to access the item' }, + size: { type: 'number', description: 'The size of the item in bytes', optional: true }, + createdDateTime: { type: 'string', description: 'When the item was created' }, + lastModifiedDateTime: { type: 'string', description: 'When the item was last modified' }, + file: { + type: 'object', + description: 'Present if the item is a file (contains mimeType)', + optional: true, + }, + folder: { + type: 'object', + description: 'Present if the item is a folder (contains childCount)', + optional: true, + }, + parentReference: { + type: 'object', + description: 'Reference to the parent folder/drive', + optional: true, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts index bcf3036a977..5b1d4d44a21 100644 --- a/apps/sim/tools/sharepoint/get_list.ts +++ b/apps/sim/tools/sharepoint/get_list.ts @@ -107,7 +107,7 @@ export const getListTool: ToolConfig 0) url.searchParams.append('$expand', expandParts.join(',')) const finalUrl = url.toString() @@ -128,21 +128,23 @@ export const getListTool: ToolConfig { - const data = await response.json() + const data: Record = await response.json() + const value = data.value // If the response is a collection of items (from the items endpoint) if ( - Array.isArray((data as any).value) && - (data as any).value.length > 0 && - (data as any).value[0] && - 'fields' in (data as any).value[0] + Array.isArray(value) && + value.length > 0 && + value[0] && + typeof value[0] === 'object' && + 'fields' in value[0] ) { - const items = (data as any).value.map((i: any) => ({ - id: i.id, + const items = value.map((i: Record) => ({ + id: i.id as string, fields: i.fields as Record, })) - const nextPageUrl = getGraphNextPageUrl(data as Record) + const nextPageUrl = getGraphNextPageUrl(data) return { success: true, @@ -154,18 +156,18 @@ export const getListTool: ToolConfig ({ - id: l.id, - displayName: l.displayName ?? l.name, - name: l.name, - webUrl: l.webUrl, - createdDateTime: l.createdDateTime, - lastModifiedDateTime: l.lastModifiedDateTime, - list: l.list, + if (Array.isArray(value)) { + const lists: SharepointList[] = value.map((l: Record) => ({ + id: l.id as string, + displayName: (l.displayName ?? l.name) as string | undefined, + name: l.name as string | undefined, + webUrl: l.webUrl as string | undefined, + createdDateTime: l.createdDateTime as string | undefined, + lastModifiedDateTime: l.lastModifiedDateTime as string | undefined, + list: l.list as SharepointList['list'], })) - const nextPageUrl = getGraphNextPageUrl(data as Record) + const nextPageUrl = getGraphNextPageUrl(data) return { success: true, @@ -174,29 +176,32 @@ export const getListTool: ToolConfig ({ - id: c.id, - name: c.name, - displayName: c.displayName, - description: c.description, - indexed: c.indexed, - enforcedUniqueValues: c.enforcedUniqueValues, - hidden: c.hidden, - readOnly: c.readOnly, - required: c.required, - columnGroup: c.columnGroup, + ? data.columns.map((c: Record) => ({ + id: c.id as string | undefined, + name: c.name as string | undefined, + displayName: c.displayName as string | undefined, + description: c.description as string | undefined, + indexed: c.indexed as boolean | undefined, + enforcedUniqueValues: c.enforcedUniqueValues as boolean | undefined, + hidden: c.hidden as boolean | undefined, + readOnly: c.readOnly as boolean | undefined, + required: c.required as boolean | undefined, + columnGroup: c.columnGroup as string | undefined, })) : undefined, items: Array.isArray(data.items) - ? data.items.map((i: any) => ({ id: i.id, fields: i.fields as Record })) + ? data.items.map((i: Record) => ({ + id: i.id as string, + fields: i.fields as Record, + })) : undefined, } diff --git a/apps/sim/tools/sharepoint/get_list_item.ts b/apps/sim/tools/sharepoint/get_list_item.ts new file mode 100644 index 00000000000..1423d8b1b14 --- /dev/null +++ b/apps/sim/tools/sharepoint/get_list_item.ts @@ -0,0 +1,96 @@ +import type { SharepointGetListItemResponse, SharepointToolParams } from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +export const getListItemTool: ToolConfig = { + id: 'sharepoint_get_list_item', + name: 'Get SharePoint List Item', + description: 'Get a single item (with field values) from a SharePoint list', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + listId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the list containing the item. Example: b!abc123def456 or a GUID like 12345678-1234-1234-1234-123456789012', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the list item to retrieve. Example: 1, 42, or 123', + }, + }, + + request: { + url: (params) => { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const listId = optionalTrim(params.listId) + const itemId = optionalTrim(params.itemId) + if (!listId) throw new Error('listId must be provided') + if (!itemId) throw new Error('itemId must be provided') + const listSegment = encodeURIComponent(listId) + const itemSegment = encodeURIComponent(itemId) + const url = new URL( + `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}` + ) + url.searchParams.set('$expand', 'fields') + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (response: Response) => { + const data: Record = await response.json() + + return { + success: true, + output: { + item: { + id: data.id as string, + fields: data.fields as Record | undefined, + }, + }, + } + }, + + outputs: { + item: { + type: 'object', + description: 'SharePoint list item with field values', + properties: { + id: { type: 'string', description: 'Item ID' }, + fields: { type: 'object', description: 'Field values for the item' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sharepoint/index.ts b/apps/sim/tools/sharepoint/index.ts index eaa798cd192..01feb9f8618 100644 --- a/apps/sim/tools/sharepoint/index.ts +++ b/apps/sim/tools/sharepoint/index.ts @@ -1,19 +1,35 @@ import { addListItemTool } from '@/tools/sharepoint/add_list_items' import { createListTool } from '@/tools/sharepoint/create_list' import { createPageTool } from '@/tools/sharepoint/create_page' +import { deleteFileTool } from '@/tools/sharepoint/delete_file' +import { deleteListItemTool } from '@/tools/sharepoint/delete_list_item' +import { deletePageTool } from '@/tools/sharepoint/delete_page' +import { downloadFileTool } from '@/tools/sharepoint/download_file' +import { getDriveItemTool } from '@/tools/sharepoint/get_drive_item' import { getListTool } from '@/tools/sharepoint/get_list' +import { getListItemTool } from '@/tools/sharepoint/get_list_item' import { listSitesTool } from '@/tools/sharepoint/list_sites' +import { publishPageTool } from '@/tools/sharepoint/publish_page' import { readPageTool } from '@/tools/sharepoint/read_page' import { updateListItemTool } from '@/tools/sharepoint/update_list' +import { updatePageTool } from '@/tools/sharepoint/update_page' import { uploadFileTool } from '@/tools/sharepoint/upload_file' +export const sharepointAddListItemTool = addListItemTool export const sharepointCreatePageTool = createPageTool export const sharepointCreateListTool = createListTool +export const sharepointDeleteFileTool = deleteFileTool +export const sharepointDeleteListItemTool = deleteListItemTool +export const sharepointDeletePageTool = deletePageTool +export const sharepointDownloadFileTool = downloadFileTool +export const sharepointGetDriveItemTool = getDriveItemTool export const sharepointGetListTool = getListTool +export const sharepointGetListItemTool = getListItemTool export const sharepointListSitesTool = listSitesTool +export const sharepointPublishPageTool = publishPageTool export const sharepointReadPageTool = readPageTool export const sharepointUpdateListItemTool = updateListItemTool -export const sharepointAddListItemTool = addListItemTool +export const sharepointUpdatePageTool = updatePageTool export const sharepointUploadFileTool = uploadFileTool export * from '@/tools/sharepoint/types' diff --git a/apps/sim/tools/sharepoint/list_sites.ts b/apps/sim/tools/sharepoint/list_sites.ts index 089010ac4c3..d35ecc3a03d 100644 --- a/apps/sim/tools/sharepoint/list_sites.ts +++ b/apps/sim/tools/sharepoint/list_sites.ts @@ -140,9 +140,9 @@ export const listSitesTool: ToolConfig = { + id: 'sharepoint_publish_page', + name: 'Publish SharePoint Page', + description: 'Publish the latest version of a SharePoint page, making it available to all users', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the page to publish. Example: a GUID like 12345678-1234-1234-1234-123456789012', + }, + }, + + request: { + url: (params) => { + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + const pageId = optionalTrim(params.pageId) + if (!pageId) throw new Error('pageId must be provided') + return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish` + }, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + Accept: 'application/json', + }), + }, + + transformResponse: async (_response: Response, params) => { + return { + success: true, + output: { + published: true, + pageId: params?.pageId ?? '', + }, + } + }, + + outputs: { + published: { type: 'boolean', description: 'Whether the page was published' }, + pageId: { type: 'string', description: 'The ID of the published page' }, + }, +} diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts index 554f747af0b..926ba9a1c48 100644 --- a/apps/sim/tools/sharepoint/read_page.ts +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -85,12 +85,13 @@ export const readPageTool: ToolConfig ({ id: p.id, name: p.name, title: p.title })), + foundPages: data.value.map((p) => ({ id: p.id, name: p.name, title: p.title })), totalCount: data.value.length, }) if (params?.pageName) { const pageData = data.value[0] const siteId = params?.siteId || params?.siteSelector || 'root' - const contentUrl = `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${pageData.id}/microsoft.graph.sitePage?$expand=canvasLayout` + const contentUrl = `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageData.id)}/microsoft.graph.sitePage?$expand=canvasLayout` logger.info('Making API call to get page content for searched page', { pageId: pageData.id, @@ -262,8 +263,9 @@ export const readPageTool: ToolConfig - }> - webparts?: Array<{ - id: string - innerHtml: string - }> - }> - } + canvasLayout?: CanvasLayout } export interface SharepointPageContent { content: string - canvasLayout?: { - horizontalSections: Array<{ - layout: string - id: string - emphasis: string - webparts: Array<{ - id: string - innerHtml: string - }> - }> - } | null + canvasLayout?: CanvasLayout | null } interface SharepointColumn { @@ -132,9 +104,8 @@ export interface SharepointReadSiteResponse extends ToolResponse { createdDateTime?: string lastModifiedDateTime?: string isPersonalSite?: boolean - root?: { - serverRelativeUrl: string - } + // Graph returns an empty object marker (not a URL) when this site is the root of its site collection + root?: Record siteCollection?: { hostname: string } @@ -177,14 +148,15 @@ export interface SharepointToolParams { listDisplayName?: string listDescription?: string listTemplate?: string - // Update List Item + // Update List Item / Delete List Item / Get List Item itemId?: string listItemFields?: Record - // Upload File + // Upload File / Download File / Delete File / Get Drive Item driveId?: string folderPath?: string fileName?: string files?: UserFile[] + driveItemId?: string } export interface GraphApiResponse { @@ -220,6 +192,8 @@ export interface CanvasLayout { id?: string emphasis?: string columns?: Array<{ + id?: string + width?: number webparts?: Array<{ id?: string innerHtml?: string @@ -242,6 +216,14 @@ export type SharepointResponse = | SharepointUpdateListItemResponse | SharepointAddListItemResponse | SharepointUploadFileResponse + | SharepointDeleteListItemResponse + | SharepointGetListItemResponse + | SharepointDeletePageResponse + | SharepointUpdatePageResponse + | SharepointPublishPageResponse + | SharepointDownloadFileResponse + | SharepointDeleteFileResponse + | SharepointGetDriveItemResponse export interface SharepointGetListResponse extends ToolResponse { output: { @@ -307,3 +289,83 @@ export interface SharepointUploadFileResponse extends ToolResponse { errors?: SharepointUploadError[] } } + +export interface SharepointDeleteListItemResponse extends ToolResponse { + output: { + deleted: boolean + itemId: string + } +} + +export interface SharepointGetListItemResponse extends ToolResponse { + output: { + item: { + id: string + fields?: Record + } + } +} + +export interface SharepointDeletePageResponse extends ToolResponse { + output: { + deleted: boolean + pageId: string + } +} + +export interface SharepointUpdatePageResponse extends ToolResponse { + output: { + page: SharepointPage + } +} + +export interface SharepointPublishPageResponse extends ToolResponse { + output: { + published: boolean + pageId: string + } +} + +export interface SharepointDriveItem { + id: string + name: string + webUrl?: string + size?: number + createdDateTime?: string + lastModifiedDateTime?: string + file?: { + mimeType?: string + } | null + folder?: { + childCount?: number + } | null + parentReference?: { + id?: string + driveId?: string + path?: string + } | null +} + +export interface SharepointDownloadFileResponse extends ToolResponse { + output: { + file: { + name: string + mimeType: string + data: Buffer | string + size: number + } + } +} + +export interface SharepointDeleteFileResponse extends ToolResponse { + output: { + deleted: boolean + itemId: string + } +} + +export interface SharepointGetDriveItemResponse extends ToolResponse { + output: { + driveItem: SharepointDriveItem + } +} diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts index a0511e45f2b..9fe9e1a69c3 100644 --- a/apps/sim/tools/sharepoint/update_list.ts +++ b/apps/sim/tools/sharepoint/update_list.ts @@ -1,13 +1,10 @@ -import { createLogger } from '@sim/logger' import type { SharepointToolParams, SharepointUpdateListItemResponse, } from '@/tools/sharepoint/types' -import { optionalTrim } from '@/tools/sharepoint/utils' +import { optionalTrim, sanitizeListItemFields } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' -const logger = createLogger('SharePointUpdateListItem') - export const updateListItemTool: ToolConfig< SharepointToolParams, SharepointUpdateListItemResponse @@ -85,53 +82,7 @@ export const updateListItemTool: ToolConfig< throw new Error('listItemFields must not be empty') } - // Filter out system/read-only fields that cannot be updated via Graph - const readOnlyFields = new Set([ - 'Id', - 'id', - 'UniqueId', - 'GUID', - 'ContentTypeId', - 'Created', - 'Modified', - 'Author', - 'Editor', - 'CreatedBy', - 'ModifiedBy', - 'AuthorId', - 'EditorId', - '_UIVersionString', - 'Attachments', - 'FileRef', - 'FileDirRef', - 'FileLeafRef', - ]) - - const entries = Object.entries(params.listItemFields) - const updatableEntries = entries.filter(([key]) => !readOnlyFields.has(key)) - - if (updatableEntries.length !== entries.length) { - const removed = entries.filter(([key]) => readOnlyFields.has(key)).map(([key]) => key) - logger.warn('Removed read-only SharePoint fields from update', { - removed, - }) - } - - if (updatableEntries.length === 0) { - const requestedKeys = Object.keys(params.listItemFields) - throw new Error( - `All provided fields are read-only and cannot be updated: ${requestedKeys.join(', ')}` - ) - } - - const sanitizedFields = Object.fromEntries(updatableEntries) - - logger.info('Updating SharePoint list item fields', { - listItemId: params.itemId, - listId: params.listId, - fieldsKeys: Object.keys(sanitizedFields), - }) - return sanitizedFields + return sanitizeListItemFields(params.listItemFields, { action: 'update' }) }, }, diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts new file mode 100644 index 00000000000..7c08d9b7dae --- /dev/null +++ b/apps/sim/tools/sharepoint/update_page.ts @@ -0,0 +1,168 @@ +import { createLogger } from '@sim/logger' +import type { + CanvasLayout, + SharepointToolParams, + SharepointUpdatePageResponse, +} from '@/tools/sharepoint/types' +import { optionalTrim } from '@/tools/sharepoint/utils' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('SharePointUpdatePage') + +export const updatePageTool: ToolConfig = { + id: 'sharepoint_update_page', + name: 'Update SharePoint Page', + description: 'Update the title and/or content of a SharePoint page', + version: '1.0.0', + + oauth: { + required: true, + provider: 'sharepoint', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'The access token for the SharePoint API', + }, + siteId: { + type: 'string', + required: false, + visibility: 'hidden', + description: 'The ID of the SharePoint site (internal use)', + }, + siteSelector: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Select the SharePoint site', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'The ID of the page to update. Example: a GUID like 12345678-1234-1234-1234-123456789012', + }, + pageTitle: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'The new title of the page', + }, + pageContent: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'The new text content of the page. Replaces the entire canvas layout of the page.', + }, + }, + + request: { + url: (params) => { + const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' + const pageId = optionalTrim(params.pageId) + if (!pageId) throw new Error('pageId must be provided') + return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage` + }, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }), + body: (params) => { + const pageTitle = optionalTrim(params.pageTitle) + const pageContent = typeof params.pageContent === 'string' ? params.pageContent : undefined + + if (!pageTitle && !pageContent) { + throw new Error('At least one of pageTitle or pageContent must be provided') + } + + const pageData: { + '@odata.type': string + title?: string + canvasLayout?: CanvasLayout + } = { + '@odata.type': '#microsoft.graph.sitePage', + } + if (pageTitle) pageData.title = pageTitle + + if (pageContent) { + pageData.canvasLayout = { + horizontalSections: [ + { + layout: 'oneColumn', + id: '1', + emphasis: 'none', + columns: [ + { + id: '1', + width: 12, + webparts: [ + { + id: '6f9230af-2a98-4952-b205-9ede4f9ef548', + innerHtml: `

${pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, + }, + ], + }, + ], + }, + ], + } + } + + logger.info('Updating SharePoint page', { + pageId: params.pageId, + hasTitle: !!pageTitle, + hasContent: !!pageContent, + }) + + return pageData + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + logger.info('SharePoint page updated successfully', { + pageId: data.id, + pageName: data.name, + pageTitle: data.title, + }) + + return { + success: true, + output: { + page: { + id: data.id, + name: data.name, + title: data.title || data.name, + webUrl: data.webUrl, + pageLayout: data.pageLayout, + createdDateTime: data.createdDateTime, + lastModifiedDateTime: data.lastModifiedDateTime, + }, + }, + } + }, + + outputs: { + page: { + type: 'object', + description: 'Updated SharePoint page information', + properties: { + id: { type: 'string', description: 'The unique ID of the page' }, + name: { type: 'string', description: 'The name of the page' }, + title: { type: 'string', description: 'The title of the page' }, + webUrl: { type: 'string', description: 'The URL to access the page' }, + pageLayout: { type: 'string', description: 'The layout type of the page' }, + createdDateTime: { type: 'string', description: 'When the page was created' }, + lastModifiedDateTime: { type: 'string', description: 'When the page was last modified' }, + }, + }, + }, +} diff --git a/apps/sim/tools/sharepoint/utils.ts b/apps/sim/tools/sharepoint/utils.ts index 200e67156eb..353843f370e 100644 --- a/apps/sim/tools/sharepoint/utils.ts +++ b/apps/sim/tools/sharepoint/utils.ts @@ -102,6 +102,57 @@ export function extractTextFromCanvasLayout(canvasLayout: CanvasLayout | null | return finalContent } +/** SharePoint list item fields that are system-managed and cannot be set via the Graph API. */ +export const READ_ONLY_LIST_ITEM_FIELDS = new Set([ + 'Id', + 'id', + 'UniqueId', + 'GUID', + 'ContentTypeId', + 'Created', + 'Modified', + 'Author', + 'Editor', + 'CreatedBy', + 'ModifiedBy', + 'AuthorId', + 'EditorId', + '_UIVersionString', + 'Attachments', + 'FileRef', + 'FileDirRef', + 'FileLeafRef', +]) + +/** + * Removes read-only/system-managed fields from a SharePoint list item field set, logging any + * fields that were stripped. Throws if no updatable fields remain. + */ +export function sanitizeListItemFields( + fields: Record, + context: { action: 'update' | 'create' } +): Record { + const entries = Object.entries(fields) + const updatableEntries = entries.filter(([key]) => !READ_ONLY_LIST_ITEM_FIELDS.has(key)) + + if (updatableEntries.length !== entries.length) { + const removed = entries + .filter(([key]) => READ_ONLY_LIST_ITEM_FIELDS.has(key)) + .map(([key]) => key) + logger.warn(`Removed read-only SharePoint fields from ${context.action}`, { removed }) + } + + if (updatableEntries.length === 0) { + const requestedKeys = Object.keys(fields) + const verb = context.action === 'update' ? 'updated' : 'set' + throw new Error( + `All provided fields are read-only and cannot be ${verb}: ${requestedKeys.join(', ')}` + ) + } + + return Object.fromEntries(updatableEntries) +} + export function cleanODataMetadata(obj: T): T { if (!obj || typeof obj !== 'object') return obj diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 0d0d94230aa..80f935786b0 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 882, - zodRoutes: 882, + totalRoutes: 883, + zodRoutes: 883, nonZodRoutes: 0, } as const From da094764957de4d254332ebd661f66607482c991 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:26:51 -0700 Subject: [PATCH 2/6] fix(sharepoint): encode siteId consistently and fix create_page precedence - URL-encode siteId/groupId everywhere it's interpolated into a Graph request path (Graph site IDs like "host,guid,guid" contain commas that must be encoded) - addresses Cursor Bugbot findings on update_page, delete_page, publish_page, and applies the same fix consistently across every other sharepoint tool for consistency - Fix create_page's site-resolution precedence (was siteSelector before siteId, inconsistent with every sibling tool) --- apps/sim/tools/sharepoint/add_list_items.ts | 2 +- apps/sim/tools/sharepoint/create_list.ts | 2 +- apps/sim/tools/sharepoint/create_page.ts | 4 ++-- apps/sim/tools/sharepoint/delete_list_item.ts | 2 +- apps/sim/tools/sharepoint/delete_page.ts | 2 +- apps/sim/tools/sharepoint/get_list.ts | 7 ++++--- apps/sim/tools/sharepoint/get_list_item.ts | 2 +- apps/sim/tools/sharepoint/list_sites.ts | 4 ++-- apps/sim/tools/sharepoint/publish_page.ts | 2 +- apps/sim/tools/sharepoint/update_list.ts | 2 +- apps/sim/tools/sharepoint/update_page.ts | 4 ++-- 11 files changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts index 1958a0fe9e6..c335c996cda 100644 --- a/apps/sim/tools/sharepoint/add_list_items.ts +++ b/apps/sim/tools/sharepoint/add_list_items.ts @@ -77,7 +77,7 @@ export const addListItemTool: ToolConfig ({ diff --git a/apps/sim/tools/sharepoint/create_list.ts b/apps/sim/tools/sharepoint/create_list.ts index 1fb20f65180..d5640189be1 100644 --- a/apps/sim/tools/sharepoint/create_list.ts +++ b/apps/sim/tools/sharepoint/create_list.ts @@ -71,7 +71,7 @@ export const createListTool: ToolConfig { const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' - return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists` + return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index 32a1e169269..4a56fa179fe 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -61,8 +61,8 @@ export const createPageTool: ToolConfig { - const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' - return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages` + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' + return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages` }, method: 'POST', headers: (params) => ({ diff --git a/apps/sim/tools/sharepoint/delete_list_item.ts b/apps/sim/tools/sharepoint/delete_list_item.ts index b71e6b4e823..3c0c7dbb4ff 100644 --- a/apps/sim/tools/sharepoint/delete_list_item.ts +++ b/apps/sim/tools/sharepoint/delete_list_item.ts @@ -62,7 +62,7 @@ export const deleteListItemTool: ToolConfig< if (!itemId) throw new Error('itemId must be provided') const listSegment = encodeURIComponent(listId) const itemSegment = encodeURIComponent(itemId) - return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${itemSegment}` + return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${itemSegment}` }, method: 'DELETE', headers: (params) => ({ diff --git a/apps/sim/tools/sharepoint/delete_page.ts b/apps/sim/tools/sharepoint/delete_page.ts index 4b5b77872eb..6a061951cb7 100644 --- a/apps/sim/tools/sharepoint/delete_page.ts +++ b/apps/sim/tools/sharepoint/delete_page.ts @@ -46,7 +46,7 @@ export const deletePageTool: ToolConfig ({ diff --git a/apps/sim/tools/sharepoint/get_list.ts b/apps/sim/tools/sharepoint/get_list.ts index 5b1d4d44a21..a4061e32f57 100644 --- a/apps/sim/tools/sharepoint/get_list.ts +++ b/apps/sim/tools/sharepoint/get_list.ts @@ -73,10 +73,11 @@ export const getListTool: ToolConfig ({ diff --git a/apps/sim/tools/sharepoint/update_list.ts b/apps/sim/tools/sharepoint/update_list.ts index 9fe9e1a69c3..850c97f8f03 100644 --- a/apps/sim/tools/sharepoint/update_list.ts +++ b/apps/sim/tools/sharepoint/update_list.ts @@ -69,7 +69,7 @@ export const updateListItemTool: ToolConfig< throw new Error('listId must be provided') } const listSegment = encodeURIComponent(listId) - return `https://graph.microsoft.com/v1.0/sites/${siteId}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields` + return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/lists/${listSegment}/items/${encodeURIComponent(itemId)}/fields` }, method: 'PATCH', headers: (params) => ({ diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts index 7c08d9b7dae..58cbf095552 100644 --- a/apps/sim/tools/sharepoint/update_page.ts +++ b/apps/sim/tools/sharepoint/update_page.ts @@ -63,10 +63,10 @@ export const updatePageTool: ToolConfig { - const siteId = optionalTrim(params.siteSelector) || optionalTrim(params.siteId) || 'root' + const siteId = optionalTrim(params.siteId) || optionalTrim(params.siteSelector) || 'root' const pageId = optionalTrim(params.pageId) if (!pageId) throw new Error('pageId must be provided') - return `https://graph.microsoft.com/v1.0/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage` + return `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage` }, method: 'PATCH', headers: (params) => ({ From fe7b493bbc977537bd149939b36417c8b678a983 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:34:33 -0700 Subject: [PATCH 3/6] fix(sharepoint): escape HTML in page content and stop fallback from throwing - create_page/update_page only escaped quotes when building innerHtml; add a shared escapeHtml() helper that also escapes &, <, > so page content with angle brackets/ampersands doesn't corrupt the canvas layout - add_list_items' fields fallback (sanitized re-derivation when Graph's response omits fields) could throw on a malformed fallback input after the item was already created successfully; catch and fall back to undefined instead of failing an already-successful response --- apps/sim/tools/sharepoint/add_list_items.ts | 11 ++++++++--- apps/sim/tools/sharepoint/create_page.ts | 4 ++-- apps/sim/tools/sharepoint/update_page.ts | 4 ++-- apps/sim/tools/sharepoint/utils.ts | 9 +++++++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/sim/tools/sharepoint/add_list_items.ts b/apps/sim/tools/sharepoint/add_list_items.ts index c335c996cda..3bfcd4836b9 100644 --- a/apps/sim/tools/sharepoint/add_list_items.ts +++ b/apps/sim/tools/sharepoint/add_list_items.ts @@ -99,9 +99,14 @@ export const addListItemTool: ToolConfig | undefined) || - (params ? resolveSanitizedFields(params.listItemFields) : undefined) + let fields = data?.fields as Record | undefined + if (!fields && params) { + try { + fields = resolveSanitizedFields(params.listItemFields) + } catch { + // Item was already created successfully; a malformed fallback input must not fail the response. + } + } return { success: true, diff --git a/apps/sim/tools/sharepoint/create_page.ts b/apps/sim/tools/sharepoint/create_page.ts index 4a56fa179fe..79453994c04 100644 --- a/apps/sim/tools/sharepoint/create_page.ts +++ b/apps/sim/tools/sharepoint/create_page.ts @@ -4,7 +4,7 @@ import type { SharepointPage, SharepointToolParams, } from '@/tools/sharepoint/types' -import { optionalTrim } from '@/tools/sharepoint/utils' +import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointCreatePage') @@ -103,7 +103,7 @@ export const createPageTool: ToolConfig${pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, + innerHtml: `

${escapeHtml(pageContent)}

`, }, ], }, diff --git a/apps/sim/tools/sharepoint/update_page.ts b/apps/sim/tools/sharepoint/update_page.ts index 58cbf095552..52f5da6153b 100644 --- a/apps/sim/tools/sharepoint/update_page.ts +++ b/apps/sim/tools/sharepoint/update_page.ts @@ -4,7 +4,7 @@ import type { SharepointToolParams, SharepointUpdatePageResponse, } from '@/tools/sharepoint/types' -import { optionalTrim } from '@/tools/sharepoint/utils' +import { escapeHtml, optionalTrim } from '@/tools/sharepoint/utils' import type { ToolConfig } from '@/tools/types' const logger = createLogger('SharePointUpdatePage') @@ -105,7 +105,7 @@ export const updatePageTool: ToolConfig${pageContent.replace(/"/g, '"').replace(/'/g, ''')}

`, + innerHtml: `

${escapeHtml(pageContent)}

`, }, ], }, diff --git a/apps/sim/tools/sharepoint/utils.ts b/apps/sim/tools/sharepoint/utils.ts index 353843f370e..9e7d60abacd 100644 --- a/apps/sim/tools/sharepoint/utils.ts +++ b/apps/sim/tools/sharepoint/utils.ts @@ -13,6 +13,15 @@ export function escapeODataString(value: string): string { return value.replace(/'/g, "''") } +export function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + export function getGraphNextPageUrl(data: object): string | undefined { const nextLink = (data as Record)['@odata.nextLink'] return typeof nextLink === 'string' ? nextLink : undefined From 50ed83cff26b7b5ddf0b548444f8e4b00c09d988 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 00:41:52 -0700 Subject: [PATCH 4/6] fix(sharepoint): scope columnDefinitions->pageContent mapping to create_list Both V1 and V2 tools.config.params mapped columnDefinitions onto pageContent whenever columnDefinitions was truthy, with no operation check. Stale list-column JSON left in block state after switching to create_page/ update_page would silently replace the user's intended page text. Gate the mapping on operation === create_list (V1) / sharepoint_create_list (V2). --- apps/sim/blocks/blocks/sharepoint.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index 84b1ba230fd..ed61efdf9b1 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -588,7 +588,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, baseParams.files = normalizedFiles } - if (columnDefinitions) { + if (columnDefinitions && others.operation === 'create_list') { baseParams.pageContent = columnDefinitions } @@ -1154,7 +1154,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, maxPages: maxPages ? Number.parseInt(String(maxPages), 10) : undefined, } - if (columnDefinitions) { + if (columnDefinitions && rest.operation === 'sharepoint_create_list') { result.pageContent = columnDefinitions } if (normalizedFiles) { From 52793b2f911baec78fd47b6ba4bea8b57db422b5 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 01:09:35 -0700 Subject: [PATCH 5/6] fix(sharepoint): cap download-file content fetch at MAX_FILE_SIZE Greptile flagged the content fetch as unbounded - a large file would buffer entirely in memory before base64-encoding into the JSON response. Pass maxResponseBytes: MAX_FILE_SIZE (100MB, same constant used by the upload path) to secureFetchWithPinnedIP so oversized files reject early with a clear PayloadSizeLimitError instead of exhausting memory. --- apps/sim/app/api/tools/sharepoint/download-file/route.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/sim/app/api/tools/sharepoint/download-file/route.ts b/apps/sim/app/api/tools/sharepoint/download-file/route.ts index 6f3bb947770..1d9adcd5552 100644 --- a/apps/sim/app/api/tools/sharepoint/download-file/route.ts +++ b/apps/sim/app/api/tools/sharepoint/download-file/route.ts @@ -10,6 +10,7 @@ import { } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { MAX_FILE_SIZE } from '@/lib/uploads/utils/validation' export const dynamic = 'force-dynamic' @@ -122,6 +123,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { headers: { Authorization: authHeader }, // The content endpoint 302s to a preauthenticated URL on a different origin that needs no auth. stripAuthOnRedirect: true, + maxResponseBytes: MAX_FILE_SIZE, } ) From 5a076d509ce123926ecf7d585d7bc9bc162110ee Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 08:31:35 -0700 Subject: [PATCH 6/6] fix(sharepoint): close V1 block input/output gaps and encode upload URLs Final validation pass (4 parallel audit agents against live Graph docs) surfaced remaining gaps: - apps/sim/app/api/tools/sharepoint/upload/route.ts: siteId/driveId were not encodeURIComponent-ed in the Graph upload URL, unlike every other sharepoint route/tool - same encoding-gap class fixed everywhere else - read_page.ts: transformResponse recomputed siteId with a raw `||` in 3 spots instead of the optionalTrim(...) || optionalTrim(...) || 'root' precedence used by request.url and every sibling tool - SharepointBlock (V1, legacy/hidden-from-toolbar but still executable for existing saved workflows): listItemFields and listItemId were missing `required` for update_list despite the tool requiring them; maxPages/groupId/includeColumns/includeItems/nextPageUrl subBlocks were entirely absent, making those tool params unreachable from the UI; block-level outputs were missing site/pages/content/totalPages/ nextPageUrl/lists/skippedFiles/skippedCount/errors even though the underlying tools return them - V2 already covered all of these --- .../app/api/tools/sharepoint/upload/route.ts | 4 +- apps/sim/blocks/blocks/sharepoint.ts | 98 ++++++++++++++++++- apps/sim/tools/sharepoint/read_page.ts | 6 +- 3 files changed, 102 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index 3c85a7bd7d1..55bb4eec935 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -135,8 +135,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { .join('/') const uploadUrl = driveId - ? `https://graph.microsoft.com/v1.0/drives/${driveId}/root:${encodedPath}:/content` - : `https://graph.microsoft.com/v1.0/sites/${siteId}/drive/root:${encodedPath}:/content` + ? `https://graph.microsoft.com/v1.0/drives/${encodeURIComponent(driveId)}/root:${encodedPath}:/content` + : `https://graph.microsoft.com/v1.0/sites/${encodeURIComponent(siteId)}/drive/root:${encodedPath}:/content` logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index ed61efdf9b1..d73eb1adf09 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -183,7 +183,7 @@ export const SharepointBlock: BlockConfig = { field: 'operation', value: ['update_list', 'get_list_item', 'delete_list_item'], }, - required: { field: 'operation', value: ['get_list_item', 'delete_list_item'] }, + required: { field: 'operation', value: ['update_list', 'get_list_item', 'delete_list_item'] }, }, { @@ -348,6 +348,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, 'Enter list item fields as JSON (e.g., {"Title": "My Item", "Status": "Active"})', canonicalParamId: 'listItemFields', condition: { field: 'operation', value: ['update_list', 'add_list_items'] }, + required: { field: 'operation', value: ['update_list', 'add_list_items'] }, wandConfig: { enabled: true, prompt: `Generate a JSON object for SharePoint list item fields based on the user's description. @@ -390,6 +391,58 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, }, }, + { + id: 'maxPages', + title: 'Max Pages', + type: 'short-input', + placeholder: 'Default 10, maximum 50', + condition: { field: 'operation', value: 'read_page' }, + mode: 'advanced', + }, + { + id: 'groupId', + title: 'Group ID', + type: 'short-input', + placeholder: 'Optional Microsoft 365 group ID', + condition: { field: 'operation', value: 'list_sites' }, + mode: 'advanced', + }, + { + id: 'includeColumns', + title: 'Include Columns', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'read_list' }, + mode: 'advanced', + }, + { + id: 'includeItems', + title: 'Include Items', + type: 'dropdown', + options: [ + { label: 'Yes', id: 'true' }, + { label: 'No', id: 'false' }, + ], + value: () => 'true', + condition: { field: 'operation', value: 'read_list' }, + mode: 'advanced', + }, + { + id: 'nextPageUrl', + title: 'Next Page URL', + type: 'short-input', + placeholder: 'Paste the @odata.nextLink URL from a previous result', + condition: { + field: 'operation', + value: ['read_page', 'list_sites', 'read_list'], + }, + mode: 'advanced', + }, + // Upload / Download / Delete File / Get Drive Item operation fields { id: 'driveId', @@ -624,6 +677,12 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, folderPath: { type: 'string', description: 'Folder path for file upload' }, fileName: { type: 'string', description: 'File name override' }, files: { type: 'array', description: 'Files to upload' }, + maxPages: { type: 'number', description: 'Maximum pages to return when reading all pages' }, + groupId: { type: 'string', description: 'Microsoft 365 group ID for group-owned sites' }, + nextPageUrl: { + type: 'string', + description: 'Full Microsoft Graph @odata.nextLink URL from a previous result', + }, }, outputs: { sites: { @@ -631,16 +690,40 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'An array of SharePoint site objects, each containing details such as id, name, and more.', }, + site: { + type: 'json', + description: 'Single SharePoint site object (id, name, displayName, webUrl)', + }, page: { type: 'json', description: 'SharePoint page object (id, name, title, webUrl, pageLayout)', }, + pages: { + type: 'json', + description: 'Array of SharePoint pages with content ([{page, content}])', + }, + content: { + type: 'json', + description: 'Content of the SharePoint page (content, canvasLayout)', + }, + totalPages: { + type: 'number', + description: 'Total number of pages found when listing all pages', + }, + nextPageUrl: { + type: 'string', + description: 'Full Microsoft Graph @odata.nextLink URL for the next page of results', + }, published: { type: 'boolean', description: 'Whether the page was published' }, deleted: { type: 'boolean', description: 'Whether the item/page/file was deleted' }, list: { type: 'json', description: 'SharePoint list object (id, displayName, name, webUrl, etc.)', }, + lists: { + type: 'json', + description: 'Array of SharePoint list objects when no specific list is requested', + }, item: { type: 'json', description: 'SharePoint list item with fields', @@ -665,6 +748,19 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, type: 'number', description: 'Number of files uploaded', }, + skippedFiles: { + type: 'json', + description: + 'Files skipped during upload for exceeding the size limit (name, size, limit, reason)', + }, + skippedCount: { + type: 'number', + description: 'Number of files skipped during upload', + }, + errors: { + type: 'json', + description: 'Per-file upload errors ([{name, error, status}])', + }, itemId: { type: 'string', description: 'ID of the deleted list item or file' }, pageId: { type: 'string', description: 'ID of the deleted or published page' }, success: { diff --git a/apps/sim/tools/sharepoint/read_page.ts b/apps/sim/tools/sharepoint/read_page.ts index 926ba9a1c48..8785b21a5da 100644 --- a/apps/sim/tools/sharepoint/read_page.ts +++ b/apps/sim/tools/sharepoint/read_page.ts @@ -181,7 +181,7 @@ export const readPageTool: ToolConfig