From 9669ca46ca63ad717215594c8212e26ec1f1b55b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:30:23 -0700 Subject: [PATCH 01/17] feat(microsoft-excel): add SharePoint drive support for Excel integration --- .../content/docs/en/tools/microsoft_excel.mdx | 2 + .../api/auth/oauth/microsoft/files/route.ts | 7 +- .../api/tools/microsoft_excel/drives/route.ts | 94 +++++++++++++++++++ .../api/tools/microsoft_excel/sheets/route.ts | 23 ++--- apps/sim/blocks/blocks/microsoft_excel.ts | 71 +++++++++++++- apps/sim/hooks/selectors/registry.ts | 50 ++++++++++ apps/sim/hooks/selectors/types.ts | 2 + apps/sim/lib/workflows/subblocks/context.ts | 1 + apps/sim/tools/microsoft_excel/read.ts | 80 ++++++++-------- apps/sim/tools/microsoft_excel/table_add.ts | 32 +++---- apps/sim/tools/microsoft_excel/types.ts | 4 + apps/sim/tools/microsoft_excel/utils.ts | 35 ++++--- .../tools/microsoft_excel/worksheet_add.ts | 19 ++-- apps/sim/tools/microsoft_excel/write.ts | 49 +++++----- 14 files changed, 355 insertions(+), 114 deletions(-) create mode 100644 apps/sim/app/api/tools/microsoft_excel/drives/route.ts diff --git a/apps/docs/content/docs/en/tools/microsoft_excel.mdx b/apps/docs/content/docs/en/tools/microsoft_excel.mdx index 4da61a5b9cd..08733e5eb30 100644 --- a/apps/docs/content/docs/en/tools/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_excel.mdx @@ -45,6 +45,7 @@ Read data from a specific sheet in a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to read from \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | | `range` | string | No | The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet. | #### Output @@ -67,6 +68,7 @@ Write data to a specific sheet in a Microsoft Excel spreadsheet | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `spreadsheetId` | string | Yes | The ID of the spreadsheet/workbook to write to \(e.g., "01ABC123DEF456"\) | +| `driveId` | string | No | The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive. | | `range` | string | No | The range of cells to write to \(e.g., "Sheet1!A1:B2"\) | | `values` | array | Yes | The data to write as a 2D array \(e.g., \[\["Name", "Age"\], \["Alice", 30\]\]\) or array of objects | | `valueInputOption` | string | No | The format of the data to write | diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index 23bd2e57e5e..e0151e46e93 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -19,6 +19,7 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const credentialId = searchParams.get('credentialId') const query = searchParams.get('query') || '' + const driveId = searchParams.get('driveId') || undefined const workflowId = searchParams.get('workflowId') || undefined if (!credentialId) { @@ -72,8 +73,12 @@ export async function GET(request: NextRequest) { ) searchParams_new.append('$top', '50') + // When driveId is provided (SharePoint), search within that specific drive. + // Otherwise, search the user's personal OneDrive. + const drivePath = driveId ? `drives/${driveId}` : 'me/drive' + const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/root/search(q='${encodeURIComponent(searchQuery)}')?${searchParams_new.toString()}`, + `https://graph.microsoft.com/v1.0/${drivePath}/root/search(q='${encodeURIComponent(searchQuery)}')?${searchParams_new.toString()}`, { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts new file mode 100644 index 00000000000..f0d42b6d574 --- /dev/null +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import { NextResponse } from 'next/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { generateRequestId } from '@/lib/core/utils/request' +import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('MicrosoftExcelDrivesAPI') + +interface GraphDrive { + id: string + name: string + driveType: string + webUrl?: string +} + +/** + * List document libraries (drives) for a SharePoint site. + * Used by the microsoft.excel.drives selector to let users pick + * which drive contains their Excel file. + */ +export async function POST(request: Request) { + const requestId = generateRequestId() + + try { + const body = await request.json() + const { credential, workflowId, siteId } = body + + if (!credential) { + logger.warn(`[${requestId}] Missing credential in request`) + return NextResponse.json({ error: 'Credential is required' }, { status: 400 }) + } + + if (!siteId) { + logger.warn(`[${requestId}] Missing siteId in request`) + return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) + } + + const authz = await authorizeCredentialUse(request as Request, { + credentialId: credential, + workflowId, + }) + if (!authz.ok || !authz.credentialOwnerUserId) { + return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + credential, + authz.credentialOwnerUserId, + requestId + ) + if (!accessToken) { + logger.warn(`[${requestId}] Failed to obtain valid access token`) + return NextResponse.json( + { error: 'Failed to obtain valid access token', authRequired: true }, + { status: 401 } + ) + } + + const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=id,name,driveType,webUrl` + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })) + logger.error(`[${requestId}] Microsoft Graph API error fetching drives`, { + status: response.status, + error: errorData.error?.message, + }) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch drives' }, + { status: response.status } + ) + } + + const data = await response.json() + const drives = (data.value || []).map((drive: GraphDrive) => ({ + id: drive.id, + name: drive.name, + driveType: drive.driveType, + })) + + logger.info(`[${requestId}] Successfully fetched ${drives.length} drives for site ${siteId}`) + return NextResponse.json({ drives }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching drives`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index d4f8035149e..6e95775ccf7 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -30,6 +30,7 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const credentialId = searchParams.get('credentialId') const spreadsheetId = searchParams.get('spreadsheetId') + const driveId = searchParams.get('driveId') || undefined const workflowId = searchParams.get('workflowId') || undefined if (!credentialId) { @@ -61,17 +62,17 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) - // Fetch worksheets from Microsoft Graph API - const worksheetsResponse = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets`, - { - method: 'GET', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - } - ) + const basePath = driveId + ? `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` + : `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` + + const worksheetsResponse = await fetch(`${basePath}/workbook/worksheets`, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) if (!worksheetsResponse.ok) { const errorData = await worksheetsResponse diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 4b04368742c..a3a41251eb6 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -77,6 +77,13 @@ export const MicrosoftExcelBlock: BlockConfig = { dependsOn: ['credential'], mode: 'advanced', }, + { + id: 'driveId', + title: 'Drive ID (SharePoint)', + type: 'short-input', + placeholder: 'Leave empty for OneDrive, or enter drive ID for SharePoint', + mode: 'advanced', + }, { id: 'range', title: 'Range', @@ -249,9 +256,17 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, } }, params: (params) => { - const { oauthCredential, values, spreadsheetId, tableName, worksheetName, ...rest } = params + const { + oauthCredential, + values, + spreadsheetId, + tableName, + worksheetName, + driveId, + siteId: _siteId, + ...rest + } = params - // Use canonical param ID (raw subBlock IDs are deleted after serialization) const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : '' let parsedValues @@ -276,6 +291,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, const baseParams = { ...rest, spreadsheetId: effectiveSpreadsheetId, + driveId: driveId ? String(driveId).trim() : undefined, values: parsedValues, oauthCredential, } @@ -302,6 +318,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Microsoft Excel access token' }, spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' }, + driveId: { type: 'string', description: 'Drive ID for SharePoint document libraries' }, range: { type: 'string', description: 'Cell range' }, tableName: { type: 'string', description: 'Table name' }, worksheetName: { type: 'string', description: 'Worksheet name' }, @@ -377,6 +394,32 @@ export const MicrosoftExcelV2Block: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, + // SharePoint Site Selector (basic mode, optional) + { + id: 'siteSelector', + title: 'SharePoint Site (Optional)', + type: 'file-selector', + canonicalParamId: 'siteId', + serviceId: 'sharepoint', + selectorKey: 'sharepoint.sites', + requiredScopes: [], + placeholder: 'Select a SharePoint site (leave empty for OneDrive)', + dependsOn: ['credential'], + mode: 'basic', + }, + // SharePoint Drive Selector (basic mode, depends on site) + { + id: 'driveSelector', + title: 'Document Library', + type: 'file-selector', + canonicalParamId: 'driveId', + serviceId: 'microsoft-excel', + selectorKey: 'microsoft.excel.drives', + selectorAllowSearch: false, + placeholder: 'Select a document library', + dependsOn: ['credential', 'siteSelector'], + mode: 'basic', + }, // Spreadsheet Selector (basic mode) { id: 'spreadsheetId', @@ -401,6 +444,15 @@ export const MicrosoftExcelV2Block: BlockConfig = { dependsOn: ['credential'], mode: 'advanced', }, + // Drive ID for SharePoint (advanced mode) + { + id: 'manualDriveId', + title: 'Drive ID (SharePoint)', + type: 'short-input', + canonicalParamId: 'driveId', + placeholder: 'Leave empty for OneDrive, or enter drive ID for SharePoint', + mode: 'advanced', + }, // Sheet Name Selector (basic mode) { id: 'sheetName', @@ -514,11 +566,19 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, fallbackToolId: 'microsoft_excel_read_v2', }), params: (params) => { - const { oauthCredential, values, spreadsheetId, sheetName, cellRange, ...rest } = params + const { + oauthCredential, + values, + spreadsheetId, + sheetName, + cellRange, + driveId, + siteId: _siteId, + ...rest + } = params const parsedValues = values ? JSON.parse(values as string) : undefined - // Use canonical param IDs (raw subBlock IDs are deleted after serialization) const effectiveSpreadsheetId = spreadsheetId ? String(spreadsheetId).trim() : '' const effectiveSheetName = sheetName ? String(sheetName).trim() : '' @@ -535,6 +595,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, spreadsheetId: effectiveSpreadsheetId, sheetName: effectiveSheetName, cellRange: cellRange ? (cellRange as string).trim() : undefined, + driveId: driveId ? String(driveId).trim() : undefined, values: parsedValues, oauthCredential, } @@ -544,6 +605,8 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, inputs: { operation: { type: 'string', description: 'Operation to perform' }, oauthCredential: { type: 'string', description: 'Microsoft Excel access token' }, + siteId: { type: 'string', description: 'SharePoint site ID (used for drive/file browsing)' }, + driveId: { type: 'string', description: 'Drive ID for SharePoint document libraries' }, spreadsheetId: { type: 'string', description: 'Spreadsheet identifier (canonical param)' }, sheetName: { type: 'string', description: 'Name of the sheet/tab (canonical param)' }, cellRange: { type: 'string', description: 'Cell range (e.g., A1:D10)' }, diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 0fe0d1b84bb..e6535d23754 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -1504,6 +1504,7 @@ const registry: Record = { 'microsoft.excel.sheets', context.oauthCredential ?? 'none', context.spreadsheetId ?? 'none', + context.driveId ?? 'none', ], enabled: ({ context }) => Boolean(context.oauthCredential && context.spreadsheetId), fetchList: async ({ context }: SelectorQueryArgs) => { @@ -1517,6 +1518,7 @@ const registry: Record = { searchParams: { credentialId, spreadsheetId: context.spreadsheetId, + driveId: context.driveId, workflowId: context.workflowId, }, } @@ -1527,6 +1529,52 @@ const registry: Record = { })) }, }, + 'microsoft.excel.drives': { + key: 'microsoft.excel.drives', + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'microsoft.excel.drives', + context.oauthCredential ?? 'none', + context.siteId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.oauthCredential && context.siteId), + fetchList: async ({ context }: SelectorQueryArgs) => { + const credentialId = ensureCredential(context, 'microsoft.excel.drives') + if (!context.siteId) { + throw new Error('Missing site ID for microsoft.excel.drives selector') + } + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + }) + const data = await fetchJson<{ drives: { id: string; name: string }[] }>( + '/api/tools/microsoft_excel/drives', + { method: 'POST', body } + ) + return (data.drives || []).map((drive) => ({ + id: drive.id, + label: drive.name, + })) + }, + fetchById: async ({ context, detailId }: SelectorQueryArgs) => { + if (!detailId || !context.siteId) return null + const credentialId = ensureCredential(context, 'microsoft.excel.drives') + const body = JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + }) + const data = await fetchJson<{ drives: { id: string; name: string }[] }>( + '/api/tools/microsoft_excel/drives', + { method: 'POST', body } + ) + const drive = (data.drives || []).find((d) => d.id === detailId) ?? null + if (!drive) return null + return { id: drive.id, label: drive.name } + }, + }, 'microsoft.excel': { key: 'microsoft.excel', staleTime: SELECTOR_STALE, @@ -1534,6 +1582,7 @@ const registry: Record = { 'selectors', 'microsoft.excel', context.oauthCredential ?? 'none', + context.driveId ?? 'none', search ?? '', ], enabled: ({ context }) => Boolean(context.oauthCredential), @@ -1545,6 +1594,7 @@ const registry: Record = { searchParams: { credentialId, query: search, + driveId: context.driveId, workflowId: context.workflowId, }, } diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index bd5bcac547b..c4423b52e33 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -40,6 +40,7 @@ export type SelectorKey = | 'onedrive.folders' | 'sharepoint.sites' | 'microsoft.excel' + | 'microsoft.excel.drives' | 'microsoft.excel.sheets' | 'microsoft.word' | 'microsoft.planner' @@ -75,6 +76,7 @@ export interface SelectorContext { siteId?: string collectionId?: string spreadsheetId?: string + driveId?: string excludeWorkflowId?: string baseId?: string datasetId?: string diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 6f41759cffa..eca39260ecc 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -17,6 +17,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'siteId', 'collectionId', 'spreadsheetId', + 'driveId', 'fileId', 'baseId', 'datasetId', diff --git a/apps/sim/tools/microsoft_excel/read.ts b/apps/sim/tools/microsoft_excel/read.ts index b1ffe5f3207..22a3aea0a39 100644 --- a/apps/sim/tools/microsoft_excel/read.ts +++ b/apps/sim/tools/microsoft_excel/read.ts @@ -6,6 +6,7 @@ import type { MicrosoftExcelV2ToolParams, } from '@/tools/microsoft_excel/types' import { + getItemBasePath, getSpreadsheetWebUrl, trimTrailingEmptyRowsAndColumns, } from '@/tools/microsoft_excel/utils' @@ -35,6 +36,13 @@ export const readTool: ToolConfig { @@ -91,6 +98,9 @@ export const readTool: ToolConfig { + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId + // If we came from the worksheets listing (no range provided), resolve first sheet name then fetch range if (response.url.includes('/workbook/worksheets?')) { const listData = await response.json() @@ -100,23 +110,19 @@ export const readTool: ToolConfig { @@ -294,20 +289,19 @@ export const readV2Tool: ToolConfig { const data = await response.json() - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) const address: string = data.address || data.addressLocal || '' const rawValues: ExcelCellValue[][] = data.values || [] const values = trimTrailingEmptyRowsAndColumns(rawValues) - // Extract sheet name from address (format: SheetName!A1:B2) const sheetName = params?.sheetName || address.split('!')[0] || '' return { diff --git a/apps/sim/tools/microsoft_excel/table_add.ts b/apps/sim/tools/microsoft_excel/table_add.ts index c84047e1146..eeca045617a 100644 --- a/apps/sim/tools/microsoft_excel/table_add.ts +++ b/apps/sim/tools/microsoft_excel/table_add.ts @@ -2,7 +2,7 @@ import type { MicrosoftExcelTableAddResponse, MicrosoftExcelTableToolParams, } from '@/tools/microsoft_excel/types' -import { getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' +import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' import type { ToolConfig } from '@/tools/types' export const tableAddTool: ToolConfig< @@ -33,6 +33,13 @@ export const tableAddTool: ToolConfig< description: 'The ID of the spreadsheet/workbook containing the table (e.g., "01ABC123DEF456")', }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, tableName: { type: 'string', required: true, @@ -51,7 +58,8 @@ export const tableAddTool: ToolConfig< request: { url: (params) => { const tableName = encodeURIComponent(params.tableName) - return `https://graph.microsoft.com/v1.0/me/drive/items/${params.spreadsheetId}/workbook/tables('${tableName}')/rows/add` + const basePath = getItemBasePath(params.spreadsheetId, params.driveId) + return `${basePath}/workbook/tables('${tableName}')/rows/add` }, method: 'POST', headers: (params) => ({ @@ -106,34 +114,26 @@ export const tableAddTool: ToolConfig< transformResponse: async (response: Response, params?: MicrosoftExcelTableToolParams) => { const data = await response.json() - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId - // Fetch the browser-accessible web URL const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) - const metadata = { - spreadsheetId, - spreadsheetUrl: webUrl, - } - - const result = { + return { success: true, output: { index: data.index || 0, values: data.values || [], metadata: { - spreadsheetId: metadata.spreadsheetId, - spreadsheetUrl: metadata.spreadsheetUrl, + spreadsheetId, + spreadsheetUrl: webUrl, }, }, } - - return result }, outputs: { diff --git a/apps/sim/tools/microsoft_excel/types.ts b/apps/sim/tools/microsoft_excel/types.ts index 1a05fafbcec..2032ce34cb5 100644 --- a/apps/sim/tools/microsoft_excel/types.ts +++ b/apps/sim/tools/microsoft_excel/types.ts @@ -63,6 +63,7 @@ export interface MicrosoftExcelWorksheetAddResponse extends ToolResponse { export interface MicrosoftExcelToolParams { accessToken: string spreadsheetId: string + driveId?: string range?: string values?: ExcelCellValue[][] valueInputOption?: 'RAW' | 'USER_ENTERED' @@ -75,6 +76,7 @@ export interface MicrosoftExcelToolParams { export interface MicrosoftExcelTableToolParams { accessToken: string spreadsheetId: string + driveId?: string tableName: string values: ExcelCellValue[][] rowIndex?: number @@ -83,6 +85,7 @@ export interface MicrosoftExcelTableToolParams { export interface MicrosoftExcelWorksheetToolParams { accessToken: string spreadsheetId: string + driveId?: string worksheetName: string } @@ -96,6 +99,7 @@ export type MicrosoftExcelResponse = export interface MicrosoftExcelV2ToolParams { accessToken: string spreadsheetId: string + driveId?: string sheetName: string cellRange?: string values?: ExcelCellValue[][] diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index dc10e74629d..600ecbf0354 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -3,6 +3,18 @@ import type { ExcelCellValue } from '@/tools/microsoft_excel/types' const logger = createLogger('MicrosoftExcelUtils') +/** + * Returns the Graph API base path for an Excel item. + * When driveId is provided, uses /drives/{driveId}/items/{itemId} (SharePoint/shared drives). + * When driveId is omitted, uses /me/drive/items/{itemId} (personal OneDrive). + */ +export function getItemBasePath(spreadsheetId: string, driveId?: string): string { + if (driveId) { + return `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` + } + return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` +} + export function trimTrailingEmptyRowsAndColumns(matrix: ExcelCellValue[][]): ExcelCellValue[][] { if (!Array.isArray(matrix) || matrix.length === 0) return [] @@ -43,33 +55,32 @@ export function trimTrailingEmptyRowsAndColumns(matrix: ExcelCellValue[][]): Exc */ export async function getSpreadsheetWebUrl( spreadsheetId: string, - accessToken: string + accessToken: string, + driveId?: string ): Promise { + const basePath = getItemBasePath(spreadsheetId, driveId) try { - const response = await fetch( - `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}?$select=id,webUrl`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ) + const response = await fetch(`${basePath}?$select=id,webUrl`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) if (!response.ok) { logger.warn('Failed to fetch spreadsheet webUrl, using Graph API URL as fallback', { spreadsheetId, status: response.status, }) - return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` + return basePath } const data = await response.json() - return data.webUrl || `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` + return data.webUrl || basePath } catch (error) { logger.warn('Error fetching spreadsheet webUrl, using Graph API URL as fallback', { spreadsheetId, error, }) - return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` + return basePath } } diff --git a/apps/sim/tools/microsoft_excel/worksheet_add.ts b/apps/sim/tools/microsoft_excel/worksheet_add.ts index 1350bc55532..6cfc91b47db 100644 --- a/apps/sim/tools/microsoft_excel/worksheet_add.ts +++ b/apps/sim/tools/microsoft_excel/worksheet_add.ts @@ -2,7 +2,7 @@ import type { MicrosoftExcelWorksheetAddResponse, MicrosoftExcelWorksheetToolParams, } from '@/tools/microsoft_excel/types' -import { getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' +import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' import type { ToolConfig } from '@/tools/types' /** @@ -36,6 +36,13 @@ export const worksheetAddTool: ToolConfig< visibility: 'user-or-llm', description: 'The ID of the Excel workbook to add the worksheet to (e.g., "01ABC123DEF456")', }, + driveId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'The ID of the drive containing the spreadsheet. Required for SharePoint files. If omitted, uses personal OneDrive.', + }, worksheetName: { type: 'string', required: true, @@ -51,7 +58,8 @@ export const worksheetAddTool: ToolConfig< if (!spreadsheetId) { throw new Error('Spreadsheet ID is required') } - return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets/add` + const basePath = getItemBasePath(spreadsheetId, params.driveId) + return `${basePath}/workbook/worksheets/add` }, method: 'POST', headers: (params) => { @@ -106,15 +114,14 @@ export const worksheetAddTool: ToolConfig< const data = await response.json() - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId - // Fetch the browser-accessible web URL const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) const result: MicrosoftExcelWorksheetAddResponse = { success: true, diff --git a/apps/sim/tools/microsoft_excel/write.ts b/apps/sim/tools/microsoft_excel/write.ts index 335d59acc6c..1e5fa7b3bde 100644 --- a/apps/sim/tools/microsoft_excel/write.ts +++ b/apps/sim/tools/microsoft_excel/write.ts @@ -4,7 +4,7 @@ import type { MicrosoftExcelV2WriteResponse, MicrosoftExcelWriteResponse, } from '@/tools/microsoft_excel/types' -import { getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' +import { getItemBasePath, getSpreadsheetWebUrl } from '@/tools/microsoft_excel/utils' import type { ToolConfig } from '@/tools/types' export const writeTool: ToolConfig = { @@ -31,6 +31,13 @@ export const writeTool: ToolConfig { const data = await response.json() - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId - // Fetch the browser-accessible web URL const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) - - const metadata = { - spreadsheetId, - properties: {}, - spreadsheetUrl: webUrl, - } + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) - const result = { + return { success: true, output: { updatedRange: data.updatedRange, @@ -161,13 +162,11 @@ export const writeTool: ToolConfig { const data = await response.json() - const urlParts = response.url.split('/drive/items/') - const spreadsheetId = urlParts[1]?.split('/')[0] || '' + const spreadsheetId = params?.spreadsheetId?.trim() || '' + const driveId = params?.driveId const accessToken = params?.accessToken if (!accessToken) { throw new Error('Access token is required') } - const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken) + const webUrl = await getSpreadsheetWebUrl(spreadsheetId, accessToken, driveId) return { success: true, From 61622531de25980c2218721b0ad4b3154a3ad2b4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:42:24 -0700 Subject: [PATCH 02/17] fix(microsoft-excel): address PR review comments - Validate siteId/driveId format in drives route to prevent path traversal - Use direct single-drive endpoint for fetchById instead of filtering full list - Fix dependsOn on sheet/spreadsheet selectors so driveId flows into context - Fix NextRequest type in drives route for build compatibility --- .../api/tools/microsoft_excel/drives/route.ts | 42 +++++++++++++++++-- apps/sim/blocks/blocks/microsoft_excel.ts | 14 +++++-- apps/sim/hooks/selectors/registry.ts | 22 +++++----- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index f0d42b6d574..f504d6534d9 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -20,12 +20,12 @@ interface GraphDrive { * Used by the microsoft.excel.drives selector to let users pick * which drive contains their Excel file. */ -export async function POST(request: Request) { +export async function POST(request: NextRequest) { const requestId = generateRequestId() try { const body = await request.json() - const { credential, workflowId, siteId } = body + const { credential, workflowId, siteId, driveId } = body if (!credential) { logger.warn(`[${requestId}] Missing credential in request`) @@ -37,7 +37,12 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) } - const authz = await authorizeCredentialUse(request as Request, { + if (!/^[\w.,;:-]+$/.test(siteId)) { + logger.warn(`[${requestId}] Invalid siteId format`) + return NextResponse.json({ error: 'Invalid site ID format' }, { status: 400 }) + } + + const authz = await authorizeCredentialUse(request, { credentialId: credential, workflowId, }) @@ -58,6 +63,35 @@ export async function POST(request: Request) { ) } + // Single-drive lookup when driveId is provided (used by fetchById) + if (driveId) { + if (!/^[\w-]+$/.test(driveId)) { + return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + } + + const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives/${driveId}?$select=id,name,driveType,webUrl` + const response = await fetch(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + }) + + if (!response.ok) { + const errorData = await response + .json() + .catch(() => ({ error: { message: 'Unknown error' } })) + return NextResponse.json( + { error: errorData.error?.message || 'Failed to fetch drive' }, + { status: response.status } + ) + } + + const data: GraphDrive = await response.json() + return NextResponse.json( + { drive: { id: data.id, name: data.name, driveType: data.driveType } }, + { status: 200 } + ) + } + + // List all drives for the site const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives?$select=id,name,driveType,webUrl` const response = await fetch(url, { diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index a3a41251eb6..879fa879910 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -431,7 +431,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { requiredScopes: [], mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', placeholder: 'Select a spreadsheet', - dependsOn: ['credential'], + dependsOn: { all: ['credential'], any: ['driveSelector'] }, mode: 'basic', }, // Manual Spreadsheet ID (advanced mode) @@ -441,7 +441,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { type: 'short-input', canonicalParamId: 'spreadsheetId', placeholder: 'Enter spreadsheet ID', - dependsOn: ['credential'], + dependsOn: { all: ['credential'], any: ['manualDriveId'] }, mode: 'advanced', }, // Drive ID for SharePoint (advanced mode) @@ -464,7 +464,10 @@ export const MicrosoftExcelV2Block: BlockConfig = { selectorAllowSearch: false, placeholder: 'Select a sheet', required: true, - dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, + dependsOn: { + all: ['credential'], + any: ['spreadsheetId', 'manualSpreadsheetId', 'driveSelector'], + }, mode: 'basic', }, // Manual Sheet Name (advanced mode) @@ -475,7 +478,10 @@ export const MicrosoftExcelV2Block: BlockConfig = { canonicalParamId: 'sheetName', placeholder: 'Name of the sheet/tab (e.g., Sheet1)', required: true, - dependsOn: ['credential'], + dependsOn: { + all: ['credential'], + any: ['manualDriveId'], + }, mode: 'advanced', }, // Cell Range (optional for read/write) diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index e6535d23754..6b053994257 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -1561,18 +1561,20 @@ const registry: Record = { fetchById: async ({ context, detailId }: SelectorQueryArgs) => { if (!detailId || !context.siteId) return null const credentialId = ensureCredential(context, 'microsoft.excel.drives') - const body = JSON.stringify({ - credential: credentialId, - workflowId: context.workflowId, - siteId: context.siteId, - }) - const data = await fetchJson<{ drives: { id: string; name: string }[] }>( + const data = await fetchJson<{ drive: { id: string; name: string } }>( '/api/tools/microsoft_excel/drives', - { method: 'POST', body } + { + method: 'POST', + body: JSON.stringify({ + credential: credentialId, + workflowId: context.workflowId, + siteId: context.siteId, + driveId: detailId, + }), + } ) - const drive = (data.drives || []).find((d) => d.id === detailId) ?? null - if (!drive) return null - return { id: drive.id, label: drive.name } + if (!data.drive) return null + return { id: data.drive.id, label: data.drive.name } }, }, 'microsoft.excel': { From a3c93eeabad96bc0a143b0c4a50f0a4c62f031f8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 14:53:00 -0700 Subject: [PATCH 03/17] fix(microsoft-excel): validate driveId in files route Add regex validation for driveId query param in the Microsoft OAuth files route to prevent path traversal, matching the drives route. --- apps/sim/app/api/auth/oauth/microsoft/files/route.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index e0151e46e93..b192b37e4bf 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -75,6 +75,9 @@ export async function GET(request: NextRequest) { // When driveId is provided (SharePoint), search within that specific drive. // Otherwise, search the user's personal OneDrive. + if (driveId && !/^[\w-]+$/.test(driveId)) { + return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + } const drivePath = driveId ? `drives/${driveId}` : 'me/drive' const response = await fetch( From 780fa9073508870c8e2108c25b4f59ec70d4657e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:02:03 -0700 Subject: [PATCH 04/17] fix(microsoft-excel): unblock OneDrive users and validate driveId in sheets route - Add credential to any[] arrays so OneDrive users (no drive selected) still pass the dependsOn gate while driveSelector remains in the dependency list for context flow to SharePoint users - Add /^[\w-]+$/ validation for driveId in sheets API route --- apps/sim/app/api/tools/microsoft_excel/sheets/route.ts | 4 ++++ apps/sim/blocks/blocks/microsoft_excel.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index 6e95775ccf7..526a7b98972 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -62,6 +62,10 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) + if (driveId && !/^[\w-]+$/.test(driveId)) { + return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + } + const basePath = driveId ? `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` : `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 879fa879910..f5123d64362 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -431,7 +431,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { requiredScopes: [], mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', placeholder: 'Select a spreadsheet', - dependsOn: { all: ['credential'], any: ['driveSelector'] }, + dependsOn: { all: ['credential'], any: ['credential', 'driveSelector'] }, mode: 'basic', }, // Manual Spreadsheet ID (advanced mode) @@ -441,7 +441,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { type: 'short-input', canonicalParamId: 'spreadsheetId', placeholder: 'Enter spreadsheet ID', - dependsOn: { all: ['credential'], any: ['manualDriveId'] }, + dependsOn: { all: ['credential'], any: ['credential', 'manualDriveId'] }, mode: 'advanced', }, // Drive ID for SharePoint (advanced mode) @@ -480,7 +480,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { required: true, dependsOn: { all: ['credential'], - any: ['manualDriveId'], + any: ['credential', 'manualDriveId'], }, mode: 'advanced', }, From f18af3ca7fc4e936a1aadf18d50a3c08d671503a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:15:18 -0700 Subject: [PATCH 05/17] fix(microsoft-excel): validate driveId in getItemBasePath utility Add regex validation for driveId at the shared utility level to prevent path traversal through the tool execution path, which bypasses the API route validators. --- apps/sim/tools/microsoft_excel/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index 600ecbf0354..65745f840d9 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -10,6 +10,9 @@ const logger = createLogger('MicrosoftExcelUtils') */ export function getItemBasePath(spreadsheetId: string, driveId?: string): string { if (driveId) { + if (!/^[\w-]+$/.test(driveId)) { + throw new Error('Invalid drive ID format') + } return `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` } return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` From 65308e4e3979f58873a5f6520dedd5c2f0c3701f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:21:35 -0700 Subject: [PATCH 06/17] fix(microsoft-excel): use centralized input validation Replace inline regex validation with platform validators from @/lib/core/security/input-validation: - validateSharePointSiteId for siteId in drives route - validateAlphanumericId for driveId in drives, sheets, files routes and getItemBasePath utility --- .../app/api/auth/oauth/microsoft/files/route.ts | 8 ++++++-- .../app/api/tools/microsoft_excel/drives/route.ts | 14 ++++++++++---- .../app/api/tools/microsoft_excel/sheets/route.ts | 8 ++++++-- apps/sim/blocks/blocks/microsoft_excel.ts | 2 +- apps/sim/tools/microsoft_excel/utils.ts | 6 ++++-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index b192b37e4bf..43241314df7 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -75,8 +76,11 @@ export async function GET(request: NextRequest) { // When driveId is provided (SharePoint), search within that specific drive. // Otherwise, search the user's personal OneDrive. - if (driveId && !/^[\w-]+$/.test(driveId)) { - return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + if (driveId) { + const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + if (!driveIdValidation.isValid) { + return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) + } } const drivePath = driveId ? `drives/${driveId}` : 'me/drive' diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index f504d6534d9..d554a6d32d5 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -1,6 +1,10 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { + validateAlphanumericId, + validateSharePointSiteId, +} from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -37,9 +41,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Site ID is required' }, { status: 400 }) } - if (!/^[\w.,;:-]+$/.test(siteId)) { + const siteIdValidation = validateSharePointSiteId(siteId, 'siteId') + if (!siteIdValidation.isValid) { logger.warn(`[${requestId}] Invalid siteId format`) - return NextResponse.json({ error: 'Invalid site ID format' }, { status: 400 }) + return NextResponse.json({ error: siteIdValidation.error }, { status: 400 }) } const authz = await authorizeCredentialUse(request, { @@ -65,8 +70,9 @@ export async function POST(request: NextRequest) { // Single-drive lookup when driveId is provided (used by fetchById) if (driveId) { - if (!/^[\w-]+$/.test(driveId)) { - return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + if (!driveIdValidation.isValid) { + return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/drives/${driveId}?$select=id,name,driveType,webUrl` diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index 526a7b98972..a795c762f39 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -62,8 +63,11 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) - if (driveId && !/^[\w-]+$/.test(driveId)) { - return NextResponse.json({ error: 'Invalid drive ID format' }, { status: 400 }) + if (driveId) { + const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + if (!driveIdValidation.isValid) { + return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) + } } const basePath = driveId diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index f5123d64362..42f7d0f607b 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -407,7 +407,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { dependsOn: ['credential'], mode: 'basic', }, - // SharePoint Drive Selector (basic mode, depends on site) + // SharePoint Drive Selector (basic mode, only visible when a site is selected) { id: 'driveSelector', title: 'Document Library', diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index 65745f840d9..697f6025381 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import type { ExcelCellValue } from '@/tools/microsoft_excel/types' +import { validateAlphanumericId } from '@/lib/core/security/input-validation' const logger = createLogger('MicrosoftExcelUtils') @@ -10,8 +11,9 @@ const logger = createLogger('MicrosoftExcelUtils') */ export function getItemBasePath(spreadsheetId: string, driveId?: string): string { if (driveId) { - if (!/^[\w-]+$/.test(driveId)) { - throw new Error('Invalid drive ID format') + const validation = validateAlphanumericId(driveId, 'driveId') + if (!validation.isValid) { + throw new Error(validation.error) } return `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` } From 28845871f1d8bf0177f93167880337c5da1994ae Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:24:12 -0700 Subject: [PATCH 07/17] lint --- apps/sim/tools/microsoft_excel/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index 697f6025381..f9978d4cda2 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' -import type { ExcelCellValue } from '@/tools/microsoft_excel/types' import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import type { ExcelCellValue } from '@/tools/microsoft_excel/types' const logger = createLogger('MicrosoftExcelUtils') From 649c3e645a1a796482f5f6ce1122a48d5d6f1784 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 15:41:53 -0700 Subject: [PATCH 08/17] improvement(microsoft-excel): add File Source dropdown to control SharePoint visibility Replace always-visible optional SharePoint fields with a File Source dropdown (OneDrive/SharePoint) that conditionally shows site and drive selectors. OneDrive users see zero extra fields (default). SharePoint users switch the dropdown and get the full cascade. --- apps/sim/blocks/blocks/microsoft_excel.ts | 26 +++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 42f7d0f607b..8cf80fe0d34 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -394,20 +394,34 @@ export const MicrosoftExcelV2Block: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, - // SharePoint Site Selector (basic mode, optional) + // File Source selector (basic mode) + { + id: 'fileSource', + title: 'File Source', + type: 'dropdown', + options: [ + { label: 'OneDrive', id: 'onedrive' }, + { label: 'SharePoint', id: 'sharepoint' }, + ], + value: () => 'onedrive', + mode: 'basic', + }, + // SharePoint Site Selector (basic mode, only when SharePoint is selected) { id: 'siteSelector', - title: 'SharePoint Site (Optional)', + title: 'SharePoint Site', type: 'file-selector', canonicalParamId: 'siteId', serviceId: 'sharepoint', selectorKey: 'sharepoint.sites', requiredScopes: [], - placeholder: 'Select a SharePoint site (leave empty for OneDrive)', + placeholder: 'Select a SharePoint site', dependsOn: ['credential'], + condition: { field: 'fileSource', value: 'sharepoint' }, + required: { field: 'fileSource', value: 'sharepoint' }, mode: 'basic', }, - // SharePoint Drive Selector (basic mode, only visible when a site is selected) + // SharePoint Drive Selector (basic mode, only when SharePoint is selected) { id: 'driveSelector', title: 'Document Library', @@ -418,6 +432,8 @@ export const MicrosoftExcelV2Block: BlockConfig = { selectorAllowSearch: false, placeholder: 'Select a document library', dependsOn: ['credential', 'siteSelector'], + condition: { field: 'fileSource', value: 'sharepoint' }, + required: { field: 'fileSource', value: 'sharepoint' }, mode: 'basic', }, // Spreadsheet Selector (basic mode) @@ -580,6 +596,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, cellRange, driveId, siteId: _siteId, + fileSource: _fileSource, ...rest } = params @@ -610,6 +627,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, }, inputs: { operation: { type: 'string', description: 'Operation to perform' }, + fileSource: { type: 'string', description: 'File source (onedrive or sharepoint)' }, oauthCredential: { type: 'string', description: 'Microsoft Excel access token' }, siteId: { type: 'string', description: 'SharePoint site ID (used for drive/file browsing)' }, driveId: { type: 'string', description: 'Drive ID for SharePoint document libraries' }, From 8b1c88cf0cf4a606714e9b505a75a43d3e03f209 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 17:43:23 -0700 Subject: [PATCH 09/17] fix(microsoft-excel): fix canonical param test failures Make fileSource dropdown mode:'both' so it appears in basic and advanced modes. Add condition to manualDriveId to match driveSelector's condition, satisfying the canonical pair consistency test. --- apps/sim/blocks/blocks/microsoft_excel.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 8cf80fe0d34..628dfb37d65 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -394,7 +394,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { placeholder: 'Enter credential ID', required: true, }, - // File Source selector (basic mode) + // File Source selector (both modes) { id: 'fileSource', title: 'File Source', @@ -404,7 +404,6 @@ export const MicrosoftExcelV2Block: BlockConfig = { { label: 'SharePoint', id: 'sharepoint' }, ], value: () => 'onedrive', - mode: 'basic', }, // SharePoint Site Selector (basic mode, only when SharePoint is selected) { @@ -460,13 +459,14 @@ export const MicrosoftExcelV2Block: BlockConfig = { dependsOn: { all: ['credential'], any: ['credential', 'manualDriveId'] }, mode: 'advanced', }, - // Drive ID for SharePoint (advanced mode) + // Drive ID for SharePoint (advanced mode, only when SharePoint is selected) { id: 'manualDriveId', - title: 'Drive ID (SharePoint)', + title: 'Drive ID', type: 'short-input', canonicalParamId: 'driveId', - placeholder: 'Leave empty for OneDrive, or enter drive ID for SharePoint', + placeholder: 'Enter the SharePoint drive ID', + condition: { field: 'fileSource', value: 'sharepoint' }, mode: 'advanced', }, // Sheet Name Selector (basic mode) From 326114d1ea9931853319853933dffbbee58bd37b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 18:07:43 -0700 Subject: [PATCH 10/17] fix(microsoft-excel): address PR review feedback for SharePoint drive support - Clear stale driveId/siteId/spreadsheetId when fileSource changes by adding fileSource to dependsOn arrays for siteSelector, driveSelector, and spreadsheetId selectors - Reorder manualDriveId before manualSpreadsheetId in advanced mode for logical top-down flow - Validate spreadsheetId with validateMicrosoftGraphId in getItemBasePath() and sheets route to close injection vector (uses permissive validator that accepts ! chars in OneDrive item IDs) Co-Authored-By: Claude Opus 4.6 --- .../api/tools/microsoft_excel/sheets/route.ts | 10 ++++++- apps/sim/blocks/blocks/microsoft_excel.ts | 26 +++++++++---------- apps/sim/tools/microsoft_excel/utils.ts | 16 +++++++++--- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index a795c762f39..f17830d1263 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -1,7 +1,10 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { + validateAlphanumericId, + validateMicrosoftGraphId, +} from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -63,6 +66,11 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) + const spreadsheetValidation = validateMicrosoftGraphId(spreadsheetId, 'spreadsheetId') + if (!spreadsheetValidation.isValid) { + return NextResponse.json({ error: spreadsheetValidation.error }, { status: 400 }) + } + if (driveId) { const driveIdValidation = validateAlphanumericId(driveId, 'driveId') if (!driveIdValidation.isValid) { diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 628dfb37d65..fcf373651a8 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -415,7 +415,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { selectorKey: 'sharepoint.sites', requiredScopes: [], placeholder: 'Select a SharePoint site', - dependsOn: ['credential'], + dependsOn: ['credential', 'fileSource'], condition: { field: 'fileSource', value: 'sharepoint' }, required: { field: 'fileSource', value: 'sharepoint' }, mode: 'basic', @@ -430,7 +430,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { selectorKey: 'microsoft.excel.drives', selectorAllowSearch: false, placeholder: 'Select a document library', - dependsOn: ['credential', 'siteSelector'], + dependsOn: ['credential', 'siteSelector', 'fileSource'], condition: { field: 'fileSource', value: 'sharepoint' }, required: { field: 'fileSource', value: 'sharepoint' }, mode: 'basic', @@ -446,19 +446,9 @@ export const MicrosoftExcelV2Block: BlockConfig = { requiredScopes: [], mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', placeholder: 'Select a spreadsheet', - dependsOn: { all: ['credential'], any: ['credential', 'driveSelector'] }, + dependsOn: { all: ['credential', 'fileSource'], any: ['credential', 'driveSelector'] }, mode: 'basic', }, - // Manual Spreadsheet ID (advanced mode) - { - id: 'manualSpreadsheetId', - title: 'Spreadsheet ID', - type: 'short-input', - canonicalParamId: 'spreadsheetId', - placeholder: 'Enter spreadsheet ID', - dependsOn: { all: ['credential'], any: ['credential', 'manualDriveId'] }, - mode: 'advanced', - }, // Drive ID for SharePoint (advanced mode, only when SharePoint is selected) { id: 'manualDriveId', @@ -469,6 +459,16 @@ export const MicrosoftExcelV2Block: BlockConfig = { condition: { field: 'fileSource', value: 'sharepoint' }, mode: 'advanced', }, + // Manual Spreadsheet ID (advanced mode) + { + id: 'manualSpreadsheetId', + title: 'Spreadsheet ID', + type: 'short-input', + canonicalParamId: 'spreadsheetId', + placeholder: 'Enter spreadsheet ID', + dependsOn: { all: ['credential'], any: ['credential', 'manualDriveId'] }, + mode: 'advanced', + }, // Sheet Name Selector (basic mode) { id: 'sheetName', diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index f9978d4cda2..a7fe3ad1fc1 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -1,5 +1,8 @@ import { createLogger } from '@sim/logger' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { + validateAlphanumericId, + validateMicrosoftGraphId, +} from '@/lib/core/security/input-validation' import type { ExcelCellValue } from '@/tools/microsoft_excel/types' const logger = createLogger('MicrosoftExcelUtils') @@ -10,10 +13,15 @@ const logger = createLogger('MicrosoftExcelUtils') * When driveId is omitted, uses /me/drive/items/{itemId} (personal OneDrive). */ export function getItemBasePath(spreadsheetId: string, driveId?: string): string { + const spreadsheetValidation = validateMicrosoftGraphId(spreadsheetId, 'spreadsheetId') + if (!spreadsheetValidation.isValid) { + throw new Error(spreadsheetValidation.error) + } + if (driveId) { - const validation = validateAlphanumericId(driveId, 'driveId') - if (!validation.isValid) { - throw new Error(validation.error) + const driveValidation = validateAlphanumericId(driveId, 'driveId') + if (!driveValidation.isValid) { + throw new Error(driveValidation.error) } return `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` } From 3be18cab8655ff985730b35d09a531a6aa0f8c82 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 19:25:02 -0700 Subject: [PATCH 11/17] fix(microsoft-excel): use validateMicrosoftGraphId for driveId validation SharePoint drive IDs use the format b! which contains ! characters rejected by validateAlphanumericId. Switch all driveId validation to validateMicrosoftGraphId which blocks path traversal and control characters while accepting valid Microsoft Graph identifiers. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/auth/oauth/microsoft/files/route.ts | 4 ++-- apps/sim/app/api/tools/microsoft_excel/drives/route.ts | 4 ++-- apps/sim/app/api/tools/microsoft_excel/sheets/route.ts | 7 ++----- apps/sim/tools/microsoft_excel/utils.ts | 7 ++----- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index 43241314df7..c4603ea9ed8 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateAlphanumericId } from '@/lib/core/security/input-validation' +import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -77,7 +77,7 @@ export async function GET(request: NextRequest) { // When driveId is provided (SharePoint), search within that specific drive. // Otherwise, search the user's personal OneDrive. if (driveId) { - const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index d554a6d32d5..55dbe63b7b4 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { - validateAlphanumericId, + validateMicrosoftGraphId, validateSharePointSiteId, } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' @@ -70,7 +70,7 @@ export async function POST(request: NextRequest) { // Single-drive lookup when driveId is provided (used by fetchById) if (driveId) { - const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index f17830d1263..7d81d820455 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -1,10 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { - validateAlphanumericId, - validateMicrosoftGraphId, -} from '@/lib/core/security/input-validation' +import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -72,7 +69,7 @@ export async function GET(request: NextRequest) { } if (driveId) { - const driveIdValidation = validateAlphanumericId(driveId, 'driveId') + const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index a7fe3ad1fc1..c4b07c07bf1 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -1,8 +1,5 @@ import { createLogger } from '@sim/logger' -import { - validateAlphanumericId, - validateMicrosoftGraphId, -} from '@/lib/core/security/input-validation' +import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' import type { ExcelCellValue } from '@/tools/microsoft_excel/types' const logger = createLogger('MicrosoftExcelUtils') @@ -19,7 +16,7 @@ export function getItemBasePath(spreadsheetId: string, driveId?: string): string } if (driveId) { - const driveValidation = validateAlphanumericId(driveId, 'driveId') + const driveValidation = validateMicrosoftGraphId(driveId, 'driveId') if (!driveValidation.isValid) { throw new Error(driveValidation.error) } From 12231db9343f420e9c85ba8e66458b76de683e4b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 19:53:36 -0700 Subject: [PATCH 12/17] fix(microsoft-excel): use validatePathSegment with strict pattern for driveId/spreadsheetId Replace validateMicrosoftGraphId with validatePathSegment using a custom pattern ^[a-zA-Z0-9!_-]+$ for all URL-interpolated IDs. validatePathSegment blocks /, \, path traversal, and null bytes before checking the pattern, preventing URL-modifying characters like ?, #, & from altering the Graph API endpoint. The pattern allows ! for SharePoint b! drive IDs. Co-Authored-By: Claude Opus 4.6 --- .../app/api/auth/oauth/microsoft/files/route.ts | 7 +++++-- .../app/api/tools/microsoft_excel/drives/route.ts | 7 +++++-- .../app/api/tools/microsoft_excel/sheets/route.ts | 14 +++++++++++--- apps/sim/tools/microsoft_excel/utils.ts | 15 ++++++++++++--- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index c4603ea9ed8..a6e6add80f3 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { validatePathSegment } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { getCredential, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -77,7 +77,10 @@ export async function GET(request: NextRequest) { // When driveId is provided (SharePoint), search within that specific drive. // Otherwise, search the user's personal OneDrive. if (driveId) { - const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') + const driveIdValidation = validatePathSegment(driveId, { + paramName: 'driveId', + customPattern: /^[a-zA-Z0-9!_-]+$/, + }) if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index 55dbe63b7b4..a9e562440e2 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { - validateMicrosoftGraphId, + validatePathSegment, validateSharePointSiteId, } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' @@ -70,7 +70,10 @@ export async function POST(request: NextRequest) { // Single-drive lookup when driveId is provided (used by fetchById) if (driveId) { - const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') + const driveIdValidation = validatePathSegment(driveId, { + paramName: 'driveId', + customPattern: /^[a-zA-Z0-9!_-]+$/, + }) if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index 7d81d820455..51e941fa19c 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { validatePathSegment } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' @@ -63,13 +63,21 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) - const spreadsheetValidation = validateMicrosoftGraphId(spreadsheetId, 'spreadsheetId') + const graphIdPattern = /^[a-zA-Z0-9!_-]+$/ + + const spreadsheetValidation = validatePathSegment(spreadsheetId, { + paramName: 'spreadsheetId', + customPattern: graphIdPattern, + }) if (!spreadsheetValidation.isValid) { return NextResponse.json({ error: spreadsheetValidation.error }, { status: 400 }) } if (driveId) { - const driveIdValidation = validateMicrosoftGraphId(driveId, 'driveId') + const driveIdValidation = validatePathSegment(driveId, { + paramName: 'driveId', + customPattern: graphIdPattern, + }) if (!driveIdValidation.isValid) { return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) } diff --git a/apps/sim/tools/microsoft_excel/utils.ts b/apps/sim/tools/microsoft_excel/utils.ts index c4b07c07bf1..ebb99034fca 100644 --- a/apps/sim/tools/microsoft_excel/utils.ts +++ b/apps/sim/tools/microsoft_excel/utils.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation' +import { validatePathSegment } from '@/lib/core/security/input-validation' import type { ExcelCellValue } from '@/tools/microsoft_excel/types' const logger = createLogger('MicrosoftExcelUtils') @@ -9,14 +9,23 @@ const logger = createLogger('MicrosoftExcelUtils') * When driveId is provided, uses /drives/{driveId}/items/{itemId} (SharePoint/shared drives). * When driveId is omitted, uses /me/drive/items/{itemId} (personal OneDrive). */ +/** Pattern for Microsoft Graph item/drive IDs: alphanumeric, hyphens, underscores, and ! (for SharePoint b! format) */ +const GRAPH_ID_PATTERN = /^[a-zA-Z0-9!_-]+$/ + export function getItemBasePath(spreadsheetId: string, driveId?: string): string { - const spreadsheetValidation = validateMicrosoftGraphId(spreadsheetId, 'spreadsheetId') + const spreadsheetValidation = validatePathSegment(spreadsheetId, { + paramName: 'spreadsheetId', + customPattern: GRAPH_ID_PATTERN, + }) if (!spreadsheetValidation.isValid) { throw new Error(spreadsheetValidation.error) } if (driveId) { - const driveValidation = validateMicrosoftGraphId(driveId, 'driveId') + const driveValidation = validatePathSegment(driveId, { + paramName: 'driveId', + customPattern: GRAPH_ID_PATTERN, + }) if (!driveValidation.isValid) { throw new Error(driveValidation.error) } From 8148260f92768d353041477122526b22f538b664 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 20:21:45 -0700 Subject: [PATCH 13/17] lint --- apps/sim/app/api/tools/microsoft_excel/drives/route.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts index a9e562440e2..d9c9aa845bd 100644 --- a/apps/sim/app/api/tools/microsoft_excel/drives/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/drives/route.ts @@ -1,10 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { - validatePathSegment, - validateSharePointSiteId, -} from '@/lib/core/security/input-validation' +import { validatePathSegment, validateSharePointSiteId } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' From 16ad6ce9171a713e5e0529b4c59197dd4b09fffd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 20:33:15 -0700 Subject: [PATCH 14/17] fix(microsoft-excel): reorder driveId before spreadsheetId in v1 block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move driveId subBlock before manualSpreadsheetId in the legacy v1 block to match the logical top-down flow (Drive ID → Spreadsheet ID), consistent with the v2 block ordering. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/microsoft_excel.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index fcf373651a8..bbcef6df75a 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -68,6 +68,13 @@ export const MicrosoftExcelBlock: BlockConfig = { dependsOn: ['credential'], mode: 'basic', }, + { + id: 'driveId', + title: 'Drive ID (SharePoint)', + type: 'short-input', + placeholder: 'Leave empty for OneDrive, or enter drive ID for SharePoint', + mode: 'advanced', + }, { id: 'manualSpreadsheetId', title: 'Spreadsheet ID', @@ -77,13 +84,6 @@ export const MicrosoftExcelBlock: BlockConfig = { dependsOn: ['credential'], mode: 'advanced', }, - { - id: 'driveId', - title: 'Drive ID (SharePoint)', - type: 'short-input', - placeholder: 'Leave empty for OneDrive, or enter drive ID for SharePoint', - mode: 'advanced', - }, { id: 'range', title: 'Range', From d1b87783b40837e7b4e9daa64f35ab24187e7a69 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 20:46:57 -0700 Subject: [PATCH 15/17] fix(microsoft-excel): clear manualDriveId when fileSource changes Add dependsOn: ['fileSource'] to manualDriveId so its value is cleared when switching from SharePoint back to OneDrive. Without this, the stale driveId would still be serialized and forwarded to getItemBasePath, routing through the SharePoint drive path instead of me/drive. Co-Authored-By: Claude Opus 4.6 --- apps/sim/blocks/blocks/microsoft_excel.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index bbcef6df75a..8106d234251 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -457,6 +457,7 @@ export const MicrosoftExcelV2Block: BlockConfig = { canonicalParamId: 'driveId', placeholder: 'Enter the SharePoint drive ID', condition: { field: 'fileSource', value: 'sharepoint' }, + dependsOn: ['fileSource'], mode: 'advanced', }, // Manual Spreadsheet ID (advanced mode) From def6e90e093f883fe04f3de0bcc07cd46a26216d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 21:02:41 -0700 Subject: [PATCH 16/17] refactor(microsoft-excel): use getItemBasePath in sheets route to remove duplication Replace inline URL construction and validation logic with the shared getItemBasePath utility, eliminating duplicated GRAPH_ID_PATTERN regex and conditional URL building. Co-Authored-By: Claude Opus 4.6 --- .../api/tools/microsoft_excel/sheets/route.ts | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index 51e941fa19c..ad6660cded7 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' -import { validatePathSegment } from '@/lib/core/security/input-validation' import { generateRequestId } from '@/lib/core/utils/request' +import { getItemBasePath } from '@/tools/microsoft_excel/utils' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' export const dynamic = 'force-dynamic' @@ -63,30 +63,16 @@ export async function GET(request: NextRequest) { `[${requestId}] Fetching worksheets from Microsoft Graph API for workbook ${spreadsheetId}` ) - const graphIdPattern = /^[a-zA-Z0-9!_-]+$/ - - const spreadsheetValidation = validatePathSegment(spreadsheetId, { - paramName: 'spreadsheetId', - customPattern: graphIdPattern, - }) - if (!spreadsheetValidation.isValid) { - return NextResponse.json({ error: spreadsheetValidation.error }, { status: 400 }) - } - - if (driveId) { - const driveIdValidation = validatePathSegment(driveId, { - paramName: 'driveId', - customPattern: graphIdPattern, - }) - if (!driveIdValidation.isValid) { - return NextResponse.json({ error: driveIdValidation.error }, { status: 400 }) - } + let basePath: string + try { + basePath = getItemBasePath(spreadsheetId, driveId) + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Invalid parameters' }, + { status: 400 } + ) } - const basePath = driveId - ? `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${spreadsheetId}` - : `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}` - const worksheetsResponse = await fetch(`${basePath}/workbook/worksheets`, { method: 'GET', headers: { From 5334c2bc90bb52591e8ecf576d5b2a30b7632690 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 14 Apr 2026 21:05:41 -0700 Subject: [PATCH 17/17] lint --- apps/sim/app/api/tools/microsoft_excel/sheets/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts index ad6660cded7..367e04fc413 100644 --- a/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts +++ b/apps/sim/app/api/tools/microsoft_excel/sheets/route.ts @@ -2,8 +2,8 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { generateRequestId } from '@/lib/core/utils/request' -import { getItemBasePath } from '@/tools/microsoft_excel/utils' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' +import { getItemBasePath } from '@/tools/microsoft_excel/utils' export const dynamic = 'force-dynamic'