Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
622d0ca
Merge pull request #3172 from simstudioai/fix/notifs
waleedlatif1 Feb 9, 2026
b3dbb44
improvement(jsm): destructured outputs for jsm, jira, and added 1pass…
waleedlatif1 Feb 10, 2026
e5d3049
fix(slack): resolve file metadata via files.info when event payload i…
waleedlatif1 Feb 10, 2026
190f12f
feat(copilot): copilot mcp + server side copilot execution (#3173)
Sg312 Feb 10, 2026
8b4b3af
fix(mcp): harden notification system against race conditions (#3168)
waleedlatif1 Feb 10, 2026
e321f88
improvement(preview): added trigger mode context for deploy preview (…
waleedlatif1 Feb 10, 2026
73540e3
feat(logs): add skill icon to trace spans (#3181)
emir-karabeg Feb 10, 2026
be3cdcf
Merge pull request #3179 from simstudioai/improvement/file-download-t…
icecrasher321 Feb 10, 2026
20b230d
improvement(schema): centralize derivation of block schemas (#3175)
icecrasher321 Feb 11, 2026
c5dd90e
feat(copilot): enterprise configuration (#3184)
Sg312 Feb 11, 2026
f8e9614
improvement(helm): support copilot-only deployments (#3185)
waleedlatif1 Feb 11, 2026
6d16f21
improvement(mcp): improved mcp sse events notifs, update jira to hand…
waleedlatif1 Feb 11, 2026
78fef22
fix(execution): scope execution state per workflow to prevent cross-w…
waleedlatif1 Feb 11, 2026
f5dc180
fix(memory): upgrade bun from 1.3.3 to 1.3.9 (#3186)
waleedlatif1 Feb 11, 2026
c471627
fix(posthog): replace proxy rewrite with route handler for reliable b…
waleedlatif1 Feb 11, 2026
8a24b56
improvement(terminal): increase workflow logs limit from 1k to 5k per…
waleedlatif1 Feb 11, 2026
af01dce
fix(terminal): subflow logs rendering (#3189)
icecrasher321 Feb 11, 2026
13a9111
fix(logs): surface handled errors as info in logs (#3190)
waleedlatif1 Feb 11, 2026
3d5bd00
fix(triggers): add copilot as a trigger type (#3191)
waleedlatif1 Feb 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(slack): resolve file metadata via files.info when event payload i…
…s partial (#3176)
  • Loading branch information
waleedlatif1 authored Feb 10, 2026
commit e5d30494cbc2e470e4e8c478891f51ab16e52385
87 changes: 66 additions & 21 deletions apps/sim/lib/webhooks/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,17 +527,61 @@ export async function validateTwilioSignature(
}
}

const SLACK_FILE_HOSTS = new Set(['files.slack.com', 'files-pri.slack.com'])
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
const SLACK_MAX_FILES = 10
const SLACK_MAX_FILES = 15

/**
* Resolves the full file object from the Slack API when the event payload
* only contains a partial file (e.g. missing url_private due to file_access restrictions).
* @see https://docs.slack.dev/reference/methods/files.info
*/
async function resolveSlackFileInfo(
fileId: string,
botToken: string
): Promise<{ url_private?: string; name?: string; mimetype?: string; size?: number } | null> {
try {
const response = await fetch(
`https://slack.com/api/files.info?file=${encodeURIComponent(fileId)}`,
{
headers: { Authorization: `Bearer ${botToken}` },
}
)

const data = (await response.json()) as {
ok: boolean
error?: string
file?: Record<string, any>
}

if (!data.ok || !data.file) {
logger.warn('Slack files.info failed', { fileId, error: data.error })
return null
}

return {
url_private: data.file.url_private,
name: data.file.name,
mimetype: data.file.mimetype,
size: data.file.size,
}
} catch (error) {
logger.error('Error calling Slack files.info', {
fileId,
error: error instanceof Error ? error.message : String(error),
})
return null
}
}

/**
* Downloads file attachments from Slack using the bot token.
* Returns files in the format expected by WebhookAttachmentProcessor:
* { name, data (base64 string), mimeType, size }
*
* When the event payload contains partial file objects (missing url_private),
* falls back to the Slack files.info API to resolve the full file metadata.
*
* Security:
* - Validates each url_private against allowlisted Slack file hosts
* - Uses validateUrlWithDNS + secureFetchWithPinnedIP to prevent SSRF
* - Enforces per-file size limit and max file count
*/
Expand All @@ -549,30 +593,31 @@ async function downloadSlackFiles(
const downloaded: Array<{ name: string; data: string; mimeType: string; size: number }> = []

for (const file of filesToProcess) {
const urlPrivate = file.url_private as string | undefined
if (!urlPrivate) {
continue
}

// Validate the URL points to a known Slack file host
let parsedUrl: URL
try {
parsedUrl = new URL(urlPrivate)
} catch {
logger.warn('Slack file has invalid url_private, skipping', { fileId: file.id })
continue
let urlPrivate = file.url_private as string | undefined
let fileName = file.name as string | undefined
let fileMimeType = file.mimetype as string | undefined
let fileSize = file.size as number | undefined

// If url_private is missing, resolve via files.info API
if (!urlPrivate && file.id) {
const resolved = await resolveSlackFileInfo(file.id, botToken)
if (resolved?.url_private) {
urlPrivate = resolved.url_private
fileName = fileName || resolved.name
fileMimeType = fileMimeType || resolved.mimetype
fileSize = fileSize ?? resolved.size
}
}

if (!SLACK_FILE_HOSTS.has(parsedUrl.hostname)) {
logger.warn('Slack file url_private points to unexpected host, skipping', {
if (!urlPrivate) {
logger.warn('Slack file has no url_private and could not be resolved, skipping', {
fileId: file.id,
hostname: sanitizeUrlForLog(urlPrivate),
})
continue
}

// Skip files that exceed the size limit
const reportedSize = Number(file.size) || 0
const reportedSize = Number(fileSize) || 0
if (reportedSize > SLACK_MAX_FILE_SIZE) {
logger.warn('Slack file exceeds size limit, skipping', {
fileId: file.id,
Expand Down Expand Up @@ -618,9 +663,9 @@ async function downloadSlackFiles(
}

downloaded.push({
name: file.name || 'download',
name: fileName || 'download',
data: buffer.toString('base64'),
mimeType: file.mimetype || 'application/octet-stream',
mimeType: fileMimeType || 'application/octet-stream',
size: buffer.length,
})
} catch (error) {
Expand Down