@@ -53,6 +53,7 @@ type ProgramStats struct {
5353
5454type programScopeRequest struct {
5555 Programs []programScopeRequestItem `json:"programs"`
56+ Force bool `json:"force"` // bypass the scope cache (manual Refresh)
5657}
5758
5859type programScopeRequestItem struct {
@@ -270,11 +271,13 @@ func apiProgramScopeSummaries(c *gin.Context) {
270271 }
271272
272273 cacheKey := programScopeCacheKey (item .Platform , item .Handle )
273- if summary , ok := getCachedProgramScope (cacheKey ); ok {
274- mu .Lock ()
275- summaries [cacheKey ] = summary
276- mu .Unlock ()
277- continue
274+ if ! req .Force {
275+ if summary , ok := getCachedProgramScope (cacheKey ); ok {
276+ mu .Lock ()
277+ summaries [cacheKey ] = summary
278+ mu .Unlock ()
279+ continue
280+ }
278281 }
279282
280283 wg .Add (1 )
@@ -288,8 +291,7 @@ func apiProgramScopeSummaries(c *gin.Context) {
288291 switch item .Platform {
289292 case "h1" , "hackerone" :
290293 if h1Auth != "" {
291- summary = fetchH1ScopeSummary (item .Handle , h1Auth )
292- fetched = true
294+ summary , fetched = fetchH1ScopeSummary (item .Handle , h1Auth )
293295 }
294296 case "bc" , "bugcrowd" :
295297 if bcToken != "" {
@@ -300,6 +302,8 @@ func apiProgramScopeSummaries(c *gin.Context) {
300302
301303 summary .Platform = item .Platform
302304 summary .Handle = item .Handle
305+ // Only cache a successful fetch — caching a rate-limited/empty result would
306+ // hide the program's real scope until the TTL expires.
303307 if fetched {
304308 setCachedProgramScope (cacheKey , summary )
305309 }
@@ -476,33 +480,45 @@ func enrichH1ScopeCounts(programs []ProgramSummary, auth string) {
476480 defer wg .Done ()
477481 defer func () { <- sem }()
478482
479- summary := fetchH1ScopeSummary (p .Handle , auth )
480- p .ScopeTargets = summary .ScopeTargets
481- p .LatestTarget = summary .LatestTarget
482- p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
483- p .LatestTargetBrief = summary .LatestTargetBrief
483+ summary , ok := fetchH1ScopeSummary (p .Handle , auth )
484+ if ok {
485+ p .ScopeTargets = summary .ScopeTargets
486+ p .LatestTarget = summary .LatestTarget
487+ p .LatestTargetUpdatedAt = summary .LatestTargetUpdatedAt
488+ p .LatestTargetBrief = summary .LatestTargetBrief
489+ }
484490 }(& programs [i ])
485491 }
486492 wg .Wait ()
487493}
488494
489- func fetchH1ScopeSummary (handle , auth string ) ProgramSummary {
495+ // fetchH1ScopeSummary returns the scope summary and an ok flag. ok is false when the
496+ // request fails or H1 returns a non-200 (e.g. 429 rate-limit / 403) — the caller must
497+ // NOT cache a !ok result, otherwise a transient failure would hide a program's real
498+ // scope (showing "—") for the whole cache TTL.
499+ func fetchH1ScopeSummary (handle , auth string ) (ProgramSummary , bool ) {
490500 url := fmt .Sprintf ("https://api.hackerone.com/v1/hackers/programs/%s/structured_scopes?page%%5Bsize%%5D=100" , handle )
491501 req , err := http .NewRequest ("GET" , url , nil )
492502 if err != nil {
493- return ProgramSummary {}
503+ return ProgramSummary {}, false
494504 }
495505 req .Header .Set ("Accept" , "application/json" )
496506 req .Header .Set ("Authorization" , "Basic " + auth )
497507
498508 client := & http.Client {Timeout : 15 * time .Second }
499509 resp , err := client .Do (req )
500510 if err != nil {
501- return ProgramSummary {}
511+ return ProgramSummary {}, false
502512 }
503513 body , _ := io .ReadAll (resp .Body )
504514 resp .Body .Close ()
505515
516+ if resp .StatusCode != http .StatusOK {
517+ // 429 (rate-limited by hammering the API), 403, 401, etc. — signal failure so
518+ // the empty result isn't cached and the next refresh retries.
519+ return ProgramSummary {}, false
520+ }
521+
506522 scopes := gjson .Get (string (body ), "data" )
507523 summary := ProgramSummary {Platform : "h1" , Handle : handle }
508524 for _ , s := range scopes .Array () {
@@ -518,7 +534,7 @@ func fetchH1ScopeSummary(handle, auth string) ProgramSummary {
518534 }
519535 }
520536 }
521- return summary
537+ return summary , true
522538}
523539
524540// ─────────────────────────────────────────────────────────────────────────────
0 commit comments