tsvesync 1.0.101 → 1.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,3 +5,4 @@ export { VeSyncBulb } from './lib/vesyncBulb';
5
5
  export { VeSyncWallSwitch } from './lib/vesyncSwitchImpl';
6
6
  export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
7
7
  export { Helpers, setApiBaseUrl } from './lib/helpers';
8
+ export { CREDENTIAL_ERROR_CODES, CROSS_REGION_ERROR_CODES, isCredentialError, isCrossRegionError } from './lib/constants';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setApiBaseUrl = exports.Helpers = exports.VeSyncBaseDevice = exports.VeSyncWallSwitch = exports.VeSyncBulb = exports.VeSyncOutdoorPlug = exports.VeSyncOutlet15A = exports.VeSyncOutlet10A = exports.VeSyncOutlet7A = exports.VeSyncAirBypass = exports.VeSync = void 0;
3
+ exports.isCrossRegionError = exports.isCredentialError = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = exports.setApiBaseUrl = exports.Helpers = exports.VeSyncBaseDevice = exports.VeSyncWallSwitch = exports.VeSyncBulb = exports.VeSyncOutdoorPlug = exports.VeSyncOutlet15A = exports.VeSyncOutlet10A = exports.VeSyncOutlet7A = exports.VeSyncAirBypass = exports.VeSync = void 0;
4
4
  // Export main VeSync manager class
5
5
  var vesync_1 = require("./lib/vesync");
6
6
  Object.defineProperty(exports, "VeSync", { enumerable: true, get: function () { return vesync_1.VeSync; } });
@@ -23,3 +23,9 @@ Object.defineProperty(exports, "VeSyncBaseDevice", { enumerable: true, get: func
23
23
  var helpers_1 = require("./lib/helpers");
24
24
  Object.defineProperty(exports, "Helpers", { enumerable: true, get: function () { return helpers_1.Helpers; } });
25
25
  Object.defineProperty(exports, "setApiBaseUrl", { enumerable: true, get: function () { return helpers_1.setApiBaseUrl; } });
26
+ // Export constants
27
+ var constants_1 = require("./lib/constants");
28
+ Object.defineProperty(exports, "CREDENTIAL_ERROR_CODES", { enumerable: true, get: function () { return constants_1.CREDENTIAL_ERROR_CODES; } });
29
+ Object.defineProperty(exports, "CROSS_REGION_ERROR_CODES", { enumerable: true, get: function () { return constants_1.CROSS_REGION_ERROR_CODES; } });
30
+ Object.defineProperty(exports, "isCredentialError", { enumerable: true, get: function () { return constants_1.isCredentialError; } });
31
+ Object.defineProperty(exports, "isCrossRegionError", { enumerable: true, get: function () { return constants_1.isCrossRegionError; } });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * VeSync API Error Codes
3
+ */
4
+ export declare const CREDENTIAL_ERROR_CODES: number[];
5
+ export declare const CROSS_REGION_ERROR_CODES: number[];
6
+ export declare const APP_VERSION_ERROR_CODES: number[];
7
+ /**
8
+ * Check if an error code indicates bad credentials
9
+ */
10
+ export declare function isCredentialError(code: number): boolean;
11
+ /**
12
+ * Check if an error code indicates cross-region issue
13
+ */
14
+ export declare function isCrossRegionError(code: number): boolean;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * VeSync API Error Codes
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.APP_VERSION_ERROR_CODES = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = void 0;
7
+ exports.isCredentialError = isCredentialError;
8
+ exports.isCrossRegionError = isCrossRegionError;
9
+ // Authentication error codes that indicate bad credentials (no point retrying)
10
+ exports.CREDENTIAL_ERROR_CODES = [
11
+ -11201129, // account or password incorrect
12
+ -11202129, // the account does not exist
13
+ -11000129, // illegal argument (empty credentials)
14
+ ];
15
+ // Cross-region error codes that require region switching
16
+ exports.CROSS_REGION_ERROR_CODES = [
17
+ -11260022, // cross region error
18
+ -11261022, // access region conflict error
19
+ ];
20
+ // Other error codes
21
+ exports.APP_VERSION_ERROR_CODES = [
22
+ -11012022, // app version is too low
23
+ ];
24
+ /**
25
+ * Check if an error code indicates bad credentials
26
+ */
27
+ function isCredentialError(code) {
28
+ return exports.CREDENTIAL_ERROR_CODES.includes(code);
29
+ }
30
+ /**
31
+ * Check if an error code indicates cross-region issue
32
+ */
33
+ function isCrossRegionError(code) {
34
+ return exports.CROSS_REGION_ERROR_CODES.includes(code);
35
+ }
@@ -9,7 +9,7 @@ export declare function setCurrentRegion(region: string): void;
9
9
  export declare function getRegionFromCountryCode(countryCode: string): string;
10
10
  export declare const API_RATE_LIMIT = 30;
11
11
  export declare const API_TIMEOUT = 15000;
12
- export declare const APP_VERSION = "5.6.60";
12
+ export declare const APP_VERSION = "5.7.16";
13
13
  export declare const PHONE_BRAND = "SM N9005";
14
14
  export declare const PHONE_OS = "Android";
15
15
  export declare const CLIENT_INFO = "SM N9005";
@@ -18,7 +18,7 @@ export declare const DEFAULT_TZ = "America/New_York";
18
18
  export declare const DEFAULT_REGION = "US";
19
19
  export declare const MOBILE_ID = "1234567890123456";
20
20
  export declare const BYPASS_HEADER_UA = "okhttp/3.12.1";
21
- export declare const CLIENT_VERSION = "VeSync 5.6.60";
21
+ export declare const CLIENT_VERSION = "VeSync 5.7.16";
22
22
  export declare const REGION_ENDPOINTS: {
23
23
  US: string;
24
24
  CA: string;
@@ -26,8 +26,21 @@ export declare const REGION_ENDPOINTS: {
26
26
  JP: string;
27
27
  EU: string;
28
28
  };
29
- export declare const CROSS_REGION_ERROR_CODE = -11260022;
30
29
  export declare const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
30
+ export declare const AUTH_ERROR_CODES: {
31
+ ACCOUNT_PASSWORD_INCORRECT: number;
32
+ ILLEGAL_ARGUMENT: number;
33
+ CROSS_REGION: number;
34
+ CROSS_REGION_ALT: number;
35
+ };
36
+ /**
37
+ * Detect user's home region from email domain or country hints
38
+ */
39
+ export declare function detectUserRegion(email: string): string;
40
+ /**
41
+ * Get country code from region
42
+ */
43
+ export declare function getCountryCodeFromRegion(region: string, email?: string): string;
31
44
  export declare function generateAppId(): string;
32
45
  export declare function generateTerminalId(): string;
33
46
  export interface RequestBody {
@@ -113,7 +126,7 @@ export declare class Helpers {
113
126
  /**
114
127
  * Build request body for initial authentication step
115
128
  */
116
- static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string): Record<string, any>;
129
+ static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string, region?: string): Record<string, any>;
117
130
  /**
118
131
  * Build request body for second authentication step (login with authorize code)
119
132
  */
@@ -6,17 +6,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.Helpers = exports.APP_VERSION_TOO_LOW_ERROR_CODE = exports.CROSS_REGION_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 = void 0;
9
+ 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 = void 0;
10
10
  exports.getApiBaseUrl = getApiBaseUrl;
11
11
  exports.setApiBaseUrl = setApiBaseUrl;
12
12
  exports.getCurrentRegion = getCurrentRegion;
13
13
  exports.setCurrentRegion = setCurrentRegion;
14
14
  exports.getRegionFromCountryCode = getRegionFromCountryCode;
15
+ exports.detectUserRegion = detectUserRegion;
16
+ exports.getCountryCodeFromRegion = getCountryCodeFromRegion;
15
17
  exports.generateAppId = generateAppId;
16
18
  exports.generateTerminalId = generateTerminalId;
17
19
  const axios_1 = __importDefault(require("axios"));
18
20
  const crypto_1 = __importDefault(require("crypto"));
19
21
  const logger_1 = require("./logger");
22
+ const constants_1 = require("./constants");
20
23
  // API configuration - Support regional endpoints
21
24
  let _apiBaseUrl = 'https://smartapi.vesync.com';
22
25
  let _currentRegion = 'US';
@@ -51,7 +54,7 @@ function getRegionFromCountryCode(countryCode) {
51
54
  }
52
55
  exports.API_RATE_LIMIT = 30;
53
56
  exports.API_TIMEOUT = 15000;
54
- exports.APP_VERSION = '5.6.60';
57
+ exports.APP_VERSION = '5.7.16'; // Updated to newer version
55
58
  exports.PHONE_BRAND = 'SM N9005';
56
59
  exports.PHONE_OS = 'Android';
57
60
  exports.CLIENT_INFO = 'SM N9005';
@@ -60,7 +63,7 @@ exports.DEFAULT_TZ = 'America/New_York';
60
63
  exports.DEFAULT_REGION = 'US';
61
64
  exports.MOBILE_ID = '1234567890123456';
62
65
  exports.BYPASS_HEADER_UA = 'okhttp/3.12.1';
63
- exports.CLIENT_VERSION = 'VeSync 5.6.60';
66
+ exports.CLIENT_VERSION = 'VeSync 5.7.16';
64
67
  // Regional API endpoints
65
68
  exports.REGION_ENDPOINTS = {
66
69
  'US': 'https://smartapi.vesync.com',
@@ -69,12 +72,57 @@ exports.REGION_ENDPOINTS = {
69
72
  'JP': 'https://smartapi.vesync.com',
70
73
  'EU': 'https://smartapi.vesync.eu'
71
74
  };
72
- // Cross-region error code
73
- exports.CROSS_REGION_ERROR_CODE = -11260022;
75
+ // Cross-region error codes - multiple versions exist
76
+ // CROSS_REGION_ERROR_CODES moved to constants.ts
74
77
  exports.APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
78
+ // Authentication error codes
79
+ exports.AUTH_ERROR_CODES = {
80
+ ACCOUNT_PASSWORD_INCORRECT: -11201129,
81
+ ILLEGAL_ARGUMENT: -11000022,
82
+ CROSS_REGION: -11260022,
83
+ CROSS_REGION_ALT: -11261022
84
+ };
85
+ /**
86
+ * Detect user's home region from email domain or country hints
87
+ */
88
+ function detectUserRegion(email) {
89
+ // Check for common EU email domains
90
+ const euDomains = ['.de', '.fr', '.it', '.es', '.nl', '.be', '.at', '.dk', '.se', '.no', '.fi', '.eu'];
91
+ const emailLower = email.toLowerCase();
92
+ for (const domain of euDomains) {
93
+ if (emailLower.includes(domain)) {
94
+ return 'EU';
95
+ }
96
+ }
97
+ // Default to US for unknown domains
98
+ return 'US';
99
+ }
100
+ /**
101
+ * Get country code from region
102
+ */
103
+ function getCountryCodeFromRegion(region, email) {
104
+ if (region === 'EU') {
105
+ // Try to detect specific EU country from email
106
+ if (email) {
107
+ const emailLower = email.toLowerCase();
108
+ if (emailLower.includes('.de') || emailLower.includes('german'))
109
+ return 'DE';
110
+ if (emailLower.includes('.fr'))
111
+ return 'FR';
112
+ if (emailLower.includes('.it'))
113
+ return 'IT';
114
+ if (emailLower.includes('.es'))
115
+ return 'ES';
116
+ if (emailLower.includes('.nl'))
117
+ return 'NL';
118
+ }
119
+ return 'DE'; // Default to Germany for EU
120
+ }
121
+ return 'US'; // Default for non-EU
122
+ }
75
123
  // Generate unique APP_ID for each session
76
124
  function generateAppId() {
77
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
125
+ const chars = 'ABCDEFGHIJKLMNOPqRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
78
126
  let result = '';
79
127
  for (let i = 0; i < 8; i++) {
80
128
  result += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -131,7 +179,9 @@ class Helpers {
131
179
  return {
132
180
  'Content-Type': 'application/json; charset=UTF-8',
133
181
  'User-Agent': exports.BYPASS_HEADER_UA,
134
- 'accept-language': 'en'
182
+ 'accept-language': 'en',
183
+ 'appVersion': exports.APP_VERSION,
184
+ 'clientVersion': exports.CLIENT_VERSION
135
185
  };
136
186
  }
137
187
  /**
@@ -169,7 +219,16 @@ class Helpers {
169
219
  /**
170
220
  * Build request body for initial authentication step
171
221
  */
172
- static reqBodyAuthStep1(manager, appId, terminalId) {
222
+ static reqBodyAuthStep1(manager, appId, terminalId, region = 'US') {
223
+ // Detect user's home region and country code
224
+ const userRegion = detectUserRegion(manager.username);
225
+ const userCountryCode = getCountryCodeFromRegion(region === 'EU' ? 'EU' : userRegion, manager.username);
226
+ logger_1.logger.debug('Authentication Step 1 - Region detection:', {
227
+ email: manager.username,
228
+ detectedRegion: userRegion,
229
+ requestRegion: region,
230
+ userCountryCode: userCountryCode
231
+ });
173
232
  return {
174
233
  'email': manager.username,
175
234
  'method': 'authByPWDOrOTM',
@@ -185,7 +244,7 @@ class Helpers {
185
244
  'terminalId': terminalId,
186
245
  'timeZone': manager.timeZone || exports.DEFAULT_TZ,
187
246
  'token': '',
188
- 'userCountryCode': 'US',
247
+ 'userCountryCode': userCountryCode,
189
248
  'appID': appId,
190
249
  'sourceAppID': appId,
191
250
  'traceId': `APP${appId}${Math.floor(Date.now() / 1000)}`
@@ -402,7 +461,7 @@ class Helpers {
402
461
  // Generate terminal ID to be used in both steps
403
462
  const terminalId = generateTerminalId();
404
463
  // Step 1: Get authorization code
405
- const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId);
464
+ const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, region);
406
465
  const step1Headers = this.reqHeaderAuth();
407
466
  // Set the correct regional endpoint
408
467
  const originalUrl = getApiBaseUrl();
@@ -422,16 +481,22 @@ class Helpers {
422
481
  }
423
482
  if (authResponse.code !== 0) {
424
483
  logger_1.logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
425
- // Handle cross-region error
426
- if (authResponse.code === exports.CROSS_REGION_ERROR_CODE) {
427
- logger_1.logger.debug('Cross-region error detected, will try different region');
484
+ // Check if it's a credential error (no point retrying)
485
+ if ((0, constants_1.isCredentialError)(authResponse.code)) {
486
+ logger_1.logger.error('Credential error detected - invalid username or password');
487
+ setApiBaseUrl(originalUrl); // Restore original URL
488
+ return [false, null, null, 'credential_error'];
489
+ }
490
+ // Handle cross-region error (multiple error codes possible)
491
+ if ((0, constants_1.isCrossRegionError)(authResponse.code)) {
492
+ logger_1.logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
428
493
  setApiBaseUrl(originalUrl); // Restore original URL
429
494
  return [false, null, null, 'cross_region'];
430
495
  }
431
496
  setApiBaseUrl(originalUrl); // Restore original URL
432
497
  return [false, null, null, null];
433
498
  }
434
- const { authorizeCode, bizToken, userCountryCode } = authResponse.result || {};
499
+ const { authorizeCode, bizToken } = authResponse.result || {};
435
500
  if (!authorizeCode) {
436
501
  logger_1.logger.error('Missing authorization code in step 1 response:', authResponse.result);
437
502
  setApiBaseUrl(originalUrl); // Restore original URL
@@ -439,59 +504,31 @@ class Helpers {
439
504
  }
440
505
  logger_1.logger.debug('Step 1 successful, got authorization code');
441
506
  // Step 2: Login with authorization code
442
- const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, userCountryCode);
507
+ // Use the appropriate country code based on the region we're authenticating with
508
+ const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
509
+ const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
443
510
  logger_1.logger.debug('Step 2: Logging in with authorization code...');
444
511
  const [loginResponse, loginStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', step2Body, step1Headers, manager);
512
+ logger_1.logger.debug('Step 2 response:', {
513
+ status: loginStatus,
514
+ code: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.code,
515
+ hasResult: !!(loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.result),
516
+ msg: loginResponse === null || loginResponse === void 0 ? void 0 : loginResponse.msg
517
+ });
445
518
  if (!loginResponse || loginStatus !== 200) {
446
519
  logger_1.logger.error('Step 2 failed:', loginResponse);
447
520
  setApiBaseUrl(originalUrl); // Restore original URL on failure
448
521
  return [false, null, null, null];
449
522
  }
450
523
  if (loginResponse.code !== 0) {
451
- // Handle cross-region error with retry (like pyvesync PR #340)
452
- if (loginResponse.code === exports.CROSS_REGION_ERROR_CODE) {
453
- logger_1.logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
454
- const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
455
- if (regionChangeToken && newCountryCode) {
456
- // Retry Step 2 with region change token on the SAME endpoint
457
- // The region change token tells the server to handle the cross-region authentication
458
- const retryBody = {
459
- method: 'loginByAuthorizeCode4Vesync',
460
- authorizeCode: authorizeCode,
461
- acceptLanguage: 'en',
462
- clientInfo: exports.CLIENT_INFO,
463
- clientType: 'vesyncApp',
464
- clientVersion: exports.CLIENT_VERSION,
465
- debugMode: false,
466
- emailSubscriptions: false,
467
- osInfo: exports.PHONE_OS,
468
- terminalId: terminalId,
469
- timeZone: exports.DEFAULT_TZ,
470
- userCountryCode: newCountryCode,
471
- traceId: `APP${appId}${Math.floor(Date.now() / 1000)}`,
472
- bizToken: regionChangeToken,
473
- regionChange: 'last_region'
474
- };
475
- logger_1.logger.debug('Retrying Step 2 with region change token...', {
476
- newCountryCode,
477
- currentEndpoint: getApiBaseUrl()
478
- });
479
- const [retryResponse, retryStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', retryBody, step1Headers, manager);
480
- if (retryResponse && retryResponse.code === 0) {
481
- const { token, accountID, countryCode } = retryResponse.result || {};
482
- if (token && accountID) {
483
- logger_1.logger.debug('Step 2 retry successful with region change');
484
- setApiBaseUrl(originalUrl); // Restore original URL on success
485
- return [true, token, accountID, countryCode || newCountryCode];
486
- }
487
- }
488
- logger_1.logger.error('Step 2 retry failed:', retryResponse);
489
- setApiBaseUrl(originalUrl); // Restore original URL on failure
490
- return [false, null, null, null];
491
- }
492
- else {
493
- logger_1.logger.error('No region change token in cross-region error response');
494
- }
524
+ logger_1.logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...');
525
+ // Handle cross-region error - for EU accounts, we need to switch regions entirely
526
+ if (constants_1.CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
527
+ logger_1.logger.debug('Cross-region error detected in Step 2 - need to use different region');
528
+ // For cross-region errors, we need to retry the entire flow with a different region
529
+ // The authorization code from Step 1 is tied to the endpoint it was obtained from
530
+ setApiBaseUrl(originalUrl); // Restore original URL
531
+ return [false, null, null, 'cross_region_retry'];
495
532
  }
496
533
  logger_1.logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
497
534
  setApiBaseUrl(originalUrl); // Restore original URL on error
@@ -504,7 +541,7 @@ class Helpers {
504
541
  return [false, null, null, null];
505
542
  }
506
543
  logger_1.logger.debug('Step 2 successful, got token and accountID');
507
- return [true, token, accountID, countryCode || userCountryCode];
544
+ return [true, token, accountID, countryCode || countryCodeForStep2];
508
545
  }
509
546
  catch (error) {
510
547
  logger_1.logger.error('New authentication flow error:', error);
@@ -528,6 +565,11 @@ class Helpers {
528
565
  }
529
566
  if (response.code && response.code !== 0) {
530
567
  logger_1.logger.error('Legacy login error code:', response.code, 'message:', response.msg);
568
+ // Check if it's a credential error
569
+ if ((0, constants_1.isCredentialError)(response.code)) {
570
+ logger_1.logger.error('Legacy auth: Credential error detected');
571
+ return [false, null, null, 'credential_error'];
572
+ }
531
573
  return [false, null, null, null];
532
574
  }
533
575
  const { token, accountID, countryCode } = response.result || {};
@@ -36,6 +36,8 @@ export declare class VeSync {
36
36
  enabled: boolean;
37
37
  updateInterval: number;
38
38
  timeZone: string;
39
+ authFlowUsed?: 'legacy' | 'new';
40
+ apiBaseUrl?: string;
39
41
  fans: VeSyncBaseDevice[];
40
42
  outlets: VeSyncBaseDevice[];
41
43
  switches: VeSyncBaseDevice[];
@@ -46,13 +48,21 @@ export declare class VeSync {
46
48
  * @param username - VeSync account username
47
49
  * @param password - VeSync account password
48
50
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
49
- * @param debug - Optional debug mode flag
50
- * @param redact - Optional redact mode flag
51
- * @param apiUrl - Optional API base URL override
52
- * @param customLogger - Optional custom logger implementation
53
- * @param excludeConfig - Optional device exclusion configuration
54
- */
55
- constructor(username: string, password: string, timeZone?: string, debug?: boolean, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
51
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
52
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
53
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
54
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
55
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
56
+ */
57
+ constructor(username: string, password: string, timeZone?: string, optionsOrDebug?: boolean | {
58
+ debug?: boolean;
59
+ redact?: boolean;
60
+ apiUrl?: string;
61
+ customLogger?: Logger;
62
+ excludeConfig?: ExcludeConfig;
63
+ region?: string;
64
+ debugMode?: boolean;
65
+ }, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
56
66
  /**
57
67
  * Get/Set debug mode
58
68
  */
@@ -125,22 +125,37 @@ class VeSync {
125
125
  * @param username - VeSync account username
126
126
  * @param password - VeSync account password
127
127
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
128
- * @param debug - Optional debug mode flag
129
- * @param redact - Optional redact mode flag
130
- * @param apiUrl - Optional API base URL override
131
- * @param customLogger - Optional custom logger implementation
132
- * @param excludeConfig - Optional device exclusion configuration
128
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
129
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
130
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
131
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
132
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
133
133
  */
134
- constructor(username, password, timeZone = helpers_1.DEFAULT_TZ, debug = false, redact = true, apiUrl, customLogger, excludeConfig) {
135
- this._debug = debug;
136
- this._redact = redact;
134
+ constructor(username, password, timeZone = helpers_1.DEFAULT_TZ, optionsOrDebug = false, redact = true, apiUrl, customLogger, excludeConfig) {
135
+ // Handle options object or backward compatible parameters
136
+ let options = {};
137
+ if (typeof optionsOrDebug === 'object' && optionsOrDebug !== null) {
138
+ // Using new options pattern
139
+ options = optionsOrDebug;
140
+ this._debug = options.debug || options.debugMode || false;
141
+ this._redact = options.redact !== undefined ? options.redact : true;
142
+ this._excludeConfig = options.excludeConfig || null;
143
+ this._region = options.region || 'US';
144
+ customLogger = options.customLogger;
145
+ apiUrl = options.apiUrl;
146
+ }
147
+ else {
148
+ // Using old parameter pattern for backward compatibility
149
+ this._debug = optionsOrDebug;
150
+ this._redact = redact;
151
+ this._excludeConfig = excludeConfig || null;
152
+ this._region = 'US';
153
+ }
137
154
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
138
155
  this._energyCheck = true;
139
156
  this._lastUpdateTs = null;
140
157
  this._inProcess = false;
141
- this._excludeConfig = excludeConfig || null;
142
158
  this._appId = (0, helpers_1.generateAppId)();
143
- this._region = 'US';
144
159
  this.username = username;
145
160
  this.password = password;
146
161
  this.token = null;
@@ -175,25 +190,28 @@ class VeSync {
175
190
  this.timeZone = helpers_1.DEFAULT_TZ;
176
191
  logger_1.logger.debug('Time zone is not a string');
177
192
  }
178
- // Set custom API URL if provided, otherwise use default US endpoint
193
+ // Set custom API URL if provided, otherwise use region-based endpoint
179
194
  if (apiUrl) {
180
195
  (0, helpers_1.setApiBaseUrl)(apiUrl);
181
196
  }
182
197
  else {
183
- // Start with US endpoint as default
184
- (0, helpers_1.setApiBaseUrl)('https://smartapi.vesync.com');
185
- (0, helpers_1.setCurrentRegion)('US');
198
+ // Set endpoint based on region
199
+ const regionEndpoints = {
200
+ 'US': 'https://smartapi.vesync.com',
201
+ 'EU': 'https://smartapi.vesync.eu',
202
+ 'CA': 'https://smartapi.vesync.com',
203
+ 'MX': 'https://smartapi.vesync.com',
204
+ 'JP': 'https://smartapi.vesync.com'
205
+ };
206
+ const endpoint = regionEndpoints[this._region] || 'https://smartapi.vesync.com';
207
+ (0, helpers_1.setApiBaseUrl)(endpoint);
208
+ (0, helpers_1.setCurrentRegion)(this._region);
186
209
  }
187
210
  // Set custom logger if provided
188
211
  if (customLogger) {
189
212
  (0, logger_1.setLogger)(customLogger);
190
213
  }
191
- if (debug) {
192
- this.debug = true;
193
- }
194
- if (redact) {
195
- this.redact = true;
196
- }
214
+ // Debug and redact are already set above in the options handling
197
215
  }
198
216
  /**
199
217
  * Get/Set debug mode
@@ -443,20 +461,55 @@ class VeSync {
443
461
  for (let attempt = 0; attempt < retryAttempts; attempt++) {
444
462
  try {
445
463
  logger_1.logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
446
- // Try new authentication flow first
464
+ // Skip email-based region detection for now since we'll handle cross-region errors
465
+ // The cross-region error handling below will automatically switch to the correct region
466
+ // Try new authentication flow first with detected region
447
467
  let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, this._region);
468
+ // Track if new flow succeeded
469
+ if (success) {
470
+ this.authFlowUsed = 'new';
471
+ }
448
472
  if (!success) {
473
+ // Check if it's a credential error (no point retrying)
474
+ if (countryCode === 'credential_error') {
475
+ logger_1.logger.error('Authentication failed due to invalid credentials');
476
+ return false; // Exit immediately, don't retry
477
+ }
449
478
  // Check if we need to try a different region
450
- if (countryCode === 'cross_region') {
451
- logger_1.logger.debug('Cross-region error detected, trying EU region...');
452
- this._region = 'EU';
453
- (0, helpers_1.setCurrentRegion)('EU');
454
- [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, 'EU');
479
+ if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
480
+ logger_1.logger.debug('Cross-region error detected, trying opposite region...');
481
+ const alternateRegion = this._region === 'US' ? 'EU' : 'US';
482
+ logger_1.logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
483
+ this._region = alternateRegion;
484
+ (0, helpers_1.setCurrentRegion)(alternateRegion);
485
+ // Update the API endpoint for the new region
486
+ const regionEndpoints = {
487
+ 'US': 'https://smartapi.vesync.com',
488
+ 'EU': 'https://smartapi.vesync.eu'
489
+ };
490
+ if (alternateRegion in regionEndpoints) {
491
+ (0, helpers_1.setApiBaseUrl)(regionEndpoints[alternateRegion]);
492
+ logger_1.logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
493
+ }
494
+ // Retry with the alternate region
495
+ [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion);
496
+ if (success) {
497
+ this.authFlowUsed = 'new';
498
+ logger_1.logger.debug(`Authentication successful with ${alternateRegion} region`);
499
+ }
455
500
  }
456
501
  // If new flow still fails, try legacy authentication as fallback
457
502
  if (!success) {
458
503
  logger_1.logger.debug('New authentication flow failed, trying legacy flow...');
459
504
  [success, token, accountId, countryCode] = await helpers_1.Helpers.authLegacyFlow(this);
505
+ if (success) {
506
+ this.authFlowUsed = 'legacy';
507
+ }
508
+ else if (countryCode === 'credential_error') {
509
+ // Legacy auth also detected bad credentials
510
+ logger_1.logger.error('Both authentication methods report invalid credentials');
511
+ return false; // Exit immediately
512
+ }
460
513
  }
461
514
  }
462
515
  if (success && token && accountId) {
@@ -464,6 +517,7 @@ class VeSync {
464
517
  this.accountId = accountId;
465
518
  this.countryCode = countryCode;
466
519
  this.enabled = true;
520
+ this.apiBaseUrl = (0, helpers_1.getApiBaseUrl)(); // Track the API endpoint being used
467
521
  // Set region based on country code if we got one
468
522
  if (countryCode) {
469
523
  const detectedRegion = (0, helpers_1.getRegionFromCountryCode)(countryCode);
@@ -474,6 +528,7 @@ class VeSync {
474
528
  }
475
529
  }
476
530
  logger_1.logger.debug('Authentication successful!', {
531
+ authFlow: this.authFlowUsed,
477
532
  region: this._region,
478
533
  countryCode: countryCode,
479
534
  endpoint: (0, helpers_1.getApiBaseUrl)()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.0.101",
3
+ "version": "1.0.103",
4
4
  "description": "A TypeScript library for interacting with VeSync smart home devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -11,4 +11,12 @@ export { VeSyncWallSwitch } from './lib/vesyncSwitchImpl';
11
11
  export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
12
12
 
13
13
  // Export helper functions
14
- export { Helpers, setApiBaseUrl } from './lib/helpers';
14
+ export { Helpers, setApiBaseUrl } from './lib/helpers';
15
+
16
+ // Export constants
17
+ export {
18
+ CREDENTIAL_ERROR_CODES,
19
+ CROSS_REGION_ERROR_CODES,
20
+ isCredentialError,
21
+ isCrossRegionError
22
+ } from './lib/constants';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * VeSync API Error Codes
3
+ */
4
+
5
+ // Authentication error codes that indicate bad credentials (no point retrying)
6
+ export const CREDENTIAL_ERROR_CODES = [
7
+ -11201129, // account or password incorrect
8
+ -11202129, // the account does not exist
9
+ -11000129, // illegal argument (empty credentials)
10
+ ];
11
+
12
+ // Cross-region error codes that require region switching
13
+ export const CROSS_REGION_ERROR_CODES = [
14
+ -11260022, // cross region error
15
+ -11261022, // access region conflict error
16
+ ];
17
+
18
+ // Other error codes
19
+ export const APP_VERSION_ERROR_CODES = [
20
+ -11012022, // app version is too low
21
+ ];
22
+
23
+ /**
24
+ * Check if an error code indicates bad credentials
25
+ */
26
+ export function isCredentialError(code: number): boolean {
27
+ return CREDENTIAL_ERROR_CODES.includes(code);
28
+ }
29
+
30
+ /**
31
+ * Check if an error code indicates cross-region issue
32
+ */
33
+ export function isCrossRegionError(code: number): boolean {
34
+ return CROSS_REGION_ERROR_CODES.includes(code);
35
+ }
@@ -6,6 +6,7 @@ import axios from 'axios';
6
6
  import crypto from 'crypto';
7
7
  import { VeSync } from './vesync';
8
8
  import { logger } from './logger';
9
+ import { isCredentialError, isCrossRegionError, CROSS_REGION_ERROR_CODES } from './constants';
9
10
 
10
11
  // API configuration - Support regional endpoints
11
12
  let _apiBaseUrl = 'https://smartapi.vesync.com';
@@ -51,7 +52,7 @@ export function getRegionFromCountryCode(countryCode: string): string {
51
52
  export const API_RATE_LIMIT = 30;
52
53
  export const API_TIMEOUT = 15000;
53
54
 
54
- export const APP_VERSION = '5.6.60';
55
+ export const APP_VERSION = '5.7.16'; // Updated to newer version
55
56
  export const PHONE_BRAND = 'SM N9005';
56
57
  export const PHONE_OS = 'Android';
57
58
  export const CLIENT_INFO = 'SM N9005';
@@ -60,7 +61,7 @@ export const DEFAULT_TZ = 'America/New_York';
60
61
  export const DEFAULT_REGION = 'US';
61
62
  export const MOBILE_ID = '1234567890123456';
62
63
  export const BYPASS_HEADER_UA = 'okhttp/3.12.1';
63
- export const CLIENT_VERSION = 'VeSync 5.6.60';
64
+ export const CLIENT_VERSION = 'VeSync 5.7.16';
64
65
 
65
66
  // Regional API endpoints
66
67
  export const REGION_ENDPOINTS = {
@@ -71,13 +72,58 @@ export const REGION_ENDPOINTS = {
71
72
  'EU': 'https://smartapi.vesync.eu'
72
73
  };
73
74
 
74
- // Cross-region error code
75
- export const CROSS_REGION_ERROR_CODE = -11260022;
75
+ // Cross-region error codes - multiple versions exist
76
+ // CROSS_REGION_ERROR_CODES moved to constants.ts
76
77
  export const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
77
78
 
79
+ // Authentication error codes
80
+ export const AUTH_ERROR_CODES = {
81
+ ACCOUNT_PASSWORD_INCORRECT: -11201129,
82
+ ILLEGAL_ARGUMENT: -11000022,
83
+ CROSS_REGION: -11260022,
84
+ CROSS_REGION_ALT: -11261022
85
+ };
86
+
87
+ /**
88
+ * Detect user's home region from email domain or country hints
89
+ */
90
+ export function detectUserRegion(email: string): string {
91
+ // Check for common EU email domains
92
+ const euDomains = ['.de', '.fr', '.it', '.es', '.nl', '.be', '.at', '.dk', '.se', '.no', '.fi', '.eu'];
93
+ const emailLower = email.toLowerCase();
94
+
95
+ for (const domain of euDomains) {
96
+ if (emailLower.includes(domain)) {
97
+ return 'EU';
98
+ }
99
+ }
100
+
101
+ // Default to US for unknown domains
102
+ return 'US';
103
+ }
104
+
105
+ /**
106
+ * Get country code from region
107
+ */
108
+ export function getCountryCodeFromRegion(region: string, email?: string): string {
109
+ if (region === 'EU') {
110
+ // Try to detect specific EU country from email
111
+ if (email) {
112
+ const emailLower = email.toLowerCase();
113
+ if (emailLower.includes('.de') || emailLower.includes('german')) return 'DE';
114
+ if (emailLower.includes('.fr')) return 'FR';
115
+ if (emailLower.includes('.it')) return 'IT';
116
+ if (emailLower.includes('.es')) return 'ES';
117
+ if (emailLower.includes('.nl')) return 'NL';
118
+ }
119
+ return 'DE'; // Default to Germany for EU
120
+ }
121
+ return 'US'; // Default for non-EU
122
+ }
123
+
78
124
  // Generate unique APP_ID for each session
79
125
  export function generateAppId(): string {
80
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
126
+ const chars = 'ABCDEFGHIJKLMNOPqRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
81
127
  let result = '';
82
128
  for (let i = 0; i < 8; i++) {
83
129
  result += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -196,7 +242,9 @@ export class Helpers {
196
242
  return {
197
243
  'Content-Type': 'application/json; charset=UTF-8',
198
244
  'User-Agent': BYPASS_HEADER_UA,
199
- 'accept-language': 'en'
245
+ 'accept-language': 'en',
246
+ 'appVersion': APP_VERSION,
247
+ 'clientVersion': CLIENT_VERSION
200
248
  };
201
249
  }
202
250
 
@@ -238,7 +286,18 @@ export class Helpers {
238
286
  /**
239
287
  * Build request body for initial authentication step
240
288
  */
241
- static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string): Record<string, any> {
289
+ static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string, region: string = 'US'): Record<string, any> {
290
+ // Detect user's home region and country code
291
+ const userRegion = detectUserRegion(manager.username);
292
+ const userCountryCode = getCountryCodeFromRegion(region === 'EU' ? 'EU' : userRegion, manager.username);
293
+
294
+ logger.debug('Authentication Step 1 - Region detection:', {
295
+ email: manager.username,
296
+ detectedRegion: userRegion,
297
+ requestRegion: region,
298
+ userCountryCode: userCountryCode
299
+ });
300
+
242
301
  return {
243
302
  'email': manager.username,
244
303
  'method': 'authByPWDOrOTM',
@@ -254,7 +313,7 @@ export class Helpers {
254
313
  'terminalId': terminalId,
255
314
  'timeZone': manager.timeZone || DEFAULT_TZ,
256
315
  'token': '',
257
- 'userCountryCode': 'US',
316
+ 'userCountryCode': userCountryCode,
258
317
  'appID': appId,
259
318
  'sourceAppID': appId,
260
319
  'traceId': `APP${appId}${Math.floor(Date.now() / 1000)}`
@@ -504,7 +563,7 @@ export class Helpers {
504
563
  const terminalId = generateTerminalId();
505
564
 
506
565
  // Step 1: Get authorization code
507
- const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId);
566
+ const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, region);
508
567
  const step1Headers = this.reqHeaderAuth();
509
568
 
510
569
  // Set the correct regional endpoint
@@ -536,9 +595,16 @@ export class Helpers {
536
595
  if (authResponse.code !== 0) {
537
596
  logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
538
597
 
539
- // Handle cross-region error
540
- if (authResponse.code === CROSS_REGION_ERROR_CODE) {
541
- logger.debug('Cross-region error detected, will try different region');
598
+ // Check if it's a credential error (no point retrying)
599
+ if (isCredentialError(authResponse.code)) {
600
+ logger.error('Credential error detected - invalid username or password');
601
+ setApiBaseUrl(originalUrl); // Restore original URL
602
+ return [false, null, null, 'credential_error'];
603
+ }
604
+
605
+ // Handle cross-region error (multiple error codes possible)
606
+ if (isCrossRegionError(authResponse.code)) {
607
+ logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
542
608
  setApiBaseUrl(originalUrl); // Restore original URL
543
609
  return [false, null, null, 'cross_region'];
544
610
  }
@@ -547,7 +613,7 @@ export class Helpers {
547
613
  return [false, null, null, null];
548
614
  }
549
615
 
550
- const { authorizeCode, bizToken, userCountryCode } = authResponse.result || {};
616
+ const { authorizeCode, bizToken } = authResponse.result || {};
551
617
 
552
618
  if (!authorizeCode) {
553
619
  logger.error('Missing authorization code in step 1 response:', authResponse.result);
@@ -558,7 +624,9 @@ export class Helpers {
558
624
  logger.debug('Step 1 successful, got authorization code');
559
625
 
560
626
  // Step 2: Login with authorization code
561
- const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, userCountryCode);
627
+ // Use the appropriate country code based on the region we're authenticating with
628
+ const countryCodeForStep2 = region === 'EU' ? 'DE' : 'US';
629
+ const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, terminalId, countryCodeForStep2);
562
630
 
563
631
  logger.debug('Step 2: Logging in with authorization code...');
564
632
 
@@ -570,6 +638,13 @@ export class Helpers {
570
638
  manager
571
639
  );
572
640
 
641
+ logger.debug('Step 2 response:', {
642
+ status: loginStatus,
643
+ code: loginResponse?.code,
644
+ hasResult: !!loginResponse?.result,
645
+ msg: loginResponse?.msg
646
+ });
647
+
573
648
  if (!loginResponse || loginStatus !== 200) {
574
649
  logger.error('Step 2 failed:', loginResponse);
575
650
  setApiBaseUrl(originalUrl); // Restore original URL on failure
@@ -577,61 +652,14 @@ export class Helpers {
577
652
  }
578
653
 
579
654
  if (loginResponse.code !== 0) {
580
- // Handle cross-region error with retry (like pyvesync PR #340)
581
- if (loginResponse.code === CROSS_REGION_ERROR_CODE) {
582
- logger.debug('Cross-region error detected in Step 2, retrying with region change token...');
583
-
584
- const { bizToken: regionChangeToken, countryCode: newCountryCode } = loginResponse.result || {};
585
-
586
- if (regionChangeToken && newCountryCode) {
587
- // Retry Step 2 with region change token on the SAME endpoint
588
- // The region change token tells the server to handle the cross-region authentication
589
- const retryBody = {
590
- method: 'loginByAuthorizeCode4Vesync',
591
- authorizeCode: authorizeCode,
592
- acceptLanguage: 'en',
593
- clientInfo: CLIENT_INFO,
594
- clientType: 'vesyncApp',
595
- clientVersion: CLIENT_VERSION,
596
- debugMode: false,
597
- emailSubscriptions: false,
598
- osInfo: PHONE_OS,
599
- terminalId: terminalId,
600
- timeZone: DEFAULT_TZ,
601
- userCountryCode: newCountryCode,
602
- traceId: `APP${appId}${Math.floor(Date.now() / 1000)}`,
603
- bizToken: regionChangeToken,
604
- regionChange: 'last_region'
605
- };
606
-
607
- logger.debug('Retrying Step 2 with region change token...', {
608
- newCountryCode,
609
- currentEndpoint: getApiBaseUrl()
610
- });
611
-
612
- const [retryResponse, retryStatus] = await this.callApi(
613
- '/user/api/accountManage/v1/loginByAuthorizeCode4Vesync',
614
- 'post',
615
- retryBody,
616
- step1Headers,
617
- manager
618
- );
619
-
620
- if (retryResponse && retryResponse.code === 0) {
621
- const { token, accountID, countryCode } = retryResponse.result || {};
622
- if (token && accountID) {
623
- logger.debug('Step 2 retry successful with region change');
624
- setApiBaseUrl(originalUrl); // Restore original URL on success
625
- return [true, token, accountID, countryCode || newCountryCode];
626
- }
627
- }
628
-
629
- logger.error('Step 2 retry failed:', retryResponse);
630
- setApiBaseUrl(originalUrl); // Restore original URL on failure
631
- return [false, null, null, null];
632
- } else {
633
- logger.error('No region change token in cross-region error response');
634
- }
655
+ logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...');
656
+ // Handle cross-region error - for EU accounts, we need to switch regions entirely
657
+ if (CROSS_REGION_ERROR_CODES.includes(loginResponse.code)) {
658
+ logger.debug('Cross-region error detected in Step 2 - need to use different region');
659
+ // For cross-region errors, we need to retry the entire flow with a different region
660
+ // The authorization code from Step 1 is tied to the endpoint it was obtained from
661
+ setApiBaseUrl(originalUrl); // Restore original URL
662
+ return [false, null, null, 'cross_region_retry'];
635
663
  }
636
664
 
637
665
  logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
@@ -649,7 +677,7 @@ export class Helpers {
649
677
  }
650
678
 
651
679
  logger.debug('Step 2 successful, got token and accountID');
652
- return [true, token, accountID, countryCode || userCountryCode];
680
+ return [true, token, accountID, countryCode || countryCodeForStep2];
653
681
 
654
682
  } catch (error) {
655
683
  logger.error('New authentication flow error:', error);
@@ -684,6 +712,13 @@ export class Helpers {
684
712
 
685
713
  if (response.code && response.code !== 0) {
686
714
  logger.error('Legacy login error code:', response.code, 'message:', response.msg);
715
+
716
+ // Check if it's a credential error
717
+ if (isCredentialError(response.code)) {
718
+ logger.error('Legacy auth: Credential error detected');
719
+ return [false, null, null, 'credential_error'];
720
+ }
721
+
687
722
  return [false, null, null, null];
688
723
  }
689
724
 
package/src/lib/vesync.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * VeSync API Device Library
3
3
  */
4
4
 
5
- import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS } from './helpers';
5
+ import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS, getCountryCodeFromRegion } from './helpers';
6
6
  import { VeSyncBaseDevice } from './vesyncBaseDevice';
7
7
  import { fanModules } from './vesyncFanImpl';
8
8
  import { outletModules } from './vesyncOutletImpl';
@@ -171,6 +171,8 @@ export class VeSync {
171
171
  enabled: boolean;
172
172
  updateInterval: number;
173
173
  timeZone: string;
174
+ authFlowUsed?: 'legacy' | 'new'; // Track which auth flow was used
175
+ apiBaseUrl?: string; // Track which API endpoint is being used
174
176
 
175
177
  fans: VeSyncBaseDevice[];
176
178
  outlets: VeSyncBaseDevice[];
@@ -183,31 +185,54 @@ export class VeSync {
183
185
  * @param username - VeSync account username
184
186
  * @param password - VeSync account password
185
187
  * @param timeZone - Optional timezone for device operations (defaults to America/New_York)
186
- * @param debug - Optional debug mode flag
187
- * @param redact - Optional redact mode flag
188
- * @param apiUrl - Optional API base URL override
189
- * @param customLogger - Optional custom logger implementation
190
- * @param excludeConfig - Optional device exclusion configuration
188
+ * @param optionsOrDebug - Either options object or debug flag for backward compatibility
189
+ * @param redact - Optional redact mode flag (used when optionsOrDebug is boolean)
190
+ * @param apiUrl - Optional API base URL override (used when optionsOrDebug is boolean)
191
+ * @param customLogger - Optional custom logger implementation (used when optionsOrDebug is boolean)
192
+ * @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
191
193
  */
192
194
  constructor(
193
195
  username: string,
194
196
  password: string,
195
197
  timeZone: string = DEFAULT_TZ,
196
- debug = false,
198
+ optionsOrDebug: boolean | {
199
+ debug?: boolean;
200
+ redact?: boolean;
201
+ apiUrl?: string;
202
+ customLogger?: Logger;
203
+ excludeConfig?: ExcludeConfig;
204
+ region?: string;
205
+ debugMode?: boolean; // Alias for debug
206
+ } = false,
197
207
  redact = true,
198
208
  apiUrl?: string,
199
209
  customLogger?: Logger,
200
210
  excludeConfig?: ExcludeConfig
201
211
  ) {
202
- this._debug = debug;
203
- this._redact = redact;
212
+ // Handle options object or backward compatible parameters
213
+ let options: any = {};
214
+ if (typeof optionsOrDebug === 'object' && optionsOrDebug !== null) {
215
+ // Using new options pattern
216
+ options = optionsOrDebug;
217
+ this._debug = options.debug || options.debugMode || false;
218
+ this._redact = options.redact !== undefined ? options.redact : true;
219
+ this._excludeConfig = options.excludeConfig || null;
220
+ this._region = options.region || 'US';
221
+ customLogger = options.customLogger;
222
+ apiUrl = options.apiUrl;
223
+ } else {
224
+ // Using old parameter pattern for backward compatibility
225
+ this._debug = optionsOrDebug as boolean;
226
+ this._redact = redact;
227
+ this._excludeConfig = excludeConfig || null;
228
+ this._region = 'US';
229
+ }
230
+
204
231
  this._energyUpdateInterval = DEFAULT_ENERGY_UPDATE_INTERVAL;
205
232
  this._energyCheck = true;
206
233
  this._lastUpdateTs = null;
207
234
  this._inProcess = false;
208
- this._excludeConfig = excludeConfig || null;
209
235
  this._appId = generateAppId();
210
- this._region = 'US';
211
236
 
212
237
  this.username = username;
213
238
  this.password = password;
@@ -245,13 +270,21 @@ export class VeSync {
245
270
  logger.debug('Time zone is not a string');
246
271
  }
247
272
 
248
- // Set custom API URL if provided, otherwise use default US endpoint
273
+ // Set custom API URL if provided, otherwise use region-based endpoint
249
274
  if (apiUrl) {
250
275
  setApiBaseUrl(apiUrl);
251
276
  } else {
252
- // Start with US endpoint as default
253
- setApiBaseUrl('https://smartapi.vesync.com');
254
- setCurrentRegion('US');
277
+ // Set endpoint based on region
278
+ const regionEndpoints: Record<string, string> = {
279
+ 'US': 'https://smartapi.vesync.com',
280
+ 'EU': 'https://smartapi.vesync.eu',
281
+ 'CA': 'https://smartapi.vesync.com',
282
+ 'MX': 'https://smartapi.vesync.com',
283
+ 'JP': 'https://smartapi.vesync.com'
284
+ };
285
+ const endpoint = regionEndpoints[this._region] || 'https://smartapi.vesync.com';
286
+ setApiBaseUrl(endpoint);
287
+ setCurrentRegion(this._region);
255
288
  }
256
289
 
257
290
  // Set custom logger if provided
@@ -259,13 +292,7 @@ export class VeSync {
259
292
  setLogger(customLogger);
260
293
  }
261
294
 
262
- if (debug) {
263
- this.debug = true;
264
- }
265
-
266
- if (redact) {
267
- this.redact = true;
268
- }
295
+ // Debug and redact are already set above in the options handling
269
296
  }
270
297
 
271
298
  /**
@@ -548,22 +575,62 @@ export class VeSync {
548
575
  try {
549
576
  logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
550
577
 
551
- // Try new authentication flow first
578
+ // Skip email-based region detection for now since we'll handle cross-region errors
579
+ // The cross-region error handling below will automatically switch to the correct region
580
+
581
+ // Try new authentication flow first with detected region
552
582
  let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region);
583
+
584
+ // Track if new flow succeeded
585
+ if (success) {
586
+ this.authFlowUsed = 'new';
587
+ }
553
588
 
554
589
  if (!success) {
590
+ // Check if it's a credential error (no point retrying)
591
+ if (countryCode === 'credential_error') {
592
+ logger.error('Authentication failed due to invalid credentials');
593
+ return false; // Exit immediately, don't retry
594
+ }
595
+
555
596
  // Check if we need to try a different region
556
- if (countryCode === 'cross_region') {
557
- logger.debug('Cross-region error detected, trying EU region...');
558
- this._region = 'EU';
559
- setCurrentRegion('EU');
560
- [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, 'EU');
597
+ if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
598
+ logger.debug('Cross-region error detected, trying opposite region...');
599
+ const alternateRegion = this._region === 'US' ? 'EU' : 'US';
600
+ logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
601
+
602
+ this._region = alternateRegion;
603
+ setCurrentRegion(alternateRegion);
604
+
605
+ // Update the API endpoint for the new region
606
+ const regionEndpoints: Record<string, string> = {
607
+ 'US': 'https://smartapi.vesync.com',
608
+ 'EU': 'https://smartapi.vesync.eu'
609
+ };
610
+ if (alternateRegion in regionEndpoints) {
611
+ setApiBaseUrl(regionEndpoints[alternateRegion]);
612
+ logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
613
+ }
614
+
615
+ // Retry with the alternate region
616
+ [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, alternateRegion);
617
+ if (success) {
618
+ this.authFlowUsed = 'new';
619
+ logger.debug(`Authentication successful with ${alternateRegion} region`);
620
+ }
561
621
  }
562
622
 
563
623
  // If new flow still fails, try legacy authentication as fallback
564
624
  if (!success) {
565
625
  logger.debug('New authentication flow failed, trying legacy flow...');
566
626
  [success, token, accountId, countryCode] = await Helpers.authLegacyFlow(this);
627
+ if (success) {
628
+ this.authFlowUsed = 'legacy';
629
+ } else if (countryCode === 'credential_error') {
630
+ // Legacy auth also detected bad credentials
631
+ logger.error('Both authentication methods report invalid credentials');
632
+ return false; // Exit immediately
633
+ }
567
634
  }
568
635
  }
569
636
 
@@ -572,6 +639,7 @@ export class VeSync {
572
639
  this.accountId = accountId;
573
640
  this.countryCode = countryCode;
574
641
  this.enabled = true;
642
+ this.apiBaseUrl = getApiBaseUrl(); // Track the API endpoint being used
575
643
 
576
644
  // Set region based on country code if we got one
577
645
  if (countryCode) {
@@ -584,6 +652,7 @@ export class VeSync {
584
652
  }
585
653
 
586
654
  logger.debug('Authentication successful!', {
655
+ authFlow: this.authFlowUsed,
587
656
  region: this._region,
588
657
  countryCode: countryCode,
589
658
  endpoint: getApiBaseUrl()