Repository: node-red/node-red (js)

Issue:
3.1.3 - stale cache causing "change node: RED.utils.validateTypedProperty is not a function"
### Current Behavior

Since one of the recent upgrades ( I was running 3.1.0 without the symptom, the symptom occurs with 3.1.3), the "change" and "filter"-nodes are marked with a red triangle in the editor. I have not been able to observe other side-effects.

This looks related: https://github.com/node-red/node-red/pull/4368

### Expected Behavior

_No response_

### Steps To Reproduce

add a change node to a new flow, observe the error message "RED.utils.validateTypedProperty is not a function"

### Example flow

```
paste your flow here
[
    {
        "id": "e8390a6a0ba978e8",
        "type": "change",
        "z": "412362beaf88c86c",
        "name": "block input from rkueche",
        "rules": [
            {
                "t": "set",
                "p": "block_rkueche",
                "pt": "flow",
                "to": "true",
                "tot": "bool"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 850,
        "y": 3160,
        "wires": [
            [
                "d1a2a8a508cf7509"
            ]
        ]
    }
]
```


### Environment

- Node-RED version: 3.1.3
- Node.js version: 
- npm version: 
- Platform/OS:  docker.io/nodered/node-red:3.1.3
- Browser: firefox


Pull request:
Restore caching busting functionality without using explict version number
Fixes #4503

PR #4179 removed the `version` property due to security concerns about exposing it via unsecured end points. At the time I overlooked this property was also being injected into `index.mst` as the cache-busting query parameter value.

This PR restores the cache busting functionality, but uses a hash of the version. It isn't ideal in that there is still a 1:1 mapping between hash and version - so this just slightly obscures the value. .. but it's better than nothing.


Patch:
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js
index 42be1f270..648daa09b 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js
@@ -51,7 +51,7 @@ module.exports = {
 
             var ui = require("./ui");
 
-            ui.init(runtimeAPI);
+            ui.init(settings, runtimeAPI);
 
             const editorApp = apiUtil.createExpressApp(settings)
 
diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js
index 998816f5e..e7bf15069 100644
--- a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js
+++ b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  **/
+const crypto = require('crypto')
 var express = require('express');
 var fs = require("fs");
 var path = require("path");
@@ -24,13 +25,16 @@ var apiUtils = require("../util");
 var theme = require("./theme");
 
 var runtimeAPI;
+let settings;
 var editorClientDir = path.dirname(require.resolve("@node-red/editor-client"));
 var defaultNodeIcon = path.join(editorClientDir,"public","red","images","icons","arrow-in.svg");
 var editorTemplatePath = path.join(editorClientDir,"templates","index.mst");
 var editorTemplate;
+let cacheBuster
 
 module.exports = {
-    init: function(_runtimeAPI) {
+    init: function(_settings, _runtimeAPI) {
+        settings = _settings;
         runtimeAPI = _runtimeAPI;
         editorTemplate = fs.readFileSync(editorTemplatePath,"utf8");
         Mustache.parse(editorTemplate);
@@ -91,6 +95,12 @@ module.exports = {
     },
 
     editor: async function(req,res) {
+        if (!cacheBuster) {
+            // settings.instanceId is set asynchronously to the editor-api
+            // being initiaised. So we defer calculating the cacheBuster hash
+            // until the first load of the editor
+            cacheBuster = crypto.createHash('md5').update(`${settings.version || 'version'}-${settings.instanceId || 'instanceId'}`).digest("hex").substring(0,12)    
+        }
 
         let sessionMessages;
         if (req.session && req.session.messages) {
@@ -99,6 +109,7 @@ module.exports = {
         }
         res.send(Mustache.render(editorTemplate,{
             sessionMessages,
+            cacheBuster,
             ...await theme.context()
         }));
     },
diff --git a/packages/node_modules/@node-red/editor-client/templates/index.mst b/packages/node_modules/@node-red/editor-client/templates/index.mst
index 01bf06545..748a54bab 100644
--- a/packages/node_modules/@node-red/editor-client/templates/index.mst
+++ b/packages/node_modules/@node-red/editor-client/templates/index.mst
@@ -24,24 +24,24 @@
 <title>{{ page.title }}</title>
 <link rel="icon" type="image/png" href="{{ page.favicon }}">
 <link rel="mask-icon" href="{{ page.tabicon.icon }}" color="{{ page.tabicon.colour }}">
-<link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css?v={{ page.version }}">
-<link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css?v={{ page.version }}">
-<link rel="stylesheet" href="red/style.min.css?v={{ page.version }}">
+<link rel="stylesheet" href="vendor/jquery/css/base/jquery-ui.min.css?v={{ cacheBuster }}">
+<link rel="stylesheet" href="vendor/font-awesome/css/font-awesome.min.css?v={{ cacheBuster }}">
+<link rel="stylesheet" href="red/style.min.css?v={{ cacheBuster }}">
 {{#page.css}}
 <link rel="stylesheet" href="{{.}}">
 {{/page.css}}
 {{#asset.vendorMonaco}}
-<link rel="stylesheet" href="vendor/monaco/style.css?v={{ page.version }}">
+<link rel="stylesheet" href="vendor/monaco/style.css?v={{ cacheBuster }}">
 {{/asset.vendorMonaco}}
 </head>
 <body spellcheck="false">
 <div id="red-ui-editor"></div>
-<script src="vendor/vendor.js?v={{ page.version }}"></script>
+<script src="vendor/vendor.js?v={{ cacheBuster }}"></script>
 {{#asset.vendorMonaco}}
-<script src="{{ asset.vendorMonaco }}?v={{ page.version }}"></script>
+<script src="{{ asset.vendorMonaco }}?v={{ cacheBuster }}"></script>
 {{/asset.vendorMonaco}}
-<script src="{{ asset.red }}?v={{ page.version }}"></script>
-<script src="{{ asset.main }}?v={{ page.version }}"></script>
+<script src="{{ asset.red }}?v={{ cacheBuster }}"></script>
+<script src="{{ asset.main }}?v={{ cacheBuster }}"></script>
 {{# page.scripts }}
 <script src="{{.}}"></script>
 {{/ page.scripts }}
diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js
index 74f03c55c..39b2025c5 100644
--- a/packages/node_modules/@node-red/runtime/lib/index.js
+++ b/packages/node_modules/@node-red/runtime/lib/index.js
@@ -27,6 +27,7 @@ var express = require("express");
 var path = require('path');
 var fs = require("fs");
 var os = require("os");
+const crypto = require("crypto")
 
 const {log,i18n,events,exec,util,hooks} = require("@node-red/util");
 
@@ -51,7 +52,7 @@ var adminApi = {
 var nodeApp;
 var adminApp;
 var server;
-
+let userSettings;
 
 /**
  * Initialise the runtime module.
@@ -61,8 +62,9 @@ var server;
  *                              better abstracted.
  * @memberof @node-red/runtime
  */
-function init(userSettings,httpServer,_adminApi) {
+function init(_userSettings,httpServer,_adminApi) {
     server = httpServer;
+    userSettings = _userSettings
 
     if (server && server.on) {
         // Add a listener to the upgrade event so that we can properly timeout connection
@@ -134,7 +136,12 @@ function start() {
         .then(function() { return settings.load(storage)})
         .then(function() { return library.init(runtime)})
         .then(function() {
-
+            if (settings.available()) {
+                if (settings.get('instanceId') === undefined) {
+                    settings.set('instanceId', crypto.randomBytes(8).toString('hex'))
+                }
+                userSettings.instanceId = settings.get('instanceId') || ''
+            }
             if (log.metric()) {
                 runtimeMetricInterval = setInterval(function() {
                     reportMetrics();


Test patch:
diff --git a/test/unit/@node-red/editor-api/lib/editor/ui_spec.js b/test/unit/@node-red/editor-api/lib/editor/ui_spec.js
index 0380adcde..beb562650 100644
--- a/test/unit/@node-red/editor-api/lib/editor/ui_spec.js
+++ b/test/unit/@node-red/editor-api/lib/editor/ui_spec.js
@@ -29,7 +29,7 @@ describe("api/editor/ui", function() {
     var app;
 
     before(function() {
-        ui.init({
+        ui.init({}, {
             nodes: {
                 getIcon: function(opts) {
                     return new Promise(function(resolve,reject) {