Skip to content

Commit 0b1feb5

Browse files
committed
fix(ci): revert client-bundled tools to avoid .server import in client
1 parent e8641d6 commit 0b1feb5

5 files changed

Lines changed: 46 additions & 92 deletions

File tree

apps/sim/app/api/tools/agiloft/attach/route.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { agiloftAttachContract } from '@/lib/api/contracts/tools/agiloft'
55
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateAgiloftInstanceUrl } from '@/lib/core/security/input-validation'
8-
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
7+
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
98
import { generateRequestId } from '@/lib/core/utils/request'
109
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1110
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
@@ -70,26 +69,26 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7069
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
7170
const resolvedFileName = data.fileName || userFile.name || 'attachment'
7271

73-
const surfaceCheck = validateAgiloftInstanceUrl(data.instanceUrl)
74-
if (!surfaceCheck.isValid) {
72+
const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl')
73+
if (!urlValidation.isValid) {
7574
logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, {
7675
instanceUrl: data.instanceUrl,
7776
})
7877
return NextResponse.json(
79-
{ success: false, error: surfaceCheck.error || 'Invalid instance URL' },
78+
{ success: false, error: urlValidation.error || 'Invalid instance URL' },
8079
{ status: 400 }
8180
)
8281
}
8382

84-
const { token, resolvedIP } = await agiloftLogin(data)
83+
const token = await agiloftLogin(data)
8584
const base = data.instanceUrl.replace(/\/$/, '')
8685

8786
try {
8887
const url = buildAttachFileUrl(base, data, resolvedFileName)
8988

9089
logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`)
9190

92-
const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, {
91+
const agiloftResponse = await fetch(url, {
9392
method: 'PUT',
9493
headers: {
9594
'Content-Type': 'application/octet-stream',
@@ -133,7 +132,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
133132
},
134133
})
135134
} finally {
136-
await agiloftLogout(data.instanceUrl, data.knowledgeBase, token, resolvedIP)
135+
await agiloftLogout(data.instanceUrl, data.knowledgeBase, token)
137136
}
138137
} catch (error) {
139138
logger.error(`[${requestId}] Error attaching file to Agiloft:`, error)

apps/sim/app/api/tools/agiloft/retrieve/route.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { agiloftRetrieveContract } from '@/lib/api/contracts/tools/agiloft'
55
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateAgiloftInstanceUrl } from '@/lib/core/security/input-validation'
8-
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
7+
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
98
import { generateRequestId } from '@/lib/core/utils/request'
109
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1110
import { agiloftLogin, agiloftLogout, buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils'
@@ -49,18 +48,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4948
if (!parsed.success) return parsed.response
5049
const data = parsed.data.body
5150

52-
const surfaceCheck = validateAgiloftInstanceUrl(data.instanceUrl)
53-
if (!surfaceCheck.isValid) {
51+
const urlValidation = await validateUrlWithDNS(data.instanceUrl, 'instanceUrl')
52+
if (!urlValidation.isValid) {
5453
logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, {
5554
instanceUrl: data.instanceUrl,
5655
})
5756
return NextResponse.json(
58-
{ success: false, error: surfaceCheck.error || 'Invalid instance URL' },
57+
{ success: false, error: urlValidation.error || 'Invalid instance URL' },
5958
{ status: 400 }
6059
)
6160
}
6261

63-
const { token, resolvedIP } = await agiloftLogin(data)
62+
const token = await agiloftLogin(data)
6463
const base = data.instanceUrl.replace(/\/$/, '')
6564

6665
try {
@@ -72,7 +71,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7271
position: data.position,
7372
})
7473

75-
const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, {
74+
const agiloftResponse = await fetch(url, {
7675
method: 'GET',
7776
headers: {
7877
Authorization: `Bearer ${token}`,
@@ -124,7 +123,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
124123
},
125124
})
126125
} finally {
127-
await agiloftLogout(data.instanceUrl, data.knowledgeBase, token, resolvedIP)
126+
await agiloftLogout(data.instanceUrl, data.knowledgeBase, token)
128127
}
129128
} catch (error) {
130129
logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error)

apps/sim/tools/agiloft/utils.ts

Lines changed: 25 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { createLogger } from '@sim/logger'
2-
import { toError } from '@sim/utils/errors'
3-
import { validateAgiloftInstanceUrl } from '@/lib/core/security/input-validation'
4-
import {
5-
type SecureFetchResponse,
6-
secureFetchWithPinnedIP,
7-
validateUrlWithDNS,
8-
} from '@/lib/core/security/input-validation.server'
2+
import { validateExternalUrl } from '@/lib/core/security/input-validation'
93
import type {
104
AgiloftAttachmentInfoParams,
115
AgiloftBaseParams,
@@ -27,69 +21,40 @@ interface AgiloftRequestConfig {
2721
url: string
2822
method: HttpMethod
2923
headers?: Record<string, string>
30-
body?: string | Buffer | Uint8Array
31-
}
32-
33-
/**
34-
* Result of a successful Agiloft authentication. The `resolvedIP` is the
35-
* DNS-resolved address of the instance and MUST be reused (via
36-
* `secureFetchWithPinnedIP`) for every follow-up request to defeat DNS
37-
* rebinding between the validation step and the actual call.
38-
*/
39-
export interface AgiloftSession {
40-
token: string
41-
resolvedIP: string
42-
}
43-
44-
/**
45-
* Validates the user-supplied Agiloft instance URL with DNS resolution so
46-
* SSRF protections cannot be bypassed by hostnames that resolve to internal
47-
* addresses (or that flip after a sync-only check).
48-
*/
49-
async function validateInstanceUrl(instanceUrl: string): Promise<string> {
50-
const surfaceCheck = validateAgiloftInstanceUrl(instanceUrl)
51-
if (!surfaceCheck.isValid) {
52-
throw new Error(`Invalid Agiloft instance URL: ${surfaceCheck.error}`)
53-
}
54-
55-
const dnsCheck = await validateUrlWithDNS(instanceUrl, 'instanceUrl')
56-
if (!dnsCheck.isValid || !dnsCheck.resolvedIP) {
57-
throw new Error(`Invalid Agiloft instance URL: ${dnsCheck.error ?? 'unresolved hostname'}`)
58-
}
59-
60-
return dnsCheck.resolvedIP
24+
body?: BodyInit
6125
}
6226

6327
/**
6428
* Exchanges login/password for a short-lived Bearer token via EWLogin.
65-
*
66-
* Returns the token alongside the DNS-resolved IP of the instance so callers
67-
* can pin every subsequent request to the same address.
6829
*/
69-
async function agiloftLogin(params: AgiloftBaseParams): Promise<AgiloftSession> {
70-
const resolvedIP = await validateInstanceUrl(params.instanceUrl)
30+
async function agiloftLogin(params: AgiloftBaseParams): Promise<string> {
7131
const base = params.instanceUrl.replace(/\/$/, '')
7232

33+
const urlValidation = validateExternalUrl(params.instanceUrl, 'instanceUrl')
34+
if (!urlValidation.isValid) {
35+
throw new Error(`Invalid Agiloft instance URL: ${urlValidation.error}`)
36+
}
37+
7338
const kb = encodeURIComponent(params.knowledgeBase)
7439
const login = encodeURIComponent(params.login)
7540
const password = encodeURIComponent(params.password)
7641

7742
const url = `${base}/ewws/EWLogin?$KB=${kb}&$login=${login}&$password=${password}`
78-
const response = await secureFetchWithPinnedIP(url, resolvedIP, { method: 'POST' })
43+
const response = await fetch(url, { method: 'POST' })
7944

8045
if (!response.ok) {
8146
const errorText = await response.text()
8247
throw new Error(`Agiloft login failed: ${response.status} - ${errorText}`)
8348
}
8449

85-
const data = (await response.json()) as { access_token?: string }
50+
const data = await response.json()
8651
const token = data.access_token
8752

8853
if (!token) {
8954
throw new Error('Agiloft login did not return an access token')
9055
}
9156

92-
return { token, resolvedIP }
57+
return token
9358
}
9459

9560
/**
@@ -98,41 +63,41 @@ async function agiloftLogin(params: AgiloftBaseParams): Promise<AgiloftSession>
9863
async function agiloftLogout(
9964
instanceUrl: string,
10065
knowledgeBase: string,
101-
token: string,
102-
resolvedIP: string
66+
token: string
10367
): Promise<void> {
10468
try {
10569
const base = instanceUrl.replace(/\/$/, '')
10670
const kb = encodeURIComponent(knowledgeBase)
107-
await secureFetchWithPinnedIP(`${base}/ewws/EWLogout?$KB=${kb}`, resolvedIP, {
71+
await fetch(`${base}/ewws/EWLogout?$KB=${kb}`, {
10872
method: 'POST',
10973
headers: { Authorization: `Bearer ${token}` },
11074
})
11175
} catch (error) {
112-
logger.warn('Agiloft logout failed (best-effort)', { error: toError(error).message })
76+
logger.warn('Agiloft logout failed (best-effort)', { error })
11377
}
11478
}
11579

11680
/**
11781
* Shared wrapper that handles the full auth lifecycle:
118-
* 1. Validate the instance URL (with DNS resolution) and login to get a Bearer token
119-
* 2. Execute the request against the pinned IP with the token
120-
* 3. Logout to clean up the session (also pinned)
82+
* 1. Login to get Bearer token
83+
* 2. Execute the request with the token
84+
* 3. Logout to clean up the session
12185
*
122-
* Every fetch is performed via `secureFetchWithPinnedIP`, so DNS rebinding
123-
* between validation and the subsequent calls is not possible.
86+
* The `buildRequest` callback receives the token and base URL, and returns
87+
* the request config. The `transformResponse` callback converts the raw
88+
* Response into the tool's output format.
12489
*/
12590
export async function executeAgiloftRequest<R extends ToolResponse>(
12691
params: AgiloftBaseParams,
12792
buildRequest: (base: string) => AgiloftRequestConfig,
128-
transformResponse: (response: SecureFetchResponse) => Promise<R>
93+
transformResponse: (response: Response) => Promise<R>
12994
): Promise<R> {
130-
const { token, resolvedIP } = await agiloftLogin(params)
95+
const token = await agiloftLogin(params)
13196
const base = params.instanceUrl.replace(/\/$/, '')
13297

13398
try {
13499
const req = buildRequest(base)
135-
const response = await secureFetchWithPinnedIP(req.url, resolvedIP, {
100+
const response = await fetch(req.url, {
136101
method: req.method,
137102
headers: {
138103
...req.headers,
@@ -142,14 +107,12 @@ export async function executeAgiloftRequest<R extends ToolResponse>(
142107
})
143108
return await transformResponse(response)
144109
} finally {
145-
await agiloftLogout(params.instanceUrl, params.knowledgeBase, token, resolvedIP)
110+
await agiloftLogout(params.instanceUrl, params.knowledgeBase, token)
146111
}
147112
}
148113

149114
/**
150-
* Login helper exported for use in the attach/retrieve API routes. The route
151-
* is responsible for using the returned `resolvedIP` with
152-
* `secureFetchWithPinnedIP` (or `agiloftLogout`) on every follow-up request.
115+
* Login helper exported for use in the attach file API route.
153116
*/
154117
export { agiloftLogin, agiloftLogout }
155118

apps/sim/tools/grafana/update_alert_rule.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
21
import type { GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types'
32
import type { ToolConfig, ToolResponse } from '@/tools/types'
43

@@ -189,14 +188,13 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
189188
headers['X-Grafana-Org-Id'] = params.organizationId
190189
}
191190

192-
const updateResponse = await secureFetchWithValidation(
191+
const updateResponse = await fetch(
193192
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`,
194193
{
195194
method: 'PUT',
196195
headers,
197196
body: JSON.stringify(updatedRule),
198-
},
199-
'baseUrl'
197+
}
200198
)
201199

202200
if (!updateResponse.ok) {

apps/sim/tools/grafana/update_dashboard.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server'
21
import type { GrafanaUpdateDashboardParams } from '@/tools/grafana/types'
32
import type { ToolConfig, ToolResponse } from '@/tools/types'
43

@@ -183,15 +182,11 @@ export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolR
183182
headers['X-Grafana-Org-Id'] = params.organizationId
184183
}
185184

186-
const updateResponse = await secureFetchWithValidation(
187-
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`,
188-
{
189-
method: 'POST',
190-
headers,
191-
body: JSON.stringify(body),
192-
},
193-
'baseUrl'
194-
)
185+
const updateResponse = await fetch(`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`, {
186+
method: 'POST',
187+
headers,
188+
body: JSON.stringify(body),
189+
})
195190

196191
if (!updateResponse.ok) {
197192
const errorText = await updateResponse.text()

0 commit comments

Comments
 (0)