tsvesync 1.4.5 → 1.4.6

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.
@@ -22,6 +22,7 @@ exports.CROSS_REGION_ERROR_CODES = [
22
22
  // pyvesync parity: `-11001000` maps to TOKEN_EXPIRED (invalid token)
23
23
  exports.TOKEN_ERROR_CODES = [
24
24
  -11001000, // token expired/invalid
25
+ -11001022, // token has expired (observed from device list responses)
25
26
  ];
26
27
  // Other error codes
27
28
  exports.APP_VERSION_ERROR_CODES = [
@@ -34,6 +34,9 @@ export declare const AUTH_ERROR_CODES: {
34
34
  CROSS_REGION: number;
35
35
  CROSS_REGION_ALT: number;
36
36
  };
37
+ type CallApiOptions = {
38
+ didRetryAuth?: boolean;
39
+ };
37
40
  /**
38
41
  * Get list of country codes that use the US endpoint
39
42
  * These are tried when we get a cross-region error
@@ -154,7 +157,7 @@ export declare class Helpers {
154
157
  /**
155
158
  * Call VeSync API
156
159
  */
157
- static callApi(endpoint: string, method: string, data: any | undefined, headers: Record<string, string> | undefined, manager: VeSync): Promise<[any, number]>;
160
+ static callApi(endpoint: string, method: string, data: any | undefined, headers: Record<string, string> | undefined, manager: VeSync, options?: CallApiOptions): Promise<[any, number]>;
158
161
  /**
159
162
  * Calculate hex value from energy response
160
163
  */
@@ -180,3 +183,4 @@ export declare class Helpers {
180
183
  */
181
184
  static authLegacyFlow(manager: VeSync): Promise<[boolean, string | null, string | null, string | null]>;
182
185
  }
186
+ export {};
@@ -93,6 +93,9 @@ exports.AUTH_ERROR_CODES = {
93
93
  CROSS_REGION: -11260022,
94
94
  CROSS_REGION_ALT: -11261022
95
95
  };
96
+ function isTokenExpiredMessage(msg) {
97
+ return typeof msg === 'string' && /token\s*(?:has\s*)?expired/i.test(msg);
98
+ }
96
99
  /**
97
100
  * Get list of country codes that use the US endpoint
98
101
  * These are tried when we get a cross-region error
@@ -436,7 +439,8 @@ class Helpers {
436
439
  /**
437
440
  * Call VeSync API
438
441
  */
439
- static async callApi(endpoint, method, data = null, headers = {}, manager) {
442
+ static async callApi(endpoint, method, data = null, headers = {}, manager, options = {}) {
443
+ var _a, _b;
440
444
  let url = '';
441
445
  try {
442
446
  // Prefer manager-scoped base URL (pyvesync resolves per-manager from current region).
@@ -457,6 +461,21 @@ class Helpers {
457
461
  headers,
458
462
  timeout: exports.API_TIMEOUT
459
463
  });
464
+ // Some VeSync API calls return HTTP 200 with an error `code` in the body.
465
+ // Treat token-expiration responses as recoverable: re-auth once and retry.
466
+ const didRetryAuth = options.didRetryAuth === true;
467
+ const responseCode = (_a = response.data) === null || _a === void 0 ? void 0 : _a.code;
468
+ const responseMsg = (_b = response.data) === null || _b === void 0 ? void 0 : _b.msg;
469
+ if (!didRetryAuth &&
470
+ response.status === 200 &&
471
+ ((typeof responseCode === 'number' && (0, constants_1.isTokenError)(responseCode)) ||
472
+ isTokenExpiredMessage(responseMsg))) {
473
+ logger_1.logger.debug('Token expired, attempting to re-login...');
474
+ if (await manager.login()) {
475
+ logger_1.logger.debug('Re-login successful, retrying original request...');
476
+ return await this.callApi(endpoint, method, data, headers, manager, { ...options, didRetryAuth: true });
477
+ }
478
+ }
460
479
  return [response.data, response.status];
461
480
  }
462
481
  catch (error) {
@@ -465,16 +484,18 @@ class Helpers {
465
484
  // Check for token expiration or auth invalidation
466
485
  const httpStatus = error.response.status;
467
486
  const msg = responseData === null || responseData === void 0 ? void 0 : responseData.msg;
468
- if (httpStatus === 401 ||
487
+ const didRetryAuth = options.didRetryAuth === true;
488
+ if (!didRetryAuth && (httpStatus === 401 ||
469
489
  httpStatus === 419 ||
470
490
  (responseData === null || responseData === void 0 ? void 0 : responseData.code) === 4001004 ||
471
- (typeof msg === 'string' && /token\s*expired/i.test(msg))) {
491
+ (typeof (responseData === null || responseData === void 0 ? void 0 : responseData.code) === 'number' && (0, constants_1.isTokenError)(responseData.code)) ||
492
+ isTokenExpiredMessage(msg))) {
472
493
  logger_1.logger.debug('Token expired, attempting to re-login...');
473
494
  // Re-login
474
495
  if (await manager.login()) {
475
496
  // Retry the original request
476
497
  logger_1.logger.debug('Re-login successful, retrying original request...');
477
- return await this.callApi(endpoint, method, data, headers, manager);
498
+ return await this.callApi(endpoint, method, data, headers, manager, { ...options, didRetryAuth: true });
478
499
  }
479
500
  }
480
501
  // Log specific error details for debugging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tsvesync",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "A TypeScript library for interacting with VeSync smart home devices",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,6 +19,7 @@ export const CROSS_REGION_ERROR_CODES = [
19
19
  // pyvesync parity: `-11001000` maps to TOKEN_EXPIRED (invalid token)
20
20
  export const TOKEN_ERROR_CODES = [
21
21
  -11001000, // token expired/invalid
22
+ -11001022, // token has expired (observed from device list responses)
22
23
  ];
23
24
 
24
25
  // Other error codes
@@ -6,7 +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 } from './constants';
9
+ import { isCredentialError, isCrossRegionError, isTokenError } from './constants';
10
10
 
11
11
  // API configuration - Support regional endpoints
12
12
  let _apiBaseUrl = 'https://smartapi.vesync.com';
@@ -91,6 +91,14 @@ export const AUTH_ERROR_CODES = {
91
91
  CROSS_REGION_ALT: -11261022
92
92
  };
93
93
 
94
+ function isTokenExpiredMessage(msg: unknown): boolean {
95
+ return typeof msg === 'string' && /token\s*(?:has\s*)?expired/i.test(msg);
96
+ }
97
+
98
+ type CallApiOptions = {
99
+ didRetryAuth?: boolean;
100
+ };
101
+
94
102
  /**
95
103
  * Get list of country codes that use the US endpoint
96
104
  * These are tried when we get a cross-region error
@@ -522,7 +530,8 @@ export class Helpers {
522
530
  method: string,
523
531
  data: any = null,
524
532
  headers: Record<string, string> = {},
525
- manager: VeSync
533
+ manager: VeSync,
534
+ options: CallApiOptions = {}
526
535
  ): Promise<[any, number]> {
527
536
  let url = '';
528
537
  try {
@@ -548,6 +557,27 @@ export class Helpers {
548
557
  timeout: API_TIMEOUT
549
558
  });
550
559
 
560
+ // Some VeSync API calls return HTTP 200 with an error `code` in the body.
561
+ // Treat token-expiration responses as recoverable: re-auth once and retry.
562
+ const didRetryAuth = options.didRetryAuth === true;
563
+ const responseCode = response.data?.code;
564
+ const responseMsg = response.data?.msg;
565
+ if (
566
+ !didRetryAuth &&
567
+ response.status === 200 &&
568
+ (
569
+ (typeof responseCode === 'number' && isTokenError(responseCode)) ||
570
+ isTokenExpiredMessage(responseMsg)
571
+ )
572
+ ) {
573
+ logger.debug('Token expired, attempting to re-login...');
574
+
575
+ if (await manager.login()) {
576
+ logger.debug('Re-login successful, retrying original request...');
577
+ return await this.callApi(endpoint, method, data, headers, manager, { ...options, didRetryAuth: true });
578
+ }
579
+ }
580
+
551
581
  return [response.data, response.status];
552
582
  } catch (error: any) {
553
583
  if (error.response) {
@@ -556,19 +586,21 @@ export class Helpers {
556
586
  // Check for token expiration or auth invalidation
557
587
  const httpStatus = error.response.status;
558
588
  const msg: string | undefined = responseData?.msg;
559
- if (
589
+ const didRetryAuth = options.didRetryAuth === true;
590
+ if (!didRetryAuth && (
560
591
  httpStatus === 401 ||
561
592
  httpStatus === 419 ||
562
593
  responseData?.code === 4001004 ||
563
- (typeof msg === 'string' && /token\s*expired/i.test(msg))
564
- ) {
594
+ (typeof responseData?.code === 'number' && isTokenError(responseData.code)) ||
595
+ isTokenExpiredMessage(msg)
596
+ )) {
565
597
  logger.debug('Token expired, attempting to re-login...');
566
598
 
567
599
  // Re-login
568
600
  if (await manager.login()) {
569
601
  // Retry the original request
570
602
  logger.debug('Re-login successful, retrying original request...');
571
- return await this.callApi(endpoint, method, data, headers, manager);
603
+ return await this.callApi(endpoint, method, data, headers, manager, { ...options, didRetryAuth: true });
572
604
  }
573
605
  }
574
606