(
url: string,
credential: ResolvedCredential,
allowRefresh: boolean,
)
| 117 | |
| 118 | // fallow-ignore-next-line complexity |
| 119 | private async fetchUser( |
| 120 | url: string, |
| 121 | credential: ResolvedCredential, |
| 122 | allowRefresh: boolean, |
| 123 | ): Promise<UserInfo> { |
| 124 | const headers = buildAuthHeaders(credential); |
| 125 | const res = await this.fetchImpl(url, { method: "GET", headers }); |
| 126 | |
| 127 | if (res.status === 401) { |
| 128 | if ( |
| 129 | allowRefresh && |
| 130 | credential.type === "oauth" && |
| 131 | credential.refresh_token && |
| 132 | this.onRefresh |
| 133 | ) { |
| 134 | const refreshed = await this.tryRefresh(credential.refresh_token); |
| 135 | if (refreshed) { |
| 136 | // Carry the new refresh_token forward too — for IdPs that |
| 137 | // rotate RTs on every refresh, a future retry on this |
| 138 | // in-memory credential would otherwise re-send the old |
| 139 | // (now-invalidated) one. |
| 140 | const next: ResolvedCredential = { |
| 141 | ...credential, |
| 142 | access_token: refreshed.access_token, |
| 143 | ...(refreshed.refresh_token ? { refresh_token: refreshed.refresh_token } : {}), |
| 144 | }; |
| 145 | return await this.fetchUser(url, next, false); |
| 146 | } |
| 147 | } |
| 148 | const detail = await safeText(res); |
| 149 | throw ErrUnauthenticated(detail || `${res.status} ${res.statusText}`); |
| 150 | } |
| 151 | if (!res.ok) { |
| 152 | throw ErrApi(res.status, (await safeText(res)) || res.statusText); |
| 153 | } |
| 154 | |
| 155 | let payload: unknown; |
| 156 | try { |
| 157 | payload = await res.json(); |
| 158 | } catch (err) { |
| 159 | throw ErrApi(res.status, `non-JSON body: ${(err as Error).message}`); |
| 160 | } |
| 161 | return extractUserInfo(payload); |
| 162 | } |
| 163 | |
| 164 | private async tryRefresh(refresh_token: string): Promise<OAuthTokens | null> { |
| 165 | if (!this.onRefresh) return null; |
no test coverage detected