fix(security): harden file access controls, webhook auth, and input bounds (#4601)

* fix(security): harden file access controls, webhook auth, and input bounds

* fix(security): extend file access checks to remaining tool routes

* fix(logs): address PR review comments on time filter

* fix(logs): set end-time milliseconds to 999 for datetime filter strings

* fix(files): return 404 instead of 500 on file access denial in utility paths

* remove tooltip from resource tabs
This commit is contained in:
Waleed
2026-05-14 13:32:40 -07:00
committed by GitHub
parent 4a9e248eac
commit 80c9a01275
52 changed files with 517 additions and 277 deletions
+9 -5
View File
@@ -14,6 +14,14 @@ import { isUuid } from '@/executor/constants'
const logger = createLogger('FileAuthorization')
/** Thrown by utility functions when file access is denied, so route handlers can return 404. */
export class FileAccessDeniedError extends Error {
constructor() {
super('File not found')
this.name = 'FileAccessDeniedError'
}
}
interface AuthorizationResult {
granted: boolean
reason: string
@@ -598,7 +606,7 @@ async function authorizeFileAccess(
*/
export async function assertToolFileAccess(
key: unknown,
userId: string | undefined,
userId: string,
requestId: string,
routeLogger: ReturnType<typeof createLogger>
): Promise<NextResponse | null> {
@@ -606,10 +614,6 @@ export async function assertToolFileAccess(
routeLogger.warn(`[${requestId}] File access check rejected: missing key`)
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
if (!userId) {
routeLogger.warn(`[${requestId}] File access check requires userId but none available`)
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
const hasAccess = await verifyFileAccess(key, userId)
if (!hasAccess) {
routeLogger.warn(`[${requestId}] File access denied for user`, { userId, key })
@@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils'
export const dynamic = 'force-dynamic'
@@ -22,7 +23,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -66,6 +67,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
`[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)`
)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
const resolvedFileName = data.fileName || userFile.name || 'attachment'
+4 -1
View File
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -49,6 +50,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = validatedData.fileName || userFile.name
} else if (validatedData.fileContent) {
@@ -7,6 +7,7 @@ import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
@@ -80,6 +81,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(
userFile.key,
auth.userId,
'confluence-upload',
logger
)
if (denied) return denied
let fileBuffer: Buffer
try {
fileBuffer = await downloadFileFromStorage(userFile, 'confluence-upload', logger)
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -30,8 +31,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Discord send request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(discordSendMessageContract, request, {})
@@ -134,17 +136,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
formData.append('payload_json', JSON.stringify(payload))
const downloadedFiles = await Promise.all(
userFiles.map(async (userFile, i) => {
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
return { userFile, buffer }
const accessResults = await Promise.all(
userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
userFiles.map(async (file, i) => {
try {
logger.info(`[${requestId}] Downloading file ${i}: ${file.name}`)
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
})
)
for (let i = 0; i < downloadedFiles.length; i++) {
const { userFile, buffer } = downloadedFiles[i]
for (let i = 0; i < userFiles.length; i++) {
const userFile = userFiles[i]
const buffer = buffers[i]
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
filesOutput.push({
name: userFile.name,
mimeType: userFile.type || 'application/octet-stream',
+7 -3
View File
@@ -7,6 +7,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
const logger = createLogger('DocuSignAPI')
@@ -54,7 +55,7 @@ async function resolveAccount(accessToken: string): Promise<DocuSignAccountInfo>
export const POST = withRouteHandler(async (request: NextRequest) => {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
@@ -84,7 +85,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
switch (operation) {
case 'send_envelope':
return await handleSendEnvelope(apiBase, headers, params)
return await handleSendEnvelope(apiBase, headers, params, authResult.userId)
case 'create_from_template':
return await handleCreateFromTemplate(apiBase, headers, params)
case 'get_envelope':
@@ -115,7 +116,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
async function handleSendEnvelope(
apiBase: string,
headers: Record<string, string>,
params: Record<string, unknown>
params: Record<string, unknown>,
userId: string
) {
const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params
@@ -135,6 +137,8 @@ async function handleSendEnvelope(
const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger)
if (userFiles.length > 0) {
const userFile = userFiles[0]
const denied = await assertToolFileAccess(userFile.key, userId, 'docusign-send', logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, 'docusign-send', logger)
documentBase64 = buffer.toString('base64')
documentName = userFile.name
@@ -8,6 +8,7 @@ import { httpHeaderSafeJson } from '@/lib/core/utils/validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -52,6 +53,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (validatedData.fileContent) {
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -43,6 +44,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
size: userFile.size,
})
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
const formData = new FormData()
+18 -11
View File
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
base64UrlEncode,
buildMimeMessage,
@@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Gmail draft request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(gmailDraftContract, request, {})
@@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentBuffers = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
const buffer = await downloadFileFromStorage(file, requestId, logger)
return {
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffer,
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
@@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)
const attachmentBuffers = attachments.map((file, i) => ({
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffers[i],
}))
const mimeMessage = buildMimeMessage({
to: validatedData.to,
cc: validatedData.cc ?? undefined,
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
base64UrlEncode,
buildMimeMessage,
@@ -25,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Gmail edit draft attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -36,9 +37,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(
`[${requestId}] Authenticated Gmail edit draft request via ${authResult.authType}`,
{ userId: authResult.userId }
{ userId }
)
const parsed = await parseRequest(gmailEditDraftContract, request, {})
@@ -81,17 +83,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentBuffers = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
const buffer = await downloadFileFromStorage(file, requestId, logger)
return {
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffer,
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
})
)
const attachmentBuffers = attachments.map((file, i) => ({
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffers[i],
}))
const mimeMessage = buildMimeMessage({
to: validatedData.to,
cc: validatedData.cc ?? undefined,
+18 -11
View File
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
base64UrlEncode,
buildMimeMessage,
@@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Gmail send request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(gmailSendContract, request, {})
@@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentBuffers = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
const buffer = await downloadFileFromStorage(file, requestId, logger)
return {
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffer,
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
@@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)
const attachmentBuffers = attachments.map((file, i) => ({
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffers[i],
}))
const mimeMessage = buildMimeMessage({
to: validatedData.to,
cc: validatedData.cc ?? undefined,
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
GOOGLE_WORKSPACE_MIME_TYPES,
handleSheetsFormat,
@@ -52,7 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -113,6 +114,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
size: userFile.size,
})
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
let fileBuffer: Buffer
try {
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
const logger = createLogger('JiraAddAttachmentAPI')
@@ -17,7 +18,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json(
{ success: false, error: authResult.error || 'Unauthorized' },
{ status: 401 }
@@ -43,6 +44,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const formData = new FormData()
for (const file of userFiles) {
const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(file, requestId, logger)
const blob = new Blob([new Uint8Array(buffer)], {
type: file.type || 'application/octet-stream',
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Dataverse upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -66,6 +67,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} else if (validatedData.fileContent) {
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { FileAccessDeniedError } from '@/app/api/files/authorization'
import { uploadFilesForTeamsMessage } from '@/tools/microsoft_teams/server-utils'
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
import { resolveMentionsForChannel, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -20,7 +21,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Teams channel write attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -31,10 +32,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(
`[${requestId}] Authenticated Teams channel write request via ${authResult.authType}`,
{
userId: authResult.userId,
userId,
}
)
@@ -54,6 +56,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
accessToken: validatedData.accessToken,
requestId,
logger,
userId,
})
let messageContent = validatedData.content
@@ -160,6 +163,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
},
})
} catch (error) {
if (error instanceof FileAccessDeniedError) {
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
logger.error(`[${requestId}] Error sending Teams channel message:`, error)
return NextResponse.json(
{
@@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { FileAccessDeniedError } from '@/app/api/files/authorization'
import { uploadFilesForTeamsMessage } from '@/tools/microsoft_teams/server-utils'
import type { GraphApiErrorResponse, GraphChatMessage } from '@/tools/microsoft_teams/types'
import { resolveMentionsForChat, type TeamsMention } from '@/tools/microsoft_teams/utils'
@@ -20,7 +21,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Teams chat write attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -31,10 +32,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(
`[${requestId}] Authenticated Teams chat write request via ${authResult.authType}`,
{
userId: authResult.userId,
userId,
}
)
@@ -53,6 +55,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
accessToken: validatedData.accessToken,
requestId,
logger,
userId,
})
let messageContent = validatedData.content
@@ -157,6 +160,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
},
})
} catch (error) {
if (error instanceof FileAccessDeniedError) {
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
logger.error(`[${requestId}] Error sending Teams chat message:`, error)
return NextResponse.json(
{
@@ -14,6 +14,7 @@ import {
downloadFileFromStorage,
resolveInternalFileUrl,
} from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -120,6 +121,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
let base64 = userFile.base64
if (!base64) {
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
base64 = buffer.toString('base64')
}
@@ -13,6 +13,7 @@ import {
processSingleFileToUserFile,
} from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { normalizeExcelValues } from '@/tools/onedrive/utils'
export const dynamic = 'force-dynamic'
@@ -47,7 +48,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -108,6 +109,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
try {
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
} catch (error) {
+19 -14
View File
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Outlook draft request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(outlookDraftContract, request, {})
@@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentObjects = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
const buffer = await downloadFileFromStorage(file, requestId, logger)
const base64Content = buffer.toString('base64')
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: file.name,
contentType: file.type || 'application/octet-stream',
contentBytes: base64Content,
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
@@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)
const attachmentObjects = attachments.map((file, i) => ({
'@odata.type': '#microsoft.graph.fileAttachment',
name: file.name,
contentType: file.type || 'application/octet-stream',
contentBytes: buffers[i].toString('base64'),
}))
logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`)
message.attachments = attachmentObjects
}
+19 -14
View File
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Outlook send request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(outlookSendContract, request, {})
@@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentObjects = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
const buffer = await downloadFileFromStorage(file, requestId, logger)
const base64Content = buffer.toString('base64')
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: file.name,
contentType: file.type || 'application/octet-stream',
contentBytes: base64Content,
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
@@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)
const attachmentObjects = attachments.map((file, i) => ({
'@odata.type': '#microsoft.graph.fileAttachment',
name: file.name,
contentType: file.type || 'application/octet-stream',
contentBytes: buffers[i].toString('base64'),
}))
logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`)
message.attachments = attachmentObjects
}
@@ -8,6 +8,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
const logger = createLogger('QuiverImageToSvgAPI')
@@ -15,7 +16,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
@@ -47,6 +48,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
if (parsed && typeof parsed === 'object') {
const userFiles = processFilesToUserFiles([parsed as RawFileInput], requestId, logger)
if (userFiles.length > 0) {
const denied = await assertToolFileAccess(
userFiles[0].key,
authResult.userId,
requestId,
logger
)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
apiImage = { base64: buffer.toString('base64') }
} else {
@@ -64,6 +72,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
} else if (typeof data.image === 'object' && data.image !== null) {
const userFiles = processFilesToUserFiles([data.image as RawFileInput], requestId, logger)
if (userFiles.length > 0) {
const denied = await assertToolFileAccess(
userFiles[0].key,
authResult.userId,
requestId,
logger
)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
apiImage = { base64: buffer.toString('base64') }
} else {
@@ -8,6 +8,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
const logger = createLogger('QuiverTextToSvgAPI')
@@ -15,9 +16,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const userId = authResult.userId
try {
const parsed = await parseRequest(
@@ -51,6 +53,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
if (parsed && typeof parsed === 'object') {
const userFiles = processFilesToUserFiles([parsed as RawFileInput], requestId, logger)
if (userFiles.length > 0) {
const denied = await assertToolFileAccess(
userFiles[0].key,
userId,
requestId,
logger
)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
apiReferences.push({ base64: buffer.toString('base64') })
}
@@ -61,6 +70,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
} else if (typeof ref === 'object' && ref !== null) {
const userFiles = processFilesToUserFiles([ref as RawFileInput], requestId, logger)
if (userFiles.length > 0) {
const denied = await assertToolFileAccess(userFiles[0].key, userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger)
apiReferences.push({ base64: buffer.toString('base64') })
}
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -76,6 +77,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
uploadBody = buffer
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
assertSafeExternalUrl,
extractSapConcurError,
@@ -180,13 +181,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Concur upload request: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const userId = authResult.userId
// boundary-raw-json: internal upload envelope validated by SapConcurUploadRequestSchema below; not a public boundary
const json = await request.json()
@@ -204,6 +206,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userFile = userFiles[0]
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
if (denied) return denied
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
const fileName = userFile.name
const mimeType = inferMimeType(fileName, userFile.type)
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized SendGrid send attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -26,6 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated SendGrid send request via ${authResult.authType}`)
const parsed = await parseRequest(sendGridSendMailContract, request, {})
@@ -97,20 +99,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFiles = processFilesToUserFiles(rawAttachments, requestId, logger)
if (userFiles.length > 0) {
const sendGridAttachments = await Promise.all(
const accessResults = await Promise.all(
userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
userFiles.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
const buffer = await downloadFileFromStorage(file, requestId, logger)
return {
content: buffer.toString('base64'),
filename: file.name,
type: file.type || 'application/octet-stream',
disposition: 'attachment',
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
@@ -120,6 +121,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)
const sendGridAttachments = userFiles.map((file, i) => ({
content: buffers[i].toString('base64'),
filename: file.name,
type: file.type || 'application/octet-stream',
disposition: 'attachment',
}))
mailBody.attachments = sendGridAttachments
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
@@ -24,7 +24,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -5,7 +5,8 @@ import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { sendSlackMessage } from '../utils'
import { FileAccessDeniedError } from '@/app/api/files/authorization'
import { sendSlackMessage } from '@/app/api/tools/slack/utils'
export const dynamic = 'force-dynamic'
@@ -17,7 +18,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Slack send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -28,8 +29,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Slack send request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(slackSendMessageContract, request, {})
@@ -50,6 +52,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
accessToken: validatedData.accessToken,
channel: validatedData.channel ?? undefined,
userId: validatedData.userId ?? undefined,
ownerUserId: userId,
text: validatedData.text,
threadTs: validatedData.thread_ts ?? undefined,
blocks: validatedData.blocks ?? undefined,
@@ -65,6 +68,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
return NextResponse.json({ success: true, output: result.output })
} catch (error) {
if (error instanceof FileAccessDeniedError) {
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
logger.error(`[${requestId}] Error sending Slack message:`, error)
return NextResponse.json(
{
+12 -3
View File
@@ -2,6 +2,7 @@ import type { Logger } from '@sim/logger'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization'
import type { ToolFileData } from '@/tools/types'
/**
@@ -73,7 +74,8 @@ async function uploadFilesToSlack(
files: any[],
accessToken: string,
requestId: string,
logger: Logger
logger: Logger,
ownerUserId: string
): Promise<{ fileIds: string[]; files: ToolFileData[] }> {
const userFiles = processFilesToUserFiles(files, requestId, logger)
const uploadedFileIds: string[] = []
@@ -82,6 +84,11 @@ async function uploadFilesToSlack(
for (const userFile of userFiles) {
logger.info(`[${requestId}] Uploading file: ${userFile.name}`)
const hasAccess = await verifyFileAccess(userFile.key, ownerUserId)
if (!hasAccess) {
throw new FileAccessDeniedError()
}
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
const getUrlResponse = await fetch('https://slack.com/api/files.getUploadURLExternal', {
@@ -224,6 +231,7 @@ export interface SlackMessageParams {
accessToken: string
channel?: string
userId?: string
ownerUserId: string
text: string
threadTs?: string | null
blocks?: unknown[] | null
@@ -249,7 +257,7 @@ export async function sendSlackMessage(
}
error?: string
}> {
const { accessToken, text, threadTs, blocks, files } = params
const { accessToken, text, threadTs, blocks, files, ownerUserId } = params
let { channel } = params
if (!channel && params.userId) {
@@ -282,7 +290,8 @@ export async function sendSlackMessage(
files,
accessToken,
requestId,
logger
logger,
ownerUserId
)
// No valid files uploaded - send text-only
+30 -21
View File
@@ -22,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -33,8 +33,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const userId = authResult.userId
logger.info(`[${requestId}] Authenticated SMTP request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})
const parsed = await parseRequest(smtpSendContract, request, {})
@@ -120,25 +121,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const attachmentBuffers: { filename: string; content: Buffer; contentType: string }[] = []
for (const file of attachments) {
const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger)
if (denied) return denied
try {
logger.info(`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`)
const buffer = await downloadFileFromStorage(file, requestId, logger)
attachmentBuffers.push({
filename: file.name,
content: buffer,
contentType: file.type || 'application/octet-stream',
})
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
}
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied
const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
})
)
const attachmentBuffers = attachments.map((file, i) => ({
filename: file.name,
content: buffers[i],
contentType: file.type || 'application/octet-stream',
}))
logger.info(`[${requestId}] Processed ${attachmentBuffers.length} attachment(s)`)
mailOptions.attachments = attachmentBuffers
+6 -1
View File
@@ -17,6 +17,7 @@ import {
downloadFileFromStorage,
resolveInternalFileUrl,
} from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import type { TranscriptSegment } from '@/tools/stt/types'
const logger = createLogger('SttProxyAPI')
@@ -31,7 +32,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
@@ -79,6 +80,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const file = Array.isArray(body.audioFile) ? body.audioFile[0] : body.audioFile
logger.info(`[${requestId}] Processing uploaded file: ${file.name}`)
const deniedAudio = await assertToolFileAccess(file.key, userId, requestId, logger)
if (deniedAudio) return deniedAudio
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
audioFileName = file.name
// file.type may be missing if the file came from a block that doesn't preserve it
@@ -97,6 +100,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
: body.audioFileReference
logger.info(`[${requestId}] Processing referenced file: ${file.name}`)
const deniedRef = await assertToolFileAccess(file.key, userId, requestId, logger)
if (deniedRef) return deniedRef
audioBuffer = await downloadFileFromStorage(file, requestId, logger)
audioFileName = file.name
@@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(
`[${requestId}] Unauthorized Supabase storage upload attempt: ${authResult.error}`
)
@@ -143,6 +144,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
uploadBody = buffer
@@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { convertMarkdownToHTML } from '@/tools/telegram/utils'
export const dynamic = 'force-dynamic'
@@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
requireWorkflowId: false,
})
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Telegram send attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -88,6 +89,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFile = userFiles[0]
logger.info(`[${requestId}] Uploading document: ${userFile.name}`)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
const filesOutput = [
{
@@ -18,6 +18,7 @@ import {
downloadFileFromStorage,
resolveInternalFileUrl,
} from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
export const maxDuration = 300 // 5 minutes for large multi-page PDF processing
@@ -428,6 +429,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
bytes = buffer.toString('base64')
contentType = userFile.type || 'application/octet-stream'
+12 -1
View File
@@ -8,6 +8,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import type { UserFile } from '@/executor/types'
const logger = createLogger('VideoProxyAPI')
@@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
@@ -100,6 +101,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
let jobId: string | undefined
let actualDuration: number | undefined
if (body.visualReference) {
const denied = await assertToolFileAccess(
body.visualReference.key,
authResult.userId,
requestId,
logger
)
if (denied) return denied
}
try {
if (provider === 'runway') {
const result = await generateWithRunway(
@@ -15,6 +15,7 @@ import {
downloadFileFromStorage,
resolveInternalFileUrl,
} from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { convertUsageMetadata, extractTextContent } from '@/providers/google/utils'
export const dynamic = 'force-dynamic'
@@ -27,7 +28,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Vision analyze attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -87,6 +88,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
let base64 = userFile.base64
let bufferLength = 0
if (!base64) {
const denied = await assertToolFileAccess(
userFile.key,
authResult.userId,
requestId,
logger
)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
base64 = buffer.toString('base64')
bufferLength = buffer.length
@@ -25,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized WordPress upload attempt: ${authResult.error}`)
return NextResponse.json(
{
@@ -98,7 +98,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
<Button
key={tag.label}
variant='subtle'
className='max-w-[200px] px-2 py-1 text-caption'
className='max-w-[280px] px-2 py-1 text-caption'
onClick={tag.onRemove}
>
<span className='truncate'>{tag.label}</span>
@@ -178,64 +178,57 @@ const ResourceTabItem = memo(function ResourceTabItem({
{showGapBefore && (
<div className='-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute top-1/2 left-0 z-10 h-[16px] w-[2px] rounded-full bg-[var(--text-subtle)]' />
)}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
draggable
data-resource-tab-id={resource.id}
onDragStart={(e) => onDragStart(e, idx)}
onDragOver={(e) => onDragOver(e, idx)}
onDragLeave={onDragLeave}
onDragEnd={onDragEnd}
onMouseDown={(e) => {
if (e.button === 1) {
e.preventDefault()
if (chatId) onRemove(e, resource)
}
<Button
variant='subtle'
draggable
data-resource-tab-id={resource.id}
onDragStart={(e) => onDragStart(e, idx)}
onDragOver={(e) => onDragOver(e, idx)}
onDragLeave={onDragLeave}
onDragEnd={onDragEnd}
onMouseDown={(e) => {
if (e.button === 1) {
e.preventDefault()
if (chatId) onRemove(e, resource)
}
}}
onClick={(e) => onTabClick(e, idx)}
onMouseEnter={() => setHoveredTabId(resource.id)}
onMouseLeave={() => setHoveredTabId(null)}
className={cn(
'group relative shrink-0 bg-transparent px-2 py-[3px] pr-[22px] text-caption transition-colors duration-150',
isActive && 'bg-[var(--surface-4)]',
isSelected && !isActive && 'bg-[var(--surface-3)]',
isDragging && 'opacity-30'
)}
>
{config.renderTabIcon(resource, 'mr-1.5 h-[14px] w-[14px]')}
{displayName}
{(isHovered || isActive) && chatId && (
<span
role='button'
tabIndex={-1}
onClick={(e) => onRemove(e, resource)}
onKeyDown={(e) => {
if (e.key === 'Enter') onRemove(e, resource)
}}
onClick={(e) => onTabClick(e, idx)}
onMouseEnter={() => setHoveredTabId(resource.id)}
onMouseLeave={() => setHoveredTabId(null)}
className={cn(
'group relative shrink-0 bg-transparent px-2 py-1 pr-[22px] text-caption transition-opacity duration-150',
isActive && 'bg-[var(--surface-4)]',
isSelected && !isActive && 'bg-[var(--surface-3)]',
isDragging && 'opacity-30'
)}
className='-translate-y-1/2 absolute top-1/2 right-[4px] flex items-center justify-center rounded-sm p-[1px] hover-hover:bg-[var(--surface-5)]'
aria-label={`Close ${displayName}`}
>
{config.renderTabIcon(resource, 'mr-1.5 h-[14px] w-[14px]')}
{displayName}
{(isHovered || isActive) && chatId && (
<span
role='button'
tabIndex={-1}
onClick={(e) => onRemove(e, resource)}
onKeyDown={(e) => {
if (e.key === 'Enter') onRemove(e, resource)
}}
className='-translate-y-1/2 absolute top-1/2 right-[4px] flex items-center justify-center rounded-sm p-[1px] hover-hover:bg-[var(--surface-5)]'
aria-label={`Close ${displayName}`}
>
<svg
className='size-[10px] text-[var(--text-icon)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2.5'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M18 6 6 18M6 6l12 12' />
</svg>
</span>
)}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>{displayName}</p>
</Tooltip.Content>
</Tooltip.Root>
<svg
className='size-[10px] text-[var(--text-icon)]'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2.5'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M18 6 6 18M6 6l12 12' />
</svg>
</span>
)}
</Button>
{showGapAfter && (
<div className='-translate-y-1/2 pointer-events-none absolute top-1/2 right-0 z-10 h-[16px] w-[2px] translate-x-1/2 rounded-full bg-[var(--text-subtle)]' />
)}
@@ -21,7 +21,11 @@ import { hasActiveFilters } from '@/lib/logs/filters'
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
import { captureEvent } from '@/lib/posthog/client'
import { workflowBorderColor } from '@/lib/workspaces/colors'
import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils'
import {
formatDateShort,
type LogStatus,
STATUS_CONFIG,
} from '@/app/workspace/[workspaceId]/logs/utils'
import { getBlock } from '@/blocks/registry'
import { useFolderMap } from '@/hooks/queries/folders'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -43,28 +47,6 @@ const TIME_RANGE_OPTIONS: ComboboxOption[] = [
{ value: 'Custom range', label: 'Custom range' },
] as const
/**
* Formats a date string (YYYY-MM-DD) for display.
*/
function formatDateShort(dateStr: string): string {
const date = new Date(dateStr)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
return `${months[date.getMonth()]} ${date.getDate()}`
}
type ViewMode = 'logs' | 'dashboard'
interface LogsToolbarProps {
@@ -794,11 +776,13 @@ export const LogsToolbar = memo(function LogsToolbar({
}
size='sm'
align='end'
className='h-[32px] w-[120px] rounded-md'
className='h-[32px] w-[160px] rounded-md'
maxHeight={320}
/>
<DatePicker
mode='range'
showTrigger={false}
showTime
open={datePickerOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
@@ -91,6 +91,7 @@ import {
DELETED_WORKFLOW_LABEL,
extractRetryInput,
formatDate,
formatDateShort,
getDisplayStatus,
type LogStatus,
parseDuration,
@@ -205,25 +206,6 @@ function SpinningRefreshCw(props: React.SVGProps<SVGSVGElement>) {
return <RefreshCw {...props} animate />
}
function formatDateShort(dateStr: string): string {
const date = new Date(dateStr)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
return `${months[date.getMonth()]} ${date.getDate()}`
}
/**
* Logs page component displaying workflow execution history.
* Supports filtering, search, live updates, and detailed log inspection.
@@ -866,7 +848,7 @@ export default function Logs() {
tags.push({
label:
timeRange === 'Custom range' && startDate && endDate
? `${startDate} – ${endDate}`
? `${formatDateShort(startDate)} – ${formatDateShort(endDate)}`
: timeRange,
onRemove: () => {
clearDateRange()
@@ -1519,10 +1501,12 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
}
size='sm'
className='h-[32px] w-full rounded-md'
maxHeight={320}
/>
<DatePicker
mode='range'
showTrigger={false}
showTime
open={datePickerOpen}
onOpenChange={(isOpen) => {
if (!isOpen) {
@@ -179,6 +179,31 @@ export function formatLatency(ms: number): string {
return formatDuration(ms, { precision: 2 }) ?? '—'
}
export function formatDateShort(dateStr: string): string {
const hasTime = dateStr.includes('T')
const [datePart, timePart] = dateStr.split('T')
const [, month, day] = datePart.split('-').map(Number)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
]
const dateLabel = `${months[month - 1]} ${day}`
if (hasTime && timePart) {
return `${dateLabel} ${timePart.slice(0, 5)}`
}
return dateLabel
}
export const formatDate = (dateString: string) => {
const date = new Date(dateString)
return {
@@ -33,6 +33,7 @@ import {
PopoverAnchor,
PopoverContent,
} from '@/components/emcn/components/popover/popover'
import { TimePicker } from '@/components/emcn/components/time-picker/time-picker'
import { cn } from '@/lib/core/utils/cn'
/**
@@ -96,22 +97,26 @@ interface DatePickerSingleProps extends DatePickerBaseProps {
onCancel?: never
/** Not used in single mode */
onClear?: never
/** Not used in single mode */
showTime?: never
}
/** Props for range date mode */
interface DatePickerRangeProps extends DatePickerBaseProps {
/** Selection mode */
mode: 'range'
/** Start date for range mode (YYYY-MM-DD string or Date) */
/** Start date for range mode (YYYY-MM-DD or YYYY-MM-DDTHH:mm string or Date) */
startDate?: string | Date
/** End date for range mode (YYYY-MM-DD string or Date) */
/** End date for range mode (YYYY-MM-DD or YYYY-MM-DDTHH:mm string or Date) */
endDate?: string | Date
/** Callback when date range is applied */
/** Callback when date range is applied — returns YYYY-MM-DD or YYYY-MM-DDTHH:mm depending on showTime */
onRangeChange?: (startDate: string, endDate: string) => void
/** Callback when range selection is cancelled */
onCancel?: () => void
/** Callback when range is cleared */
onClear?: () => void
/** Whether to show time inputs for precise range selection */
showTime?: boolean
/** Not used in range mode */
value?: never
/** Not used in range mode */
@@ -503,6 +508,7 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
onRangeChange: _onRangeChange,
onCancel: _onCancel,
onClear: _onClear,
showTime = false,
...htmlProps
} = rest as any
@@ -530,6 +536,8 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
const [rangeEnd, setRangeEnd] = React.useState<Date | null>(initialEnd)
const [hoverDate, setHoverDate] = React.useState<Date | null>(null)
const [selectingEnd, setSelectingEnd] = React.useState(false)
const [startTime, setStartTime] = React.useState('00:00')
const [endTime, setEndTime] = React.useState('23:59')
const [viewMonth, setViewMonth] = React.useState(() => {
const d = selectedDate || initialStart || new Date()
@@ -548,6 +556,12 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
setRangeStart(initialStart)
setRangeEnd(initialEnd)
setSelectingEnd(false)
if (showTime) {
const sd = isRangeMode ? props.startDate : undefined
const ed = isRangeMode ? props.endDate : undefined
setStartTime(typeof sd === 'string' && sd.includes('T') ? sd.slice(11, 16) : '00:00')
setEndTime(typeof ed === 'string' && ed.includes('T') ? ed.slice(11, 16) : '23:59')
}
if (initialStart) {
setViewMonth(initialStart.getMonth())
setViewYear(initialStart.getFullYear())
@@ -557,7 +571,7 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
setViewYear(now.getFullYear())
}
}
}, [open, isRangeMode, initialStart, initialEnd])
}, [open, isRangeMode, initialStart, initialEnd, showTime, props.startDate, props.endDate])
const singleValueKey = !isRangeMode && selectedDate ? selectedDate.getTime() : undefined
const [prevSingleValueKey, setPrevSingleValueKey] = React.useState(singleValueKey)
@@ -661,13 +675,32 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
if (isRangeMode && props.onRangeChange && rangeStart) {
const start = rangeEnd && rangeEnd < rangeStart ? rangeEnd : rangeStart
const end = rangeEnd && rangeEnd < rangeStart ? rangeStart : rangeEnd || rangeStart
const startStr = formatDateAsString(start.getFullYear(), start.getMonth(), start.getDate())
const endStr = formatDateAsString(end.getFullYear(), end.getMonth(), end.getDate())
let effectiveStartTime = startTime
let effectiveEndTime = endTime
if (showTime && startStr === endStr && startTime > endTime) {
effectiveStartTime = endTime
effectiveEndTime = startTime
}
props.onRangeChange(
formatDateAsString(start.getFullYear(), start.getMonth(), start.getDate()),
formatDateAsString(end.getFullYear(), end.getMonth(), end.getDate())
showTime ? `${startStr}T${effectiveStartTime}` : startStr,
showTime ? `${endStr}T${effectiveEndTime}:59` : endStr
)
setOpen(false)
}
}, [isRangeMode, props.onRangeChange, rangeStart, rangeEnd, setOpen])
}, [
isRangeMode,
props.onRangeChange,
rangeStart,
rangeEnd,
showTime,
startTime,
endTime,
setOpen,
])
/**
* Cancels range selection.
@@ -754,6 +787,21 @@ const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>((props, ref
/>
</div>
{/* Time inputs */}
{showTime && (
<div className='flex border-[var(--border-1)] border-t'>
<div className='flex flex-1 items-center justify-between gap-2 px-3 py-2'>
<span className='shrink-0 text-[12px] text-[var(--text-muted)]'>Start</span>
<TimePicker size='sm' value={startTime} onChange={setStartTime} />
</div>
<div className='w-[1px] bg-[var(--border-1)]' />
<div className='flex flex-1 items-center justify-between gap-2 px-3 py-2'>
<span className='shrink-0 text-[12px] text-[var(--text-muted)]'>End</span>
<TimePicker size='sm' value={endTime} onChange={setEndTime} />
</div>
</div>
)}
{/* Actions */}
<div className='flex items-center justify-between border-[var(--border-1)] border-t px-3 py-2'>
<Button
@@ -243,7 +243,7 @@ export const sshReadFileContentBodySchema = requirePasswordOrPrivateKey(
...connectionFields,
path: z.string().min(1, 'Path is required'),
encoding: z.string().default('utf-8'),
maxSize: z.coerce.number().default(10),
maxSize: z.coerce.number().min(0.01).max(50).default(10),
})
)
+6 -2
View File
@@ -66,7 +66,7 @@ export function getStartDateFromTimeRange(timeRange: TimeRange, startDate?: stri
if (timeRange === 'Custom range') {
if (startDate) {
const date = new Date(startDate)
date.setHours(0, 0, 0, 0)
if (!startDate.includes('T')) date.setHours(0, 0, 0, 0)
return date
}
return null
@@ -111,7 +111,11 @@ export function getEndDateFromTimeRange(timeRange: TimeRange, endDate?: string):
if (endDate) {
const date = new Date(endDate)
date.setHours(23, 59, 59, 999)
if (!endDate.includes('T')) {
date.setHours(23, 59, 59, 999)
} else {
date.setMilliseconds(999)
}
return date
}
+1 -11
View File
@@ -30,13 +30,6 @@ function validateAttioSignature(secret: string, signature: string, body: string)
return false
}
const computedHash = hmacSha256Hex(body, secret)
logger.debug('Attio signature comparison', {
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${signature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: signature.length,
match: computedHash === signature,
})
return safeCompare(computedHash, signature)
} catch (error) {
logger.error('Error validating Attio signature:', error)
@@ -65,10 +58,7 @@ export const attioHandler: WebhookProviderHandler = {
const isValidSignature = validateAttioSignature(secret, signature, rawBody)
if (!isValidSignature) {
logger.warn(`[${requestId}] Attio signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
logger.warn(`[${requestId}] Attio signature verification failed`)
return new NextResponse('Unauthorized - Invalid Attio signature', {
status: 401,
})
-10
View File
@@ -37,14 +37,6 @@ function validateGitHubSignature(secret: string, signature: string, body: string
return false
}
const computedHash = crypto.createHmac(algorithm, secret).update(body, 'utf8').digest('hex')
logger.debug('GitHub signature comparison', {
algorithm,
computedSignature: `${computedHash.substring(0, 10)}...`,
providedSignature: `${providedSignature.substring(0, 10)}...`,
computedLength: computedHash.length,
providedLength: providedSignature.length,
match: computedHash === providedSignature,
})
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating GitHub signature:', error)
@@ -68,8 +60,6 @@ export const githubHandler: WebhookProviderHandler = {
if (!validateGitHubSignature(secret, signature, rawBody)) {
logger.warn(`[${requestId}] GitHub signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
usingSha256: !!request.headers.get('X-Hub-Signature-256'),
})
return new NextResponse('Unauthorized - Invalid GitHub signature', { status: 401 })
+1 -4
View File
@@ -59,10 +59,7 @@ export const intercomHandler: WebhookProviderHandler = {
}
if (!validateIntercomSignature(secret, signature, rawBody)) {
logger.warn(`[${requestId}] Intercom signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
logger.warn(`[${requestId}] Intercom signature verification failed`)
return new NextResponse('Unauthorized - Invalid Intercom signature', { status: 401 })
}
+1 -1
View File
@@ -179,7 +179,7 @@ async function handleWhatsAppVerification(
continue
}
if (token === verificationToken) {
if (safeCompare(token, verificationToken as string)) {
logger.info(`[${requestId}] WhatsApp verification successful for webhook ${wh.id}`)
return new NextResponse(challenge, {
status: 200,
@@ -1390,7 +1390,7 @@ export class PauseResumeManager {
await tx
.update(pausedExecutions)
.set({
pausePoints: sql`jsonb_set(jsonb_set(pause_points, ARRAY[${contextId}, 'resumeStatus'], '"resumed"'::jsonb), ARRAY[${contextId}, 'resumedAt'], '"${sql.raw(now.toISOString())}"'::jsonb)`,
pausePoints: sql`jsonb_set(jsonb_set(pause_points, ARRAY[${contextId}, 'resumeStatus'], '"resumed"'::jsonb), ARRAY[${contextId}, 'resumedAt'], ${JSON.stringify(now.toISOString())}::jsonb)`,
resumedCount: sql`resumed_count + 1`,
status: sql`CASE WHEN status = 'cancelling' THEN 'cancelling' WHEN resumed_count + 1 >= total_pause_count THEN 'fully_resumed' ELSE 'partially_resumed' END`,
updatedAt: now,
@@ -7,6 +7,7 @@ import type { Logger } from '@sim/logger'
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { FileAccessDeniedError, verifyFileAccess } from '@/app/api/files/authorization'
import type { UserFile } from '@/executor/types'
import type { GraphApiErrorResponse, GraphDriveItem } from '@/tools/microsoft_teams/types'
@@ -45,8 +46,9 @@ export async function uploadFilesForTeamsMessage(params: {
accessToken: string
requestId: string
logger: Logger
userId: string
}): Promise<TeamsFileUploadResult> {
const { rawFiles, accessToken, requestId, logger: log } = params
const { rawFiles, accessToken, requestId, logger: log, userId } = params
const attachments: TeamsAttachmentRef[] = []
const filesOutput: TeamsFileOutput[] = []
@@ -72,6 +74,11 @@ export async function uploadFilesForTeamsMessage(params: {
log.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`)
const hasAccess = await verifyFileAccess(file.key, userId)
if (!hasAccess) {
throw new FileAccessDeniedError()
}
// Download file from storage
const buffer = await downloadFileFromStorage(file, requestId, log)
filesOutput.push({
+2 -2
View File
@@ -368,7 +368,7 @@ export async function resolveMentionsForChat(
})
}
resolvedTags.add(mention.fullTag)
updatedContent = updatedContent.replace(
updatedContent = updatedContent.replaceAll(
mention.fullTag,
`<at id="${mention.mentionId}">${mention.name}</at>`
)
@@ -435,7 +435,7 @@ export async function resolveMentionsForChannel(
})
}
resolvedTags.add(mention.fullTag)
updatedContent = updatedContent.replace(
updatedContent = updatedContent.replaceAll(
mention.fullTag,
`<at id="${mention.mentionId}">${mention.name}</at>`
)