@@ -57,6 +57,7 @@ interface InMemorySession {
5757 orgProjectsMap : OrgProjectsMap ;
5858 currentOrgId : string | null ;
5959 currentProjectId : number | null ;
60+ scopedProjectIds : number [ ] ;
6061 orgProjectsIncomplete : boolean ;
6162}
6263
@@ -638,27 +639,36 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
638639 options : TokenResponseOptions ,
639640 ) : Promise < InMemorySession > {
640641 const scopedOrgIds = tokenResponse . scoped_organizations ?? [ ] ;
641- const { accountKey, currentOrgId } = await this . fetchUserContext (
642- tokenResponse . access_token ,
643- options . cloudRegion ,
644- ) ;
645- // Team-scoped tokens (required_access_level=project) can arrive with an
646- // empty scoped_organizations list — the server only populates scoped_teams.
647- // Fall back to the current org from /api/users/@me / so the picker isn't
648- // empty; without this the user is stranded on "No projects".
649- const orgIdsToFetch =
650- scopedOrgIds . length > 0
651- ? scopedOrgIds
652- : currentOrgId
653- ? [ currentOrgId ]
654- : [ ] ;
655- const { map : orgProjectsMap , incomplete : orgProjectsIncomplete } =
656- await this . buildOrgProjectsMap (
642+ const scopedProjectIds = tokenResponse . scoped_teams ?? [ ] ;
643+ const { accountKey, currentOrgId, currentOrgName } =
644+ await this . fetchUserContext (
657645 tokenResponse . access_token ,
658646 options . cloudRegion ,
659- orgIdsToFetch ,
660- this . session ?. orgProjectsMap ?? { } ,
661647 ) ;
648+ const previousMap = this . session ?. orgProjectsMap ?? { } ;
649+ const { map : orgProjectsMap , incomplete : orgProjectsIncomplete } =
650+ scopedOrgIds . length > 0
651+ ? await this . buildOrgProjectsMap (
652+ tokenResponse . access_token ,
653+ options . cloudRegion ,
654+ scopedOrgIds ,
655+ previousMap ,
656+ )
657+ : scopedProjectIds . length > 0
658+ ? await this . buildScopedProjectMap (
659+ tokenResponse . access_token ,
660+ options . cloudRegion ,
661+ scopedProjectIds ,
662+ currentOrgId ,
663+ currentOrgName ,
664+ previousMap ,
665+ )
666+ : await this . buildOrgProjectsMap (
667+ tokenResponse . access_token ,
668+ options . cloudRegion ,
669+ currentOrgId ? [ currentOrgId ] : [ ] ,
670+ previousMap ,
671+ ) ;
662672 const lastPrefs = accountKey
663673 ? this . authPreference . get ( accountKey , options . cloudRegion )
664674 : null ;
@@ -670,20 +680,110 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
670680 lastSelectedOrgId : lastPrefs ?. lastSelectedOrgId ?? null ,
671681 } ) ;
672682
683+ const resolvedCurrentOrgId =
684+ currentOrgId && orgProjectsMap [ currentOrgId ]
685+ ? currentOrgId
686+ : ( Object . keys ( orgProjectsMap ) [ 0 ] ?? currentOrgId ) ;
673687 const session : InMemorySession = {
674688 accountKey,
675689 accessToken : tokenResponse . access_token ,
676690 accessTokenExpiresAt : Date . now ( ) + tokenResponse . expires_in * 1000 ,
677691 refreshToken : tokenResponse . refresh_token ,
678692 cloudRegion : options . cloudRegion ,
679693 orgProjectsMap,
680- currentOrgId,
694+ currentOrgId : resolvedCurrentOrgId ,
681695 currentProjectId,
696+ scopedProjectIds,
682697 orgProjectsIncomplete,
683698 } ;
684699
685700 return session ;
686701 }
702+ private async buildScopedProjectMap (
703+ accessToken : string ,
704+ cloudRegion : CloudRegion ,
705+ projectIds : number [ ] ,
706+ currentOrgId : string | null ,
707+ currentOrgName : string | null ,
708+ previousMap : OrgProjectsMap ,
709+ ) : Promise < { map : OrgProjectsMap ; incomplete : boolean } > {
710+ let incomplete = false ;
711+ const map : OrgProjectsMap = { } ;
712+
713+ await Promise . all (
714+ projectIds . map ( async ( projectId ) => {
715+ const result = await this . fetchScopedProject (
716+ accessToken ,
717+ cloudRegion ,
718+ projectId ,
719+ ) ;
720+ if ( ! result . ok ) {
721+ incomplete ||= result . retryable ;
722+ return ;
723+ }
724+
725+ const { orgId, project } = result . data ;
726+ const existing = map [ orgId ] ;
727+ map [ orgId ] = {
728+ orgName :
729+ existing ?. orgName ??
730+ ( orgId === currentOrgId ? currentOrgName : null ) ??
731+ previousMap [ orgId ] ?. orgName ??
732+ "(unknown)" ,
733+ projects : [ ...( existing ?. projects ?? [ ] ) , project ] ,
734+ } ;
735+ } ) ,
736+ ) ;
737+
738+ return { map, incomplete } ;
739+ }
740+ private async fetchScopedProject (
741+ accessToken : string ,
742+ cloudRegion : CloudRegion ,
743+ projectId : number ,
744+ ) : Promise <
745+ | {
746+ ok : true ;
747+ data : { orgId : string ; project : { id : number ; name : string } } ;
748+ }
749+ | { ok : false ; retryable : boolean }
750+ > {
751+ try {
752+ const response = await this . executeAuthenticatedFetch (
753+ fetch ,
754+ `${ getCloudUrlFromRegion ( cloudRegion ) } /api/projects/${ projectId } /` ,
755+ { } ,
756+ accessToken ,
757+ ) ;
758+ if ( ! response . ok ) {
759+ return { ok : false , retryable : response . status >= 500 } ;
760+ }
761+ const raw = ( await response . json ( ) . catch ( ( ) => null ) ) as {
762+ id ?: unknown ;
763+ name ?: unknown ;
764+ organization ?: unknown ;
765+ } | null ;
766+ const responseProjectId =
767+ typeof raw ?. id === "number" ? raw . id : projectId ;
768+ const name = typeof raw ?. name === "string" ? raw . name : null ;
769+ const orgId =
770+ typeof raw ?. organization === "string"
771+ ? raw . organization
772+ : typeof raw ?. organization === "object" && raw . organization !== null
773+ ? ( raw . organization as { id ?: unknown } ) . id
774+ : null ;
775+ if ( typeof orgId !== "string" || ! name ) {
776+ return { ok : false , retryable : false } ;
777+ }
778+ return {
779+ ok : true ,
780+ data : { orgId, project : { id : responseProjectId , name } } ,
781+ } ;
782+ } catch ( error ) {
783+ this . logger . warn ( "Failed to fetch scoped project" , { projectId, error } ) ;
784+ return { ok : false , retryable : true } ;
785+ }
786+ }
687787 private async buildOrgProjectsMap (
688788 accessToken : string ,
689789 cloudRegion : CloudRegion ,
@@ -896,7 +996,11 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
896996 private async fetchUserContext (
897997 accessToken : string ,
898998 cloudRegion : CloudRegion ,
899- ) : Promise < { accountKey : string | null ; currentOrgId : string | null } > {
999+ ) : Promise < {
1000+ accountKey : string | null ;
1001+ currentOrgId : string | null ;
1002+ currentOrgName : string | null ;
1003+ } > {
9001004 try {
9011005 const response = await this . executeAuthenticatedFetch (
9021006 fetch ,
@@ -906,14 +1010,18 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
9061010 ) ;
9071011
9081012 if ( ! response . ok ) {
909- return { accountKey : null , currentOrgId : null } ;
1013+ return {
1014+ accountKey : null ,
1015+ currentOrgId : null ,
1016+ currentOrgName : null ,
1017+ } ;
9101018 }
9111019
9121020 const data = ( await response . json ( ) . catch ( ( ) => ( { } ) ) ) as {
9131021 uuid ?: unknown ;
9141022 distinct_id ?: unknown ;
9151023 email ?: unknown ;
916- organization ?: { id ?: unknown } | null ;
1024+ organization ?: { id ?: unknown ; name ?: unknown } | null ;
9171025 } ;
9181026
9191027 let accountKey : string | null = null ;
@@ -931,11 +1039,14 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
9311039 const orgId = data . organization ?. id ;
9321040 const currentOrgId =
9331041 typeof orgId === "string" && orgId . length > 0 ? orgId : null ;
1042+ const orgName = data . organization ?. name ;
1043+ const currentOrgName =
1044+ typeof orgName === "string" && orgName . length > 0 ? orgName : null ;
9341045
935- return { accountKey, currentOrgId } ;
1046+ return { accountKey, currentOrgId, currentOrgName } ;
9361047 } catch ( error ) {
9371048 this . logger . warn ( "Failed to resolve user context" , { error } ) ;
938- return { accountKey : null , currentOrgId : null } ;
1049+ return { accountKey : null , currentOrgId : null , currentOrgName : null } ;
9391050 }
9401051 }
9411052 private requireSession ( ) : InMemorySession {
@@ -1167,12 +1278,23 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
11671278 if ( ! session . orgProjectsIncomplete ) return ;
11681279
11691280 const orgIds = Object . keys ( session . orgProjectsMap ) ;
1170- const { map, incomplete } = await this . buildOrgProjectsMap (
1171- session . accessToken ,
1172- session . cloudRegion ,
1173- orgIds ,
1174- session . orgProjectsMap ,
1175- ) ;
1281+ const { map, incomplete } =
1282+ session . scopedProjectIds . length > 0
1283+ ? await this . buildScopedProjectMap (
1284+ session . accessToken ,
1285+ session . cloudRegion ,
1286+ session . scopedProjectIds ,
1287+ session . currentOrgId ,
1288+ session . orgProjectsMap [ session . currentOrgId ?? "" ] ?. orgName ??
1289+ null ,
1290+ session . orgProjectsMap ,
1291+ )
1292+ : await this . buildOrgProjectsMap (
1293+ session . accessToken ,
1294+ session . cloudRegion ,
1295+ orgIds ,
1296+ session . orgProjectsMap ,
1297+ ) ;
11761298
11771299 // The session may have been replaced (logout, re-login) while the fetch
11781300 // was in flight; committing the stale one would resurrect it.
0 commit comments