tsvesync
Version:
A TypeScript library for interacting with VeSync smart home devices
752 lines (751 loc) • 33.3 kB
JavaScript
"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;