mysa-js-sdk 2.1.2 → 3.0.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/CHANGELOG.md +20 -0
- package/README.md +18 -34
- package/dist/index.cjs +113 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -54
- package/dist/index.d.ts +71 -54
- package/dist/index.js +113 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,14 +24,24 @@ SOFTWARE.
|
|
|
24
24
|
|
|
25
25
|
// src/api/Errors.ts
|
|
26
26
|
var UnauthenticatedError = class extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* The underlying failure that prevented authentication.
|
|
29
|
+
*
|
|
30
|
+
* Callers need it to tell a credential rejection apart from a transport or service failure, since both surface as
|
|
31
|
+
* this error. Declared explicitly rather than passed to `super` so the SDK keeps compiling against its current lib,
|
|
32
|
+
* which predates the two-argument `Error` constructor.
|
|
33
|
+
*/
|
|
34
|
+
cause;
|
|
27
35
|
/**
|
|
28
36
|
* Creates a new UnauthenticatedError instance.
|
|
29
37
|
*
|
|
30
38
|
* @param message - The error message
|
|
39
|
+
* @param cause - The underlying failure that prevented authentication, if any.
|
|
31
40
|
*/
|
|
32
|
-
constructor(message) {
|
|
41
|
+
constructor(message, cause) {
|
|
33
42
|
super(message);
|
|
34
43
|
this.name = "UnauthenticatedError";
|
|
44
|
+
this.cause = cause;
|
|
35
45
|
}
|
|
36
46
|
};
|
|
37
47
|
var MysaApiError = class extends Error {
|
|
@@ -53,6 +63,19 @@ var MysaApiError = class extends Error {
|
|
|
53
63
|
this.statusText = apiResponse.statusText;
|
|
54
64
|
}
|
|
55
65
|
};
|
|
66
|
+
var UnknownDeviceError = class extends Error {
|
|
67
|
+
/**
|
|
68
|
+
* Creates a new UnknownDeviceError instance.
|
|
69
|
+
*
|
|
70
|
+
* @param deviceId - The device id that could not be resolved
|
|
71
|
+
*/
|
|
72
|
+
constructor(deviceId) {
|
|
73
|
+
super(`Unknown device id '${deviceId}': no such device on this account.`);
|
|
74
|
+
this.deviceId = deviceId;
|
|
75
|
+
this.name = "UnknownDeviceError";
|
|
76
|
+
}
|
|
77
|
+
deviceId;
|
|
78
|
+
};
|
|
56
79
|
var MqttPublishError = class extends Error {
|
|
57
80
|
/**
|
|
58
81
|
* Creates a new MqttPublishError instance.
|
|
@@ -150,8 +173,9 @@ function parseMqttPayload(payload) {
|
|
|
150
173
|
const jsonString = decoder.decode(payload);
|
|
151
174
|
return JSON.parse(jsonString);
|
|
152
175
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
const parseError = new Error("Failed to parse MQTT payload");
|
|
177
|
+
parseError.cause = error;
|
|
178
|
+
throw parseError;
|
|
155
179
|
}
|
|
156
180
|
}
|
|
157
181
|
function serializeMqttPayload(payload) {
|
|
@@ -191,15 +215,7 @@ var OutMessageType = /* @__PURE__ */ ((OutMessageType2) => {
|
|
|
191
215
|
// src/api/MysaApiClient.ts
|
|
192
216
|
import { DescribeThingCommand, IoTClient } from "@aws-sdk/client-iot";
|
|
193
217
|
import { fromCognitoIdentityPool } from "@aws-sdk/credential-providers";
|
|
194
|
-
import {
|
|
195
|
-
AuthenticationDetails,
|
|
196
|
-
CognitoAccessToken,
|
|
197
|
-
CognitoIdToken,
|
|
198
|
-
CognitoRefreshToken,
|
|
199
|
-
CognitoUser,
|
|
200
|
-
CognitoUserPool,
|
|
201
|
-
CognitoUserSession
|
|
202
|
-
} from "amazon-cognito-identity-js";
|
|
218
|
+
import { AuthenticationDetails, CognitoUser, CognitoUserPool } from "amazon-cognito-identity-js";
|
|
203
219
|
import { auth, iot, mqtt } from "aws-iot-device-sdk-v2";
|
|
204
220
|
import { hash } from "crypto";
|
|
205
221
|
import dayjs from "dayjs";
|
|
@@ -217,10 +233,14 @@ var MysaApiBaseUrl = "https://app-prod.mysa.cloud";
|
|
|
217
233
|
var RealtimeKeepAliveInterval = dayjs.duration(5, "minutes");
|
|
218
234
|
var PublishAckTimeout = dayjs.duration(30, "seconds");
|
|
219
235
|
var MysaApiClient = class {
|
|
236
|
+
/** The credentials of the Mysa account this client authenticates as. */
|
|
237
|
+
_credentials;
|
|
220
238
|
/** The current session object, if any. */
|
|
221
239
|
_cognitoUserSession;
|
|
222
240
|
/** The current user object, if any. */
|
|
223
241
|
_cognitoUser;
|
|
242
|
+
/** The in-flight session acquisition, if any, so that concurrent callers share a single refresh or login. */
|
|
243
|
+
_freshSessionPromise;
|
|
224
244
|
/** The logger instance used by the client. */
|
|
225
245
|
_logger;
|
|
226
246
|
/** The fetcher function used by the client. */
|
|
@@ -245,89 +265,62 @@ var MysaApiClient = class {
|
|
|
245
265
|
* @see {@link MysaApiClientEventTypes} for the possible events and their payloads.
|
|
246
266
|
*/
|
|
247
267
|
emitter = new EventEmitter();
|
|
248
|
-
/**
|
|
249
|
-
* Gets the persistable session object.
|
|
250
|
-
*
|
|
251
|
-
* @returns The current persistable session object, if any.
|
|
252
|
-
*/
|
|
253
|
-
get session() {
|
|
254
|
-
if (!this._cognitoUserSession || !this._cognitoUser) {
|
|
255
|
-
return void 0;
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
username: this._cognitoUser.getUsername(),
|
|
259
|
-
idToken: this._cognitoUserSession.getIdToken().getJwtToken(),
|
|
260
|
-
accessToken: this._cognitoUserSession.getAccessToken().getJwtToken(),
|
|
261
|
-
refreshToken: this._cognitoUserSession.getRefreshToken().getToken()
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Returns whether the client currently has an active session.
|
|
266
|
-
*
|
|
267
|
-
* @returns True if the client has an active session, false otherwise.
|
|
268
|
-
*/
|
|
269
|
-
get isAuthenticated() {
|
|
270
|
-
return !!this.session;
|
|
271
|
-
}
|
|
272
268
|
/**
|
|
273
269
|
* Constructs a new instance of the MysaApiClient.
|
|
274
270
|
*
|
|
275
|
-
* @param
|
|
271
|
+
* @param credentials - The credentials of the Mysa account to authenticate as.
|
|
276
272
|
* @param options - The options for the client.
|
|
277
273
|
*/
|
|
278
|
-
constructor(
|
|
274
|
+
constructor(credentials, options) {
|
|
275
|
+
this._credentials = credentials;
|
|
279
276
|
this._logger = (options == null ? void 0 : options.logger) || new VoidLogger();
|
|
280
277
|
this._fetcher = (options == null ? void 0 : options.fetcher) || fetch;
|
|
281
|
-
if (session) {
|
|
282
|
-
this._cognitoUser = new CognitoUser({
|
|
283
|
-
Username: session.username,
|
|
284
|
-
Pool: new CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
285
|
-
});
|
|
286
|
-
this._cognitoUserSession = new CognitoUserSession({
|
|
287
|
-
IdToken: new CognitoIdToken({ IdToken: session.idToken }),
|
|
288
|
-
AccessToken: new CognitoAccessToken({ AccessToken: session.accessToken }),
|
|
289
|
-
RefreshToken: new CognitoRefreshToken({ RefreshToken: session.refreshToken })
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
278
|
}
|
|
293
279
|
/**
|
|
294
|
-
*
|
|
280
|
+
* Ensures the client has a usable session, logging in with the credentials it was constructed with if needed.
|
|
295
281
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
282
|
+
* Calling this method is optional: the client authenticates on demand before its first API call, and re-authenticates
|
|
283
|
+
* on its own whenever its session can no longer be refreshed. Call it explicitly at startup to fail fast on invalid
|
|
284
|
+
* credentials instead of on the first API call. It is a no-op when the current session is still usable.
|
|
298
285
|
*
|
|
299
286
|
* @example
|
|
300
287
|
*
|
|
301
288
|
* ```typescript
|
|
302
289
|
* try {
|
|
303
|
-
* await client.login(
|
|
290
|
+
* await client.login();
|
|
304
291
|
* console.log('Login successful!');
|
|
305
292
|
* } catch (error) {
|
|
306
293
|
* console.error('Login failed:', error.message);
|
|
307
294
|
* }
|
|
308
295
|
* ```
|
|
309
296
|
*
|
|
310
|
-
* @
|
|
311
|
-
|
|
297
|
+
* @throws {@link UnauthenticatedError} When authentication fails due to invalid credentials or network issues.
|
|
298
|
+
*/
|
|
299
|
+
async login() {
|
|
300
|
+
await this._getFreshSession();
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Authenticates with Mysa's Cognito user pool and replaces the current session, if any.
|
|
304
|
+
*
|
|
305
|
+
* @returns A promise that resolves to the newly established session.
|
|
312
306
|
* @throws {@link Error} When authentication fails due to invalid credentials or network issues.
|
|
313
307
|
*/
|
|
314
|
-
|
|
308
|
+
_login() {
|
|
315
309
|
this._cognitoUser = void 0;
|
|
316
310
|
this._cognitoUserSession = void 0;
|
|
317
311
|
this._mqttClientId = void 0;
|
|
318
312
|
this._mqttInterrupts = [];
|
|
319
|
-
|
|
313
|
+
const { username, password } = this._credentials;
|
|
320
314
|
return new Promise((resolve, reject) => {
|
|
321
315
|
const user = new CognitoUser({
|
|
322
|
-
Username:
|
|
316
|
+
Username: username,
|
|
323
317
|
Pool: new CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
|
|
324
318
|
});
|
|
325
|
-
user.authenticateUser(new AuthenticationDetails({ Username:
|
|
319
|
+
user.authenticateUser(new AuthenticationDetails({ Username: username, Password: password }), {
|
|
326
320
|
onSuccess: (session) => {
|
|
327
321
|
this._cognitoUser = user;
|
|
328
322
|
this._cognitoUserSession = session;
|
|
329
|
-
|
|
330
|
-
resolve();
|
|
323
|
+
resolve(session);
|
|
331
324
|
},
|
|
332
325
|
onFailure: (err) => {
|
|
333
326
|
reject(err);
|
|
@@ -352,7 +345,7 @@ var MysaApiClient = class {
|
|
|
352
345
|
*
|
|
353
346
|
* @returns A promise that resolves to the list of devices.
|
|
354
347
|
* @throws {@link MysaApiError} When the API request fails.
|
|
355
|
-
* @throws {@link UnauthenticatedError} When the
|
|
348
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
356
349
|
*/
|
|
357
350
|
async getDevices() {
|
|
358
351
|
this._logger.debug(`Fetching devices...`);
|
|
@@ -386,7 +379,7 @@ var MysaApiClient = class {
|
|
|
386
379
|
*
|
|
387
380
|
* @param deviceId - The ID of the device to get the serial number for.
|
|
388
381
|
* @returns A promise that resolves to the serial number, or undefined if not found.
|
|
389
|
-
* @throws {@link UnauthenticatedError} When the
|
|
382
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
390
383
|
*/
|
|
391
384
|
async getDeviceSerialNumber(deviceId) {
|
|
392
385
|
var _a;
|
|
@@ -424,7 +417,7 @@ var MysaApiClient = class {
|
|
|
424
417
|
*
|
|
425
418
|
* @returns A promise that resolves to the firmware information for all devices.
|
|
426
419
|
* @throws {@link MysaApiError} When the API request fails.
|
|
427
|
-
* @throws {@link UnauthenticatedError} When the
|
|
420
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
428
421
|
*/
|
|
429
422
|
async getDeviceFirmwares() {
|
|
430
423
|
this._logger.debug(`Fetching device firmwares...`);
|
|
@@ -444,7 +437,7 @@ var MysaApiClient = class {
|
|
|
444
437
|
*
|
|
445
438
|
* @returns A promise that resolves to the current state of all devices.
|
|
446
439
|
* @throws {@link MysaApiError} When the API request fails.
|
|
447
|
-
* @throws {@link UnauthenticatedError} When the
|
|
440
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
448
441
|
*/
|
|
449
442
|
async getDeviceStates() {
|
|
450
443
|
this._logger.debug(`Fetching device states...`);
|
|
@@ -464,7 +457,7 @@ var MysaApiClient = class {
|
|
|
464
457
|
*
|
|
465
458
|
* @returns A promise that resolves to the homes information.
|
|
466
459
|
* @throws {@link MysaApiError} When the API request fails.
|
|
467
|
-
* @throws {@link UnauthenticatedError} When the
|
|
460
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
468
461
|
*/
|
|
469
462
|
async getHomes() {
|
|
470
463
|
this._logger.debug(`Fetching homes...`);
|
|
@@ -506,16 +499,20 @@ var MysaApiClient = class {
|
|
|
506
499
|
* @param mode - The operating mode to set (one of MysaDeviceMode values, or undefined to leave unchanged).
|
|
507
500
|
* @param fanSpeed - The fan speed mode to set ('low', 'medium', 'high', 'max', 'auto', or undefined to leave
|
|
508
501
|
* unchanged).
|
|
509
|
-
* @throws {@link UnauthenticatedError} When the
|
|
502
|
+
* @throws {@link UnauthenticatedError} When the client cannot authenticate with its credentials.
|
|
503
|
+
* @throws {@link UnknownDeviceError} When the device id does not match any device on the account.
|
|
510
504
|
* @throws {@link Error} When MQTT connection or command sending fails.
|
|
511
505
|
*/
|
|
512
506
|
async setDeviceState(deviceId, setPoint, mode, fanSpeed) {
|
|
513
|
-
var _a;
|
|
514
507
|
this._logger.debug(`Setting device state for '${deviceId}'`);
|
|
515
508
|
if (!this._cachedDevices) {
|
|
516
509
|
this._cachedDevices = await this.getDevices();
|
|
517
510
|
}
|
|
511
|
+
if (!Object.prototype.hasOwnProperty.call(this._cachedDevices.DevicesObj, deviceId)) {
|
|
512
|
+
throw new UnknownDeviceError(deviceId);
|
|
513
|
+
}
|
|
518
514
|
const device = this._cachedDevices.DevicesObj[deviceId];
|
|
515
|
+
await this._getFreshSession();
|
|
519
516
|
this._logger.debug(`Initializing MQTT connection...`);
|
|
520
517
|
const mqttConnection = await this._getMqttConnection();
|
|
521
518
|
const now = dayjs();
|
|
@@ -528,7 +525,7 @@ var MysaApiClient = class {
|
|
|
528
525
|
time: now.unix(),
|
|
529
526
|
ver: "1.0",
|
|
530
527
|
src: {
|
|
531
|
-
ref:
|
|
528
|
+
ref: this._cognitoUser.getUsername(),
|
|
532
529
|
type: 100
|
|
533
530
|
},
|
|
534
531
|
dest: {
|
|
@@ -646,33 +643,58 @@ var MysaApiClient = class {
|
|
|
646
643
|
* Ensures a valid, non-expired session is available.
|
|
647
644
|
*
|
|
648
645
|
* This method checks if the current session is valid and not expired. If the session is expired, it automatically
|
|
649
|
-
* refreshes it using the refresh token.
|
|
646
|
+
* refreshes it using the refresh token. If there is no session yet, or if the refresh token has itself expired or
|
|
647
|
+
* been revoked, the client logs back in with its credentials.
|
|
648
|
+
*
|
|
649
|
+
* Concurrent callers share a single in-flight acquisition, so a burst of API calls never triggers more than one
|
|
650
|
+
* refresh or login.
|
|
650
651
|
*
|
|
651
652
|
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
652
|
-
* @throws {@link UnauthenticatedError} When
|
|
653
|
+
* @throws {@link UnauthenticatedError} When neither refreshing nor logging back in succeeds.
|
|
653
654
|
*/
|
|
654
655
|
async _getFreshSession() {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
658
|
-
if (this._cognitoUserSession.isValid() && dayjs.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
656
|
+
var _a;
|
|
657
|
+
if (this._cognitoUser && ((_a = this._cognitoUserSession) == null ? void 0 : _a.isValid()) && dayjs.unix(this._cognitoUserSession.getIdToken().getExpiration()).isAfter()) {
|
|
659
658
|
this._logger.debug("Session is valid, no need to refresh");
|
|
660
|
-
return
|
|
659
|
+
return this._cognitoUserSession;
|
|
661
660
|
}
|
|
662
|
-
this.
|
|
663
|
-
|
|
664
|
-
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
665
|
-
if (error) {
|
|
666
|
-
this._logger.error("Failed to refresh session:", error);
|
|
667
|
-
reject(new UnauthenticatedError("Unable to refresh the authentication session."));
|
|
668
|
-
} else {
|
|
669
|
-
this._logger.debug("Session refreshed successfully");
|
|
670
|
-
this._cognitoUserSession = session;
|
|
671
|
-
this.emitter.emit("sessionChanged", this.session);
|
|
672
|
-
resolve(session);
|
|
673
|
-
}
|
|
674
|
-
});
|
|
661
|
+
this._freshSessionPromise ??= this._acquireFreshSession().finally(() => {
|
|
662
|
+
this._freshSessionPromise = void 0;
|
|
675
663
|
});
|
|
664
|
+
return this._freshSessionPromise;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Refreshes the current session, falling back to a full login when it cannot be refreshed.
|
|
668
|
+
*
|
|
669
|
+
* @returns A promise that resolves to a valid CognitoUserSession.
|
|
670
|
+
* @throws {@link UnauthenticatedError} When logging back in fails.
|
|
671
|
+
*/
|
|
672
|
+
async _acquireFreshSession() {
|
|
673
|
+
if (this._cognitoUser && this._cognitoUserSession) {
|
|
674
|
+
this._logger.debug("Session is not valid or expired, refreshing...");
|
|
675
|
+
try {
|
|
676
|
+
return await new Promise((resolve, reject) => {
|
|
677
|
+
this._cognitoUser.refreshSession(this._cognitoUserSession.getRefreshToken(), (error, session) => {
|
|
678
|
+
if (error) {
|
|
679
|
+
reject(error);
|
|
680
|
+
} else {
|
|
681
|
+
this._logger.debug("Session refreshed successfully");
|
|
682
|
+
this._cognitoUserSession = session;
|
|
683
|
+
resolve(session);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
} catch (error) {
|
|
688
|
+
this._logger.warn("Failed to refresh session, logging back in:", error);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
this._logger.info("Logging in...");
|
|
693
|
+
return await this._login();
|
|
694
|
+
} catch (error) {
|
|
695
|
+
this._logger.error("Failed to log in:", error);
|
|
696
|
+
throw new UnauthenticatedError("Unable to establish an authentication session.", error);
|
|
697
|
+
}
|
|
676
698
|
}
|
|
677
699
|
/**
|
|
678
700
|
* Establishes and returns an MQTT connection for real-time communication.
|
|
@@ -814,7 +836,7 @@ var MysaApiClient = class {
|
|
|
814
836
|
throw new Error("MQTT credentials are already expired.");
|
|
815
837
|
}
|
|
816
838
|
if (!this._mqttClientId) {
|
|
817
|
-
const username = ((_a = this.
|
|
839
|
+
const username = ((_a = this._cognitoUser) == null ? void 0 : _a.getUsername()) ?? "anon";
|
|
818
840
|
const usernameHash = hash("sha1", username);
|
|
819
841
|
this._mqttClientId = `mysa-js-sdk-${usernameHash}-${process.pid}-${getRandomClientId()}`;
|
|
820
842
|
}
|
|
@@ -1026,6 +1048,7 @@ export {
|
|
|
1026
1048
|
MysaApiError,
|
|
1027
1049
|
OutMessageType,
|
|
1028
1050
|
UnauthenticatedError,
|
|
1051
|
+
UnknownDeviceError,
|
|
1029
1052
|
VoidLogger
|
|
1030
1053
|
};
|
|
1031
1054
|
//# sourceMappingURL=index.js.map
|