node-red-contrib-room-tools 1.8.3-visual-binding.17 → 1.8.4

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/README.md CHANGED
@@ -6,6 +6,8 @@ This package provides `ui-control-panel` for Node-RED and MUSE Automator.
6
6
 
7
7
  The page author writes ordinary HTML/CSS/Vue and pastes it into the `ui-control-panel` Template. After configuring a Dashboard page, room ID, control login, and separate editor login, save and Deploy. Reopen the node and select **Open page binding editor**. Sign in with the editor account, right-click a supported control, choose its device points, and Apply.
8
8
 
9
+ Both logins work over HTTP on a trusted local network. HTTPS is recommended for other networks because HTTP transmits credentials without encryption. Session cookies are marked `Secure` when the connection uses HTTPS.
10
+
9
11
  Bindings are stored in `visualBindings` on the node. The runtime reads that mapping to authenticate commands, route feedback, and generate a native bridge. The page author does not put device paths, `data-cp-*` attributes, or device-send code in the Template. Author-provided `data-cp-*` attributes are rejected.
10
12
 
11
13
  Supported controls include native range, checkbox, semantic pressed button, select, text input, meter, progress, and simple text display. Give repeated controls distinct standard IDs. Simple top-level Vue `v-model` works; controls with existing send handlers or ambiguous dynamic behavior are rejected until a suitable adapter exists. See `CONTROL_PANEL_AI_GUIDE.md` for examples and limits.
package/gateway.cjs CHANGED
@@ -292,7 +292,7 @@ function createGateway ({
292
292
  }
293
293
 
294
294
  const cookieName = 'room_panel_' + Buffer.from(room).toString('hex')
295
- const cookie = (token, maxAge) => `${cookieName}=${token}; Path=${routePrefix}; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secureCookie ? '; Secure' : ''}`
295
+ const cookie = (token, maxAge, req) => `${cookieName}=${token}; Path=${routePrefix}; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secureCookie && req.socket.encrypted ? '; Secure' : ''}`
296
296
 
297
297
  async function handler (req, res) {
298
298
  const path = new URL(req.url, 'http://localhost').pathname
@@ -353,7 +353,7 @@ function createGateway ({
353
353
  const csrf = crypto.randomBytes(24).toString('hex')
354
354
  sessions.set(token, { csrf, expiry: now + sessionTtlMs })
355
355
  reply(res, 200, { room, username, csrf }, {
356
- 'Set-Cookie': cookie(token, Math.ceil(sessionTtlMs / 1000))
356
+ 'Set-Cookie': cookie(token, Math.ceil(sessionTtlMs / 1000), req)
357
357
  })
358
358
  if (boundOnly && typeof onBoundLogin === 'function') {
359
359
  queueMicrotask(() => { if (!closed) onBoundLogin({ room, user: username }) })
@@ -405,7 +405,7 @@ function createGateway ({
405
405
  if (path === routePrefix + '/logout' && req.method === 'POST') {
406
406
  sessions.delete(token)
407
407
  closeSubscribersForToken(token)
408
- reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0) })
408
+ reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0, req) })
409
409
  return true
410
410
  }
411
411
  if (path === routePrefix + '/bound-command' && req.method === 'POST') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-room-tools",
3
- "version": "1.8.3-visual-binding.17",
3
+ "version": "1.8.4",
4
4
  "description": "Room panel message generator and authenticated region gateway for Node-RED and MUSE Automator",
5
5
  "license": "UNLICENSED",
6
6
  "keywords": [
@@ -30,8 +30,8 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
30
30
  res.end(JSON.stringify(data))
31
31
  }
32
32
 
33
- function cookie (value, age) {
34
- return `${cookieName}=${value}; Path=${prefix}; HttpOnly; SameSite=Strict; Max-Age=${age}${secureCookie ? '; Secure' : ''}`
33
+ function cookie (value, age, req) {
34
+ return `${cookieName}=${value}; Path=${prefix}; HttpOnly; SameSite=Strict; Max-Age=${age}${secureCookie && req.socket.encrypted ? '; Secure' : ''}`
35
35
  }
36
36
 
37
37
  function sameOrigin (req) {
@@ -44,12 +44,6 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
44
44
  } catch (_) { return false }
45
45
  }
46
46
 
47
- function localOrSecure (req) {
48
- const address = String(req.socket.remoteAddress || '')
49
- return ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address) ||
50
- req.socket.encrypted === true
51
- }
52
-
53
47
  async function jsonBody (req) {
54
48
  if (!String(req.headers['content-type'] || '').startsWith('application/json')) {
55
49
  throw new Error('JSON body required')
@@ -71,7 +65,6 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
71
65
  const path = new URL(req.url, 'http://localhost').pathname
72
66
  if (!path.startsWith(prefix + '/')) return false
73
67
  if (closed) { reply(res, 503, { error: 'Editor unavailable' }); return true }
74
- if (!localOrSecure(req)) { reply(res, 403, { error: 'HTTPS required' }); return true }
75
68
  if (req.method === 'POST' && !sameOrigin(req)) {
76
69
  reply(res, 403, { error: 'Origin rejected' })
77
70
  return true
@@ -103,7 +96,7 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
103
96
  const token = crypto.randomBytes(32).toString('hex')
104
97
  const csrf = crypto.randomBytes(24).toString('hex')
105
98
  sessions.set(token, { csrf, expiry: now + ttl })
106
- reply(res, 200, { csrf }, { 'Set-Cookie': cookie(token, ttl / 1000) })
99
+ reply(res, 200, { csrf }, { 'Set-Cookie': cookie(token, ttl / 1000, req) })
107
100
  return true
108
101
  }
109
102
  const token = String(req.headers.cookie || '').split(';').map(part => part.trim())
@@ -118,7 +111,7 @@ function createPanelEditor ({ panelId, username, password, secureCookie, getConf
118
111
  reply(res, 200, { csrf: session.csrf })
119
112
  } else if (path === prefix + '/logout' && req.method === 'POST') {
120
113
  sessions.delete(token)
121
- reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0) })
114
+ reply(res, 200, { ok: true }, { 'Set-Cookie': cookie('', 0, req) })
122
115
  } else if (path === prefix + '/config' && req.method === 'GET') {
123
116
  reply(res, 200, await getConfig(req))
124
117
  } else if (path === prefix + '/devices' && req.method === 'GET') {