mastercontroller 1.2.4 → 1.2.6

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/MasterControl.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // MasterControl - by Alexander rich
2
- // version 1.0.245
2
+ // version 1.0.246
3
3
 
4
4
  var url = require('url');
5
5
  var fileserver = require('fs');
6
6
  var http = require('http');
7
7
  var https = require('https');
8
+ var tls = require('tls');
8
9
  var fs = require('fs');
9
10
  var url = require('url');
10
11
  var path = require('path');
@@ -20,6 +21,8 @@ class MasterControl {
20
21
  _serverProtocol = null
21
22
  _scopedList = []
22
23
  _loadedFunc = null
24
+ _tlsOptions = null
25
+ _hstsEnabled = false
23
26
 
24
27
  #loadTransientListClasses(name, params){
25
28
  Object.defineProperty(this.requestList, name, {
@@ -148,19 +151,25 @@ class MasterControl {
148
151
 
149
152
  component(folderLocation, innerFolder){
150
153
 
151
- var rootFolderLocation = `${this.root}/${folderLocation}/${innerFolder}`;
152
- var search = `${rootFolderLocation}/**/*config.js`;
153
- var files = globSearch.sync(search, rootFolderLocation);
154
- require(files[0]);
155
- var searchRoutes = `${rootFolderLocation}/**/*routes.js`;
156
- var routeFiles = globSearch.sync(searchRoutes, rootFolderLocation);
157
- var route = routeFiles[0];
154
+ var rootFolderLocation = path.join(this.root, folderLocation, innerFolder);
155
+ var files = globSearch.sync("**/*config.js", { cwd: rootFolderLocation, absolute: true });
156
+ if(files && files.length > 0){
157
+ require(files[0]);
158
+ }else{
159
+ master.error.log(`Cannot find config file under ${rootFolderLocation}`, "error");
160
+ }
161
+ var routeFiles = globSearch.sync("**/*routes.js", { cwd: rootFolderLocation, absolute: true });
162
+ var route = routeFiles && routeFiles.length > 0 ? routeFiles[0] : null;
158
163
  var routeObject = {
159
164
  isComponent : true,
160
165
  root : rootFolderLocation
161
166
  }
162
167
  this.router.setup(routeObject);
163
- require(route);
168
+ if(route){
169
+ require(route);
170
+ }else{
171
+ master.error.log(`Cannot find routes file under ${rootFolderLocation}`, "error");
172
+ }
164
173
  }
165
174
 
166
175
 
@@ -169,11 +178,12 @@ class MasterControl {
169
178
 
170
179
  if(settings.httpPort || settings.requestTimeout){
171
180
  this.server.timeout = settings.requestTimeout;
172
- if(settings.http){
173
- this.server.listen(settings.httpPort, settings.http);
181
+ var host = settings.hostname || settings.host || settings.http;
182
+ if(host){
183
+ this.server.listen(settings.httpPort, host);
174
184
  }else{
175
- this.server.listen(settings.httpPort);
176
- }
185
+ this.server.listen(settings.httpPort);
186
+ }
177
187
  }
178
188
  else{
179
189
  throw "HTTP, HTTPS, HTTPPORT and REQUEST TIMEOUT MISSING";
@@ -201,7 +211,16 @@ class MasterControl {
201
211
  }
202
212
  if(type === "https"){
203
213
  $that.serverProtocol = "https";
214
+ // Initialize TLS from env if no credentials passed
215
+ if(!credentials){
216
+ $that._initializeTlsFromEnv();
217
+ credentials = $that._tlsOptions;
218
+ }
219
+ // Apply secure defaults if missing
204
220
  if(credentials){
221
+ if(!credentials.minVersion){ credentials.minVersion = 'TLSv1.2'; }
222
+ if(credentials.honorCipherOrder === undefined){ credentials.honorCipherOrder = true; }
223
+ if(!credentials.ALPNProtocols){ credentials.ALPNProtocols = ['h2', 'http/1.1']; }
205
224
  return https.createServer(credentials, async function(req, res) {
206
225
  $that.serverRun(req, res);
207
226
  });
@@ -216,6 +235,130 @@ class MasterControl {
216
235
  }
217
236
  }
218
237
 
238
+ // Creates an HTTP server that 301-redirects to HTTPS counterpart
239
+ startHttpToHttpsRedirect(redirectPort, bindHost){
240
+ var $that = this;
241
+ return http.createServer(function (req, res) {
242
+ try{
243
+ var host = req.headers['host'] || '';
244
+ // Force original host, just change scheme
245
+ var location = 'https://' + host + req.url;
246
+ res.statusCode = 301;
247
+ res.setHeader('Location', location);
248
+ res.end();
249
+ }catch(e){
250
+ res.statusCode = 500;
251
+ res.end();
252
+ }
253
+ }).listen(redirectPort, bindHost);
254
+ }
255
+
256
+ // Load TLS configuration from env and build SNI contexts with live reload
257
+ _initializeTlsFromEnv(){
258
+ try{
259
+ var cfg = this.env;
260
+ if(!cfg || !cfg.server || !cfg.server.tls){
261
+ return;
262
+ }
263
+ var tlsCfg = cfg.server.tls;
264
+
265
+ var defaultCreds = this._buildSecureContextFromPaths(tlsCfg.default);
266
+ var defaultContext = defaultCreds ? tls.createSecureContext(defaultCreds) : null;
267
+
268
+ var sniMap = {};
269
+ if(tlsCfg.sni && typeof tlsCfg.sni === 'object'){
270
+ for (var domain in tlsCfg.sni){
271
+ if (Object.prototype.hasOwnProperty.call(tlsCfg.sni, domain)){
272
+ var domCreds = this._buildSecureContextFromPaths(tlsCfg.sni[domain]);
273
+ if(domCreds){
274
+ sniMap[domain] = tls.createSecureContext(domCreds);
275
+ // watch domain certs for reload
276
+ this._watchTlsFilesAndReload(tlsCfg.sni[domain], function(){
277
+ try{
278
+ var updated = tls.createSecureContext(
279
+ this._buildSecureContextFromPaths(tlsCfg.sni[domain])
280
+ );
281
+ sniMap[domain] = updated;
282
+ }catch(e){
283
+ console.error('Failed to reload TLS context for domain', domain, e);
284
+ }
285
+ }.bind(this));
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ var options = defaultCreds ? Object.assign({}, defaultCreds) : {};
292
+ options.SNICallback = function(servername, cb){
293
+ var ctx = sniMap[servername];
294
+ if(!ctx && defaultContext){ ctx = defaultContext; }
295
+ if(cb){ return cb(null, ctx); }
296
+ return ctx;
297
+ };
298
+
299
+ // Apply top-level TLS defaults/hardening from env if provided
300
+ if(tlsCfg.minVersion){ options.minVersion = tlsCfg.minVersion; }
301
+ if(tlsCfg.honorCipherOrder !== undefined){ options.honorCipherOrder = tlsCfg.honorCipherOrder; }
302
+ if(tlsCfg.ciphers){ options.ciphers = tlsCfg.ciphers; }
303
+ if(tlsCfg.alpnProtocols){ options.ALPNProtocols = tlsCfg.alpnProtocols; }
304
+
305
+ // HSTS
306
+ this._hstsEnabled = !!tlsCfg.hsts;
307
+ this._hstsMaxAge = tlsCfg.hstsMaxAge || 15552000; // 180 days by default
308
+
309
+ // Watch default certs for reload
310
+ if(tlsCfg.default){
311
+ this._watchTlsFilesAndReload(tlsCfg.default, function(){
312
+ try{
313
+ var updatedCreds = this._buildSecureContextFromPaths(tlsCfg.default);
314
+ defaultContext = tls.createSecureContext(updatedCreds);
315
+ // keep key/cert on options for non-SNI connections
316
+ Object.assign(options, updatedCreds);
317
+ }catch(e){
318
+ console.error('Failed to reload default TLS context', e);
319
+ }
320
+ }.bind(this));
321
+ }
322
+
323
+ this._tlsOptions = options;
324
+ }catch(e){
325
+ console.error('Failed to initialize TLS from env', e);
326
+ }
327
+ }
328
+
329
+ _buildSecureContextFromPaths(desc){
330
+ if(!desc){ return null; }
331
+ var opts = {};
332
+ try{
333
+ if(desc.keyPath){ opts.key = fs.readFileSync(desc.keyPath); }
334
+ if(desc.certPath){ opts.cert = fs.readFileSync(desc.certPath); }
335
+ if(desc.caPath){ opts.ca = fs.readFileSync(desc.caPath); }
336
+ if(desc.pfxPath){ opts.pfx = fs.readFileSync(desc.pfxPath); }
337
+ if(desc.passphrase){ opts.passphrase = desc.passphrase; }
338
+ return opts;
339
+ }catch(e){
340
+ console.error('Failed to read TLS files', e);
341
+ return null;
342
+ }
343
+ }
344
+
345
+ _watchTlsFilesAndReload(desc, onChange){
346
+ var paths = [];
347
+ if(desc.keyPath){ paths.push(desc.keyPath); }
348
+ if(desc.certPath){ paths.push(desc.certPath); }
349
+ if(desc.caPath){ paths.push(desc.caPath); }
350
+ if(desc.pfxPath){ paths.push(desc.pfxPath); }
351
+ paths.forEach(function(p){
352
+ try{
353
+ fs.watchFile(p, { interval: 5000 }, function(){
354
+ onChange();
355
+ });
356
+ }catch(e){
357
+ console.error('Failed to watch TLS file', p, e);
358
+ }
359
+ });
360
+ }
361
+
219
362
  async serverRun(req, res){
220
363
  var $that = this;
221
364
  console.log("path", `${req.method} ${req.url}`);
@@ -271,6 +414,10 @@ class MasterControl {
271
414
  if(ext === ""){
272
415
  var requestObject = await this.middleware(req, res);
273
416
  if(requestObject !== -1){
417
+ // HSTS header if enabled
418
+ if(this.serverProtocol === 'https' && this._hstsEnabled){
419
+ res.setHeader('strict-transport-security', `max-age=${this._hstsMaxAge}; includeSubDomains`);
420
+ }
274
421
  var loadedDone = false;
275
422
  if (typeof $that._loadedFunc === 'function') {
276
423
  loadedDone = $that._loadedFunc(requestObject);
@@ -323,15 +470,18 @@ class MasterControl {
323
470
  }
324
471
 
325
472
  startMVC(foldername){
326
- var rootFolderLocation = `${this.root}/${foldername}`;
327
- var search = `${rootFolderLocation}/**/*routes.js`;
328
- var files = globSearch.sync(search, rootFolderLocation);
473
+ var rootFolderLocation = path.join(this.root, foldername);
474
+ var files = globSearch.sync("**/*routes.js", { cwd: rootFolderLocation, absolute: true });
329
475
  var route = {
330
476
  isComponent : false,
331
477
  root : `${this.root}`
332
478
  }
333
479
  this.router.setup(route);
334
- require(files[0]);
480
+ if(files && files.length > 0){
481
+ require(files[0]);
482
+ }else{
483
+ master.error.log(`Cannot find routes file under ${rootFolderLocation}`, "error");
484
+ }
335
485
  }
336
486
 
337
487
 
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ ## MasterController Framework
2
+
3
+ MasterController is a lightweight MVC-style server framework for Node.js with routing, controllers, views, dependency injection, CORS, sessions, sockets, and more.
4
+
5
+ ### Install
6
+ ```
7
+ npm install mastercontroller
8
+ ```
9
+
10
+ ### Quickstart
11
+ ```js
12
+ // server.js
13
+ const master = require('./MasterControl');
14
+
15
+ master.root = __dirname; // your project root
16
+ master.environmentType = 'development'; // or process.env.NODE_ENV
17
+
18
+ const server = master.setupServer('http'); // or 'https'
19
+ master.start(server);
20
+ master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
21
+
22
+ // Or load from config/environments/env.<env>.json
23
+ // master.serverSettings(master.env.server);
24
+
25
+ // Load your routes
26
+ master.startMVC('app');
27
+ ```
28
+
29
+ ### Routes
30
+ Create `app/config/routes.js` and define routes with `master.router.start()` API.
31
+
32
+ ### Controllers
33
+ Place controllers under `app/controllers/*.js` and export methods matching your routes.
34
+
35
+ ### Views and Templates
36
+ Views live under `app/views/<controller>/<action>.html` with a layout at `app/views/layouts/master.html`.
37
+
38
+ ### CORS and Preflight
39
+ `MasterCors` configures CORS headers. Preflight `OPTIONS` requests are short-circuited with 204.
40
+
41
+ ### HTTPS
42
+ Use `setupServer('https', credentials)` or configure via environment TLS; see docs in `docs/` for multiple setups.
43
+
44
+ ### Docs
45
+ - `docs/server-setup-http.md`
46
+ - `docs/server-setup-https-credentials.md`
47
+ - `docs/server-setup-https-env-tls-sni.md`
48
+ - `docs/server-setup-hostname-binding.md`
49
+ - `docs/server-setup-nginx-reverse-proxy.md`
50
+ - `docs/environment-tls-reference.md`
51
+
52
+ ### Production tips
53
+ - Prefer a reverse proxy for TLS and serve Node on a high port.
54
+ - If keeping TLS in Node, harden TLS and manage cert rotation.
55
+
56
+ ### License
57
+ ISC
58
+
@@ -0,0 +1,60 @@
1
+ ## Environment TLS/SNI reference
2
+
3
+ Place environment files at `config/environments/env.<environment>.json`.
4
+
5
+ ### server section
6
+ ```json
7
+ {
8
+ "server": {
9
+ "httpPort": 3000,
10
+ "hostname": "127.0.0.1",
11
+ "requestTimeout": 60000,
12
+ "tls": { /* optional, for HTTPS when using setupServer('https') without credentials */ }
13
+ }
14
+ }
15
+ ```
16
+
17
+ ### tls section
18
+ ```json
19
+ "tls": {
20
+ "hsts": true, // add HSTS header on HTTPS responses
21
+ "hstsMaxAge": 15552000, // HSTS max-age in seconds (default 180 days)
22
+ "minVersion": "TLSv1.2", // minimum TLS version ('TLSv1.2' or 'TLSv1.3')
23
+ "honorCipherOrder": true, // prefer server cipher order
24
+ "alpnProtocols": ["h2", "http/1.1"], // enable HTTP/2 and HTTP/1.1
25
+ "default": { // fallback certificate if SNI host doesn't match
26
+ "keyPath": "/path/to/key.pem",
27
+ "certPath": "/path/to/cert.pem",
28
+ "caPath": "/path/to/chain.pem",
29
+ "pfxPath": null, // optional if using PFX
30
+ "passphrase": null // optional if key is encrypted
31
+ },
32
+ "sni": { // per-domain certificates
33
+ "example.com": {
34
+ "keyPath": "/path/to/example.key",
35
+ "certPath": "/path/to/example.crt",
36
+ "caPath": "/path/to/chain.pem"
37
+ },
38
+ "api.example.com": {
39
+ "keyPath": "/path/to/api.key",
40
+ "certPath": "/path/to/api.crt",
41
+ "caPath": "/path/to/chain.pem"
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Terminology
48
+ - tls: Transport Layer Security. Encrypts traffic between client and server.
49
+ - SNI: Server Name Indication. Lets the server present different certificates based on the requested hostname during TLS handshake.
50
+ - default: The certificate used when no SNI match is found.
51
+
52
+ ### Behavior
53
+ - If you call `setupServer('https')` without credentials, MasterControl reads `server.tls` and builds secure contexts (default + SNI) and watches the key/cert files for changes. Updates apply in-memory without restart.
54
+ - If you pass credentials directly to `setupServer('https', credentials)`, those are used instead and env `tls` is ignored.
55
+
56
+ ### Tips
57
+ - Keep private keys readable only by the process user.
58
+ - Prefer `TLSv1.2`+ and enable HTTP/2 via ALPN.
59
+ - If binding to 443 without a proxy, consider using a high port (8443) or grant `CAP_NET_BIND_SERVICE` to avoid running as root.
60
+
@@ -0,0 +1,24 @@
1
+ ## Server setup: Hostname binding
2
+
3
+ Bind the listener to a specific interface using `hostname` (or `host`/`http`).
4
+
5
+ ### server.js
6
+ ```js
7
+ const master = require('./MasterControl');
8
+
9
+ master.root = __dirname;
10
+ master.environmentType = process.env.NODE_ENV || 'development';
11
+
12
+ const server = master.setupServer('http');
13
+ master.start(server);
14
+
15
+ // Bind to localhost only
16
+ master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
17
+
18
+ master.startMVC('app');
19
+ ```
20
+
21
+ ### Notes
22
+ - Use `0.0.0.0` to accept connections on all interfaces.
23
+ - In production with a reverse proxy, bind to `127.0.0.1` so only the proxy can reach the app.
24
+
@@ -0,0 +1,32 @@
1
+ ## Server setup: HTTP
2
+
3
+ This example starts a plain HTTP server. Useful for local development, or when you run behind a reverse proxy that terminates TLS.
4
+
5
+ ### server.js (HTTP)
6
+ ```js
7
+ const master = require('./MasterControl');
8
+
9
+ // Point master to your project root and environment
10
+ master.root = __dirname;
11
+ master.environmentType = process.env.NODE_ENV || 'development';
12
+
13
+ // Create HTTP server and bind it
14
+ const server = master.setupServer('http');
15
+ master.start(server);
16
+
17
+ // Use either explicit settings or your environment JSON
18
+ // Option A: explicit
19
+ // master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
20
+
21
+ // Option B: from env config at config/environments/env.<env>.json
22
+ master.serverSettings(master.env.server);
23
+
24
+ // Load your routes and controllers
25
+ // If your routes are under <root>/app/**/routes.js
26
+ master.startMVC('app');
27
+ ```
28
+
29
+ ### Notes
30
+ - `master.serverSettings` now honors `hostname` (or `host`/`http`) if provided; otherwise it listens on all interfaces.
31
+ - For production, prefer running behind a reverse proxy and keep the app on a high port (e.g., 3000).
32
+
@@ -0,0 +1,32 @@
1
+ ## Server setup: HTTPS with direct credentials
2
+
3
+ Pass key/cert (and optional chain/ca) directly to `setupServer('https', credentials)`.
4
+
5
+ ### server.js (HTTPS credentials)
6
+ ```js
7
+ const fs = require('fs');
8
+ const master = require('./MasterControl');
9
+
10
+ master.root = __dirname;
11
+ master.environmentType = process.env.NODE_ENV || 'production';
12
+
13
+ const credentials = {
14
+ key: fs.readFileSync('/etc/ssl/private/site.key'),
15
+ cert: fs.readFileSync('/etc/ssl/certs/site.crt'),
16
+ ca: fs.readFileSync('/etc/ssl/certs/chain.pem'),
17
+ minVersion: 'TLSv1.2',
18
+ honorCipherOrder: true,
19
+ ALPNProtocols: ['h2', 'http/1.1']
20
+ };
21
+
22
+ const server = master.setupServer('https', credentials);
23
+ master.start(server);
24
+ master.serverSettings({ httpPort: 8443, hostname: '0.0.0.0', requestTimeout: 60000 });
25
+ master.startMVC('app');
26
+ ```
27
+
28
+ ### Notes
29
+ - Use a high port (e.g., 8443) to avoid running as root, or grant `CAP_NET_BIND_SERVICE` if binding to 443.
30
+ - Strong defaults are ensured if you omit them, but explicitly setting them is recommended.
31
+ - For multiple domains, see the TLS/SNI guide.
32
+
@@ -0,0 +1,62 @@
1
+ ## Server setup: HTTPS via environment TLS (with SNI and live reload)
2
+
3
+ This uses `config/environments/env.<env>.json` to configure TLS, SNI (multi-domain), HSTS, and watches cert files for live reload.
4
+
5
+ ### Example env.production.json
6
+ ```json
7
+ {
8
+ "server": {
9
+ "httpPort": 8443,
10
+ "hostname": "0.0.0.0",
11
+ "requestTimeout": 60000,
12
+ "tls": {
13
+ "hsts": true,
14
+ "hstsMaxAge": 15552000,
15
+ "minVersion": "TLSv1.2",
16
+ "honorCipherOrder": true,
17
+ "alpnProtocols": ["h2", "http/1.1"],
18
+ "default": {
19
+ "keyPath": "/etc/ssl/private/site.key",
20
+ "certPath": "/etc/ssl/certs/site.crt",
21
+ "caPath": "/etc/ssl/certs/chain.pem"
22
+ },
23
+ "sni": {
24
+ "example.com": {
25
+ "keyPath": "/etc/ssl/private/example.key",
26
+ "certPath": "/etc/ssl/certs/example.crt",
27
+ "caPath": "/etc/ssl/certs/chain.pem"
28
+ },
29
+ "api.example.com": {
30
+ "keyPath": "/etc/ssl/private/api.key",
31
+ "certPath": "/etc/ssl/certs/api.crt",
32
+ "caPath": "/etc/ssl/certs/chain.pem"
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### server.js (HTTPS from env)
41
+ ```js
42
+ const master = require('./MasterControl');
43
+
44
+ master.root = __dirname;
45
+ master.environmentType = process.env.NODE_ENV || 'production';
46
+
47
+ // No credentials passed; MasterControl will auto-load TLS from env
48
+ const server = master.setupServer('https');
49
+ master.start(server);
50
+ master.serverSettings(master.env.server);
51
+ master.startMVC('app');
52
+
53
+ // Optional: HTTP->HTTPS redirect (listen on 80)
54
+ // master.startHttpToHttpsRedirect(80, '0.0.0.0');
55
+ ```
56
+
57
+ ### How it works
58
+ - `default`: certs used when SNI domain does not match any entry.
59
+ - `sni`: per-domain certificates; the server chooses the right cert via `SNICallback`.
60
+ - Live reload: when any `keyPath`/`certPath`/`caPath` changes, the secure context is rebuilt in-memory (no restart needed).
61
+ - HSTS: when enabled, responses over HTTPS include `strict-transport-security` with the configured max-age.
62
+
@@ -0,0 +1,46 @@
1
+ ## Server setup: Nginx reverse proxy with HTTP→HTTPS redirect
2
+
3
+ Recommended production pattern: Node app on a high port (HTTP), Nginx on 80/443 handling TLS and redirects.
4
+
5
+ ### server.js (app on HTTP localhost:3000)
6
+ ```js
7
+ const master = require('./MasterControl');
8
+
9
+ master.root = __dirname;
10
+ master.environmentType = process.env.NODE_ENV || 'production';
11
+
12
+ const server = master.setupServer('http');
13
+ master.start(server);
14
+ master.serverSettings({ httpPort: 3000, hostname: '127.0.0.1', requestTimeout: 60000 });
15
+ master.startMVC('app');
16
+ ```
17
+
18
+ ### Nginx config
19
+ ```nginx
20
+ server {
21
+ listen 80;
22
+ server_name yourdomain.com;
23
+ return 301 https://$host$request_uri;
24
+ }
25
+
26
+ server {
27
+ listen 443 ssl http2;
28
+ server_name yourdomain.com;
29
+
30
+ ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
31
+ ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
32
+
33
+ location / {
34
+ proxy_pass http://127.0.0.1:3000;
35
+ proxy_set_header Host $host;
36
+ proxy_set_header X-Real-IP $remote_addr;
37
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38
+ proxy_set_header X-Forwarded-Proto $scheme;
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Notes
44
+ - Use certbot or another ACME client to manage certificates and renewals automatically.
45
+ - This keeps Node unprivileged (no need to bind to 443) and simplifies TLS.
46
+
package/package.json CHANGED
@@ -18,5 +18,5 @@
18
18
  "scripts": {
19
19
  "test": "echo \"Error: no test specified\" && exit 1"
20
20
  },
21
- "version": "1.2.4"
21
+ "version": "1.2.6"
22
22
  }