@@ -36,8 +36,8 @@ import {
3636import {
3737 APIFY_CLIENT_DEFAULT_HEADERS ,
3838 AUTH_FILE_PATH ,
39+ DEFAULT_APIFY_API_BASE_URL ,
3940 DEFAULT_LOCAL_STORAGE_DIR ,
40- GLOBAL_CONFIGS_FOLDER ,
4141 INPUT_FILE_REG_EXP ,
4242 LOCAL_CONFIG_PATH ,
4343 MINIMUM_SUPPORTED_PYTHON_VERSION ,
@@ -106,18 +106,59 @@ export const getApifyAPIBaseUrl = () => {
106106 return process . env [ legacyVar ] ;
107107 }
108108
109- return process . env [ envVar ] ;
109+ // here we _could_ fallback to `undefined` and let ApifyClient to fill the default value, but this function is also
110+ // used for identifying the stored token in the global auth file
111+ // (to allow keeping a separate login for api.apify.com and localhost)
112+ // it is probably safe to assume that the default is https://api.apify.com
113+ return process . env [ envVar ] || DEFAULT_APIFY_API_BASE_URL ;
110114} ;
111115
116+ interface MultiBackendAuthJSON {
117+ _authFileVersion : 2 ;
118+ /** Mapping of ApifyAPIBaseUrl to the AuthJSON for that backend */
119+ backends : Record < string , AuthJSON > ;
120+ }
121+
112122/**
113- * Returns object from auth file or empty object .
123+ * Returns info about logins stored for all available backends .
114124 */
115- export const getLocalUserInfo = async ( ) : Promise < AuthJSON > => {
116- let result : AuthJSON = { } ;
125+ const getAllLocalUserInfos = async ( ) : Promise < MultiBackendAuthJSON > => {
126+ let result : AuthJSON | MultiBackendAuthJSON = { } ;
117127 try {
118128 const raw = await readFile ( AUTH_FILE_PATH ( ) , 'utf-8' ) ;
119- result = JSON . parse ( raw ) as AuthJSON ;
129+ result = JSON . parse ( raw ) as AuthJSON | MultiBackendAuthJSON ;
120130 } catch {
131+ return { _authFileVersion : 2 , backends : { } } ;
132+ }
133+
134+ if ( '_authFileVersion' in result ) return result ;
135+
136+ // migrate to multi-backend format, assume the stored data is for the current backend
137+ const backendUrl = getApifyAPIBaseUrl ( ) ;
138+ const multiBackendResult : MultiBackendAuthJSON = { _authFileVersion : 2 , backends : { } } ;
139+ multiBackendResult . backends [ backendUrl ] = result ;
140+ return multiBackendResult ;
141+ } ;
142+
143+ /**
144+ * Lists stored user infos for all backends.
145+ */
146+ export const listLocalUserInfos = async ( ) : Promise < ( { baseUrl : string } & Pick < AuthJSON , 'username' | 'id' > ) [ ] > => {
147+ const allInfos = await getAllLocalUserInfos ( ) ;
148+ return Object . entries ( allInfos . backends ) . map ( ( [ baseUrl , info ] ) => ( {
149+ baseUrl,
150+ username : info . username ,
151+ id : info . id ,
152+ } ) ) ;
153+ } ;
154+
155+ /**
156+ * Returns object from auth file or empty object.
157+ */
158+ export const getLocalUserInfo = async ( ) : Promise < AuthJSON > => {
159+ const allInfos = await getAllLocalUserInfos ( ) ;
160+ const result = allInfos . backends [ getApifyAPIBaseUrl ( ) ] ;
161+ if ( ! result ) {
121162 return { } ;
122163 }
123164
@@ -128,6 +169,34 @@ export const getLocalUserInfo = async (): Promise<AuthJSON> => {
128169 return result ;
129170} ;
130171
172+ /**
173+ * Persists auth info for the current backend
174+ */
175+ export async function storeLocalUserInfo ( userInfo : AuthJSON ) {
176+ ensureApifyDirectory ( AUTH_FILE_PATH ( ) ) ;
177+
178+ const allInfos = await getAllLocalUserInfos ( ) ;
179+ allInfos . backends [ getApifyAPIBaseUrl ( ) ] = userInfo ;
180+
181+ writeFileSync ( AUTH_FILE_PATH ( ) , JSON . stringify ( allInfos , null , '\t' ) ) ;
182+ }
183+
184+ /**
185+ * Removes auth info for the current backend - effectively logs out the user.
186+ *
187+ * Returns true if info was removed, false if there was no info for this backend.
188+ */
189+ export async function clearLocalUserInfo ( ) {
190+ const allInfos = await getAllLocalUserInfos ( ) ;
191+ const backendUrl = getApifyAPIBaseUrl ( ) ;
192+
193+ if ( ! allInfos . backends [ backendUrl ] ) return false ;
194+
195+ delete allInfos . backends [ backendUrl ] ;
196+ writeFileSync ( AUTH_FILE_PATH ( ) , JSON . stringify ( allInfos , null , '\t' ) ) ;
197+ return true ;
198+ }
199+
131200/**
132201 * Gets instance of ApifyClient for user otherwise throws error
133202 */
@@ -141,13 +210,11 @@ export async function getLoggedClientOrThrow() {
141210 return loggedClient ;
142211}
143212
144- const getTokenWithAuthFileFallback = ( existingToken ?: string ) => {
145- if ( ! existingToken && existsSync ( GLOBAL_CONFIGS_FOLDER ( ) ) && existsSync ( AUTH_FILE_PATH ( ) ) ) {
146- const raw = readFileSync ( AUTH_FILE_PATH ( ) , 'utf-8' ) ;
147- return JSON . parse ( raw ) . token ;
148- }
213+ const getTokenWithAuthFileFallback = async ( existingToken ?: string ) => {
214+ if ( existingToken ) return existingToken ;
149215
150- return existingToken ;
216+ const userInfo = await getLocalUserInfo ( ) ;
217+ return userInfo . token ;
151218} ;
152219
153220// biome-ignore format: off
@@ -156,8 +223,8 @@ type CJSAxiosHeaders = import('axios', { with: { 'resolution-mode': 'require' }
156223/**
157224 * Returns options for ApifyClient
158225 */
159- export const getApifyClientOptions = ( token ?: string , apiBaseUrl ?: string ) : ApifyClientOptions => {
160- token = getTokenWithAuthFileFallback ( token ) ;
226+ export const getApifyClientOptions = async ( token ?: string , apiBaseUrl ?: string ) : Promise < ApifyClientOptions > => {
227+ token = await getTokenWithAuthFileFallback ( token ) ;
161228
162229 return {
163230 token,
@@ -182,9 +249,9 @@ export const getApifyClientOptions = (token?: string, apiBaseUrl?: string): Apif
182249 * @param [token]
183250 */
184251export async function getLoggedClient ( token ?: string , apiBaseUrl ?: string ) {
185- token = getTokenWithAuthFileFallback ( token ) ;
252+ token = await getTokenWithAuthFileFallback ( token ) ;
186253
187- const apifyClient = new ApifyClient ( getApifyClientOptions ( token , apiBaseUrl ) ) ;
254+ const apifyClient = new ApifyClient ( await getApifyClientOptions ( token , apiBaseUrl ) ) ;
188255
189256 let userInfo ;
190257 try {
@@ -194,9 +261,7 @@ export async function getLoggedClient(token?: string, apiBaseUrl?: string) {
194261 }
195262
196263 // Always refresh Auth file
197- ensureApifyDirectory ( AUTH_FILE_PATH ( ) ) ;
198-
199- writeFileSync ( AUTH_FILE_PATH ( ) , JSON . stringify ( { token : apifyClient . token , ...userInfo } , null , '\t' ) ) ;
264+ await storeLocalUserInfo ( { token : apifyClient . token , ...userInfo } ) ;
200265
201266 return apifyClient ;
202267}
0 commit comments