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 +1 -0
- package/dist/index.js +7 -1
- package/dist/lib/constants.d.ts +14 -0
- package/dist/lib/constants.js +35 -0
- package/dist/lib/helpers.d.ts +17 -4
- package/dist/lib/helpers.js +102 -60
- package/dist/lib/vesync.d.ts +17 -7
- package/dist/lib/vesync.js +81 -26
- package/package.json +1 -1
- package/src/index.ts +9 -1
- package/src/lib/constants.ts +35 -0
- package/src/lib/helpers.ts +105 -70
- package/src/lib/vesync.ts +97 -28
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
|
+
}
|
package/dist/lib/helpers.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
*/
|
package/dist/lib/helpers.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
73
|
-
|
|
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 = '
|
|
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':
|
|
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
|
-
//
|
|
426
|
-
if (authResponse.code
|
|
427
|
-
logger_1.logger.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 ||
|
|
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 || {};
|
package/dist/lib/vesync.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
*/
|
package/dist/lib/vesync.js
CHANGED
|
@@ -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
|
|
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,
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
452
|
-
this._region
|
|
453
|
-
(
|
|
454
|
-
|
|
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
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
|
+
}
|
package/src/lib/helpers.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
75
|
-
|
|
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 = '
|
|
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':
|
|
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
|
-
//
|
|
540
|
-
if (authResponse.code
|
|
541
|
-
logger.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
273
|
+
// Set custom API URL if provided, otherwise use region-based endpoint
|
|
249
274
|
if (apiUrl) {
|
|
250
275
|
setApiBaseUrl(apiUrl);
|
|
251
276
|
} else {
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
558
|
-
this._region
|
|
559
|
-
|
|
560
|
-
|
|
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()
|