@saltcorn/server 1.0.0-beta.13 → 1.0.0-beta.14

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/app.js CHANGED
@@ -447,6 +447,9 @@ Sitemap: ${base}sitemap.xml
447
447
  app.get("*", function (req, res) {
448
448
  res.status(404).sendWrap(req.__("Not found"), h1(req.__("Page not found")));
449
449
  });
450
+
451
+ //prevent prototype pollution
452
+ delete Object.prototype.__proto__;
450
453
  return app;
451
454
  };
452
455
  module.exports = getApp;
package/load_plugins.js CHANGED
@@ -19,17 +19,46 @@ const {
19
19
  resolveLatest,
20
20
  } = require("@saltcorn/plugins-loader/stable_versioning");
21
21
 
22
+ const isFixedPlugin = (plugin) =>
23
+ plugin.location === "@saltcorn/sbadmin2" ||
24
+ plugin.location === "@saltcorn/base-plugin";
25
+
26
+ /**
27
+ * return the cached engine infos or fetch them from npm and update the cache
28
+ * @param plugin plugin to load
29
+ */
30
+ const getEngineInfos = async (plugin, forceFetch) => {
31
+ const rootState = getRootState();
32
+ const cached = rootState.getConfig("engines_cache", {}) || {};
33
+ if (cached[plugin.location] && !forceFetch) {
34
+ return cached[plugin.location];
35
+ } else {
36
+ getState().log(5, `Fetching versions for '${plugin.location}'`);
37
+ const pkgInfo = await npmFetch.json(
38
+ `https://registry.npmjs.org/${plugin.location}`
39
+ );
40
+ const versions = pkgInfo.versions;
41
+ const newCached = {};
42
+ for (const [k, v] of Object.entries(versions)) {
43
+ newCached[k] = v.engines?.saltcorn
44
+ ? { engines: { saltcorn: v.engines.saltcorn } }
45
+ : {};
46
+ }
47
+ cached[plugin.location] = newCached;
48
+ await rootState.setConfig("engines_cache", { ...cached });
49
+ return newCached;
50
+ }
51
+ };
52
+
22
53
  /**
23
54
  * checks the saltcorn engine property and changes the plugin version if necessary
24
55
  * @param plugin plugin to load
25
56
  */
26
- const ensurePluginSupport = async (plugin) => {
27
- const pkgInfo = await npmFetch.json(
28
- `https://registry.npmjs.org/${plugin.location}`
29
- );
57
+ const ensurePluginSupport = async (plugin, forceFetch) => {
58
+ const versions = await getEngineInfos(plugin, forceFetch);
30
59
  const supported = supportedVersion(
31
60
  plugin.version || "latest",
32
- pkgInfo.versions,
61
+ versions,
33
62
  packagejson.version
34
63
  );
35
64
  if (!supported)
@@ -38,8 +67,7 @@ const ensurePluginSupport = async (plugin) => {
38
67
  );
39
68
  else if (
40
69
  supported !== plugin.version ||
41
- (plugin.version === "latest" &&
42
- supported !== resolveLatest(pkgInfo.versions))
70
+ (plugin.version === "latest" && supported !== resolveLatest(versions))
43
71
  )
44
72
  plugin.version = supported;
45
73
  };
@@ -50,10 +78,10 @@ const ensurePluginSupport = async (plugin) => {
50
78
  * @param plugin - plugin to load
51
79
  * @param force - force flag
52
80
  */
53
- const loadPlugin = async (plugin, force) => {
54
- if (plugin.source === "npm" && isRoot()) {
81
+ const loadPlugin = async (plugin, force, forceFetch) => {
82
+ if (plugin.source === "npm" && !isFixedPlugin(plugin)) {
55
83
  try {
56
- await ensurePluginSupport(plugin);
84
+ await ensurePluginSupport(plugin, forceFetch);
57
85
  } catch (e) {
58
86
  console.log(
59
87
  `Warning: Unable to find a supported version for '${plugin.location}' Continuing with the installed version`
@@ -279,6 +307,6 @@ module.exports = {
279
307
  loadAllPlugins,
280
308
  loadPlugin,
281
309
  requirePlugin,
282
- supportedVersion,
310
+ getEngineInfos,
283
311
  ensurePluginSupport,
284
312
  };
package/locales/en.json CHANGED
@@ -1468,5 +1468,9 @@
1468
1468
  "Time to run": "Time to run",
1469
1469
  "Mobile": "Mobile",
1470
1470
  "Plain password trigger row": "Plain password trigger row",
1471
- "Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate)."
1471
+ "Send plaintext password changes to Users table triggers (Insert, Update and Validate).": "Send plaintext password changes to Users table triggers (Insert, Update and Validate).",
1472
+ "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>": "Minimum user role required to create a new tenant<div class=\"alert alert-danger fst-normal\" role=\"alert\" data-show-if=\"showIfFormulaInputs($('select[name=role_to_create_tenant]'), '+role_to_create_tenant>1')\">Giving non-trusted users access to create tenants is a security risk and not recommended.</div>",
1473
+ "Select tag": "Select tag",
1474
+ "Invalid build directory path": "Invalid build directory path",
1475
+ "Invalid build directory name": "Invalid build directory name"
1472
1476
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.0.0-beta.13",
3
+ "version": "1.0.0-beta.14",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "1.0.0-beta.13",
11
- "@saltcorn/builder": "1.0.0-beta.13",
12
- "@saltcorn/data": "1.0.0-beta.13",
13
- "@saltcorn/admin-models": "1.0.0-beta.13",
14
- "@saltcorn/filemanager": "1.0.0-beta.13",
15
- "@saltcorn/markup": "1.0.0-beta.13",
16
- "@saltcorn/plugins-loader": "1.0.0-beta.13",
17
- "@saltcorn/sbadmin2": "1.0.0-beta.13",
10
+ "@saltcorn/base-plugin": "1.0.0-beta.14",
11
+ "@saltcorn/builder": "1.0.0-beta.14",
12
+ "@saltcorn/data": "1.0.0-beta.14",
13
+ "@saltcorn/admin-models": "1.0.0-beta.14",
14
+ "@saltcorn/filemanager": "1.0.0-beta.14",
15
+ "@saltcorn/markup": "1.0.0-beta.14",
16
+ "@saltcorn/plugins-loader": "1.0.0-beta.14",
17
+ "@saltcorn/sbadmin2": "1.0.0-beta.14",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -184,8 +184,21 @@ function apply_showif() {
184
184
  var current = e.attr("data-selected") || e.val();
185
185
  //console.log({ field: e.attr("name"), target: data[0], val, current });
186
186
  e.empty();
187
+ //TODO clean repetition in following cose
187
188
  (options || []).forEach((o) => {
188
- if (
189
+ if (o && o.optgroup) {
190
+ const opts = o.options
191
+ .map(
192
+ (innero) =>
193
+ `<option ${
194
+ `${current}` === `${innero.value || innero}` ? "selected " : ""
195
+ }value="${innero.value || innero}">${
196
+ innero.label || innero
197
+ }</option>`
198
+ )
199
+ .join("");
200
+ e.append($(`<optgroup label="${o.label}">` + opts + "</optgroup>"));
201
+ } else if (
189
202
  !(o && typeof o.label !== "undefined" && typeof o.value !== "undefined")
190
203
  ) {
191
204
  if (`${current}` === `${o}`)
package/routes/actions.js CHANGED
@@ -57,21 +57,6 @@ const {
57
57
  blocklyToolbox,
58
58
  } = require("../markup/blockly.js");
59
59
 
60
- /**
61
- * @returns {Promise<object>}
62
- */
63
- const getActions = async () => {
64
- return Object.entries(getState().actions).map(([k, v]) => {
65
- const hasConfig = !!v.configFields;
66
- const requireRow = !!v.requireRow;
67
- return {
68
- name: k,
69
- hasConfig,
70
- requireRow,
71
- };
72
- });
73
- };
74
-
75
60
  /**
76
61
  * Show list of Actions (Triggers) (HTTP GET)
77
62
  * @name get
@@ -96,7 +81,7 @@ router.get(
96
81
  triggers = triggers.filter((t) => tagged_trigger_ids.has(t.id));
97
82
  filterOnTag = await Tag.findOne({ id: +req.query._tag });
98
83
  }
99
- const actions = await getActions();
84
+ const actions = Trigger.abbreviated_actions;
100
85
  send_events_page({
101
86
  res,
102
87
  req,
@@ -156,7 +141,7 @@ const triggerForm = async (req, trigger) => {
156
141
  value: r.id,
157
142
  label: r.role,
158
143
  }));
159
- const actions = await getActions();
144
+ const actions = Trigger.abbreviated_actions;
160
145
  const tables = await Table.find({});
161
146
  let id;
162
147
  let form_action;
@@ -168,14 +153,11 @@ const triggerForm = async (req, trigger) => {
168
153
  const hasChannel = Object.entries(getState().eventTypes)
169
154
  .filter(([k, v]) => v.hasChannel)
170
155
  .map(([k, v]) => k);
171
- const allActions = actions.map((t) => t.name);
172
- allActions.push("Multi-step action");
156
+
157
+ const allActions = Trigger.action_options(false);
173
158
  const table_triggers = ["Insert", "Update", "Delete", "Validate"];
174
159
  const action_options = {};
175
- const actionsNotRequiringRow = actions
176
- .filter((a) => !a.requireRow)
177
- .map((t) => t.name);
178
- actionsNotRequiringRow.push("Multi-step action");
160
+ const actionsNotRequiringRow = Trigger.action_options(true);
179
161
 
180
162
  Trigger.when_options.forEach((t) => {
181
163
  if (table_triggers.includes(t)) action_options[t] = allActions;
package/routes/admin.js CHANGED
@@ -673,7 +673,10 @@ router.get(
673
673
  const backup_file_prefix = getState().getConfig("backup_file_prefix");
674
674
  if (
675
675
  !isRoot ||
676
- !(filename.startsWith(backup_file_prefix) && filename.endsWith(".zip"))
676
+ !(
677
+ path.resolve(filename).startsWith(backup_file_prefix) &&
678
+ filename.endsWith(".zip")
679
+ )
677
680
  ) {
678
681
  res.redirect("/admin/backup");
679
682
  return;
@@ -1312,6 +1315,23 @@ router.get(
1312
1315
  )
1313
1316
  )
1314
1317
  ),
1318
+ div(
1319
+ { class: "form-group" },
1320
+ input({
1321
+ id: "deep_clean",
1322
+ class: "form-check-input",
1323
+ type: "checkbox",
1324
+ name: "deep_clean",
1325
+ checked: false,
1326
+ }),
1327
+ label(
1328
+ {
1329
+ for: "deep_clean",
1330
+ class: "form-label ms-2",
1331
+ },
1332
+ req.__("clean node_modules")
1333
+ )
1334
+ ),
1315
1335
  div(
1316
1336
  { class: "d-flex justify-content-end" },
1317
1337
  button(
@@ -1343,7 +1363,17 @@ router.get(
1343
1363
  })
1344
1364
  );
1345
1365
 
1346
- const doInstall = async (req, res, version, runPull) => {
1366
+ const cleanNodeModules = async () => {
1367
+ const topSaltcornDir = path.join(__dirname, "..", "..", "..", "..", "..");
1368
+ if (path.basename(topSaltcornDir) === "@saltcorn")
1369
+ await fs.promises.rm(topSaltcornDir, { recursive: true, force: true });
1370
+ else
1371
+ throw new Error(
1372
+ `'${topSaltcornDir}' is not a Saltcorn installation directory`
1373
+ );
1374
+ };
1375
+
1376
+ const doInstall = async (req, res, version, deepClean, runPull) => {
1347
1377
  if (db.getTenantSchema() !== db.connectObj.default_schema) {
1348
1378
  req.flash("error", req.__("Not possible for tenant"));
1349
1379
  res.redirect("/admin");
@@ -1353,6 +1383,14 @@ const doInstall = async (req, res, version, runPull) => {
1353
1383
  ? req.__("Starting upgrade, please wait...\n")
1354
1384
  : req.__("Installing %s, please wait...\n", version)
1355
1385
  );
1386
+ if (deepClean) {
1387
+ res.write(req.__("Cleaning node_modules...\n"));
1388
+ try {
1389
+ await cleanNodeModules();
1390
+ } catch (e) {
1391
+ res.write(req.__("Error cleaning node_modules: %s\n", e.message));
1392
+ }
1393
+ }
1356
1394
  const child = spawn(
1357
1395
  "npm",
1358
1396
  ["install", "-g", `@saltcorn/cli@${version}`, "--unsafe"],
@@ -1390,8 +1428,8 @@ const doInstall = async (req, res, version, runPull) => {
1390
1428
  };
1391
1429
 
1392
1430
  router.post("/install", isAdmin, async (req, res) => {
1393
- const { version } = req.body;
1394
- await doInstall(req, res, version, false);
1431
+ const { version, deep_clean } = req.body;
1432
+ await doInstall(req, res, version, deep_clean === "on", false);
1395
1433
  });
1396
1434
 
1397
1435
  /**
@@ -1404,7 +1442,7 @@ router.post(
1404
1442
  "/upgrade",
1405
1443
  isAdmin,
1406
1444
  error_catcher(async (req, res) => {
1407
- await doInstall(req, res, "latest", true);
1445
+ await doInstall(req, res, "latest", false, true);
1408
1446
  })
1409
1447
  );
1410
1448
  /**
@@ -2877,17 +2915,66 @@ router.get(
2877
2915
  })
2878
2916
  );
2879
2917
 
2918
+ const validateBuildDirName = (buildDirName) => {
2919
+ // ensure characters
2920
+ if (!/^[a-zA-Z0-9_-]+$/.test(buildDirName)) {
2921
+ getState().log(
2922
+ 4,
2923
+ `Invalid characters in build directory name '${buildDirName}'`
2924
+ );
2925
+ return false;
2926
+ }
2927
+ // ensure format is 'build_1234567890'
2928
+ if (!/^build_\d+$/.test(buildDirName)) {
2929
+ getState().log(4, `Invalid build directory name format '${buildDirName}'`);
2930
+ return false;
2931
+ }
2932
+ return true;
2933
+ };
2934
+
2935
+ const validateBuildDir = (buildDir, rootPath) => {
2936
+ const resolvedBuildDir = path.resolve(buildDir);
2937
+ if (!resolvedBuildDir.startsWith(path.join(rootPath, "mobile_app"))) {
2938
+ getState().log(4, `Invalid build directory path '${buildDir}'`);
2939
+ return false;
2940
+ }
2941
+ return true;
2942
+ };
2943
+
2880
2944
  router.get(
2881
2945
  "/build-mobile-app/result",
2882
2946
  isAdmin,
2883
2947
  error_catcher(async (req, res) => {
2884
2948
  const { build_dir_name } = req.query;
2949
+ if (!validateBuildDirName(build_dir_name)) {
2950
+ return res.sendWrap(req.__(`Admin`), {
2951
+ above: [
2952
+ {
2953
+ type: "card",
2954
+ title: req.__("Build Result"),
2955
+ contents: div(req.__("Invalid build directory name")),
2956
+ },
2957
+ ],
2958
+ });
2959
+ }
2885
2960
  const rootFolder = await File.rootFolder();
2886
2961
  const buildDir = path.join(
2887
2962
  rootFolder.location,
2888
2963
  "mobile_app",
2889
2964
  build_dir_name
2890
2965
  );
2966
+ if (!validateBuildDir(buildDir, rootFolder.location)) {
2967
+ return res.sendWrap(req.__(`Admin`), {
2968
+ above: [
2969
+ {
2970
+ type: "card",
2971
+ title: req.__("Build Result"),
2972
+ contents: div(req.__("Invalid build directory path")),
2973
+ },
2974
+ ],
2975
+ });
2976
+ }
2977
+
2891
2978
  const files = await Promise.all(
2892
2979
  fs
2893
2980
  .readdirSync(buildDir)
@@ -234,7 +234,14 @@ router.post(
234
234
  isAdmin,
235
235
  error_catcher(async (req, res) => {
236
236
  const { lang, defstring } = req.params;
237
-
237
+ if (
238
+ lang === "__proto__" ||
239
+ defstring === "__proto__" ||
240
+ lang === "constructor"
241
+ ) {
242
+ res.redirect(`/`);
243
+ return;
244
+ }
238
245
  const cfgStrings = getState().getConfigCopy("localizer_strings");
239
246
  if (cfgStrings[lang]) cfgStrings[lang][defstring] = text(req.body.value);
240
247
  else cfgStrings[lang] = { [defstring]: text(req.body.value) };
package/routes/plugins.js CHANGED
@@ -49,6 +49,8 @@ const {
49
49
  input,
50
50
  label,
51
51
  text,
52
+ script,
53
+ domReady,
52
54
  } = require("@saltcorn/markup/tags");
53
55
  const { search_bar } = require("@saltcorn/markup/helpers");
54
56
  const fs = require("fs");
@@ -614,13 +616,14 @@ router.get(
614
616
  res.set("Page-Title", req.__("%s versions", text(withoutOrg)));
615
617
  const versions = Object.keys(pkgInfo.versions);
616
618
  if (versions.length === 0) throw new Error(req.__("No versions found"));
619
+ const tags = pkgInfo["dist-tags"] || {};
617
620
  let selected = null;
618
621
  if (getState().plugins[plugin.name]) {
619
622
  const mod = await load_plugins.requirePlugin(plugin);
620
623
  if (mod) selected = mod.version;
621
624
  }
622
625
  if (!selected) selected = versions[versions.length - 1];
623
- const packageJson = require("../package.json");
626
+ const scVersion = getState().scVersion;
624
627
  return res.send(
625
628
  form(
626
629
  {
@@ -630,6 +633,7 @@ router.get(
630
633
  input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
631
634
  div(
632
635
  { class: "form-group" },
636
+ // version
633
637
  label(
634
638
  {
635
639
  for: "version_select",
@@ -645,7 +649,7 @@ router.get(
645
649
  },
646
650
  versions
647
651
  .filter((v) =>
648
- isVersionSupported(v, pkgInfo.versions, packageJson.version)
652
+ isVersionSupported(v, pkgInfo.versions, scVersion)
649
653
  )
650
654
  .map((version) =>
651
655
  option({
@@ -655,6 +659,37 @@ router.get(
655
659
  selected: version === selected,
656
660
  })
657
661
  )
662
+ ),
663
+ // tag
664
+ label(
665
+ {
666
+ for: "tag_select",
667
+ class: "form-label fw-bold mt-2",
668
+ },
669
+ req.__("Tags")
670
+ ),
671
+ select(
672
+ {
673
+ id: "tag_select",
674
+ class: "form-control form-select",
675
+ },
676
+ option({
677
+ id: "empty_opt",
678
+ value: "",
679
+ label: req.__("Select tag"),
680
+ selected: true,
681
+ }),
682
+ Object.keys(tags)
683
+ .filter((tag) =>
684
+ isVersionSupported(tags[tag], pkgInfo.versions, scVersion)
685
+ )
686
+ .map((tag) =>
687
+ option({
688
+ id: `${tag}_opt`,
689
+ value: tags[tag],
690
+ label: `${tag} (${tags[tag]})`,
691
+ })
692
+ )
658
693
  )
659
694
  ),
660
695
  div(
@@ -676,7 +711,19 @@ router.get(
676
711
  req.__("Install")
677
712
  )
678
713
  )
679
- )
714
+ ) +
715
+ script(
716
+ domReady(`
717
+ document.getElementById('tag_select').onchange = () => {
718
+ const version = document.getElementById('tag_select').value;
719
+ if (version) document.getElementById('version_select').value = version;
720
+ };
721
+ document.getElementById('version_select').onchange = () => {
722
+ const tagSelect = document.getElementById('tag_select');
723
+ tagSelect.value = '';
724
+ };
725
+ `)
726
+ )
680
727
  );
681
728
  } catch (error) {
682
729
  getState().log(
@@ -1184,9 +1231,20 @@ router.get(
1184
1231
  const update_permitted =
1185
1232
  db.getTenantSchema() === db.connectObj.default_schema &&
1186
1233
  plugin_db.source === "npm";
1187
- const latest =
1234
+
1235
+ let latest =
1188
1236
  update_permitted &&
1189
1237
  (await get_latest_npm_version(plugin_db.location, 1000));
1238
+ if (
1239
+ latest &&
1240
+ !isVersionSupported(latest, await load_plugins.getEngineInfos(plugin_db)) // with cache
1241
+ ) {
1242
+ // with force fetch
1243
+ latest = supportedVersion(
1244
+ latest,
1245
+ await load_plugins.getEngineInfos(plugin_db, true)
1246
+ );
1247
+ }
1190
1248
  const can_update = update_permitted && latest && mod.version !== latest;
1191
1249
  const can_select_version = update_permitted && plugin_db.source === "npm";
1192
1250
  let pkgjson;
@@ -1332,8 +1390,8 @@ router.get(
1332
1390
  error_catcher(async (req, res) => {
1333
1391
  const schema = db.getTenantSchema();
1334
1392
  if (schema === db.connectObj.default_schema) {
1335
- await upgrade_all_tenants_plugins((p, f) =>
1336
- load_plugins.loadPlugin(p, f)
1393
+ await upgrade_all_tenants_plugins((p, f, forceFetch) =>
1394
+ load_plugins.loadPlugin(p, f, forceFetch)
1337
1395
  );
1338
1396
  req.flash(
1339
1397
  "success",
@@ -1344,7 +1402,9 @@ router.get(
1344
1402
  } else {
1345
1403
  const installed_plugins = await Plugin.find({});
1346
1404
  for (const plugin of installed_plugins) {
1347
- await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
1405
+ await plugin.upgrade_version((p, f, forceFetch) =>
1406
+ load_plugins.loadPlugin(p, f, forceFetch)
1407
+ );
1348
1408
  }
1349
1409
  req.flash("success", req.__(`Modules up-to-date`));
1350
1410
  await restart_tenant(loadAllPlugins);
@@ -1370,16 +1430,11 @@ router.get(
1370
1430
  const { name } = req.params;
1371
1431
 
1372
1432
  const plugin = await Plugin.findOne({ name });
1373
- const pkgInfo = await npmFetch.json(
1374
- `https://registry.npmjs.org/${plugin.location}`
1375
- );
1433
+ const versions = await load_plugins.getEngineInfos(plugin, true);
1434
+
1376
1435
  await plugin.upgrade_version(
1377
1436
  (p, f) => load_plugins.loadPlugin(p, f),
1378
- supportedVersion(
1379
- "latest",
1380
- pkgInfo.versions,
1381
- require("../package.json").version
1382
- )
1437
+ supportedVersion("latest", versions, require("../package.json").version)
1383
1438
  );
1384
1439
  req.flash("success", req.__(`Module up-to-date`));
1385
1440
 
package/serve.js CHANGED
@@ -27,7 +27,7 @@ const {
27
27
  loadAndSaveNewPlugin,
28
28
  loadPlugin,
29
29
  } = require("./load_plugins");
30
- const { getConfig } = require("@saltcorn/data/models/config");
30
+ const { getConfig, setConfig } = require("@saltcorn/data/models/config");
31
31
  const { migrate } = require("@saltcorn/data/migrate");
32
32
  const socketio = require("socket.io");
33
33
  const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
@@ -77,6 +77,17 @@ const ensureJwtSecret = () => {
77
77
  }
78
78
  };
79
79
 
80
+ /**
81
+ * Ensure the engines cache is up to date with the current sc version
82
+ */
83
+ const ensureEnginesCache = async () => {
84
+ const cacheScVersion = await getConfig("engines_cache_sc_version", "");
85
+ if (!cacheScVersion || cacheScVersion !== getState().scVersion) {
86
+ await setConfig("engines_cache", {});
87
+ await setConfig("engines_cache_sc_version", getState().scVersion);
88
+ }
89
+ };
90
+
80
91
  // helpful https://gist.github.com/jpoehls/2232358
81
92
  /**
82
93
  * @param {object} opts
@@ -222,7 +233,10 @@ module.exports =
222
233
  dev,
223
234
  ...appargs
224
235
  } = {}) => {
225
- ensureJwtSecret();
236
+ if (cluster.isMaster) {
237
+ ensureJwtSecret();
238
+ await ensureEnginesCache();
239
+ }
226
240
  process.on("unhandledRejection", (reason, p) => {
227
241
  console.error(reason, "Unhandled Rejection at Promise");
228
242
  });