@openfin/cloud-interop-core-api 0.0.1-alpha.0c7a36f → 0.0.1-alpha.0c7ee0b
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/README.md +2 -2
- package/bundle.d.ts +13 -12
- package/index.cjs +121 -24
- package/index.mjs +121 -24
- package/package.json +5 -8
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# @openfin/cloud-interop-core-api
|
|
2
2
|
|
|
3
|
-
This package contains the core interop library that handles all interactions with the
|
|
3
|
+
This package contains the core interop library that handles all interactions with the HERE Cloud Interop Service.
|
|
4
4
|
|
|
5
5
|
It is callable via browser or node applications.
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Authentication
|
|
9
9
|
|
|
10
|
-
The library supports authentication with the
|
|
10
|
+
The library supports authentication with the HERE Cloud Interop Service using the following methods:
|
|
11
11
|
- Basic Authentication
|
|
12
12
|
- JWT Token Authentication
|
|
13
13
|
- Default Authentication i.e. Interactive session based authentication using cookies
|
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
|
|
249
|
-
* @returns
|
|
248
|
+
* @param parameters - The parameters to use to connect
|
|
249
|
+
* @returns Promise that resolves when connection is established
|
|
250
250
|
* @memberof CloudInteropAPI
|
|
251
|
-
* @throws
|
|
252
|
-
* @throws
|
|
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
|
|
258
|
+
* @returns Promise that resolves when disconnected
|
|
259
259
|
* @memberof CloudInteropAPI
|
|
260
|
-
* @throws
|
|
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
|
|
267
|
-
* @param
|
|
268
|
-
* @returns
|
|
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
|
|
275
|
+
* @returns Promise that resolves when intent discovery is started
|
|
276
276
|
* @memberof CloudInteropAPI
|
|
277
|
-
* @throws
|
|
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
|
|
369
|
-
* @returns
|
|
374
|
+
* @param parameters - The parameters to use to connect
|
|
375
|
+
* @returns Promise that resolves when connection is established
|
|
370
376
|
* @memberof CloudInteropAPI
|
|
371
|
-
* @throws
|
|
372
|
-
* @throws
|
|
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
|
-
|
|
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
|
|
491
|
+
* @returns Promise that resolves when disconnected
|
|
482
492
|
* @memberof CloudInteropAPI
|
|
483
|
-
* @throws
|
|
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
|
|
492
|
-
* @param
|
|
493
|
-
* @returns
|
|
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
|
|
530
|
+
* @returns Promise that resolves when intent discovery is started
|
|
521
531
|
* @memberof CloudInteropAPI
|
|
522
|
-
* @throws
|
|
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
|
|
367
|
-
* @returns
|
|
372
|
+
* @param parameters - The parameters to use to connect
|
|
373
|
+
* @returns Promise that resolves when connection is established
|
|
368
374
|
* @memberof CloudInteropAPI
|
|
369
|
-
* @throws
|
|
370
|
-
* @throws
|
|
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
|
-
|
|
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
|
|
489
|
+
* @returns Promise that resolves when disconnected
|
|
480
490
|
* @memberof CloudInteropAPI
|
|
481
|
-
* @throws
|
|
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
|
|
490
|
-
* @param
|
|
491
|
-
* @returns
|
|
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
|
|
528
|
+
* @returns Promise that resolves when intent discovery is started
|
|
519
529
|
* @memberof CloudInteropAPI
|
|
520
|
-
* @throws
|
|
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.
|
|
4
|
-
"type": "module",
|
|
3
|
+
"version": "0.0.1-alpha.0c7ee0b",
|
|
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": "
|
|
13
|
-
"zod": "
|
|
14
|
-
},
|
|
15
|
-
"optionalDependencies": {
|
|
16
|
-
"@rollup/rollup-linux-x64-gnu": "*"
|
|
12
|
+
"mqtt": "catalog:",
|
|
13
|
+
"zod": "catalog:"
|
|
17
14
|
}
|
|
18
15
|
}
|