UNPKG

tsvesync

Version:

A TypeScript library for interacting with VeSync smart home devices

752 lines (751 loc) 33.3 kB
"use strict"; /** * Helper functions for VeSync API */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Helpers = exports.AUTH_ERROR_CODES = exports.APP_VERSION_TOO_LOW_ERROR_CODE = exports.REGION_ENDPOINTS = exports.CLIENT_VERSION = exports.BYPASS_HEADER_UA = exports.MOBILE_ID = exports.DEFAULT_REGION = exports.DEFAULT_TZ = exports.USER_TYPE = exports.CLIENT_INFO = exports.PHONE_OS = exports.PHONE_BRAND = exports.APP_VERSION = exports.API_TIMEOUT = exports.API_RATE_LIMIT = exports.NON_EU_COUNTRY_CODES = void 0; exports.getApiBaseUrl = getApiBaseUrl; exports.setApiBaseUrl = setApiBaseUrl; exports.getCurrentRegion = getCurrentRegion; exports.setCurrentRegion = setCurrentRegion; exports.getRegionFromCountryCode = getRegionFromCountryCode; exports.getUSEndpointCountryCodes = getUSEndpointCountryCodes; exports.getEndpointForCountryCode = getEndpointForCountryCode; exports.detectUserRegion = detectUserRegion; exports.getCountryCodeFromRegion = getCountryCodeFromRegion; exports.generateAppId = generateAppId; exports.generateTerminalId = generateTerminalId; exports.generateTraceId = generateTraceId; const axios_1 = __importDefault(require("axios")); const crypto_1 = __importDefault(require("crypto")); const logger_1 = require("./logger"); const constants_1 = require("./constants"); // API configuration - Support regional endpoints let _apiBaseUrl = 'https://smartapi.vesync.com'; let _currentRegion = 'US'; function getApiBaseUrl() { return _apiBaseUrl; } function setApiBaseUrl(url) { _apiBaseUrl = url; } function getCurrentRegion() { return _currentRegion; } function setCurrentRegion(region) { _currentRegion = region; if (region in exports.REGION_ENDPOINTS) { _apiBaseUrl = exports.REGION_ENDPOINTS[region]; } } // pyvesync parity: // VeSync only exposes two API base URLs (US + EU). pyvesync intentionally uses a // *deny-list* mapping here: // - `US` region: US/CA/MX/JP // - `EU` region: everything else // // This is counter-intuitive if you read it as "EU countries", but it's really // "the EU *endpoint*". In practice, many accounts outside Europe (e.g. AU/NZ/SG) // can be hosted on either endpoint depending on how/when the account was created. // // The correct/robust behavior comes from the Step 2 cross-region flow: when the // region guess is wrong, the API returns `currentRegion`/`countryCode` plus a // `bizToken`, and we retry Step 2 using the server-provided values (pyvesync-style). exports.NON_EU_COUNTRY_CODES = ['US', 'CA', 'MX', 'JP']; // Determine region from country code function getRegionFromCountryCode(countryCode) { const normalized = countryCode.trim().toUpperCase(); if (exports.NON_EU_COUNTRY_CODES.includes(normalized)) { return 'US'; } return 'EU'; } exports.API_RATE_LIMIT = 30; exports.API_TIMEOUT = 15000; exports.APP_VERSION = '5.7.16'; // Updated to newer version exports.PHONE_BRAND = 'SM N9005'; exports.PHONE_OS = 'Android'; exports.CLIENT_INFO = 'SM N9005'; exports.USER_TYPE = '1'; exports.DEFAULT_TZ = 'America/New_York'; exports.DEFAULT_REGION = 'US'; exports.MOBILE_ID = '1234567890123456'; exports.BYPASS_HEADER_UA = 'okhttp/3.12.1'; exports.CLIENT_VERSION = 'VeSync 5.7.16'; // Regional API endpoints exports.REGION_ENDPOINTS = { 'US': 'https://smartapi.vesync.com', 'CA': 'https://smartapi.vesync.com', 'MX': 'https://smartapi.vesync.com', 'JP': 'https://smartapi.vesync.com', 'EU': 'https://smartapi.vesync.eu' }; // Cross-region error codes - multiple versions exist // CROSS_REGION_ERROR_CODES moved to constants.ts exports.APP_VERSION_TOO_LOW_ERROR_CODE = -11012022; // Authentication error codes exports.AUTH_ERROR_CODES = { ACCOUNT_PASSWORD_INCORRECT: -11201129, ILLEGAL_ARGUMENT: -11000022, CROSS_REGION: -11260022, CROSS_REGION_ALT: -11261022 }; /** * Get list of country codes that use the US endpoint * These are tried when we get a cross-region error */ function getUSEndpointCountryCodes() { // Countries that use the US endpoint but might need their own country code return ['US', 'AU', 'NZ', 'JP', 'CA', 'MX', 'SG']; } /** * Get the appropriate API endpoint based on country code */ function getEndpointForCountryCode(countryCode) { // Alias for clarity: this is the region-to-endpoint mapping used by pyvesync. return getRegionFromCountryCode(countryCode); } /** * Detect user's home region from email domain or country hints */ function detectUserRegion(email) { // Check for common EU email domains const euDomains = ['.de', '.fr', '.it', '.es', '.nl', '.be', '.at', '.dk', '.se', '.no', '.fi', '.eu']; const emailLower = email.toLowerCase(); for (const domain of euDomains) { if (emailLower.includes(domain)) { return 'EU'; } } // Default to US for unknown domains return 'US'; } /** * Get country code from region */ function getCountryCodeFromRegion(region, email) { if (region === 'EU') { // Try to detect specific EU country from email if (email) { const emailLower = email.toLowerCase(); if (emailLower.includes('.de') || emailLower.includes('german')) return 'DE'; if (emailLower.includes('.fr')) return 'FR'; if (emailLower.includes('.it')) return 'IT'; if (emailLower.includes('.es')) return 'ES'; if (emailLower.includes('.nl')) return 'NL'; } return 'DE'; // Default to Germany for EU } return 'US'; // Default for non-EU } // Generate unique APP_ID for each session function generateAppId() { const chars = 'ABCDEFGHIJKLMNOPqRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < 8; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } // Generate unique terminal ID for authentication function generateTerminalId() { // pyvesync parity: terminalId is a UUID-like identifier prefixed with "2". return `2${crypto_1.default.randomUUID().replace(/-/g, '')}`; } function generateTraceId() { const timestamp = Math.floor(Date.now() / 1000); const randomPart = Math.floor(Math.random() * 100000).toString().padStart(5, '0'); return `APP${exports.MOBILE_ID.slice(-5, -1)}${timestamp}-${randomPart}`; } let _authTraceCallNumber = 0; function generateAuthTraceId(terminalId) { _authTraceCallNumber += 1; const suffix = terminalId.slice(-5, -1); const timestamp = Math.floor(Date.now() / 1000); return `APP${suffix}${timestamp}-${String(_authTraceCallNumber).padStart(5, '0')}`; } class Helpers { static generateTraceId() { return generateTraceId(); } static normalizeAirQuality(value) { const stringMap = { 'excellent': 1, 'very good': 1, 'good': 2, 'moderate': 3, 'fair': 3, 'inferior': 4, 'poor': 4, 'bad': 4, }; if (typeof value === 'number') { const level = Number.isFinite(value) ? Math.trunc(value) : -1; if (level >= 1 && level <= 4) { return { level, label: level === 1 ? 'excellent' : level === 2 ? 'good' : level === 3 ? 'moderate' : 'poor', }; } } else if (typeof value === 'string') { const normalized = value.trim().toLowerCase(); const level = stringMap[normalized]; if (level) { return { level, label: level === 1 ? 'excellent' : level === 2 ? 'good' : level === 3 ? 'moderate' : 'poor', }; } } return { level: -1, label: 'unknown' }; } /** * Calculate MD5 hash */ static hashPassword(text) { return crypto_1.default.createHash('md5').update(text).digest('hex'); } /** * Build header for legacy api GET requests */ static reqHeaders(manager) { if (!manager.accountId || !manager.token) { throw new Error('Manager accountId and token must be set'); } return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': exports.BYPASS_HEADER_UA, 'accept-language': 'en', 'accountId': manager.accountId, 'appVersion': exports.APP_VERSION, 'content-type': 'application/json', 'tk': manager.token, 'tz': manager.timeZone }; } /** * Build header for api requests on 'bypass' endpoint */ static reqHeaderBypass() { return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': exports.BYPASS_HEADER_UA }; } /** * Build header for new authentication endpoints */ static reqHeaderAuth() { return { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': exports.BYPASS_HEADER_UA, 'accept-language': 'en', 'appVersion': exports.APP_VERSION, 'clientVersion': exports.CLIENT_VERSION }; } /** * Return universal keys for body of api requests */ static reqBodyBase(manager) { return { 'timeZone': manager.timeZone, 'acceptLanguage': 'en' }; } /** * Keys for authenticating api requests */ static reqBodyAuth(manager) { if (!manager.accountId || !manager.token) { throw new Error('Manager accountId and token must be set'); } return { 'accountID': manager.accountId, 'token': manager.token }; } /** * Detail keys for api requests */ static reqBodyDetails() { return { 'appVersion': exports.APP_VERSION, 'phoneBrand': exports.PHONE_BRAND, 'phoneOS': exports.PHONE_OS, 'traceId': Date.now().toString() }; } /** * Build request body for initial authentication step */ static reqBodyAuthStep1(manager, appId, terminalId, userCountryCode) { // pyvesync parity: // - Mirrors `pyvesync.models.vesync_models.RequestGetTokenModel` // - `userCountryCode` defaults to `DEFAULT_REGION` ("US") unless the caller overrides it // - `timeZone` defaults to `DEFAULT_TZ` ("America/New_York") during auth; the API later updates the // effective timezone used for device operations. return { 'email': manager.username, 'method': 'authByPWDOrOTM', 'password': this.hashPassword(manager.password), 'acceptLanguage': 'en', 'accountID': '', 'authProtocolType': 'generic', 'clientInfo': exports.PHONE_BRAND, 'clientType': 'vesyncApp', 'clientVersion': exports.CLIENT_VERSION, 'debugMode': false, 'osInfo': exports.PHONE_OS, 'terminalId': terminalId, 'timeZone': exports.DEFAULT_TZ, 'token': '', 'userCountryCode': userCountryCode.trim().toUpperCase(), 'appID': appId, 'sourceAppID': appId, 'traceId': generateAuthTraceId(terminalId) }; } /** * Build request body for second authentication step (login with authorize code) */ static reqBodyAuthStep2(authorizeCode, bizToken, _appId, terminalId, userCountryCode, regionChange) { // pyvesync parity: // - Mirrors `pyvesync.models.vesync_models.RequestLoginTokenModel` // - `timeZone` remains `DEFAULT_TZ` during auth (device timezone is handled elsewhere) // - Step 2 intentionally does NOT include `appID` / `sourceAppID`. Those are only sent in Step 1 // (`RequestGetTokenModel`). pyvesync's Step 2 request model has no appID fields. // If you see older diagnostics including appID/sourceAppID in Step 2, treat it as a superset payload // rather than a requirement — our goal here is to match the known-working pyvesync request shape. // - IMPORTANT: `bizToken` here is the *region-change token* returned by VeSync on Step 2 CROSS_REGION // errors. It is not part of the normal Step 1 -> Step 2 flow. pyvesync does not send a Step 1 // bizToken in the initial Step 2 request (Step 1 result is just `{ accountID, authorizeCode }`); // it only includes `bizToken` + `regionChange='lastRegion'` // when retrying Step 2 after a cross-region response. See `pyvesync/auth.py:_exchange_authorization_code`. const body = { 'method': 'loginByAuthorizeCode4Vesync', 'authorizeCode': authorizeCode, 'acceptLanguage': 'en', 'accountID': '', 'clientInfo': exports.PHONE_BRAND, 'clientType': 'vesyncApp', 'clientVersion': exports.CLIENT_VERSION, 'debugMode': false, 'emailSubscriptions': false, 'osInfo': exports.PHONE_OS, 'terminalId': terminalId, 'timeZone': exports.DEFAULT_TZ, 'token': '', 'userCountryCode': (userCountryCode || exports.DEFAULT_REGION).trim().toUpperCase(), 'traceId': generateAuthTraceId(terminalId) }; // Only include bizToken if it's not null if (bizToken) { body.bizToken = bizToken; } // Only include regionChange if provided if (regionChange) { body.regionChange = regionChange; } return body; } /** * Builder for body of api requests */ static reqBody(manager, type) { const body = { ...this.reqBodyBase(manager) }; if (type === 'login') { return { ...body, ...this.reqBodyDetails(), email: manager.username, password: this.hashPassword(manager.password), devToken: '', userType: exports.USER_TYPE, method: 'login' }; } const authBody = { ...body, ...this.reqBodyAuth(manager) }; if (type === 'devicestatus') { return authBody; } const fullBody = { ...authBody, ...this.reqBodyDetails() }; switch (type) { case 'devicelist': return { ...fullBody, method: 'devices', pageNo: '1', pageSize: '100' }; case 'devicedetail': return { ...fullBody, method: 'devicedetail', mobileId: exports.MOBILE_ID }; case 'bypass': return { ...fullBody, method: 'bypass' }; case 'bypassV2': return { ...fullBody, deviceRegion: exports.DEFAULT_REGION, method: 'bypassV2' }; case 'bypass_config': return { ...fullBody, method: 'firmwareUpdateInfo' }; default: return fullBody; } } /** * Call VeSync API */ static async callApi(endpoint, method, data = null, headers = {}, manager) { let url = ''; try { // Prefer manager-scoped base URL (pyvesync resolves per-manager from current region). // Keep the module-level base URL as a fallback for older callers. let baseUrl = manager.apiBaseUrl || _apiBaseUrl; // Ensure API base URL is properly set if (!baseUrl || baseUrl === 'undefined') { logger_1.logger.error('API base URL is not properly configured. Falling back to US endpoint...'); baseUrl = 'https://smartapi.vesync.com'; manager.apiBaseUrl = baseUrl; } url = baseUrl + endpoint; logger_1.logger.debug(`Making API call to: ${url}`); const response = await (0, axios_1.default)({ method, url, data, headers, timeout: exports.API_TIMEOUT }); return [response.data, response.status]; } catch (error) { if (error.response) { const responseData = error.response.data; // Check for token expiration or auth invalidation const httpStatus = error.response.status; const msg = responseData === null || responseData === void 0 ? void 0 : responseData.msg; if (httpStatus === 401 || httpStatus === 419 || (responseData === null || responseData === void 0 ? void 0 : responseData.code) === 4001004 || (typeof msg === 'string' && /token\s*expired/i.test(msg))) { logger_1.logger.debug('Token expired, attempting to re-login...'); // Re-login if (await manager.login()) { // Retry the original request logger_1.logger.debug('Re-login successful, retrying original request...'); return await this.callApi(endpoint, method, data, headers, manager); } } // Log specific error details for debugging logger_1.logger.error('API call failed with response:', { status: error.response.status, code: responseData === null || responseData === void 0 ? void 0 : responseData.code, message: responseData === null || responseData === void 0 ? void 0 : responseData.msg, url }); return [responseData, error.response.status]; } logger_1.logger.error('API call failed:', { code: error.code, message: error.message, url: url || (manager.apiBaseUrl || _apiBaseUrl || '') + endpoint }); return [null, 0]; } } /** * Calculate hex value from energy response */ static calculateHex(hexStr) { if (!hexStr || !hexStr.includes(':')) { return '0'; } const [prefix, value] = hexStr.split(':'); if (!prefix || !value) { return '0'; } try { const decoded = Buffer.from(value, 'hex'); return decoded.readFloatBE(0).toString(); } catch (error) { logger_1.logger.debug('Error decoding hex value:', error); return '0'; } } /** * Build energy dictionary from API response */ static buildEnergyDict(result) { if (!result) { return {}; } return { energy_consumption_of_today: result.energyConsumptionOfToday || 0, cost_per_kwh: result.costPerKWH || 0, max_energy: result.maxEnergy || 0, total_energy: result.totalEnergy || 0, data: result.data || [], energy_consumption: result.energy || 0, start_time: result.startTime || '', end_time: result.endTime || '' }; } /** * Build configuration dictionary from API response */ static buildConfigDict(result) { if (!result) { return {}; } return { configModule: result.configModule || '', firmwareVersion: result.currentFirmVersion || '', deviceRegion: result.deviceRegion || '', debugMode: result.debugMode || false, deviceTimezone: result.deviceTimezone || exports.DEFAULT_TZ, ...result }; } /** * Calculate MD5 hash */ static md5(text) { return crypto_1.default.createHash('md5').update(text).digest('hex'); } /** * Perform new two-step authentication flow */ static async authNewFlow(manager, appId, region = 'US', countryCodeOverride) { var _a, _b, _c; try { // Generate terminal ID to be used in both steps const terminalId = generateTerminalId(); // pyvesync parity: // - `userCountryCode` defaults to `DEFAULT_REGION` ("US"), even if we first try the EU base URL. // - If the account truly belongs to another region, the Step 2 response includes the correct // `countryCode`/`currentRegion` and a `bizToken` to retry Step 2 (handled below). // Avoid "guessing" a country code from `region` or email heuristics here; it diverges from pyvesync. const userCountryCode = (countryCodeOverride || exports.DEFAULT_REGION).trim().toUpperCase(); // Step 1: Get authorization code const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, userCountryCode); const step1Headers = this.reqHeaderAuth(); // Set the correct regional endpoint const originalUrl = manager.apiBaseUrl || getApiBaseUrl(); const knownBaseUrls = new Set(Object.values(exports.REGION_ENDPOINTS)); const hasCustomBaseUrlOverride = !!manager.apiUrlOverride || !knownBaseUrls.has(originalUrl); if (!hasCustomBaseUrlOverride && region in exports.REGION_ENDPOINTS) { const nextUrl = exports.REGION_ENDPOINTS[region]; manager.apiBaseUrl = nextUrl; } logger_1.logger.debug('Step 1: Getting authorization code...', { region, endpoint: manager.apiBaseUrl || getApiBaseUrl(), body: { ...step1Body, password: '[REDACTED]' } }); const [authResponse, authStatus] = await this.callApi('/globalPlatform/api/accountAuth/v1/authByPWDOrOTM', 'post', step1Body, step1Headers, manager); if (!authResponse || authStatus !== 200) { logger_1.logger.error('Step 1 failed:', authResponse); manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } if (authResponse.code !== 0) { logger_1.logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg); // Check if it's a credential error (no point retrying) if ((0, constants_1.isCredentialError)(authResponse.code)) { logger_1.logger.error('Credential error detected - invalid username or password'); manager.apiBaseUrl = originalUrl; return [false, null, null, 'credential_error']; } // Handle cross-region error (multiple error codes possible) if ((0, constants_1.isCrossRegionError)(authResponse.code)) { logger_1.logger.debug('Cross-region error detected:', authResponse.code, 'will try different region'); manager.apiBaseUrl = originalUrl; return [false, null, null, 'cross_region']; } manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } // pyvesync parity note: // - Step 1 "authByPWDOrOTM" returns a result shaped like RespGetTokenResultModel: // `{ accountID, authorizeCode }`. There is no Step 1 bizToken to forward. // See `pyvesync/models/vesync_models.py:RespGetTokenResultModel`. const { authorizeCode } = authResponse.result || {}; if (!authorizeCode) { logger_1.logger.error('Missing authorization code in step 1 response:', authResponse.result); manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } logger_1.logger.debug('Step 1 successful, got authorization code'); // Step 2: Login with authorization code // Use the override if provided, otherwise default to 'US' // Users should specify their actual country code in the configuration const countryCodeForStep2 = userCountryCode; // pyvesync parity note: // - Step 1 returns `authorizeCode` (and an `accountID`) but does not require/provide a bizToken for // the initial Step 2 request. // - Step 2 initial request omits `bizToken`. // - Only if Step 2 returns a CROSS_REGION error does VeSync provide `result.bizToken`, which is then // echoed back as `bizToken` along with `regionChange='lastRegion'` for a Step 2 retry. // See `pyvesync/auth.py:_exchange_authorization_code` for the canonical behavior. // - Step 2 also intentionally omits appID/sourceAppID (pyvesync parity); we still pass `appId` through // to keep the function signature stable/historical. const step2Body = this.reqBodyAuthStep2(authorizeCode, null, appId, terminalId, countryCodeForStep2); logger_1.logger.debug('Step 2: Logging in with authorization code...'); const [loginResponse, loginStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', step2Body, step1Headers, manager); logger_1.logger.debug('Step 2 response:', { status: loginStatus, code: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.code, hasResult: !!(loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.result), msg: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.msg }); if (!loginResponse || loginStatus !== 200) { logger_1.logger.error('Step 2 failed:', loginResponse); manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } if (loginResponse.code !== 0) { logger_1.logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...'); // Align with pyvesync: on cross-region errors, retry Step 2 using the server-provided // country/region and bizToken + regionChange. if ((0, constants_1.isCrossRegionError)(loginResponse.code)) { const serverCountryCode = (_a = loginResponse.result) === null || _a === void 0 ? void 0 : _a.countryCode; const serverRegion = (_b = loginResponse.result) === null || _b === void 0 ? void 0 : _b.currentRegion; const regionChangeToken = (_c = loginResponse.result) === null || _c === void 0 ? void 0 : _c.bizToken; logger_1.logger.debug('Cross-region error detected in Step 2; attempting pyvesync-style retry', { requestedRegion: region, requestedCountryCode: countryCodeForStep2, serverCountryCode, serverRegion, hasRegionChangeToken: !!regionChangeToken, }); // If VeSync tells us which region to use, switch to it for subsequent requests. if (!hasCustomBaseUrlOverride && serverRegion && serverRegion in exports.REGION_ENDPOINTS) { manager.region = serverRegion; logger_1.logger.debug(`Updated region to ${serverRegion} for Step 2 retry`); } // If VeSync gives us a region-change token, retry Step 2 without redoing Step 1. if (regionChangeToken) { const retryCountryCode = serverCountryCode || countryCodeForStep2; const retryBody = this.reqBodyAuthStep2(authorizeCode, regionChangeToken, appId, terminalId, retryCountryCode, 'lastRegion'); const [retryResponse, retryStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', retryBody, step1Headers, manager); if (retryResponse && retryStatus === 200 && retryResponse.code === 0) { const { token, accountID, countryCode } = retryResponse.result || {}; if (token && accountID) { logger_1.logger.debug('Step 2 retry successful, got token and accountID'); return [true, token, accountID, countryCode || retryCountryCode]; } logger_1.logger.error('Missing required fields in Step 2 retry response:', retryResponse.result); manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } logger_1.logger.error('Step 2 retry failed:', { status: retryStatus, code: retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.code, msg: retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.msg, }); manager.apiBaseUrl = originalUrl; return [false, null, null, 'cross_region_retry']; } logger_1.logger.error('Cross-region error returned no region-change token; cannot retry Step 2'); manager.apiBaseUrl = originalUrl; return [false, null, null, 'cross_region_retry']; } logger_1.logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg); manager.apiBaseUrl = originalUrl; return [false, null, null, null]; } const { token, accountID, countryCode } = loginResponse.result || {}; if (!token || !accountID) { logger_1.logger.error('Missing required fields in step 2 response:', loginResponse.result); return [false, null, null, null]; } logger_1.logger.debug('Step 2 successful, got token and accountID'); return [true, token, accountID, countryCode || countryCodeForStep2]; } catch (error) { logger_1.logger.error('New authentication flow error:', error); return [false, null, null, null]; } } /** * Perform legacy authentication (fallback) */ static async authLegacyFlow(manager) { try { const body = this.reqBody(manager, 'login'); logger_1.logger.debug('Legacy login attempt...', { endpoint: manager.apiBaseUrl || getApiBaseUrl(), body: { ...body, password: '[REDACTED]' } }); const [response, status] = await this.callApi('/cloud/v1/user/login', 'post', body, {}, manager); if (!response || status !== 200) { logger_1.logger.error('Legacy login failed:', response); return [false, null, null, null]; } if (response.code && response.code !== 0) { logger_1.logger.error('Legacy login error code:', response.code, 'message:', response.msg); // Check if it's a credential error if ((0, constants_1.isCredentialError)(response.code)) { logger_1.logger.error('Legacy auth: Credential error detected'); return [false, null, null, 'credential_error']; } return [false, null, null, null]; } const { token, accountID, countryCode } = response.result || {}; if (!token || !accountID) { logger_1.logger.error('Missing required fields in legacy login response:', response.result); return [false, null, null, null]; } logger_1.logger.debug('Legacy login successful'); return [true, token, accountID, countryCode]; } catch (error) { logger_1.logger.error('Legacy authentication flow error:', error); return [false, null, null, null]; } } } exports.Helpers = Helpers; Helpers.shouldRedact = true;