diff --git a/apps/sim/app/api/files/parse/route.ts b/apps/sim/app/api/files/parse/route.ts index 50dc55572..89d5867bf 100644 --- a/apps/sim/app/api/files/parse/route.ts +++ b/apps/sim/app/api/files/parse/route.ts @@ -7,6 +7,7 @@ import binaryExtensionsList from 'binary-extensions' import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { isSupportedFileType, parseFile } from '@/lib/file-parsers' import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads' import { uploadExecutionFile } from '@/lib/uploads/contexts/execution' @@ -367,7 +368,7 @@ async function handleExternalUrl( throw new Error(`File too large: ${buffer.length} bytes (max: ${MAX_DOWNLOAD_SIZE_BYTES})`) } - logger.info(`Downloaded file from URL: ${url}, size: ${buffer.length} bytes`) + logger.info(`Downloaded file from URL: ${sanitizeUrlForLog(url)}, size: ${buffer.length} bytes`) let userFile: UserFile | undefined const mimeType = response.headers.get('content-type') || getMimeTypeFromExtension(extension) @@ -420,7 +421,7 @@ async function handleExternalUrl( return parseResult } catch (error) { - logger.error(`Error handling external URL ${url}:`, error) + logger.error(`Error handling external URL ${sanitizeUrlForLog(url)}:`, error) return { success: false, error: `Error fetching URL: ${(error as Error).message}`, diff --git a/apps/sim/app/api/tools/discord/send-message/route.ts b/apps/sim/app/api/tools/discord/send-message/route.ts index c597ae467..f5bf7d27f 100644 --- a/apps/sim/app/api/tools/discord/send-message/route.ts +++ b/apps/sim/app/api/tools/discord/send-message/route.ts @@ -102,6 +102,12 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Processing ${validatedData.files.length} file(s)`) const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + const filesOutput: Array<{ + name: string + mimeType: string + data: string + size: number + }> = [] if (userFiles.length === 0) { logger.warn(`[${requestId}] No valid files to upload, falling back to text-only`) @@ -138,6 +144,12 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`) const buffer = await downloadFileFromStorage(userFile, requestId, logger) + filesOutput.push({ + name: userFile.name, + mimeType: userFile.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }) const blob = new Blob([new Uint8Array(buffer)], { type: userFile.type }) formData.append(`files[${i}]`, blob, userFile.name) @@ -174,6 +186,7 @@ export async function POST(request: NextRequest) { message: data.content, data: data, fileCount: userFiles.length, + files: filesOutput, }, }) } catch (error) { diff --git a/apps/sim/app/api/tools/image/route.ts b/apps/sim/app/api/tools/image/route.ts index 633e61068..96dc58cad 100644 --- a/apps/sim/app/api/tools/image/route.ts +++ b/apps/sim/app/api/tools/image/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateImageUrl } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { generateRequestId } from '@/lib/core/utils/request' const logger = createLogger('ImageProxyAPI') @@ -29,13 +30,13 @@ export async function GET(request: NextRequest) { const urlValidation = validateImageUrl(imageUrl) if (!urlValidation.isValid) { logger.warn(`[${requestId}] Blocked image proxy request`, { - url: imageUrl.substring(0, 100), + url: sanitizeUrlForLog(imageUrl), error: urlValidation.error, }) return new NextResponse(urlValidation.error || 'Invalid image URL', { status: 403 }) } - logger.info(`[${requestId}] Proxying image request for: ${imageUrl}`) + logger.info(`[${requestId}] Proxying image request for: ${sanitizeUrlForLog(imageUrl)}`) try { const imageResponse = await fetch(imageUrl, { diff --git a/apps/sim/app/api/tools/jira/add-attachment/route.ts b/apps/sim/app/api/tools/jira/add-attachment/route.ts new file mode 100644 index 000000000..52b36b24a --- /dev/null +++ b/apps/sim/app/api/tools/jira/add-attachment/route.ts @@ -0,0 +1,121 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkInternalAuth } from '@/lib/auth/hybrid' +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' +import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { getJiraCloudId } from '@/tools/jira/utils' + +const logger = createLogger('JiraAddAttachmentAPI') + +export const dynamic = 'force-dynamic' + +const JiraAddAttachmentSchema = z.object({ + accessToken: z.string().min(1, 'Access token is required'), + domain: z.string().min(1, 'Domain is required'), + issueKey: z.string().min(1, 'Issue key is required'), + files: RawFileInputArraySchema, + cloudId: z.string().optional().nullable(), +}) + +export async function POST(request: NextRequest) { + const requestId = `jira-attach-${Date.now()}` + + try { + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + return NextResponse.json( + { success: false, error: authResult.error || 'Unauthorized' }, + { status: 401 } + ) + } + + const body = await request.json() + const validatedData = JiraAddAttachmentSchema.parse(body) + + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) + if (userFiles.length === 0) { + return NextResponse.json( + { success: false, error: 'No valid files provided for upload' }, + { status: 400 } + ) + } + + const cloudId = + validatedData.cloudId || + (await getJiraCloudId(validatedData.domain, validatedData.accessToken)) + + const formData = new FormData() + const filesOutput: Array<{ name: string; mimeType: string; data: string; size: number }> = [] + + for (const file of userFiles) { + const buffer = await downloadFileFromStorage(file, requestId, logger) + filesOutput.push({ + name: file.name, + mimeType: file.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }) + const blob = new Blob([new Uint8Array(buffer)], { + type: file.type || 'application/octet-stream', + }) + formData.append('file', blob, file.name) + } + + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${validatedData.issueKey}/attachments` + + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${validatedData.accessToken}`, + 'X-Atlassian-Token': 'no-check', + }, + body: formData, + }) + + if (!response.ok) { + const errorText = await response.text() + logger.error(`[${requestId}] Jira attachment upload failed`, { + status: response.status, + statusText: response.statusText, + error: errorText, + }) + return NextResponse.json( + { + success: false, + error: `Failed to upload attachments: ${response.statusText}`, + }, + { status: response.status } + ) + } + + const attachments = await response.json() + const attachmentIds = Array.isArray(attachments) + ? attachments.map((attachment) => attachment.id).filter(Boolean) + : [] + + return NextResponse.json({ + success: true, + output: { + ts: new Date().toISOString(), + issueKey: validatedData.issueKey, + attachmentIds, + files: filesOutput, + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json( + { success: false, error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + logger.error(`[${requestId}] Jira attachment upload error`, error) + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/jsm/queues/route.ts b/apps/sim/app/api/tools/jsm/queues/route.ts index 2921008ef..f7dc234f3 100644 --- a/apps/sim/app/api/tools/jsm/queues/route.ts +++ b/apps/sim/app/api/tools/jsm/queues/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -62,7 +63,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/servicedesk/${serviceDeskId}/queue${params.toString() ? `?${params.toString()}` : ''}` - logger.info('Fetching queues from:', url) + logger.info('Fetching queues from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/request/route.ts b/apps/sim/app/api/tools/jsm/request/route.ts index 92e5e9f4c..213786706 100644 --- a/apps/sim/app/api/tools/jsm/request/route.ts +++ b/apps/sim/app/api/tools/jsm/request/route.ts @@ -6,6 +6,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -66,7 +67,7 @@ export async function POST(request: NextRequest) { } const url = `${baseUrl}/request` - logger.info('Creating request at:', url) + logger.info('Creating request at:', sanitizeUrlForLog(url)) const requestBody: Record = { serviceDeskId, @@ -128,7 +129,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/request/${issueIdOrKey}` - logger.info('Fetching request from:', url) + logger.info('Fetching request from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/requests/route.ts b/apps/sim/app/api/tools/jsm/requests/route.ts index f2f0dc0e7..fff27fe82 100644 --- a/apps/sim/app/api/tools/jsm/requests/route.ts +++ b/apps/sim/app/api/tools/jsm/requests/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -68,7 +69,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/request${params.toString() ? `?${params.toString()}` : ''}` - logger.info('Fetching requests from:', url) + logger.info('Fetching requests from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/requesttypes/route.ts b/apps/sim/app/api/tools/jsm/requesttypes/route.ts index 8591f116b..fa7f826ae 100644 --- a/apps/sim/app/api/tools/jsm/requesttypes/route.ts +++ b/apps/sim/app/api/tools/jsm/requesttypes/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -53,7 +54,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/servicedesk/${serviceDeskId}/requesttype${params.toString() ? `?${params.toString()}` : ''}` - logger.info('Fetching request types from:', url) + logger.info('Fetching request types from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/servicedesks/route.ts b/apps/sim/app/api/tools/jsm/servicedesks/route.ts index 607508a61..875280575 100644 --- a/apps/sim/app/api/tools/jsm/servicedesks/route.ts +++ b/apps/sim/app/api/tools/jsm/servicedesks/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -43,7 +44,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/servicedesk${params.toString() ? `?${params.toString()}` : ''}` - logger.info('Fetching service desks from:', url) + logger.info('Fetching service desks from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/sla/route.ts b/apps/sim/app/api/tools/jsm/sla/route.ts index dc414ac83..ea5b88559 100644 --- a/apps/sim/app/api/tools/jsm/sla/route.ts +++ b/apps/sim/app/api/tools/jsm/sla/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -53,7 +54,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/request/${issueIdOrKey}/sla${params.toString() ? `?${params.toString()}` : ''}` - logger.info('Fetching SLA info from:', url) + logger.info('Fetching SLA info from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/jsm/transition/route.ts b/apps/sim/app/api/tools/jsm/transition/route.ts index 45a9e3a5c..5f1065b6f 100644 --- a/apps/sim/app/api/tools/jsm/transition/route.ts +++ b/apps/sim/app/api/tools/jsm/transition/route.ts @@ -6,6 +6,7 @@ import { validateJiraCloudId, validateJiraIssueKey, } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -69,7 +70,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/request/${issueIdOrKey}/transition` - logger.info('Transitioning request at:', url) + logger.info('Transitioning request at:', sanitizeUrlForLog(url)) const body: Record = { id: transitionId, diff --git a/apps/sim/app/api/tools/jsm/transitions/route.ts b/apps/sim/app/api/tools/jsm/transitions/route.ts index 5d5f2e260..c80a27ab8 100644 --- a/apps/sim/app/api/tools/jsm/transitions/route.ts +++ b/apps/sim/app/api/tools/jsm/transitions/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { getJiraCloudId, getJsmApiBaseUrl, getJsmHeaders } from '@/tools/jsm/utils' export const dynamic = 'force-dynamic' @@ -49,7 +50,7 @@ export async function POST(request: NextRequest) { const url = `${baseUrl}/request/${issueIdOrKey}/transition` - logger.info('Fetching transitions from:', url) + logger.info('Fetching transitions from:', sanitizeUrlForLog(url)) const response = await fetch(url, { method: 'GET', diff --git a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts index bcfcb0b40..a3789ca99 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -55,6 +56,12 @@ export async function POST(request: NextRequest) { }) const attachments: any[] = [] + const filesOutput: Array<{ + name: string + mimeType: string + data: string + size: number + }> = [] if (validatedData.files && validatedData.files.length > 0) { const rawFiles = validatedData.files logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to OneDrive`) @@ -66,13 +73,19 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) const buffer = await downloadFileFromStorage(file, requestId, logger) + filesOutput.push({ + name: file.name, + mimeType: file.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }) const uploadUrl = 'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' + encodeURIComponent(file.name) + ':/content' - logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) + logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`) const uploadResponse = await fetch(uploadUrl, { method: 'PUT', @@ -238,6 +251,7 @@ export async function POST(request: NextRequest) { url: responseData.webUrl || '', attachmentCount: attachments.length, }, + files: filesOutput, }, }) } catch (error) { diff --git a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts index 6b940e17c..1137cc9a1 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -53,6 +54,12 @@ export async function POST(request: NextRequest) { }) const attachments: any[] = [] + const filesOutput: Array<{ + name: string + mimeType: string + data: string + size: number + }> = [] if (validatedData.files && validatedData.files.length > 0) { const rawFiles = validatedData.files logger.info(`[${requestId}] Processing ${rawFiles.length} file(s) for upload to Teams`) @@ -64,13 +71,19 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) const buffer = await downloadFileFromStorage(file, requestId, logger) + filesOutput.push({ + name: file.name, + mimeType: file.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }) const uploadUrl = 'https://graph.microsoft.com/v1.0/me/drive/root:/TeamsAttachments/' + encodeURIComponent(file.name) + ':/content' - logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) + logger.info(`[${requestId}] Uploading to Teams: ${sanitizeUrlForLog(uploadUrl)}`) const uploadResponse = await fetch(uploadUrl, { method: 'PUT', @@ -234,6 +247,7 @@ export async function POST(request: NextRequest) { url: responseData.webUrl || '', attachmentCount: attachments.length, }, + files: filesOutput, }, }) } catch (error) { diff --git a/apps/sim/app/api/tools/mistral/parse/route.ts b/apps/sim/app/api/tools/mistral/parse/route.ts index 89ff35b77..642bb15e2 100644 --- a/apps/sim/app/api/tools/mistral/parse/route.ts +++ b/apps/sim/app/api/tools/mistral/parse/route.ts @@ -5,6 +5,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { getBaseUrl } from '@/lib/core/utils/urls' import { StorageService } from '@/lib/uploads' +import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' import { extractStorageKey, inferContextFromKey, @@ -19,7 +20,7 @@ const logger = createLogger('MistralParseAPI') const MistralParseSchema = z.object({ apiKey: z.string().min(1, 'API key is required'), filePath: z.string().min(1, 'File path is required').optional(), - fileData: z.unknown().optional(), + fileData: FileInputSchema.optional(), resultType: z.string().optional(), pages: z.array(z.number()).optional(), includeImageBase64: z.boolean().optional(), diff --git a/apps/sim/app/api/tools/sftp/download/route.ts b/apps/sim/app/api/tools/sftp/download/route.ts index 4914703fc..849e1ee09 100644 --- a/apps/sim/app/api/tools/sftp/download/route.ts +++ b/apps/sim/app/api/tools/sftp/download/route.ts @@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' import { createSftpConnection, getSftp, isPathSafe, sanitizePath } from '@/app/api/tools/sftp/utils' export const dynamic = 'force-dynamic' @@ -111,6 +112,8 @@ export async function POST(request: NextRequest) { const buffer = Buffer.concat(chunks) const fileName = path.basename(remotePath) + const extension = getFileExtension(fileName) + const mimeType = getMimeTypeFromExtension(extension) let content: string if (params.encoding === 'base64') { @@ -124,6 +127,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, fileName, + file: { + name: fileName, + mimeType, + data: buffer.toString('base64'), + size: buffer.length, + }, content, size: buffer.length, encoding: params.encoding, diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index b5826c6ec..b15421d00 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -144,7 +145,7 @@ export async function POST(request: NextRequest) { const uploadUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drives/${effectiveDriveId}/root:${encodedPath}:/content` - logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) + logger.info(`[${requestId}] Uploading to: ${sanitizeUrlForLog(uploadUrl)}`) const uploadResponse = await fetch(uploadUrl, { method: 'PUT', diff --git a/apps/sim/app/api/tools/slack/utils.ts b/apps/sim/app/api/tools/slack/utils.ts index 4a18071bf..a5527d95d 100644 --- a/apps/sim/app/api/tools/slack/utils.ts +++ b/apps/sim/app/api/tools/slack/utils.ts @@ -1,6 +1,7 @@ import type { Logger } from '@sim/logger' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import type { ToolFileData } from '@/tools/types' /** * Sends a message to a Slack channel using chat.postMessage @@ -70,14 +71,21 @@ export async function uploadFilesToSlack( accessToken: string, requestId: string, logger: Logger -): Promise { +): Promise<{ fileIds: string[]; files: ToolFileData[] }> { const userFiles = processFilesToUserFiles(files, requestId, logger) const uploadedFileIds: string[] = [] + const uploadedFiles: ToolFileData[] = [] for (const userFile of userFiles) { logger.info(`[${requestId}] Uploading file: ${userFile.name}`) const buffer = await downloadFileFromStorage(userFile, requestId, logger) + uploadedFiles.push({ + name: userFile.name, + mimeType: userFile.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }) const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', { method: 'POST', @@ -114,7 +122,7 @@ export async function uploadFilesToSlack( uploadedFileIds.push(urlData.file_id) } - return uploadedFileIds + return { fileIds: uploadedFileIds, files: uploadedFiles } } /** @@ -217,7 +225,13 @@ export async function sendSlackMessage( logger: Logger ): Promise<{ success: boolean - output?: { message: any; ts: string; channel: string; fileCount?: number } + output?: { + message: any + ts: string + channel: string + fileCount?: number + files?: ToolFileData[] + } error?: string }> { const { accessToken, text, threadTs, files } = params @@ -249,10 +263,15 @@ export async function sendSlackMessage( // Process files logger.info(`[${requestId}] Processing ${files.length} file(s)`) - const uploadedFileIds = await uploadFilesToSlack(files, accessToken, requestId, logger) + const { fileIds, files: uploadedFiles } = await uploadFilesToSlack( + files, + accessToken, + requestId, + logger + ) // No valid files uploaded - send text-only - if (uploadedFileIds.length === 0) { + if (fileIds.length === 0) { logger.warn(`[${requestId}] No valid files to upload, sending text-only message`) const data = await postSlackMessage(accessToken, channel, text, threadTs) @@ -265,7 +284,7 @@ export async function sendSlackMessage( } // Complete file upload - const completeData = await completeSlackFileUpload(uploadedFileIds, channel, text, accessToken) + const completeData = await completeSlackFileUpload(fileIds, channel, text, accessToken) if (!completeData.ok) { logger.error(`[${requestId}] Failed to complete upload:`, completeData.error) @@ -282,7 +301,8 @@ export async function sendSlackMessage( message: fileMessage, ts: fileMessage.ts, channel, - fileCount: uploadedFileIds.length, + fileCount: fileIds.length, + files: uploadedFiles, }, } } diff --git a/apps/sim/app/api/tools/ssh/download-file/route.ts b/apps/sim/app/api/tools/ssh/download-file/route.ts index e3bffd29d..818d0ed41 100644 --- a/apps/sim/app/api/tools/ssh/download-file/route.ts +++ b/apps/sim/app/api/tools/ssh/download-file/route.ts @@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server' import type { Client, SFTPWrapper } from 'ssh2' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils' import { createSSHConnection, sanitizePath } from '@/app/api/tools/ssh/utils' const logger = createLogger('SSHDownloadFileAPI') @@ -96,6 +97,8 @@ export async function POST(request: NextRequest) { }) const fileName = path.basename(remotePath) + const extension = getFileExtension(fileName) + const mimeType = getMimeTypeFromExtension(extension) // Encode content as base64 for binary safety const base64Content = content.toString('base64') @@ -104,6 +107,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ downloaded: true, + file: { + name: fileName, + mimeType, + data: base64Content, + size: stats.size, + }, content: base64Content, fileName: fileName, remotePath: remotePath, diff --git a/apps/sim/app/api/tools/stagehand/extract/route.ts b/apps/sim/app/api/tools/stagehand/extract/route.ts index b663f575d..db0d2848a 100644 --- a/apps/sim/app/api/tools/stagehand/extract/route.ts +++ b/apps/sim/app/api/tools/stagehand/extract/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/core/config/env' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { ensureZodObject, normalizeUrl } from '@/app/api/tools/stagehand/utils' const logger = createLogger('StagehandExtractAPI') @@ -120,7 +121,7 @@ export async function POST(request: NextRequest) { const page = stagehand.context.pages()[0] - logger.info(`Navigating to ${url}`) + logger.info(`Navigating to ${sanitizeUrlForLog(url)}`) await page.goto(url, { waitUntil: 'networkidle' }) logger.info('Navigation complete') diff --git a/apps/sim/app/api/tools/stt/route.ts b/apps/sim/app/api/tools/stt/route.ts index 8a3ed3ef2..d14db9175 100644 --- a/apps/sim/app/api/tools/stt/route.ts +++ b/apps/sim/app/api/tools/stt/route.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { extractAudioFromVideo, isVideoFile } from '@/lib/audio/extractor' import { checkInternalAuth } from '@/lib/auth/hybrid' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import type { UserFile } from '@/executor/types' import type { TranscriptSegment } from '@/tools/stt/types' @@ -88,7 +89,7 @@ export async function POST(request: NextRequest) { audioFileName = file.name audioMimeType = file.type } else if (body.audioUrl) { - logger.info(`[${requestId}] Downloading from URL: ${body.audioUrl}`) + logger.info(`[${requestId}] Downloading from URL: ${sanitizeUrlForLog(body.audioUrl)}`) const response = await fetch(body.audioUrl) if (!response.ok) { diff --git a/apps/sim/app/api/tools/telegram/send-document/route.ts b/apps/sim/app/api/tools/telegram/send-document/route.ts index 27d3277d4..0ddaac702 100644 --- a/apps/sim/app/api/tools/telegram/send-document/route.ts +++ b/apps/sim/app/api/tools/telegram/send-document/route.ts @@ -94,6 +94,14 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading document: ${userFile.name}`) const buffer = await downloadFileFromStorage(userFile, requestId, logger) + const filesOutput = [ + { + name: userFile.name, + mimeType: userFile.type || 'application/octet-stream', + data: buffer.toString('base64'), + size: buffer.length, + }, + ] logger.info(`[${requestId}] Downloaded file: ${buffer.length} bytes`) @@ -136,6 +144,7 @@ export async function POST(request: NextRequest) { output: { message: 'Document sent successfully', data: data.result, + files: filesOutput, }, }) } catch (error) { diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx index 74397b9bb..9e3b163a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { ArrowDown, Loader2 } from 'lucide-react' import { useRouter } from 'next/navigation' import { Button } from '@/components/emcn' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils' const logger = createLogger('FileCards') @@ -57,7 +58,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) if (file.key.startsWith('url/')) { if (file.url) { window.open(file.url, '_blank') - logger.info(`Opened URL-type file directly: ${file.url}`) + logger.info(`Opened URL-type file directly: ${sanitizeUrlForLog(file.url)}`) return } throw new Error('URL is required for URL-type files') @@ -77,13 +78,13 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps) const serveUrl = file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution` window.open(serveUrl, '_blank') - logger.info(`Opened execution file serve URL: ${serveUrl}`) + logger.info(`Opened execution file serve URL: ${sanitizeUrlForLog(serveUrl)}`) } else { const viewerUrl = resolvedWorkspaceId ? getViewerUrl(file.key, resolvedWorkspaceId) : null if (viewerUrl) { router.push(viewerUrl) - logger.info(`Navigated to viewer URL: ${viewerUrl}`) + logger.info(`Navigated to viewer URL: ${sanitizeUrlForLog(viewerUrl)}`) } else { logger.warn( `Could not construct viewer URL for file: ${file.name}, falling back to serve URL` diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index 1c1d468be..7b66b8369 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -807,7 +807,7 @@ export function Chat() { const newReservedFields: StartInputFormatField[] = missingStartReservedFields.map( (fieldName) => { - const defaultType = fieldName === 'files' ? 'files' : 'string' + const defaultType = fieldName === 'files' ? 'file[]' : 'string' return { id: crypto.randomUUID(), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index 0d2569690..770dd3f17 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -225,7 +225,7 @@ const getOutputTypeForPath = ( const chatModeTypes: Record = { input: 'string', conversationId: 'string', - files: 'files', + files: 'file[]', } return chatModeTypes[outputPath] || 'any' } @@ -1563,16 +1563,11 @@ export const TagDropdown: React.FC = ({ blockTagGroups.sort((a, b) => a.distance - b.distance) finalBlockTagGroups.push(...blockTagGroups) - const contextualTags: string[] = [] - if (loopBlockGroup) { - contextualTags.push(...loopBlockGroup.tags) - } - if (parallelBlockGroup) { - contextualTags.push(...parallelBlockGroup.tags) - } + const groupTags = finalBlockTagGroups.flatMap((group) => group.tags) + const tags = [...groupTags, ...variableTags] return { - tags: [...allBlockTags, ...variableTags, ...contextualTags], + tags, variableInfoMap, blockTagGroups: finalBlockTagGroups, } diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index 0d1108a09..94c27d448 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -578,13 +578,20 @@ export const DiscordBlock: BlockConfig = { if (!params.serverId) throw new Error('Server ID is required') switch (params.operation) { - case 'discord_send_message': + case 'discord_send_message': { + const fileParam = params.attachmentFiles || params.files + const normalizedFiles = fileParam + ? Array.isArray(fileParam) + ? fileParam + : [fileParam] + : undefined return { ...commonParams, channelId: params.channelId, content: params.content, - files: params.attachmentFiles || params.files, + files: normalizedFiles, } + } case 'discord_get_messages': return { ...commonParams, @@ -789,6 +796,7 @@ export const DiscordBlock: BlockConfig = { }, outputs: { message: { type: 'string', description: 'Status message' }, + files: { type: 'file[]', description: 'Files attached to the message' }, data: { type: 'json', description: 'Response data' }, }, } diff --git a/apps/sim/blocks/blocks/elevenlabs.ts b/apps/sim/blocks/blocks/elevenlabs.ts index 9589a9f47..58d79fe67 100644 --- a/apps/sim/blocks/blocks/elevenlabs.ts +++ b/apps/sim/blocks/blocks/elevenlabs.ts @@ -73,5 +73,6 @@ export const ElevenLabsBlock: BlockConfig = { outputs: { audioUrl: { type: 'string', description: 'Generated audio URL' }, + audioFile: { type: 'file', description: 'Generated audio file' }, }, } diff --git a/apps/sim/blocks/blocks/jira.ts b/apps/sim/blocks/blocks/jira.ts index 66d42a43d..c2e64ce1e 100644 --- a/apps/sim/blocks/blocks/jira.ts +++ b/apps/sim/blocks/blocks/jira.ts @@ -34,6 +34,7 @@ export const JiraBlock: BlockConfig = { { label: 'Update Comment', id: 'update_comment' }, { label: 'Delete Comment', id: 'delete_comment' }, { label: 'Get Attachments', id: 'get_attachments' }, + { label: 'Add Attachment', id: 'add_attachment' }, { label: 'Delete Attachment', id: 'delete_attachment' }, { label: 'Add Worklog', id: 'add_worklog' }, { label: 'Get Worklogs', id: 'get_worklogs' }, @@ -137,6 +138,7 @@ export const JiraBlock: BlockConfig = { 'update_comment', 'delete_comment', 'get_attachments', + 'add_attachment', 'add_worklog', 'get_worklogs', 'update_worklog', @@ -168,6 +170,7 @@ export const JiraBlock: BlockConfig = { 'update_comment', 'delete_comment', 'get_attachments', + 'add_attachment', 'add_worklog', 'get_worklogs', 'update_worklog', @@ -407,6 +410,27 @@ Return ONLY the comment text - no explanations.`, condition: { field: 'operation', value: ['update_comment', 'delete_comment'] }, }, // Attachment fields + { + id: 'attachmentFiles', + title: 'Attachments', + type: 'file-upload', + canonicalParamId: 'files', + placeholder: 'Upload files', + condition: { field: 'operation', value: 'add_attachment' }, + mode: 'basic', + multiple: true, + required: true, + }, + { + id: 'files', + title: 'File References', + type: 'short-input', + canonicalParamId: 'files', + placeholder: 'File reference from previous block', + condition: { field: 'operation', value: 'add_attachment' }, + mode: 'advanced', + required: true, + }, { id: 'attachmentId', title: 'Attachment ID', @@ -576,6 +600,7 @@ Return ONLY the comment text - no explanations.`, 'jira_update_comment', 'jira_delete_comment', 'jira_get_attachments', + 'jira_add_attachment', 'jira_delete_attachment', 'jira_add_worklog', 'jira_get_worklogs', @@ -623,6 +648,8 @@ Return ONLY the comment text - no explanations.`, return 'jira_delete_comment' case 'get_attachments': return 'jira_get_attachments' + case 'add_attachment': + return 'jira_add_attachment' case 'delete_attachment': return 'jira_delete_attachment' case 'add_worklog': @@ -838,6 +865,21 @@ Return ONLY the comment text - no explanations.`, issueKey: effectiveIssueKey, } } + case 'add_attachment': { + if (!effectiveIssueKey) { + throw new Error('Issue Key is required to add attachments.') + } + const fileParam = params.attachmentFiles || params.files + if (!fileParam || (Array.isArray(fileParam) && fileParam.length === 0)) { + throw new Error('At least one attachment file is required.') + } + const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam] + return { + ...baseParams, + issueKey: effectiveIssueKey, + files: normalizedFiles, + } + } case 'delete_attachment': { return { ...baseParams, @@ -982,6 +1024,8 @@ Return ONLY the comment text - no explanations.`, commentBody: { type: 'string', description: 'Text content for comment operations' }, commentId: { type: 'string', description: 'Comment ID for update/delete operations' }, // Attachment operation inputs + attachmentFiles: { type: 'json', description: 'Files to attach (UI upload)' }, + files: { type: 'array', description: 'Files to attach (UserFile array)' }, attachmentId: { type: 'string', description: 'Attachment ID for delete operation' }, // Worklog operation inputs timeSpentSeconds: { @@ -1049,9 +1093,11 @@ Return ONLY the comment text - no explanations.`, // jira_get_attachments outputs attachments: { - type: 'file[]', + type: 'json', description: 'Array of attachments with id, filename, size, mimeType, created, author', }, + files: { type: 'file[]', description: 'Uploaded attachment files' }, + attachmentIds: { type: 'json', description: 'Uploaded attachment IDs' }, // jira_delete_attachment, jira_delete_comment, jira_delete_issue, jira_delete_worklog, jira_delete_issue_link outputs attachmentId: { type: 'string', description: 'Deleted attachment ID' }, diff --git a/apps/sim/blocks/blocks/linear.ts b/apps/sim/blocks/blocks/linear.ts index 300c51b70..fefe38bc9 100644 --- a/apps/sim/blocks/blocks/linear.ts +++ b/apps/sim/blocks/blocks/linear.ts @@ -668,17 +668,44 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n generationType: 'timestamp', }, }, + // Attachment file + { + id: 'attachmentFileUpload', + title: 'Attachment', + type: 'file-upload', + canonicalParamId: 'file', + placeholder: 'Upload attachment', + condition: { + field: 'operation', + value: ['linear_create_attachment'], + }, + mode: 'basic', + multiple: false, + }, + { + id: 'file', + title: 'File Reference', + type: 'short-input', + canonicalParamId: 'file', + placeholder: 'File reference from previous block', + condition: { + field: 'operation', + value: ['linear_create_attachment'], + }, + mode: 'advanced', + }, // Attachment URL { id: 'url', title: 'URL', type: 'short-input', placeholder: 'Enter URL', - required: true, + required: false, condition: { field: 'operation', value: ['linear_create_attachment'], }, + mode: 'advanced', }, // Attachment title { @@ -1742,16 +1769,31 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n teamId: effectiveTeamId, } - case 'linear_create_attachment': - if (!params.issueId?.trim() || !params.url?.trim()) { - throw new Error('Issue ID and URL are required.') + case 'linear_create_attachment': { + if (!params.issueId?.trim()) { + throw new Error('Issue ID is required.') + } + if (Array.isArray(params.file)) { + throw new Error('Attachment file must be a single file.') + } + if (Array.isArray(params.attachmentFileUpload)) { + throw new Error('Attachment file must be a single file.') + } + const attachmentFile = params.attachmentFileUpload || params.file + const attachmentUrl = + params.url?.trim() || + (attachmentFile && !Array.isArray(attachmentFile) ? attachmentFile.url : undefined) + if (!attachmentUrl) { + throw new Error('URL or file is required.') } return { ...baseParams, issueId: params.issueId.trim(), - url: params.url.trim(), + url: attachmentUrl, + file: attachmentFile, title: params.attachmentTitle, } + } case 'linear_list_attachments': if (!params.issueId?.trim()) { @@ -2248,6 +2290,8 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n endDate: { type: 'string', description: 'End date' }, targetDate: { type: 'string', description: 'Target date' }, url: { type: 'string', description: 'URL' }, + attachmentFileUpload: { type: 'json', description: 'File to attach (UI upload)' }, + file: { type: 'json', description: 'File to attach (UserFile)' }, attachmentTitle: { type: 'string', description: 'Attachment title' }, attachmentId: { type: 'string', description: 'Attachment identifier' }, relationType: { type: 'string', description: 'Relation type' }, @@ -2341,7 +2385,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n cycles: { type: 'json', description: 'Cycles list' }, // Attachment outputs attachment: { type: 'json', description: 'Attachment data' }, - attachments: { type: 'file[]', description: 'Attachments list' }, + attachments: { type: 'json', description: 'Attachments list' }, // Relation outputs relation: { type: 'json', description: 'Issue relation data' }, relations: { type: 'json', description: 'Issue relations list' }, diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 69dedc8af..04cb2d242 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -346,7 +346,10 @@ export const MicrosoftTeamsBlock: BlockConfig = { // Add files if provided const fileParam = attachmentFiles || files if (fileParam && (operation === 'write_chat' || operation === 'write_channel')) { - baseParams.files = fileParam + const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam] + if (normalizedFiles.length > 0) { + baseParams.files = normalizedFiles + } } // Add messageId if provided @@ -463,6 +466,7 @@ export const MicrosoftTeamsBlock: BlockConfig = { totalAttachments: { type: 'number', description: 'Total number of attachments' }, attachmentTypes: { type: 'json', description: 'Array of attachment content types' }, attachments: { type: 'file[]', description: 'Downloaded message attachments' }, + files: { type: 'file[]', description: 'Files attached to the message' }, updatedContent: { type: 'boolean', description: 'Whether content was successfully updated/sent', diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index 1d3b939d4..b6bd6fb8e 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -803,7 +803,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n outputs: { deals: { type: 'json', description: 'Array of deal objects' }, deal: { type: 'json', description: 'Single deal object' }, - files: { type: 'file[]', description: 'Array of file objects' }, + files: { type: 'json', description: 'Array of file objects' }, messages: { type: 'json', description: 'Array of mail message objects' }, pipelines: { type: 'json', description: 'Array of pipeline objects' }, projects: { type: 'json', description: 'Array of project objects' }, diff --git a/apps/sim/blocks/blocks/sftp.ts b/apps/sim/blocks/blocks/sftp.ts index f459c1a03..3621ee5b4 100644 --- a/apps/sim/blocks/blocks/sftp.ts +++ b/apps/sim/blocks/blocks/sftp.ts @@ -293,6 +293,7 @@ export const SftpBlock: BlockConfig = { outputs: { success: { type: 'boolean', description: 'Whether the operation was successful' }, uploadedFiles: { type: 'json', description: 'Array of uploaded file details' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileName: { type: 'string', description: 'Downloaded file name' }, content: { type: 'string', description: 'Downloaded file content' }, size: { type: 'number', description: 'File size in bytes' }, diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index b6cdbfbda..5fe1dfb6d 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -522,7 +522,7 @@ Return ONLY the JSON object - no explanations, no markdown, no extra text.`, description: 'Array of SharePoint list items with fields', }, uploadedFiles: { - type: 'file[]', + type: 'json', description: 'Array of uploaded file objects with id, name, webUrl, size', }, fileCount: { diff --git a/apps/sim/blocks/blocks/slack.ts b/apps/sim/blocks/blocks/slack.ts index 9bd6292b7..0ce640032 100644 --- a/apps/sim/blocks/blocks/slack.ts +++ b/apps/sim/blocks/blocks/slack.ts @@ -622,7 +622,10 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, } const fileParam = attachmentFiles || files if (fileParam) { - baseParams.files = fileParam + const normalizedFiles = Array.isArray(fileParam) ? fileParam : [fileParam] + if (normalizedFiles.length > 0) { + baseParams.files = normalizedFiles + } } break } @@ -796,6 +799,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, type: 'number', description: 'Number of files uploaded (when files are attached)', }, + files: { type: 'file[]', description: 'Files attached to the message' }, // slack_canvas outputs canvas_id: { type: 'string', description: 'Canvas identifier for created canvases' }, diff --git a/apps/sim/blocks/blocks/ssh.ts b/apps/sim/blocks/blocks/ssh.ts index 924b26c45..32dbffd72 100644 --- a/apps/sim/blocks/blocks/ssh.ts +++ b/apps/sim/blocks/blocks/ssh.ts @@ -507,6 +507,7 @@ export const SSHBlock: BlockConfig = { stderr: { type: 'string', description: 'Command standard error' }, exitCode: { type: 'number', description: 'Command exit code' }, success: { type: 'boolean', description: 'Operation success status' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileContent: { type: 'string', description: 'Downloaded/read file content' }, entries: { type: 'json', description: 'Directory entries' }, exists: { type: 'boolean', description: 'File/directory existence' }, diff --git a/apps/sim/blocks/blocks/telegram.ts b/apps/sim/blocks/blocks/telegram.ts index e45f27cee..65b18677a 100644 --- a/apps/sim/blocks/blocks/telegram.ts +++ b/apps/sim/blocks/blocks/telegram.ts @@ -314,9 +314,14 @@ export const TelegramBlock: BlockConfig = { case 'telegram_send_document': { // Handle file upload const fileParam = params.attachmentFiles || params.files + const normalizedFiles = fileParam + ? Array.isArray(fileParam) + ? fileParam + : [fileParam] + : undefined return { ...commonParams, - files: fileParam, + files: normalizedFiles, caption: params.caption, } } @@ -359,6 +364,7 @@ export const TelegramBlock: BlockConfig = { }, message: { type: 'string', description: 'Success or error message' }, data: { type: 'json', description: 'Response data' }, + files: { type: 'file[]', description: 'Files attached to the message' }, // Specific result fields messageId: { type: 'number', description: 'Sent message ID' }, chatId: { type: 'number', description: 'Chat ID where message was sent' }, diff --git a/apps/sim/lib/core/utils/logging.ts b/apps/sim/lib/core/utils/logging.ts new file mode 100644 index 000000000..5670d6a5d --- /dev/null +++ b/apps/sim/lib/core/utils/logging.ts @@ -0,0 +1,19 @@ +/** + * Sanitize URLs for logging by stripping query/hash and truncating. + */ +export function sanitizeUrlForLog(url: string, maxLength = 120): string { + if (!url) return '' + + const trimmed = url.trim() + try { + const hasProtocol = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed) + const parsed = new URL(trimmed, hasProtocol ? undefined : 'http://localhost') + const origin = parsed.origin === 'null' ? '' : parsed.origin + const sanitized = `${origin}${parsed.pathname}` + const result = sanitized || parsed.pathname || trimmed + return result.length > maxLength ? `${result.slice(0, maxLength)}...` : result + } catch { + const withoutQuery = trimmed.split('?')[0].split('#')[0] + return withoutQuery.length > maxLength ? `${withoutQuery.slice(0, maxLength)}...` : withoutQuery + } +} diff --git a/apps/sim/lib/knowledge/documents/document-processor.ts b/apps/sim/lib/knowledge/documents/document-processor.ts index 632e91fa8..fadd43fa1 100644 --- a/apps/sim/lib/knowledge/documents/document-processor.ts +++ b/apps/sim/lib/knowledge/documents/document-processor.ts @@ -3,6 +3,7 @@ import { PDFDocument } from 'pdf-lib' import { getBYOKKey } from '@/lib/api-key/byok' import { type Chunk, JsonYamlChunker, StructuredDataChunker, TextChunker } from '@/lib/chunkers' import { env } from '@/lib/core/config/env' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import { parseBuffer, parseFile } from '@/lib/file-parsers' import type { FileParseMetadata } from '@/lib/file-parsers/types' import { retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils' @@ -489,7 +490,7 @@ async function parseWithMistralOCR( workspaceId ) - logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${httpsUrl.substring(0, 120)}...`) + logger.info(`Mistral OCR: Using presigned URL for ${filename}: ${sanitizeUrlForLog(httpsUrl)}`) let pageCount = 0 if (mimeType === 'application/pdf' && buffer) { diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index f3cd6b436..ad17cd774 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -11,6 +11,7 @@ import { secureFetchWithPinnedIP, validateUrlWithDNS, } from '@/lib/core/security/input-validation' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import type { DbOrTx } from '@/lib/db/types' import { getProviderIdFromServiceId } from '@/lib/oauth' import { @@ -114,7 +115,7 @@ async function fetchWithDNSPinning( const urlValidation = await validateUrlWithDNS(url, 'contentUrl') if (!urlValidation.isValid) { logger.warn(`[${requestId}] Invalid content URL: ${urlValidation.error}`, { - url: url.substring(0, 100), + url: sanitizeUrlForLog(url), }) return null } @@ -133,7 +134,7 @@ async function fetchWithDNSPinning( } catch (error) { logger.error(`[${requestId}] Error fetching URL with DNS pinning`, { error: error instanceof Error ? error.message : String(error), - url: url.substring(0, 100), + url: sanitizeUrlForLog(url), }) return null } diff --git a/apps/sim/tools/browser_use/run_task.ts b/apps/sim/tools/browser_use/run_task.ts index 9dbeeb5b6..76edcfe8b 100644 --- a/apps/sim/tools/browser_use/run_task.ts +++ b/apps/sim/tools/browser_use/run_task.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import type { BrowserUseRunTaskParams, BrowserUseRunTaskResponse } from '@/tools/browser_use/types' import type { ToolConfig, ToolResponse } from '@/tools/types' @@ -183,7 +184,7 @@ async function pollForCompletion( } if (!liveUrlLogged && taskData.live_url) { - logger.info(`BrowserUse task ${taskId} live URL: ${taskData.live_url}`) + logger.info(`BrowserUse task ${taskId} live URL: ${sanitizeUrlForLog(taskData.live_url)}`) liveUrlLogged = true } diff --git a/apps/sim/tools/discord/send_message.ts b/apps/sim/tools/discord/send_message.ts index 074285e48..abe95515d 100644 --- a/apps/sim/tools/discord/send_message.ts +++ b/apps/sim/tools/discord/send_message.ts @@ -72,6 +72,7 @@ export const discordSendMessageTool: ToolConfig< outputs: { message: { type: 'string', description: 'Success or error message' }, + files: { type: 'file[]', description: 'Files attached to the message' }, data: { type: 'object', description: 'Discord message data', diff --git a/apps/sim/tools/discord/types.ts b/apps/sim/tools/discord/types.ts index 9573309e9..76a5d016e 100644 --- a/apps/sim/tools/discord/types.ts +++ b/apps/sim/tools/discord/types.ts @@ -1,4 +1,5 @@ import type { UserFile } from '@/executor/types' +import type { ToolFileData } from '@/tools/types' export interface DiscordMessage { id: string @@ -85,6 +86,7 @@ interface BaseDiscordResponse { export interface DiscordSendMessageResponse extends BaseDiscordResponse { output: { message: string + files?: ToolFileData[] data?: DiscordMessage } } diff --git a/apps/sim/tools/elevenlabs/types.ts b/apps/sim/tools/elevenlabs/types.ts index 06d043698..5627d1f72 100644 --- a/apps/sim/tools/elevenlabs/types.ts +++ b/apps/sim/tools/elevenlabs/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' export interface ElevenLabsTtsParams { @@ -12,11 +13,13 @@ export interface ElevenLabsTtsParams { export interface ElevenLabsTtsResponse extends ToolResponse { output: { audioUrl: string + audioFile?: UserFile } } export interface ElevenLabsBlockResponse extends ToolResponse { output: { audioUrl: string + audioFile?: UserFile } } diff --git a/apps/sim/tools/jira/add_attachment.ts b/apps/sim/tools/jira/add_attachment.ts new file mode 100644 index 000000000..8055c7230 --- /dev/null +++ b/apps/sim/tools/jira/add_attachment.ts @@ -0,0 +1,83 @@ +import type { JiraAddAttachmentParams, JiraAddAttachmentResponse } from '@/tools/jira/types' +import type { ToolConfig } from '@/tools/types' + +export const jiraAddAttachmentTool: ToolConfig = + { + id: 'jira_add_attachment', + name: 'Jira Add Attachment', + description: 'Add attachments to a Jira issue', + version: '1.0.0', + + oauth: { + required: true, + provider: 'jira', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Jira', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Jira domain (e.g., yourcompany.atlassian.net)', + }, + issueKey: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jira issue key to add attachments to (e.g., PROJ-123)', + }, + files: { + type: 'file[]', + required: true, + visibility: 'user-only', + description: 'Files to attach to the Jira issue', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'hidden', + description: + 'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: '/api/tools/jira/add-attachment', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params: JiraAddAttachmentParams) => ({ + accessToken: params.accessToken, + domain: params.domain, + issueKey: params.issueKey, + files: params.files, + cloudId: params.cloudId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + if (!response.ok || !data.success) { + throw new Error(data.error || 'Failed to add Jira attachment') + } + + return { + success: true, + output: data.output, + } + }, + + outputs: { + ts: { type: 'string', description: 'Timestamp of the operation' }, + issueKey: { type: 'string', description: 'Issue key' }, + attachmentIds: { type: 'json', description: 'IDs of uploaded attachments' }, + files: { type: 'file[]', description: 'Uploaded attachment files' }, + }, + } diff --git a/apps/sim/tools/jira/index.ts b/apps/sim/tools/jira/index.ts index a9bed6699..ced24d2d0 100644 --- a/apps/sim/tools/jira/index.ts +++ b/apps/sim/tools/jira/index.ts @@ -1,3 +1,4 @@ +import { jiraAddAttachmentTool } from '@/tools/jira/add_attachment' import { jiraAddCommentTool } from '@/tools/jira/add_comment' import { jiraAddWatcherTool } from '@/tools/jira/add_watcher' import { jiraAddWorklogTool } from '@/tools/jira/add_worklog' @@ -32,6 +33,7 @@ export { jiraTransitionIssueTool, jiraSearchIssuesTool, jiraAddCommentTool, + jiraAddAttachmentTool, jiraGetCommentsTool, jiraUpdateCommentTool, jiraDeleteCommentTool, diff --git a/apps/sim/tools/jira/types.ts b/apps/sim/tools/jira/types.ts index b7d840f95..0ed2ba646 100644 --- a/apps/sim/tools/jira/types.ts +++ b/apps/sim/tools/jira/types.ts @@ -1,4 +1,5 @@ -import type { ToolResponse } from '@/tools/types' +import type { UserFile } from '@/executor/types' +import type { ToolFileData, ToolResponse } from '@/tools/types' export interface JiraRetrieveParams { accessToken: string @@ -312,6 +313,23 @@ export interface JiraDeleteAttachmentResponse extends ToolResponse { } } +export interface JiraAddAttachmentParams { + accessToken: string + domain: string + issueKey: string + files: UserFile[] + cloudId?: string +} + +export interface JiraAddAttachmentResponse extends ToolResponse { + output: { + ts: string + issueKey: string + attachmentIds: string[] + files: ToolFileData[] + } +} + // Worklogs export interface JiraAddWorklogParams { accessToken: string @@ -482,6 +500,7 @@ export type JiraResponse = | JiraUpdateCommentResponse | JiraDeleteCommentResponse | JiraGetAttachmentsResponse + | JiraAddAttachmentResponse | JiraDeleteAttachmentResponse | JiraAddWorklogResponse | JiraGetWorklogsResponse diff --git a/apps/sim/tools/linear/create_attachment.ts b/apps/sim/tools/linear/create_attachment.ts index 5e366c5c6..03019de91 100644 --- a/apps/sim/tools/linear/create_attachment.ts +++ b/apps/sim/tools/linear/create_attachment.ts @@ -28,10 +28,16 @@ export const linearCreateAttachmentTool: ToolConfig< }, url: { type: 'string', - required: true, + required: false, visibility: 'user-or-llm', description: 'URL of the attachment', }, + file: { + type: 'file', + required: false, + visibility: 'user-only', + description: 'File to attach', + }, title: { type: 'string', required: true, @@ -59,9 +65,14 @@ export const linearCreateAttachmentTool: ToolConfig< } }, body: (params) => { + const attachmentUrl = params.url || params.file?.url + if (!attachmentUrl) { + throw new Error('URL or file is required') + } + const input: Record = { issueId: params.issueId, - url: params.url, + url: attachmentUrl, title: params.title, } diff --git a/apps/sim/tools/linear/types.ts b/apps/sim/tools/linear/types.ts index 5a24f0a82..b66bae5b4 100644 --- a/apps/sim/tools/linear/types.ts +++ b/apps/sim/tools/linear/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -875,7 +876,8 @@ export interface LinearGetActiveCycleParams { export interface LinearCreateAttachmentParams { issueId: string - url: string + url?: string + file?: UserFile title?: string subtitle?: string accessToken?: string diff --git a/apps/sim/tools/microsoft_planner/read_task.ts b/apps/sim/tools/microsoft_planner/read_task.ts index e44d5e175..730d3f788 100644 --- a/apps/sim/tools/microsoft_planner/read_task.ts +++ b/apps/sim/tools/microsoft_planner/read_task.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import type { MicrosoftPlannerReadResponse, MicrosoftPlannerToolParams, @@ -76,7 +77,7 @@ export const readTaskTool: ToolConfig = { jira_update_comment: jiraUpdateCommentTool, jira_delete_comment: jiraDeleteCommentTool, jira_get_attachments: jiraGetAttachmentsTool, + jira_add_attachment: jiraAddAttachmentTool, jira_delete_attachment: jiraDeleteAttachmentTool, jira_add_worklog: jiraAddWorklogTool, jira_get_worklogs: jiraGetWorklogsTool, diff --git a/apps/sim/tools/sftp/download.ts b/apps/sim/tools/sftp/download.ts index d02532e86..0c026258d 100644 --- a/apps/sim/tools/sftp/download.ts +++ b/apps/sim/tools/sftp/download.ts @@ -94,6 +94,7 @@ export const sftpDownloadTool: ToolConfig = success: true, output: { downloaded: true, + file: data.file, fileContent: data.content, fileName: data.fileName, remotePath: data.remotePath, @@ -91,6 +92,7 @@ export const downloadFileTool: ToolConfig = outputs: { downloaded: { type: 'boolean', description: 'Whether the file was downloaded successfully' }, + file: { type: 'file', description: 'Downloaded file stored in execution files' }, fileContent: { type: 'string', description: 'File content (base64 encoded for binary files)' }, fileName: { type: 'string', description: 'Name of the downloaded file' }, remotePath: { type: 'string', description: 'Source path on the remote server' }, diff --git a/apps/sim/tools/ssh/types.ts b/apps/sim/tools/ssh/types.ts index e68f6302f..5c4267f98 100644 --- a/apps/sim/tools/ssh/types.ts +++ b/apps/sim/tools/ssh/types.ts @@ -1,4 +1,4 @@ -import type { ToolResponse } from '@/tools/types' +import type { ToolFileData, ToolResponse } from '@/tools/types' // Base SSH connection configuration export interface SSHConnectionConfig { @@ -149,6 +149,7 @@ export interface SSHResponse extends ToolResponse { uploaded?: boolean downloaded?: boolean + file?: ToolFileData fileContent?: string fileName?: string remotePath?: string diff --git a/apps/sim/tools/stagehand/agent.ts b/apps/sim/tools/stagehand/agent.ts index f3d055a8e..7884b4575 100644 --- a/apps/sim/tools/stagehand/agent.ts +++ b/apps/sim/tools/stagehand/agent.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { sanitizeUrlForLog } from '@/lib/core/utils/logging' import type { StagehandAgentParams, StagehandAgentResponse } from '@/tools/stagehand/types' import { STAGEHAND_AGENT_RESULT_OUTPUT_PROPERTIES } from '@/tools/stagehand/types' import type { ToolConfig } from '@/tools/types' @@ -61,7 +62,9 @@ export const agentTool: ToolConfig let startUrl = params.startUrl if (startUrl && !startUrl.match(/^https?:\/\//i)) { startUrl = `https://${startUrl.trim()}` - logger.info(`Normalized URL from ${params.startUrl} to ${startUrl}`) + logger.info( + `Normalized URL from ${sanitizeUrlForLog(params.startUrl)} to ${sanitizeUrlForLog(startUrl)}` + ) } return { diff --git a/apps/sim/tools/supabase/types.ts b/apps/sim/tools/supabase/types.ts index b26e065b4..cf2ca9c00 100644 --- a/apps/sim/tools/supabase/types.ts +++ b/apps/sim/tools/supabase/types.ts @@ -1,3 +1,4 @@ +import type { UserFile } from '@/executor/types' import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -441,7 +442,7 @@ export interface SupabaseStorageUploadParams { bucket: string fileName: string path?: string - fileData: any // UserFile object (basic mode) or string (advanced mode: base64/plain text) + fileData: UserFile | string contentType?: string upsert?: boolean } diff --git a/apps/sim/tools/telegram/send_document.ts b/apps/sim/tools/telegram/send_document.ts index a9800e809..85c3724a8 100644 --- a/apps/sim/tools/telegram/send_document.ts +++ b/apps/sim/tools/telegram/send_document.ts @@ -75,6 +75,7 @@ export const telegramSendDocumentTool: ToolConfig< outputs: { message: { type: 'string', description: 'Success or error message' }, + files: { type: 'file[]', description: 'Files attached to the message' }, data: { type: 'object', description: 'Telegram message data including document', diff --git a/apps/sim/tools/telegram/types.ts b/apps/sim/tools/telegram/types.ts index 0e54deeb4..b7a887856 100644 --- a/apps/sim/tools/telegram/types.ts +++ b/apps/sim/tools/telegram/types.ts @@ -1,5 +1,5 @@ import type { UserFile } from '@/executor/types' -import type { ToolResponse } from '@/tools/types' +import type { ToolFileData, ToolResponse } from '@/tools/types' export interface TelegramMessage { message_id: number @@ -167,6 +167,7 @@ export interface TelegramSendDocumentResponse extends ToolResponse { output: { message: string data?: TelegramMedia + files?: ToolFileData[] } } diff --git a/apps/sim/tools/wordpress/types.ts b/apps/sim/tools/wordpress/types.ts index fbb948335..595e9e3e6 100644 --- a/apps/sim/tools/wordpress/types.ts +++ b/apps/sim/tools/wordpress/types.ts @@ -1,4 +1,5 @@ // Common types for WordPress REST API tools +import type { UserFile } from '@/executor/types' import type { ToolResponse } from '@/tools/types' // Common parameters for all WordPress tools (WordPress.com OAuth) @@ -254,7 +255,7 @@ export interface WordPressListPagesResponse extends ToolResponse { // Upload Media export interface WordPressUploadMediaParams extends WordPressBaseParams { - file: any // UserFile object from file upload + file: UserFile filename?: string // Optional filename override title?: string caption?: string diff --git a/apps/sim/triggers/microsoftteams/webhook.ts b/apps/sim/triggers/microsoftteams/webhook.ts index abec61982..1f17a7719 100644 --- a/apps/sim/triggers/microsoftteams/webhook.ts +++ b/apps/sim/triggers/microsoftteams/webhook.ts @@ -98,7 +98,7 @@ export const microsoftTeamsWebhookTrigger: TriggerConfig = { }, message: { raw: { - attachments: { type: 'file[]', description: 'Array of attachments' }, + attachments: { type: 'json', description: 'Array of attachments' }, channelData: { team: { id: { type: 'string', description: 'Team ID' } }, tenant: { id: { type: 'string', description: 'Tenant ID' } },