@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
@@ -27,20 +27,11 @@
27
27
  'use strict';
28
28
 
29
29
  const { hostname } = require('node:os');
30
- const EventEmitter = require('node:events');
31
- const http = require('http');
32
- const https = require('https');
33
- const _ = require('lodash');
30
+ const { merge } = require('lodash');
34
31
  const { name, version } = require('../../../package.json');
35
32
 
33
+ const SdkServer = require('./SdkServer');
36
34
  const config = require('./config');
37
- const InboundServer = require('./InboundServer');
38
- const OutboundServer = require('./OutboundServer');
39
- const OAuthTestServer = require('./OAuthTestServer');
40
- const { BackendEventHandler } = require('./BackendEventHandler');
41
- const { FSPIOPEventHandler } = require('./FSPIOPEventHandler');
42
- const { MetricsServer, MetricsClient } = require('./lib/metrics');
43
- const TestServer = require('./TestServer');
44
35
  const ControlAgent = require('./ControlAgent');
45
36
 
46
37
  // import things we want to expose e.g. for unit tests and users who dont want to use the entire
@@ -51,531 +42,19 @@ const Router = require('./lib/router');
51
42
  const Validate = require('./lib/validate');
52
43
  const Cache = require('./lib/cache');
53
44
  const { SDKStateEnum } = require('./lib/model/common');
54
- const { createAuthClient } = require('./lib/utils');
55
45
  const { logger } = require('./lib/logger');
56
46
 
57
- const PING_INTERVAL_MS = 30_000;
58
-
59
- const createCache = (config) => new Cache({
60
- logger,
61
- cacheUrl: config.cacheUrl,
62
- enableTestFeatures: config.enableTestFeatures,
63
- subscribeTimeoutSeconds: config.requestProcessingTimeoutSeconds,
64
- });
65
-
66
- /**
67
- * Class that creates and manages http servers that expose the scheme adapter APIs.
68
- */
69
- class Server extends EventEmitter {
70
- constructor(conf, logger) {
71
- super({ captureExceptions: true });
72
- this.conf = conf;
73
- this.logger = logger;
74
- this.cache = createCache(conf);
75
-
76
- this.metricsClient = new MetricsClient();
77
- this.metricsServer = new MetricsServer({
78
- port: this.conf.metrics.port,
79
- logger: this.logger
80
- });
81
-
82
- // Create shared Mojaloop agents for switch communication (used by both servers)
83
- this.mojaloopSharedAgents = this._createMojaloopSharedAgents(this.conf);
84
-
85
- this.oidc = createAuthClient(conf, logger);
86
- this.oidc.auth.on('error', (msg) => {
87
- this.emit('error', 'OIDC auth error in InboundApi', msg);
88
- });
89
-
90
- this.inboundServer = new InboundServer(
91
- this.conf,
92
- this.logger,
93
- this.cache,
94
- this.oidc,
95
- this.mojaloopSharedAgents,
96
- );
97
- this.inboundServer.on('error', (...args) => {
98
- this.logger.isErrorEnabled && this.logger.push({ args }).error('Unhandled error in Inbound Server');
99
- this.emit('error', 'Unhandled error in Inbound Server');
100
- });
101
-
102
- this.outboundServer = new OutboundServer(
103
- this.conf,
104
- this.logger,
105
- this.cache,
106
- this.metricsClient,
107
- this.oidc,
108
- this.mojaloopSharedAgents,
109
- );
110
- this.outboundServer.on('error', (...args) => {
111
- this.logger.isErrorEnabled && this.logger.push({ args }).error('Unhandled error in Outbound Server');
112
- this.emit('error', 'Unhandled error in Outbound Server');
113
- });
114
-
115
- if (this.conf.oauthTestServer.enabled) {
116
- this.oauthTestServer = new OAuthTestServer({
117
- clientKey: this.conf.oauthTestServer.clientKey,
118
- clientSecret: this.conf.oauthTestServer.clientSecret,
119
- port: this.conf.oauthTestServer.listenPort,
120
- logger: this.logger,
121
- });
122
- }
123
-
124
- if (this.conf.enableTestFeatures) {
125
- this.testServer = new TestServer({
126
- config: this.conf,
127
- port: this.conf.test.port,
128
- logger: this.logger,
129
- cache: this.cache,
130
- });
131
- }
132
-
133
- if (this.conf.backendEventHandler.enabled) {
134
- this.backendEventHandler = new BackendEventHandler({
135
- config: this.conf,
136
- logger: this.logger,
137
- });
138
- }
139
-
140
- if (this.conf.fspiopEventHandler.enabled) {
141
- this.fspiopEventHandler = new FSPIOPEventHandler({
142
- config: this.conf,
143
- logger: this.logger,
144
- cache: this.cache,
145
- oidc: this.oidc,
146
- });
147
- }
148
- }
149
-
150
- _shouldUpdateInboundServer(newConf) {
151
- const isInboundDifferent = !_.isEqual(this.conf.inbound, newConf.inbound);
152
- const isOutboundDifferent = !_.isEqual(this.conf.outbound, newConf.outbound);
153
- const isPeerJWSKeysDifferent = !_.isEqual(this.conf.peerJWSKeys, newConf.peerJWSKeys);
154
- const isJwsSigningKeyDifferent = !_.isEqual(this.conf.jwsSigningKey, newConf.jwsSigningKey);
155
-
156
- if (isInboundDifferent) {
157
- this.logger.debug('Inbound config is different', {
158
- oldInbound: this.conf.inbound,
159
- newInbound: newConf.inbound
160
- });
161
- }
162
- if (isOutboundDifferent) {
163
- this.logger.debug('Outbound config is different (checked in inbound update)', {
164
- oldOutbound: this.conf.outbound,
165
- newOutbound: newConf.outbound
166
- });
167
- }
168
-
169
- if (isPeerJWSKeysDifferent) {
170
- this.logger.debug('Peer JWS Keys config is different', {
171
- oldPeerJWSKeys: this.conf.peerJWSKeys,
172
- newPeerJWSKeys: newConf.peerJWSKeys
173
- });
174
- }
175
-
176
- if (isJwsSigningKeyDifferent) {
177
- this.logger.debug('JWS Signing Key config is different', {
178
- oldJwsSigningKey: this.conf.jwsSigningKey,
179
- newJwsSigningKey: newConf.jwsSigningKey
180
- });
181
- }
182
-
183
- return isInboundDifferent || isOutboundDifferent || isPeerJWSKeysDifferent || isJwsSigningKeyDifferent;
184
- }
185
-
186
- _shouldUpdateOutboundServer(newConf) {
187
- const isOutboundDifferent = !_.isEqual(this.conf.outbound, newConf.outbound);
188
- const isJwsSigningKeyDifferent = !_.isEqual(this.conf.jwsSigningKey, newConf.jwsSigningKey);
189
-
190
- if (isOutboundDifferent) {
191
- this.logger.debug('Outbound config is different', {
192
- oldOutbound: this.conf.outbound,
193
- newOutbound: newConf.outbound
194
- });
195
- }
196
-
197
- if (isJwsSigningKeyDifferent) {
198
- this.logger.debug('JWS Signing Key config is different', {
199
- oldJwsSigningKey: this.conf.jwsSigningKey,
200
- newJwsSigningKey: newConf.jwsSigningKey
201
- });
202
- }
203
-
204
- return isOutboundDifferent || isJwsSigningKeyDifferent;
205
- }
206
-
207
- /**
208
- * Starts periodic polling of Management API for configuration updates.
209
- * Only runs if PM4ML enabled and a polling interval configured.
210
- */
211
- _startConfigPolling() {
212
- if (!this.conf.pm4mlEnabled || !this.conf.control.mgmtAPIPollIntervalMs) {
213
- this.logger.info('No failsafe config polling configured');
214
- return;
215
- }
216
-
217
- this.logger.info('starting failsafe config polling from Management API...', { intervalMs: this.conf.control.mgmtAPIPollIntervalMs });
218
-
219
- this._configPollInterval = setInterval(
220
- () => this._pollConfigFromMgmtAPI(),
221
- this.conf.control.mgmtAPIPollIntervalMs
222
- );
223
-
224
- // Unref so it doesn't prevent process exit
225
- this._configPollInterval.unref();
226
- }
227
-
228
- /**
229
- * Polls Management API for configuration updates.
230
- * Reuses the existing persistent WebSocket client (this.controlClient).
231
- * Skips polling if:
232
- * - Another config update is in progress
233
- * - WebSocket client is not connected
234
- */
235
- async _pollConfigFromMgmtAPI() {
236
- // Race condition prevention: skip if restart in progress
237
- if (this._configUpdateInProgress) {
238
- this.logger.info('config updating already in progress, skipping poll');
239
- return;
240
- }
241
-
242
- // WebSocket readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
243
- if (this.controlClient?.readyState !== 1) {
244
- this.logger.warn('Control client not ready (not OPEN), skipping poll', { readyState: this.controlClient?.readyState });
245
- return;
246
- }
247
-
248
- try {
249
- const newConfig = await this.controlClient.getUpdatedConfig();
250
- if (!newConfig) {
251
- this.logger.warn('No config received from polling');
252
- return;
253
- }
254
- this.logger.info('polling config from mgmt-api is done, checking if SDK server restart needed...');
255
-
256
- const mergedConfig = _.merge({}, this.conf, newConfig);
257
- await this.restart(mergedConfig, { source: 'polling' });
258
- } catch (err) {
259
- this.logger.error('error in polling config from Management API: ', err);
260
- }
261
- }
262
-
263
- /** Stops the config polling interval. */
264
- _stopConfigPolling() {
265
- if (this._configPollInterval) {
266
- this.logger.verbose('stopping config polling');
267
- clearInterval(this._configPollInterval);
268
- this._configPollInterval = null;
269
- }
270
- }
271
-
272
- async start() {
273
- await this.cache.connect();
274
- await this.oidc.auth.start();
275
-
276
- // We only start the control client if we're running within Mojaloop Payment Manager.
277
- // The control server is the Payment Manager Management API Service.
278
- // We only start the client to connect to and listen to the Management API service for
279
- // management protocol messages e.g configuration changes, certificate updates etc.
280
- if (this.conf.pm4mlEnabled) {
281
- const RESTART_INTERVAL_MS = 10000;
282
- this.controlClient = await ControlAgent.createConnectedControlAgentWs(this.conf, this.logger);
283
- this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
284
-
285
- const schedulePing = () => {
286
- clearTimeout(this.pingTimeout);
287
- this.pingTimeout = setTimeout(() => {
288
- this.logger.error('Ping timeout, possible broken connection. Restarting server...');
289
- this.restart(_.merge({}, this.conf, {
290
- control: { stopped: Date.now() }
291
- }));
292
- }, PING_INTERVAL_MS + this.conf.control.mgmtAPILatencyAssumption);
293
- };
294
-
295
- this.controlClient.on('ping', () => {
296
- this.logger.debug('Received ping from control server');
297
- schedulePing();
298
- });
299
-
300
- this.controlClient.on('close', () => {
301
- clearTimeout(this.pingTimeout);
302
- setTimeout(() => {
303
- this.logger.debug('Control client closed. Restarting server...');
304
- this.restart(_.merge({}, this.conf, {
305
- control: { stopped: Date.now() }
306
- }));
307
- }, RESTART_INTERVAL_MS);
308
- });
309
-
310
- schedulePing();
311
- this._startConfigPolling();
312
- }
313
-
314
- await Promise.all([
315
- this.inboundServer.start(),
316
- this.outboundServer.start(),
317
- this.metricsServer.start(),
318
- this.testServer?.start(),
319
- this.oauthTestServer?.start(),
320
- this.backendEventHandler?.start(),
321
- this.fspiopEventHandler?.start(),
322
- ]);
323
- }
324
-
325
- async restart(newConf, options = {}) {
326
- const source = options.source || 'websocket'; // Track source of restart call - websocket or polling
327
-
328
- // Race condition prevention
329
- if (this._configUpdateInProgress) {
330
- this.logger.info('restart already in progress, skipping', { source });
331
- return;
332
- }
333
-
334
- const restartActionsTaken = {};
335
- this.logger.debug('Server is restarting...', { source });
336
- this._configUpdateInProgress = true;
337
-
338
- try {
339
- let oldCache;
340
- const updateCache = !_.isEqual(this.conf.cacheUrl, newConf.cacheUrl)
341
- || !_.isEqual(this.conf.enableTestFeatures, newConf.enableTestFeatures);
342
- if (updateCache) {
343
- oldCache = this.cache;
344
- await this.cache.disconnect();
345
- this.cache = createCache(newConf);
346
- await this.cache.connect();
347
- restartActionsTaken.updateCache = true;
348
- }
349
-
350
- const updateOIDC = !_.isEqual(this.conf.oidc, newConf.oidc)
351
- || !_.isEqual(this.conf.outbound.tls, newConf.outbound.tls);
352
- if (updateOIDC) {
353
- this.oidc.auth.stop();
354
- this.oidc = createAuthClient(newConf, this.logger);
355
- this.oidc.auth.on('error', (msg) => {
356
- this.emit('error', 'OIDC auth error in InboundApi', msg);
357
- });
358
- await this.oidc.auth.start();
359
- restartActionsTaken.updateOIDC = true;
360
- }
361
-
362
- this.logger.isDebugEnabled && this.logger.push({ oldConf: this.conf.inbound, newConf: newConf.inbound }).debug('Inbound server configuration');
363
- const updateInboundServer = this._shouldUpdateInboundServer(newConf);
364
- if (updateInboundServer) {
365
- const stopStartLabel = 'InboundServer stop/start duration';
366
- // eslint-disable-next-line no-console
367
- console.time(stopStartLabel); // todo: remove console.time
368
- await this.inboundServer.stop();
369
-
370
- this.mojaloopSharedAgents = this._createMojaloopSharedAgents(newConf);
371
- this.inboundServer = new InboundServer(
372
- newConf,
373
- this.logger,
374
- this.cache,
375
- this.oidc,
376
- this.mojaloopSharedAgents,
377
- );
378
- this.inboundServer.on('error', (...args) => {
379
- const errMessage = 'Unhandled error in Inbound Server';
380
- this.logger.push({ args }).error(errMessage);
381
- this.emit('error', errMessage);
382
- });
383
- await this.inboundServer.start();
384
- // eslint-disable-next-line no-console
385
- console.timeEnd(stopStartLabel);
386
- restartActionsTaken.updateInboundServer = true;
387
- }
388
-
389
- this.logger.isDebugEnabled && this.logger.push({ oldConf: this.conf.outbound, newConf: newConf.outbound }).debug('Outbound server configuration');
390
- const updateOutboundServer = this._shouldUpdateOutboundServer(newConf);
391
- if (updateOutboundServer) {
392
- const stopStartLabel = 'OutboundServer stop/start duration';
393
- // eslint-disable-next-line no-console
394
- console.time(stopStartLabel);
395
- await this.outboundServer.stop();
396
-
397
- this.mojaloopSharedAgents = this._createMojaloopSharedAgents(newConf);
398
- this.outboundServer = new OutboundServer(
399
- newConf,
400
- this.logger,
401
- this.cache,
402
- this.metricsClient,
403
- this.oidc,
404
- this.mojaloopSharedAgents,
405
- );
406
- this.outboundServer.on('error', (...args) => {
407
- const errMessage = 'Unhandled error in Outbound Server';
408
- this.logger.push({ args }).error(errMessage);
409
- this.emit('error', errMessage);
410
- });
411
- await this.outboundServer.start();
412
- // eslint-disable-next-line no-console
413
- console.timeEnd(stopStartLabel);
414
- restartActionsTaken.updateOutboundServer = true;
415
- }
416
-
417
- const updateFspiopEventHandler = !_.isEqual(this.conf.outbound, newConf.outbound)
418
- && this.conf.fspiopEventHandler.enabled;
419
- if (updateFspiopEventHandler) {
420
- await this.fspiopEventHandler.stop();
421
- this.fspiopEventHandler = new FSPIOPEventHandler({
422
- config: newConf,
423
- logger: this.logger,
424
- cache: this.cache,
425
- oidc: this.oidc,
426
- });
427
- await this.fspiopEventHandler.start();
428
- restartActionsTaken.updateFspiopEventHandler = true;
429
- }
430
-
431
- const updateControlClient = !_.isEqual(this.conf.control, newConf.control);
432
- if (updateControlClient) {
433
- await this.controlClient?.stop();
434
- if (this.conf.pm4mlEnabled) {
435
- const RESTART_INTERVAL_MS = 10000;
436
-
437
- const schedulePing = () => {
438
- clearTimeout(this.pingTimeout);
439
- this.pingTimeout = setTimeout(() => {
440
- this.logger.error('Ping timeout, possible broken connection. Restarting server...');
441
- this.restart(_.merge({}, newConf, {
442
- control: { stopped: Date.now() }
443
- }));
444
- }, PING_INTERVAL_MS + this.conf.control.mgmtAPILatencyAssumption);
445
- };
446
-
447
- schedulePing();
448
-
449
- this.controlClient = await ControlAgent.createConnectedControlAgentWs(newConf, this.logger);
450
- this.controlClient.on(ControlAgent.EVENT.RECONFIGURE, this.restart.bind(this));
451
-
452
- this.controlClient.on('ping', () => {
453
- this.logger.debug('Received ping from control server');
454
- schedulePing();
455
- });
456
-
457
- this.controlClient.on('close', () => {
458
- clearTimeout(this.pingTimeout);
459
- setTimeout(() => {
460
- this.logger.debug('Control client closed. Restarting server...');
461
- this.restart(_.merge({}, newConf, {
462
- control: { stopped: Date.now() }
463
- }));
464
- }, RESTART_INTERVAL_MS);
465
- });
466
-
467
- restartActionsTaken.updateControlClient = true;
468
- }
469
- }
470
-
471
- const updateOAuthTestServer = !_.isEqual(newConf.oauthTestServer, this.conf.oauthTestServer);
472
- if (updateOAuthTestServer) {
473
- await this.oauthTestServer?.stop();
474
- if (this.conf.oauthTestServer.enabled) {
475
- this.oauthTestServer = new OAuthTestServer({
476
- clientKey: newConf.oauthTestServer.clientKey,
477
- clientSecret: newConf.oauthTestServer.clientSecret,
478
- port: newConf.oauthTestServer.listenPort,
479
- logger: this.logger,
480
- });
481
- await this.oauthTestServer.start();
482
- restartActionsTaken.updateOAuthTestServer = true;
483
- }
484
- }
485
-
486
- const updateTestServer = !_.isEqual(newConf.test.port, this.conf.test.port);
487
- if (updateTestServer) {
488
- await this.testServer?.stop();
489
- if (this.conf.enableTestFeatures) {
490
- this.testServer = new TestServer({
491
- port: newConf.test.port,
492
- logger: this.logger,
493
- cache: this.cache,
494
- });
495
- await this.testServer.start();
496
- restartActionsTaken.updateTestServer = true;
497
- }
498
- }
499
-
500
- this.conf = newConf;
501
-
502
- await oldCache?.disconnect();
503
-
504
- if (Object.keys(restartActionsTaken).length > 0) {
505
- this.logger.info('Server is restarted', { restartActionsTaken, source });
506
- } else {
507
- this.logger.verbose('Server not restarted, no config changes detected', { source });
508
- }
509
- } catch (err) {
510
- this.logger.error('error in Server restart: ', err);
511
- } finally {
512
- this._configUpdateInProgress = false;
513
- }
514
- }
515
-
516
- stop() {
517
- this._stopConfigPolling();
518
-
519
- clearTimeout(this.pingTimeout);
520
- this.oidc.auth.stop();
521
- this.controlClient?.removeAllListeners();
522
- this.inboundServer.removeAllListeners();
523
- return Promise.all([
524
- this.cache.disconnect(),
525
- this.inboundServer.stop(),
526
- this.outboundServer.stop(),
527
- this.metricsServer.stop(),
528
- this.oauthTestServer?.stop(),
529
- this.testServer?.stop(),
530
- this.controlClient?.stop(),
531
- this.backendEventHandler?.stop(),
532
- this.fspiopEventHandler?.stop(),
533
- ]);
534
- }
535
-
536
- _createMojaloopSharedAgents(conf) {
537
- const httpAgent = new http.Agent({
538
- keepAlive: true,
539
- maxSockets: conf.outbound?.maxSockets || 256,
540
- });
541
-
542
- // Create HTTPS agent based on TLS configuration for Mojaloop switch communication
543
- const httpsAgentOptions = {
544
- keepAlive: true,
545
- maxSockets: conf.outbound?.maxSockets || 256,
546
- };
547
-
548
- // Apply TLS configuration if mTLS is enabled for switch communication
549
- if (conf.outbound?.tls?.mutualTLS?.enabled && conf.outbound?.tls?.creds) {
550
- Object.assign(httpsAgentOptions, conf.outbound.tls.creds);
551
- }
552
-
553
- const httpsAgent = new https.Agent(httpsAgentOptions);
554
-
555
- // Prevent accidental logging of agent internals
556
- httpAgent.toJSON = () => ({ type: 'HttpAgent', keepAlive: httpAgent.keepAlive });
557
- httpsAgent.toJSON = () => ({ type: 'HttpsAgent', keepAlive: httpsAgent.keepAlive });
558
-
559
- this.logger.isInfoEnabled && this.logger.info('Created shared HTTP and HTTPS agents for Mojaloop switch communication');
560
-
561
- return {
562
- httpAgent,
563
- httpsAgent
564
- };
565
- }
566
- }
567
-
568
- async function start(config) {
569
- if (config.pm4mlEnabled) {
570
- const controlClient = await ControlAgent.createConnectedControlAgentWs(config, logger);
47
+ async function start(conf) {
48
+ if (conf.pm4mlEnabled) {
49
+ const controlClient = await ControlAgent.createConnectedControlAgentWs(conf, logger);
571
50
  const updatedConfigFromMgmtAPI = await controlClient.getUpdatedConfig();
572
- _.merge(config, updatedConfigFromMgmtAPI);
51
+ merge(conf, updatedConfigFromMgmtAPI);
573
52
  controlClient.terminate();
574
53
  // todo: - clarify, why do we need to terminate the client? (use .stop() method?)
575
54
  // - can we use persistent ws controlClient from Server? (why do we need to establish a brand new ws connection here?)
576
55
  }
577
56
 
578
- const svr = new Server(config, logger);
57
+ const svr = new SdkServer(conf, logger);
579
58
  svr.on('error', (err) => {
580
59
  logger.error('Unhandled server error: ', err);
581
60
  process.exit(2);
@@ -611,7 +90,7 @@ module.exports = {
611
90
  InboundServerMiddleware,
612
91
  OutboundServerMiddleware,
613
92
  Router,
614
- Server,
93
+ Server: SdkServer,
615
94
  Validate,
616
95
  SDKStateEnum,
617
96
  start,
@@ -36,6 +36,7 @@ const dto = require('../dto');
36
36
  const shared = require('./lib/shared');
37
37
  const { BackendRequests, HTTPResponseError } = require('./lib/requests');
38
38
  const { SDKStateEnum, CacheKeyPrefixes } = require('./common');
39
+ const { extractStatusCodeFromError } = require('../utils');
39
40
 
40
41
  const TRACESTATE_KEY_CALLBACK_START_TS = 'tx_callback_start_ts';
41
42
 
@@ -1075,6 +1076,13 @@ class InboundTransfersModel {
1075
1076
  log.verbose(`putFxTransfersNotification attempt ${currentAttempt} succeeded for conversionId ${conversionId}`);
1076
1077
  resolve();
1077
1078
  } catch (err) {
1079
+ // Don't retry on 4xx errors
1080
+ let status = extractStatusCodeFromError(err);
1081
+ if (status && Number(status) >= 400 && Number(status) < 500) {
1082
+ log.warn(`putFxTransfersNotification attempt ${currentAttempt} got 4xx error (${status}), not retrying`, err);
1083
+ resolve();
1084
+ return;
1085
+ }
1078
1086
  log.warn(`putFxTransfersNotification attempt ${currentAttempt} threw error, retrying...`, err);
1079
1087
  if (!operation.retry(err)) {
1080
1088
  log.verbose(`putFxTransfersNotification giving up after ${currentAttempt} attempts`);
@@ -1159,6 +1167,13 @@ class InboundTransfersModel {
1159
1167
  log.verbose(`putTransfersNotification attempt ${currentAttempt} succeeded for transferId ${transferId}`);
1160
1168
  resolve();
1161
1169
  } catch (err) {
1170
+ // Don't retry on 4xx errors
1171
+ let status = extractStatusCodeFromError(err);
1172
+ if (status && Number(status) >= 400 && Number(status) < 500) {
1173
+ log.warn(`putTransfersNotification attempt ${currentAttempt} got 4xx error (${status}), not retrying`, err);
1174
+ resolve();
1175
+ return;
1176
+ }
1162
1177
  this._logger.warn(`putTransfersNotification attempt ${currentAttempt} threw error, retrying...`, err);
1163
1178
  if (!operation.retry(err)) {
1164
1179
  log.verbose(`putTransfersNotification giving up after ${currentAttempt} attempts`);
@@ -92,9 +92,28 @@ const generateTraceparent = (traceId = randomBytes(16).toString('hex'), traceFla
92
92
  return `00-${traceId}-${spanId}-${traceFlags.toLowerCase()}`;
93
93
  };
94
94
 
95
+ /** Extracts the HTTP status code from various error object structures
96
+ * @param {object} err - The error object to extract the status code from
97
+ * @returns {number|undefined} - The extracted status code, or undefined if not found
98
+ */
99
+ const extractStatusCodeFromError = (err) => {
100
+ let status = err?.response?.status || err?.statusCode;
101
+ if (!status && err?.response?.statusCode) {
102
+ status = err.response.statusCode;
103
+ }
104
+ if (!status && err?.status) {
105
+ status = err.status;
106
+ }
107
+ if (!status && err?.output?.statusCode) {
108
+ status = err.output.statusCode;
109
+ }
110
+ return status;
111
+ };
112
+
95
113
  module.exports = {
96
114
  createAuthClient,
97
115
  generateTraceparent,
98
116
  isValidTraceFlags,
99
- transformHeadersIsoToFspiop
117
+ transformHeadersIsoToFspiop,
118
+ extractStatusCodeFromError
100
119
  };