frugal-iot-server 2.0.5 → 2.0.7

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/INSTALLATION.md CHANGED
@@ -308,9 +308,9 @@ node -v
308
308
 
309
309
  What each is for:
310
310
 
311
- * `nodejs` — the server needs **Node 18 or later**. Current Raspberry Pi OS (Debian 13, trixie)
312
- provides 20.19.2, which is fine, on 32-bit as well as 64-bit. Older images shipped Node 18, also
313
- fine. If `node -v` reports anything below 18, install a current version from
311
+ * `nodejs` — the server needs **Node 20.17 or later**. Current Raspberry Pi OS (Debian 13, trixie)
312
+ provides 20.19.2, which is fine, on 32-bit as well as 64-bit. Older images shipped Node 18, which
313
+ is too old. If `node -v` reports anything below 20.17, install a current version from
314
314
  [NodeSource](https://github.com/nodesource/distributions) instead.
315
315
  * `npm` — installs the server; it is a separate package from `nodejs` on Debian.
316
316
  * `sqlite3` — the database the server keeps its accounts in.
package/README.md CHANGED
@@ -8,7 +8,7 @@ thing in one go once you can ssh in.
8
8
 
9
9
  On a unix box that is already running ...
10
10
 
11
- Check you have node & npm installed `node -v` - version 18 or later.
11
+ Check you have node & npm installed `node -v` - version 20.17 or later.
12
12
  If not then you'll need nodejs from [nodejs.org](https://nodejs.org) or your package manager.
13
13
  You also need `sqlite3` and `zsh` for the setup commands below.
14
14
 
@@ -110,9 +110,9 @@ import { createAPIRouter, createAPIErrorHandler } from './lib/api-routes.js';
110
110
  import { buildConfigFor, hasPermissions } from './lib/config-for-user.js';
111
111
  import { ensureSecrets, addEnrolmentSecret, removeEnrolmentSecret } from './lib/secrets.js';
112
112
  import { syncUser, syncUserById, syncLoggers, syncNode, dropNode, syncBridge } from './lib/dynsec-server.js';
113
- import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode,
113
+ import { enrol, ENROL, enrolmentSecretsFor, makeRateLimiter, forgetNode, formerProjects,
114
114
  makeAttemptLog, setGrant, clearGrant, readGrants } from './lib/enrol.js';
115
- import { deleteRetained } from './lib/retained.js';
115
+ import { deleteRetained, clearRetainedUnder } from './lib/retained.js';
116
116
  import { replicaFor, bridgeForToken, noteBridgePull, startReplica } from './lib/replica.js';
117
117
  import { addMissingColumns } from './lib/schema-columns.js';
118
118
  import { createLoggerClient } from './lib/logger-client.js';
@@ -780,6 +780,38 @@ function addLoggedNodesToConfig() {
780
780
  });
781
781
  });
782
782
  }
783
+ /*
784
+ * A node has enrolled in a new project, so its old address is retired: enrolment, broker account,
785
+ * retained messages, and what the logger and /config.json remember of it.
786
+ *
787
+ * Done here, once it has arrived, rather than when the move is asked for. A sleeping node only gets
788
+ * the move when it next wakes, and it needs its old credential to receive it - removed earlier, the
789
+ * node would be refused, enrol again in the OLD project, and never hear of the move.
790
+ */
791
+ function retireFormerHome(org, project, nodeid, cb) {
792
+ waterfall([
793
+ (cb1) => forgetNode(db, org, project, nodeid, (err) => cb1(err)),
794
+ // Best effort, as for /node_reset - frugal-iot-diagnostic reports anything left behind.
795
+ (cb1) => dropNode(config, org, project, nodeid, () => cb1(null)),
796
+ (cb1) => clearRetainedUnder(config, org, `${org}/${project}/${nodeid}`, (err, result) => {
797
+ if (err) { console.error("Retained messages for", `${org}/${project}/${nodeid}`, "not cleared:", err.message); }
798
+ cb1(null, result);
799
+ }),
800
+ (result, cb1) => {
801
+ // After the retained messages, or the logger would hear them again and remember the node.
802
+ mqttLogger.forgetNode(org, project, nodeid);
803
+ const nodes = config.organizations?.[org]?.projects?.[project]?.nodes;
804
+ if (nodes) { delete nodes[nodeid]; }
805
+ console.log("Node", nodeid, "moved out of", `${org}/${project}`, "-",
806
+ result ? `${result.deleted} retained topic(s) cleared` : "retained topics left");
807
+ cb1(null);
808
+ },
809
+ ], (err) => {
810
+ if (err) { console.error("Could not retire", `${org}/${project}/${nodeid}:`, err.message); }
811
+ cb(null);
812
+ });
813
+ }
814
+
783
815
  // See lib/config-for-user.js - kept there so it can be tested without starting a server.
784
816
  function unsafeCopyConfigFor(user) {
785
817
  return buildConfigFor(config, user);
@@ -906,7 +938,13 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
906
938
  req.body, (err, cred) => {
907
939
  if (!err) {
908
940
  console.log("Enrolled", cred.username, req.body.lora ? "(LoRa-capable)" : "");
909
- return res.status(200).json(cred);
941
+ res.status(200).json(cred);
942
+ const { org, project, nodeid } = req.body;
943
+ formerProjects(db, org, project, nodeid, (ferr, olds) => {
944
+ if (ferr) { return console.error("Could not look for", nodeid, "elsewhere:", ferr.message); }
945
+ each(olds, (old, cb) => retireFormerHome(org, old, nodeid, cb), () => {});
946
+ });
947
+ return;
910
948
  }
911
949
  const status = {
912
950
  [ENROL.BAD_REQUEST]: 400,
package/lib/enrol.js CHANGED
@@ -389,3 +389,10 @@ export function forgetNode(db, org, project, nodeid, cb) {
389
389
  db.run('DELETE FROM nodes WHERE org = ? AND project = ? AND nodeid = ?',
390
390
  [org, project, nodeid], function (err) { cb(err, this && this.changes); });
391
391
  }
392
+
393
+ // Where else this node is enrolled. A node id is one device, so any other project is somewhere it
394
+ // used to be - see retireFormerHome in frugal-iot-server.js.
395
+ export function formerProjects(db, org, project, nodeid, cb) {
396
+ db.all('SELECT project FROM nodes WHERE org = ? AND nodeid = ? AND project != ?',
397
+ [org, nodeid, project], (err, rows) => cb(err, (rows || []).map((r) => r.project)));
398
+ }
package/lib/retained.js CHANGED
@@ -108,3 +108,61 @@ function refusal(status, message) {
108
108
  }
109
109
  function err500(message) { return refusal(500, message); }
110
110
  function refuse(cb, status, message) { return cb(refusal(status, message)); }
111
+
112
+ // Reading needs a different account from deleting: the admin account may publish anywhere in the
113
+ // organization but subscribe nowhere. The logger's reads the whole organization.
114
+ function readCredentialFor(config, org) {
115
+ const o = (config && config.organizations && config.organizations[org]) || {};
116
+ if (o.logger_userid) return { username: o.logger_userid, password: o.logger_password };
117
+ if (o.mqtt_password) return { username: org, password: o.mqtt_password };
118
+ return null;
119
+ }
120
+
121
+ // Retained topics arrive straight after the subscription is granted; this is how long to wait for
122
+ // the last of them, as frugal-iot-clearretained does.
123
+ const COLLECT_MS = 3000;
124
+
125
+ /*
126
+ * Delete every retained message under `prefix` - one node's whole subtree, when it has moved.
127
+ *
128
+ * Takes a prefix, not a pattern, so it cannot be widened to more than the caller named: the
129
+ * wildcard is added here.
130
+ */
131
+ export function clearRetainedUnder(config, org, prefix, cb) {
132
+ if (!NAME.test(String(org || ''))) return refuse(cb, 400, 'Not an organization name');
133
+ if (!String(prefix || '').startsWith(`${org}/`) || /[+#]/.test(prefix) || prefix.split('/').length < 3) {
134
+ return refuse(cb, 400, `${prefix} is not a node within ${org}`);
135
+ }
136
+ const broker = config && config.mqtt && config.mqtt.broker;
137
+ if (!broker) return refuse(cb, 500, 'No mqtt.broker in the configuration');
138
+ const cred = readCredentialFor(config, org);
139
+ if (!cred) return refuse(cb, 500, `No broker credential to read ${org}`);
140
+
141
+ const client = mqtt.connect(broker, {
142
+ username: cred.username, password: cred.password,
143
+ connectTimeout: 5000, reconnectPeriod: 0, clean: true,
144
+ });
145
+ const topics = new Set();
146
+ let settled = false;
147
+ const finish = (err) => {
148
+ if (settled) return;
149
+ settled = true;
150
+ clearTimeout(timer);
151
+ client.end(true, () => {
152
+ if (err) return cb(err);
153
+ if (!topics.size) return cb(null, { deleted: 0, failed: 0 });
154
+ deleteRetained(config, org, [...topics], cb);
155
+ });
156
+ };
157
+ const timer = setTimeout(() => finish(err500('The broker did not answer')), 30000);
158
+ client.on('error', (err) => finish(refusal(502, `Could not reach the broker: ${err.message}`)));
159
+ client.on('message', (topic, message, packet) => {
160
+ if (packet && packet.retain && message.length) topics.add(topic);
161
+ });
162
+ client.on('connect', () => {
163
+ client.subscribe(`${prefix}/#`, { qos: 0 }, (err) => {
164
+ if (err) return finish(refusal(502, `Could not subscribe to ${prefix}/#: ${err.message}`));
165
+ setTimeout(() => finish(null), COLLECT_MS);
166
+ });
167
+ });
168
+ }
package/package.json CHANGED
@@ -12,17 +12,17 @@
12
12
  "cookie-parser": "^1.4.7",
13
13
  "express": "^5.2.1",
14
14
  "express-session": "^1.19.0",
15
- "frugal-iot-client": "^2.0.5",
16
- "frugal-iot-logger": "^2.0.5",
15
+ "frugal-iot-client": "^2.0.7",
16
+ "frugal-iot-logger": "^2.0.7",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
20
20
  "mqtt": "^5.15.2",
21
21
  "multer": "^2.1.1",
22
- "nodemailer": "^9.0.6",
22
+ "nodemailer": "^10.1.2",
23
23
  "passport": "^0.7.0",
24
24
  "passport-local": "^1.0.0",
25
- "sqlite3": "^5.1.7"
25
+ "sqlite3": "^6.0.1"
26
26
  },
27
27
  "bin": {
28
28
  "frugal-iot-server": "frugal-iot-server.js",
@@ -63,10 +63,8 @@
63
63
  "INSTALLATION.md"
64
64
  ],
65
65
  "devDependencies": {
66
- "@types/jest": "^30.0.0",
67
- "jest": "^30.4.2",
68
66
  "supertest": "^7.2.2",
69
- "vitest": "^1.0.0"
67
+ "vitest": "^4.1.12"
70
68
  },
71
69
  "homepage": "https://github.com/mitra42/frugal-iot-server",
72
70
  "keywords": [
@@ -89,5 +87,8 @@
89
87
  "check-files": "node scripts/check-published-files.js"
90
88
  },
91
89
  "type": "module",
92
- "version": "2.0.5"
90
+ "version": "2.0.7",
91
+ "overrides": {
92
+ "uuid": "^11.1.1"
93
+ }
93
94
  }
@@ -6,7 +6,7 @@
6
6
  // it, an installed browser keeps serving the files it already cached - and since the list now covers
7
7
  // the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
8
8
  // needs this bumped, or it does not reach anyone who already has the old one.
9
- const CACHE_NAME = 'frugal-iot-cache-2.0.5';
9
+ const CACHE_NAME = 'frugal-iot-cache-2.0.7';
10
10
  /*
11
11
  * Everything the app needs, so that a phone on a poor or expensive link fetches it once.
12
12
  *
@@ -571,10 +571,11 @@ PACKAGES=(nodejs npm sqlite3 zsh curl)
571
571
  sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}"
572
572
  NODE_VERSION="$(node -v)"
573
573
  info "node ${NODE_VERSION}, npm $(npm -v), sqlite3 $(sqlite3 --version | awk '{print $1}')"
574
- # The server needs node 18 or later
574
+ # The server needs node 20.17 or later (sqlite3 6 needs 20.17, nodemailer 10 needs 20)
575
575
  NODE_MAJOR="$(echo "${NODE_VERSION#v}" | cut -d. -f1)"
576
- if (( NODE_MAJOR < 18 )); then
577
- echo "node ${NODE_VERSION} is too old - the server needs 18 or later." >&2
576
+ NODE_MINOR="$(echo "${NODE_VERSION#v}" | cut -d. -f2)"
577
+ if (( NODE_MAJOR < 20 || (NODE_MAJOR == 20 && NODE_MINOR < 17) )); then
578
+ echo "node ${NODE_VERSION} is too old - the server needs 20.17 or later." >&2
578
579
  echo "Install a current version from https://github.com/nodesource/distributions and run this again." >&2
579
580
  exit 1
580
581
  fi
@@ -1,54 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Frugal IoT Demo</title>
6
- <link href="./frugaliot.css" rel="stylesheet" type="text/css">
7
- <!-- this maps files that webcomponents uses, -->
8
- <script type="importmap">
9
- {
10
- "imports": {
11
- "@kurkle/color": "https://frugaliot.naturalinnovation.org/node_modules/@kurkle/color/dist/color.esm.js",
12
- "chart.js": "https://frugaliot.naturalinnovation.org/node_modules/chart.js/dist/Chart.js",
13
- "dial-gauge": "/node_modules/dial-gauge/dial-gauge.js",
14
- "luxon": "https://frugaliot.naturalinnovation.org/node_modules/luxon/src/luxon.js",
15
- "chartjs-adapter-luxon": "https://frugaliot.naturalinnovation.org/node_modules/chartjs-adapter-luxon/dist/chartjs-adapter-luxon.esm.js",
16
- "csv-parse": "https://frugaliot.naturalinnovation.org/node_modules/csv-parse/dist/esm/index.js"
17
- }
18
- }
19
- </script>
20
- <!-- load webcomponents.js either from local server, or from frugaliot.naturalinnovation.org -->
21
- <script src="./webcomponents.js" type="module"></script>
22
- <!--<script src="./webcomponents.js" type="module"></script>-->
23
- </head>
24
- <body>
25
- <h1>This is an example of embedding a widget in a page</h1>
26
- <div style="display:none">
27
- <!-- every embedded page needs a mqtt-client, it sets up the MQTT connection to the broker -->
28
- <mqtt-client server="wss://frugaliot.naturalinnovation.org/wss"></mqtt-client>
29
- </div>
30
- <div style="float:right;border:2px,solid,blue;width:50%">
31
- <!-- this is a simple display widget - in this case a meter bar -->
32
- <mqtt-bar max="45" min="0" topic="dev/lotus/esp8266-fb94bb/sht/temperature" label="Temperature" type="float" color="red"></mqtt-bar>
33
- <!--<mqtt-bar max="45" min="0" topic="dev/lotus/esp32-70c376/soil" label="Soil" type="float" color="brown"></mqtt-bar>-->
34
- </div>
35
- <div style="float:left;border:2px,solid,blue;width:10%">
36
- <!-- this is a simple control widget - in this case toggles an LED on a device at my office -->
37
- <mqtt-toggle topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" label="LED" type="bool" color="blue"></mqtt-toggle>
38
- </div>
39
- <p>
40
- This is a demo of a custom web page with a bar widget; a graph; and a control.
41
- These all go to the same device which may or may not always be turned on!
42
- </p>
43
- <p>
44
- <!-- And a pretty simple graph - in this case its displaying the same value from above, but it can
45
- <!-- display any topic -->
46
- <!-- Note - I think there is an issue with back button if loaded from a different server, will fix -->
47
- <mqtt-graph>
48
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/temperature" color="red" label="Office Temperature" min=0 max=50 type="float"></mqtt-graphdataset>
49
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/humidity" color="blue" label="Office Humidity" min=0 max=100 type="float"></mqtt-graphdataset>
50
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" color="orange" label="LED" type="bool"></mqtt-graphdataset>
51
- </mqtt-graph>
52
- </p>
53
- </body>
54
- </html>