@@ -28,6 +28,11 @@ export class ProjectRoleCredentialsProvider implements CredentialsProvider {
28
28
credentials : AWS . Credentials
29
29
expiresAt : Date
30
30
}
31
+ private refreshTimer ?: NodeJS . Timeout
32
+ private readonly refreshInterval = 10 * 60 * 1000 // 10 minutes
33
+ private readonly checkInterval = 10 * 1000 // 10 seconds - check frequently, refresh based on actual time
34
+ private sshRefreshActive = false
35
+ private lastRefreshTime ?: Date
31
36
32
37
constructor (
33
38
private readonly smusAuthProvider : SmusAuthenticationProvider ,
@@ -162,11 +167,21 @@ export class ProjectRoleCredentialsProvider implements CredentialsProvider {
162
167
163
168
return awsCredentials
164
169
} catch ( err ) {
165
- this . logger . error (
166
- 'SMUS Project: Failed to get project credentials for project %s: %s' ,
167
- this . projectId ,
168
- ( err as Error ) . message
169
- )
170
+ this . logger . error ( 'SMUS Project: Failed to get project credentials for project %s: %s' , this . projectId , err )
171
+
172
+ // Handle InvalidGrantException specially - indicates need for reauthentication
173
+ if ( err instanceof Error && err . name === 'InvalidGrantException' ) {
174
+ // Invalidate cache when authentication fails
175
+ this . invalidate ( )
176
+ throw new ToolkitError (
177
+ `Failed to get project credentials for project ${ this . projectId } : ${ err . message } . Reauthentication required.` ,
178
+ {
179
+ code : 'InvalidRefreshToken' ,
180
+ cause : err ,
181
+ }
182
+ )
183
+ }
184
+
170
185
throw new ToolkitError ( `Failed to get project credentials for project ${ this . projectId } : ${ err } ` , {
171
186
code : 'ProjectCredentialsFetchFailed' ,
172
187
cause : err instanceof Error ? err : undefined ,
@@ -192,6 +207,139 @@ export class ProjectRoleCredentialsProvider implements CredentialsProvider {
192
207
}
193
208
}
194
209
210
+ /**
211
+ * Starts proactive credential refresh for SSH connections
212
+ *
213
+ * Uses an expiry-based approach with safety buffer:
214
+ * - Checks every 10 seconds using setTimeout
215
+ * - Refreshes when credentials expire within 5 minutes (safety buffer)
216
+ * - Falls back to 10-minute time-based refresh if no expiry information available
217
+ * - Handles sleep/resume because it uses wall-clock time for expiry checks
218
+ *
219
+ * This means credentials are refreshed just before they expire, reducing
220
+ * unnecessary API calls while ensuring credentials remain valid.
221
+ */
222
+ public startProactiveCredentialRefresh ( ) : void {
223
+ if ( this . sshRefreshActive ) {
224
+ this . logger . debug ( `SMUS Project: SSH refresh already active for project ${ this . projectId } ` )
225
+ return
226
+ }
227
+
228
+ this . logger . info ( `SMUS Project: Starting SSH credential refresh for project ${ this . projectId } ` )
229
+ this . sshRefreshActive = true
230
+ this . lastRefreshTime = new Date ( ) // Initialize refresh time
231
+
232
+ // Start the check timer (checks every 10 seconds, refreshes every 10 minutes based on actual time)
233
+ this . scheduleNextCheck ( )
234
+ }
235
+
236
+ /**
237
+ * Stops proactive credential refresh
238
+ * Called when SSH connection ends or SMUS disconnects
239
+ */
240
+ public stopProactiveCredentialRefresh ( ) : void {
241
+ if ( ! this . sshRefreshActive ) {
242
+ return
243
+ }
244
+
245
+ this . logger . info ( `SMUS Project: Stopping SSH credential refresh for project ${ this . projectId } ` )
246
+ this . sshRefreshActive = false
247
+ this . lastRefreshTime = undefined
248
+
249
+ // Clean up timer
250
+ if ( this . refreshTimer ) {
251
+ clearTimeout ( this . refreshTimer )
252
+ this . refreshTimer = undefined
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Schedules the next credential check (every 10 seconds)
258
+ * Refreshes credentials when they expire within 5 minutes (safety buffer)
259
+ * Falls back to 10-minute time-based refresh if no expiry information available
260
+ * This handles sleep/resume scenarios correctly
261
+ */
262
+ private scheduleNextCheck ( ) : void {
263
+ if ( ! this . sshRefreshActive ) {
264
+ return
265
+ }
266
+ // Check every 10 seconds, but only refresh every 10 minutes based on actual time elapsed
267
+ this . refreshTimer = setTimeout ( async ( ) => {
268
+ try {
269
+ const now = new Date ( )
270
+ // Check if we need to refresh based on actual time elapsed
271
+ if ( this . shouldPerformRefresh ( now ) ) {
272
+ await this . refresh ( )
273
+ }
274
+ // Schedule next check if still active
275
+ if ( this . sshRefreshActive ) {
276
+ this . scheduleNextCheck ( )
277
+ }
278
+ } catch ( error ) {
279
+ this . logger . error (
280
+ `SMUS Project: Failed to refresh credentials for project ${ this . projectId } : %O` ,
281
+ error
282
+ )
283
+ // Continue trying even if refresh fails. Dispose will handle stopping the refresh.
284
+ if ( this . sshRefreshActive ) {
285
+ this . scheduleNextCheck ( )
286
+ }
287
+ }
288
+ } , this . checkInterval )
289
+ }
290
+
291
+ /**
292
+ * Determines if a credential refresh should be performed based on credential expiration
293
+ * This handles sleep/resume scenarios properly and is more efficient than time-based refresh
294
+ */
295
+ private shouldPerformRefresh ( now : Date ) : boolean {
296
+ if ( ! this . lastRefreshTime || ! this . credentialCache ) {
297
+ // First refresh or no cached credentials
298
+ this . logger . debug ( `SMUS Project: First refresh - no previous credentials for ${ this . projectId } ` )
299
+ return true
300
+ }
301
+
302
+ // Check if credentials expire soon (with 5-minute safety buffer)
303
+ const safetyBufferMs = 5 * 60 * 1000 // 5 minutes before expiry
304
+ const expiryTime = this . credentialCache . credentials . expiration ?. getTime ( )
305
+
306
+ if ( ! expiryTime ) {
307
+ // No expiry info - fall back to time-based refresh as safety net
308
+ const timeSinceLastRefresh = now . getTime ( ) - this . lastRefreshTime . getTime ( )
309
+ const shouldRefresh = timeSinceLastRefresh >= this . refreshInterval
310
+ return shouldRefresh
311
+ }
312
+
313
+ const timeUntilExpiry = expiryTime - now . getTime ( )
314
+ const shouldRefresh = timeUntilExpiry < safetyBufferMs
315
+ return shouldRefresh
316
+ }
317
+
318
+ /**
319
+ * Performs credential refresh by invalidating cache and fetching fresh credentials
320
+ */
321
+ private async refresh ( ) : Promise < void > {
322
+ const now = new Date ( )
323
+ const expiryTime = this . credentialCache ?. credentials . expiration ?. getTime ( )
324
+
325
+ if ( expiryTime ) {
326
+ const minutesUntilExpiry = Math . round ( ( expiryTime - now . getTime ( ) ) / 60000 )
327
+ this . logger . debug (
328
+ `SMUS Project: Refreshing credentials for project ${ this . projectId } - expires in ${ minutesUntilExpiry } minutes`
329
+ )
330
+ } else {
331
+ const minutesSinceLastRefresh = this . lastRefreshTime
332
+ ? Math . round ( ( now . getTime ( ) - this . lastRefreshTime . getTime ( ) ) / 60000 )
333
+ : 0
334
+ this . logger . debug (
335
+ `SMUS Project: Refreshing credentials for project ${ this . projectId } - time-based refresh after ${ minutesSinceLastRefresh } minutes`
336
+ )
337
+ }
338
+
339
+ await this . getCredentials ( )
340
+ this . lastRefreshTime = new Date ( )
341
+ }
342
+
195
343
/**
196
344
* Invalidates cached project credentials
197
345
* Clears the internal cache without fetching new credentials
@@ -204,4 +352,12 @@ export class ProjectRoleCredentialsProvider implements CredentialsProvider {
204
352
`SMUS Project: Successfully invalidated project credentials cache for project ${ this . projectId } `
205
353
)
206
354
}
355
+
356
+ /**
357
+ * Disposes of the provider and cleans up resources
358
+ */
359
+ public dispose ( ) : void {
360
+ this . stopProactiveCredentialRefresh ( )
361
+ this . invalidate ( )
362
+ }
207
363
}
0 commit comments