tsvesync 1.1.2 → 1.2.0
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 +4 -1
- package/dist/lib/helpers.js +8 -2
- package/dist/lib/session.d.ts +24 -0
- package/dist/lib/session.js +20 -0
- package/dist/lib/vesync.d.ts +11 -0
- package/dist/lib/vesync.js +190 -130
- package/package.json +1 -1
- package/src/index.ts +4 -1
- package/src/lib/helpers.ts +11 -3
- package/src/lib/session.ts +35 -0
- package/src/lib/vesync.ts +203 -143
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,4 @@ export { VeSyncWallSwitch } from './lib/vesyncSwitchImpl';
|
|
|
6
6
|
export { VeSyncBaseDevice } from './lib/vesyncBaseDevice';
|
|
7
7
|
export { Helpers, setApiBaseUrl } from './lib/helpers';
|
|
8
8
|
export { CREDENTIAL_ERROR_CODES, CROSS_REGION_ERROR_CODES, isCredentialError, isCrossRegionError } from './lib/constants';
|
|
9
|
+
export { Session, SessionStore, decodeJwtTimestamps } from './lib/session';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
3
|
+
exports.decodeJwtTimestamps = 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; } });
|
|
@@ -29,3 +29,6 @@ Object.defineProperty(exports, "CREDENTIAL_ERROR_CODES", { enumerable: true, get
|
|
|
29
29
|
Object.defineProperty(exports, "CROSS_REGION_ERROR_CODES", { enumerable: true, get: function () { return constants_1.CROSS_REGION_ERROR_CODES; } });
|
|
30
30
|
Object.defineProperty(exports, "isCredentialError", { enumerable: true, get: function () { return constants_1.isCredentialError; } });
|
|
31
31
|
Object.defineProperty(exports, "isCrossRegionError", { enumerable: true, get: function () { return constants_1.isCrossRegionError; } });
|
|
32
|
+
// Export session types and utilities
|
|
33
|
+
var session_1 = require("./lib/session");
|
|
34
|
+
Object.defineProperty(exports, "decodeJwtTimestamps", { enumerable: true, get: function () { return session_1.decodeJwtTimestamps; } });
|
package/dist/lib/helpers.js
CHANGED
|
@@ -400,8 +400,14 @@ class Helpers {
|
|
|
400
400
|
catch (error) {
|
|
401
401
|
if (error.response) {
|
|
402
402
|
const responseData = error.response.data;
|
|
403
|
-
// Check for token expiration
|
|
404
|
-
|
|
403
|
+
// Check for token expiration or auth invalidation
|
|
404
|
+
const httpStatus = error.response.status;
|
|
405
|
+
const msg = responseData === null || responseData === void 0 ? void 0 : responseData.msg;
|
|
406
|
+
if ((responseData === null || responseData === void 0 ? void 0 : responseData.code) === 4001004 ||
|
|
407
|
+
msg === 'token expired' ||
|
|
408
|
+
httpStatus === 401 ||
|
|
409
|
+
httpStatus === 419 ||
|
|
410
|
+
(typeof msg === 'string' && /token|login/i.test(msg))) {
|
|
405
411
|
logger_1.logger.debug('Token expired, attempting to re-login...');
|
|
406
412
|
// Re-login
|
|
407
413
|
if (await manager.login()) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
token: string;
|
|
3
|
+
accountId: string;
|
|
4
|
+
countryCode?: string | null;
|
|
5
|
+
region: string;
|
|
6
|
+
apiBaseUrl: string;
|
|
7
|
+
authFlowUsed?: 'legacy' | 'new';
|
|
8
|
+
issuedAt?: number | null;
|
|
9
|
+
expiresAt?: number | null;
|
|
10
|
+
lastValidatedAt?: number | null;
|
|
11
|
+
libraryVersion?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SessionStore {
|
|
14
|
+
load(): Promise<Session | null>;
|
|
15
|
+
save(session: Session): Promise<void>;
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Decode a JWT token payload to extract iat/exp without verifying the signature.
|
|
20
|
+
*/
|
|
21
|
+
export declare function decodeJwtTimestamps(token: string): {
|
|
22
|
+
iat?: number;
|
|
23
|
+
exp?: number;
|
|
24
|
+
} | null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decodeJwtTimestamps = decodeJwtTimestamps;
|
|
4
|
+
/**
|
|
5
|
+
* Decode a JWT token payload to extract iat/exp without verifying the signature.
|
|
6
|
+
*/
|
|
7
|
+
function decodeJwtTimestamps(token) {
|
|
8
|
+
try {
|
|
9
|
+
const parts = token.split('.');
|
|
10
|
+
if (parts.length !== 3)
|
|
11
|
+
return null;
|
|
12
|
+
const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
|
|
13
|
+
const iat = typeof payload.iat === 'number' ? payload.iat : undefined;
|
|
14
|
+
const exp = typeof payload.exp === 'number' ? payload.exp : undefined;
|
|
15
|
+
return { iat, exp };
|
|
16
|
+
}
|
|
17
|
+
catch (_a) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/lib/vesync.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* VeSync API Device Library
|
|
3
3
|
*/
|
|
4
|
+
import { Session, SessionStore } from './session';
|
|
4
5
|
import { VeSyncBaseDevice } from './vesyncBaseDevice';
|
|
5
6
|
import { Logger } from './logger';
|
|
6
7
|
/**
|
|
@@ -28,6 +29,9 @@ export declare class VeSync {
|
|
|
28
29
|
private _appId;
|
|
29
30
|
private _region;
|
|
30
31
|
private _countryCodeOverride;
|
|
32
|
+
private _sessionStore?;
|
|
33
|
+
private _onTokenChange?;
|
|
34
|
+
private _loginPromise;
|
|
31
35
|
username: string;
|
|
32
36
|
password: string;
|
|
33
37
|
token: string | null;
|
|
@@ -64,7 +68,14 @@ export declare class VeSync {
|
|
|
64
68
|
region?: string;
|
|
65
69
|
countryCode?: string;
|
|
66
70
|
debugMode?: boolean;
|
|
71
|
+
sessionStore?: SessionStore;
|
|
72
|
+
onTokenChange?: (session: Session) => void;
|
|
67
73
|
}, redact?: boolean, apiUrl?: string, customLogger?: Logger, excludeConfig?: ExcludeConfig);
|
|
74
|
+
/**
|
|
75
|
+
* Hydrate manager from a previously persisted session
|
|
76
|
+
*/
|
|
77
|
+
hydrateSession(session: Session): void;
|
|
78
|
+
private emitTokenChange;
|
|
68
79
|
/**
|
|
69
80
|
* Get/Set debug mode
|
|
70
81
|
*/
|
package/dist/lib/vesync.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.VeSync = void 0;
|
|
7
7
|
const helpers_1 = require("./helpers");
|
|
8
|
+
const session_1 = require("./session");
|
|
8
9
|
const vesyncFanImpl_1 = require("./vesyncFanImpl");
|
|
9
10
|
const vesyncOutletImpl_1 = require("./vesyncOutletImpl");
|
|
10
11
|
const vesyncSwitchImpl_1 = require("./vesyncSwitchImpl");
|
|
@@ -132,6 +133,7 @@ class VeSync {
|
|
|
132
133
|
* @param excludeConfig - Optional device exclusion configuration (used when optionsOrDebug is boolean)
|
|
133
134
|
*/
|
|
134
135
|
constructor(username, password, timeZone = helpers_1.DEFAULT_TZ, optionsOrDebug = false, redact = true, apiUrl, customLogger, excludeConfig) {
|
|
136
|
+
this._loginPromise = null;
|
|
135
137
|
// Handle options object or backward compatible parameters
|
|
136
138
|
let options = {};
|
|
137
139
|
if (typeof optionsOrDebug === 'object' && optionsOrDebug !== null) {
|
|
@@ -144,6 +146,8 @@ class VeSync {
|
|
|
144
146
|
this._countryCodeOverride = options.countryCode || null;
|
|
145
147
|
customLogger = options.customLogger;
|
|
146
148
|
apiUrl = options.apiUrl;
|
|
149
|
+
this._sessionStore = options.sessionStore;
|
|
150
|
+
this._onTokenChange = options.onTokenChange;
|
|
147
151
|
}
|
|
148
152
|
else {
|
|
149
153
|
// Using old parameter pattern for backward compatibility
|
|
@@ -221,6 +225,51 @@ class VeSync {
|
|
|
221
225
|
}
|
|
222
226
|
// Debug and redact are already set above in the options handling
|
|
223
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Hydrate manager from a previously persisted session
|
|
230
|
+
*/
|
|
231
|
+
hydrateSession(session) {
|
|
232
|
+
try {
|
|
233
|
+
if (session.apiBaseUrl) {
|
|
234
|
+
(0, helpers_1.setApiBaseUrl)(session.apiBaseUrl);
|
|
235
|
+
this.apiBaseUrl = session.apiBaseUrl;
|
|
236
|
+
}
|
|
237
|
+
if (session.region) {
|
|
238
|
+
this._region = session.region;
|
|
239
|
+
(0, helpers_1.setCurrentRegion)(session.region);
|
|
240
|
+
}
|
|
241
|
+
this.token = session.token;
|
|
242
|
+
this.accountId = session.accountId;
|
|
243
|
+
this.countryCode = session.countryCode || null;
|
|
244
|
+
this.enabled = true;
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
logger_1.logger.warn('Failed to hydrate session; will require fresh login');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
emitTokenChange() {
|
|
251
|
+
var _a, _b, _c, _d;
|
|
252
|
+
if (!this.token || !this.accountId || !this.apiBaseUrl)
|
|
253
|
+
return;
|
|
254
|
+
const timestamps = (0, session_1.decodeJwtTimestamps)(this.token);
|
|
255
|
+
const session = {
|
|
256
|
+
token: this.token,
|
|
257
|
+
accountId: this.accountId,
|
|
258
|
+
countryCode: this.countryCode,
|
|
259
|
+
region: this._region,
|
|
260
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
261
|
+
authFlowUsed: this.authFlowUsed,
|
|
262
|
+
issuedAt: (_a = timestamps === null || timestamps === void 0 ? void 0 : timestamps.iat) !== null && _a !== void 0 ? _a : null,
|
|
263
|
+
expiresAt: (_b = timestamps === null || timestamps === void 0 ? void 0 : timestamps.exp) !== null && _b !== void 0 ? _b : null,
|
|
264
|
+
lastValidatedAt: Date.now(),
|
|
265
|
+
};
|
|
266
|
+
// Best-effort persist and callback; errors are non-fatal
|
|
267
|
+
Promise.resolve((_c = this._sessionStore) === null || _c === void 0 ? void 0 : _c.save(session)).catch(() => { });
|
|
268
|
+
try {
|
|
269
|
+
(_d = this._onTokenChange) === null || _d === void 0 ? void 0 : _d.call(this, session);
|
|
270
|
+
}
|
|
271
|
+
catch ( /* noop */_e) { /* noop */ }
|
|
272
|
+
}
|
|
224
273
|
/**
|
|
225
274
|
* Get/Set debug mode
|
|
226
275
|
*/
|
|
@@ -461,148 +510,159 @@ class VeSync {
|
|
|
461
510
|
* Login to VeSync server with new authentication flow and backwards compatibility
|
|
462
511
|
*/
|
|
463
512
|
async login(retryAttempts = 3, initialDelayMs = 1000) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Check if it's a credential error (no point retrying)
|
|
484
|
-
if (countryCode === 'credential_error') {
|
|
485
|
-
logger_1.logger.error('Authentication failed due to invalid credentials');
|
|
486
|
-
return false; // Exit immediately, don't retry
|
|
513
|
+
if (this._loginPromise) {
|
|
514
|
+
return this._loginPromise;
|
|
515
|
+
}
|
|
516
|
+
this._loginPromise = (async () => {
|
|
517
|
+
logger_1.logger.debug('Starting VeSync authentication...', {
|
|
518
|
+
username: this.username,
|
|
519
|
+
appId: this._appId,
|
|
520
|
+
region: this._region
|
|
521
|
+
});
|
|
522
|
+
// Track which regions have been attempted to prevent infinite loops
|
|
523
|
+
const triedRegions = new Set();
|
|
524
|
+
for (let attempt = 0; attempt < retryAttempts; attempt++) {
|
|
525
|
+
try {
|
|
526
|
+
logger_1.logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
|
|
527
|
+
// Try new authentication flow first with detected region
|
|
528
|
+
let [success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, this._region, this._countryCodeOverride || undefined);
|
|
529
|
+
// Track if new flow succeeded
|
|
530
|
+
if (success) {
|
|
531
|
+
this.authFlowUsed = 'new';
|
|
487
532
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const alternateRegion = this._region === 'US' ? 'EU' : 'US';
|
|
494
|
-
// Check if we've already tried this region to prevent infinite loops
|
|
495
|
-
if (triedRegions.has(alternateRegion)) {
|
|
496
|
-
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
497
|
-
logger_1.logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
|
|
498
|
-
logger_1.logger.error('');
|
|
499
|
-
logger_1.logger.error('Both US and EU endpoints rejected your account.');
|
|
500
|
-
logger_1.logger.error('This means you MUST specify your country code.');
|
|
501
|
-
logger_1.logger.error('');
|
|
502
|
-
logger_1.logger.error('SOLUTION:');
|
|
503
|
-
logger_1.logger.error('1. In Homebridge UI:');
|
|
504
|
-
logger_1.logger.error(' - Go to plugin settings');
|
|
505
|
-
logger_1.logger.error(' - Select your country from the "Country Code" dropdown');
|
|
506
|
-
logger_1.logger.error('');
|
|
507
|
-
logger_1.logger.error('2. In config.json:');
|
|
508
|
-
logger_1.logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
|
|
509
|
-
logger_1.logger.error('');
|
|
510
|
-
logger_1.logger.error('Common non-US/EU country codes:');
|
|
511
|
-
logger_1.logger.error(' 🇦🇺 Australia: "AU"');
|
|
512
|
-
logger_1.logger.error(' 🇳🇿 New Zealand: "NZ"');
|
|
513
|
-
logger_1.logger.error(' 🇯🇵 Japan: "JP"');
|
|
514
|
-
logger_1.logger.error(' 🇨🇦 Canada: "CA"');
|
|
515
|
-
logger_1.logger.error(' 🇸🇬 Singapore: "SG"');
|
|
516
|
-
logger_1.logger.error(' 🇲🇽 Mexico: "MX"');
|
|
517
|
-
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
logger_1.logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
|
|
521
|
-
this._region = alternateRegion;
|
|
522
|
-
(0, helpers_1.setCurrentRegion)(alternateRegion);
|
|
523
|
-
// Update the API endpoint for the new region
|
|
524
|
-
const regionEndpoints = {
|
|
525
|
-
'US': 'https://smartapi.vesync.com',
|
|
526
|
-
'EU': 'https://smartapi.vesync.eu'
|
|
527
|
-
};
|
|
528
|
-
if (alternateRegion in regionEndpoints) {
|
|
529
|
-
(0, helpers_1.setApiBaseUrl)(regionEndpoints[alternateRegion]);
|
|
530
|
-
logger_1.logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
|
|
533
|
+
if (!success) {
|
|
534
|
+
// Check if it's a credential error (no point retrying)
|
|
535
|
+
if (countryCode === 'credential_error') {
|
|
536
|
+
logger_1.logger.error('Authentication failed due to invalid credentials');
|
|
537
|
+
return false; // Exit immediately, don't retry
|
|
531
538
|
}
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
this.
|
|
536
|
-
logger_1.logger.debug(
|
|
539
|
+
// Check if we need to try a different region
|
|
540
|
+
if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
|
|
541
|
+
// Mark current region as tried
|
|
542
|
+
triedRegions.add(this._region);
|
|
543
|
+
logger_1.logger.debug('Cross-region error detected, trying opposite region...');
|
|
544
|
+
const alternateRegion = this._region === 'US' ? 'EU' : 'US';
|
|
545
|
+
// Check if we've already tried this region to prevent infinite loops
|
|
546
|
+
if (triedRegions.has(alternateRegion)) {
|
|
547
|
+
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
548
|
+
logger_1.logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
|
|
549
|
+
logger_1.logger.error('');
|
|
550
|
+
logger_1.logger.error('Both US and EU endpoints rejected your account.');
|
|
551
|
+
logger_1.logger.error('This means you MUST specify your country code.');
|
|
552
|
+
logger_1.logger.error('');
|
|
553
|
+
logger_1.logger.error('SOLUTION:');
|
|
554
|
+
logger_1.logger.error('1. In Homebridge UI:');
|
|
555
|
+
logger_1.logger.error(' - Go to plugin settings');
|
|
556
|
+
logger_1.logger.error(' - Select your country from the "Country Code" dropdown');
|
|
557
|
+
logger_1.logger.error('');
|
|
558
|
+
logger_1.logger.error('2. In config.json:');
|
|
559
|
+
logger_1.logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
|
|
560
|
+
logger_1.logger.error('');
|
|
561
|
+
logger_1.logger.error('Common non-US/EU country codes:');
|
|
562
|
+
logger_1.logger.error(' 🇦🇺 Australia: "AU"');
|
|
563
|
+
logger_1.logger.error(' 🇳🇿 New Zealand: "NZ"');
|
|
564
|
+
logger_1.logger.error(' 🇯🇵 Japan: "JP"');
|
|
565
|
+
logger_1.logger.error(' 🇨🇦 Canada: "CA"');
|
|
566
|
+
logger_1.logger.error(' 🇸🇬 Singapore: "SG"');
|
|
567
|
+
logger_1.logger.error(' 🇲🇽 Mexico: "MX"');
|
|
568
|
+
logger_1.logger.error('═══════════════════════════════════════════════════════════════');
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
logger_1.logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
|
|
572
|
+
this._region = alternateRegion;
|
|
573
|
+
(0, helpers_1.setCurrentRegion)(alternateRegion);
|
|
574
|
+
// Update the API endpoint for the new region
|
|
575
|
+
const regionEndpoints = {
|
|
576
|
+
'US': 'https://smartapi.vesync.com',
|
|
577
|
+
'EU': 'https://smartapi.vesync.eu'
|
|
578
|
+
};
|
|
579
|
+
if (alternateRegion in regionEndpoints) {
|
|
580
|
+
(0, helpers_1.setApiBaseUrl)(regionEndpoints[alternateRegion]);
|
|
581
|
+
logger_1.logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
|
|
582
|
+
}
|
|
583
|
+
// Retry with the alternate region
|
|
584
|
+
[success, token, accountId, countryCode] = await helpers_1.Helpers.authNewFlow(this, this._appId, alternateRegion, this._countryCodeOverride || undefined);
|
|
585
|
+
if (success) {
|
|
586
|
+
this.authFlowUsed = 'new';
|
|
587
|
+
logger_1.logger.debug(`Authentication successful with ${alternateRegion} region`);
|
|
588
|
+
}
|
|
589
|
+
else if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
|
|
590
|
+
// Both regions failed with cross-region error
|
|
591
|
+
logger_1.logger.error('Authentication failed: Account rejected by both US and EU regions');
|
|
592
|
+
logger_1.logger.error('This may indicate your account was created in a region not yet supported');
|
|
593
|
+
logger_1.logger.error('Please contact VeSync support to determine your account region');
|
|
594
|
+
// Don't retry further attempts as we've exhausted both regions
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
537
597
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
logger_1.logger.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
598
|
+
// If new flow still fails, try legacy authentication as fallback
|
|
599
|
+
if (!success) {
|
|
600
|
+
logger_1.logger.debug('New authentication flow failed, trying legacy flow...');
|
|
601
|
+
[success, token, accountId, countryCode] = await helpers_1.Helpers.authLegacyFlow(this);
|
|
602
|
+
if (success) {
|
|
603
|
+
this.authFlowUsed = 'legacy';
|
|
604
|
+
}
|
|
605
|
+
else if (countryCode === 'credential_error') {
|
|
606
|
+
// Legacy auth also detected bad credentials
|
|
607
|
+
logger_1.logger.error('Both authentication methods report invalid credentials');
|
|
608
|
+
return false; // Exit immediately
|
|
609
|
+
}
|
|
545
610
|
}
|
|
546
611
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
612
|
+
if (success && token && accountId) {
|
|
613
|
+
this.token = token;
|
|
614
|
+
this.accountId = accountId;
|
|
615
|
+
this.countryCode = countryCode;
|
|
616
|
+
this.enabled = true;
|
|
617
|
+
this.apiBaseUrl = (0, helpers_1.getApiBaseUrl)(); // Track the API endpoint being used
|
|
618
|
+
// Persist session details for reuse across restarts
|
|
619
|
+
this.emitTokenChange();
|
|
620
|
+
// DO NOT change the region/endpoint after successful authentication!
|
|
621
|
+
// The endpoint that successfully authenticated is the one we must continue using.
|
|
622
|
+
// Tokens are endpoint-specific and won't work if we switch endpoints.
|
|
623
|
+
// This is especially important for AU/NZ users who may authenticate via EU endpoint
|
|
624
|
+
// but have AU/NZ country codes.
|
|
625
|
+
// Log a warning if the country code doesn't match the successful region
|
|
626
|
+
if (countryCode) {
|
|
627
|
+
const expectedRegion = (0, helpers_1.getRegionFromCountryCode)(countryCode);
|
|
628
|
+
if (expectedRegion !== this._region) {
|
|
629
|
+
logger_1.logger.warn(`Note: Account authenticated with ${this._region} endpoint despite country code ${countryCode} typically using ${expectedRegion}`);
|
|
630
|
+
logger_1.logger.warn(`This is normal for accounts created through different regional apps`);
|
|
631
|
+
}
|
|
558
632
|
}
|
|
633
|
+
logger_1.logger.debug('Authentication successful!', {
|
|
634
|
+
authFlow: this.authFlowUsed,
|
|
635
|
+
region: this._region,
|
|
636
|
+
countryCode: countryCode,
|
|
637
|
+
endpoint: (0, helpers_1.getApiBaseUrl)()
|
|
638
|
+
});
|
|
639
|
+
return true;
|
|
559
640
|
}
|
|
641
|
+
// If we reach here, authentication failed
|
|
642
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
643
|
+
logger_1.logger.debug(`Authentication attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
644
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
560
645
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this.apiBaseUrl = (0, helpers_1.getApiBaseUrl)(); // Track the API endpoint being used
|
|
567
|
-
// DO NOT change the region/endpoint after successful authentication!
|
|
568
|
-
// The endpoint that successfully authenticated is the one we must continue using.
|
|
569
|
-
// Tokens are endpoint-specific and won't work if we switch endpoints.
|
|
570
|
-
// This is especially important for AU/NZ users who may authenticate via EU endpoint
|
|
571
|
-
// but have AU/NZ country codes.
|
|
572
|
-
// Log a warning if the country code doesn't match the successful region
|
|
573
|
-
if (countryCode) {
|
|
574
|
-
const expectedRegion = (0, helpers_1.getRegionFromCountryCode)(countryCode);
|
|
575
|
-
if (expectedRegion !== this._region) {
|
|
576
|
-
logger_1.logger.warn(`Note: Account authenticated with ${this._region} endpoint despite country code ${countryCode} typically using ${expectedRegion}`);
|
|
577
|
-
logger_1.logger.warn(`This is normal for accounts created through different regional apps`);
|
|
578
|
-
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
logger_1.logger.error(`Authentication attempt ${attempt + 1} error:`, error);
|
|
648
|
+
if (attempt === retryAttempts - 1) {
|
|
649
|
+
logger_1.logger.error('Authentication failed after all retry attempts');
|
|
650
|
+
return false;
|
|
579
651
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
countryCode: countryCode,
|
|
584
|
-
endpoint: (0, helpers_1.getApiBaseUrl)()
|
|
585
|
-
});
|
|
586
|
-
return true;
|
|
587
|
-
}
|
|
588
|
-
// If we reach here, authentication failed
|
|
589
|
-
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
590
|
-
logger_1.logger.debug(`Authentication attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
591
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
592
|
-
}
|
|
593
|
-
catch (error) {
|
|
594
|
-
logger_1.logger.error(`Authentication attempt ${attempt + 1} error:`, error);
|
|
595
|
-
if (attempt === retryAttempts - 1) {
|
|
596
|
-
logger_1.logger.error('Authentication failed after all retry attempts');
|
|
597
|
-
return false;
|
|
652
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
653
|
+
logger_1.logger.debug(`Retrying authentication in ${delay}ms...`);
|
|
654
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
598
655
|
}
|
|
599
|
-
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
600
|
-
logger_1.logger.debug(`Retrying authentication in ${delay}ms...`);
|
|
601
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
602
656
|
}
|
|
657
|
+
logger_1.logger.error('Unable to authenticate with supplied credentials after all retry attempts');
|
|
658
|
+
return false;
|
|
659
|
+
})();
|
|
660
|
+
try {
|
|
661
|
+
return await this._loginPromise;
|
|
662
|
+
}
|
|
663
|
+
finally {
|
|
664
|
+
this._loginPromise = null;
|
|
603
665
|
}
|
|
604
|
-
logger_1.logger.error('Unable to authenticate with supplied credentials after all retry attempts');
|
|
605
|
-
return false;
|
|
606
666
|
}
|
|
607
667
|
/**
|
|
608
668
|
* Test if update interval has been exceeded
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/lib/helpers.ts
CHANGED
|
@@ -491,8 +491,16 @@ export class Helpers {
|
|
|
491
491
|
if (error.response) {
|
|
492
492
|
const responseData = error.response.data;
|
|
493
493
|
|
|
494
|
-
// Check for token expiration
|
|
495
|
-
|
|
494
|
+
// Check for token expiration or auth invalidation
|
|
495
|
+
const httpStatus = error.response.status;
|
|
496
|
+
const msg: string | undefined = responseData?.msg;
|
|
497
|
+
if (
|
|
498
|
+
responseData?.code === 4001004 ||
|
|
499
|
+
msg === 'token expired' ||
|
|
500
|
+
httpStatus === 401 ||
|
|
501
|
+
httpStatus === 419 ||
|
|
502
|
+
(typeof msg === 'string' && /token|login/i.test(msg))
|
|
503
|
+
) {
|
|
496
504
|
logger.debug('Token expired, attempting to re-login...');
|
|
497
505
|
|
|
498
506
|
// Re-login
|
|
@@ -789,4 +797,4 @@ export class Helpers {
|
|
|
789
797
|
return [false, null, null, null];
|
|
790
798
|
}
|
|
791
799
|
}
|
|
792
|
-
}
|
|
800
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
token: string;
|
|
3
|
+
accountId: string;
|
|
4
|
+
countryCode?: string | null;
|
|
5
|
+
region: string;
|
|
6
|
+
apiBaseUrl: string;
|
|
7
|
+
authFlowUsed?: 'legacy' | 'new';
|
|
8
|
+
issuedAt?: number | null; // epoch seconds
|
|
9
|
+
expiresAt?: number | null; // epoch seconds
|
|
10
|
+
lastValidatedAt?: number | null; // epoch ms
|
|
11
|
+
libraryVersion?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SessionStore {
|
|
15
|
+
load(): Promise<Session | null>;
|
|
16
|
+
save(session: Session): Promise<void>;
|
|
17
|
+
clear(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode a JWT token payload to extract iat/exp without verifying the signature.
|
|
22
|
+
*/
|
|
23
|
+
export function decodeJwtTimestamps(token: string): { iat?: number; exp?: number } | null {
|
|
24
|
+
try {
|
|
25
|
+
const parts = token.split('.');
|
|
26
|
+
if (parts.length !== 3) return null;
|
|
27
|
+
const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
|
|
28
|
+
const iat = typeof payload.iat === 'number' ? payload.iat : undefined;
|
|
29
|
+
const exp = typeof payload.exp === 'number' ? payload.exp : undefined;
|
|
30
|
+
return { iat, exp };
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
package/src/lib/vesync.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Helpers, API_RATE_LIMIT, DEFAULT_TZ, setApiBaseUrl, getApiBaseUrl, generateAppId, getRegionFromCountryCode, setCurrentRegion, getCurrentRegion, REGION_ENDPOINTS, getCountryCodeFromRegion, getEndpointForCountryCode } from './helpers';
|
|
6
|
+
import { Session, SessionStore, decodeJwtTimestamps } from './session';
|
|
6
7
|
import { VeSyncBaseDevice } from './vesyncBaseDevice';
|
|
7
8
|
import { fanModules } from './vesyncFanImpl';
|
|
8
9
|
import { outletModules } from './vesyncOutletImpl';
|
|
@@ -162,6 +163,9 @@ export class VeSync {
|
|
|
162
163
|
private _appId: string;
|
|
163
164
|
private _region: string;
|
|
164
165
|
private _countryCodeOverride: string | null;
|
|
166
|
+
private _sessionStore?: SessionStore;
|
|
167
|
+
private _onTokenChange?: (session: Session) => void;
|
|
168
|
+
private _loginPromise: Promise<boolean> | null = null;
|
|
165
169
|
|
|
166
170
|
username: string;
|
|
167
171
|
password: string;
|
|
@@ -205,6 +209,8 @@ export class VeSync {
|
|
|
205
209
|
region?: string;
|
|
206
210
|
countryCode?: string; // Override country code for Step 2 authentication
|
|
207
211
|
debugMode?: boolean; // Alias for debug
|
|
212
|
+
sessionStore?: SessionStore;
|
|
213
|
+
onTokenChange?: (session: Session) => void;
|
|
208
214
|
} = false,
|
|
209
215
|
redact = true,
|
|
210
216
|
apiUrl?: string,
|
|
@@ -223,6 +229,8 @@ export class VeSync {
|
|
|
223
229
|
this._countryCodeOverride = options.countryCode || null;
|
|
224
230
|
customLogger = options.customLogger;
|
|
225
231
|
apiUrl = options.apiUrl;
|
|
232
|
+
this._sessionStore = options.sessionStore;
|
|
233
|
+
this._onTokenChange = options.onTokenChange;
|
|
226
234
|
} else {
|
|
227
235
|
// Using old parameter pattern for backward compatibility
|
|
228
236
|
this._debug = optionsOrDebug as boolean;
|
|
@@ -306,6 +314,47 @@ export class VeSync {
|
|
|
306
314
|
// Debug and redact are already set above in the options handling
|
|
307
315
|
}
|
|
308
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Hydrate manager from a previously persisted session
|
|
319
|
+
*/
|
|
320
|
+
hydrateSession(session: Session): void {
|
|
321
|
+
try {
|
|
322
|
+
if (session.apiBaseUrl) {
|
|
323
|
+
setApiBaseUrl(session.apiBaseUrl);
|
|
324
|
+
this.apiBaseUrl = session.apiBaseUrl;
|
|
325
|
+
}
|
|
326
|
+
if (session.region) {
|
|
327
|
+
this._region = session.region;
|
|
328
|
+
setCurrentRegion(session.region);
|
|
329
|
+
}
|
|
330
|
+
this.token = session.token;
|
|
331
|
+
this.accountId = session.accountId;
|
|
332
|
+
this.countryCode = session.countryCode || null;
|
|
333
|
+
this.enabled = true;
|
|
334
|
+
} catch (e) {
|
|
335
|
+
logger.warn('Failed to hydrate session; will require fresh login');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private emitTokenChange(): void {
|
|
340
|
+
if (!this.token || !this.accountId || !this.apiBaseUrl) return;
|
|
341
|
+
const timestamps = decodeJwtTimestamps(this.token);
|
|
342
|
+
const session: Session = {
|
|
343
|
+
token: this.token,
|
|
344
|
+
accountId: this.accountId,
|
|
345
|
+
countryCode: this.countryCode,
|
|
346
|
+
region: this._region,
|
|
347
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
348
|
+
authFlowUsed: this.authFlowUsed,
|
|
349
|
+
issuedAt: timestamps?.iat ?? null,
|
|
350
|
+
expiresAt: timestamps?.exp ?? null,
|
|
351
|
+
lastValidatedAt: Date.now(),
|
|
352
|
+
};
|
|
353
|
+
// Best-effort persist and callback; errors are non-fatal
|
|
354
|
+
Promise.resolve(this._sessionStore?.save(session)).catch(() => {});
|
|
355
|
+
try { this._onTokenChange?.(session); } catch { /* noop */ }
|
|
356
|
+
}
|
|
357
|
+
|
|
309
358
|
/**
|
|
310
359
|
* Get/Set debug mode
|
|
311
360
|
*/
|
|
@@ -576,168 +625,179 @@ export class VeSync {
|
|
|
576
625
|
* Login to VeSync server with new authentication flow and backwards compatibility
|
|
577
626
|
*/
|
|
578
627
|
async login(retryAttempts: number = 3, initialDelayMs: number = 1000): Promise<boolean> {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
for (let attempt = 0; attempt < retryAttempts; attempt++) {
|
|
589
|
-
try {
|
|
590
|
-
logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
|
|
628
|
+
if (this._loginPromise) {
|
|
629
|
+
return this._loginPromise;
|
|
630
|
+
}
|
|
631
|
+
this._loginPromise = (async () => {
|
|
632
|
+
logger.debug('Starting VeSync authentication...', {
|
|
633
|
+
username: this.username,
|
|
634
|
+
appId: this._appId,
|
|
635
|
+
region: this._region
|
|
636
|
+
});
|
|
591
637
|
|
|
592
|
-
|
|
593
|
-
|
|
638
|
+
// Track which regions have been attempted to prevent infinite loops
|
|
639
|
+
const triedRegions = new Set<string>();
|
|
594
640
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// Track if new flow succeeded
|
|
599
|
-
if (success) {
|
|
600
|
-
this.authFlowUsed = 'new';
|
|
601
|
-
}
|
|
641
|
+
for (let attempt = 0; attempt < retryAttempts; attempt++) {
|
|
642
|
+
try {
|
|
643
|
+
logger.debug(`Authentication attempt ${attempt + 1} of ${retryAttempts}`);
|
|
602
644
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (countryCode === 'credential_error') {
|
|
606
|
-
logger.error('Authentication failed due to invalid credentials');
|
|
607
|
-
return false; // Exit immediately, don't retry
|
|
608
|
-
}
|
|
645
|
+
// Try new authentication flow first with detected region
|
|
646
|
+
let [success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, this._region, this._countryCodeOverride || undefined);
|
|
609
647
|
|
|
610
|
-
//
|
|
611
|
-
if (
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
logger.error('═══════════════════════════════════════════════════════════════');
|
|
621
|
-
logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
|
|
622
|
-
logger.error('');
|
|
623
|
-
logger.error('Both US and EU endpoints rejected your account.');
|
|
624
|
-
logger.error('This means you MUST specify your country code.');
|
|
625
|
-
logger.error('');
|
|
626
|
-
logger.error('SOLUTION:');
|
|
627
|
-
logger.error('1. In Homebridge UI:');
|
|
628
|
-
logger.error(' - Go to plugin settings');
|
|
629
|
-
logger.error(' - Select your country from the "Country Code" dropdown');
|
|
630
|
-
logger.error('');
|
|
631
|
-
logger.error('2. In config.json:');
|
|
632
|
-
logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
|
|
633
|
-
logger.error('');
|
|
634
|
-
logger.error('Common non-US/EU country codes:');
|
|
635
|
-
logger.error(' 🇦🇺 Australia: "AU"');
|
|
636
|
-
logger.error(' 🇳🇿 New Zealand: "NZ"');
|
|
637
|
-
logger.error(' 🇯🇵 Japan: "JP"');
|
|
638
|
-
logger.error(' 🇨🇦 Canada: "CA"');
|
|
639
|
-
logger.error(' 🇸🇬 Singapore: "SG"');
|
|
640
|
-
logger.error(' 🇲🇽 Mexico: "MX"');
|
|
641
|
-
logger.error('═══════════════════════════════════════════════════════════════');
|
|
642
|
-
return false;
|
|
648
|
+
// Track if new flow succeeded
|
|
649
|
+
if (success) {
|
|
650
|
+
this.authFlowUsed = 'new';
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (!success) {
|
|
654
|
+
// Check if it's a credential error (no point retrying)
|
|
655
|
+
if (countryCode === 'credential_error') {
|
|
656
|
+
logger.error('Authentication failed due to invalid credentials');
|
|
657
|
+
return false; // Exit immediately, don't retry
|
|
643
658
|
}
|
|
644
659
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
'
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
660
|
+
// Check if we need to try a different region
|
|
661
|
+
if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
|
|
662
|
+
// Mark current region as tried
|
|
663
|
+
triedRegions.add(this._region);
|
|
664
|
+
|
|
665
|
+
logger.debug('Cross-region error detected, trying opposite region...');
|
|
666
|
+
const alternateRegion = this._region === 'US' ? 'EU' : 'US';
|
|
667
|
+
|
|
668
|
+
// Check if we've already tried this region to prevent infinite loops
|
|
669
|
+
if (triedRegions.has(alternateRegion)) {
|
|
670
|
+
logger.error('═══════════════════════════════════════════════════════════════');
|
|
671
|
+
logger.error('AUTHENTICATION FAILED: COUNTRY CODE REQUIRED');
|
|
672
|
+
logger.error('');
|
|
673
|
+
logger.error('Both US and EU endpoints rejected your account.');
|
|
674
|
+
logger.error('This means you MUST specify your country code.');
|
|
675
|
+
logger.error('');
|
|
676
|
+
logger.error('SOLUTION:');
|
|
677
|
+
logger.error('1. In Homebridge UI:');
|
|
678
|
+
logger.error(' - Go to plugin settings');
|
|
679
|
+
logger.error(' - Select your country from the "Country Code" dropdown');
|
|
680
|
+
logger.error('');
|
|
681
|
+
logger.error('2. In config.json:');
|
|
682
|
+
logger.error(' Add: "countryCode": "YOUR_COUNTRY_CODE"');
|
|
683
|
+
logger.error('');
|
|
684
|
+
logger.error('Common non-US/EU country codes:');
|
|
685
|
+
logger.error(' 🇦🇺 Australia: "AU"');
|
|
686
|
+
logger.error(' 🇳🇿 New Zealand: "NZ"');
|
|
687
|
+
logger.error(' 🇯🇵 Japan: "JP"');
|
|
688
|
+
logger.error(' 🇨🇦 Canada: "CA"');
|
|
689
|
+
logger.error(' 🇸🇬 Singapore: "SG"');
|
|
690
|
+
logger.error(' 🇲🇽 Mexico: "MX"');
|
|
691
|
+
logger.error('═══════════════════════════════════════════════════════════════');
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
logger.debug(`Switching from ${this._region} to ${alternateRegion} region`);
|
|
696
|
+
|
|
697
|
+
this._region = alternateRegion;
|
|
698
|
+
setCurrentRegion(alternateRegion);
|
|
699
|
+
|
|
700
|
+
// Update the API endpoint for the new region
|
|
701
|
+
const regionEndpoints: Record<string, string> = {
|
|
702
|
+
'US': 'https://smartapi.vesync.com',
|
|
703
|
+
'EU': 'https://smartapi.vesync.eu'
|
|
704
|
+
};
|
|
705
|
+
if (alternateRegion in regionEndpoints) {
|
|
706
|
+
setApiBaseUrl(regionEndpoints[alternateRegion]);
|
|
707
|
+
logger.debug(`API endpoint set to ${regionEndpoints[alternateRegion]}`);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Retry with the alternate region
|
|
711
|
+
[success, token, accountId, countryCode] = await Helpers.authNewFlow(this, this._appId, alternateRegion, this._countryCodeOverride || undefined);
|
|
712
|
+
if (success) {
|
|
713
|
+
this.authFlowUsed = 'new';
|
|
714
|
+
logger.debug(`Authentication successful with ${alternateRegion} region`);
|
|
715
|
+
} else if (countryCode === 'cross_region' || countryCode === 'cross_region_retry') {
|
|
716
|
+
// Both regions failed with cross-region error
|
|
717
|
+
logger.error('Authentication failed: Account rejected by both US and EU regions');
|
|
718
|
+
logger.error('This may indicate your account was created in a region not yet supported');
|
|
719
|
+
logger.error('Please contact VeSync support to determine your account region');
|
|
720
|
+
// Don't retry further attempts as we've exhausted both regions
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
658
723
|
}
|
|
659
|
-
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
return false;
|
|
724
|
+
|
|
725
|
+
// If new flow still fails, try legacy authentication as fallback
|
|
726
|
+
if (!success) {
|
|
727
|
+
logger.debug('New authentication flow failed, trying legacy flow...');
|
|
728
|
+
[success, token, accountId, countryCode] = await Helpers.authLegacyFlow(this);
|
|
729
|
+
if (success) {
|
|
730
|
+
this.authFlowUsed = 'legacy';
|
|
731
|
+
} else if (countryCode === 'credential_error') {
|
|
732
|
+
// Legacy auth also detected bad credentials
|
|
733
|
+
logger.error('Both authentication methods report invalid credentials');
|
|
734
|
+
return false; // Exit immediately
|
|
735
|
+
}
|
|
672
736
|
}
|
|
673
737
|
}
|
|
674
738
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
739
|
+
if (success && token && accountId) {
|
|
740
|
+
this.token = token;
|
|
741
|
+
this.accountId = accountId;
|
|
742
|
+
this.countryCode = countryCode;
|
|
743
|
+
this.enabled = true;
|
|
744
|
+
this.apiBaseUrl = getApiBaseUrl(); // Track the API endpoint being used
|
|
745
|
+
|
|
746
|
+
// Persist session details for reuse across restarts
|
|
747
|
+
this.emitTokenChange();
|
|
748
|
+
|
|
749
|
+
// DO NOT change the region/endpoint after successful authentication!
|
|
750
|
+
// The endpoint that successfully authenticated is the one we must continue using.
|
|
751
|
+
// Tokens are endpoint-specific and won't work if we switch endpoints.
|
|
752
|
+
// This is especially important for AU/NZ users who may authenticate via EU endpoint
|
|
753
|
+
// but have AU/NZ country codes.
|
|
754
|
+
|
|
755
|
+
// Log a warning if the country code doesn't match the successful region
|
|
756
|
+
if (countryCode) {
|
|
757
|
+
const expectedRegion = getRegionFromCountryCode(countryCode);
|
|
758
|
+
if (expectedRegion !== this._region) {
|
|
759
|
+
logger.warn(`Note: Account authenticated with ${this._region} endpoint despite country code ${countryCode} typically using ${expectedRegion}`);
|
|
760
|
+
logger.warn(`This is normal for accounts created through different regional apps`);
|
|
761
|
+
}
|
|
685
762
|
}
|
|
763
|
+
|
|
764
|
+
logger.debug('Authentication successful!', {
|
|
765
|
+
authFlow: this.authFlowUsed,
|
|
766
|
+
region: this._region,
|
|
767
|
+
countryCode: countryCode,
|
|
768
|
+
endpoint: getApiBaseUrl()
|
|
769
|
+
});
|
|
770
|
+
return true;
|
|
686
771
|
}
|
|
687
|
-
}
|
|
688
772
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
// DO NOT change the region/endpoint after successful authentication!
|
|
697
|
-
// The endpoint that successfully authenticated is the one we must continue using.
|
|
698
|
-
// Tokens are endpoint-specific and won't work if we switch endpoints.
|
|
699
|
-
// This is especially important for AU/NZ users who may authenticate via EU endpoint
|
|
700
|
-
// but have AU/NZ country codes.
|
|
773
|
+
// If we reach here, authentication failed
|
|
774
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
775
|
+
logger.debug(`Authentication attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
776
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
777
|
+
|
|
778
|
+
} catch (error) {
|
|
779
|
+
logger.error(`Authentication attempt ${attempt + 1} error:`, error);
|
|
701
780
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
if (expectedRegion !== this._region) {
|
|
706
|
-
logger.warn(`Note: Account authenticated with ${this._region} endpoint despite country code ${countryCode} typically using ${expectedRegion}`);
|
|
707
|
-
logger.warn(`This is normal for accounts created through different regional apps`);
|
|
708
|
-
}
|
|
781
|
+
if (attempt === retryAttempts - 1) {
|
|
782
|
+
logger.error('Authentication failed after all retry attempts');
|
|
783
|
+
return false;
|
|
709
784
|
}
|
|
710
785
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
countryCode: countryCode,
|
|
715
|
-
endpoint: getApiBaseUrl()
|
|
716
|
-
});
|
|
717
|
-
return true;
|
|
786
|
+
const delay = initialDelayMs * Math.pow(2, attempt);
|
|
787
|
+
logger.debug(`Retrying authentication in ${delay}ms...`);
|
|
788
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
718
789
|
}
|
|
790
|
+
}
|
|
719
791
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
724
|
-
|
|
725
|
-
} catch (error) {
|
|
726
|
-
logger.error(`Authentication attempt ${attempt + 1} error:`, error);
|
|
727
|
-
|
|
728
|
-
if (attempt === retryAttempts - 1) {
|
|
729
|
-
logger.error('Authentication failed after all retry attempts');
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
792
|
+
logger.error('Unable to authenticate with supplied credentials after all retry attempts');
|
|
793
|
+
return false;
|
|
794
|
+
})();
|
|
732
795
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
796
|
+
try {
|
|
797
|
+
return await this._loginPromise;
|
|
798
|
+
} finally {
|
|
799
|
+
this._loginPromise = null;
|
|
737
800
|
}
|
|
738
|
-
|
|
739
|
-
logger.error('Unable to authenticate with supplied credentials after all retry attempts');
|
|
740
|
-
return false;
|
|
741
801
|
}
|
|
742
802
|
|
|
743
803
|
/**
|
|
@@ -858,4 +918,4 @@ export class VeSync {
|
|
|
858
918
|
): Promise<[any, number]> {
|
|
859
919
|
return await Helpers.callApi(endpoint, method, data, headers, this);
|
|
860
920
|
}
|
|
861
|
-
}
|
|
921
|
+
}
|