@@ -436,6 +436,64 @@ function setupTabs(): void {
436436 } ) ;
437437}
438438
439+ function toSafeNumber ( value : unknown ) : number {
440+ const n = Number ( value ) ;
441+ return Number . isFinite ( n ) && n >= 0 ? n : 0 ;
442+ }
443+
444+ function toSafeHttpUrl ( value : unknown ) : string {
445+ const raw = typeof value === 'string' ? value . trim ( ) : '' ;
446+ try {
447+ const parsed = new URL ( raw ) ;
448+ if ( parsed . protocol === 'http:' || parsed . protocol === 'https:' ) {
449+ return parsed . toString ( ) ;
450+ }
451+ } catch {
452+ // Ignore invalid URL and fall back to placeholder.
453+ }
454+ return '#' ;
455+ }
456+
457+ function sanitizeRepoPrStatsData ( input : unknown ) : RepoPrStatsResult {
458+ const src = ( input && typeof input === 'object' ) ? ( input as Record < string , unknown > ) : { } ;
459+ const repos = Array . isArray ( src . repos ) ? src . repos : [ ] ;
460+ return {
461+ authenticated : Boolean ( src . authenticated ) ,
462+ since : typeof src . since === 'string' || typeof src . since === 'number' ? src . since : Date . now ( ) ,
463+ repos : repos . map ( ( repo ) => {
464+ const r = ( repo && typeof repo === 'object' ) ? ( repo as Record < string , unknown > ) : { } ;
465+ const aiDetails = Array . isArray ( r . aiDetails ) ? r . aiDetails : [ ] ;
466+ return {
467+ repoUrl : toSafeHttpUrl ( r . repoUrl ) ,
468+ owner : escapeHtml ( typeof r . owner === 'string' ? r . owner : '' ) ,
469+ repo : escapeHtml ( typeof r . repo === 'string' ? r . repo : '' ) ,
470+ error : typeof r . error === 'string' ? escapeHtml ( r . error ) : '' ,
471+ totalPrs : toSafeNumber ( r . totalPrs ) ,
472+ aiAuthoredPrs : toSafeNumber ( r . aiAuthoredPrs ) ,
473+ aiReviewRequestedPrs : toSafeNumber ( r . aiReviewRequestedPrs ) ,
474+ aiDetails : aiDetails . map ( ( d ) => {
475+ const detail = ( d && typeof d === 'object' ) ? ( d as Record < string , unknown > ) : { } ;
476+ const validAiTypes = [ 'copilot' , 'claude' , 'openai' , 'other-ai' ] as const ;
477+ const validRoles = [ 'author' , 'reviewer-requested' ] as const ;
478+ const aiType = validAiTypes . includes ( detail . aiType as typeof validAiTypes [ number ] )
479+ ? detail . aiType as typeof validAiTypes [ number ]
480+ : 'other-ai' ;
481+ const role = validRoles . includes ( detail . role as typeof validRoles [ number ] )
482+ ? detail . role as typeof validRoles [ number ]
483+ : 'author' ;
484+ return {
485+ number : toSafeNumber ( detail . number ) ,
486+ title : escapeHtml ( typeof detail . title === 'string' ? detail . title : '' ) ,
487+ url : toSafeHttpUrl ( detail . url ) ,
488+ aiType,
489+ role,
490+ } ;
491+ } ) ,
492+ } ;
493+ } ) ,
494+ } as RepoPrStatsResult ;
495+ }
496+
439497function renderReposPrContent ( data : RepoPrStatsResult ) : string {
440498 const sinceDate = escapeHtml ( new Date ( data . since ) . toLocaleDateString ( ) ) ;
441499 if ( ! data . authenticated ) {
@@ -1356,7 +1414,7 @@ window.addEventListener('message', (event) => {
13561414 break ;
13571415 }
13581416 case 'repoPrStatsLoaded' : {
1359- repoPrStatsData = message . data as RepoPrStatsResult ;
1417+ repoPrStatsData = sanitizeRepoPrStatsData ( message . data ) ;
13601418 // Reset the loaded flag when not authenticated so re-authenticating and clicking the tab
13611419 // again triggers a fresh fetch instead of showing the stale "not authenticated" placeholder.
13621420 if ( ! repoPrStatsData . authenticated ) {
0 commit comments