@mojaloop/sdk-scheme-adapter 12.0.2 → 12.1.0

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/.env.example CHANGED
@@ -11,6 +11,7 @@ TEST_LISTEN_PORT=4002
11
11
  # environment, i.e. when you're running it locally against your own implementation.
12
12
  INBOUND_MUTUAL_TLS_ENABLED=false
13
13
  OUTBOUND_MUTUAL_TLS_ENABLED=false
14
+ TEST_MUTUAL_TLS_ENABLED=false
14
15
 
15
16
  # Enable verification or incoming JWS signatures
16
17
  # Note that signatures will be required on incoming messages
@@ -42,6 +43,10 @@ OUT_CA_CERT_PATH=./secrets/cacert.pem
42
43
  OUT_CLIENT_CERT_PATH=./secrets/servercert.pem
43
44
  OUT_CLIENT_KEY_PATH=./secrets/serverkey.pem
44
45
 
46
+ TEST_CA_CERT_PATH=./secrets/cacert.pem
47
+ TEST_CLIENT_CERT_PATH=./secrets/servercert.pem
48
+ TEST_CLIENT_KEY_PATH=./secrets/serverkey.pem
49
+
45
50
  # The number of space characters by which to indent pretty-printed logs. If set to zero, log events
46
51
  # will each be printed on a single line.
47
52
  LOG_INDENT=0
@@ -138,3 +143,11 @@ RESERVE_NOTIFICATION=true
138
143
 
139
144
  # resources API versions should be string in format: "resouceOneName=1.0,resourceTwoName=1.1"
140
145
  RESOURCE_VERSIONS="transfers=1.1,participants=1.1"
146
+
147
+ # Management API websocket connection settings.
148
+ # The Management API uses this for exchanging connector management messages.
149
+ MGMT_API_WS_URL=127.0.0.1
150
+ MGMT_API_WS_PORT=4005
151
+ # Set to true to enable the use of PM4ML-related services e.g MCM, Management API service
152
+ # when running the scheme-adapter as a mojaloop connector component within Payment Manager for Mojaloop.
153
+ PM4ML_ENABLED=false
package/CHANGELOG.md CHANGED
@@ -1,4 +1,19 @@
1
1
  # Changelog: [mojaloop/thirdparty-api-svc](https://github.com/mojaloop/thirdparty-api-svc)
2
+ ## [12.1.0](https://github.com/mojaloop/sdk-scheme-adapter/compare/v12.0.2...v12.1.0) (2022-04-21)
3
+
4
+
5
+ ### Features
6
+
7
+ * port control client and service from ml connector ([#308](https://github.com/mojaloop/sdk-scheme-adapter/issues/308)) ([e6c963c](https://github.com/mojaloop/sdk-scheme-adapter/commit/e6c963c5e5faa17f6a39e0b70f34c3e3717ba090))
8
+
9
+
10
+ ### Chore
11
+
12
+ * **deps:** bump async from 2.6.3 to 2.6.4 ([#305](https://github.com/mojaloop/sdk-scheme-adapter/issues/305)) ([a20d7fd](https://github.com/mojaloop/sdk-scheme-adapter/commit/a20d7fd0c324fcac948b30e5521fd798e387c6d3))
13
+ * **deps:** bump minimist from 1.2.5 to 1.2.6 ([#307](https://github.com/mojaloop/sdk-scheme-adapter/issues/307)) ([cf33fdc](https://github.com/mojaloop/sdk-scheme-adapter/commit/cf33fdc7691d29f07e36ea2460c2686333e5f449))
14
+ * **deps:** bump trim-off-newlines from 1.0.1 to 1.0.3 ([#306](https://github.com/mojaloop/sdk-scheme-adapter/issues/306)) ([086bee6](https://github.com/mojaloop/sdk-scheme-adapter/commit/086bee692f6c5ab12c7e3bcdd6ff8688d26ff69d))
15
+ * **deps:** bump urijs from 1.19.10 to 1.19.11 ([#304](https://github.com/mojaloop/sdk-scheme-adapter/issues/304)) ([17aebdc](https://github.com/mojaloop/sdk-scheme-adapter/commit/17aebdcae89169540a32d3ae61f7dadab24868c1))
16
+
2
17
  ### [12.0.2](https://github.com/mojaloop/sdk-scheme-adapter/compare/v12.0.1...v12.0.2) (2022-04-19)
3
18
 
4
19
 
@@ -414,6 +414,26 @@
414
414
  "decision": "ignore",
415
415
  "madeAt": 1649898257344,
416
416
  "expiresAt": 1652490250295
417
+ },
418
+ "1070030|@mojaloop/central-services-shared>widdershins>markdown-it": {
419
+ "decision": "ignore",
420
+ "madeAt": 1650460940438,
421
+ "expiresAt": 1653052932045
422
+ },
423
+ "1070030|@mojaloop/central-services-shared>shins>markdown-it": {
424
+ "decision": "ignore",
425
+ "madeAt": 1650459472663,
426
+ "expiresAt": 1653051469252
427
+ },
428
+ "1068154|@mojaloop/central-services-shared>shins>sanitize-html": {
429
+ "decision": "ignore",
430
+ "madeAt": 1650459474362,
431
+ "expiresAt": 1653051469252
432
+ },
433
+ "1068155|@mojaloop/central-services-shared>shins>sanitize-html": {
434
+ "decision": "ignore",
435
+ "madeAt": 1650459475376,
436
+ "expiresAt": 1653051469252
417
437
  }
418
438
  },
419
439
  "rules": {},
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@mojaloop/sdk-scheme-adapter",
3
- "version": "12.0.2",
3
+ "version": "12.1.0",
4
4
  "description": "An adapter for connecting to Mojaloop API enabled switches.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "engines": {
8
8
  "node": "=16.x"
9
9
  },
10
+ "_moduleAliases": {
11
+ "~": "src"
12
+ },
10
13
  "scripts": {
11
14
  "audit:resolve": "SHELL=sh resolve-audit --production",
12
15
  "audit:check": "SHELL=sh check-audit --production",
@@ -65,6 +68,7 @@
65
68
  "dotenv": "^10.0.0",
66
69
  "env-var": "^7.0.1",
67
70
  "express": "^4.17.2",
71
+ "fast-json-patch": "^3.1.1",
68
72
  "javascript-state-machine": "^3.1.0",
69
73
  "js-yaml": "^4.1.0",
70
74
  "json-schema-ref-parser": "^9.0.9",
@@ -0,0 +1,221 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2020 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * Matt Kingston - matt.kingston@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ // This server has deliberately been written separate from any other server in the SDK. There is
12
+ // some reasonable argument that it could be part of the outbound or test server. It has not been
13
+ // incorporated in either as, at the time of writing, it is intended to be maintained in a
14
+ // proprietary fork. Therefore, keeping it independent of other servers will avoid the maintenance
15
+ // burden that would otherwise be associated with incorporating it with those.
16
+ //
17
+ // It inherits from the Server class from the 'ws' websocket library for Node, which in turn
18
+ // inherits from EventEmitter. We exploit this to emit an event when a reconfigure message is sent
19
+ // to this server. Then, when this server's reconfigure method is called, it reconfigures itself
20
+ // and sends a message to all clients notifying them of the new application configuration.
21
+ //
22
+ // It expects new configuration to be supplied as an array of JSON patches. It therefore exposes
23
+ // the current configuration to
24
+
25
+ const assert = require('assert').strict;
26
+ const ws = require('ws');
27
+ const jsonPatch = require('fast-json-patch');
28
+ const randomPhrase = require('~/lib/randomphrase');
29
+
30
+
31
+ /**************************************************************************
32
+ * The message protocol messages, verbs, and errors
33
+ *************************************************************************/
34
+ const MESSAGE = {
35
+ CONFIGURATION: 'CONFIGURATION',
36
+ ERROR: 'ERROR',
37
+ };
38
+
39
+ const VERB = {
40
+ READ: 'READ',
41
+ NOTIFY: 'NOTIFY',
42
+ PATCH: 'PATCH'
43
+ };
44
+
45
+ const ERROR = {
46
+ UNSUPPORTED_MESSAGE: 'UNSUPPORTED_MESSAGE',
47
+ UNSUPPORTED_VERB: 'UNSUPPORTED_VERB',
48
+ JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
49
+ };
50
+
51
+ /**************************************************************************
52
+ * Events emitted by the control client
53
+ *************************************************************************/
54
+ const EVENT = {
55
+ RECONFIGURE: 'RECONFIGURE',
56
+ };
57
+
58
+ /**************************************************************************
59
+ * Private convenience functions
60
+ *************************************************************************/
61
+ const serialise = JSON.stringify;
62
+ const deserialise = (msg) => {
63
+ //reviver function
64
+ return JSON.parse(msg.toString(), (k, v) => {
65
+ if (
66
+ v !== null &&
67
+ typeof v === 'object' &&
68
+ 'type' in v &&
69
+ v.type === 'Buffer' &&
70
+ 'data' in v &&
71
+ Array.isArray(v.data)) {
72
+ return new Buffer(v.data);
73
+ }
74
+ return v;
75
+ });
76
+ };
77
+
78
+ const buildMsg = (verb, msg, data, id = randomPhrase()) => serialise({
79
+ verb,
80
+ msg,
81
+ data,
82
+ id,
83
+ });
84
+
85
+ const buildPatchConfiguration = (oldConf, newConf, id) => {
86
+ const patches = jsonPatch.compare(oldConf, newConf);
87
+ return buildMsg(VERB.PATCH, MESSAGE.CONFIGURATION, patches, id);
88
+ };
89
+
90
+ /**************************************************************************
91
+ * build
92
+ *
93
+ * Public object exposing an API to build valid protocol messages.
94
+ * It is not the only way to build valid messages within the protocol.
95
+ *************************************************************************/
96
+ const build = {
97
+ CONFIGURATION: {
98
+ PATCH: buildPatchConfiguration,
99
+ READ: (id) => buildMsg(VERB.READ, MESSAGE.CONFIGURATION, {}, id),
100
+ NOTIFY: (config, id) => buildMsg(VERB.NOTIFY, MESSAGE.CONFIGURATION, config, id),
101
+ },
102
+ ERROR: {
103
+ NOTIFY: {
104
+ UNSUPPORTED_MESSAGE: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_MESSAGE, id),
105
+ UNSUPPORTED_VERB: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_VERB, id),
106
+ JSON_PARSE_ERROR: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.JSON_PARSE_ERROR, id),
107
+ }
108
+ },
109
+ };
110
+
111
+ /**************************************************************************
112
+ * Client
113
+ *
114
+ * The Control Client. Client for the websocket control API.
115
+ * Used to hot-restart the SDK.
116
+ *
117
+ * logger - Logger- see SDK logger used elsewhere
118
+ * address - address of control server
119
+ * port - port of control server
120
+ *************************************************************************/
121
+ class Client extends ws {
122
+ /**
123
+ * Consider this a private constructor.
124
+ * `Client` instances outside of this class should be created via the `Create(...args)` static method.
125
+ */
126
+ constructor({ address = 'localhost', port, logger, appConfig }) {
127
+ super(`ws://${address}:${port}`);
128
+ this._logger = logger;
129
+ this._appConfig = appConfig;
130
+ }
131
+
132
+ // Really only exposed so that a user can import only the client for convenience
133
+ get Build() {
134
+ return build;
135
+ }
136
+
137
+ static Create(...args) {
138
+ return new Promise((resolve, reject) => {
139
+ const client = new Client(...args);
140
+ client.on('open', () => resolve(client));
141
+ client.on('error', (err) => reject(err));
142
+ client.on('message', client._handle);
143
+ });
144
+ }
145
+
146
+ async send(msg) {
147
+ const data = typeof msg === 'string' ? msg : serialise(msg);
148
+ this._logger.push({ data }).log('Sending message');
149
+ return new Promise((resolve) => super.send.call(this, data, resolve));
150
+ }
151
+
152
+ // Receive a single message
153
+ async receive() {
154
+ return new Promise((resolve) => this.once('message', (data) => {
155
+ const msg = deserialise(data);
156
+ this._logger.push({ msg }).log('Received');
157
+ resolve(msg);
158
+ }));
159
+ }
160
+
161
+ // Close connection
162
+ async stop() {
163
+ this._logger.log('Control client shutting down...');
164
+ this.close();
165
+ }
166
+
167
+ reconfigure({ logger = this._logger, port = 0, appConfig = this._appConfig }) {
168
+ assert(port === this._socket.remotePort, 'Cannot reconfigure running port');
169
+ return () => {
170
+ this._logger = logger;
171
+ this._appConfig = appConfig;
172
+ this._logger.log('restarted');
173
+ };
174
+ }
175
+
176
+ // Handle incoming message from the server.
177
+ _handle(data) {
178
+ // TODO: json-schema validation of received message- should be pretty straight-forward
179
+ // and will allow better documentation of the API
180
+ let msg;
181
+ try {
182
+ msg = deserialise(data);
183
+ } catch (err) {
184
+ this._logger.push({ data }).log('Couldn\'t parse received message');
185
+ this.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
186
+ }
187
+ this._logger.push({ msg }).log('Handling received message');
188
+ switch (msg.msg) {
189
+ case MESSAGE.CONFIGURATION:
190
+ switch (msg.verb) {
191
+ case VERB.NOTIFY:
192
+ case VERB.PATCH: {
193
+ const dup = JSON.parse(JSON.stringify(this._appConfig)); // fast-json-patch explicitly mutates
194
+ jsonPatch.applyPatch(dup, msg.data);
195
+ this._logger.push({ oldConf: this._appConfig, newConf: dup }).log('Emitting new configuration');
196
+ this.emit(EVENT.RECONFIGURE, dup);
197
+ break;
198
+ }
199
+ default:
200
+ this.send(build.ERROR.NOTIFY.UNSUPPORTED_VERB(msg.id));
201
+ break;
202
+ }
203
+ break;
204
+ default:
205
+ this.send(build.ERROR.NOTIFY.UNSUPPORTED_MESSAGE(msg.id));
206
+ break;
207
+ }
208
+
209
+ }
210
+ }
211
+
212
+
213
+
214
+ module.exports = {
215
+ Client,
216
+ build,
217
+ MESSAGE,
218
+ VERB,
219
+ ERROR,
220
+ EVENT,
221
+ };
@@ -0,0 +1,63 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * James Bush - james.bush@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ 'use strict';
12
+
13
+ const healthCheck = async(ctx) => {
14
+ ctx.response.status = 204;
15
+ ctx.response.body = '';
16
+ };
17
+
18
+
19
+ /**
20
+ * Handles a GET /requests/{ID} request. This is a test support method that allows the caller
21
+ * to see the body of a previous incoming request.
22
+ */
23
+ const getRequestById = async(ctx) => {
24
+ try {
25
+ const req = await ctx.state.cache.get(`request_${ctx.state.path.params.ID}`);
26
+ ctx.response.status = 200;
27
+ ctx.response.body = req;
28
+ }
29
+ catch(err) {
30
+ ctx.status = 500;
31
+ ctx.response.body = err;
32
+ }
33
+ };
34
+
35
+
36
+ /**
37
+ * Handles a GET /callbacks/{ID} request. This is a test support method that allows the caller
38
+ * to see the body of a previous incoming callback.
39
+ */
40
+ const getCallbackById = async(ctx) => {
41
+ try {
42
+ const req = await ctx.state.cache.get(`callback_${ctx.state.path.params.ID}`);
43
+ ctx.response.status = 200;
44
+ ctx.response.body = req;
45
+ }
46
+ catch(err) {
47
+ ctx.status = 500;
48
+ ctx.response.body = err;
49
+ }
50
+ };
51
+
52
+
53
+ module.exports = {
54
+ '/': {
55
+ get: healthCheck
56
+ },
57
+ '/requests/{ID}': {
58
+ get: getRequestById
59
+ },
60
+ '/callbacks/{ID}': {
61
+ get: getCallbackById
62
+ },
63
+ };
@@ -0,0 +1,294 @@
1
+ /**************************************************************************
2
+ * (C) Copyright ModusBox Inc. 2020 - All rights reserved. *
3
+ * *
4
+ * This file is made available under the terms of the license agreement *
5
+ * specified in the corresponding source code repository. *
6
+ * *
7
+ * ORIGINAL AUTHOR: *
8
+ * Matt Kingston - matt.kingston@modusbox.com *
9
+ **************************************************************************/
10
+
11
+ // This server has deliberately been written separate from any other server in the SDK. There is
12
+ // some reasonable argument that it could be part of the outbound or test server. It has not been
13
+ // incorporated in either as, at the time of writing, it is intended to be maintained in a
14
+ // proprietary fork. Therefore, keeping it independent of other servers will avoid the maintenance
15
+ // burden that would otherwise be associated with incorporating it with those.
16
+ //
17
+ // It inherits from the Server class from the 'ws' websocket library for Node, which in turn
18
+ // inherits from EventEmitter. We exploit this to emit an event when a reconfigure message is sent
19
+ // to this server. Then, when this server's reconfigure method is called, it reconfigures itself
20
+ // and sends a message to all clients notifying them of the new application configuration.
21
+ //
22
+ // It expects new configuration to be supplied as an array of JSON patches. It therefore exposes
23
+ // the current configuration to
24
+
25
+ const assert = require('assert').strict;
26
+
27
+ const ws = require('ws');
28
+ const jsonPatch = require('fast-json-patch');
29
+
30
+ const randomPhrase = require('~/lib/randomphrase');
31
+
32
+ /**************************************************************************
33
+ * The message protocol messages, verbs, and errors
34
+ *************************************************************************/
35
+ const MESSAGE = {
36
+ CONFIGURATION: 'CONFIGURATION',
37
+ ERROR: 'ERROR',
38
+ };
39
+
40
+ const VERB = {
41
+ READ: 'READ',
42
+ NOTIFY: 'NOTIFY',
43
+ PATCH: 'PATCH'
44
+ };
45
+
46
+ const ERROR = {
47
+ UNSUPPORTED_MESSAGE: 'UNSUPPORTED_MESSAGE',
48
+ UNSUPPORTED_VERB: 'UNSUPPORTED_VERB',
49
+ JSON_PARSE_ERROR: 'JSON_PARSE_ERROR',
50
+ };
51
+
52
+ /**************************************************************************
53
+ * Events emitted by the control server
54
+ *************************************************************************/
55
+ const EVENT = {
56
+ RECONFIGURE: 'RECONFIGURE',
57
+ };
58
+
59
+ /**************************************************************************
60
+ * Private convenience functions
61
+ *************************************************************************/
62
+ const serialise = JSON.stringify;
63
+ const deserialise = (msg) => {
64
+ //reviver function
65
+ return JSON.parse(msg.toString(), (k, v) => {
66
+ if (
67
+ v !== null &&
68
+ typeof v === 'object' &&
69
+ 'type' in v &&
70
+ v.type === 'Buffer' &&
71
+ 'data' in v &&
72
+ Array.isArray(v.data)) {
73
+ return new Buffer(v.data);
74
+ }
75
+ return v;
76
+ });
77
+ };
78
+
79
+ const buildMsg = (verb, msg, data, id = randomPhrase()) => serialise({
80
+ verb,
81
+ msg,
82
+ data,
83
+ id,
84
+ });
85
+
86
+ const buildPatchConfiguration = (oldConf, newConf, id) => {
87
+ const patches = jsonPatch.compare(oldConf, newConf);
88
+ return buildMsg(VERB.PATCH, MESSAGE.CONFIGURATION, patches, id);
89
+ };
90
+
91
+ const getWsIp = (req) => [
92
+ req.socket.remoteAddress,
93
+ ...(
94
+ req.headers['x-forwarded-for']
95
+ ? req.headers['x-forwarded-for'].split(/\s*,\s*/)
96
+ : []
97
+ )
98
+ ];
99
+
100
+ /**************************************************************************
101
+ * build
102
+ *
103
+ * Public object exposing an API to build valid protocol messages.
104
+ * It is not the only way to build valid messages within the protocol.
105
+ *************************************************************************/
106
+ const build = {
107
+ CONFIGURATION: {
108
+ PATCH: buildPatchConfiguration,
109
+ READ: (id) => buildMsg(VERB.READ, MESSAGE.CONFIGURATION, {}, id),
110
+ NOTIFY: (config, id) => buildMsg(VERB.NOTIFY, MESSAGE.CONFIGURATION, config, id),
111
+ },
112
+ ERROR: {
113
+ NOTIFY: {
114
+ UNSUPPORTED_MESSAGE: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_MESSAGE, id),
115
+ UNSUPPORTED_VERB: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.UNSUPPORTED_VERB, id),
116
+ JSON_PARSE_ERROR: (id) => buildMsg(VERB.NOTIFY, MESSAGE.ERROR, ERROR.JSON_PARSE_ERROR, id),
117
+ }
118
+ },
119
+ };
120
+
121
+ /**************************************************************************
122
+ * Client
123
+ *
124
+ * The Control Client. Client for the websocket control API.
125
+ * Used to hot-restart the SDK.
126
+ *
127
+ * logger - Logger- see SDK logger used elsewhere
128
+ * address - address of control server
129
+ * port - port of control server
130
+ *************************************************************************/
131
+ class Client extends ws {
132
+ constructor({ address = 'localhost', port, logger }) {
133
+ super(`ws://${address}:${port}`);
134
+ this._logger = logger;
135
+ }
136
+
137
+ // Really only exposed so that a user can import only the client for convenience
138
+ get Build() {
139
+ return build;
140
+ }
141
+
142
+ static async Create(...args) {
143
+ const result = new Client(...args);
144
+ await new Promise((resolve, reject) => {
145
+ result.on('open', resolve);
146
+ result.on('error', reject);
147
+ });
148
+ return result;
149
+ }
150
+
151
+ async send(msg) {
152
+ const data = typeof msg === 'string' ? msg : serialise(msg);
153
+ this._logger.push({ data }).log('Sending message');
154
+ return new Promise((resolve) => super.send.call(this, data, resolve));
155
+ }
156
+
157
+ // Receive a single message
158
+ async receive() {
159
+ return new Promise((resolve) => this.once('message', (data) => {
160
+ const msg = deserialise(data);
161
+ this._logger.push({ msg }).log('Received');
162
+ resolve(msg);
163
+ }));
164
+ }
165
+ }
166
+
167
+ /**************************************************************************
168
+ * Server
169
+ *
170
+ * The Control Server. Exposes a websocket control API.
171
+ * Used to hot-restart the SDK.
172
+ *
173
+ * logger - Logger- see SDK logger used elsewhere
174
+ * port - HTTP port to host on
175
+ * appConfig - The configuration for the entire application- supplied here as this class uses it to
176
+ * validate reconfiguration requests- it is not used for configuration here, however
177
+ * server - optional HTTP/S server on which to serve the websocket
178
+ *************************************************************************/
179
+ class Server extends ws.Server {
180
+ constructor({ logger, port = 0, appConfig = {} }) {
181
+ super({ clientTracking: true, port });
182
+
183
+ this._logger = logger;
184
+ this._port = port;
185
+ this._appConfig = appConfig;
186
+ this._clientData = new Map();
187
+
188
+ this.on('error', err => {
189
+ this._logger.push({ err })
190
+ .log('Unhandled websocket error occurred. Shutting down.');
191
+ process.exit(1);
192
+ });
193
+
194
+ this.on('connection', (socket, req) => {
195
+ const logger = this._logger.push({
196
+ url: req.url,
197
+ ip: getWsIp(req),
198
+ remoteAddress: req.socket.remoteAddress,
199
+ });
200
+ logger.log('Websocket connection received');
201
+ this._clientData.set(socket, { ip: req.connection.remoteAddress, logger });
202
+
203
+ socket.on('close', (code, reason) => {
204
+ logger.push({ code, reason }).log('Websocket connection closed');
205
+ this._clientData.delete(socket);
206
+ });
207
+
208
+ socket.on('message', this._handle(socket, logger));
209
+ });
210
+
211
+ this._logger.push(this.address()).log('running on');
212
+ }
213
+
214
+ // Close the server then wait for all the client sockets to close
215
+ async stop() {
216
+ await new Promise(this.close.bind(this));
217
+ this._logger.log('Control server shutdown complete');
218
+ }
219
+
220
+ reconfigure({ logger = this._logger, port = 0, appConfig = this._appConfig }) {
221
+ assert(port === this._port, 'Cannot reconfigure running port');
222
+ return () => {
223
+ const reconfigureClientLogger =
224
+ ({ logger: clientLogger }) => clientLogger.configure(logger);
225
+ this._clientData.values(reconfigureClientLogger);
226
+ this._logger = logger;
227
+ this._appConfig = appConfig;
228
+ this._logger.log('restarted');
229
+ };
230
+ }
231
+
232
+ async notifyClientsOfCurrentConfig() {
233
+ const updateConfMsg = build.CONFIGURATION.NOTIFY(this._appConfig);
234
+ const logError = (socket, message) => (err) =>
235
+ this._logger
236
+ .push({ message, ip: this._clientData.get(socket).ip, err })
237
+ .log('Error sending reconfigure notification to client');
238
+ const sendToAllClients = (msg) => Promise.all(
239
+ [...this.clients.values()].map((socket) =>
240
+ (new Promise((resolve) => socket.send(msg, resolve))).catch(logError(socket, msg))
241
+ )
242
+ );
243
+ return await sendToAllClients(updateConfMsg);
244
+ }
245
+
246
+ _handle(client, logger) {
247
+ return (data) => {
248
+ // TODO: json-schema validation of received message- should be pretty straight-forward
249
+ // and will allow better documentation of the API
250
+ let msg;
251
+ try {
252
+ msg = deserialise(data);
253
+ } catch (err) {
254
+ logger.push({ data }).log('Couldn\'t parse received message');
255
+ client.send(build.ERROR.NOTIFY.JSON_PARSE_ERROR());
256
+ }
257
+ logger.push({ msg }).log('Handling received message');
258
+ switch (msg.msg) {
259
+ case MESSAGE.CONFIGURATION:
260
+ switch (msg.verb) {
261
+ case VERB.READ:
262
+ client.send(build.CONFIGURATION.NOTIFY(this._appConfig, msg.id));
263
+ break;
264
+ case VERB.PATCH: {
265
+ // TODO: validate the incoming patch? Or assume clients have used the
266
+ // client library?
267
+ const dup = JSON.parse(JSON.stringify(this._appConfig)); // fast-json-patch explicitly mutates
268
+ jsonPatch.applyPatch(dup, msg.data);
269
+ logger.push({ oldConf: this._appConfig, newConf: dup }).log('Emitting new configuration');
270
+ this.emit(EVENT.RECONFIGURE, dup);
271
+ break;
272
+ }
273
+ default:
274
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_VERB(msg.id));
275
+ break;
276
+ }
277
+ break;
278
+ default:
279
+ client.send(build.ERROR.NOTIFY.UNSUPPORTED_MESSAGE(msg.id));
280
+ break;
281
+ }
282
+ };
283
+ }
284
+ }
285
+
286
+ module.exports = {
287
+ Client,
288
+ Server,
289
+ build,
290
+ MESSAGE,
291
+ VERB,
292
+ ERROR,
293
+ EVENT,
294
+ };