@@ -44,46 +44,12 @@ export async function getOSPSBaselineInsights(repoUrl: string, token: string): P
4444
4545 const combinedOutput = `${ stdout } \n${ stderr } `
4646
47- if (
48- combinedOutput . includes ( '401 Unauthorized' ) ||
49- combinedOutput . includes ( '401 Bad credentials' )
50- ) {
51- svc . log . warn ( 'Detected 401 error in privateer output - token invalid or expired!' )
52- throw ApplicationFailure . create ( {
53- message : 'GitHub token invalid or expired' ,
54- type : 'TokenAuthError' ,
55- nonRetryable : true ,
56- } )
57- }
58- if ( combinedOutput . includes ( '403' ) ) {
59- svc . log . warn ( 'Detected 403 error in privateer output - token rate-limited!' )
60- throw ApplicationFailure . create ( {
61- message : 'GitHub token rate-limited' ,
62- type : 'Token403Error' ,
63- nonRetryable : true ,
64- } )
65- }
47+ classifyTokenError ( combinedOutput , 'privateer output' )
6648 } catch ( err ) {
6749 svc . log . error ( `Privateer run failed: ${ err . message } ` )
6850
69- // check for auth errors in captured output if available
7051 const output = `${ err . stdout || '' } \n${ err . stderr || '' } `
71- if ( output . includes ( '401 Unauthorized' ) || output . includes ( '401 Bad credentials' ) ) {
72- svc . log . warn ( 'Detected 401 error in failed privateer output - token invalid or expired!' )
73- throw ApplicationFailure . create ( {
74- message : 'GitHub token invalid or expired' ,
75- type : 'TokenAuthError' ,
76- nonRetryable : true ,
77- } )
78- }
79- if ( output . includes ( '403' ) ) {
80- svc . log . warn ( 'Detected 403 error in failed privateer output - token rate-limited!' )
81- throw ApplicationFailure . create ( {
82- message : 'GitHub token rate-limited' ,
83- type : 'Token403Error' ,
84- nonRetryable : true ,
85- } )
86- }
52+ classifyTokenError ( output , 'failed privateer output' )
8753 throw err
8854 }
8955
@@ -217,6 +183,40 @@ export async function saveOSPSBaselineInsightsToRedis(
217183 await redisCache . set ( key , JSON . stringify ( insights ) , 60 * 60 * 24 ) // 1 day
218184}
219185
186+ // GitHub returns 403 for both rate-limit exhaustion AND persistent permission/SAML failures.
187+ // Only rate-limit responses should trigger the 1h token-rotation cooldown; permission failures
188+ // must be marked as invalid so the token isn't retried in a loop.
189+ function classifyTokenError ( output : string , source : string ) : void {
190+ if ( output . includes ( '401 Unauthorized' ) || output . includes ( '401 Bad credentials' ) ) {
191+ svc . log . warn ( `Detected 401 error in ${ source } - token invalid or expired!` )
192+ throw ApplicationFailure . create ( {
193+ message : 'GitHub token invalid or expired' ,
194+ type : 'TokenAuthError' ,
195+ nonRetryable : true ,
196+ } )
197+ }
198+ if ( ! output . includes ( '403' ) ) return
199+
200+ // Rate-limit 403s from GitHub carry a distinctive body: "API rate limit exceeded" or
201+ // "secondary rate limit". Anything else (SAML enforcement, missing scopes, resource
202+ // not accessible) is a permission problem and the token won't recover.
203+ const isRateLimit = / r a t e l i m i t | r a t e _ l i m i t | s e c o n d a r y r a t e / i. test ( output )
204+ if ( isRateLimit ) {
205+ svc . log . warn ( `Detected 403 rate-limit in ${ source } - token rate-limited!` )
206+ throw ApplicationFailure . create ( {
207+ message : 'GitHub token rate-limited' ,
208+ type : 'Token403Error' ,
209+ nonRetryable : true ,
210+ } )
211+ }
212+ svc . log . warn ( `Detected 403 permission error in ${ source } - token lacks required access!` )
213+ throw ApplicationFailure . create ( {
214+ message : 'GitHub token lacks required permissions' ,
215+ type : 'TokenAuthError' ,
216+ nonRetryable : true ,
217+ } )
218+ }
219+
220220function computeRunDuration ( start : string | undefined , end : string | undefined ) : string {
221221 if ( ! start || ! end ) return ''
222222 const startMs = new Date ( start ) . getTime ( )
0 commit comments