@openfin/cloud-interop-core-api 0.0.1-alpha.03770df → 0.0.1-alpha.03879c9

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.
Files changed (4) hide show
  1. package/bundle.d.ts +13 -12
  2. package/index.cjs +121 -24
  3. package/index.mjs +121 -24
  4. package/package.json +5 -8
package/bundle.d.ts CHANGED
@@ -245,36 +245,36 @@ export declare class CloudInteropAPI {
245
245
  /**
246
246
  * Connects and creates a session on the Cloud Interop service
247
247
  *
248
- * @param {ConnectParameters} parameters - The parameters to use to connect
249
- * @returns {*} {Promise<void>}
248
+ * @param parameters - The parameters to use to connect
249
+ * @returns Promise that resolves when connection is established
250
250
  * @memberof CloudInteropAPI
251
- * @throws {CloudInteropAPIError} - If an error occurs during connection
252
- * @throws {AuthorizationError} - If the connection is unauthorized
251
+ * @throws CloudInteropAPIError - If an error occurs during connection
252
+ * @throws AuthorizationError - If the connection is unauthorized
253
253
  */
254
254
  connect(parameters: ConnectParameters): Promise<void>;
255
255
  /**
256
256
  * Disconnects from the Cloud Interop service
257
257
  *
258
- * @returns {*} {Promise<void>}
258
+ * @returns Promise that resolves when disconnected
259
259
  * @memberof CloudInteropAPI
260
- * @throws {CloudInteropAPIError} - If an error occurs during disconnection
260
+ * @throws CloudInteropAPIError - If an error occurs during disconnection
261
261
  */
262
262
  disconnect(): Promise<void>;
263
263
  /**
264
264
  * Publishes a new context for the given context group to the other connected sessions
265
265
  *
266
- * @param {string} contextGroup - The context group to publish to
267
- * @param {object} context - The context to publish
268
- * @returns {*} {Promise<void>}
266
+ * @param contextGroup - The context group to publish to
267
+ * @param context - The context to publish
268
+ * @returns Promise that resolves when context is published
269
269
  * @memberof CloudInteropAPI
270
270
  */
271
271
  setContext(contextGroup: string, context: InferredContext): Promise<void>;
272
272
  /**
273
273
  * Starts an intent discovery operation
274
274
  *
275
- * @returns {*} {Promise<void>}
275
+ * @returns Promise that resolves when intent discovery is started
276
276
  * @memberof CloudInteropAPI
277
- * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
277
+ * @throws CloudInteropAPIError - If an error occurs during intent discovery
278
278
  */
279
279
  startIntentDiscovery(options: StartIntentDiscoveryOptions): Promise<void>;
280
280
  raiseIntent(options: RaiseIntentAPIOptions): Promise<void>;
@@ -364,7 +364,7 @@ export declare type ConnectParameters = {
364
364
  /**
365
365
  * When JWT authentication is being used, this will be invoked just whenever a JWT token is required for a request
366
366
  */
367
- jwtRequestCallback: () => string | object;
367
+ jwtRequestCallback: () => string | object | Promise<string | object>;
368
368
  /**
369
369
  * The id of the service gateway JWT authentication definition to use
370
370
  *
@@ -418,6 +418,7 @@ export declare type CreateSessionResponse = {
418
418
  sub: string;
419
419
  platformId: string;
420
420
  sourceId: string;
421
+ localSessionExpiryHandling?: boolean;
421
422
  };
422
423
 
423
424
  declare const errorSchema: z.ZodObject<{
package/index.cjs CHANGED
@@ -1,8 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  var buffer = require('buffer');
4
+ require('fs');
5
+ require('path');
4
6
  var mqtt = require('mqtt');
5
7
 
8
+ var u=n=>{let e=n.replaceAll("-","+").replaceAll("_","/");return e.padEnd(e.length+(4-e.length%4)%4,"=")};
9
+
6
10
  class CloudInteropAPIError extends Error {
7
11
  code;
8
12
  constructor(message = 'An unexpected error has occurred', code = 'UNEXPECTED_ERROR', cause) {
@@ -49,11 +53,11 @@ class EventController {
49
53
  const isErrorIntentResult = (result) => 'error' in result;
50
54
 
51
55
  const APP_ID_DELIM = '::';
52
- const getRequestHeaders = (connectionParameters) => {
56
+ const getRequestHeaders = async (connectionParameters) => {
53
57
  const headers = {};
54
58
  headers['Content-Type'] = 'application/json';
55
59
  if (connectionParameters.authenticationType === 'jwt' && connectionParameters.jwtAuthenticationParameters) {
56
- const tokenResult = connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
60
+ const tokenResult = await connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
57
61
  if (!tokenResult) {
58
62
  throw new Error('jwtRequestCallback must return a token');
59
63
  }
@@ -158,7 +162,7 @@ class IntentController {
158
162
  try {
159
163
  const startResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}`, {
160
164
  method: 'POST',
161
- headers: getRequestHeaders(this.#connectionParams),
165
+ headers: await getRequestHeaders(this.#connectionParams),
162
166
  body: JSON.stringify({ findOptions }),
163
167
  });
164
168
  if (!startResponse.ok) {
@@ -203,7 +207,7 @@ class IntentController {
203
207
  }
204
208
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
205
209
  method: 'DELETE',
206
- headers: getRequestHeaders(this.#connectionParams),
210
+ headers: await getRequestHeaders(this.#connectionParams),
207
211
  })
208
212
  .then((deleteResponse) => {
209
213
  if (!deleteResponse.ok) {
@@ -224,7 +228,7 @@ class IntentController {
224
228
  }
225
229
  const postResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/sessions/${targetSessionId}`, {
226
230
  method: 'POST',
227
- headers: getRequestHeaders(this.#connectionParams),
231
+ headers: await getRequestHeaders(this.#connectionParams),
228
232
  body: JSON.stringify({ raiseOptions }),
229
233
  });
230
234
  if (!postResponse.ok) {
@@ -238,7 +242,7 @@ class IntentController {
238
242
  try {
239
243
  const reportResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${discoveryId}`, {
240
244
  method: 'POST',
241
- headers: getRequestHeaders(this.#connectionParams),
245
+ headers: await getRequestHeaders(this.#connectionParams),
242
246
  body: JSON.stringify({ intents }),
243
247
  });
244
248
  if (reportResponse.ok) {
@@ -262,7 +266,7 @@ class IntentController {
262
266
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
263
267
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
264
268
  method: 'POST',
265
- headers: getRequestHeaders(this.#connectionParams),
269
+ headers: await getRequestHeaders(this.#connectionParams),
266
270
  body: JSON.stringify({ result }),
267
271
  });
268
272
  if (!resultResponse.ok) {
@@ -346,6 +350,7 @@ class CloudInteropAPI {
346
350
  #reconnectRetryLimit = 30;
347
351
  #keepAliveIntervalSeconds = 30;
348
352
  #logger = (level, message) => {
353
+ // eslint-disable-next-line security/detect-object-injection -- level is restricted to the LogLevel union
349
354
  console[level](message);
350
355
  };
351
356
  #reconnectRetries = 0;
@@ -353,6 +358,7 @@ class CloudInteropAPI {
353
358
  #attemptingToReconnect = false;
354
359
  #events = new EventController();
355
360
  #intents;
361
+ #sessionTimer;
356
362
  constructor(cloudInteropSettings) {
357
363
  this.#cloudInteropSettings = cloudInteropSettings;
358
364
  }
@@ -365,11 +371,11 @@ class CloudInteropAPI {
365
371
  /**
366
372
  * Connects and creates a session on the Cloud Interop service
367
373
  *
368
- * @param {ConnectParameters} parameters - The parameters to use to connect
369
- * @returns {*} {Promise<void>}
374
+ * @param parameters - The parameters to use to connect
375
+ * @returns Promise that resolves when connection is established
370
376
  * @memberof CloudInteropAPI
371
- * @throws {CloudInteropAPIError} - If an error occurs during connection
372
- * @throws {AuthorizationError} - If the connection is unauthorized
377
+ * @throws CloudInteropAPIError - If an error occurs during connection
378
+ * @throws AuthorizationError - If the connection is unauthorized
373
379
  */
374
380
  async connect(parameters) {
375
381
  this.#validateConnectParams(parameters);
@@ -380,7 +386,7 @@ class CloudInteropAPI {
380
386
  const { sourceId, platformId } = this.#connectionParams;
381
387
  const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
382
388
  method: 'POST',
383
- headers: getRequestHeaders(this.#connectionParams),
389
+ headers: await getRequestHeaders(this.#connectionParams),
384
390
  body: JSON.stringify({ sourceId: sourceId.trim(), platformId }),
385
391
  });
386
392
  if (!createSessionResponse.ok) {
@@ -393,6 +399,11 @@ class CloudInteropAPI {
393
399
  throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
394
400
  }
395
401
  this.#sessionDetails = (await createSessionResponse.json());
402
+ // If local session expiry handling is enabled, start the session timer
403
+ if (this.#sessionDetails.localSessionExpiryHandling) {
404
+ this.#logger('debug', `Local session expiry handling is enabled`);
405
+ this.#startSessionTimer();
406
+ }
396
407
  const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
397
408
  const clientOptions = {
398
409
  keepalive: this.#keepAliveIntervalSeconds,
@@ -413,6 +424,7 @@ class CloudInteropAPI {
413
424
  // TODO: Dynamic intent discovery
414
425
  // search for any ongoing discoveries in DB and fire report-intents on self
415
426
  this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
427
+ // eslint-disable-next-line security-node/detect-unhandled-event-errors -- this callback handles MQTT error events
416
428
  this.#mqttClient.on('error', async (error) => {
417
429
  // We will receive errors for each failed reconnection attempt
418
430
  // We don't want to disconnect on these else we will never reconnect
@@ -422,9 +434,7 @@ class CloudInteropAPI {
422
434
  if (error instanceof mqtt.ErrorWithReasonCode) {
423
435
  switch (error.code) {
424
436
  case BadUserNamePasswordError: {
425
- await this.#disconnect(false);
426
- this.#logger('warn', `Session expired`);
427
- this.#events.emitEvent('session-expired');
437
+ this.#handleSessionExpiry();
428
438
  return;
429
439
  }
430
440
  default: {
@@ -478,9 +488,9 @@ class CloudInteropAPI {
478
488
  /**
479
489
  * Disconnects from the Cloud Interop service
480
490
  *
481
- * @returns {*} {Promise<void>}
491
+ * @returns Promise that resolves when disconnected
482
492
  * @memberof CloudInteropAPI
483
- * @throws {CloudInteropAPIError} - If an error occurs during disconnection
493
+ * @throws CloudInteropAPIError - If an error occurs during disconnection
484
494
  */
485
495
  async disconnect() {
486
496
  await this.#disconnect(true);
@@ -488,9 +498,9 @@ class CloudInteropAPI {
488
498
  /**
489
499
  * Publishes a new context for the given context group to the other connected sessions
490
500
  *
491
- * @param {string} contextGroup - The context group to publish to
492
- * @param {object} context - The context to publish
493
- * @returns {*} {Promise<void>}
501
+ * @param contextGroup - The context group to publish to
502
+ * @param context - The context to publish
503
+ * @returns Promise that resolves when context is published
494
504
  * @memberof CloudInteropAPI
495
505
  */
496
506
  async setContext(contextGroup, context) {
@@ -507,7 +517,7 @@ class CloudInteropAPI {
507
517
  };
508
518
  const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
509
519
  method: 'POST',
510
- headers: getRequestHeaders(this.#connectionParams),
520
+ headers: await getRequestHeaders(this.#connectionParams),
511
521
  body: JSON.stringify(payload),
512
522
  });
513
523
  if (!postResponse.ok) {
@@ -517,9 +527,9 @@ class CloudInteropAPI {
517
527
  /**
518
528
  * Starts an intent discovery operation
519
529
  *
520
- * @returns {*} {Promise<void>}
530
+ * @returns Promise that resolves when intent discovery is started
521
531
  * @memberof CloudInteropAPI
522
- * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
532
+ * @throws CloudInteropAPIError - If an error occurs during intent discovery
523
533
  */
524
534
  async startIntentDiscovery(options) {
525
535
  this.#throwIfNotConnected();
@@ -560,9 +570,14 @@ class CloudInteropAPI {
560
570
  if (!this.#connectionParams) {
561
571
  throw new Error('Connect parameters must be provided');
562
572
  }
573
+ // Cancel session timer if it's running
574
+ if (this.#sessionTimer) {
575
+ clearTimeout(this.#sessionTimer);
576
+ this.#sessionTimer = undefined;
577
+ }
563
578
  const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
564
579
  method: 'DELETE',
565
- headers: getRequestHeaders(this.#connectionParams),
580
+ headers: await getRequestHeaders(this.#connectionParams),
566
581
  });
567
582
  if (disconnectResponse.status !== 200) {
568
583
  throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
@@ -658,6 +673,88 @@ class CloudInteropAPI {
658
673
  throw new Error('MQTT client not connected');
659
674
  }
660
675
  }
676
+ /**
677
+ * Extracts the expiration timestamp from a JWT token.
678
+ *
679
+ * @param token - The JWT token string
680
+ * @returns The expiration timestamp in seconds, or null if extraction fails
681
+ */
682
+ #extractExpirationFromJwt(token) {
683
+ try {
684
+ // JWT tokens have three parts separated by dots: header.payload.signature
685
+ // The exp claim is in the payload
686
+ const parts = token.split('.');
687
+ if (parts.length < 2) {
688
+ this.#logger('warn', 'Invalid JWT token format: expected at least 2 parts');
689
+ return null;
690
+ }
691
+ const payload = parts[1];
692
+ // Decode base64url encoded payload
693
+ const decodedBytes = buffer.Buffer.from(u(payload), 'base64');
694
+ const payloadJson = decodedBytes.toString('utf8');
695
+ // Parse JSON to get the exp claim
696
+ const claims = JSON.parse(payloadJson);
697
+ const exp = claims.exp;
698
+ if (exp === undefined || exp === null) {
699
+ this.#logger('warn', "JWT token does not contain 'exp' claim");
700
+ return null;
701
+ }
702
+ if (typeof exp !== 'number') {
703
+ this.#logger('warn', `JWT token 'exp' claim is not a number: ${exp}`);
704
+ return null;
705
+ }
706
+ return exp;
707
+ }
708
+ catch (error) {
709
+ this.#logger('error', `Failed to extract expiration from JWT token: ${error instanceof Error ? error.message : error}`);
710
+ return null;
711
+ }
712
+ }
713
+ /**
714
+ * Start a session timer that will expire at the time specified in the JWT token's exp claim.
715
+ * When the timer fires, it executes the same actions as the BadUserNamePasswordError case.
716
+ */
717
+ #startSessionTimer() {
718
+ if (!this.#sessionDetails?.localSessionExpiryHandling) {
719
+ return;
720
+ }
721
+ const token = this.#sessionDetails.token;
722
+ if (!token) {
723
+ this.#logger('warn', 'Cannot start session timer: token not available');
724
+ return;
725
+ }
726
+ // Extract expiration time from JWT token
727
+ const expTimestamp = this.#extractExpirationFromJwt(token);
728
+ if (expTimestamp === null) {
729
+ this.#logger('warn', 'Cannot start session timer: could not extract expiration from JWT token');
730
+ return;
731
+ }
732
+ const currentTimeSeconds = Math.floor(Date.now() / 1000);
733
+ const delaySeconds = expTimestamp - currentTimeSeconds;
734
+ if (delaySeconds <= 0) {
735
+ this.#logger('warn', 'JWT token has already expired or expires immediately');
736
+ // Execute the same actions as BadUserNamePasswordError case
737
+ this.#handleSessionExpiry();
738
+ return;
739
+ }
740
+ // Clear any existing timer
741
+ if (this.#sessionTimer) {
742
+ clearTimeout(this.#sessionTimer);
743
+ }
744
+ const expirationTimeString = new Date(expTimestamp * 1000).toISOString();
745
+ this.#logger('debug', `Starting session timer to expire in ${delaySeconds} seconds (at ${expirationTimeString})`);
746
+ this.#sessionTimer = setTimeout(async () => {
747
+ this.#handleSessionExpiry();
748
+ }, delaySeconds * 1000);
749
+ }
750
+ /**
751
+ * Handles session expiry by executing the same actions as the BadUserNamePasswordError case.
752
+ */
753
+ async #handleSessionExpiry() {
754
+ await this.#disconnect(false);
755
+ this.#logger('warn', 'Session expired');
756
+ this.#events.emitEvent('session-expired');
757
+ }
661
758
  }
662
759
 
663
760
  exports.AuthorizationError = AuthorizationError;
package/index.mjs CHANGED
@@ -1,6 +1,10 @@
1
1
  import { Buffer } from 'buffer';
2
+ import 'fs';
3
+ import 'path';
2
4
  import mqtt from 'mqtt';
3
5
 
6
+ var u=n=>{let e=n.replaceAll("-","+").replaceAll("_","/");return e.padEnd(e.length+(4-e.length%4)%4,"=")};
7
+
4
8
  class CloudInteropAPIError extends Error {
5
9
  code;
6
10
  constructor(message = 'An unexpected error has occurred', code = 'UNEXPECTED_ERROR', cause) {
@@ -47,11 +51,11 @@ class EventController {
47
51
  const isErrorIntentResult = (result) => 'error' in result;
48
52
 
49
53
  const APP_ID_DELIM = '::';
50
- const getRequestHeaders = (connectionParameters) => {
54
+ const getRequestHeaders = async (connectionParameters) => {
51
55
  const headers = {};
52
56
  headers['Content-Type'] = 'application/json';
53
57
  if (connectionParameters.authenticationType === 'jwt' && connectionParameters.jwtAuthenticationParameters) {
54
- const tokenResult = connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
58
+ const tokenResult = await connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
55
59
  if (!tokenResult) {
56
60
  throw new Error('jwtRequestCallback must return a token');
57
61
  }
@@ -156,7 +160,7 @@ class IntentController {
156
160
  try {
157
161
  const startResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}`, {
158
162
  method: 'POST',
159
- headers: getRequestHeaders(this.#connectionParams),
163
+ headers: await getRequestHeaders(this.#connectionParams),
160
164
  body: JSON.stringify({ findOptions }),
161
165
  });
162
166
  if (!startResponse.ok) {
@@ -201,7 +205,7 @@ class IntentController {
201
205
  }
202
206
  await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
203
207
  method: 'DELETE',
204
- headers: getRequestHeaders(this.#connectionParams),
208
+ headers: await getRequestHeaders(this.#connectionParams),
205
209
  })
206
210
  .then((deleteResponse) => {
207
211
  if (!deleteResponse.ok) {
@@ -222,7 +226,7 @@ class IntentController {
222
226
  }
223
227
  const postResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/sessions/${targetSessionId}`, {
224
228
  method: 'POST',
225
- headers: getRequestHeaders(this.#connectionParams),
229
+ headers: await getRequestHeaders(this.#connectionParams),
226
230
  body: JSON.stringify({ raiseOptions }),
227
231
  });
228
232
  if (!postResponse.ok) {
@@ -236,7 +240,7 @@ class IntentController {
236
240
  try {
237
241
  const reportResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${discoveryId}`, {
238
242
  method: 'POST',
239
- headers: getRequestHeaders(this.#connectionParams),
243
+ headers: await getRequestHeaders(this.#connectionParams),
240
244
  body: JSON.stringify({ intents }),
241
245
  });
242
246
  if (reportResponse.ok) {
@@ -260,7 +264,7 @@ class IntentController {
260
264
  const { sessionId } = getSourceFromSession(this.#sessionDetails);
261
265
  const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
262
266
  method: 'POST',
263
- headers: getRequestHeaders(this.#connectionParams),
267
+ headers: await getRequestHeaders(this.#connectionParams),
264
268
  body: JSON.stringify({ result }),
265
269
  });
266
270
  if (!resultResponse.ok) {
@@ -344,6 +348,7 @@ class CloudInteropAPI {
344
348
  #reconnectRetryLimit = 30;
345
349
  #keepAliveIntervalSeconds = 30;
346
350
  #logger = (level, message) => {
351
+ // eslint-disable-next-line security/detect-object-injection -- level is restricted to the LogLevel union
347
352
  console[level](message);
348
353
  };
349
354
  #reconnectRetries = 0;
@@ -351,6 +356,7 @@ class CloudInteropAPI {
351
356
  #attemptingToReconnect = false;
352
357
  #events = new EventController();
353
358
  #intents;
359
+ #sessionTimer;
354
360
  constructor(cloudInteropSettings) {
355
361
  this.#cloudInteropSettings = cloudInteropSettings;
356
362
  }
@@ -363,11 +369,11 @@ class CloudInteropAPI {
363
369
  /**
364
370
  * Connects and creates a session on the Cloud Interop service
365
371
  *
366
- * @param {ConnectParameters} parameters - The parameters to use to connect
367
- * @returns {*} {Promise<void>}
372
+ * @param parameters - The parameters to use to connect
373
+ * @returns Promise that resolves when connection is established
368
374
  * @memberof CloudInteropAPI
369
- * @throws {CloudInteropAPIError} - If an error occurs during connection
370
- * @throws {AuthorizationError} - If the connection is unauthorized
375
+ * @throws CloudInteropAPIError - If an error occurs during connection
376
+ * @throws AuthorizationError - If the connection is unauthorized
371
377
  */
372
378
  async connect(parameters) {
373
379
  this.#validateConnectParams(parameters);
@@ -378,7 +384,7 @@ class CloudInteropAPI {
378
384
  const { sourceId, platformId } = this.#connectionParams;
379
385
  const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
380
386
  method: 'POST',
381
- headers: getRequestHeaders(this.#connectionParams),
387
+ headers: await getRequestHeaders(this.#connectionParams),
382
388
  body: JSON.stringify({ sourceId: sourceId.trim(), platformId }),
383
389
  });
384
390
  if (!createSessionResponse.ok) {
@@ -391,6 +397,11 @@ class CloudInteropAPI {
391
397
  throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
392
398
  }
393
399
  this.#sessionDetails = (await createSessionResponse.json());
400
+ // If local session expiry handling is enabled, start the session timer
401
+ if (this.#sessionDetails.localSessionExpiryHandling) {
402
+ this.#logger('debug', `Local session expiry handling is enabled`);
403
+ this.#startSessionTimer();
404
+ }
394
405
  const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
395
406
  const clientOptions = {
396
407
  keepalive: this.#keepAliveIntervalSeconds,
@@ -411,6 +422,7 @@ class CloudInteropAPI {
411
422
  // TODO: Dynamic intent discovery
412
423
  // search for any ongoing discoveries in DB and fire report-intents on self
413
424
  this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
425
+ // eslint-disable-next-line security-node/detect-unhandled-event-errors -- this callback handles MQTT error events
414
426
  this.#mqttClient.on('error', async (error) => {
415
427
  // We will receive errors for each failed reconnection attempt
416
428
  // We don't want to disconnect on these else we will never reconnect
@@ -420,9 +432,7 @@ class CloudInteropAPI {
420
432
  if (error instanceof mqtt.ErrorWithReasonCode) {
421
433
  switch (error.code) {
422
434
  case BadUserNamePasswordError: {
423
- await this.#disconnect(false);
424
- this.#logger('warn', `Session expired`);
425
- this.#events.emitEvent('session-expired');
435
+ this.#handleSessionExpiry();
426
436
  return;
427
437
  }
428
438
  default: {
@@ -476,9 +486,9 @@ class CloudInteropAPI {
476
486
  /**
477
487
  * Disconnects from the Cloud Interop service
478
488
  *
479
- * @returns {*} {Promise<void>}
489
+ * @returns Promise that resolves when disconnected
480
490
  * @memberof CloudInteropAPI
481
- * @throws {CloudInteropAPIError} - If an error occurs during disconnection
491
+ * @throws CloudInteropAPIError - If an error occurs during disconnection
482
492
  */
483
493
  async disconnect() {
484
494
  await this.#disconnect(true);
@@ -486,9 +496,9 @@ class CloudInteropAPI {
486
496
  /**
487
497
  * Publishes a new context for the given context group to the other connected sessions
488
498
  *
489
- * @param {string} contextGroup - The context group to publish to
490
- * @param {object} context - The context to publish
491
- * @returns {*} {Promise<void>}
499
+ * @param contextGroup - The context group to publish to
500
+ * @param context - The context to publish
501
+ * @returns Promise that resolves when context is published
492
502
  * @memberof CloudInteropAPI
493
503
  */
494
504
  async setContext(contextGroup, context) {
@@ -505,7 +515,7 @@ class CloudInteropAPI {
505
515
  };
506
516
  const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
507
517
  method: 'POST',
508
- headers: getRequestHeaders(this.#connectionParams),
518
+ headers: await getRequestHeaders(this.#connectionParams),
509
519
  body: JSON.stringify(payload),
510
520
  });
511
521
  if (!postResponse.ok) {
@@ -515,9 +525,9 @@ class CloudInteropAPI {
515
525
  /**
516
526
  * Starts an intent discovery operation
517
527
  *
518
- * @returns {*} {Promise<void>}
528
+ * @returns Promise that resolves when intent discovery is started
519
529
  * @memberof CloudInteropAPI
520
- * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
530
+ * @throws CloudInteropAPIError - If an error occurs during intent discovery
521
531
  */
522
532
  async startIntentDiscovery(options) {
523
533
  this.#throwIfNotConnected();
@@ -558,9 +568,14 @@ class CloudInteropAPI {
558
568
  if (!this.#connectionParams) {
559
569
  throw new Error('Connect parameters must be provided');
560
570
  }
571
+ // Cancel session timer if it's running
572
+ if (this.#sessionTimer) {
573
+ clearTimeout(this.#sessionTimer);
574
+ this.#sessionTimer = undefined;
575
+ }
561
576
  const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
562
577
  method: 'DELETE',
563
- headers: getRequestHeaders(this.#connectionParams),
578
+ headers: await getRequestHeaders(this.#connectionParams),
564
579
  });
565
580
  if (disconnectResponse.status !== 200) {
566
581
  throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
@@ -656,6 +671,88 @@ class CloudInteropAPI {
656
671
  throw new Error('MQTT client not connected');
657
672
  }
658
673
  }
674
+ /**
675
+ * Extracts the expiration timestamp from a JWT token.
676
+ *
677
+ * @param token - The JWT token string
678
+ * @returns The expiration timestamp in seconds, or null if extraction fails
679
+ */
680
+ #extractExpirationFromJwt(token) {
681
+ try {
682
+ // JWT tokens have three parts separated by dots: header.payload.signature
683
+ // The exp claim is in the payload
684
+ const parts = token.split('.');
685
+ if (parts.length < 2) {
686
+ this.#logger('warn', 'Invalid JWT token format: expected at least 2 parts');
687
+ return null;
688
+ }
689
+ const payload = parts[1];
690
+ // Decode base64url encoded payload
691
+ const decodedBytes = Buffer.from(u(payload), 'base64');
692
+ const payloadJson = decodedBytes.toString('utf8');
693
+ // Parse JSON to get the exp claim
694
+ const claims = JSON.parse(payloadJson);
695
+ const exp = claims.exp;
696
+ if (exp === undefined || exp === null) {
697
+ this.#logger('warn', "JWT token does not contain 'exp' claim");
698
+ return null;
699
+ }
700
+ if (typeof exp !== 'number') {
701
+ this.#logger('warn', `JWT token 'exp' claim is not a number: ${exp}`);
702
+ return null;
703
+ }
704
+ return exp;
705
+ }
706
+ catch (error) {
707
+ this.#logger('error', `Failed to extract expiration from JWT token: ${error instanceof Error ? error.message : error}`);
708
+ return null;
709
+ }
710
+ }
711
+ /**
712
+ * Start a session timer that will expire at the time specified in the JWT token's exp claim.
713
+ * When the timer fires, it executes the same actions as the BadUserNamePasswordError case.
714
+ */
715
+ #startSessionTimer() {
716
+ if (!this.#sessionDetails?.localSessionExpiryHandling) {
717
+ return;
718
+ }
719
+ const token = this.#sessionDetails.token;
720
+ if (!token) {
721
+ this.#logger('warn', 'Cannot start session timer: token not available');
722
+ return;
723
+ }
724
+ // Extract expiration time from JWT token
725
+ const expTimestamp = this.#extractExpirationFromJwt(token);
726
+ if (expTimestamp === null) {
727
+ this.#logger('warn', 'Cannot start session timer: could not extract expiration from JWT token');
728
+ return;
729
+ }
730
+ const currentTimeSeconds = Math.floor(Date.now() / 1000);
731
+ const delaySeconds = expTimestamp - currentTimeSeconds;
732
+ if (delaySeconds <= 0) {
733
+ this.#logger('warn', 'JWT token has already expired or expires immediately');
734
+ // Execute the same actions as BadUserNamePasswordError case
735
+ this.#handleSessionExpiry();
736
+ return;
737
+ }
738
+ // Clear any existing timer
739
+ if (this.#sessionTimer) {
740
+ clearTimeout(this.#sessionTimer);
741
+ }
742
+ const expirationTimeString = new Date(expTimestamp * 1000).toISOString();
743
+ this.#logger('debug', `Starting session timer to expire in ${delaySeconds} seconds (at ${expirationTimeString})`);
744
+ this.#sessionTimer = setTimeout(async () => {
745
+ this.#handleSessionExpiry();
746
+ }, delaySeconds * 1000);
747
+ }
748
+ /**
749
+ * Handles session expiry by executing the same actions as the BadUserNamePasswordError case.
750
+ */
751
+ async #handleSessionExpiry() {
752
+ await this.#disconnect(false);
753
+ this.#logger('warn', 'Session expired');
754
+ this.#events.emitEvent('session-expired');
755
+ }
659
756
  }
660
757
 
661
758
  export { AuthorizationError, CloudInteropAPI, CloudInteropAPIError };
package/package.json CHANGED
@@ -1,18 +1,15 @@
1
1
  {
2
2
  "name": "@openfin/cloud-interop-core-api",
3
- "version": "0.0.1-alpha.03770df",
4
- "type": "module",
3
+ "version": "0.0.1-alpha.03879c9",
5
4
  "description": "",
5
+ "type": "module",
6
6
  "main": "./index.cjs",
7
7
  "browser": "./index.mjs",
8
8
  "types": "./bundle.d.ts",
9
- "author": "",
9
+ "author": "support@here.io",
10
10
  "license": "SEE LICENSE IN LICENSE.md",
11
11
  "dependencies": {
12
- "mqtt": "^5.13.0",
13
- "zod": "^3.24.4"
14
- },
15
- "optionalDependencies": {
16
- "@rollup/rollup-linux-x64-gnu": "*"
12
+ "mqtt": "catalog:",
13
+ "zod": "catalog:"
17
14
  }
18
15
  }