@openfin/cloud-interop-core-api 0.0.1-alpha.6d7b62b → 0.0.1-alpha.6d9e190
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/{dist/bundle.d.ts → bundle.d.ts} +113 -26
- package/{dist/index.cjs → index.cjs} +177 -47
- package/{dist/index.mjs → index.mjs} +177 -47
- package/package.json +13 -239
- package/dist/LICENSE.md +0 -4
- package/dist/README.md +0 -14
- package/dist/package.json +0 -18
|
@@ -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) {
|
|
@@ -46,6 +50,8 @@ class EventController {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
const isErrorIntentResult = (result) => 'error' in result;
|
|
54
|
+
|
|
49
55
|
const APP_ID_DELIM = '::';
|
|
50
56
|
const getRequestHeaders = (connectionParameters) => {
|
|
51
57
|
const headers = {};
|
|
@@ -72,13 +78,9 @@ const getRequestHeaders = (connectionParameters) => {
|
|
|
72
78
|
* @param source
|
|
73
79
|
* @returns
|
|
74
80
|
*/
|
|
75
|
-
const encodeAppIntents = (appIntents,
|
|
81
|
+
const encodeAppIntents = (appIntents, source) => appIntents.map((intent) => ({
|
|
76
82
|
...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
|
-
}),
|
|
83
|
+
apps: intent.apps.map((app) => ({ ...app, appId: encodeAppId(app.appId, source) })),
|
|
82
84
|
}));
|
|
83
85
|
/**
|
|
84
86
|
* Decodes all app intents by URI decoding the parts previously encoded by `encodeAppIntents`
|
|
@@ -87,19 +89,35 @@ const encodeAppIntents = (appIntents, { sessionId, sourceId }) => appIntents.map
|
|
|
87
89
|
*/
|
|
88
90
|
const decodeAppIntents = (appIntents) => appIntents.map((intent) => ({
|
|
89
91
|
...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
|
-
}),
|
|
92
|
+
apps: intent.apps.map((app) => ({ ...app, appId: decodeAppId(app.appId) })),
|
|
96
93
|
}));
|
|
94
|
+
const encodeAppId = (appIdString, { sessionId, sourceId }) => {
|
|
95
|
+
const id = encodeURIComponent(appIdString);
|
|
96
|
+
const sId = encodeURIComponent(sourceId);
|
|
97
|
+
return `${id}${APP_ID_DELIM}${sId}${APP_ID_DELIM}${sessionId}`;
|
|
98
|
+
};
|
|
99
|
+
const decodeAppId = (appId) => {
|
|
100
|
+
const [encodedAppId, encodedSourceId, sessionId] = appId.split(APP_ID_DELIM);
|
|
101
|
+
const id = decodeURIComponent(encodedAppId);
|
|
102
|
+
const sourceId = decodeURIComponent(encodedSourceId);
|
|
103
|
+
return `${id}${APP_ID_DELIM}${sourceId}${APP_ID_DELIM}${sessionId}`;
|
|
104
|
+
};
|
|
97
105
|
/**
|
|
98
|
-
* Decodes the
|
|
99
|
-
* @
|
|
100
|
-
*
|
|
106
|
+
* Decodes the AppIdentifier to extract the appId, sourceId, and sessionId.
|
|
107
|
+
* @returns an object with:
|
|
108
|
+
* - appId: The appId, or the original appId if unable to parse.
|
|
109
|
+
* - sourceId: The sourceId, or an '' if unable to parse.
|
|
110
|
+
* - sessionId: The sessionId, or an '' if unable to parse.
|
|
101
111
|
*/
|
|
102
|
-
const
|
|
112
|
+
const parseCloudAppId = (appId = '') => {
|
|
113
|
+
const originalAppString = typeof appId === 'string' ? appId : (appId.appId ?? '');
|
|
114
|
+
const parts = originalAppString.split(APP_ID_DELIM);
|
|
115
|
+
return {
|
|
116
|
+
appId: parts[0]?.trim() ?? originalAppString,
|
|
117
|
+
sourceId: parts[1]?.trim() ?? '',
|
|
118
|
+
sessionId: parts[2]?.trim() ?? '',
|
|
119
|
+
};
|
|
120
|
+
};
|
|
103
121
|
const getSourceFromSession = (sessionDetails) => ({
|
|
104
122
|
sessionId: sessionDetails.sessionId,
|
|
105
123
|
sourceId: sessionDetails.sourceId,
|
|
@@ -148,13 +166,18 @@ class IntentController {
|
|
|
148
166
|
body: JSON.stringify({ findOptions }),
|
|
149
167
|
});
|
|
150
168
|
if (!startResponse.ok) {
|
|
151
|
-
throw new Error(startResponse.statusText);
|
|
169
|
+
throw new Error(`Error creating intent discovery record: ${startResponse.statusText}`);
|
|
152
170
|
}
|
|
153
171
|
// TODO: type this response?
|
|
154
172
|
const json = await startResponse.json();
|
|
155
173
|
this.#discovery.id = json.discoveryId;
|
|
156
174
|
this.#discovery.sessionCount = json.sessionCount;
|
|
157
175
|
this.#discovery.state = 'in-progress';
|
|
176
|
+
if (this.#discovery.sessionCount === 1) {
|
|
177
|
+
// since we have no other connected sessions, we can end discovery immediately
|
|
178
|
+
await this.#endIntentDiscovery(false);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
158
181
|
// Listen out for discovery results directly sent to us
|
|
159
182
|
await this.#mqttClient.subscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`);
|
|
160
183
|
this.#discoveryTimeout = setTimeout(() => this.#endIntentDiscovery(), clampedTimeout);
|
|
@@ -165,10 +188,8 @@ class IntentController {
|
|
|
165
188
|
throw new CloudInteropAPIError('Error starting intent discovery', 'ERR_STARTING_INTENT_DISCOVERY', error);
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
|
-
async #endIntentDiscovery() {
|
|
191
|
+
async #endIntentDiscovery(mqttUnsubscribe = true) {
|
|
169
192
|
if (this.#discovery.state !== 'in-progress') {
|
|
170
|
-
// TODO: remove debug logs
|
|
171
|
-
this.#logger('debug', 'Intent discovery not in progress');
|
|
172
193
|
return;
|
|
173
194
|
}
|
|
174
195
|
if (this.#discoveryTimeout) {
|
|
@@ -178,10 +199,12 @@ class IntentController {
|
|
|
178
199
|
this.#discovery.state = 'ended';
|
|
179
200
|
// emit our aggregated events
|
|
180
201
|
this.#events.emitEvent('aggregate-intent-details', { responses: this.#discovery.pendingIntentDetailsEvents });
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.#
|
|
184
|
-
|
|
202
|
+
if (mqttUnsubscribe) {
|
|
203
|
+
// gracefully end discovery
|
|
204
|
+
await this.#mqttClient.unsubscribeAsync(`${this.#sessionDetails.sessionRootTopic}/commands/${this.#discovery.id}`).catch(() => {
|
|
205
|
+
this.#logger('warn', `Error ending intent discovery: could not unsubscribe from discovery id ${this.#discovery.id}`);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
185
208
|
await fetch(`${this.#url}/api/intents/${this.#sessionDetails.sessionId}/${this.#discovery.id}`, {
|
|
186
209
|
method: 'DELETE',
|
|
187
210
|
headers: getRequestHeaders(this.#connectionParams),
|
|
@@ -190,7 +213,6 @@ class IntentController {
|
|
|
190
213
|
if (!deleteResponse.ok) {
|
|
191
214
|
throw new Error(`Error ending intent discovery: ${deleteResponse.statusText}`);
|
|
192
215
|
}
|
|
193
|
-
this.#logger('debug', 'Intent discovery ended');
|
|
194
216
|
})
|
|
195
217
|
.catch((error) => {
|
|
196
218
|
this.#logger('warn', `Error ending intent discovery: ${error}`);
|
|
@@ -199,7 +221,7 @@ class IntentController {
|
|
|
199
221
|
this.#discovery = newDiscovery();
|
|
200
222
|
}
|
|
201
223
|
async raiseIntent({ raiseOptions, appId }) {
|
|
202
|
-
const targetSessionId =
|
|
224
|
+
const targetSessionId = parseCloudAppId(appId).sessionId;
|
|
203
225
|
if (!targetSessionId) {
|
|
204
226
|
// TODO: should we add more info here about the format?
|
|
205
227
|
throw new CloudInteropAPIError(`Invalid AppId specified, must be encoded as a cloud-session app id`, 'ERR_INVALID_TARGET_SESSION_ID');
|
|
@@ -234,6 +256,13 @@ class IntentController {
|
|
|
234
256
|
return false;
|
|
235
257
|
}
|
|
236
258
|
async sendIntentResult(initiatingSessionId, result) {
|
|
259
|
+
if (!isErrorIntentResult(result)) {
|
|
260
|
+
// cloud-encode the source app id to support chained intent actions over cloud
|
|
261
|
+
// https://fdc3.finos.org/docs/2.0/api/spec#resolution-object -> "Use metadata about the resolving app instance to target a further intent"
|
|
262
|
+
const source = getSourceFromSession(this.#sessionDetails);
|
|
263
|
+
const encoded = encodeAppId(typeof result.source === 'string' ? result.source : result.source.appId, source);
|
|
264
|
+
result.source = typeof result.source === 'string' ? encoded : { ...result.source, appId: encoded };
|
|
265
|
+
}
|
|
237
266
|
const { sessionId } = getSourceFromSession(this.#sessionDetails);
|
|
238
267
|
const resultResponse = await fetch(`${this.#url}/api/intents/${initiatingSessionId}/result/${sessionId}`, {
|
|
239
268
|
method: 'POST',
|
|
@@ -311,9 +340,8 @@ const BadUserNamePasswordError = 134;
|
|
|
311
340
|
/**
|
|
312
341
|
* Represents a single connection to a Cloud Interop service
|
|
313
342
|
*
|
|
314
|
-
* @
|
|
315
|
-
* @class
|
|
316
|
-
* @implements {Client}
|
|
343
|
+
* @public
|
|
344
|
+
* @class
|
|
317
345
|
*/
|
|
318
346
|
class CloudInteropAPI {
|
|
319
347
|
#cloudInteropSettings;
|
|
@@ -329,6 +357,7 @@ class CloudInteropAPI {
|
|
|
329
357
|
#attemptingToReconnect = false;
|
|
330
358
|
#events = new EventController();
|
|
331
359
|
#intents;
|
|
360
|
+
#sessionTimer;
|
|
332
361
|
constructor(cloudInteropSettings) {
|
|
333
362
|
this.#cloudInteropSettings = cloudInteropSettings;
|
|
334
363
|
}
|
|
@@ -341,11 +370,11 @@ class CloudInteropAPI {
|
|
|
341
370
|
/**
|
|
342
371
|
* Connects and creates a session on the Cloud Interop service
|
|
343
372
|
*
|
|
344
|
-
* @param
|
|
345
|
-
* @
|
|
373
|
+
* @param parameters - The parameters to use to connect
|
|
374
|
+
* @returns Promise that resolves when connection is established
|
|
346
375
|
* @memberof CloudInteropAPI
|
|
347
|
-
* @throws
|
|
348
|
-
* @throws
|
|
376
|
+
* @throws CloudInteropAPIError - If an error occurs during connection
|
|
377
|
+
* @throws AuthorizationError - If the connection is unauthorized
|
|
349
378
|
*/
|
|
350
379
|
async connect(parameters) {
|
|
351
380
|
this.#validateConnectParams(parameters);
|
|
@@ -369,6 +398,11 @@ class CloudInteropAPI {
|
|
|
369
398
|
throw new CloudInteropAPIError(`Failed to connect to the Cloud Interop service: ${this.#cloudInteropSettings.url}`, 'ERR_CONNECT', new Error(createSessionResponse.statusText));
|
|
370
399
|
}
|
|
371
400
|
this.#sessionDetails = (await createSessionResponse.json());
|
|
401
|
+
// If local session expiry handling is enabled, start the session timer
|
|
402
|
+
if (this.#sessionDetails.localSessionExpiryHandling) {
|
|
403
|
+
this.#logger('debug', `Local session expiry handling is enabled`);
|
|
404
|
+
this.#startSessionTimer();
|
|
405
|
+
}
|
|
372
406
|
const sessionRootTopic = this.#sessionDetails.sessionRootTopic;
|
|
373
407
|
const clientOptions = {
|
|
374
408
|
keepalive: this.#keepAliveIntervalSeconds,
|
|
@@ -398,9 +432,7 @@ class CloudInteropAPI {
|
|
|
398
432
|
if (error instanceof mqtt.ErrorWithReasonCode) {
|
|
399
433
|
switch (error.code) {
|
|
400
434
|
case BadUserNamePasswordError: {
|
|
401
|
-
|
|
402
|
-
this.#logger('warn', `Session expired`);
|
|
403
|
-
this.#events.emitEvent('session-expired');
|
|
435
|
+
this.#handleSessionExpiry();
|
|
404
436
|
return;
|
|
405
437
|
}
|
|
406
438
|
default: {
|
|
@@ -454,9 +486,9 @@ class CloudInteropAPI {
|
|
|
454
486
|
/**
|
|
455
487
|
* Disconnects from the Cloud Interop service
|
|
456
488
|
*
|
|
457
|
-
* @
|
|
489
|
+
* @returns Promise that resolves when disconnected
|
|
458
490
|
* @memberof CloudInteropAPI
|
|
459
|
-
* @throws
|
|
491
|
+
* @throws CloudInteropAPIError - If an error occurs during disconnection
|
|
460
492
|
*/
|
|
461
493
|
async disconnect() {
|
|
462
494
|
await this.#disconnect(true);
|
|
@@ -464,9 +496,9 @@ class CloudInteropAPI {
|
|
|
464
496
|
/**
|
|
465
497
|
* Publishes a new context for the given context group to the other connected sessions
|
|
466
498
|
*
|
|
467
|
-
* @param
|
|
468
|
-
* @param
|
|
469
|
-
* @
|
|
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
|
|
470
502
|
* @memberof CloudInteropAPI
|
|
471
503
|
*/
|
|
472
504
|
async setContext(contextGroup, context) {
|
|
@@ -493,9 +525,9 @@ class CloudInteropAPI {
|
|
|
493
525
|
/**
|
|
494
526
|
* Starts an intent discovery operation
|
|
495
527
|
*
|
|
496
|
-
* @
|
|
528
|
+
* @returns Promise that resolves when intent discovery is started
|
|
497
529
|
* @memberof CloudInteropAPI
|
|
498
|
-
* @throws
|
|
530
|
+
* @throws CloudInteropAPIError - If an error occurs during intent discovery
|
|
499
531
|
*/
|
|
500
532
|
async startIntentDiscovery(options) {
|
|
501
533
|
this.#throwIfNotConnected();
|
|
@@ -514,7 +546,10 @@ class CloudInteropAPI {
|
|
|
514
546
|
return this.#intents?.sendIntentResult(initiatingSessionId, result);
|
|
515
547
|
}
|
|
516
548
|
parseSessionId(appId) {
|
|
517
|
-
return
|
|
549
|
+
return parseCloudAppId(appId).sessionId;
|
|
550
|
+
}
|
|
551
|
+
parseAppId(appId) {
|
|
552
|
+
return parseCloudAppId(appId).appId;
|
|
518
553
|
}
|
|
519
554
|
addEventListener(type, callback) {
|
|
520
555
|
this.#events.addEventListener(type, callback);
|
|
@@ -526,10 +561,18 @@ class CloudInteropAPI {
|
|
|
526
561
|
this.#events.once(type, callback);
|
|
527
562
|
}
|
|
528
563
|
async #disconnect(fireDisconnectedEvent) {
|
|
529
|
-
if (!this.#sessionDetails
|
|
564
|
+
if (!this.#sessionDetails) {
|
|
530
565
|
return;
|
|
531
566
|
}
|
|
532
567
|
try {
|
|
568
|
+
if (!this.#connectionParams) {
|
|
569
|
+
throw new Error('Connect parameters must be provided');
|
|
570
|
+
}
|
|
571
|
+
// Cancel session timer if it's running
|
|
572
|
+
if (this.#sessionTimer) {
|
|
573
|
+
clearTimeout(this.#sessionTimer);
|
|
574
|
+
this.#sessionTimer = undefined;
|
|
575
|
+
}
|
|
533
576
|
const disconnectResponse = await fetch(`${this.#cloudInteropSettings.url}/api/sessions/${this.#sessionDetails.sessionId}`, {
|
|
534
577
|
method: 'DELETE',
|
|
535
578
|
headers: getRequestHeaders(this.#connectionParams),
|
|
@@ -565,8 +608,13 @@ class CloudInteropAPI {
|
|
|
565
608
|
if (contextEvent.source.sessionId === sessionDetails.sessionId) {
|
|
566
609
|
return;
|
|
567
610
|
}
|
|
568
|
-
const { contextGroup,
|
|
569
|
-
this.#events.emitEvent('context', {
|
|
611
|
+
const { context, payload, contextGroup, channelName, source, history } = contextEvent;
|
|
612
|
+
this.#events.emitEvent('context', {
|
|
613
|
+
contextGroup: channelName || contextGroup,
|
|
614
|
+
context: payload || context,
|
|
615
|
+
source,
|
|
616
|
+
history: { ...history, clientReceived: Date.now() },
|
|
617
|
+
});
|
|
570
618
|
}
|
|
571
619
|
else if (topic.startsWith(`${sessionDetails.sessionRootTopic}/commands`)) {
|
|
572
620
|
this.#handleCommandMessage(messageEnvelope);
|
|
@@ -623,6 +671,88 @@ class CloudInteropAPI {
|
|
|
623
671
|
throw new Error('MQTT client not connected');
|
|
624
672
|
}
|
|
625
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.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
|
+
}
|
|
626
756
|
}
|
|
627
757
|
|
|
628
758
|
exports.AuthorizationError = AuthorizationError;
|