Skip to content

Commit 9ec0c8f

Browse files
committed
separate server and client logic
1 parent 39ca1f6 commit 9ec0c8f

File tree

129 files changed

+4563
-1939
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+4563
-1939
lines changed

apps/sim/app/api/a2a/serve/[agentId]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
import { checkHybridAuth } from '@/lib/auth/hybrid'
1717
import { getBrandConfig } from '@/lib/branding/branding'
1818
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
19-
import { validateExternalUrl } from '@/lib/core/security/input-validation'
19+
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
2020
import { SSE_HEADERS } from '@/lib/core/utils/sse'
2121
import { getBaseUrl } from '@/lib/core/utils/urls'
2222
import { markExecutionCancelled } from '@/lib/execution/cancellation'
@@ -1119,7 +1119,7 @@ async function handlePushNotificationSet(
11191119
)
11201120
}
11211121

1122-
const urlValidation = validateExternalUrl(
1122+
const urlValidation = await validateUrlWithDNS(
11231123
params.pushNotificationConfig.url,
11241124
'Push notification URL'
11251125
)

apps/sim/app/api/files/parse/route.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { createLogger } from '@sim/logger'
66
import binaryExtensionsList from 'binary-extensions'
77
import { type NextRequest, NextResponse } from 'next/server'
88
import { checkHybridAuth } from '@/lib/auth/hybrid'
9-
import { secureFetchWithPinnedIP, validateUrlWithDNS } from '@/lib/core/security/input-validation'
9+
import {
10+
secureFetchWithPinnedIP,
11+
validateUrlWithDNS,
12+
} from '@/lib/core/security/input-validation.server'
1013
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
1114
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
1215
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
@@ -20,6 +23,7 @@ import {
2023
getMimeTypeFromExtension,
2124
getViewerUrl,
2225
inferContextFromKey,
26+
isInternalFileUrl,
2327
} from '@/lib/uploads/utils/file-utils'
2428
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
2529
import { verifyFileAccess } from '@/app/api/files/authorization'
@@ -216,7 +220,7 @@ async function parseFileSingle(
216220
}
217221
}
218222

219-
if (filePath.includes('/api/files/serve/')) {
223+
if (isInternalFileUrl(filePath)) {
220224
return handleCloudFile(filePath, fileType, undefined, userId, executionContext)
221225
}
222226

@@ -247,7 +251,7 @@ function validateFilePath(filePath: string): { isValid: boolean; error?: string
247251
return { isValid: false, error: 'Invalid path: tilde character not allowed' }
248252
}
249253

250-
if (filePath.startsWith('/') && !filePath.startsWith('/api/files/serve/')) {
254+
if (filePath.startsWith('/') && !isInternalFileUrl(filePath)) {
251255
return { isValid: false, error: 'Path outside allowed directory' }
252256
}
253257

@@ -368,7 +372,7 @@ async function handleExternalUrl(
368372
throw new Error(`File too large: ${buffer.length} bytes (max: ${MAX_DOWNLOAD_SIZE_BYTES})`)
369373
}
370374

371-
logger.info(`Downloaded file from URL: ${sanitizeUrlForLog(url)}, size: ${buffer.length} bytes`)
375+
logger.info(`Downloaded file from URL: ${url}, size: ${buffer.length} bytes`)
372376

373377
let userFile: UserFile | undefined
374378
const mimeType = response.headers.get('content-type') || getMimeTypeFromExtension(extension)

apps/sim/app/api/tools/a2a/send-message/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
55
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
66
import { checkHybridAuth } from '@/lib/auth/hybrid'
7+
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
78
import { generateRequestId } from '@/lib/core/utils/request'
89

910
export const dynamic = 'force-dynamic'
@@ -95,6 +96,14 @@ export async function POST(request: NextRequest) {
9596
if (validatedData.files && validatedData.files.length > 0) {
9697
for (const file of validatedData.files) {
9798
if (file.type === 'url') {
99+
const urlValidation = await validateUrlWithDNS(file.data, 'fileUrl')
100+
if (!urlValidation.isValid) {
101+
return NextResponse.json(
102+
{ success: false, error: urlValidation.error },
103+
{ status: 400 }
104+
)
105+
}
106+
98107
const filePart: FilePart = {
99108
kind: 'file',
100109
file: {

apps/sim/app/api/tools/a2a/set-push-notification/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { createA2AClient } from '@/lib/a2a/utils'
55
import { checkHybridAuth } from '@/lib/auth/hybrid'
6-
import { validateExternalUrl } from '@/lib/core/security/input-validation'
6+
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
77
import { generateRequestId } from '@/lib/core/utils/request'
88

99
export const dynamic = 'force-dynamic'
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
4040
const body = await request.json()
4141
const validatedData = A2ASetPushNotificationSchema.parse(body)
4242

43-
const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
43+
const urlValidation = await validateUrlWithDNS(validatedData.webhookUrl, 'Webhook URL')
4444
if (!urlValidation.isValid) {
4545
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
4646
return NextResponse.json(
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { z } from 'zod'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
5+
import {
6+
secureFetchWithPinnedIP,
7+
validateUrlWithDNS,
8+
} from '@/lib/core/security/input-validation.server'
9+
import { generateRequestId } from '@/lib/core/utils/request'
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
const logger = createLogger('GitHubLatestCommitAPI')
14+
15+
const GitHubLatestCommitSchema = z.object({
16+
owner: z.string().min(1, 'Owner is required'),
17+
repo: z.string().min(1, 'Repo is required'),
18+
branch: z.string().optional().nullable(),
19+
apiKey: z.string().min(1, 'API key is required'),
20+
})
21+
22+
export async function POST(request: NextRequest) {
23+
const requestId = generateRequestId()
24+
25+
try {
26+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
27+
28+
if (!authResult.success) {
29+
logger.warn(`[${requestId}] Unauthorized GitHub latest commit attempt: ${authResult.error}`)
30+
return NextResponse.json(
31+
{
32+
success: false,
33+
error: authResult.error || 'Authentication required',
34+
},
35+
{ status: 401 }
36+
)
37+
}
38+
39+
const body = await request.json()
40+
const validatedData = GitHubLatestCommitSchema.parse(body)
41+
42+
const { owner, repo, branch, apiKey } = validatedData
43+
44+
const baseUrl = `https://api.github.com/repos/${owner}/${repo}`
45+
const commitUrl = branch ? `${baseUrl}/commits/${branch}` : `${baseUrl}/commits/HEAD`
46+
47+
logger.info(`[${requestId}] Fetching latest commit from GitHub`, { owner, repo, branch })
48+
49+
const urlValidation = await validateUrlWithDNS(commitUrl, 'commitUrl')
50+
if (!urlValidation.isValid) {
51+
return NextResponse.json({ success: false, error: urlValidation.error }, { status: 400 })
52+
}
53+
54+
const response = await secureFetchWithPinnedIP(commitUrl, urlValidation.resolvedIP!, {
55+
method: 'GET',
56+
headers: {
57+
Accept: 'application/vnd.github.v3+json',
58+
Authorization: `Bearer ${apiKey}`,
59+
'X-GitHub-Api-Version': '2022-11-28',
60+
},
61+
})
62+
63+
if (!response.ok) {
64+
const errorData = await response.json().catch(() => ({}))
65+
logger.error(`[${requestId}] GitHub API error`, {
66+
status: response.status,
67+
error: errorData,
68+
})
69+
return NextResponse.json(
70+
{ success: false, error: errorData.message || `GitHub API error: ${response.status}` },
71+
{ status: 400 }
72+
)
73+
}
74+
75+
const data = await response.json()
76+
77+
const content = `Latest commit: "${data.commit.message}" by ${data.commit.author.name} on ${data.commit.author.date}. SHA: ${data.sha}`
78+
79+
const files = data.files || []
80+
const fileDetailsWithContent = []
81+
82+
for (const file of files) {
83+
const fileDetail: Record<string, any> = {
84+
filename: file.filename,
85+
additions: file.additions,
86+
deletions: file.deletions,
87+
changes: file.changes,
88+
status: file.status,
89+
raw_url: file.raw_url,
90+
blob_url: file.blob_url,
91+
patch: file.patch,
92+
content: undefined,
93+
}
94+
95+
if (file.status !== 'removed' && file.raw_url) {
96+
try {
97+
const rawUrlValidation = await validateUrlWithDNS(file.raw_url, 'rawUrl')
98+
if (rawUrlValidation.isValid) {
99+
const contentResponse = await secureFetchWithPinnedIP(
100+
file.raw_url,
101+
rawUrlValidation.resolvedIP!,
102+
{
103+
headers: {
104+
Authorization: `Bearer ${apiKey}`,
105+
'X-GitHub-Api-Version': '2022-11-28',
106+
},
107+
}
108+
)
109+
110+
if (contentResponse.ok) {
111+
fileDetail.content = await contentResponse.text()
112+
}
113+
}
114+
} catch (error) {
115+
logger.warn(`[${requestId}] Failed to fetch content for ${file.filename}:`, error)
116+
}
117+
}
118+
119+
fileDetailsWithContent.push(fileDetail)
120+
}
121+
122+
logger.info(`[${requestId}] Latest commit fetched successfully`, {
123+
sha: data.sha,
124+
fileCount: files.length,
125+
})
126+
127+
return NextResponse.json({
128+
success: true,
129+
output: {
130+
content,
131+
metadata: {
132+
sha: data.sha,
133+
html_url: data.html_url,
134+
commit_message: data.commit.message,
135+
author: {
136+
name: data.commit.author.name,
137+
login: data.author?.login || 'Unknown',
138+
avatar_url: data.author?.avatar_url || '',
139+
html_url: data.author?.html_url || '',
140+
},
141+
committer: {
142+
name: data.commit.committer.name,
143+
login: data.committer?.login || 'Unknown',
144+
avatar_url: data.committer?.avatar_url || '',
145+
html_url: data.committer?.html_url || '',
146+
},
147+
stats: data.stats
148+
? {
149+
additions: data.stats.additions,
150+
deletions: data.stats.deletions,
151+
total: data.stats.total,
152+
}
153+
: undefined,
154+
files: fileDetailsWithContent.length > 0 ? fileDetailsWithContent : undefined,
155+
},
156+
},
157+
})
158+
} catch (error) {
159+
logger.error(`[${requestId}] Error fetching GitHub latest commit:`, error)
160+
return NextResponse.json(
161+
{
162+
success: false,
163+
error: error instanceof Error ? error.message : 'Unknown error occurred',
164+
},
165+
{ status: 500 }
166+
)
167+
}
168+
}

0 commit comments

Comments
 (0)