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/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
- console.error("Error parsing MQTT payload:", error);
154
- throw new Error("Failed to parse MQTT payload");
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 session - The persistable session object, if any.
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(session, options) {
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
- * Logs in the user with the given email address and password.
280
+ * Ensures the client has a usable session, logging in with the credentials it was constructed with if needed.
295
281
  *
296
- * This method authenticates the user with Mysa's Cognito user pool and establishes a session that can be used for
297
- * subsequent API calls. Upon successful login, a 'sessionChanged' event is emitted.
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('user@example.com', 'password123');
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
- * @param emailAddress - The email address of the user.
311
- * @param password - The password of the user.
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
- async login(emailAddress, password) {
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
- this.emitter.emit("sessionChanged", this.session);
313
+ const { username, password } = this._credentials;
320
314
  return new Promise((resolve, reject) => {
321
315
  const user = new CognitoUser({
322
- Username: emailAddress,
316
+ Username: username,
323
317
  Pool: new CognitoUserPool({ UserPoolId: CognitoUserPoolId, ClientId: CognitoClientId })
324
318
  });
325
- user.authenticateUser(new AuthenticationDetails({ Username: emailAddress, Password: password }), {
319
+ user.authenticateUser(new AuthenticationDetails({ Username: username, Password: password }), {
326
320
  onSuccess: (session) => {
327
321
  this._cognitoUser = user;
328
322
  this._cognitoUserSession = session;
329
- this.emitter.emit("sessionChanged", this.session);
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 user is not authenticated.
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 user is not authenticated.
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 user is not authenticated.
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 user is not authenticated.
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 user is not authenticated.
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 user is not authenticated.
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: ((_a = this.session) == null ? void 0 : _a.username) ?? "",
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 no session exists or refresh fails.
653
+ * @throws {@link UnauthenticatedError} When neither refreshing nor logging back in succeeds.
653
654
  */
654
655
  async _getFreshSession() {
655
- if (!this._cognitoUser || !this._cognitoUserSession) {
656
- throw new UnauthenticatedError("An attempt was made to access a resource without a valid session.");
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 Promise.resolve(this._cognitoUserSession);
659
+ return this._cognitoUserSession;
661
660
  }
662
- this._logger.debug("Session is not valid or expired, refreshing...");
663
- return new Promise((resolve, reject) => {
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.session) == null ? void 0 : _a.username) ?? "anon";
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