@@ -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 ( )
@@ -64,8 +65,15 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
6465 if ( sessions . length > 0 ) {
6566 try {
6667 await this . githubService . getTokenScopes ( sessions [ 0 ] . accessToken ) ;
67- this . logger . trace ( 'GitHubAuthProvider: existing session token is valid' ) ;
68- return ;
68+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
69+ const sessionsNeedRetag = isDeviceAuthToken
70+ ? sessions . some ( session => isWorkspacePatSession ( session . scopes ) )
71+ : sessions . some ( session => ! isWorkspacePatSession ( session . scopes ) ) ;
72+ if ( ! sessionsNeedRetag ) {
73+ this . logger . info ( 'GitHubAuthProvider: existing session token is valid' ) ;
74+ return ;
75+ }
76+ this . logger . info ( 'GitHubAuthProvider: re-hydrating sessions to match current token source' ) ;
6977 } catch ( error ) {
7078 if ( isUnauthorizedError ( error ) ) {
7179 this . logger . warn ( 'GitHubAuthProvider: existing session token is not valid, clearing sessions' ) ;
@@ -74,57 +82,80 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
7482 this . sessionChangeEmitter . fire ( { added : [ ] , removed, changed : [ ] } ) ;
7583 sessions = [ ] ;
7684 } else {
77- this . logger . trace ( `GitHubAuthProvider: session validation skipped: ${ ( error as Error ) . message } ` ) ;
85+ this . logger . warn ( `GitHubAuthProvider: session validation skipped: ${ ( error as Error ) . message } ` ) ;
7886 return ;
7987 }
8088 }
8189 }
8290
91+ await this . doHydrate ( ) ;
92+ }
93+
94+ private async waitForToken ( timeoutMs : number , intervalMs : number ) : Promise < string | undefined > {
95+ const deadline = Date . now ( ) + timeoutMs ;
96+ while ( Date . now ( ) < deadline ) {
97+ try {
98+ return await this . githubService . getToken ( ) ;
99+ } catch {
100+ await new Promise ( resolve => setTimeout ( resolve , intervalMs ) ) ;
101+ }
102+ }
103+ return undefined ;
104+ }
105+
106+ private async doHydrate ( ) : Promise < void > {
107+ const token = await this . waitForToken ( 30000 , 500 ) ;
108+ if ( ! token ) {
109+ this . logger . warn ( 'GitHubAuthProvider: hydrate failed, token not available after 30s' ) ;
110+ return ;
111+ }
112+
83113 try {
84- const token = await this . githubService . getToken ( ) ;
85114 const tokenScopes = await this . githubService . getTokenScopes ( token ) ;
86115 if ( tokenScopes . length === 0 ) {
87- this . logger . trace ( 'GitHubAuthProvider: hydrate skipped, token has no scopes' ) ;
116+ this . logger . info ( 'GitHubAuthProvider: hydrate skipped, token has no scopes' ) ;
88117 return ;
89118 }
90119
91120 const githubUser = await this . githubService . getUser ( ) ;
92121 const matchingBundles = getMatchingHydrationScopeBundles ( tokenScopes ) ;
93122 if ( matchingBundles . length === 0 ) {
94- this . logger . trace ( 'GitHubAuthProvider: hydrate skipped, token scopes match no known bundle' ) ;
123+ this . logger . info ( 'GitHubAuthProvider: hydrate skipped, token scopes match no known bundle' ) ;
95124 return ;
96125 }
97126
127+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
98128 const account = { label : githubUser . login , id : githubUser . id . toString ( ) } ;
99129 const hydratedSessions = matchingBundles . map ( scopes => ( {
100130 id : v4 ( ) ,
101131 accessToken : token ,
102132 account,
103- scopes,
133+ scopes : isDeviceAuthToken ? scopes : [ ... scopes , WORKSPACE_PAT_SCOPE ] ,
104134 } ) ) ;
105135
106136 await this . storeSessions ( hydratedSessions ) ;
107137 this . sessionChangeEmitter . fire ( { added : hydratedSessions , removed : [ ] , changed : [ ] } ) ;
108- this . logger . info ( `GitHubAuthProvider: hydrated ${ hydratedSessions . length } session(s) from K8s token` ) ;
138+ const tokenSource = isDeviceAuthToken ? 'device authentication' : 'workspace PAT' ;
139+ this . logger . info ( `GitHubAuthProvider: hydrated ${ hydratedSessions . length } session(s) from K8s ${ tokenSource } ` ) ;
109140 } catch ( error ) {
110141 if ( isUnauthorizedError ( error ) ) {
111142 this . logger . warn ( 'GitHubAuthProvider: hydrate failed, token is not valid' ) ;
112143 } else {
113- this . logger . trace ( `GitHubAuthProvider: hydrate skipped : ${ ( error as Error ) . message } ` ) ;
144+ this . logger . warn ( `GitHubAuthProvider: hydrate failed : ${ ( error as Error ) . message } ` ) ;
114145 }
115146 }
116147 }
117148
118149 async getSessions ( sessionScopes ?: string [ ] ) : Promise < vscode . AuthenticationSession [ ] > {
119- this . logger . trace ( `GitHubAuthProvider: GET SESSIONS for scopes: ${ sessionScopes } ` ) ;
150+ this . logger . info ( `GitHubAuthProvider: GET SESSIONS for scopes: ${ sessionScopes } ` ) ;
120151
121152 const sessions = await this . sessionsPromise ;
122153 const sortedScopes = sessionScopes ? [ ...sessionScopes ] . sort ( ) : [ ] ;
123154 const filteredSessions = sortedScopes . length
124- ? sessions . filter ( session => arrayEquals ( [ ... session . scopes ] . sort ( ) , sortedScopes ) )
155+ ? sessions . filter ( session => sessionMatchesRequestedScopes ( session . scopes , sortedScopes ) )
125156 : [ ...sessions ] ;
126157
127- this . logger . trace ( `GitHubAuthProvider: GET sessions - found ${ filteredSessions . length } sessions for scopes: ${ sessionScopes } ` ) ;
158+ this . logger . info ( `GitHubAuthProvider: GET sessions - found ${ filteredSessions . length } sessions for scopes: ${ sessionScopes } ` ) ;
128159 return filteredSessions ;
129160 }
130161
@@ -160,14 +191,15 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
160191 }
161192
162193 const sessions = await this . sessionsPromise ;
194+ const isDeviceAuthToken = await this . githubService . isDeviceAuthToken ( ) ;
163195 const session : vscode . AuthenticationSession = {
164196 id : v4 ( ) ,
165197 accessToken : token ,
166198 account : { label : githubUser . login , id : githubUser . id . toString ( ) } ,
167- scopes,
199+ scopes : isDeviceAuthToken ? scopes : [ ... scopes , WORKSPACE_PAT_SCOPE ] ,
168200 } ;
169201
170- const sessionIndex = sessions . findIndex ( s => arrayEquals ( [ ... s . scopes ] . sort ( ) , sortedScopes ) ) ;
202+ const sessionIndex = sessions . findIndex ( s => sessionMatchesRequestedScopes ( s . scopes , sortedScopes ) ) ;
171203 const removed : vscode . AuthenticationSession [ ] = [ ] ;
172204 const updatedSessions = [ ...sessions ] ;
173205 if ( sessionIndex > - 1 ) {
@@ -184,21 +216,42 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
184216 }
185217
186218 private async resolveToken ( sortedScopes : string [ ] ) : Promise < string > {
219+ const token = await this . getTokenIfSufficient ( sortedScopes ) ;
220+ if ( ! token ) {
221+ return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
222+ }
223+ return token ;
224+ }
225+
226+ private async getTokenIfSufficient ( sortedScopes : string [ ] ) : Promise < string | undefined > {
187227 try {
188228 const token = await this . githubService . getToken ( ) ;
189229 const existingScopes = await this . githubService . getTokenScopes ( token ) ;
190230 if ( ! hasAllScopes ( existingScopes , sortedScopes ) ) {
191- this . logger . info ( ` GitHubAuthProvider: token lacks required scopes, starting device flow` ) ;
192- return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
231+ this . logger . info ( ' GitHubAuthProvider: token lacks required scopes, starting device flow' ) ;
232+ return undefined ;
193233 }
234+
235+ const isDeviceAuth = await this . githubService . isDeviceAuthToken ( ) ;
236+ if ( ! isDeviceAuth ) {
237+ const sessions = await this . sessionsPromise ;
238+ const hasExistingSession = sessions . some ( s =>
239+ sessionMatchesRequestedScopes ( s . scopes , sortedScopes )
240+ ) ;
241+ if ( hasExistingSession ) {
242+ this . logger . info ( 'GitHubAuthProvider: PAT session already exists for requested scopes, starting device auth flow' ) ;
243+ return undefined ;
244+ }
245+ }
246+
194247 return token ;
195248 } catch ( error ) {
196249 if ( isUnauthorizedError ( error ) ) {
197- this . logger . info ( ` GitHubAuthProvider: token is not valid, starting device flow` ) ;
250+ this . logger . info ( ' GitHubAuthProvider: token is not valid, starting device flow' ) ;
198251 } else {
199- this . logger . info ( ` GitHubAuthProvider: no token available, starting device flow` ) ;
252+ this . logger . info ( ' GitHubAuthProvider: no token available, starting device flow' ) ;
200253 }
201- return await this . getDeviceAuthentication ( ) . runInteractiveFlow ( sortedScopes ) ;
254+ return undefined ;
202255 }
203256 }
204257
0 commit comments