import type { Page } from 'patchright' import { randomBytes } from 'crypto' import { URLSearchParams } from 'url' import type { MicrosoftRewardsBot } from '../../../index' export class MobileAccessLogin { private clientId = '0000000040170455' private authUrl = 'https://login.live.com/oauth20_authorize.srf' private redirectUrl = 'https://login.live.com/oauth20_desktop.srf' private tokenUrl = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token' private scope = 'service::prod.rewardsplatform.microsoft.com::MBI_SSL' private maxTimeout = 180_000 // 3min constructor( private bot: MicrosoftRewardsBot, private page: Page ) {} async get(email: string): Promise { try { const authorizeUrl = new URL(this.authUrl) authorizeUrl.searchParams.append('response_type', 'code') authorizeUrl.searchParams.append('client_id', this.clientId) authorizeUrl.searchParams.append('redirect_uri', this.redirectUrl) authorizeUrl.searchParams.append('scope', this.scope) authorizeUrl.searchParams.append('state', randomBytes(16).toString('hex')) authorizeUrl.searchParams.append('access_type', 'offline_access') authorizeUrl.searchParams.append('login_hint', email) this.bot.logger.debug( this.bot.isMobile, 'LOGIN-APP', `Auth URL constructed: ${authorizeUrl.origin}${authorizeUrl.pathname}` ) await this.bot.browser.utils.disableFido(this.page) this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Navigating to OAuth authorize URL') await this.page.goto(authorizeUrl.href).catch(err => { this.bot.logger.debug( this.bot.isMobile, 'LOGIN-APP', `page.goto() failed: ${err instanceof Error ? err.message : String(err)}` ) }) this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Waiting for mobile OAuth code...') const start = Date.now() let code = '' let lastUrl = '' while (Date.now() - start < this.maxTimeout) { const currentUrl = this.page.url() // Log only when URL changes (high signal, no spam) if (currentUrl !== lastUrl) { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `OAuth poll URL changed → ${currentUrl}`) lastUrl = currentUrl } try { const url = new URL(currentUrl) if (url.hostname === 'login.live.com' && url.pathname === '/oauth20_desktop.srf') { code = url.searchParams.get('code') || '' if (code) { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'OAuth code detected in redirect URL') break } } } catch (err) { this.bot.logger.debug( this.bot.isMobile, 'LOGIN-APP', `Invalid URL while polling: ${String(currentUrl)}` ) } await this.bot.utils.wait(1000) } if (!code) { this.bot.logger.warn( this.bot.isMobile, 'LOGIN-APP', `Timed out waiting for OAuth code after ${Math.round((Date.now() - start) / 1000)}s` ) this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', `Final page URL: ${this.page.url()}`) return '' } const data = new URLSearchParams() data.append('grant_type', 'authorization_code') data.append('client_id', this.clientId) data.append('code', code) data.append('redirect_uri', this.redirectUrl) this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Exchanging OAuth code for access token') const response = await this.bot.axios.request({ url: this.tokenUrl, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: data.toString() }) const token = (response?.data?.access_token as string) ?? '' if (!token) { this.bot.logger.warn(this.bot.isMobile, 'LOGIN-APP', 'No access_token in token response') this.bot.logger.debug( this.bot.isMobile, 'LOGIN-APP', `Token response payload: ${JSON.stringify(response?.data)}` ) return '' } this.bot.logger.info(this.bot.isMobile, 'LOGIN-APP', 'Mobile access token received') return token } catch (error) { this.bot.logger.error( this.bot.isMobile, 'LOGIN-APP', `MobileAccess error: ${error instanceof Error ? error.stack || error.message : String(error)}` ) return '' } finally { this.bot.logger.debug(this.bot.isMobile, 'LOGIN-APP', 'Returning to base URL') await this.page.goto(this.bot.config.baseURL, { timeout: 10000 }).catch(() => {}) } } }