tsvesync 1.0.95 → 1.0.97

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.
@@ -7,8 +7,9 @@ import crypto from 'crypto';
7
7
  import { VeSync } from './vesync';
8
8
  import { logger } from './logger';
9
9
 
10
- // API configuration - Always use US endpoint
10
+ // API configuration - Support regional endpoints
11
11
  let _apiBaseUrl = 'https://smartapi.vesync.com';
12
+ let _currentRegion = 'US';
12
13
 
13
14
  export function getApiBaseUrl(): string {
14
15
  return _apiBaseUrl;
@@ -18,11 +19,39 @@ export function setApiBaseUrl(url: string): void {
18
19
  _apiBaseUrl = url;
19
20
  }
20
21
 
22
+ export function getCurrentRegion(): string {
23
+ return _currentRegion;
24
+ }
25
+
26
+ export function setCurrentRegion(region: string): void {
27
+ _currentRegion = region;
28
+ if (region in REGION_ENDPOINTS) {
29
+ _apiBaseUrl = REGION_ENDPOINTS[region as keyof typeof REGION_ENDPOINTS];
30
+ }
31
+ }
32
+
33
+ // Determine region from country code
34
+ export function getRegionFromCountryCode(countryCode: string): string {
35
+ const euCountries = [
36
+ 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR',
37
+ 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL',
38
+ 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB', 'IS', 'LI',
39
+ 'NO', 'CH'
40
+ ];
41
+
42
+ if (euCountries.includes(countryCode)) {
43
+ return 'EU';
44
+ }
45
+
46
+ // Default to US for other countries
47
+ return 'US';
48
+ }
49
+
21
50
 
22
51
  export const API_RATE_LIMIT = 30;
23
52
  export const API_TIMEOUT = 15000;
24
53
 
25
- export const APP_VERSION = '2.8.6';
54
+ export const APP_VERSION = '5.6.60';
26
55
  export const PHONE_BRAND = 'SM N9005';
27
56
  export const PHONE_OS = 'Android';
28
57
  export const USER_TYPE = '1';
@@ -30,6 +59,30 @@ export const DEFAULT_TZ = 'America/New_York';
30
59
  export const DEFAULT_REGION = 'US';
31
60
  export const MOBILE_ID = '1234567890123456';
32
61
  export const BYPASS_HEADER_UA = 'okhttp/3.12.1';
62
+ export const CLIENT_VERSION = 'VeSync 5.6.60';
63
+
64
+ // Regional API endpoints
65
+ export const REGION_ENDPOINTS = {
66
+ 'US': 'https://smartapi.vesync.com',
67
+ 'CA': 'https://smartapi.vesync.com',
68
+ 'MX': 'https://smartapi.vesync.com',
69
+ 'JP': 'https://smartapi.vesync.com',
70
+ 'EU': 'https://smartapi.vesync.eu'
71
+ };
72
+
73
+ // Cross-region error code
74
+ export const CROSS_REGION_ERROR_CODE = -11260022;
75
+ export const APP_VERSION_TOO_LOW_ERROR_CODE = -11012022;
76
+
77
+ // Generate unique APP_ID for each session
78
+ export function generateAppId(): string {
79
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
80
+ let result = '';
81
+ for (let i = 0; i < 8; i++) {
82
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
83
+ }
84
+ return result;
85
+ }
33
86
 
34
87
  export interface RequestBody {
35
88
  acceptLanguage?: string;
@@ -50,9 +103,42 @@ export interface RequestBody {
50
103
  userType?: string;
51
104
  uuid?: string;
52
105
  status?: string;
106
+ // New authentication parameters
107
+ authorizeCode?: string;
108
+ bizToken?: string;
109
+ regionChange?: string;
110
+ userCountryCode?: string;
111
+ authProtocolType?: string;
112
+ clientType?: string;
113
+ sourceAppID?: string;
114
+ appID?: string;
115
+ clientVersion?: string;
53
116
  [key: string]: any;
54
117
  }
55
118
 
119
+ // New authentication response interfaces
120
+ export interface AuthResponse {
121
+ code: number;
122
+ msg: string;
123
+ result?: {
124
+ authorizeCode?: string;
125
+ bizToken?: string;
126
+ userCountryCode?: string;
127
+ [key: string]: any;
128
+ };
129
+ }
130
+
131
+ export interface LoginResponse {
132
+ code: number;
133
+ msg: string;
134
+ result?: {
135
+ token?: string;
136
+ accountID?: string;
137
+ countryCode?: string;
138
+ [key: string]: any;
139
+ };
140
+ }
141
+
56
142
  export class Helpers {
57
143
  static shouldRedact = true;
58
144
 
@@ -92,6 +178,17 @@ export class Helpers {
92
178
  };
93
179
  }
94
180
 
181
+ /**
182
+ * Build header for new authentication endpoints
183
+ */
184
+ static reqHeaderAuth(): Record<string, string> {
185
+ return {
186
+ 'Content-Type': 'application/json; charset=UTF-8',
187
+ 'User-Agent': BYPASS_HEADER_UA,
188
+ 'accept-language': 'en'
189
+ };
190
+ }
191
+
95
192
  /**
96
193
  * Return universal keys for body of api requests
97
194
  */
@@ -127,6 +224,46 @@ export class Helpers {
127
224
  };
128
225
  }
129
226
 
227
+ /**
228
+ * Build request body for initial authentication step
229
+ */
230
+ static reqBodyAuthStep1(manager: VeSync, appId: string): Record<string, any> {
231
+ return {
232
+ 'email': manager.username,
233
+ 'password': this.hashPassword(manager.password),
234
+ 'timeZone': manager.timeZone,
235
+ 'acceptLanguage': 'en',
236
+ 'appVersion': APP_VERSION,
237
+ 'phoneBrand': PHONE_BRAND,
238
+ 'phoneOS': PHONE_OS,
239
+ 'traceId': Date.now().toString(),
240
+ 'authProtocolType': 'pwd',
241
+ 'clientType': 'android',
242
+ 'sourceAppID': 'vesync_android',
243
+ 'appID': appId,
244
+ 'clientVersion': CLIENT_VERSION
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Build request body for second authentication step (login with authorize code)
250
+ */
251
+ static reqBodyAuthStep2(authorizeCode: string, bizToken: string, appId: string, userCountryCode?: string): Record<string, any> {
252
+ const body: Record<string, any> = {
253
+ 'authorizeCode': authorizeCode,
254
+ 'bizToken': bizToken,
255
+ 'appID': appId,
256
+ 'clientVersion': CLIENT_VERSION,
257
+ 'acceptLanguage': 'en'
258
+ };
259
+
260
+ if (userCountryCode) {
261
+ body.userCountryCode = userCountryCode;
262
+ }
263
+
264
+ return body;
265
+ }
266
+
130
267
  /**
131
268
  * Builder for body of api requests
132
269
  */
@@ -327,4 +464,150 @@ export class Helpers {
327
464
  static md5(text: string): string {
328
465
  return crypto.createHash('md5').update(text).digest('hex');
329
466
  }
467
+
468
+ /**
469
+ * Perform new two-step authentication flow
470
+ */
471
+ static async authNewFlow(manager: VeSync, appId: string, region: string = 'US'): Promise<[boolean, string | null, string | null, string | null]> {
472
+ try {
473
+ // Step 1: Get authorization code
474
+ const step1Body = this.reqBodyAuthStep1(manager, appId);
475
+ const step1Headers = this.reqHeaderAuth();
476
+
477
+ // Set the correct regional endpoint
478
+ const originalUrl = getApiBaseUrl();
479
+ if (region in REGION_ENDPOINTS) {
480
+ setApiBaseUrl(REGION_ENDPOINTS[region as keyof typeof REGION_ENDPOINTS]);
481
+ }
482
+
483
+ logger.debug('Step 1: Getting authorization code...', {
484
+ region,
485
+ endpoint: getApiBaseUrl(),
486
+ body: { ...step1Body, password: '[REDACTED]' }
487
+ });
488
+
489
+ const [authResponse, authStatus] = await this.callApi(
490
+ '/globalPlatform/api/accountAuth/v1/authByPWDOrOTM',
491
+ 'post',
492
+ step1Body,
493
+ step1Headers,
494
+ manager
495
+ );
496
+
497
+ if (!authResponse || authStatus !== 200) {
498
+ logger.error('Step 1 failed:', authResponse);
499
+ setApiBaseUrl(originalUrl); // Restore original URL
500
+ return [false, null, null, null];
501
+ }
502
+
503
+ if (authResponse.code !== 0) {
504
+ logger.error('Step 1 error code:', authResponse.code, 'message:', authResponse.msg);
505
+
506
+ // Handle cross-region error
507
+ if (authResponse.code === CROSS_REGION_ERROR_CODE) {
508
+ logger.debug('Cross-region error detected, will try different region');
509
+ setApiBaseUrl(originalUrl); // Restore original URL
510
+ return [false, null, null, 'cross_region'];
511
+ }
512
+
513
+ setApiBaseUrl(originalUrl); // Restore original URL
514
+ return [false, null, null, null];
515
+ }
516
+
517
+ const { authorizeCode, bizToken, userCountryCode } = authResponse.result || {};
518
+
519
+ if (!authorizeCode || !bizToken) {
520
+ logger.error('Missing required fields in step 1 response:', authResponse.result);
521
+ setApiBaseUrl(originalUrl); // Restore original URL
522
+ return [false, null, null, null];
523
+ }
524
+
525
+ logger.debug('Step 1 successful, got authorization code');
526
+
527
+ // Step 2: Login with authorization code
528
+ const step2Body = this.reqBodyAuthStep2(authorizeCode, bizToken, appId, userCountryCode);
529
+
530
+ logger.debug('Step 2: Logging in with authorization code...');
531
+
532
+ const [loginResponse, loginStatus] = await this.callApi(
533
+ '/user/api/accountManage/v1/loginByAuthorizeCode4Vesync',
534
+ 'post',
535
+ step2Body,
536
+ step1Headers,
537
+ manager
538
+ );
539
+
540
+ setApiBaseUrl(originalUrl); // Restore original URL
541
+
542
+ if (!loginResponse || loginStatus !== 200) {
543
+ logger.error('Step 2 failed:', loginResponse);
544
+ return [false, null, null, null];
545
+ }
546
+
547
+ if (loginResponse.code !== 0) {
548
+ logger.error('Step 2 error code:', loginResponse.code, 'message:', loginResponse.msg);
549
+ return [false, null, null, null];
550
+ }
551
+
552
+ const { token, accountID, countryCode } = loginResponse.result || {};
553
+
554
+ if (!token || !accountID) {
555
+ logger.error('Missing required fields in step 2 response:', loginResponse.result);
556
+ return [false, null, null, null];
557
+ }
558
+
559
+ logger.debug('Step 2 successful, got token and accountID');
560
+ return [true, token, accountID, countryCode || userCountryCode];
561
+
562
+ } catch (error) {
563
+ logger.error('New authentication flow error:', error);
564
+ return [false, null, null, null];
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Perform legacy authentication (fallback)
570
+ */
571
+ static async authLegacyFlow(manager: VeSync): Promise<[boolean, string | null, string | null, string | null]> {
572
+ try {
573
+ const body = this.reqBody(manager, 'login');
574
+
575
+ logger.debug('Legacy login attempt...', {
576
+ endpoint: getApiBaseUrl(),
577
+ body: { ...body, password: '[REDACTED]' }
578
+ });
579
+
580
+ const [response, status] = await this.callApi(
581
+ '/cloud/v1/user/login',
582
+ 'post',
583
+ body,
584
+ {},
585
+ manager
586
+ );
587
+
588
+ if (!response || status !== 200) {
589
+ logger.error('Legacy login failed:', response);
590
+ return [false, null, null, null];
591
+ }
592
+
593
+ if (response.code && response.code !== 0) {
594
+ logger.error('Legacy login error code:', response.code, 'message:', response.msg);
595
+ return [false, null, null, null];
596
+ }
597
+
598
+ const { token, accountID, countryCode } = response.result || {};
599
+
600
+ if (!token || !accountID) {
601
+ logger.error('Missing required fields in legacy login response:', response.result);
602
+ return [false, null, null, null];
603
+ }
604
+
605
+ logger.debug('Legacy login successful');
606
+ return [true, token, accountID, countryCode];
607
+
608
+ } catch (error) {
609
+ logger.error('Legacy authentication flow error:', error);
610
+ return [false, null, null, null];
611
+ }
612
+ }
330
613
  }
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 } from './helpers';
5
+ import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS } from './helpers';
6
6
  import { VeSyncBaseDevice } from './vesyncBaseDevice';
7
7
  import { fanModules } from './vesyncFanImpl';
8
8
  import { outletModules } from './vesyncOutletImpl';
@@ -159,6 +159,8 @@ export class VeSync {
159
159
  private _lastUpdateTs: number | null;
160
160
  private _inProcess: boolean;
161
161
  private _excludeConfig: ExcludeConfig | null;
162
+ private _appId: string;
163
+ private _region: string;
162
164
 
163
165
  username: string;
164
166
  password: string;
@@ -204,6 +206,8 @@ export class VeSync {
204
206
  this._lastUpdateTs = null;
205
207
  this._inProcess = false;
206
208
  this._excludeConfig = excludeConfig || null;
209
+ this._appId = generateAppId();
210
+ this._region = 'US';
207
211
 
208
212
  this.username = username;
209
213
  this.password = password;
@@ -245,8 +249,9 @@ export class VeSync {
245
249
  if (apiUrl) {
246
250
  setApiBaseUrl(apiUrl);
247
251
  } else {
248
- // Always use US endpoint
252
+ // Start with US endpoint as default
249
253
  setApiBaseUrl('https://smartapi.vesync.com');
254
+ setCurrentRegion('US');
250
255
  }
251
256
 
252
257
  // Set custom logger if provided
@@ -299,6 +304,30 @@ export class VeSync {
299
304
  }
300
305
  }
301
306
 
307
+ /**
308
+ * Get current App ID
309
+ */
310
+ get appId(): string {
311
+ return this._appId;
312
+ }
313
+
314
+ /**
315
+ * Get current region
316
+ */
317
+ get region(): string {
318
+ return this._region;
319
+ }
320
+
321
+ /**
322
+ * Set region and update API endpoint
323
+ */
324
+ set region(region: string) {
325
+ if (region in REGION_ENDPOINTS) {
326
+ this._region = region;
327
+ setCurrentRegion(region);
328
+ }
329
+ }
330
+
302
331
  /**
303
332
  * Test if device should be removed
304
333
  */
@@ -506,75 +535,82 @@ export class VeSync {
506
535
  }
507
536
 
508
537
  /**
509
- * Login to VeSync server
538
+ * Login to VeSync server with new authentication flow and backwards compatibility
510
539
  */
511
540
  async login(retryAttempts: number = 3, initialDelayMs: number = 1000): Promise<boolean> {
512
- const body = Helpers.reqBody(this, 'login');
513
-
541
+ logger.debug('Starting VeSync authentication...', {
542
+ username: this.username,
543
+ appId: this._appId,
544
+ region: this._region
545
+ });
546
+
514
547
  for (let attempt = 0; attempt < retryAttempts; attempt++) {
515
548
  try {
516
- logger.debug('Login attempt', {
517
- attempt: attempt + 1,
518
- apiUrl: getApiBaseUrl(),
519
- timeZone: this.timeZone,
520
- appVersion: body.appVersion
521
- });
522
-
523
- const [response, status] = await Helpers.callApi(
524
- '/cloud/v1/user/login',
525
- 'post',
526
- body,
527
- {},
528
- this
529
- );
530
-
531
- logger.debug('Login response:', { status, response });
532
-
533
- // Handle specific error codes
534
- if (response && response.code) {
535
- switch (response.code) {
536
- case -11012022:
537
- logger.error('App version too low error. Current version:', body.appVersion);
538
- logger.error('This typically indicates an API version compatibility issue.');
539
- break;
540
- case -11003:
541
- logger.error('Authentication failed - check credentials');
542
- break;
543
- case -11001:
544
- logger.error('Invalid request format');
545
- break;
546
- default:
547
- logger.error('API error code:', response.code, 'message:', response.msg);
549
+ logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
550
+
551
+ // Try new authentication flow first
552
+ let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region);
553
+
554
+ if (!success) {
555
+ // 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');
561
+ }
562
+
563
+ // If new flow still fails, try legacy authentication as fallback
564
+ if (!success) {
565
+ logger.debug('New authentication flow failed, trying legacy flow...');
566
+ [success, token, accountId, countryCode] = await Helpers.authLegacyFlow(this);
548
567
  }
549
568
  }
550
569
 
551
- if (response?.result?.token) {
552
- this.token = response.result.token;
553
- this.accountId = response.result.accountID;
554
- this.countryCode = response.result.countryCode;
570
+ if (success && token && accountId) {
571
+ this.token = token;
572
+ this.accountId = accountId;
573
+ this.countryCode = countryCode;
555
574
  this.enabled = true;
556
- logger.debug('Login successful for region:', this.countryCode);
575
+
576
+ // Set region based on country code if we got one
577
+ if (countryCode) {
578
+ const detectedRegion = getRegionFromCountryCode(countryCode);
579
+ if (detectedRegion !== this._region) {
580
+ logger.debug(`Updating region from ${this._region} to ${detectedRegion} based on country code: ${countryCode}`);
581
+ this._region = detectedRegion;
582
+ setCurrentRegion(detectedRegion);
583
+ }
584
+ }
585
+
586
+ logger.debug('Authentication successful!', {
587
+ region: this._region,
588
+ countryCode: countryCode,
589
+ endpoint: getApiBaseUrl()
590
+ });
557
591
  return true;
558
592
  }
559
593
 
560
- // If we reach here, login failed but didn't throw an error
594
+ // If we reach here, authentication failed
561
595
  const delay = initialDelayMs * Math.pow(2, attempt);
562
- logger.debug(`Login attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
596
+ logger.debug(`Authentication attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
563
597
  await new Promise(resolve => setTimeout(resolve, delay));
564
-
598
+
565
599
  } catch (error) {
600
+ logger.error(`Authentication attempt ${attempt + 1} error:`, error);
601
+
566
602
  if (attempt === retryAttempts - 1) {
567
- logger.error('Login error after all retry attempts:', error);
603
+ logger.error('Authentication failed after all retry attempts');
568
604
  return false;
569
605
  }
570
-
606
+
571
607
  const delay = initialDelayMs * Math.pow(2, attempt);
572
- logger.debug(`Login attempt ${attempt + 1} failed with error, retrying in ${delay}ms...`, error);
608
+ logger.debug(`Retrying authentication in ${delay}ms...`);
573
609
  await new Promise(resolve => setTimeout(resolve, delay));
574
610
  }
575
611
  }
576
612
 
577
- logger.error('Unable to login with supplied credentials after all retry attempts');
613
+ logger.error('Unable to authenticate with supplied credentials after all retry attempts');
578
614
  return false;
579
615
  }
580
616
 
@@ -1,4 +0,0 @@
1
- /**
2
- * Unit tests for VeSyncAir131 air quality data parsing and filter life handling
3
- */
4
- export {};