@@ -154,7 +154,9 @@ export class CodeThreatApiClient {
154154 }
155155
156156 /**
157- * Run scan (synchronous or asynchronous)
157+ * Run scan with client-side polling (serverless-friendly)
158+ * This method always starts an async scan and polls from the client side
159+ * to avoid serverless function timeouts (Netlify/Vercel limits)
158160 */
159161 async runScan ( options : {
160162 repositoryId : string ;
@@ -176,34 +178,101 @@ export class CodeThreatApiClient {
176178 const requestBody : any = {
177179 repositoryId : options . repositoryId ,
178180 organizationSlug : organizationSlug ,
179- scanTypes : options . scanTypes
181+ scanTypes : options . scanTypes ,
182+ wait : false , // ALWAYS false - we do client-side polling to avoid serverless timeouts
180183 } ;
181184
182185 // Only add optional fields if they have values
183186 if ( options . branch ) requestBody . branch = options . branch ;
184- if ( options . wait !== undefined ) requestBody . wait = options . wait ;
185- if ( options . timeout !== undefined ) requestBody . timeout = options . timeout ;
186- if ( options . pollInterval !== undefined ) requestBody . pollInterval = options . pollInterval ;
187187 if ( options . scanTrigger ) requestBody . scanTrigger = options . scanTrigger ;
188188 if ( options . pullRequestId ) requestBody . pullRequestId = options . pullRequestId ;
189189 if ( options . commitSha ) requestBody . commitSha = options . commitSha ;
190190 if ( options . metadata ) requestBody . metadata = options . metadata ;
191191
192-
193192 // Validate organizationSlug is present
194193 if ( ! requestBody . organizationSlug ) {
195194 throw new Error ( 'Organization slug is required. Please set CT_ORG_SLUG environment variable or provide organizationSlug parameter.' ) ;
196195 }
197196
197+ // Start async scan (returns immediately, no serverless timeout)
198198 const response = await this . client . post < ApiResponse < ScanRunResponse > > (
199199 '/api/v1/scans/run' ,
200200 requestBody ,
201201 {
202- timeout : options . wait ? ( options . timeout || 43200 ) * 1000 + 30000 : 30000 , // Default 12 hours for long scans, add 30s buffer for API processing
202+ timeout : 30000 , // 30 seconds - only for starting scan, not waiting
203203 }
204204 ) ;
205205
206- return this . handleResponse ( response ) ;
206+ const scanResponse = this . handleResponse ( response ) ;
207+
208+ // If wait=true, do client-side polling
209+ if ( options . wait ) {
210+ return await this . pollScanCompletion (
211+ scanResponse . scan . id ,
212+ options . timeout || 43200 , // Default 12 hours
213+ options . pollInterval || 30 // Default 30 seconds
214+ ) ;
215+ }
216+
217+ return scanResponse ;
218+ }
219+
220+ /**
221+ * Poll scan status until completion (client-side polling)
222+ * This avoids serverless function timeouts by polling from the client
223+ */
224+ private async pollScanCompletion (
225+ scanId : string ,
226+ timeoutSeconds : number ,
227+ pollIntervalSeconds : number
228+ ) : Promise < ScanRunResponse > {
229+ const startTime = Date . now ( ) ;
230+ const timeoutMs = timeoutSeconds * 1000 ;
231+ const pollIntervalMs = pollIntervalSeconds * 1000 ;
232+
233+ if ( this . config . verbose ) {
234+ console . log ( `⏳ Polling scan ${ scanId } (timeout: ${ timeoutSeconds } s, interval: ${ pollIntervalSeconds } s)` ) ;
235+ }
236+
237+ while ( Date . now ( ) - startTime < timeoutMs ) {
238+ // Get current scan status
239+ const statusResponse = await this . getScanStatus ( scanId , false ) ;
240+
241+ if ( statusResponse . scan . status === 'COMPLETED' ) {
242+ if ( this . config . verbose ) {
243+ console . log ( `✅ Scan completed in ${ Math . round ( ( Date . now ( ) - startTime ) / 1000 ) } s` ) ;
244+ }
245+
246+ // Return full response with results
247+ return {
248+ scan : statusResponse . scan ,
249+ synchronous : true ,
250+ results : {
251+ total : statusResponse . results . violationCount ,
252+ critical : statusResponse . results . summary . critical ,
253+ high : statusResponse . results . summary . high ,
254+ medium : statusResponse . results . summary . medium ,
255+ low : statusResponse . results . summary . low ,
256+ } ,
257+ duration : Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ,
258+ } ;
259+ }
260+
261+ if ( statusResponse . scan . status === 'FAILED' ) {
262+ throw new Error ( `Scan failed during execution` ) ;
263+ }
264+
265+ if ( this . config . verbose ) {
266+ const elapsed = Math . round ( ( Date . now ( ) - startTime ) / 1000 ) ;
267+ console . log ( `⏳ Scan status: ${ statusResponse . scan . status } (${ elapsed } s elapsed)` ) ;
268+ }
269+
270+ // Wait before next poll
271+ await new Promise ( resolve => setTimeout ( resolve , pollIntervalMs ) ) ;
272+ }
273+
274+ // Timeout reached
275+ throw new Error ( `Scan timeout after ${ timeoutSeconds } seconds. Scan ID: ${ scanId } ` ) ;
207276 }
208277
209278 /**
0 commit comments