11import { createLogger } from '@sim/logger'
2+ import { validateExternalUrl } from '@/lib/core/security/input-validation'
23import type {
34 AgiloftAttachmentInfoParams ,
45 AgiloftBaseParams ,
@@ -13,76 +14,39 @@ import type {
1314} from '@/tools/agiloft/types'
1415import type { HttpMethod , ToolResponse } from '@/tools/types'
1516
16- /**
17- * Mirrors the shape of SecureFetchResponse from input-validation.server.ts.
18- * Defined locally to avoid importing the .server module into client bundles
19- * (it pulls in dns/promises which is Node-only).
20- */
21- interface SecureFetchResponse {
22- ok : boolean
23- status : number
24- statusText : string
25- headers : { get ( name : string ) : string | null }
26- text : ( ) => Promise < string >
27- json : ( ) => Promise < unknown >
28- arrayBuffer : ( ) => Promise < ArrayBuffer >
29- }
30-
3117const logger = createLogger ( 'AgiloftAuth' )
3218
33- /**
34- * Lazily imports server-only security functions to avoid pulling `dns/promises`
35- * into client bundles (this file is reachable from tools/registry.ts).
36- */
37- async function getServerSecurity ( ) {
38- const mod = await import ( '@/lib/core/security/input-validation.server' )
39- return {
40- secureFetchWithPinnedIP : mod . secureFetchWithPinnedIP ,
41- validateUrlWithDNS : mod . validateUrlWithDNS ,
42- }
43- }
44-
4519interface AgiloftRequestConfig {
4620 url : string
4721 method : HttpMethod
4822 headers ?: Record < string , string >
4923 body ?: BodyInit
5024}
5125
52- /**
53- * Validates the instance URL via DNS resolution and returns the resolved IP
54- * for use with pinned fetches to prevent SSRF via DNS rebinding.
55- */
56- async function validateInstanceUrl ( instanceUrl : string ) : Promise < string > {
57- const { validateUrlWithDNS } = await getServerSecurity ( )
58- const validation = await validateUrlWithDNS ( instanceUrl , 'instanceUrl' )
59- if ( ! validation . isValid ) {
60- throw new Error ( `Invalid Agiloft instance URL: ${ validation . error } ` )
61- }
62- return validation . resolvedIP !
63- }
64-
6526/**
6627 * Exchanges login/password for a short-lived Bearer token via EWLogin.
67- * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding.
6828 */
69- async function agiloftLogin ( params : AgiloftBaseParams , resolvedIP : string ) : Promise < string > {
29+ async function agiloftLogin ( params : AgiloftBaseParams ) : Promise < string > {
7030 const base = params . instanceUrl . replace ( / \/ $ / , '' )
7131
32+ const urlValidation = validateExternalUrl ( params . instanceUrl , 'instanceUrl' )
33+ if ( ! urlValidation . isValid ) {
34+ throw new Error ( `Invalid Agiloft instance URL: ${ urlValidation . error } ` )
35+ }
36+
7237 const kb = encodeURIComponent ( params . knowledgeBase )
7338 const login = encodeURIComponent ( params . login )
7439 const password = encodeURIComponent ( params . password )
7540
7641 const url = `${ base } /ewws/EWLogin?$KB=${ kb } &$login=${ login } &$password=${ password } `
77- const { secureFetchWithPinnedIP } = await getServerSecurity ( )
78- const response = await secureFetchWithPinnedIP ( url , resolvedIP , { method : 'POST' } )
42+ const response = await fetch ( url , { method : 'POST' } )
7943
8044 if ( ! response . ok ) {
8145 const errorText = await response . text ( )
8246 throw new Error ( `Agiloft login failed: ${ response . status } - ${ errorText } ` )
8347 }
8448
85- const data = ( await response . json ( ) ) as { access_token ?: string }
49+ const data = await response . json ( )
8650 const token = data . access_token
8751
8852 if ( ! token ) {
@@ -94,19 +58,16 @@ async function agiloftLogin(params: AgiloftBaseParams, resolvedIP: string): Prom
9458
9559/**
9660 * Cleans up the server session. Best-effort — failures are logged but not thrown.
97- * Uses DNS-pinned fetch to prevent SSRF via DNS rebinding.
9861 */
9962async function agiloftLogout (
10063 instanceUrl : string ,
10164 knowledgeBase : string ,
102- token : string ,
103- resolvedIP : string
65+ token : string
10466) : Promise < void > {
10567 try {
10668 const base = instanceUrl . replace ( / \/ $ / , '' )
10769 const kb = encodeURIComponent ( knowledgeBase )
108- const { secureFetchWithPinnedIP } = await getServerSecurity ( )
109- await secureFetchWithPinnedIP ( `${ base } /ewws/EWLogout?$KB=${ kb } ` , resolvedIP , {
70+ await fetch ( `${ base } /ewws/EWLogout?$KB=${ kb } ` , {
11071 method : 'POST' ,
11172 headers : { Authorization : `Bearer ${ token } ` } ,
11273 } )
@@ -117,43 +78,42 @@ async function agiloftLogout(
11778
11879/**
11980 * Shared wrapper that handles the full auth lifecycle:
120- * 1. Validate instance URL via DNS resolution
121- * 2. Login to get Bearer token (using pinned IP)
122- * 3. Execute the request with the token (using pinned IP)
123- * 4. Logout to clean up the session (using pinned IP)
81+ * 1. Login to get Bearer token
82+ * 2. Execute the request with the token
83+ * 3. Logout to clean up the session
12484 *
125- * All HTTP requests use the resolved IP to prevent SSRF via DNS rebinding.
85+ * The `buildRequest` callback receives the token and base URL, and returns
86+ * the request config. The `transformResponse` callback converts the raw
87+ * Response into the tool's output format.
12688 */
12789export async function executeAgiloftRequest < R extends ToolResponse > (
12890 params : AgiloftBaseParams ,
12991 buildRequest : ( base : string ) => AgiloftRequestConfig ,
130- transformResponse : ( response : SecureFetchResponse ) => Promise < R >
92+ transformResponse : ( response : Response ) => Promise < R >
13193) : Promise < R > {
132- const resolvedIP = await validateInstanceUrl ( params . instanceUrl )
133- const token = await agiloftLogin ( params , resolvedIP )
94+ const token = await agiloftLogin ( params )
13495 const base = params . instanceUrl . replace ( / \/ $ / , '' )
13596
13697 try {
13798 const req = buildRequest ( base )
138- const { secureFetchWithPinnedIP } = await getServerSecurity ( )
139- const response = await secureFetchWithPinnedIP ( req . url , resolvedIP , {
99+ const response = await fetch ( req . url , {
140100 method : req . method ,
141101 headers : {
142102 ...req . headers ,
143103 Authorization : `Bearer ${ token } ` ,
144104 } ,
145- body : req . body as string | Buffer | Uint8Array | undefined ,
105+ body : req . body ,
146106 } )
147107 return await transformResponse ( response )
148108 } finally {
149- await agiloftLogout ( params . instanceUrl , params . knowledgeBase , token , resolvedIP )
109+ await agiloftLogout ( params . instanceUrl , params . knowledgeBase , token )
150110 }
151111}
152112
153113/**
154114 * Login helper exported for use in the attach file API route.
155115 */
156- export { agiloftLogin , agiloftLogout , validateInstanceUrl }
116+ export { agiloftLogin , agiloftLogout }
157117
158118/** URL builders (credential-free -- auth is via Bearer token header) */
159119
0 commit comments