tsvesync 1.4.1 → 1.4.3
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/README.md +15 -34
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +13 -1
- package/dist/lib/helpers.d.ts +4 -3
- package/dist/lib/helpers.js +151 -97
- package/dist/lib/vesync.d.ts +5 -0
- package/dist/lib/vesync.js +173 -45
- package/package.json +1 -1
- package/src/lib/constants.ts +14 -1
- package/src/lib/helpers.ts +178 -108
- package/src/lib/vesync.ts +201 -51
package/README.md
CHANGED
|
@@ -208,40 +208,21 @@ await manager.login();
|
|
|
208
208
|
|
|
209
209
|
#### Supported Country Codes
|
|
210
210
|
|
|
211
|
-
The library supports all country codes.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
- `
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
- `
|
|
221
|
-
- `
|
|
222
|
-
|
|
223
|
-
- `
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
- `NO` - Norway
|
|
227
|
-
- `DK` - Denmark
|
|
228
|
-
- `FI` - Finland
|
|
229
|
-
- `PL` - Poland
|
|
230
|
-
- And all other European countries
|
|
231
|
-
|
|
232
|
-
**Asia-Pacific:** (uses US endpoint)
|
|
233
|
-
- `AU` - Australia
|
|
234
|
-
- `NZ` - New Zealand
|
|
235
|
-
- `JP` - Japan
|
|
236
|
-
- `SG` - Singapore
|
|
237
|
-
- `CN` - China
|
|
238
|
-
- `KR` - South Korea
|
|
239
|
-
|
|
240
|
-
**Important Notes:**
|
|
241
|
-
- If you get an authentication error about "country code mismatch" or "cross-region error", you need to set your country code
|
|
242
|
-
- The country code should match the country where your VeSync account was created
|
|
243
|
-
- European countries automatically use the EU endpoint (`smartapi.vesync.eu`)
|
|
244
|
-
- All other countries use the US endpoint (`smartapi.vesync.com`)
|
|
211
|
+
The library supports all ISO 3166-1 alpha-2 country codes (e.g. `US`, `DE`, `AU`).
|
|
212
|
+
|
|
213
|
+
##### pyvesync parity notes (regions + device list)
|
|
214
|
+
|
|
215
|
+
VeSync uses two API base URLs:
|
|
216
|
+
- US: `https://smartapi.vesync.com`
|
|
217
|
+
- EU: `https://smartapi.vesync.eu`
|
|
218
|
+
|
|
219
|
+
This library intentionally mirrors **pyvesync** behavior for the initial region selection:
|
|
220
|
+
- `US` region: `US`, `CA`, `MX`, `JP`
|
|
221
|
+
- `EU` region: everything else
|
|
222
|
+
|
|
223
|
+
Some accounts may still “live” in the opposite region for historical/app reasons; in that case the new auth flow returns a cross‑region response and we automatically retry Step 2 using the server-provided `currentRegion`/`countryCode` + `bizToken` (pyvesync-style).
|
|
224
|
+
|
|
225
|
+
Device discovery parity note: the device list call uses `/cloud/v1/deviceManaged/devices` with bypass headers only; `token` and `accountID` are supplied in the request body (matching pyvesync’s request model).
|
|
245
226
|
|
|
246
227
|
### Working with Devices
|
|
247
228
|
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare const CREDENTIAL_ERROR_CODES: number[];
|
|
5
5
|
export declare const CROSS_REGION_ERROR_CODES: number[];
|
|
6
|
+
export declare const TOKEN_ERROR_CODES: number[];
|
|
6
7
|
export declare const APP_VERSION_ERROR_CODES: number[];
|
|
7
8
|
/**
|
|
8
9
|
* Check if an error code indicates bad credentials
|
|
@@ -12,3 +13,7 @@ export declare function isCredentialError(code: number): boolean;
|
|
|
12
13
|
* Check if an error code indicates cross-region issue
|
|
13
14
|
*/
|
|
14
15
|
export declare function isCrossRegionError(code: number): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Check if an error code indicates the token is invalid/expired.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isTokenError(code: number): boolean;
|
package/dist/lib/constants.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* VeSync API Error Codes
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.APP_VERSION_ERROR_CODES = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = void 0;
|
|
6
|
+
exports.APP_VERSION_ERROR_CODES = exports.TOKEN_ERROR_CODES = exports.CROSS_REGION_ERROR_CODES = exports.CREDENTIAL_ERROR_CODES = void 0;
|
|
7
7
|
exports.isCredentialError = isCredentialError;
|
|
8
8
|
exports.isCrossRegionError = isCrossRegionError;
|
|
9
|
+
exports.isTokenError = isTokenError;
|
|
9
10
|
// Authentication error codes that indicate bad credentials (no point retrying)
|
|
10
11
|
exports.CREDENTIAL_ERROR_CODES = [
|
|
11
12
|
-11201129, // account or password incorrect
|
|
@@ -17,6 +18,11 @@ exports.CROSS_REGION_ERROR_CODES = [
|
|
|
17
18
|
-11260022, // cross region error
|
|
18
19
|
-11261022, // access region conflict error
|
|
19
20
|
];
|
|
21
|
+
// Token/auth invalidation error codes
|
|
22
|
+
// pyvesync parity: `-11001000` maps to TOKEN_EXPIRED (invalid token)
|
|
23
|
+
exports.TOKEN_ERROR_CODES = [
|
|
24
|
+
-11001000, // token expired/invalid
|
|
25
|
+
];
|
|
20
26
|
// Other error codes
|
|
21
27
|
exports.APP_VERSION_ERROR_CODES = [
|
|
22
28
|
-11012022, // app version is too low
|
|
@@ -33,3 +39,9 @@ function isCredentialError(code) {
|
|
|
33
39
|
function isCrossRegionError(code) {
|
|
34
40
|
return exports.CROSS_REGION_ERROR_CODES.includes(code);
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Check if an error code indicates the token is invalid/expired.
|
|
44
|
+
*/
|
|
45
|
+
function isTokenError(code) {
|
|
46
|
+
return exports.TOKEN_ERROR_CODES.includes(code);
|
|
47
|
+
}
|
package/dist/lib/helpers.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export declare function getApiBaseUrl(): string;
|
|
|
6
6
|
export declare function setApiBaseUrl(url: string): void;
|
|
7
7
|
export declare function getCurrentRegion(): string;
|
|
8
8
|
export declare function setCurrentRegion(region: string): void;
|
|
9
|
-
export declare
|
|
9
|
+
export declare const NON_EU_COUNTRY_CODES: string[];
|
|
10
|
+
export declare function getRegionFromCountryCode(countryCode: string): 'US' | 'EU';
|
|
10
11
|
export declare const API_RATE_LIMIT = 30;
|
|
11
12
|
export declare const API_TIMEOUT = 15000;
|
|
12
13
|
export declare const APP_VERSION = "5.7.16";
|
|
@@ -141,11 +142,11 @@ export declare class Helpers {
|
|
|
141
142
|
/**
|
|
142
143
|
* Build request body for initial authentication step
|
|
143
144
|
*/
|
|
144
|
-
static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string,
|
|
145
|
+
static reqBodyAuthStep1(manager: VeSync, appId: string, terminalId: string, userCountryCode: string): Record<string, any>;
|
|
145
146
|
/**
|
|
146
147
|
* Build request body for second authentication step (login with authorize code)
|
|
147
148
|
*/
|
|
148
|
-
static reqBodyAuthStep2(authorizeCode: string, bizToken: string | null,
|
|
149
|
+
static reqBodyAuthStep2(authorizeCode: string, bizToken: string | null, _appId: string, terminalId: string, userCountryCode?: string, regionChange?: string): Record<string, any>;
|
|
149
150
|
/**
|
|
150
151
|
* Builder for body of api requests
|
|
151
152
|
*/
|
package/dist/lib/helpers.js
CHANGED
|
@@ -6,7 +6,7 @@ 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.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;
|
|
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 = exports.NON_EU_COUNTRY_CODES = void 0;
|
|
10
10
|
exports.getApiBaseUrl = getApiBaseUrl;
|
|
11
11
|
exports.setApiBaseUrl = setApiBaseUrl;
|
|
12
12
|
exports.getCurrentRegion = getCurrentRegion;
|
|
@@ -41,19 +41,27 @@ function setCurrentRegion(region) {
|
|
|
41
41
|
_apiBaseUrl = exports.REGION_ENDPOINTS[region];
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// pyvesync parity:
|
|
45
|
+
// VeSync only exposes two API base URLs (US + EU). pyvesync intentionally uses a
|
|
46
|
+
// *deny-list* mapping here:
|
|
47
|
+
// - `US` region: US/CA/MX/JP
|
|
48
|
+
// - `EU` region: everything else
|
|
49
|
+
//
|
|
50
|
+
// This is counter-intuitive if you read it as "EU countries", but it's really
|
|
51
|
+
// "the EU *endpoint*". In practice, many accounts outside Europe (e.g. AU/NZ/SG)
|
|
52
|
+
// can be hosted on either endpoint depending on how/when the account was created.
|
|
53
|
+
//
|
|
54
|
+
// The correct/robust behavior comes from the Step 2 cross-region flow: when the
|
|
55
|
+
// region guess is wrong, the API returns `currentRegion`/`countryCode` plus a
|
|
56
|
+
// `bizToken`, and we retry Step 2 using the server-provided values (pyvesync-style).
|
|
57
|
+
exports.NON_EU_COUNTRY_CODES = ['US', 'CA', 'MX', 'JP'];
|
|
44
58
|
// Determine region from country code
|
|
45
59
|
function getRegionFromCountryCode(countryCode) {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', 'IS', 'LI',
|
|
50
|
-
'NO', 'CH'
|
|
51
|
-
];
|
|
52
|
-
if (euCountries.includes(countryCode)) {
|
|
53
|
-
return 'EU';
|
|
60
|
+
const normalized = countryCode.trim().toUpperCase();
|
|
61
|
+
if (exports.NON_EU_COUNTRY_CODES.includes(normalized)) {
|
|
62
|
+
return 'US';
|
|
54
63
|
}
|
|
55
|
-
|
|
56
|
-
return 'US';
|
|
64
|
+
return 'EU';
|
|
57
65
|
}
|
|
58
66
|
exports.API_RATE_LIMIT = 30;
|
|
59
67
|
exports.API_TIMEOUT = 15000;
|
|
@@ -97,25 +105,8 @@ function getUSEndpointCountryCodes() {
|
|
|
97
105
|
* Get the appropriate API endpoint based on country code
|
|
98
106
|
*/
|
|
99
107
|
function getEndpointForCountryCode(countryCode) {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
// Western Europe
|
|
103
|
-
'GB', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'CH', 'IE', 'PT', 'LU',
|
|
104
|
-
// Nordic countries
|
|
105
|
-
'SE', 'NO', 'DK', 'FI', 'IS',
|
|
106
|
-
// Eastern Europe
|
|
107
|
-
'PL', 'CZ', 'HU', 'RO', 'BG', 'HR', 'SI', 'SK', 'LT', 'LV', 'EE',
|
|
108
|
-
// Southern Europe
|
|
109
|
-
'GR', 'CY', 'MT',
|
|
110
|
-
// Other European
|
|
111
|
-
'AL', 'BA', 'RS', 'ME', 'MK', 'MD', 'UA', 'BY'
|
|
112
|
-
];
|
|
113
|
-
// Check if country code is in EU list
|
|
114
|
-
if (euCountryCodes.includes(countryCode)) {
|
|
115
|
-
return 'EU';
|
|
116
|
-
}
|
|
117
|
-
// Everything else uses US endpoint (including AU, NZ, JP, CA, MX, etc.)
|
|
118
|
-
return 'US';
|
|
108
|
+
// Alias for clarity: this is the region-to-endpoint mapping used by pyvesync.
|
|
109
|
+
return getRegionFromCountryCode(countryCode);
|
|
119
110
|
}
|
|
120
111
|
/**
|
|
121
112
|
* Detect user's home region from email domain or country hints
|
|
@@ -166,18 +157,21 @@ function generateAppId() {
|
|
|
166
157
|
}
|
|
167
158
|
// Generate unique terminal ID for authentication
|
|
168
159
|
function generateTerminalId() {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
for (let i = 0; i < 16; i++) {
|
|
172
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
160
|
+
// pyvesync parity: terminalId is a UUID-like identifier prefixed with "2".
|
|
161
|
+
return `2${crypto_1.default.randomUUID().replace(/-/g, '')}`;
|
|
175
162
|
}
|
|
176
163
|
function generateTraceId() {
|
|
177
164
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
178
165
|
const randomPart = Math.floor(Math.random() * 100000).toString().padStart(5, '0');
|
|
179
166
|
return `APP${exports.MOBILE_ID.slice(-5, -1)}${timestamp}-${randomPart}`;
|
|
180
167
|
}
|
|
168
|
+
let _authTraceCallNumber = 0;
|
|
169
|
+
function generateAuthTraceId(terminalId) {
|
|
170
|
+
_authTraceCallNumber += 1;
|
|
171
|
+
const suffix = terminalId.slice(-5, -1);
|
|
172
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
173
|
+
return `APP${suffix}${timestamp}-${String(_authTraceCallNumber).padStart(5, '0')}`;
|
|
174
|
+
}
|
|
181
175
|
class Helpers {
|
|
182
176
|
static generateTraceId() {
|
|
183
177
|
return generateTraceId();
|
|
@@ -306,16 +300,12 @@ class Helpers {
|
|
|
306
300
|
/**
|
|
307
301
|
* Build request body for initial authentication step
|
|
308
302
|
*/
|
|
309
|
-
static reqBodyAuthStep1(manager, appId, terminalId,
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
detectedRegion: userRegion,
|
|
316
|
-
requestRegion: region,
|
|
317
|
-
userCountryCode: userCountryCode
|
|
318
|
-
});
|
|
303
|
+
static reqBodyAuthStep1(manager, appId, terminalId, userCountryCode) {
|
|
304
|
+
// pyvesync parity:
|
|
305
|
+
// - Mirrors `pyvesync.models.vesync_models.RequestGetTokenModel`
|
|
306
|
+
// - `userCountryCode` defaults to `DEFAULT_REGION` ("US") unless the caller overrides it
|
|
307
|
+
// - `timeZone` defaults to `DEFAULT_TZ` ("America/New_York") during auth; the API later updates the
|
|
308
|
+
// effective timezone used for device operations.
|
|
319
309
|
return {
|
|
320
310
|
'email': manager.username,
|
|
321
311
|
'method': 'authByPWDOrOTM',
|
|
@@ -323,29 +313,42 @@ class Helpers {
|
|
|
323
313
|
'acceptLanguage': 'en',
|
|
324
314
|
'accountID': '',
|
|
325
315
|
'authProtocolType': 'generic',
|
|
326
|
-
'clientInfo': exports.
|
|
316
|
+
'clientInfo': exports.PHONE_BRAND,
|
|
327
317
|
'clientType': 'vesyncApp',
|
|
328
318
|
'clientVersion': exports.CLIENT_VERSION,
|
|
329
319
|
'debugMode': false,
|
|
330
320
|
'osInfo': exports.PHONE_OS,
|
|
331
321
|
'terminalId': terminalId,
|
|
332
|
-
'timeZone':
|
|
322
|
+
'timeZone': exports.DEFAULT_TZ,
|
|
333
323
|
'token': '',
|
|
334
|
-
'userCountryCode': userCountryCode,
|
|
324
|
+
'userCountryCode': userCountryCode.trim().toUpperCase(),
|
|
335
325
|
'appID': appId,
|
|
336
326
|
'sourceAppID': appId,
|
|
337
|
-
'traceId':
|
|
327
|
+
'traceId': generateAuthTraceId(terminalId)
|
|
338
328
|
};
|
|
339
329
|
}
|
|
340
330
|
/**
|
|
341
331
|
* Build request body for second authentication step (login with authorize code)
|
|
342
332
|
*/
|
|
343
|
-
static reqBodyAuthStep2(authorizeCode, bizToken,
|
|
333
|
+
static reqBodyAuthStep2(authorizeCode, bizToken, _appId, terminalId, userCountryCode, regionChange) {
|
|
334
|
+
// pyvesync parity:
|
|
335
|
+
// - Mirrors `pyvesync.models.vesync_models.RequestLoginTokenModel`
|
|
336
|
+
// - `timeZone` remains `DEFAULT_TZ` during auth (device timezone is handled elsewhere)
|
|
337
|
+
// - Step 2 intentionally does NOT include `appID` / `sourceAppID`. Those are only sent in Step 1
|
|
338
|
+
// (`RequestGetTokenModel`). pyvesync's Step 2 request model has no appID fields.
|
|
339
|
+
// If you see older diagnostics including appID/sourceAppID in Step 2, treat it as a superset payload
|
|
340
|
+
// rather than a requirement — our goal here is to match the known-working pyvesync request shape.
|
|
341
|
+
// - IMPORTANT: `bizToken` here is the *region-change token* returned by VeSync on Step 2 CROSS_REGION
|
|
342
|
+
// errors. It is not part of the normal Step 1 -> Step 2 flow. pyvesync does not send a Step 1
|
|
343
|
+
// bizToken in the initial Step 2 request (Step 1 result is just `{ accountID, authorizeCode }`);
|
|
344
|
+
// it only includes `bizToken` + `regionChange='lastRegion'`
|
|
345
|
+
// when retrying Step 2 after a cross-region response. See `pyvesync/auth.py:_exchange_authorization_code`.
|
|
344
346
|
const body = {
|
|
345
347
|
'method': 'loginByAuthorizeCode4Vesync',
|
|
346
348
|
'authorizeCode': authorizeCode,
|
|
347
349
|
'acceptLanguage': 'en',
|
|
348
|
-
'
|
|
350
|
+
'accountID': '',
|
|
351
|
+
'clientInfo': exports.PHONE_BRAND,
|
|
349
352
|
'clientType': 'vesyncApp',
|
|
350
353
|
'clientVersion': exports.CLIENT_VERSION,
|
|
351
354
|
'debugMode': false,
|
|
@@ -353,8 +356,9 @@ class Helpers {
|
|
|
353
356
|
'osInfo': exports.PHONE_OS,
|
|
354
357
|
'terminalId': terminalId,
|
|
355
358
|
'timeZone': exports.DEFAULT_TZ,
|
|
356
|
-
'
|
|
357
|
-
'
|
|
359
|
+
'token': '',
|
|
360
|
+
'userCountryCode': (userCountryCode || exports.DEFAULT_REGION).trim().toUpperCase(),
|
|
361
|
+
'traceId': generateAuthTraceId(terminalId)
|
|
358
362
|
};
|
|
359
363
|
// Only include bizToken if it's not null
|
|
360
364
|
if (bizToken) {
|
|
@@ -433,13 +437,18 @@ class Helpers {
|
|
|
433
437
|
* Call VeSync API
|
|
434
438
|
*/
|
|
435
439
|
static async callApi(endpoint, method, data = null, headers = {}, manager) {
|
|
440
|
+
let url = '';
|
|
436
441
|
try {
|
|
442
|
+
// Prefer manager-scoped base URL (pyvesync resolves per-manager from current region).
|
|
443
|
+
// Keep the module-level base URL as a fallback for older callers.
|
|
444
|
+
let baseUrl = manager.apiBaseUrl || _apiBaseUrl;
|
|
437
445
|
// Ensure API base URL is properly set
|
|
438
|
-
if (!
|
|
439
|
-
logger_1.logger.error('API base URL is not properly configured.
|
|
440
|
-
|
|
446
|
+
if (!baseUrl || baseUrl === 'undefined') {
|
|
447
|
+
logger_1.logger.error('API base URL is not properly configured. Falling back to US endpoint...');
|
|
448
|
+
baseUrl = 'https://smartapi.vesync.com';
|
|
449
|
+
manager.apiBaseUrl = baseUrl;
|
|
441
450
|
}
|
|
442
|
-
|
|
451
|
+
url = baseUrl + endpoint;
|
|
443
452
|
logger_1.logger.debug(`Making API call to: ${url}`);
|
|
444
453
|
const response = await (0, axios_1.default)({
|
|
445
454
|
method,
|
|
@@ -473,14 +482,14 @@ class Helpers {
|
|
|
473
482
|
status: error.response.status,
|
|
474
483
|
code: responseData === null || responseData === void 0 ? void 0 : responseData.code,
|
|
475
484
|
message: responseData === null || responseData === void 0 ? void 0 : responseData.msg,
|
|
476
|
-
url
|
|
485
|
+
url
|
|
477
486
|
});
|
|
478
487
|
return [responseData, error.response.status];
|
|
479
488
|
}
|
|
480
489
|
logger_1.logger.error('API call failed:', {
|
|
481
490
|
code: error.code,
|
|
482
491
|
message: error.message,
|
|
483
|
-
url: _apiBaseUrl + endpoint
|
|
492
|
+
url: url || (manager.apiBaseUrl || _apiBaseUrl || '') + endpoint
|
|
484
493
|
});
|
|
485
494
|
return [null, 0];
|
|
486
495
|
}
|
|
@@ -549,26 +558,36 @@ class Helpers {
|
|
|
549
558
|
* Perform new two-step authentication flow
|
|
550
559
|
*/
|
|
551
560
|
static async authNewFlow(manager, appId, region = 'US', countryCodeOverride) {
|
|
561
|
+
var _a, _b, _c;
|
|
552
562
|
try {
|
|
553
563
|
// Generate terminal ID to be used in both steps
|
|
554
564
|
const terminalId = generateTerminalId();
|
|
565
|
+
// pyvesync parity:
|
|
566
|
+
// - `userCountryCode` defaults to `DEFAULT_REGION` ("US"), even if we first try the EU base URL.
|
|
567
|
+
// - If the account truly belongs to another region, the Step 2 response includes the correct
|
|
568
|
+
// `countryCode`/`currentRegion` and a `bizToken` to retry Step 2 (handled below).
|
|
569
|
+
// Avoid "guessing" a country code from `region` or email heuristics here; it diverges from pyvesync.
|
|
570
|
+
const userCountryCode = (countryCodeOverride || exports.DEFAULT_REGION).trim().toUpperCase();
|
|
555
571
|
// Step 1: Get authorization code
|
|
556
|
-
const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId,
|
|
572
|
+
const step1Body = this.reqBodyAuthStep1(manager, appId, terminalId, userCountryCode);
|
|
557
573
|
const step1Headers = this.reqHeaderAuth();
|
|
558
574
|
// Set the correct regional endpoint
|
|
559
|
-
const originalUrl = getApiBaseUrl();
|
|
560
|
-
|
|
561
|
-
|
|
575
|
+
const originalUrl = manager.apiBaseUrl || getApiBaseUrl();
|
|
576
|
+
const knownBaseUrls = new Set(Object.values(exports.REGION_ENDPOINTS));
|
|
577
|
+
const hasCustomBaseUrlOverride = !!manager.apiUrlOverride || !knownBaseUrls.has(originalUrl);
|
|
578
|
+
if (!hasCustomBaseUrlOverride && region in exports.REGION_ENDPOINTS) {
|
|
579
|
+
const nextUrl = exports.REGION_ENDPOINTS[region];
|
|
580
|
+
manager.apiBaseUrl = nextUrl;
|
|
562
581
|
}
|
|
563
582
|
logger_1.logger.debug('Step 1: Getting authorization code...', {
|
|
564
583
|
region,
|
|
565
|
-
endpoint: getApiBaseUrl(),
|
|
584
|
+
endpoint: manager.apiBaseUrl || getApiBaseUrl(),
|
|
566
585
|
body: { ...step1Body, password: '[REDACTED]' }
|
|
567
586
|
});
|
|
568
587
|
const [authResponse, authStatus] = await this.callApi('/globalPlatform/api/accountAuth/v1/authByPWDOrOTM', 'post', step1Body, step1Headers, manager);
|
|
569
588
|
if (!authResponse || authStatus !== 200) {
|
|
570
589
|
logger_1.logger.error('Step 1 failed:', authResponse);
|
|
571
|
-
|
|
590
|
+
manager.apiBaseUrl = originalUrl;
|
|
572
591
|
return [false, null, null, null];
|
|
573
592
|
}
|
|
574
593
|
if (authResponse.code !== 0) {
|
|
@@ -576,30 +595,43 @@ class Helpers {
|
|
|
576
595
|
// Check if it's a credential error (no point retrying)
|
|
577
596
|
if ((0, constants_1.isCredentialError)(authResponse.code)) {
|
|
578
597
|
logger_1.logger.error('Credential error detected - invalid username or password');
|
|
579
|
-
|
|
598
|
+
manager.apiBaseUrl = originalUrl;
|
|
580
599
|
return [false, null, null, 'credential_error'];
|
|
581
600
|
}
|
|
582
601
|
// Handle cross-region error (multiple error codes possible)
|
|
583
602
|
if ((0, constants_1.isCrossRegionError)(authResponse.code)) {
|
|
584
603
|
logger_1.logger.debug('Cross-region error detected:', authResponse.code, 'will try different region');
|
|
585
|
-
|
|
604
|
+
manager.apiBaseUrl = originalUrl;
|
|
586
605
|
return [false, null, null, 'cross_region'];
|
|
587
606
|
}
|
|
588
|
-
|
|
607
|
+
manager.apiBaseUrl = originalUrl;
|
|
589
608
|
return [false, null, null, null];
|
|
590
609
|
}
|
|
591
|
-
|
|
610
|
+
// pyvesync parity note:
|
|
611
|
+
// - Step 1 "authByPWDOrOTM" returns a result shaped like RespGetTokenResultModel:
|
|
612
|
+
// `{ accountID, authorizeCode }`. There is no Step 1 bizToken to forward.
|
|
613
|
+
// See `pyvesync/models/vesync_models.py:RespGetTokenResultModel`.
|
|
614
|
+
const { authorizeCode } = authResponse.result || {};
|
|
592
615
|
if (!authorizeCode) {
|
|
593
616
|
logger_1.logger.error('Missing authorization code in step 1 response:', authResponse.result);
|
|
594
|
-
|
|
617
|
+
manager.apiBaseUrl = originalUrl;
|
|
595
618
|
return [false, null, null, null];
|
|
596
619
|
}
|
|
597
620
|
logger_1.logger.debug('Step 1 successful, got authorization code');
|
|
598
621
|
// Step 2: Login with authorization code
|
|
599
622
|
// Use the override if provided, otherwise default to 'US'
|
|
600
623
|
// Users should specify their actual country code in the configuration
|
|
601
|
-
const countryCodeForStep2 =
|
|
602
|
-
|
|
624
|
+
const countryCodeForStep2 = userCountryCode;
|
|
625
|
+
// pyvesync parity note:
|
|
626
|
+
// - Step 1 returns `authorizeCode` (and an `accountID`) but does not require/provide a bizToken for
|
|
627
|
+
// the initial Step 2 request.
|
|
628
|
+
// - Step 2 initial request omits `bizToken`.
|
|
629
|
+
// - Only if Step 2 returns a CROSS_REGION error does VeSync provide `result.bizToken`, which is then
|
|
630
|
+
// echoed back as `bizToken` along with `regionChange='lastRegion'` for a Step 2 retry.
|
|
631
|
+
// See `pyvesync/auth.py:_exchange_authorization_code` for the canonical behavior.
|
|
632
|
+
// - Step 2 also intentionally omits appID/sourceAppID (pyvesync parity); we still pass `appId` through
|
|
633
|
+
// to keep the function signature stable/historical.
|
|
634
|
+
const step2Body = this.reqBodyAuthStep2(authorizeCode, null, appId, terminalId, countryCodeForStep2);
|
|
603
635
|
logger_1.logger.debug('Step 2: Logging in with authorization code...');
|
|
604
636
|
const [loginResponse, loginStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', step2Body, step1Headers, manager);
|
|
605
637
|
logger_1.logger.debug('Step 2 response:', {
|
|
@@ -610,38 +642,60 @@ class Helpers {
|
|
|
610
642
|
});
|
|
611
643
|
if (!loginResponse || loginStatus !== 200) {
|
|
612
644
|
logger_1.logger.error('Step 2 failed:', loginResponse);
|
|
613
|
-
|
|
645
|
+
manager.apiBaseUrl = originalUrl;
|
|
614
646
|
return [false, null, null, null];
|
|
615
647
|
}
|
|
616
648
|
if (loginResponse.code !== 0) {
|
|
617
649
|
logger_1.logger.debug('Step 2 error, code:', loginResponse.code, 'checking if cross-region...');
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
logger_1.logger.
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
|
|
650
|
+
// Align with pyvesync: on cross-region errors, retry Step 2 using the server-provided
|
|
651
|
+
// country/region and bizToken + regionChange.
|
|
652
|
+
if ((0, constants_1.isCrossRegionError)(loginResponse.code)) {
|
|
653
|
+
const serverCountryCode = (_a = loginResponse.result) === null || _a === void 0 ? void 0 : _a.countryCode;
|
|
654
|
+
const serverRegion = (_b = loginResponse.result) === null || _b === void 0 ? void 0 : _b.currentRegion;
|
|
655
|
+
const regionChangeToken = (_c = loginResponse.result) === null || _c === void 0 ? void 0 : _c.bizToken;
|
|
656
|
+
logger_1.logger.debug('Cross-region error detected in Step 2; attempting pyvesync-style retry', {
|
|
657
|
+
requestedRegion: region,
|
|
658
|
+
requestedCountryCode: countryCodeForStep2,
|
|
659
|
+
serverCountryCode,
|
|
660
|
+
serverRegion,
|
|
661
|
+
hasRegionChangeToken: !!regionChangeToken,
|
|
662
|
+
});
|
|
663
|
+
// If VeSync tells us which region to use, switch to it for subsequent requests.
|
|
664
|
+
if (!hasCustomBaseUrlOverride && serverRegion && serverRegion in exports.REGION_ENDPOINTS) {
|
|
665
|
+
manager.region = serverRegion;
|
|
666
|
+
logger_1.logger.debug(`Updated region to ${serverRegion} for Step 2 retry`);
|
|
667
|
+
}
|
|
668
|
+
// If VeSync gives us a region-change token, retry Step 2 without redoing Step 1.
|
|
669
|
+
if (regionChangeToken) {
|
|
670
|
+
const retryCountryCode = serverCountryCode || countryCodeForStep2;
|
|
671
|
+
const retryBody = this.reqBodyAuthStep2(authorizeCode, regionChangeToken, appId, terminalId, retryCountryCode, 'lastRegion');
|
|
672
|
+
const [retryResponse, retryStatus] = await this.callApi('/user/api/accountManage/v1/loginByAuthorizeCode4Vesync', 'post', retryBody, step1Headers, manager);
|
|
673
|
+
if (retryResponse && retryStatus === 200 && retryResponse.code === 0) {
|
|
674
|
+
const { token, accountID, countryCode } = retryResponse.result || {};
|
|
675
|
+
if (token && accountID) {
|
|
676
|
+
logger_1.logger.debug('Step 2 retry successful, got token and accountID');
|
|
677
|
+
return [true, token, accountID, countryCode || retryCountryCode];
|
|
678
|
+
}
|
|
679
|
+
logger_1.logger.error('Missing required fields in Step 2 retry response:', retryResponse.result);
|
|
680
|
+
manager.apiBaseUrl = originalUrl;
|
|
681
|
+
return [false, null, null, null];
|
|
682
|
+
}
|
|
683
|
+
logger_1.logger.error('Step 2 retry failed:', {
|
|
684
|
+
status: retryStatus,
|
|
685
|
+
code: retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.code,
|
|
686
|
+
msg: retryResponse === null || retryResponse === void 0 ? void 0 : retryResponse.msg,
|
|
687
|
+
});
|
|
688
|
+
manager.apiBaseUrl = originalUrl;
|
|
689
|
+
return [false, null, null, 'cross_region_retry'];
|
|
690
|
+
}
|
|
691
|
+
logger_1.logger.error('Cross-region error returned no region-change token; cannot retry Step 2');
|
|
692
|
+
manager.apiBaseUrl = originalUrl;
|
|
638
693
|
return [false, null, null, 'cross_region_retry'];
|
|
639
694
|
}
|
|
640
695
|
logger_1.logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
|
|
641
|
-
|
|
696
|
+
manager.apiBaseUrl = originalUrl;
|
|
642
697
|
return [false, null, null, null];
|
|
643
698
|
}
|
|
644
|
-
setApiBaseUrl(originalUrl); // Restore original URL on success
|
|
645
699
|
const { token, accountID, countryCode } = loginResponse.result || {};
|
|
646
700
|
if (!token || !accountID) {
|
|
647
701
|
logger_1.logger.error('Missing required fields in step 2 response:', loginResponse.result);
|
|
@@ -662,7 +716,7 @@ class Helpers {
|
|
|
662
716
|
try {
|
|
663
717
|
const body = this.reqBody(manager, 'login');
|
|
664
718
|
logger_1.logger.debug('Legacy login attempt...', {
|
|
665
|
-
endpoint: getApiBaseUrl(),
|
|
719
|
+
endpoint: manager.apiBaseUrl || getApiBaseUrl(),
|
|
666
720
|
body: { ...body, password: '[REDACTED]' }
|
|
667
721
|
});
|
|
668
722
|
const [response, status] = await this.callApi('/cloud/v1/user/login', 'post', body, {}, manager);
|
package/dist/lib/vesync.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export declare class VeSync {
|
|
|
29
29
|
private _appId;
|
|
30
30
|
private _region;
|
|
31
31
|
private _countryCodeOverride;
|
|
32
|
+
private _apiUrlOverride;
|
|
32
33
|
private _sessionStore?;
|
|
33
34
|
private _onTokenChange?;
|
|
34
35
|
private _loginPromise;
|
|
@@ -99,6 +100,10 @@ export declare class VeSync {
|
|
|
99
100
|
* Get current region
|
|
100
101
|
*/
|
|
101
102
|
get region(): string;
|
|
103
|
+
/**
|
|
104
|
+
* Return the configured API URL override, if any.
|
|
105
|
+
*/
|
|
106
|
+
get apiUrlOverride(): string | null;
|
|
102
107
|
/**
|
|
103
108
|
* Set region and update API endpoint
|
|
104
109
|
*/
|