77import { Octokit } from '@octokit/rest' ;
88import { generateGitHubInstallationToken } from '@/lib/integrations/platforms/github/adapter' ;
99import type { DependabotAlertRaw , DependabotAlertState } from '../core/types' ;
10- import { sentryLogger } from '@/lib/utils.server' ;
10+ import { errorExceptInTest , sentryLogger , warnExceptInTest } from '@/lib/utils.server' ;
1111
1212const log = sentryLogger ( 'security-agent:dependabot-api' , 'info' ) ;
1313const warn = sentryLogger ( 'security-agent:dependabot-api' , 'warning' ) ;
14- const logError = sentryLogger ( 'security-agent:dependabot-api' , 'error' ) ;
1514
1615/**
1716 * Dependabot alert from GitHub API
@@ -109,9 +108,14 @@ export type FetchAlertsResult =
109108 | { status : 'success' ; alerts : DependabotAlertRaw [ ] }
110109 | { status : 'repo_not_found' }
111110 | { status : 'alerts_disabled' }
112- | { status : 'access_blocked' } ;
111+ | { status : 'access_blocked' }
112+ | { status : 'auth_invalid' } ;
113113
114- type FetchAlertsSkipStatus = 'repo_not_found' | 'alerts_disabled' | 'access_blocked' ;
114+ type FetchAlertsSkipStatus =
115+ | 'repo_not_found'
116+ | 'alerts_disabled'
117+ | 'access_blocked'
118+ | 'auth_invalid' ;
115119
116120// Permanent repo-level settings — safe to skip without blocking freshness.
117121const DEPENDABOT_DISABLED_HINTS = [
@@ -134,10 +138,14 @@ function matchesAnyHint(message: string | undefined, hints: readonly string[]):
134138 return hints . some ( hint => normalized . includes ( hint ) ) ;
135139}
136140
137- function classifyFetchAlertsError (
141+ export function classifyFetchAlertsError (
138142 httpStatus ?: number ,
139143 message ?: string
140144) : FetchAlertsSkipStatus | null {
145+ if ( httpStatus === 401 ) {
146+ return 'auth_invalid' ;
147+ }
148+
141149 if ( httpStatus === 404 ) {
142150 return 'repo_not_found' ;
143151 }
@@ -217,6 +225,14 @@ export async function fetchAllDependabotAlerts(
217225 const message = ( error as { message ?: string } ) . message ;
218226 const skipStatus = classifyFetchAlertsError ( httpStatus , message ) ;
219227
228+ if ( skipStatus === 'auth_invalid' ) {
229+ warnExceptInTest ( `GitHub App installation auth invalid for ${ owner } /${ repo } , skipping` , {
230+ status : httpStatus ,
231+ message,
232+ } ) ;
233+ return { status : 'auth_invalid' } ;
234+ }
235+
220236 if ( skipStatus === 'alerts_disabled' ) {
221237 warn ( `Dependabot alerts are disabled for ${ owner } /${ repo } , skipping` , {
222238 status : httpStatus ,
@@ -240,7 +256,7 @@ export async function fetchAllDependabotAlerts(
240256 return { status : 'repo_not_found' } ;
241257 }
242258
243- logError ( `Error fetching alerts for ${ owner } /${ repo } ` , {
259+ errorExceptInTest ( `Error fetching alerts for ${ owner } /${ repo } ` , {
244260 status : httpStatus ,
245261 message,
246262 durationMs : apiDurationMs ,
@@ -258,19 +274,42 @@ export async function fetchOpenDependabotAlerts(
258274 installationId : string ,
259275 owner : string ,
260276 repo : string
261- ) : Promise < DependabotAlertRaw [ ] > {
277+ ) : Promise < FetchAlertsResult > {
262278 const tokenData = await generateGitHubInstallationToken ( installationId ) ;
263279 const octokit = new Octokit ( { auth : tokenData . token } ) ;
264280
265- // Use Octokit's paginate helper which handles cursor-based pagination automatically
266- const data = await octokit . paginate ( octokit . rest . dependabot . listAlertsForRepo , {
267- owner,
268- repo,
269- state : 'open' ,
270- per_page : 100 ,
271- } ) ;
281+ try {
282+ // Use Octokit's paginate helper which handles cursor-based pagination automatically
283+ const data = await octokit . paginate ( octokit . rest . dependabot . listAlertsForRepo , {
284+ owner,
285+ repo,
286+ state : 'open' ,
287+ per_page : 100 ,
288+ } ) ;
272289
273- return data . map ( alert => toInternalAlert ( alert as unknown as GitHubDependabotAlert ) ) ;
290+ return {
291+ status : 'success' ,
292+ alerts : data . map ( alert => toInternalAlert ( alert as unknown as GitHubDependabotAlert ) ) ,
293+ } ;
294+ } catch ( error ) {
295+ const httpStatus = ( error as { status ?: number } ) . status ;
296+ const message = ( error as { message ?: string } ) . message ;
297+ const skipStatus = classifyFetchAlertsError ( httpStatus , message ) ;
298+
299+ if ( skipStatus === 'auth_invalid' ) {
300+ warnExceptInTest ( `GitHub App installation auth invalid for ${ owner } /${ repo } , skipping` , {
301+ status : httpStatus ,
302+ message,
303+ } ) ;
304+ return { status : 'auth_invalid' } ;
305+ }
306+
307+ if ( skipStatus ) {
308+ return { status : skipStatus } ;
309+ }
310+
311+ throw error ;
312+ }
274313}
275314
276315/**
@@ -294,8 +333,16 @@ export async function fetchDependabotAlert(
294333
295334 return toInternalAlert ( data as unknown as GitHubDependabotAlert ) ;
296335 } catch ( error ) {
336+ const status = ( error as { status ?: number } ) . status ;
337+ if ( status === 401 ) {
338+ warnExceptInTest ( `GitHub App installation auth invalid for ${ owner } /${ repo } , skipping` , {
339+ status,
340+ } ) ;
341+ return null ;
342+ }
343+
297344 // Return null if alert not found
298- if ( ( error as { status ?: number } ) . status === 404 ) {
345+ if ( status === 404 ) {
299346 return null ;
300347 }
301348 throw error ;
@@ -365,8 +412,15 @@ export async function isDependabotEnabled(
365412 } ) ;
366413 return true ;
367414 } catch ( error ) {
368- // 403 or 404 typically means Dependabot is not enabled or no access
415+ // 401 means the GitHub App installation needs reauthorization.
416+ // 403 or 404 typically means Dependabot is not enabled or no access.
369417 const status = ( error as { status ?: number } ) . status ;
418+ if ( status === 401 ) {
419+ warnExceptInTest ( `GitHub App installation auth invalid for ${ owner } /${ repo } , skipping` , {
420+ status,
421+ } ) ;
422+ return false ;
423+ }
370424 if ( status === 403 || status === 404 ) {
371425 return false ;
372426 }
0 commit comments