@mojaloop/sdk-scheme-adapter 24.15.0 → 24.15.2

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 (35) hide show
  1. package/.grype.yaml +1 -0
  2. package/.yarn/cache/{@mojaloop-central-services-shared-npm-18.34.1-fc3be8e73c-f7ad2f9394.zip → @mojaloop-central-services-shared-npm-18.34.2-df841bbba3-3daa9af31e.zip} +0 -0
  3. package/.yarn/cache/{@mojaloop-event-sdk-npm-14.8.0-c3e8d5a581-bc2be19e16.zip → @mojaloop-event-sdk-npm-14.8.1-f2a5de549c-49fe01b89c.zip} +0 -0
  4. package/.yarn/cache/@types-node-npm-24.8.1-bc0371b5f2-4f94446676.zip +0 -0
  5. package/.yarn/cache/{@typescript-eslint-eslint-plugin-npm-8.46.0-e6114965b4-415afd894a.zip → @typescript-eslint-eslint-plugin-npm-8.46.1-a9079cb527-9fd8c27958.zip} +0 -0
  6. package/.yarn/cache/{@typescript-eslint-parser-npm-8.46.0-c44629050a-6838fde776.zip → @typescript-eslint-parser-npm-8.46.1-cfdf46e1e9-4edcb49bb0.zip} +0 -0
  7. package/.yarn/cache/{@typescript-eslint-project-service-npm-8.46.0-85a4b9bb9c-de11af23ae.zip → @typescript-eslint-project-service-npm-8.46.1-ebb88f07ff-d63cbb8852.zip} +0 -0
  8. package/.yarn/cache/{@typescript-eslint-scope-manager-npm-8.46.0-fd8edaba78-ed85abd08c.zip → @typescript-eslint-scope-manager-npm-8.46.1-ad4c0a55e0-3d73812087.zip} +0 -0
  9. package/.yarn/cache/{@typescript-eslint-tsconfig-utils-npm-8.46.0-8919c1f746-e78a66a854.zip → @typescript-eslint-tsconfig-utils-npm-8.46.1-4c9ab3591b-f033d68a53.zip} +0 -0
  10. package/.yarn/cache/{@typescript-eslint-type-utils-npm-8.46.0-dbfff922bb-5405b71b91.zip → @typescript-eslint-type-utils-npm-8.46.1-2441cbea81-db989c1f55.zip} +0 -0
  11. package/.yarn/cache/@typescript-eslint-types-npm-8.46.1-79fefa883d-d162ddf6d7.zip +0 -0
  12. package/.yarn/cache/{@typescript-eslint-typescript-estree-npm-8.46.0-0b10d4388a-61053bd0c3.zip → @typescript-eslint-typescript-estree-npm-8.46.1-7899272fc5-af068a14d6.zip} +0 -0
  13. package/.yarn/cache/{@typescript-eslint-utils-npm-8.46.0-a7d3832f43-4e0da60de3.zip → @typescript-eslint-utils-npm-8.46.1-c04d3c3a0c-a8fed8aebd.zip} +0 -0
  14. package/.yarn/cache/{@typescript-eslint-visitor-keys-npm-8.46.0-7d793afea5-37e6145b6a.zip → @typescript-eslint-visitor-keys-npm-8.46.1-6179cc42f8-eed1c5ce08.zip} +0 -0
  15. package/.yarn/cache/openapi-typescript-npm-7.10.1-0695e3203a-531627b682.zip +0 -0
  16. package/.yarn/cache/semver-npm-7.7.3-9cf7b3b46c-8dbc3168e0.zip +0 -0
  17. package/.yarn/cache/{ts-jest-npm-29.4.4-fd3c97fbf0-759913fdb9.zip → ts-jest-npm-29.4.5-5dad11fc5b-48d867e070.zip} +0 -0
  18. package/.yarn/cache/winston-npm-3.18.3-60bcb643a0-0d94690e05.zip +0 -0
  19. package/.yarn/install-state.gz +0 -0
  20. package/CHANGELOG.md +16 -0
  21. package/CLAUDE.md +2 -0
  22. package/modules/api-svc/package.json +3 -3
  23. package/modules/api-svc/src/SdkServer.js +560 -0
  24. package/modules/api-svc/src/index.js +8 -529
  25. package/modules/api-svc/src/lib/model/InboundTransfersModel.js +15 -0
  26. package/modules/api-svc/src/lib/utils.js +20 -1
  27. package/modules/api-svc/test/unit/{index.configPolling.test.js → SdkServer.configPolling.test.js} +11 -11
  28. package/modules/api-svc/test/unit/lib/model/InboundTransfersModel.test.js +215 -0
  29. package/modules/outbound-command-event-handler/package.json +5 -5
  30. package/modules/outbound-domain-event-handler/package.json +4 -4
  31. package/modules/private-shared-lib/package.json +5 -5
  32. package/package.json +5 -5
  33. package/{sbom-v24.14.0.csv → sbom-v24.15.1.csv} +23 -23
  34. package/.yarn/cache/@types-node-npm-24.7.0-fa253cad8d-db0b77e9b1.zip +0 -0
  35. package/.yarn/cache/@typescript-eslint-types-npm-8.46.0-b013400d3e-0118b0dd59.zip +0 -0
package/.grype.yaml CHANGED
@@ -5,6 +5,7 @@ ignore:
5
5
  - vulnerability: CVE-2025-9230
6
6
  - vulnerability: CVE-2025-9231
7
7
  - vulnerability: CVE-2025-9232
8
+ - vulnerability: GHSA-9965-vmph-33xx # validator 13.15.15
8
9
 
9
10
 
10
11
  # Set output format defaults
Binary file
package/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
1
  # Changelog: [mojaloop/sdk-scheme-adapter](https://github.com/mojaloop/sdk-scheme-adapter)
2
+ ### [24.15.2](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.15.1...v24.15.2) (2025-10-17)
3
+
4
+
5
+ ### Chore
6
+
7
+ * adjust retry logic ([#621](https://github.com/mojaloop/sdk-scheme-adapter/issues/621)) ([ce2b72d](https://github.com/mojaloop/sdk-scheme-adapter/commit/ce2b72d33fff72e3b3566390c150fdfbe3b8d24b))
8
+ * **sbom:** update sbom [skip ci] ([e50b0d1](https://github.com/mojaloop/sdk-scheme-adapter/commit/e50b0d1c16a7e84e36691ad2fe3949e555fb2f14))
9
+
10
+ ### [24.15.1](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.15.0...v24.15.1) (2025-10-08)
11
+
12
+
13
+ ### Chore
14
+
15
+ * created a separate file for SdkServer ([#619](https://github.com/mojaloop/sdk-scheme-adapter/issues/619)) ([063087e](https://github.com/mojaloop/sdk-scheme-adapter/commit/063087eefcf63dd293d29d6fb45020490f887daf))
16
+ * **sbom:** update sbom [skip ci] ([b2cf810](https://github.com/mojaloop/sdk-scheme-adapter/commit/b2cf8101be59023e3be7a5da5510eff26c0b7648))
17
+
2
18
  ## [24.15.0](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.14.0...v24.15.0) (2025-10-08)
3
19
 
4
20
 
package/CLAUDE.md CHANGED
@@ -21,6 +21,8 @@ The project consists of multiple modules organized using Nx workspace.
21
21
 
22
22
 
23
23
  **Critical File Locations:**
24
+ - Main server class: `modules/api-svc/src/SdkServer.js`
25
+ - Entry point: `modules/api-svc/src/index.js`
24
26
  - Models: `modules/api-svc/src/lib/model/`
25
27
  - Handlers: `modules/api-svc/src/{Inbound,Outbound}Server/handlers.js`
26
28
  - Config: `modules/*/src/config/default.json`
@@ -68,8 +68,8 @@
68
68
  "@mojaloop/central-services-error-handling": "13.1.3",
69
69
  "@mojaloop/central-services-logger": "11.10.1",
70
70
  "@mojaloop/central-services-metrics": "12.8.0",
71
- "@mojaloop/central-services-shared": "18.34.1",
72
- "@mojaloop/event-sdk": "14.8.0",
71
+ "@mojaloop/central-services-shared": "18.34.2",
72
+ "@mojaloop/event-sdk": "14.8.1",
73
73
  "@mojaloop/logging-bc-client-lib": "0.5.8",
74
74
  "@mojaloop/ml-schema-transformer-lib": "2.7.8",
75
75
  "@mojaloop/sdk-scheme-adapter-private-shared-lib": "workspace:^",
@@ -117,7 +117,7 @@
117
117
  "jest-junit": "16.0.0",
118
118
  "npm-check-updates": "16.7.10",
119
119
  "openapi-response-validator": "12.1.3",
120
- "openapi-typescript": "7.9.1",
120
+ "openapi-typescript": "7.10.1",
121
121
  "redis-mock": "0.56.3",
122
122
  "replace": "1.2.2",
123
123
  "standard-version": "9.5.0",
@@ -0,0 +1,560 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2025 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * Mojaloop Foundation
23
+ * Eugen Klymniuk <eugen.klymniuk@infitx.com>
24
+
25
+ --------------
26
+ ******/
27
+
28
+ 'use strict';
29
+
30
+ const EventEmitter = require('node:events');
31
+ const http = require('node:http');
32
+ const https = require('node:https');
33
+ const _ = require('lodash');
34
+
35
+ const InboundServer = require('./InboundServer');
36
+ const OutboundServer = require('./OutboundServer');
37
+ const OAuthTestServer = require('./OAuthTestServer');
38
+ const { BackendEventHandler } = require('./BackendEventHandler');
39
+ const { FSPIOPEventHandler } = require('./FSPIOPEventHandler');
40
+ const { MetricsServer, MetricsClient } = require('./lib/metrics');
41
+ const TestServer = require('./TestServer');
42
+ const ControlAgent = require('./ControlAgent');
43
+
44
+ const Cache = require('./lib/cache');
45
+ const { createAuthClient } = require('./lib/utils');
46
+ const { logger } = require('./lib/logger');
47
+
48
+ const PING_INTERVAL_MS = 30_000;
49
+
50
+ const createCache = (config) => new Cache({
51
+ logger,
52
+ cacheUrl: config.cacheUrl,
53
+ enableTestFeatures: config.enableTestFeatures,
54
+ subscribeTimeoutSeconds: config.requestProcessingTimeoutSeconds,
55
+ });
56
+
57
+ /**
58
+ * Class that creates and manages http servers that expose the SDK Scheme Adapter APIs.
59
+ */
60
+ class SdkServer extends EventEmitter {
61
+ constructor(conf, logger) {
62
+ super({ captureExceptions: true });
63
+ this.conf = conf;
64
+ this.logger = logger;
65
+ this.cache = createCache(conf);
66
+
67
+ this.metricsClient = new MetricsClient();
68
+ this.metricsServer = new MetricsServer({
69
+ port: this.conf.metrics.port,
70
+ logger: this.logger
71
+ });
72
+
73
+ // Create shared Mojaloop agents for switch communication (used by both servers)
74
+ this.mojaloopSharedAgents = this._createMojaloopSharedAgents(this.conf);
75
+
76
+ this.oidc = createAuthClient(conf, logger);
77
+ this.oidc.auth.on('error', (msg) => {
78
+ this.emit('error', 'OIDC auth error in InboundApi', msg);
79
+ });
80
+
81
+ this.inboundServer = new InboundServer(
82
+ this.conf,
83
+ this.logger,
84
+ this.cache,
85
+ this.oidc,
86
+ this.mojaloopSharedAgents,
87
+ );
88
+ this.inboundServer.on('error', (...args) => {
89
+ this.logger.isErrorEnabled && this.logger.push({ args }).error('Unhandled error in Inbound Server');
90
+ this.emit('error', 'Unhandled error in Inbound Server');
91
+ });
92
+
93
+ this.outboundServer = new OutboundServer(
94
+ this.conf,
95
+ this.logger,
96
+ this.cache,
97
+ this.metricsClient,
98
+ this.oidc,
99
+ this.mojaloopSharedAgents,
100
+ );
101
+ this.outboundServer.on('error', (...args) => {
102
+ this.logger.isErrorEnabled && this.logger.push({ args }).error('Unhandled error in Outbound Server');
103
+ this.emit('error', 'Unhandled error in Outbound Server');
104
+ });
105
+
106
+ if (this.conf.oauthTestServer.enabled) {
107
+ this.oauthTestServer = new OAuthTestServer({
108
+ clientKey: this.conf.oauthTestServer.clientKey,
109
+ clientSecret: this.conf.oauthTestServer.clientSecret,
110
+ port: this.conf.oauthTestServer.listenPort,
111
+ logger: this.logger,
112
+ });
113
+ }
114
+
115
+ if (this.conf.enableTestFeatures) {
116
+ this.testServer = new TestServer({
117
+ config: this.conf,
118
+ port: this.conf.test.port,
119
+ logger: this.logger,
120
+ cache: this.cache,
121
+ });
122
+ }
123
+
124
+ if (this.conf.backendEventHandler.enabled) {
125
+ this.backendEventHandler = new BackendEventHandler({
126
+ config: this.conf,
127
+ logger: this.logger,
128
+ });
129
+ }
130
+
131
+ if (this.conf.fspiopEventHandler.enabled) {
132
+ this.fspiopEventHandler = new FSPIOPEventHandler({
133
+ config: this.conf,
134
+ logger: this.logger,
135
+ cache: this.cache,
136
+ oidc: this.oidc,
137
+ });
138
+ }
139
+ }
140
+
141
+ _shouldUpdateInboundServer(newConf) {
142
+ const isInboundDifferent = !_.isEqual(this.conf.inbound, newConf.inbound);
143
+ const isOutboundDifferent = !_.isEqual(this.conf.outbound, newConf.outbound);
144
+ const isPeerJWSKeysDifferent = !_.isEqual(this.conf.peerJWSKeys, newConf.peerJWSKeys);
145
+ const isJwsSigningKeyDifferent = !_.isEqual(this.conf.jwsSigningKey, newConf.jwsSigningKey);
146
+
147
+ if (isInboundDifferent) {
148
+ this.logger.debug('Inbound config is different', {
149
+ oldInbound: this.conf.inbound,
150
+ newInbound: newConf.inbound
151
+ });
152
+ }
153
+ if (isOutboundDifferent) {
154
+ this.logger.debug('Outbound config is different (checked in inbound update)', {
155
+ oldOutbound: this.conf.outbound,
156
+ newOutbound: newConf.outbound
157
+ });
158
+ }
159
+
160
+ if (isPeerJWSKeysDifferent) {
161
+ this.logger.debug('Peer JWS Keys config is different', {
162
+ oldPeerJWSKeys: this.conf.peerJWSKeys,
163
+ newPeerJWSKeys: newConf.peerJWSKeys
164
+ });
165
+ }
166
+
167
+ if (isJwsSigningKeyDifferent) {
168
+ this.logger.debug('JWS Signing Key config is different', {
169
+ oldJwsSigningKey: this.conf.jwsSigningKey,
170
+ newJwsSigningKey: newConf.jwsSigningKey
171
+ });
172
+ }
173
+
174
+ return isInboundDifferent || isOutboundDifferent || isPeerJWSKeysDifferent || isJwsSigningKeyDifferent;
175
+ }
176
+
177
+ _shouldUpdateOutboundServer(newConf) {
178
+ const isOutboundDifferent = !_.isEqual(this.conf.outbound, newConf.outbound);
179
+ const isJwsSigningKeyDifferent = !_.isEqual(this.conf.jwsSigningKey, newConf.jwsSigningKey);
180
+
181
+ if (isOutboundDifferent) {
182
+ this.logger.debug('Outbound config is different', {
183
+ oldOutbound: this.conf.outbound,
184
+ newOutbound: newConf.outbound
185
+ });
186
+ }
187
+
188
+ if (isJwsSigningKeyDifferent) {
189
+ this.logger.debug('JWS Signing Key config is different', {
190
+ oldJwsSigningKey: this.conf.jwsSigningKey,
191
+ newJwsSigningKey: newConf.jwsSigningKey
192
+ });
193
+ }
194
+
195
+ return isOutboundDifferent || isJwsSigningKeyDifferent;
196
+ }
197
+
198
+ /**
199
+ * Starts periodic polling of Management API for configuration updates.
200
+ * Only runs if PM4ML enabled and a polling interval configured.
201
+ */
202
+ _startConfigPolling() {
203
+ if (!this.conf.pm4mlEnabled || !this.conf.control.mgmtAPIPollIntervalMs) {
204
+ this.logger.info('No failsafe config polling configured');
205
+ return;
206
+ }
207
+
208
+ this.logger.info('starting failsafe config polling from Management API...', { intervalMs: this.conf.control.mgmtAPIPollIntervalMs });
209
+
210
+ this._configPollInterval = setInterval(
211
+ () => this._pollConfigFromMgmtAPI(),
212
+ this.conf.control.mgmtAPIPollIntervalMs
213
+ );
214
+
215
+ // Unref so it doesn't prevent process exit
216
+ this._configPollInterval.unref();
217
+ }
218
+
219
+ /**
220
+ * Polls Management API for configuration updates.
221
+ * Reuses the existing persistent WebSocket client (this.controlClient).
222
+ * Skips polling if:
223
+ * - Another config update is in progress
224
+ * - WebSocket client is not connected
225
+ */
226
+ async _pollConfigFromMgmtAPI() {
227
+ // Race condition prevention: skip if restart in progress
228
+ if (this._configUpdateInProgress) {
229
+ this.logger.info('config updating already in progress, skipping poll');
230
+ return;
231
+ }
232
+
233
+ // WebSocket readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
234
+ if (this.controlClient?.readyState !== 1) {
235
+ this.logger.warn('Control client not ready (not OPEN), skipping poll', { readyState: this.controlClient?.readyState });
236
+ return;
237
+ }
238
+
239
+ try {
240
+ const newConfig = await this.controlClient.getUpdatedConfig();
241
+ if (!newConfig) {
242
+ this.logger.warn('No config received from polling');
243
+ return;
244
+ }
245
+ this.logger.verbose('polling config from mgmt-api is done, checking for config changes...');
246
+
247
+ const mergedConfig = _.merge({}, this.conf, newConfig);
248
+ await this.restart(mergedConfig, { source: 'polling' });
249
+ } catch (err) {
250
+ this.logger.error('error in polling config from Management API: ', err);
251
+ }
252
+ }
253
+
254
+ /** Stops the config polling interval. */
255
+ _stopConfigPolling() {
256
+ if (this._configPollInterval) {
257
+ this.logger.verbose('stopping config polling');
258
+ clearInterval(this._configPollInterval);
259
+ this._configPollInterval = null;
260
+ }
261
+ }
262
+
263
+ async start() {
264
+ await this.cache.connect();
265
+ await this.oidc.auth.start();
266
+
267
+ // We only start the control client if we're running within Mojaloop Payment Manager.
268
+ // The control server is the Payment Manager Management API Service.
269
+ // We only start the client to connect to and listen to the Management API service for
270
+ // management protocol messages e.g configuration changes, certificate updates etc.
271
+ if (this.conf.pm4mlEnabled) {
272
+ const RESTART_INTERVAL_MS = 10000;
273
+ this.controlClient = await ControlAgent.createConnectedControlAgentWs(this.conf, this.logger);
274
+ this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
275
+
276
+ const schedulePing = () => {
277
+ clearTimeout(this.pingTimeout);
278
+ this.pingTimeout = setTimeout(() => {
279
+ this.logger.error('Ping timeout, possible broken connection. Restarting server...');
280
+ this.restart(_.merge({}, this.conf, {
281
+ control: { stopped: Date.now() }
282
+ }));
283
+ }, PING_INTERVAL_MS + this.conf.control.mgmtAPILatencyAssumption);
284
+ };
285
+
286
+ this.controlClient.on('ping', () => {
287
+ this.logger.debug('Received ping from control server');
288
+ schedulePing();
289
+ });
290
+
291
+ this.controlClient.on('close', () => {
292
+ clearTimeout(this.pingTimeout);
293
+ setTimeout(() => {
294
+ this.logger.debug('Control client closed. Restarting server...');
295
+ this.restart(_.merge({}, this.conf, {
296
+ control: { stopped: Date.now() }
297
+ }));
298
+ }, RESTART_INTERVAL_MS);
299
+ });
300
+
301
+ schedulePing();
302
+ this._startConfigPolling();
303
+ }
304
+
305
+ await Promise.all([
306
+ this.inboundServer.start(),
307
+ this.outboundServer.start(),
308
+ this.metricsServer.start(),
309
+ this.testServer?.start(),
310
+ this.oauthTestServer?.start(),
311
+ this.backendEventHandler?.start(),
312
+ this.fspiopEventHandler?.start(),
313
+ ]);
314
+ }
315
+
316
+ async restart(newConf, options = {}) {
317
+ const source = options.source || 'websocket'; // Track source of restart call - websocket or polling
318
+
319
+ // Race condition prevention
320
+ if (this._configUpdateInProgress) {
321
+ this.logger.info('restart already in progress, skipping', { source });
322
+ return;
323
+ }
324
+
325
+ const restartActionsTaken = {};
326
+ this.logger.debug('Server is restarting...', { source });
327
+ this._configUpdateInProgress = true;
328
+
329
+ try {
330
+ let oldCache;
331
+ const updateCache = !_.isEqual(this.conf.cacheUrl, newConf.cacheUrl)
332
+ || !_.isEqual(this.conf.enableTestFeatures, newConf.enableTestFeatures);
333
+ if (updateCache) {
334
+ oldCache = this.cache;
335
+ await this.cache.disconnect();
336
+ this.cache = createCache(newConf);
337
+ await this.cache.connect();
338
+ restartActionsTaken.updateCache = true;
339
+ }
340
+
341
+ const updateOIDC = !_.isEqual(this.conf.oidc, newConf.oidc)
342
+ || !_.isEqual(this.conf.outbound.tls, newConf.outbound.tls);
343
+ if (updateOIDC) {
344
+ this.oidc.auth.stop();
345
+ this.oidc = createAuthClient(newConf, this.logger);
346
+ this.oidc.auth.on('error', (msg) => {
347
+ this.emit('error', 'OIDC auth error in InboundApi', msg);
348
+ });
349
+ await this.oidc.auth.start();
350
+ restartActionsTaken.updateOIDC = true;
351
+ }
352
+
353
+ this.logger.isDebugEnabled && this.logger.push({ oldConf: this.conf.inbound, newConf: newConf.inbound }).debug('Inbound server configuration');
354
+ const updateInboundServer = this._shouldUpdateInboundServer(newConf);
355
+ if (updateInboundServer) {
356
+ const stopStartLabel = 'InboundServer stop/start duration';
357
+ // eslint-disable-next-line no-console
358
+ console.time(stopStartLabel); // todo: remove console.time
359
+ await this.inboundServer.stop();
360
+
361
+ this.mojaloopSharedAgents = this._createMojaloopSharedAgents(newConf);
362
+ this.inboundServer = new InboundServer(
363
+ newConf,
364
+ this.logger,
365
+ this.cache,
366
+ this.oidc,
367
+ this.mojaloopSharedAgents,
368
+ );
369
+ this.inboundServer.on('error', (...args) => {
370
+ const errMessage = 'Unhandled error in Inbound Server';
371
+ this.logger.push({ args }).error(errMessage);
372
+ this.emit('error', errMessage);
373
+ });
374
+ await this.inboundServer.start();
375
+ // eslint-disable-next-line no-console
376
+ console.timeEnd(stopStartLabel);
377
+ restartActionsTaken.updateInboundServer = true;
378
+ }
379
+
380
+ this.logger.isDebugEnabled && this.logger.push({ oldConf: this.conf.outbound, newConf: newConf.outbound }).debug('Outbound server configuration');
381
+ const updateOutboundServer = this._shouldUpdateOutboundServer(newConf);
382
+ if (updateOutboundServer) {
383
+ const stopStartLabel = 'OutboundServer stop/start duration';
384
+ // eslint-disable-next-line no-console
385
+ console.time(stopStartLabel);
386
+ await this.outboundServer.stop();
387
+
388
+ this.mojaloopSharedAgents = this._createMojaloopSharedAgents(newConf);
389
+ this.outboundServer = new OutboundServer(
390
+ newConf,
391
+ this.logger,
392
+ this.cache,
393
+ this.metricsClient,
394
+ this.oidc,
395
+ this.mojaloopSharedAgents,
396
+ );
397
+ this.outboundServer.on('error', (...args) => {
398
+ const errMessage = 'Unhandled error in Outbound Server';
399
+ this.logger.push({ args }).error(errMessage);
400
+ this.emit('error', errMessage);
401
+ });
402
+ await this.outboundServer.start();
403
+ // eslint-disable-next-line no-console
404
+ console.timeEnd(stopStartLabel);
405
+ restartActionsTaken.updateOutboundServer = true;
406
+ }
407
+
408
+ const updateFspiopEventHandler = !_.isEqual(this.conf.outbound, newConf.outbound)
409
+ && this.conf.fspiopEventHandler.enabled;
410
+ if (updateFspiopEventHandler) {
411
+ await this.fspiopEventHandler.stop();
412
+ this.fspiopEventHandler = new FSPIOPEventHandler({
413
+ config: newConf,
414
+ logger: this.logger,
415
+ cache: this.cache,
416
+ oidc: this.oidc,
417
+ });
418
+ await this.fspiopEventHandler.start();
419
+ restartActionsTaken.updateFspiopEventHandler = true;
420
+ }
421
+
422
+ const updateControlClient = !_.isEqual(this.conf.control, newConf.control);
423
+ if (updateControlClient) {
424
+ await this.controlClient?.stop();
425
+ if (this.conf.pm4mlEnabled) {
426
+ const RESTART_INTERVAL_MS = 10000;
427
+
428
+ const schedulePing = () => {
429
+ clearTimeout(this.pingTimeout);
430
+ this.pingTimeout = setTimeout(() => {
431
+ this.logger.error('Ping timeout, possible broken connection. Restarting server...');
432
+ this.restart(_.merge({}, newConf, {
433
+ control: { stopped: Date.now() }
434
+ }));
435
+ }, PING_INTERVAL_MS + this.conf.control.mgmtAPILatencyAssumption);
436
+ };
437
+
438
+ schedulePing();
439
+
440
+ this.controlClient = await ControlAgent.createConnectedControlAgentWs(newConf, this.logger);
441
+ this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
442
+
443
+ this.controlClient.on('ping', () => {
444
+ this.logger.debug('Received ping from control server');
445
+ schedulePing();
446
+ });
447
+
448
+ this.controlClient.on('close', () => {
449
+ clearTimeout(this.pingTimeout);
450
+ setTimeout(() => {
451
+ this.logger.debug('Control client closed. Restarting server...');
452
+ this.restart(_.merge({}, newConf, {
453
+ control: { stopped: Date.now() }
454
+ }));
455
+ }, RESTART_INTERVAL_MS);
456
+ });
457
+
458
+ restartActionsTaken.updateControlClient = true;
459
+ }
460
+ }
461
+
462
+ const updateOAuthTestServer = !_.isEqual(newConf.oauthTestServer, this.conf.oauthTestServer);
463
+ if (updateOAuthTestServer) {
464
+ await this.oauthTestServer?.stop();
465
+ if (this.conf.oauthTestServer.enabled) {
466
+ this.oauthTestServer = new OAuthTestServer({
467
+ clientKey: newConf.oauthTestServer.clientKey,
468
+ clientSecret: newConf.oauthTestServer.clientSecret,
469
+ port: newConf.oauthTestServer.listenPort,
470
+ logger: this.logger,
471
+ });
472
+ await this.oauthTestServer.start();
473
+ restartActionsTaken.updateOAuthTestServer = true;
474
+ }
475
+ }
476
+
477
+ const updateTestServer = !_.isEqual(newConf.test.port, this.conf.test.port);
478
+ if (updateTestServer) {
479
+ await this.testServer?.stop();
480
+ if (this.conf.enableTestFeatures) {
481
+ this.testServer = new TestServer({
482
+ port: newConf.test.port,
483
+ logger: this.logger,
484
+ cache: this.cache,
485
+ config: newConf.test,
486
+ });
487
+ await this.testServer.start();
488
+ restartActionsTaken.updateTestServer = true;
489
+ }
490
+ }
491
+
492
+ this.conf = newConf;
493
+
494
+ await oldCache?.disconnect();
495
+
496
+ if (Object.keys(restartActionsTaken).length > 0) {
497
+ this.logger.info('Server is restarted', { restartActionsTaken, source });
498
+ } else {
499
+ this.logger.verbose('Server not restarted, no config changes detected', { source });
500
+ }
501
+ } catch (err) {
502
+ this.logger.error('error in Server restart: ', err);
503
+ } finally {
504
+ this._configUpdateInProgress = false;
505
+ }
506
+ }
507
+
508
+ stop() {
509
+ this._stopConfigPolling();
510
+
511
+ clearTimeout(this.pingTimeout);
512
+ this.oidc.auth.stop();
513
+ this.controlClient?.removeAllListeners();
514
+ this.inboundServer.removeAllListeners();
515
+ return Promise.all([
516
+ this.cache.disconnect(),
517
+ this.inboundServer.stop(),
518
+ this.outboundServer.stop(),
519
+ this.metricsServer.stop(),
520
+ this.oauthTestServer?.stop(),
521
+ this.testServer?.stop(),
522
+ this.controlClient?.stop(),
523
+ this.backendEventHandler?.stop(),
524
+ this.fspiopEventHandler?.stop(),
525
+ ]);
526
+ }
527
+
528
+ _createMojaloopSharedAgents(conf) {
529
+ const httpAgent = new http.Agent({
530
+ keepAlive: true,
531
+ maxSockets: conf.outbound?.maxSockets || 256,
532
+ });
533
+
534
+ // Create HTTPS agent based on TLS configuration for Mojaloop switch communication
535
+ const httpsAgentOptions = {
536
+ keepAlive: true,
537
+ maxSockets: conf.outbound?.maxSockets || 256,
538
+ };
539
+
540
+ // Apply TLS configuration if mTLS is enabled for switch communication
541
+ if (conf.outbound?.tls?.mutualTLS?.enabled && conf.outbound?.tls?.creds) {
542
+ Object.assign(httpsAgentOptions, conf.outbound.tls.creds);
543
+ }
544
+
545
+ const httpsAgent = new https.Agent(httpsAgentOptions);
546
+
547
+ // Prevent accidental logging of agent internals
548
+ httpAgent.toJSON = () => ({ type: 'HttpAgent', keepAlive: httpAgent.keepAlive });
549
+ httpsAgent.toJSON = () => ({ type: 'HttpsAgent', keepAlive: httpsAgent.keepAlive });
550
+
551
+ this.logger.isInfoEnabled && this.logger.info('Created shared HTTP and HTTPS agents for Mojaloop switch communication');
552
+
553
+ return {
554
+ httpAgent,
555
+ httpsAgent
556
+ };
557
+ }
558
+ }
559
+
560
+ module.exports = SdkServer;