@mojaloop/sdk-scheme-adapter 24.15.0 → 24.15.1
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/CHANGELOG.md +8 -0
- package/CLAUDE.md +2 -0
- package/modules/api-svc/src/SdkServer.js +560 -0
- package/modules/api-svc/src/index.js +8 -529
- package/modules/api-svc/test/unit/{index.configPolling.test.js → SdkServer.configPolling.test.js} +11 -11
- package/package.json +1 -1
- package/{sbom-v24.14.0.csv → sbom-v24.15.0.csv} +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# Changelog: [mojaloop/sdk-scheme-adapter](https://github.com/mojaloop/sdk-scheme-adapter)
|
|
2
|
+
### [24.15.1](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.15.0...v24.15.1) (2025-10-08)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
### Chore
|
|
6
|
+
|
|
7
|
+
* 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))
|
|
8
|
+
* **sbom:** update sbom [skip ci] ([b2cf810](https://github.com/mojaloop/sdk-scheme-adapter/commit/b2cf8101be59023e3be7a5da5510eff26c0b7648))
|
|
9
|
+
|
|
2
10
|
## [24.15.0](https://github.com/mojaloop/sdk-scheme-adapter/compare/v24.14.0...v24.15.0) (2025-10-08)
|
|
3
11
|
|
|
4
12
|
|
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`
|
|
@@ -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;
|
|
@@ -27,20 +27,11 @@
|
|
|
27
27
|
'use strict';
|
|
28
28
|
|
|
29
29
|
const { hostname } = require('node:os');
|
|
30
|
-
const
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
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
|
-
|
|
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
|
|
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,
|
package/modules/api-svc/test/unit/{index.configPolling.test.js → SdkServer.configPolling.test.js}
RENAMED
|
@@ -35,8 +35,8 @@ jest.mock('~/lib/cache');
|
|
|
35
35
|
jest.mock('~/ControlAgent');
|
|
36
36
|
|
|
37
37
|
const promClient = require('prom-client');
|
|
38
|
+
const SdkServer = require('../../src/SdkServer');
|
|
38
39
|
const ControlAgent = require('~/ControlAgent');
|
|
39
|
-
const { Server } = require('~/index');
|
|
40
40
|
const { logger } = require('~/lib/logger');
|
|
41
41
|
const testConfig = require('./data/defaultConfig.json');
|
|
42
42
|
|
|
@@ -92,7 +92,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
92
92
|
}
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
server = new
|
|
95
|
+
server = new SdkServer(config, logger);
|
|
96
96
|
await server.start();
|
|
97
97
|
|
|
98
98
|
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 2);
|
|
@@ -105,7 +105,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
105
105
|
pm4mlEnabled: false,
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
server = new
|
|
108
|
+
server = new SdkServer(config, logger);
|
|
109
109
|
await server.start();
|
|
110
110
|
|
|
111
111
|
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 2);
|
|
@@ -113,7 +113,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('should poll at configured interval', async () => {
|
|
116
|
-
server = new
|
|
116
|
+
server = new SdkServer(mockConfig, logger);
|
|
117
117
|
await server.start();
|
|
118
118
|
expect(mockControlAgent.getUpdatedConfig).toHaveBeenCalledTimes(0);
|
|
119
119
|
|
|
@@ -130,7 +130,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
130
130
|
jwsSigningKey: newJwsKey
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
server = new
|
|
133
|
+
server = new SdkServer(mockConfig, logger);
|
|
134
134
|
const restartSpy = jest.spyOn(server, 'restart');
|
|
135
135
|
|
|
136
136
|
await server.start();
|
|
@@ -150,7 +150,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
150
150
|
it('should handle unchanged config efficiently (no-op)', async () => {
|
|
151
151
|
mockControlAgent.getUpdatedConfig.mockResolvedValue({});
|
|
152
152
|
|
|
153
|
-
server = new
|
|
153
|
+
server = new SdkServer(mockConfig, logger);
|
|
154
154
|
const restartSpy = jest.spyOn(server, 'restart');
|
|
155
155
|
await server.start();
|
|
156
156
|
|
|
@@ -165,7 +165,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
165
165
|
it('should prevent race condition when update already in progress', async () => {
|
|
166
166
|
mockControlAgent.getUpdatedConfig.mockResolvedValue({ jwsSigningKey: 'new-key' });
|
|
167
167
|
|
|
168
|
-
server = new
|
|
168
|
+
server = new SdkServer(mockConfig, logger);
|
|
169
169
|
await server.start();
|
|
170
170
|
// Simulate restart in progress
|
|
171
171
|
server._configUpdateInProgress = true;
|
|
@@ -176,7 +176,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
it('should skip polling when WebSocket not OPEN', async () => {
|
|
179
|
-
server = new
|
|
179
|
+
server = new SdkServer(mockConfig, logger);
|
|
180
180
|
await server.start();
|
|
181
181
|
|
|
182
182
|
mockControlAgent.getUpdatedConfig.mockClear();
|
|
@@ -190,7 +190,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
190
190
|
it('should handle network errors gracefully during polling', async () => {
|
|
191
191
|
mockControlAgent.getUpdatedConfig.mockRejectedValue(new Error('Connection refused'));
|
|
192
192
|
|
|
193
|
-
server = new
|
|
193
|
+
server = new SdkServer(mockConfig, logger);
|
|
194
194
|
await server.start();
|
|
195
195
|
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
196
196
|
|
|
@@ -204,7 +204,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
204
204
|
it('should handle missing config response', async () => {
|
|
205
205
|
mockControlAgent.getUpdatedConfig.mockResolvedValue(null);
|
|
206
206
|
|
|
207
|
-
server = new
|
|
207
|
+
server = new SdkServer(mockConfig, logger);
|
|
208
208
|
await server.start();
|
|
209
209
|
await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
|
|
210
210
|
|
|
@@ -214,7 +214,7 @@ describe('Config Polling Tests -->', () => {
|
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it('should stop polling when server stops', async () => {
|
|
217
|
-
server = new
|
|
217
|
+
server = new SdkServer(mockConfig, logger);
|
|
218
218
|
await server.start();
|
|
219
219
|
|
|
220
220
|
expect(server._configPollInterval).toBeDefined();
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ type,bom_ref,license-id,group,author,name,version,purl,path,description,vcs-url,
|
|
|
2
2
|
application,,,,,yarn,4.10.3,,,,,,,,,,,,,,,,,
|
|
3
3
|
library,,Apache-2.0,@cyclonedx,Jan Kowalleck,yarn-plugin-cyclonedx,3.0.3,,,Create CycloneDX Software Bill of Materials (SBOM) from yarn projects.,git+https://github.com/CycloneDX/cyclonedx-node-yarn.git,"as detected from PackageJson property ""repository.url""",https://github.com/CycloneDX/cyclonedx-node-yarn#readme,"as detected from PackageJson property ""homepage""",https://github.com/CycloneDX/cyclonedx-node-yarn/issues,"as detected from PackageJson property ""bugs.url""",,,,,,,,
|
|
4
4
|
library,,Apache-2.0,@cyclonedx,,cyclonedx-library,8.3.0,,,,,,https://github.com/CycloneDX/cyclonedx-javascript-library#readme,"as detected from PackageJson property ""homepage""",,,,,,,,,,
|
|
5
|
-
application,@mojaloop/sdk-scheme-adapter@workspace:.,Apache-2.0,@mojaloop,,sdk-scheme-adapter,24.
|
|
5
|
+
application,@mojaloop/sdk-scheme-adapter@workspace:.,Apache-2.0,@mojaloop,,sdk-scheme-adapter,24.15.0,pkg:npm/%40mojaloop/sdk-scheme-adapter@24.15.0,,mojaloop sdk-scheme-adapter,,,https://github.com/mojaloop/sdk-scheme-adapter,"as detected from PackageJson property ""homepage""",https://github.com/mojaloop/sdk-scheme-adapter/issues#readme,"as detected from PackageJson property ""bugs.url""",,,,,,active,Active in npm registry,2025-10-08T10:31:13.766Z
|
|
6
6
|
library,@types/jest@30.0.0,MIT,@types,,jest,30.0.0,pkg:npm/%40types/jest@30.0.0?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2FDefinitelyTyped%2FDefinitelyTyped.git#types/jest,,TypeScript definitions for jest,git+https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/jest,"as detected from PackageJson property ""repository.url"" and ""repository.directory""",https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest,"as detected from PackageJson property ""homepage""",https://github.com/DefinitelyTyped/DefinitelyTyped/issues,"as detected from PackageJson property ""bugs.url""",,,,,,active,Active in npm registry,2025-08-03T07:02:46.002Z
|
|
7
7
|
library,@types/node-cache@4.2.5,MIT,@types,,node-cache,4.2.5,pkg:npm/%40types/node-cache@4.2.5?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fmpneuried%2Fnodecache.git,,Stub TypeScript definitions entry for node-cache~ which provides its own types definitions,git+https://github.com/mpneuried/nodecache.git,"as detected from PackageJson property ""repository.url""",https://github.com/mpneuried/nodecache#readme,"as detected from PackageJson property ""homepage""",https://github.com/mpneuried/nodecache/issues,"as detected from PackageJson property ""bugs.url""",,,,,,deprecated,"This is a stub types definition. node-cache provides its own type definitions, so you do not need this installed.",2022-06-13T01:13:56.807Z
|
|
8
8
|
library,@types/node@24.7.0,MIT,@types,,node,24.7.0,pkg:npm/%40types/node@24.7.0?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2FDefinitelyTyped%2FDefinitelyTyped.git#types/node,,TypeScript definitions for node,git+https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/node,"as detected from PackageJson property ""repository.url"" and ""repository.directory""",https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node,"as detected from PackageJson property ""homepage""",https://github.com/DefinitelyTyped/DefinitelyTyped/issues,"as detected from PackageJson property ""bugs.url""",,,,,,active,Active in npm registry,2025-10-06T07:33:46.739Z
|