@@ -17,7 +17,7 @@ import type { DeviceAuthentication } from './device-authentication';
1717import { ErrorHandler } from './error-handler' ;
1818import { ExtensionContext } from './extension-context' ;
1919import { Logger } from './logger' ;
20- import { arrayEquals , getMatchingHydrationScopeBundles , hasAllScopes , isUnauthorizedError } from './utils' ;
20+ import { getMatchingHydrationScopeBundles , hasAllScopes , isUnauthorizedError , isWorkspacePatSession , sessionMatchesRequestedScopes , WORKSPACE_PAT_SCOPE } from './utils' ;
2121
2222export interface GithubUser {
2323 login : string ;
@@ -32,6 +32,7 @@ export interface GithubService {
3232 removeDeviceAuthToken ( ) : Promise < void > ;
3333 getUser ( ) : Promise < GithubUser > ;
3434 getTokenScopes ( token : string ) : Promise < string [ ] > ;
35+ isDeviceAuthToken ( ) : Promise < boolean > ;
3536}
3637
3738@injectable ( )
@@ -59,13 +60,28 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
5960 this . deviceAuthentication = deviceAuthentication ;
6061 }
6162
63+ async notifyExistingSessions ( ) : Promise < void > {
64+ const sessions = await this . sessionsPromise ;
65+ if ( sessions . length > 0 ) {
66+ this . logger . info ( `GitHubAuthProvider: notifying about ${ sessions . length } existing session(s)` ) ;
67+ this . sessionChangeEmitter . fire ( { added : sessions , removed : [ ] , changed : [ ] } ) ;
68+ }
69+ }
70+
6271 async hydrateFromK8sToken ( ) : Promise < void > {
6372 let sessions = await this . sessionsPromise ;
6473 if ( sessions . length > 0 ) {
6574 try {
6675 await this . githubService . getTokenScopes ( sessions [ 0 ] . accessToken ) ;
67- this . logger . trace ( 'GitHubAuthProvider: existing session token is valid' ) ;
68- return ;
76+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
77+ const sessionsNeedRetag = isDeviceAuthToken
78+ ? sessions . some ( session => isWorkspacePatSession ( session . scopes ) )
79+ : sessions . some ( session => ! isWorkspacePatSession ( session . scopes ) ) ;
80+ if ( ! sessionsNeedRetag ) {
81+ this . logger . info ( 'GitHubAuthProvider: existing session token is valid' ) ;
82+ return ;
83+ }
84+ this . logger . info ( 'GitHubAuthProvider: re-hydrating sessions to match current token source' ) ;
6985 } catch ( error ) {
7086 if ( isUnauthorizedError ( error ) ) {
7187 this . logger . warn ( 'GitHubAuthProvider: existing session token is not valid, clearing sessions' ) ;
@@ -74,57 +90,71 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
7490 this . sessionChangeEmitter . fire ( { added : [ ] , removed, changed : [ ] } ) ;
7591 sessions = [ ] ;
7692 } else {
77- this . logger . trace ( `GitHubAuthProvider: session validation skipped: ${ ( error as Error ) . message } ` ) ;
93+ this . logger . warn ( `GitHubAuthProvider: session validation skipped: ${ ( error as Error ) . message } ` ) ;
7894 return ;
7995 }
8096 }
8197 }
8298
83- try {
84- const token = await this . githubService . getToken ( ) ;
85- const tokenScopes = await this . githubService . getTokenScopes ( token ) ;
86- if ( tokenScopes . length === 0 ) {
87- this . logger . trace ( 'GitHubAuthProvider: hydrate skipped, token has no scopes' ) ;
88- return ;
89- }
99+ await this . tryHydrate ( 3 , 500 ) ;
100+ }
90101
91- const githubUser = await this . githubService . getUser ( ) ;
92- const matchingBundles = getMatchingHydrationScopeBundles ( tokenScopes ) ;
93- if ( matchingBundles . length === 0 ) {
94- this . logger . trace ( 'GitHubAuthProvider: hydrate skipped, token scopes match no known bundle' ) ;
95- return ;
96- }
102+ private async tryHydrate ( maxAttempts : number , delayMs : number ) : Promise < void > {
103+ for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
104+ try {
105+ const token = await this . githubService . getToken ( ) ;
106+ const tokenScopes = await this . githubService . getTokenScopes ( token ) ;
107+ if ( tokenScopes . length === 0 ) {
108+ this . logger . info ( 'GitHubAuthProvider: hydrate skipped, token has no scopes' ) ;
109+ return ;
110+ }
97111
98- const account = { label : githubUser . login , id : githubUser . id . toString ( ) } ;
99- const hydratedSessions = matchingBundles . map ( scopes => ( {
100- id : v4 ( ) ,
101- accessToken : token ,
102- account,
103- scopes,
104- } ) ) ;
105-
106- await this . storeSessions ( hydratedSessions ) ;
107- this . sessionChangeEmitter . fire ( { added : hydratedSessions , removed : [ ] , changed : [ ] } ) ;
108- this . logger . info ( `GitHubAuthProvider: hydrated ${ hydratedSessions . length } session(s) from K8s token` ) ;
109- } catch ( error ) {
110- if ( isUnauthorizedError ( error ) ) {
111- this . logger . warn ( 'GitHubAuthProvider: hydrate failed, token is not valid' ) ;
112- } else {
113- this . logger . trace ( `GitHubAuthProvider: hydrate skipped: ${ ( error as Error ) . message } ` ) ;
112+ const githubUser = await this . githubService . getUser ( ) ;
113+ const matchingBundles = getMatchingHydrationScopeBundles ( tokenScopes ) ;
114+ if ( matchingBundles . length === 0 ) {
115+ this . logger . info ( 'GitHubAuthProvider: hydrate skipped, token scopes match no known bundle' ) ;
116+ return ;
117+ }
118+
119+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
120+ const account = { label : githubUser . login , id : githubUser . id . toString ( ) } ;
121+ const hydratedSessions = matchingBundles . map ( scopes => ( {
122+ id : v4 ( ) ,
123+ accessToken : token ,
124+ account,
125+ scopes : isDeviceAuthToken ? scopes : [ ...scopes , WORKSPACE_PAT_SCOPE ] ,
126+ } ) ) ;
127+
128+ await this . storeSessions ( hydratedSessions ) ;
129+ this . sessionChangeEmitter . fire ( { added : hydratedSessions , removed : [ ] , changed : [ ] } ) ;
130+ const tokenSource = isDeviceAuthToken ? 'device authentication' : 'workspace PAT' ;
131+ this . logger . info ( `GitHubAuthProvider: hydrated ${ hydratedSessions . length } session(s) from K8s ${ tokenSource } ` ) ;
132+ return ;
133+ } catch ( error ) {
134+ if ( isUnauthorizedError ( error ) ) {
135+ this . logger . warn ( 'GitHubAuthProvider: hydrate failed, token is not valid' ) ;
136+ return ;
137+ }
138+ if ( attempt < maxAttempts ) {
139+ this . logger . info ( `GitHubAuthProvider: hydrate attempt ${ attempt } /${ maxAttempts } failed: ${ ( error as Error ) . message } , retrying in ${ delayMs } ms` ) ;
140+ await new Promise ( resolve => setTimeout ( resolve , delayMs ) ) ;
141+ } else {
142+ this . logger . warn ( `GitHubAuthProvider: hydrate failed after ${ maxAttempts } attempts: ${ ( error as Error ) . message } ` ) ;
143+ }
114144 }
115145 }
116146 }
117147
118148 async getSessions ( sessionScopes ?: string [ ] ) : Promise < vscode . AuthenticationSession [ ] > {
119- this . logger . trace ( `GitHubAuthProvider: GET SESSIONS for scopes: ${ sessionScopes } ` ) ;
149+ this . logger . info ( `GitHubAuthProvider: GET SESSIONS for scopes: ${ sessionScopes } ` ) ;
120150
121151 const sessions = await this . sessionsPromise ;
122152 const sortedScopes = sessionScopes ? [ ...sessionScopes ] . sort ( ) : [ ] ;
123153 const filteredSessions = sortedScopes . length
124- ? sessions . filter ( session => arrayEquals ( [ ... session . scopes ] . sort ( ) , sortedScopes ) )
154+ ? sessions . filter ( session => sessionMatchesRequestedScopes ( session . scopes , sortedScopes ) )
125155 : [ ...sessions ] ;
126156
127- this . logger . trace ( `GitHubAuthProvider: GET sessions - found ${ filteredSessions . length } sessions for scopes: ${ sessionScopes } ` ) ;
157+ this . logger . info ( `GitHubAuthProvider: GET sessions - found ${ filteredSessions . length } sessions for scopes: ${ sessionScopes } ` ) ;
128158 return filteredSessions ;
129159 }
130160
@@ -160,14 +190,15 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
160190 }
161191
162192 const sessions = await this . sessionsPromise ;
193+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
163194 const session : vscode . AuthenticationSession = {
164195 id : v4 ( ) ,
165196 accessToken : token ,
166197 account : { label : githubUser . login , id : githubUser . id . toString ( ) } ,
167- scopes,
198+ scopes : isDeviceAuthToken ? scopes : [ ... scopes , WORKSPACE_PAT_SCOPE ] ,
168199 } ;
169200
170- const sessionIndex = sessions . findIndex ( s => arrayEquals ( [ ... s . scopes ] . sort ( ) , sortedScopes ) ) ;
201+ const sessionIndex = sessions . findIndex ( s => sessionMatchesRequestedScopes ( s . scopes , sortedScopes ) ) ;
171202 const removed : vscode . AuthenticationSession [ ] = [ ] ;
172203 const updatedSessions = [ ...sessions ] ;
173204 if ( sessionIndex > - 1 ) {
@@ -188,15 +219,28 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
188219 const token = await this . githubService . getToken ( ) ;
189220 const existingScopes = await this . githubService . getTokenScopes ( token ) ;
190221 if ( ! hasAllScopes ( existingScopes , sortedScopes ) ) {
191- this . logger . info ( ` GitHubAuthProvider: token lacks required scopes, starting device flow` ) ;
222+ this . logger . info ( ' GitHubAuthProvider: token lacks required scopes, starting device flow' ) ;
192223 return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
193224 }
225+
226+ const isDeviceAuth = await this . githubService . isDeviceAuthToken ( ) ;
227+ if ( ! isDeviceAuth ) {
228+ const sessions = await this . sessionsPromise ;
229+ const hasExistingSession = sessions . some ( s =>
230+ sessionMatchesRequestedScopes ( s . scopes , sortedScopes )
231+ ) ;
232+ if ( hasExistingSession ) {
233+ this . logger . info ( 'GitHubAuthProvider: PAT session already exists for requested scopes, starting device auth flow' ) ;
234+ return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
235+ }
236+ }
237+
194238 return token ;
195239 } catch ( error ) {
196240 if ( isUnauthorizedError ( error ) ) {
197- this . logger . info ( ` GitHubAuthProvider: token is not valid, starting device flow` ) ;
241+ this . logger . info ( ' GitHubAuthProvider: token is not valid, starting device flow' ) ;
198242 } else {
199- this . logger . info ( ` GitHubAuthProvider: no token available, starting device flow` ) ;
243+ this . logger . info ( ' GitHubAuthProvider: no token available, starting device flow' ) ;
200244 }
201245 return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
202246 }
0 commit comments