@openfin/cloud-interop-core-api 0.0.1-alpha.39beb33 → 0.0.1-alpha.3cbe14e

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.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var axios = require('axios');
3
4
  var mqtt = require('mqtt');
4
- var sharedUtils = require('@openfin/shared-utils');
5
5
 
6
6
  class CloudInteropAPIError extends Error {
7
7
  code;
@@ -17,295 +17,6 @@ class AuthorizationError extends CloudInteropAPIError {
17
17
  }
18
18
  }
19
19
 
20
- class EventController {
21
- #eventListeners = new Map();
22
- addEventListener(type, callback) {
23
- const listeners = this.#eventListeners.get(type) || [];
24
- listeners.push(callback);
25
- this.#eventListeners.set(type, listeners);
26
- }
27
- removeEventListener(type, callback) {
28
- const listeners = this.#eventListeners.get(type) || [];
29
- const index = listeners.indexOf(callback);
30
- if (index !== -1) {
31
- listeners.splice(index, 1);
32
- }
33
- this.#eventListeners.set(type, listeners);
34
- }
35
- once(type, callback) {
36
- const listener = (...args) => {
37
- this.removeEventListener(type, listener);
38
- // @ts-expect-error - TS doesn't like the spread operator here
39
- callback(...args);
40
- };
41
- this.addEventListener(type, listener);
42
- }
43
- emitEvent(type, ...args) {
44
- const listeners = this.#eventListeners.get(type) || [];
45
- listeners.forEach((listener) => listener(...args));
46
- }
47
- }
48
-
49
- const APP_ID_DELIM = '::';
50
- const getRequestHeaders = (connectionParameters) => {
51
- const headers = {};
52
- headers['Content-Type'] = 'application/json';
53
- if (connectionParameters.authenticationType === 'jwt' && connectionParameters.jwtAuthenticationParameters) {
54
- const tokenResult = connectionParameters.jwtAuthenticationParameters.jwtRequestCallback();
55
- if (!tokenResult) {
56
- throw new Error('jwtRequestCallback must return a token');
57
- }
58
- headers['x-of-auth-id'] = connectionParameters.jwtAuthenticationParameters.authenticationId;
59
- headers['Authorization'] =
60
- typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
61
- }
62
- if (connectionParameters.authenticationType === 'basic' && connectionParameters.basicAuthenticationParameters) {
63
- const { username, password } = connectionParameters.basicAuthenticationParameters;
64
- headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
65
- }
66
- return headers;
67
- };
68
- /**
69
- * Encodes all app intents in the format: `appId::sourceId::sessionId`,
70
- * where sourceId and sessionId are URI encoded
71
- * @param appIntents
72
- * @param source
73
- * @returns
74
- */
75
- const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map((intent) => ({
76
- ...intent,
77
- apps: intent.apps.map((app) => {
78
- const id = encodeURIComponent(app.appId);
79
- const sId = encodeURIComponent(sourceId);
80
- return { ...app, appId: `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}` };
81
- }),
82
- }));
83
- /**
84
- * Decodes all app intents by URI decoding the parts previously encoded by `encodeAppIntents`
85
- * @param appIntents
86
- * @returns
87
- */
88
- const decodeAppIntents = (appIntents) => appIntents.map((intent) => ({
89
- ...intent,
90
- apps: intent.apps.map((app) => {
91
- const [encodedAppId, encodedSourceId, sessionId] = app.appId.split(APP_ID_DELIM);
92
- const id = decodeURIComponent(encodedAppId);
93
- const sourceId = decodeURIComponent(encodedSourceId);
94
- return { ...app, appId: `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}` };
95
- }),
96
- }));
97
- /**
98
- * Decodes the app id to extract the sessionId, returns '' if not able to parse
99
- * @param app
100
- * @returns
101
- */
102
- const parseSessionId = (appId) => (typeof appId === 'string' ? appId : appId.appId).split(APP_ID_DELIM)?.[2]?.trim() ?? '';
103
- const getSourceFromSession = (sessionDetails) => ({
104
- sessionId: sessionDetails.sessionId,
105
- sourceId: sessionDetails.sourceId,
106
- userId: sessionDetails.sub,
107
- orgId: sessionDetails.orgId,
108
- platformId: sessionDetails.platformId,
109
- });
110
-
111
- const MIN_TIMEOUT = 500;
112
- const DEFAULT_TIMEOUT = 3000;
113
- const newDiscovery = () => ({
114
- id: undefined,
115
- pendingIntentDetailsEvents: [],
116
- sessionCount: 0,
117
- responseCount: 0,
118
- state: 'not-started',
119
- });
120
- class IntentController {
121
- #url;
122
- #mqttClient;
123
- #sessionDetails;
124
- #connectionParams;
125
- #events;
126
- #logger;
127
- #discovery = newDiscovery();
128
- #discoveryTimeout;
129
- constructor(url, mqttClient, sessionDetails, connectionParameters, events, logger) {
130
- this.#url = url;
131
- this.#mqttClient = mqttClient;
132
- this.#sessionDetails = sessionDetails;
133
- this.#connectionParams = connectionParameters;
134
- this.#events = events;
135
- this.#logger = logger;
136
- }
137
- async startIntentDiscovery(options) {
138
- if (this.#discovery.state === 'in-progress') {
139
- throw new Error('Intent discovery already in progress');
140
- }
141
- const { timeout = DEFAULT_TIMEOUT, findOptions } = options;
142
- // clamp min value to 500ms
143
- const clampedTimeout = Math.max(timeout, MIN_TIMEOUT);
144
- try {
145
- const startResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}`, {
146
- method: 'POST',
147
- headers: getRequestHeaders(this.#connectionParams),
148
- body: JSON.stringify({ findOptions }),
149
- });
150
- if (!startResponse.ok) {
151
- throw new Error(startResponse.statusText);
152
- }
153
- // TODO: type this response?
154
- const json = await startResponse.json();
155
- this.#discovery.id = json.discoveryId;
156
- this.#discovery.sessionCount = json.sessionCount;
157
- this.#discovery.state = 'in-progress';
158
- // Listen out for discovery results directly sent to us
159
- await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
160
- this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
161
- }
162
- catch (error) {
163
- // Clean up any ongoing discoveries
164
- this.#endIntentDiscovery();
165
- throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
166
- }
167
- }
168
- async #endIntentDiscovery() {
169
- if (this.#discovery.state !== 'in-progress') {
170
- // TODO: remove debug logs
171
- this.#logger('debug', 'Intent discovery not in progress');
172
- return;
173
- }
174
- if (this.#discoveryTimeout) {
175
- clearTimeout(this.#discoveryTimeout);
176
- this.#discoveryTimeout = undefined;
177
- }
178
- this.#discovery.state = 'ended';
179
- // emit our aggregated events
180
- this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
181
- // gracefully end discovery
182
- await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
183
- this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
184
- });
185
- await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
186
- method: 'DELETE',
187
- headers: getRequestHeaders(this.#connectionParams),
188
- })
189
- .then((deleteResponse) => {
190
- if (!deleteResponse.ok) {
191
- throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
192
- }
193
- this.#logger('debug', 'Intent discovery ended');
194
- })
195
- .catch((error) => {
196
- this.#logger('warn', `Error ending intent discovery: ${error}`);
197
- });
198
- // clean up
199
- this.#discovery = newDiscovery();
200
- }
201
- async raiseIntent({ raiseOptions, appId }) {
202
- const targetSessionId = parseSessionId(appId);
203
- if (!targetSessionId) {
204
- // TODO: should we add more info here about the format?
205
- throw new CloudInteropAPIError(`Invalid AppId specified, must be encoded as a cloud-session app id`, 'ERR_INVALID_TARGET_SESSION_ID');
206
- }
207
- const postResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/sessions/${targetSessionId}`, {
208
- method: 'POST',
209
- headers: getRequestHeaders(this.#connectionParams),
210
- body: JSON.stringify({ raiseOptions }),
211
- });
212
- if (!postResponse.ok) {
213
- // TODO: maybe add a debug flag to print these when dev'ing?
214
- // console.log(`Error raising intent: ${await postResponse.text()}`);
215
- throw new CloudInteropAPIError(`Error raising intent`, 'ERR_RAISING_INTENT', new Error(postResponse.statusText));
216
- }
217
- }
218
- async reportAppIntents(discoveryId, intents) {
219
- intents = encodeAppIntents(intents, getSourceFromSession(this.#sessionDetails));
220
- try {
221
- const reportResponse = await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${discoveryId}`, {
222
- method: 'POST',
223
- headers: getRequestHeaders(this.#connectionParams),
224
- body: JSON.stringify({ intents }),
225
- });
226
- if (reportResponse.ok) {
227
- return true;
228
- }
229
- throw new CloudInteropAPIError('Error reporting intents', 'ERR_REPORTING_INTENTS', new Error(reportResponse.statusText));
230
- }
231
- catch (error) {
232
- this.#logger('warn', `Error reporting intents for discovery ID ${discoveryId}: ${error}`);
233
- }
234
- return false;
235
- }
236
- async sendIntentResult(initiatingSessionId, result) {
237
- const { sessionId } = getSourceFromSession(this.#sessionDetails);
238
- const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
239
- method: 'POST',
240
- headers: getRequestHeaders(this.#connectionParams),
241
- body: JSON.stringify({ result }),
242
- });
243
- if (!resultResponse.ok) {
244
- throw new CloudInteropAPIError('Error sending intent result', 'ERR_SENDING_INTENT_RESULT', new Error(resultResponse.statusText));
245
- }
246
- }
247
- handleCommandMessage(message) {
248
- switch (message.command) {
249
- case 'report-intents': {
250
- if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
251
- // Ignore if this originated from us
252
- return;
253
- }
254
- const { command: _, ...event } = message;
255
- this.#events.emitEvent('report-intents', event);
256
- break;
257
- }
258
- case 'intent-details': {
259
- /**
260
- * We aggregate intent details commands from all connected sessions on the server side
261
- * to ensure upstream clients don't have to do this. Also given @openfin/cloud-interop
262
- * exposes FDC3 compliant APIs, there is no concept of streaming available, hence we
263
- * aggregate the responses here and send them in a synchronous manner.
264
- */
265
- const { command: _, ...event } = message;
266
- // Decode intents before emitting to client
267
- event.intents = decodeAppIntents(event.intents);
268
- // always emit individual intent-details events in addition to aggregate-intent-details
269
- // for flexibility after intent discovery has ended, this can be useful for late-joining
270
- // clients nearing the timeout
271
- this.#events.emitEvent('intent-details', event);
272
- if (message.discoveryId !== this.#discovery.id || this.#discovery.state !== 'in-progress') {
273
- // Ignore if its any other discovery id for some reason, or
274
- // if we're not in the middle of a discovery
275
- return;
276
- }
277
- this.#discovery.responseCount += 1;
278
- this.#logger('debug', `Received intent details from ${message.source.sessionId}, received: ${this.#discovery.responseCount}, out of connected sessions: ${this.#discovery.sessionCount - 1}`);
279
- this.#discovery.pendingIntentDetailsEvents.push(event);
280
- const allResponded = this.#discovery.responseCount === this.#discovery.sessionCount - 1;
281
- if (allResponded) {
282
- this.#endIntentDiscovery();
283
- }
284
- break;
285
- }
286
- case 'raise-intent': {
287
- if (message.targetSessionId === this.#sessionDetails?.sessionId) {
288
- const { command: _, ...event } = message;
289
- this.#events.emitEvent('raise-intent', event);
290
- }
291
- break;
292
- }
293
- case 'intent-result': {
294
- if (message.initiatingSessionId === this.#sessionDetails?.sessionId) {
295
- // Return result to originator and end discovery
296
- const { command: _, ...resultEvent } = message;
297
- this.#events.emitEvent('intent-result', resultEvent);
298
- }
299
- break;
300
- }
301
- default: {
302
- this.#logger('warn', `Unknown command message received:\n${JSON.stringify(message, null, 2)}`);
303
- break;
304
- }
305
- }
306
- }
307
- }
308
-
309
20
  // Error codes as defined in https://docs.emqx.com/en/cloud/latest/connect_to_deployments/mqtt_client_error_codes.html
310
21
  const BadUserNamePasswordError = 134;
311
22
  /**
@@ -326,9 +37,8 @@ class CloudInteropAPI {
326
37
  };
327
38
  #reconnectRetries = 0;
328
39
  #connectionParams;
40
+ #eventListeners = new Map();
329
41
  #attemptingToReconnect = false;
330
- #events = new EventController();
331
- #intents;
332
42
  constructor(cloudInteropSettings) {
333
43
  this.#cloudInteropSettings = cloudInteropSettings;
334
44
  }
@@ -354,102 +64,105 @@ class CloudInteropAPI {
354
64
  this.#keepAliveIntervalSeconds = parameters.keepAliveIntervalSeconds || this.#keepAliveIntervalSeconds;
355
65
  this.#logger = parameters.logger || this.#logger;
356
66
  const { sourceId, platformId } = this.#connectionParams;
357
- const createSessionResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions`, {
358
- method: 'POST',
359
- headers: getRequestHeaders(this.#connectionParams),
360
- body: JSON.stringify({ sourceId: sourceId.trim(), platformId }),
361
- });
362
- if (!createSessionResponse.ok) {
363
- if (createSessionResponse.status === 401 || createSessionResponse.status === 403) {
364
- throw new AuthorizationError();
365
- }
366
- throw new CloudInteropAPIError();
367
- }
368
- if (createSessionResponse.status !== 201) {
369
- throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
370
- }
371
- this.#sessionDetails = (await createSessionResponse.json());
372
- const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
373
- const clientOptions = {
374
- keepalive: this.#keepAliveIntervalSeconds,
375
- clientId: this.#sessionDetails.sessionId,
376
- clean: true,
377
- protocolVersion: 5,
378
- // The "will" message will be published on an unexpected disconnection
379
- // The server can then tidy up. So it needs every for this client to do that, the session details is perfect
380
- will: {
381
- topic: 'interop/lastwill',
382
- payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
383
- qos: 0,
384
- retain: false,
385
- },
386
- username: this.#sessionDetails.token,
387
- };
388
- this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
389
- // TODO: Dynamic intent discovery
390
- // search for any ongoing discoveries in DB and fire report-intents on self
391
- this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
392
- this.#mqttClient.on('error', async (error) => {
393
- // We will receive errors for each failed reconnection attempt
394
- // We don't want to disconnect on these else we will never reconnect
395
- if (!this.#attemptingToReconnect) {
396
- await this.#disconnect(false);
67
+ try {
68
+ const createSessionResponse = await axios.post(`${this.#cloudInteropSettings.url}/api/sessions`, {
69
+ sourceId,
70
+ platformId,
71
+ }, {
72
+ headers: this.#getRequestHeaders(),
73
+ });
74
+ if (createSessionResponse.status !== 201) {
75
+ throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', createSessionResponse.status);
397
76
  }
398
- if (error instanceof mqtt.ErrorWithReasonCode) {
399
- switch (error.code) {
400
- case BadUserNamePasswordError: {
401
- await this.#disconnect(false);
402
- this.#logger('warn', `Session expired`);
403
- this.#events.emitEvent('session-expired');
404
- return;
405
- }
406
- default: {
407
- this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
408
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
409
- if (!this.#attemptingToReconnect) {
410
- this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
411
- break;
77
+ this.#sessionDetails = createSessionResponse.data;
78
+ const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
79
+ const clientOptions = {
80
+ keepalive: this.#keepAliveIntervalSeconds,
81
+ clientId: this.#sessionDetails.sessionId,
82
+ clean: true,
83
+ protocolVersion: 5,
84
+ // The "will" message will be published on an unexpected disconnection
85
+ // The server can then tidy up. So it needs every for this client to do that, the session details is perfect
86
+ will: {
87
+ topic: 'interop/lastwill',
88
+ payload: Buffer.from(JSON.stringify(this.#sessionDetails)),
89
+ qos: 0,
90
+ retain: false,
91
+ },
92
+ username: this.#sessionDetails.token,
93
+ };
94
+ this.#mqttClient = await mqtt.connectAsync(this.#sessionDetails.url, clientOptions);
95
+ this.#logger('log', `Cloud Interop successfully connected to ${this.#cloudInteropSettings.url}`);
96
+ this.#mqttClient.on('error', async (error) => {
97
+ // We will receive errors for each failed reconnection attempt
98
+ // We don't won't to disconnect on these else we will never reconnect
99
+ if (!this.#attemptingToReconnect) {
100
+ await this.#disconnect(false);
101
+ }
102
+ if (error instanceof mqtt.ErrorWithReasonCode) {
103
+ switch (error.code) {
104
+ case BadUserNamePasswordError: {
105
+ await this.#disconnect(false);
106
+ this.#logger('warn', `Session expired`);
107
+ this.#emitEvent('session-expired');
108
+ return;
109
+ }
110
+ default: {
111
+ this.#logger('error', `Unknown Infrastructure Error Code ${error.code} : ${error.message}${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}`);
112
+ // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
113
+ if (!this.#attemptingToReconnect) {
114
+ this.#emitEvent('error', new CloudInteropAPIError(`Unknown Infrastructure Error Code ${error.code} : ${error.message}`, 'ERR_INFRASTRUCTURE', error));
115
+ break;
116
+ }
412
117
  }
413
118
  }
414
119
  }
415
- }
416
- else {
417
- this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
418
- // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
419
- if (!this.#attemptingToReconnect) {
420
- this.#events.emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
120
+ else {
121
+ this.#logger('error', `Unknown Error${this.#attemptingToReconnect ? ' during reconnection attempt' : ''}: ${error}`);
122
+ // As we are in the middle of a reconnect, lets not emit an error to cut down on the event noise
123
+ if (!this.#attemptingToReconnect) {
124
+ this.#emitEvent('error', new CloudInteropAPIError(`Unknown Error`, 'ERR_UNKNOWN', error));
125
+ }
421
126
  }
127
+ });
128
+ this.#mqttClient.on('reconnect', () => {
129
+ this.#attemptingToReconnect = true;
130
+ this.#reconnectRetries += 1;
131
+ this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
132
+ if (this.#reconnectRetries === this.#reconnectRetryLimit) {
133
+ this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
134
+ this.#disconnect(true);
135
+ }
136
+ this.#emitEvent('reconnecting', this.#reconnectRetries);
137
+ });
138
+ // Does not fire on initial connection, only successful reconnection attempts
139
+ this.#mqttClient.on('connect', () => {
140
+ this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
141
+ this.#reconnectRetries = 0;
142
+ this.#attemptingToReconnect = false;
143
+ this.#emitEvent('reconnected');
144
+ });
145
+ this.#mqttClient.on('message', (topic, message) => {
146
+ if (!this.#sessionDetails) {
147
+ this.#logger('warn', 'Received message when session not connected');
148
+ return;
149
+ }
150
+ this.#handleCommand(topic, message, this.#sessionDetails);
151
+ });
152
+ // Subscribe to all context groups
153
+ this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
154
+ // Listen out for global commands
155
+ this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
156
+ }
157
+ catch (error) {
158
+ if (axios.isAxiosError(error)) {
159
+ if (error.response?.status === 401 || error.response?.status === 403) {
160
+ throw new AuthorizationError();
161
+ }
162
+ throw new CloudInteropAPIError();
422
163
  }
423
- });
424
- this.#mqttClient.on('reconnect', () => {
425
- this.#attemptingToReconnect = true;
426
- this.#reconnectRetries += 1;
427
- this.#logger('debug', `Cloud Interop attempting reconnection - ${this.#reconnectRetries}...`);
428
- if (this.#reconnectRetries === this.#reconnectRetryLimit) {
429
- this.#logger('warn', `Cloud Interop reached max reconnection attempts - ${this.#reconnectRetryLimit}...`);
430
- this.#disconnect(true);
431
- }
432
- this.#events.emitEvent('reconnecting', this.#reconnectRetries);
433
- });
434
- // Does not fire on initial connection, only successful reconnection attempts
435
- this.#mqttClient.on('connect', () => {
436
- this.#logger('debug', `Cloud Interop successfully reconnected after ${this.#reconnectRetries} attempts`);
437
- this.#reconnectRetries = 0;
438
- this.#attemptingToReconnect = false;
439
- this.#events.emitEvent('reconnected');
440
- });
441
- this.#mqttClient.on('message', (topic, message) => {
442
- if (!this.#sessionDetails) {
443
- this.#logger('warn', 'Received message when session not connected');
444
- return;
445
- }
446
- this.#handleMessage(topic, message, this.#sessionDetails);
447
- });
448
- // Subscribe to all context groups
449
- this.#mqttClient.subscribe(`${sessionRootTopic}/context-groups/#`);
450
- // Listen out for global commands
451
- this.#mqttClient.subscribe(`${sessionRootTopic}/commands`);
452
- this.#initControllers(this.#mqttClient, this.#sessionDetails, this.#connectionParams);
164
+ throw error;
165
+ }
453
166
  }
454
167
  /**
455
168
  * Disconnects from the Cloud Interop service
@@ -470,79 +183,46 @@ class CloudInteropAPI {
470
183
  * @memberof CloudInteropAPI
471
184
  */
472
185
  async setContext(contextGroup, context) {
473
- // TODO: make context of type OpenFin.Context
474
186
  if (!this.#sessionDetails || !this.#connectionParams) {
475
187
  throw new Error('Session not connected');
476
188
  }
477
- if (!this.#mqttClient) {
478
- throw new Error('MQTT client not connected');
479
- }
480
189
  const payload = {
481
190
  context,
482
191
  timestamp: Date.now(),
483
192
  };
484
- const postResponse = await fetch(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, {
485
- method: 'POST',
486
- headers: getRequestHeaders(this.#connectionParams),
487
- body: JSON.stringify(payload),
193
+ await axios.post(`${this.#cloudInteropSettings.url}/api/context-groups/${this.#sessionDetails.sessionId}/${contextGroup}`, payload, {
194
+ headers: this.#getRequestHeaders(),
488
195
  });
489
- if (!postResponse.ok) {
490
- throw new CloudInteropAPIError(`Error setting context for ${contextGroup}`, 'ERR_SETTING_CONTEXT', new Error(postResponse.statusText));
491
- }
492
- }
493
- /**
494
- * Starts an intent discovery operation
495
- *
496
- * @return {*} {Promise<void>}
497
- * @memberof CloudInteropAPI
498
- * @throws {CloudInteropAPIError} - If an error occurs during intent discovery
499
- */
500
- async startIntentDiscovery(options) {
501
- this.#throwIfNotConnected();
502
- return this.#intents?.startIntentDiscovery(options);
503
- }
504
- async raiseIntent(options) {
505
- this.#throwIfNotConnected();
506
- return this.#intents?.raiseIntent(options);
507
- }
508
- async reportAppIntents(discoveryId, intents) {
509
- this.#throwIfNotConnected();
510
- return this.#intents?.reportAppIntents(discoveryId, intents) ?? false;
511
- }
512
- async sendIntentResult(initiatingSessionId, result) {
513
- this.#throwIfNotConnected();
514
- return this.#intents?.sendIntentResult(initiatingSessionId, result);
515
- }
516
- parseSessionId(appId) {
517
- return parseSessionId(appId);
518
196
  }
519
197
  addEventListener(type, callback) {
520
- this.#events.addEventListener(type, callback);
198
+ const listeners = this.#eventListeners.get(type) || [];
199
+ listeners.push(callback);
200
+ this.#eventListeners.set(type, listeners);
521
201
  }
522
202
  removeEventListener(type, callback) {
523
- this.#events.removeEventListener(type, callback);
524
- }
525
- once(type, callback) {
526
- this.#events.once(type, callback);
203
+ const listeners = this.#eventListeners.get(type) || [];
204
+ const index = listeners.indexOf(callback);
205
+ if (index !== -1) {
206
+ listeners.splice(index, 1);
207
+ }
208
+ this.#eventListeners.set(type, listeners);
527
209
  }
528
210
  async #disconnect(fireDisconnectedEvent) {
529
- if (!this.#sessionDetails || !this.#connectionParams) {
211
+ if (!this.#sessionDetails) {
530
212
  return;
531
213
  }
532
214
  try {
533
- const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
534
- method: 'DELETE',
535
- headers: getRequestHeaders(this.#connectionParams),
215
+ const disconnectResponse = await axios.delete(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
216
+ headers: this.#getRequestHeaders(),
536
217
  });
537
218
  if (disconnectResponse.status !== 200) {
538
- throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', new Error(disconnectResponse.statusText));
219
+ throw new CloudInteropAPIError('Error during session tear down - unexpected status', 'ERR_DISCONNECT', disconnectResponse.status);
539
220
  }
540
221
  }
541
- catch (error) {
542
- throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT', error);
222
+ catch {
223
+ throw new CloudInteropAPIError('Error during disconnection', 'ERR_DISCONNECT');
543
224
  }
544
225
  finally {
545
- this.#destroyControllers();
546
226
  this.#mqttClient?.removeAllListeners();
547
227
  await this.#mqttClient?.endAsync(true);
548
228
  this.#sessionDetails = undefined;
@@ -550,42 +230,27 @@ class CloudInteropAPI {
550
230
  this.#reconnectRetries = 0;
551
231
  this.#attemptingToReconnect = false;
552
232
  if (fireDisconnectedEvent) {
553
- this.#events.emitEvent('disconnected');
233
+ this.#emitEvent('disconnected');
554
234
  }
555
235
  }
556
236
  }
557
- #handleMessage(topic, message, sessionDetails) {
237
+ #handleCommand(topic, message, sessionDetails) {
558
238
  if (message.length === 0 || !sessionDetails) {
559
239
  // Ignore clean up messages
560
240
  return;
561
241
  }
562
242
  const messageEnvelope = JSON.parse(message.toString());
563
243
  if (topic.startsWith(`${sessionDetails.sessionRootTopic}/context-groups/`)) {
564
- const contextEvent = messageEnvelope;
565
- if (contextEvent.source.sessionId === sessionDetails.sessionId) {
244
+ if (messageEnvelope.source.sessionId === sessionDetails.sessionId) {
566
245
  return;
567
246
  }
568
- const { contextGroup, context, source, history } = contextEvent;
569
- this.#events.emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
570
- }
571
- else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
572
- this.#handleCommandMessage(messageEnvelope);
247
+ const { channelName: contextGroup, payload: context, source, history } = messageEnvelope;
248
+ this.#emitEvent('context', { contextGroup, context, source, history: { ...history, clientReceived: Date.now() } });
573
249
  }
574
250
  }
575
- #handleCommandMessage(message) {
576
- switch (message.command) {
577
- case 'report-intents':
578
- case 'intent-details':
579
- case 'raise-intent':
580
- case 'intent-result': {
581
- this.#intents?.handleCommandMessage(message);
582
- break;
583
- }
584
- default: {
585
- this.#logger('warn', `Unknown command message received:\n${JSON.stringify(message, null, 2)}`);
586
- break;
587
- }
588
- }
251
+ #emitEvent(type, ...args) {
252
+ const listeners = this.#eventListeners.get(type) || [];
253
+ listeners.forEach((listener) => listener(...args));
589
254
  }
590
255
  #validateConnectParams = (parameters) => {
591
256
  if (!parameters) {
@@ -594,9 +259,6 @@ class CloudInteropAPI {
594
259
  if (!parameters.sourceId) {
595
260
  throw new Error('sourceId must be provided');
596
261
  }
597
- if (parameters.sourceId.includes(APP_ID_DELIM)) {
598
- throw new Error(`sourceId cannot contain "${APP_ID_DELIM}"`);
599
- }
600
262
  if (!parameters.platformId) {
601
263
  throw new Error('platformId must be provided');
602
264
  }
@@ -609,28 +271,29 @@ class CloudInteropAPI {
609
271
  throw new Error('basicAuthenticationParameters must be provided when using basic authentication');
610
272
  }
611
273
  };
612
- #initControllers(mqttClient, sessionDetails, connectionParameters) {
613
- this.#intents = new IntentController(this.#cloudInteropSettings.url, mqttClient, sessionDetails, connectionParameters, this.#events, this.#logger);
614
- }
615
- #destroyControllers() {
616
- this.#intents = undefined;
617
- }
618
- #throwIfNotConnected() {
619
- if (!this.#sessionDetails || !this.#connectionParams) {
620
- throw new Error('Session not connected');
274
+ #getRequestHeaders = () => {
275
+ if (!this.#connectionParams) {
276
+ throw new Error('Connect parameters must be provided');
621
277
  }
622
- if (!this.#mqttClient) {
623
- throw new Error('MQTT client not connected');
278
+ const headers = new axios.AxiosHeaders();
279
+ headers['Content-Type'] = 'application/json';
280
+ if (this.#connectionParams.authenticationType === 'jwt' && this.#connectionParams.jwtAuthenticationParameters) {
281
+ const tokenResult = this.#connectionParams.jwtAuthenticationParameters.jwtRequestCallback();
282
+ if (!tokenResult) {
283
+ throw new Error('jwtRequestCallback must return a token');
284
+ }
285
+ headers['x-of-auth-id'] = this.#connectionParams.jwtAuthenticationParameters.authenticationId;
286
+ headers['Authorization'] =
287
+ typeof tokenResult === 'string' ? `Bearer ${tokenResult}` : `Bearer ${Buffer.from(JSON.stringify(tokenResult)).toString('base64')}`;
624
288
  }
625
- }
289
+ if (this.#connectionParams.authenticationType === 'basic' && this.#connectionParams.basicAuthenticationParameters) {
290
+ const { username, password } = this.#connectionParams.basicAuthenticationParameters;
291
+ headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
292
+ }
293
+ return headers;
294
+ };
626
295
  }
627
296
 
628
297
  exports.AuthorizationError = AuthorizationError;
629
298
  exports.CloudInteropAPI = CloudInteropAPI;
630
299
  exports.CloudInteropAPIError = CloudInteropAPIError;
631
- Object.keys(sharedUtils).forEach(function (k) {
632
- if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
633
- enumerable: true,
634
- get: function () { return sharedUtils[k]; }
635
- });
636
- });