@@ -36,14 +36,15 @@ 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 ,
4444 SUPPORTED_NODEJS_VERSION ,
4545} from './consts.js' ;
4646import { deleteFile , ensureFolderExistsSync , rimrafPromised } from './files.js' ;
47+ import { warning } from './outputs.js' ;
4748import type { AuthJSON } from './types.js' ;
4849
4950// Export AJV properly: https://github.com/ajv-validator/ajv/issues/2132
@@ -92,15 +93,72 @@ export const getLocalRequestQueuePath = (storeId?: string) => {
9293 return join ( getLocalStorageDir ( ) , LOCAL_STORAGE_SUBDIRS . requestQueues , storeDir ) ;
9394} ;
9495
96+ let hasLoggedAPIBaseUrlDeprecation = false ;
97+ export const getApifyAPIBaseUrl = ( ) => {
98+ const envVar = APIFY_ENV_VARS . API_BASE_URL ;
99+
100+ const legacyVar = 'APIFY_CLIENT_BASE_URL' ;
101+ if ( process . env [ legacyVar ] ) {
102+ if ( ! hasLoggedAPIBaseUrlDeprecation ) {
103+ warning ( { message : `Environment variable '${ legacyVar } ' is deprecated. Please use '${ envVar } ' instead.` } ) ;
104+ hasLoggedAPIBaseUrlDeprecation = true ;
105+ }
106+ return process . env [ legacyVar ] ;
107+ }
108+
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 ;
114+ } ;
115+
116+ interface MultiBackendAuthJSON {
117+ _authFileVersion : 2 ;
118+ /** Mapping of ApifyAPIBaseUrl to the AuthJSON for that backend */
119+ backends : Record < string , AuthJSON > ;
120+ }
121+
95122/**
96- * Returns object from auth file or empty object .
123+ * Returns info about logins stored for all available backends .
97124 */
98- export const getLocalUserInfo = async ( ) : Promise < AuthJSON > => {
99- let result : AuthJSON = { } ;
125+ const getAllLocalUserInfos = async ( ) : Promise < MultiBackendAuthJSON > => {
126+ let result : AuthJSON | MultiBackendAuthJSON = { } ;
100127 try {
101128 const raw = await readFile ( AUTH_FILE_PATH ( ) , 'utf-8' ) ;
102- result = JSON . parse ( raw ) as AuthJSON ;
129+ result = JSON . parse ( raw ) as AuthJSON | MultiBackendAuthJSON ;
103130 } 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 ) {
104162 return { } ;
105163 }
106164
@@ -111,6 +169,34 @@ export const getLocalUserInfo = async (): Promise<AuthJSON> => {
111169 return result ;
112170} ;
113171
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+
114200/**
115201 * Gets instance of ApifyClient for user otherwise throws error
116202 */
@@ -124,13 +210,11 @@ export async function getLoggedClientOrThrow() {
124210 return loggedClient ;
125211}
126212
127- const getTokenWithAuthFileFallback = ( existingToken ?: string ) => {
128- if ( ! existingToken && existsSync ( GLOBAL_CONFIGS_FOLDER ( ) ) && existsSync ( AUTH_FILE_PATH ( ) ) ) {
129- const raw = readFileSync ( AUTH_FILE_PATH ( ) , 'utf-8' ) ;
130- return JSON . parse ( raw ) . token ;
131- }
213+ const getTokenWithAuthFileFallback = async ( existingToken ?: string ) => {
214+ if ( existingToken ) return existingToken ;
132215
133- return existingToken ;
216+ const userInfo = await getLocalUserInfo ( ) ;
217+ return userInfo . token ;
134218} ;
135219
136220// biome-ignore format: off
@@ -139,12 +223,12 @@ type CJSAxiosHeaders = import('axios', { with: { 'resolution-mode': 'require' }
139223/**
140224 * Returns options for ApifyClient
141225 */
142- export const getApifyClientOptions = ( token ?: string , apiBaseUrl ?: string ) : ApifyClientOptions => {
143- token = getTokenWithAuthFileFallback ( token ) ;
226+ export const getApifyClientOptions = async ( token ?: string , apiBaseUrl ?: string ) : Promise < ApifyClientOptions > => {
227+ token = await getTokenWithAuthFileFallback ( token ) ;
144228
145229 return {
146230 token,
147- baseUrl : apiBaseUrl || process . env . APIFY_CLIENT_BASE_URL ,
231+ baseUrl : apiBaseUrl || getApifyAPIBaseUrl ( ) ,
148232 requestInterceptors : [
149233 ( config ) => {
150234 config . headers ??= new AxiosHeaders ( ) as CJSAxiosHeaders ;
@@ -165,9 +249,9 @@ export const getApifyClientOptions = (token?: string, apiBaseUrl?: string): Apif
165249 * @param [token]
166250 */
167251export async function getLoggedClient ( token ?: string , apiBaseUrl ?: string ) {
168- token = getTokenWithAuthFileFallback ( token ) ;
252+ token = await getTokenWithAuthFileFallback ( token ) ;
169253
170- const apifyClient = new ApifyClient ( getApifyClientOptions ( token , apiBaseUrl ) ) ;
254+ const apifyClient = new ApifyClient ( await getApifyClientOptions ( token , apiBaseUrl ) ) ;
171255
172256 let userInfo ;
173257 try {
@@ -177,9 +261,7 @@ export async function getLoggedClient(token?: string, apiBaseUrl?: string) {
177261 }
178262
179263 // Always refresh Auth file
180- ensureApifyDirectory ( AUTH_FILE_PATH ( ) ) ;
181-
182- writeFileSync ( AUTH_FILE_PATH ( ) , JSON . stringify ( { token : apifyClient . token , ...userInfo } , null , '\t' ) ) ;
264+ await storeLocalUserInfo ( { token : apifyClient . token , ...userInfo } ) ;
183265
184266 return apifyClient ;
185267}
@@ -376,7 +458,7 @@ export const outputJobLog = async ({
376458 apifyClient ?: ApifyClient ;
377459} ) => {
378460 const { id : logId , status } = job ;
379- const client = apifyClient || new ApifyClient ( { baseUrl : process . env . APIFY_CLIENT_BASE_URL } ) ;
461+ const client = apifyClient || new ApifyClient ( { baseUrl : getApifyAPIBaseUrl ( ) } ) ;
380462
381463 // In case job was already done just output log
382464 if ( ACTOR_JOB_TERMINAL_STATUSES . includes ( status as never ) ) {
0 commit comments