Repository navigation
Expand file tree
/
Copy pathrefresh.ts
More file actions
76 lines (72 loc) · 2.84 KB
/
Copy pathrefresh.ts
File metadata and controls
76 lines (72 loc) · 2.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import {
type ResolvedProfile,
readCredentialsProfile,
readStoredOAuth,
type StoredOAuthCredential,
withCredentialsLock,
writeCredentialsProfile,
} from '../config/index'
import { SimApiError } from '../http/client'
import { OAuthTokenError, refreshTokens } from './oauth-flow'
/**
* Bounded well under the credentials lock's stale window: a hung authorization
* server must not hold the lock long enough for another process to reclaim it
* and race the same single-use refresh token.
*/
const REFRESH_TIMEOUT_MS = 10 * 1000
/**
* Renews a stored OAuth login and persists the rotated pair.
*
* Under the credentials lock because the refresh token is single-use and two
* local processes must not race it. After taking the lock the file is read
* again: if another process already rotated the token, its result is adopted
* and no request is made. This coordinates trusted local processes; detecting
* and containing a copied token remains the authorization server's job.
*
* `invalid_grant` means the server no longer honours the refresh token — it
* was revoked from Settings → General → Authorized apps, expired, or was already rotated
* by a process this one could not see — and the remedy is logout followed by a
* new login.
*/
export async function refreshStoredOAuth(
profile: Pick<ResolvedProfile, 'name' | 'endpoint' | 'authProfile'>,
current: StoredOAuthCredential
): Promise<StoredOAuthCredential> {
return withCredentialsLock(async () => {
const stored = readStoredOAuth(readCredentialsProfile(profile.authProfile))
if (!stored || stored.loginId !== current.loginId) {
throw new SimApiError(
`The stored login changed while this command was waiting to refresh it. Retry the command with the active login for profile ${profile.authProfile}.`,
401
)
}
if (stored.refreshToken !== current.refreshToken) return stored
let tokens: StoredOAuthCredential
try {
const refreshed = await refreshTokens(
profile.endpoint,
current.refreshToken,
current.scope.split(' ').filter(Boolean),
AbortSignal.timeout(REFRESH_TIMEOUT_MS)
)
tokens = {
accessToken: refreshed.accessToken,
refreshToken: refreshed.refreshToken,
expiresAt: refreshed.expiresAt,
issuer: current.issuer,
loginId: current.loginId,
scope: refreshed.scope,
}
} catch (error) {
if (error instanceof OAuthTokenError && error.oauthError === 'invalid_grant') {
throw new SimApiError(
`Your Sim login expired, was revoked, or detected refresh-token reuse. Run sim logout --profile ${profile.authProfile}, then sim login --profile ${profile.authProfile}.`,
401
)
}
throw error
}
writeCredentialsProfile(profile.authProfile, { kind: 'oauth', oauth: tokens })
return tokens
})
}