Uibuilder vNext (v5) - updates

Hi all, after some hiatus due to personal and work committments, I can finally announce that I've managed to spend some quality time on the uibuilder code over the Christmas and New Year period.

Many thanks to those who have already tried out the new code including @UnborN, @dczysz, @alex88, and @shrickus that I know of.

Whether you have tried out vNext or not, the code is now getting close to release quality as far as I can tell and all outstanding fixes are in. So please do try it out and let me know of any issues you find.

As usual, there are a ton of changes - see the changelog for details. Also as usual, I've failed to complete the built-in security features - sorry! I continue to recommend using a reverse-proxy setup anyway to secure Node-RED.

I think that you will like the new Editor panel layout which is a lot cleaner and has much better error reporting.

There is one major breaking change - sorry about that but it really will help everyone. On installation of vNext, you need to reinstall any front-end libraries you previously had installed. That is because they no longer live in the userDir folder but instead in the uibRoot folder. This prevents any clashes between them and any custom node installs.

Also don't forget the new node uib-sender which provides some nice features for sending and receiving messages from elsewhere in your flows.

Socket.IO can now also be given custom options. Useful if you need to make the default message size larger.

If you use the custom ExpressJS server settings for uibuilder, you can now also have it use different key and certificate files from Node-RED if you want to.


To install, run

cd ~/.node-red
npm install totallyinformation/node-red-contrib-uibuilder#vNext

Don't forget to update your uibuilder node instances so that they get the latest settings.

4 Likes

Hello and thumbs up for the hard work on this excellent tool

I didnt have a chance to try the new version till now .. but i was following the discussion with Dczysz.

In Windows i get an error after installation

Welcome to Node-RED
===================

2 Jan 23:33:32 - [info] Node-RED version: v2.1.4
2 Jan 23:33:32 - [info] Node.js  version: v14.18.2
2 Jan 23:33:32 - [info] Windows_NT 10.0.22000 x64 LE
2 Jan 23:33:33 - [info] Loading palette nodes
[uibuilder:runtimeSetup] Security setup error  Error: Cannot find module '../../typedefs.js'
Require stack:
- C:\Users\User\.node-red\uibuilder\.config\security.js
- C:\Users\User\.node-red\node_modules\node-red-contrib-uibuilder\nodes\libs\security.js
- C:\Users\User\.node-red\node_modules\node-red-contrib-uibuilder\nodes\libs\socket.js
- C:\Users\User\.node-red\node_modules\node-red-contrib-uibuilder\nodes\uibuilder.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\node_modules\@node-red\registry\lib\loader.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\node_modules\@node-red\registry\lib\index.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\node_modules\@node-red\runtime\lib\nodes\index.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\node_modules\@node-red\runtime\lib\index.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\lib\red.js
- C:\Users\User\AppData\Roaming\npm\node_modules\node-red\red.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:902:15)
    at Function.Module._load (internal/modules/cjs/loader.js:746:27)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:93:18)
    at Object.<anonymous> (C:\Users\User\.node-red\uibuilder\.config\security.js:49:18)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Module.require (internal/modules/cjs/loader.js:974:19) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    'C:\\Users\\User\\.node-red\\uibuilder\\.config\\security.js',
    'C:\\Users\\User\\.node-red\\node_modules\\node-red-contrib-uibuilder\\nodes\\libs\\security.js',
    'C:\\Users\\User\\.node-red\\node_modules\\node-red-contrib-uibuilder\\nodes\\libs\\socket.js',
    'C:\\Users\\User\\.node-red\\node_modules\\node-red-contrib-uibuilder\\nodes\\uibuilder.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\node_modules\\@node-red\\registry\\lib\\loader.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\node_modules\\@node-red\\registry\\lib\\index.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\node_modules\\@node-red\\runtime\\lib\\nodes\\index.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\node_modules\\@node-red\\runtime\\lib\\index.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\lib\\red.js',
    'C:\\Users\\User\\AppData\\Roaming\\npm\\node_modules\\node-red\\red.js'
  ]
}
2 Jan 23:33:36 - [info] Dashboard version 3.1.3 started at /ui
2 Jan 23:33:36 - [info] Settings file  : \Users\User\.node-red\settings.js

Sheesh, there's always one! :grimacing:

That is super confusing because it appears to be complaining about something that should only be referenced by eslint and never in live running!

Can you please check the file C:\Users\User\.node-red\uibuilder\.config\security.js and see what it contains. It may be that it is an old version, perhaps I should use a new filename for vNext?

C:\Users\User\.node-red\uibuilder\.config\security.js

'use strict'

/**
 * Standard msg._auth object exchanged in msg's between front-end and server
 * @typedef {import('../../index').MsgAuth} MsgAuth
 */ 
/**
 * Validated user object returned by the userValidate function
 * typedef {import('./security').userValidation} userValidation 
 */

const TYPEDEFS = require('../../typedefs.js')
/**
 * typedef {TYPEDEFS.MsgAuth} MsgAuth
 * @typedef {TYPEDEFS.userValidation} userValidation
 * @typedef {TYPEDEFS.userMetadata} userMetadata
 */

module.exports = {
    /** Validate user against your own user data.
     * The minimum input data is _auth.id which must represent a unique ID.
     * Called from the logon function (uiblib.js::logon) which is triggered by a uibuilder control msg from the client of type 'logon'.
     * May also be called to revalidate users at any time.
     * @param {MsgAuth} _auth Required. 
     * @return {boolean|userValidation} Either true/false or Object of type userValidation
     */
    userValidate: function(_auth) {
        console.log(`[uibuilder:security.js] userValidate Security from ${__filename} used. Replace this template with your own code. _auth:`, _auth)

        /** Manual "test" ID validates - this will be replaced with a suitable lookup in your code - maybe from a database or a file.
         * You will also want to pass through some kind of password to validate the user.
         */
        if ( _auth.id === 'test' ) {
            console.log(`[uibuilder:security.js] User id ${_auth.id} has been validated`)

            // Example of simple boolean return
            return true

            //Example of object return with additional data that gets passed back to the client
            // return {
            //     userValidated: true,
            //     authData: {
            //         name: 'Me',
            //         message: 'Hi you, don\'t forget to change your password :)'
            //     }
            // }
        }

        // In all other cases, fail the validation - optionally, you can include more info here by returning an object.
        return false
        // return {
        //     userValidated: false,
        //     authData: {
        //         message: `Login failed, User id ${_auth.id} not recognised.`
        //     }
        // }
        
    } // ---- End of userValidate ---- //


}

//EOF

[EDIT]

yea .. it looks like an old file .. from 23/12/2020

image

Ah, as I thought. The wrong version. My fault, I should have changed the name.

You can try replacing with this which is the latest template:

/* globals module */
/**
 * Copyright (c) 2020-2021 Julian Knight (Totally Information)
 * https://it.knightnet.org.uk
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is 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.
 **/
/**
 * Template security functions for uibuilder.
 * Only used if the node.useSecurity flag is set.
 * Please replace with your own code.
 * 
 * You MUST export the following functions:
 *   - userValidate - based on an id, lookup the user data to see if the user is valid.
 *                    MUST return a boolean or object of type userValidation.
 *                    Called from the server's logon process. (uiblib.js::logon)
 *      
 * 
 * Each function MUST at least return true/false. See each function for more details.
 *
 * NOTES & WARNINGS:
 *   1) IF there is an error in this JavaScript, it is very likely that Node-RED will terminate.
 *   2) You can use different security.js files for different instances of uibuilder.
 *      Simply, place a securiy.js file in the instances root folder (e.g ~/.node-red/uibuilder/<url>/security.js)
 *      Note, however, that this means that the security.js file can be edited using the admin Editor.
 *      You have to restart Node-RED to pick up the new file.
 */
'use strict'

/** Typedef: Define the _auth object
 * @typedef {Object} _auth The standard auth object used by uibuilder security. See docs for details.
 * Note that any other data may be passed from your front-end code in the uibAuth object.
 * @property {String} id Required. A unique user identifier.
 * @property {String} [password] Required for input to login only.
 * @property {String} [jwt] Required if logged in. Needed for ongoing session validation and management.
 * @property {String} [sessionExpiry] Required if logged in. ISO8601 formatted date/time string. Needed for ongoing session validation and management.
 * @property {boolean} [userValidated] Required after user validation. Whether the input ID (and optional additional data from the _auth object) validated correctly or not.
 * @property {Object=} [info] Optional metadata about the user.
 */

/** Validate user against your own user data. 
 * The minimum input data is _auth.id which must represent a unique ID.
 * Called when uib receives an 'auth' control msg and on receipt of any standard msg - via this.authCheck2
 * May also be called to revalidate users at any time.
 * SHOULD *NOT* CONTAIN A PASSWORD CHECK
 * SHOULD CONTAIN A SESSION CHECK
 * SHOULD BE KEPT SHORT - as it is called for every msg
 * SHOULD UPDATE LAST SEEN timestamp
 * REQUIRES a logon process that keeps track of client auths
 * @param {_auth} _auth Required. 
 * @returns {_auth} Updated _auth object
 */
function userValidate(_auth) {
    console.log(`[uibuilder:.common/security.js] userValidate Security from '${__filename}' used. Replace this template with your own code. _auth:`, _auth)

    /** Always start by assuming the user auth is invalid
     * Because a client could change it, previous setting must not be trusted here.
     */
    _auth.userValidated = false

    /** If the JWT is invalid, then the user auth is revoked */
    if ( _auth.info.validJwt !== true ) this.removeUserAuth(_auth)
    
    /** This is optional, a default pseudo-user that always validates
     */
    if ( _auth.id === 'anonymous' ) {

        _auth.userValidated = true

        // Add any extra metadata you want
        _auth.info.name = 'Anonymous'
        _auth.info.message = 'Lets you control data access in your flows'

    } else {

        // In all other cases, fail the validation - optionally, you can include more info here as well.
        _auth.userValidated = false
        _auth.info.message = 'Ouch! Sorry, that login is not valid'

    }
    
    //? Needed?
    if ( _auth.userValidated === false ) this.removeUserAuth(_auth)

    return _auth

} // ---- End of userValidate ---- //

/** Called from security.logon */
function captureUserAuth(_auth) {}

function removeUserAuth(_auth) {
    if ( uibSessions[_auth.id] ) {
        delete uibSessions[_auth.id]
    }
    console.log(`[uibuilder:.common/security.js:removeUserAuth] Clent session authorisation removed for ${_auth.id}`)
    return _auth
}

/** Validate user against your own user data. 
 * The minimum input data is _auth.id which must represent a unique ID.
 * Called when uib receives an 'auth' control msg and on receipt of any standard msg - via this.authCheck2
 * May also be called to revalidate users at any time.
 * SHOULD *NOT* CONTAIN A PASSWORD CHECK
 * SHOULD CONTAIN A SESSION CHECK
 * SHOULD BE KEPT SHORT - as it is called for every msg
 * SHOULD UPDATE LAST SEEN timestamp
 * REQUIRES a logon process that keeps track of client auths
 * @param {_auth} _auth Required. 
 * @param {number} sessionLength The number of seconds a session can last for before needing a new login
 * @returns {_auth} Updated _auth object
*/
function checkUserAuth(_auth, sessionLength) {
    //console.log(`[uibuilder:.common/security.js:checkUserAuth] Security from '${__filename}' used. Replace this template with your own code. _auth:`, _auth)

    /** Always start by assuming the user auth is invalid
     * Because a client could change it, previous setting must not be trusted here.
     */
    _auth.userValidated = false

    /** If the JWT is invalid, then the user auth is revoked */
    if ( _auth.info.validJwt !== true ) this.removeUserAuth(_auth)

    //! THIS SHOULD ONLY check if session, update lastSeen - move to captureUserAuth
    // If client session doesn't exist, create it
    if ( ! uibSessions[_auth.id] ) {
        uibSessions[_auth.id] = {id: _auth.id}
        // Add the session expiry timestamp
        let dt = new Date()
        dt.setSeconds(dt.getSeconds() + sessionLength)
        uibSessions[_auth.id].sessionExpiry = dt.toISOString()
        console.log(`[uibuilder:.common/security.js:checkUserAuth] Clent authorisation added ${_auth.id}`)
    }

    // If no valid session for this id, invalidate and return
    if ( ! uibSessions[_auth.id] ) {

        _auth.userValidated = false
        _auth.info.message = 'No session found, please log in'
        console.log('[uibuilder:.common/security.js:checkUserAuth] No session found')

    } else {

        // Otherwise update the timestamp ...
        const session = uibSessions[_auth.id]

        // Save last seen timestamp
        session.lastSeen = (new Date()).toISOString()

        // ... and check if the session has expired
        if ( session.lastSeen >= session.sessionExpiry ) {
            _auth.userValidated = false
            _auth.info.message = 'Session has expired, please log in again'
            console.log('[uibuilder:.common/security.js:checkUserAuth] Session expired, user not validated.', session)
        } else {
            _auth.userValidated = true
            _auth.info.message = 'Session updated'
        }

        console.log('[uibuilder:.common/security.js:checkUserAuth] Updated session.', session)

    }


    //? Needed?
    if ( _auth.userValidated === false ) _auth = this.removeUserAuth(_auth)

    return _auth

} // ---- End of checkUserAuth ---- //

// Silly client session store - replace with a file or database!
const uibSessions = {}

//! FOR TESTING
// Add the session expiry timestamp
let dt = new Date()
dt.setSeconds(dt.getSeconds() + 432000)
uibSessions['anonymous'] = {
    id: 'anonymous',
    sessionExpiry: dt.toISOString(),
}


module.exports = {

    /** Allow users to sign up for themselves.
     * Build in any workflow you like.
     * If external authorisation is required, you will need to create a separate app to write to your user store.
     * The simple template example will allow anyone to self-signup
     */
    userSignup: function () {},

    userValidate: userValidate,

    /** Capture user logins. Needed to control msgs from Node-RED to the Front-End
     * Unless some details are captured, it would not be possible to limit outgoing messages
     * to only authorised users.
     */
    captureUserAuth: captureUserAuth,
    removeUserAuth: removeUserAuth,
    checkUserAuth: checkUserAuth,

    jwtValidateCustom: function() {},

    jwtCreateCustom: function() {},

}
//EOF

I'm adding an extra error log message into the appropriate function. Will push shortly.

1 Like

Replaced the contents of the security.js file and now is working fine :+1:

Cool, I've pushed the added error message for clarity but I expect I'll need to do something more in order to make it more robust for people.

Thanks for trying it out :mage:

1 Like

@TotallyInformation

Well done, keep up the great work!

1 Like

Julian, I've finally had a chance to get back to testing this latest release. Looks like I do not know how to update my node instances, since I'm getting this error when I try to load the default index.html page:

    13 Jan 11:27:50 - [info] Started modified nodes
    TypeError: Cannot read property 'setHeader' of undefined
        at masterMiddleware (/home/srickus/develop/node-red-contrib-uibuilder/nodes/libs/web.js:712:17)
        at Layer.handle [as handle_request] (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/layer.js:95:5)
        at trim_prefix (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:323:13)
        at /home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:284:7
        at Function.process_params (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:341:12)
        at next (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:275:10)
        at Function.handle (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:174:3)
        at router (/home/srickus/develop/node-red-contrib-uibuilder/node_modules/express/lib/router/index.js:47:12)
        at Layer.handle [as handle_request] (/home/srickus/develop/node-red/node_modules/express/lib/router/layer.js:95:5)
        at trim_prefix (/home/srickus/develop/node-red/node_modules/express/lib/router/index.js:323:13)

Has anybody seen this? Any idea what step I missed?

That is very odd because it should have failed on the previous line if it were going to fail.

When did you last reinstall the node from GitHub? I don't tend to change version numbers while working on a branch so it isn't always obvious that you need to update.

I last updated that section of code on 3rd Jan according to VScode. It now looks like this:

        // Return a middleware handler
        return function masterMiddleware (/** @type {express.Request} */ req, /** @type {express.Response} */ res, /** @type {express.NextFunction} */ next) {
            //TODO: X-XSS-Protection only needed for html (and js?), not for css, etc
            const qSec = uib.customServer.type === 'http' ? false : true
            res
                // Help reduce risk of XSS and other attacks
                .setHeader('X-XSS-Protection','1;mode=block')
                .setHeader('X-Content-Type-Options','nosniff')
                //.setHeader('X-Frame-Options','SAMEORIGIN')
                //.setHeader('Content-Security-Policy',"script-src 'self'")
                // Tell the client that uibuilder is being used (overides the default "ExpressJS" entry)
                .setHeader('x-powered-by','uibuilder')
                // Tell the client what Socket.IO namespace to use,
                .setHeader('uibuilder-namespace', node.url) // only client accessible from xhr or web worker 
                .cookie('uibuilder-namespace', node.url, {
                    path: node.url, 
                    sameSite: true,
                    expires: 0, // session cookie only
                    secure: qSec,
                })
                // Give the client a fixed session id
                .cookie('uibuilder-client-id', nanoid(), {
                    path: node.url, 
                    sameSite: true,
                    expires: 0, // session cookie only
                    secure: qSec,
                })
                // Tell clients what httpNodeRoot to use (affects Socket.io path)
                .cookie('uibuilder-webRoot', uib.nodeRoot.replace(/\//g,''), {
                    //path: node.url, 
                    //sameSite: true,
                    expires: 0, // session cookie only
                    secure: qSec,
                })

            next()
        }
    } // --- End of addMasterMiddleware --- //

Line 712 is the 2nd of the setHeader lines.

The only other thing I can think of is what version of Node.js you are using? You need to be at least on v12.20 I think.

I noticed that too -- why didn't the first .setHeader fail?
Apparently, the first .setHeader function is NOT returning the res object...

I pulled the vNext branch this morning, and ran npm update:

[Thu 12:30:43 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ git status
On branch vNext
Your branch is up to date with 'origin/vNext'.

nothing to commit, working tree clean

[Thu 12:30:48 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ node -v
v12.22.8

[Thu 12:32:07 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ npm outdated
Package      Current   Wanted  Latest  Location                  Depended by
@types/node  14.18.5  14.18.5  17.0.8  node_modules/@types/node  node-red-contrib-uibuilder
execa          5.1.1    5.1.1   6.0.0  node_modules/execa        node-red-contrib-uibuilder

[Thu 12:35:10 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ npm list --no-unicode
node-red-contrib-uibuilder@5.0.0-dev /home/srickus/develop/node-red-contrib-uibuilder
+-- @totallyinformation/ti-common-event-handler@1.0.0
+-- @types/express@4.17.13
+-- @types/jquery@3.5.13
+-- @types/node-red@1.2.0
+-- @types/node@14.18.5
+-- degit@2.8.4
+-- eslint-plugin-es@4.1.0
+-- eslint-plugin-html@6.2.0
+-- eslint-plugin-jsdoc@37.6.1
+-- eslint-plugin-promise@6.0.0
+-- eslint-plugin-sonarjs@0.11.0
+-- eslint@8.6.0
+-- execa@5.1.1
+-- express-session@1.17.2
+-- express-validator@6.14.0
+-- express@4.17.2
+-- fast-glob@3.2.10
+-- fs-extra@10.0.0
+-- gulp-debug@4.0.0
+-- gulp-htmlmin@5.0.1
+-- gulp-include@2.4.1
+-- gulp-once@2.1.1
+-- gulp-prompt@1.2.0
+-- gulp-rename@2.0.0
+-- gulp-replace@1.1.3
+-- gulp-uglify@3.0.2
+-- gulp@4.0.2
+-- jsonwebtoken@8.5.1
+-- nanoid@3.1.32
+-- passport-local@1.0.0
+-- passport@0.5.2
+-- serve-index@1.9.1
`-- socket.io@4.4.1

[Thu 12:35:27 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ npm rebuild
rebuilt dependencies successfully

[Thu 12:36:51 PM]
srickus@ devshr: ~/develop/node-red-contrib-uibuilder (vNext)
$ npm -v
8.3.0

Don't know the problem and not reproduced here though I'm running on Windows. Maybe try reversing those statements and see if the error is in that same header?

Ok, it looks like I've fixed it... according to the express.Response 4.x docs there is no res.setHeader(...) method available.

I switched the code in web.js to use res.set(...) instead:

            res
                // Help reduce risk of XSS and other attacks
                .set('X-XSS-Protection','1;mode=block')
                .set('X-Content-Type-Options','nosniff')
                //.set('X-Frame-Options','SAMEORIGIN')
                //.set('Content-Security-Policy',"script-src 'self'")
                // Tell the client that uibuilder is being used (overides the default "ExpressJS" entry)
                .set('x-powered-by','uibuilder')
                // Tell the client what Socket.IO namespace to use,
                .set('uibuilder-namespace', node.url) // only client accessible from xhr or web worker 

setHeader() is a method of nodejs (http) and set() is of expressjs, according to the explanation here

Aha, yes -- from the code sample included in that StackOverflow thread, the res.header() method and its alias res.set() use return this; to allow chaining of multiple statements.

The original code used res.setHeader(), which apparently does NOT return a chainable object.

Still, it's odd that my Amazon linux2 server with express@4.17.2 running on nodejs v12.22.8 would throw an exception, while the windows server does not... :thinking:

Now that I know res.set() is an alias for res.header() -- which is a more informative method name -- I'm going to switch the code to use the latter.

Darn it, thanks both. Changing now, will push to GH later. going to use header rather than set as I think it shows the intent better. Will reduce to an object as well.

I don't understand why this works for me on Windows though. I've checked and it really does set those headers and cookies. Strange. But at least fixable.

1 Like

Fix pushed to GitHub.

Also included a temporary fix for something spotted by @meeki007 - namely that the pre-install script tells you to install VueJS and bootstrap-vue into the wrong location. I've taken that out for now. We will need a new future process that ties into the change of template function. That will be a much better option anyway and is now feasible thanks to uibuilder's newly revamped package handler.

Thanks Julian! It's working for me with the latest from vNext.

When I first started testing this branch, there were some very verbose output tables of ALL the express endpoints, including the ones inside the express.Router from the src/api.js file. Is there still a way to show those, or was that just some debugging code that has been cleaned up?

The route reporting functions are still there:

I just turned off the output once I'd proved the rework to put everything into separate routers. It took quite some working out to get the routers correct and to make sure everything got created and dropped in the right place and time. The new code is sooo much better though. And so much more flexible. So expect to see some more new features. I've not really got all the local API capabilities set up for example.

Sorry, should have said. That is in libs/web.js

Ok, never mind -- I found (and uncommented) the dumpInstanceRoutes() function in web.js

But now I think I can see why my api.js routes are NOT available in express...

Notice how every time a new uibuilder instance is added to the array of instance routers, the previously stored list is wiped out? The highlighted line shows undefined at the bottom, although it was just defined at the top of the page. The net results seems to be that the last instance "wins" :*(