node-red-contrib-tdn-eventsub 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stephen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # node-red-contrib-tdn-eventsub
2
+
3
+ Node-RED nodes for an access-control **REST Server API**.
4
+
5
+ | Node | What it does | Mode |
6
+ |---|---|---|
7
+ | `get access groups` | Downloads all access groups, stores to global context | request/reply |
8
+ | `get cardholders` | Downloads all cardholders, stores to global context | request/reply |
9
+ | `get items` | Downloads all items (optionally filtered by type/division), stores to global context | request/reply |
10
+ | `item status` | Subscribes to status of one or more item (source) ids and keeps waiting for updates indefinitely | continuous |
11
+
12
+ All share one **REST server** config node: URL/host, port (default 8904), API key, optional auth scheme, optional server certificate fingerprint, optional client certificate.
13
+
14
+ Every node has a trigger input and **three outputs**:
15
+
16
+ 1. **Response** – full server response in `msg`
17
+ 2. **Status** – state changes (`msg.payload.state`, `text`, `time`, node id/name/type) for wiring to a shared log/dashboard
18
+ 3. **Error** – `msg.payload.message`, `statusCode`, `body`, `url`, `code`
19
+
20
+ No runtime dependencies. Node 16+, Node-RED 3+.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ cd ~/.node-red
26
+ npm install /path/to/node-red-contrib-tdn-eventsub # or the .tgz
27
+ ```
28
+
29
+ Restart Node-RED. Nodes appear under **TDN REST**. Import the example via *Import → Examples → node-red-contrib-tdn-eventsub*.
30
+
31
+ ## Server config
32
+
33
+ | Field | Notes |
34
+ |---|---|
35
+ | URL / host | `rest-server`, `192.168.1.10` or `https://rest-server`. `/api` path assumed. |
36
+ | Port | Default `8904`. |
37
+ | API key | Stored as a Node-RED credential; treated as opaque. |
38
+ | Auth scheme | Blank (default): `Authorization: Basic base64(":" + key)`, accepted by server v9.0+. For older servers enter the API-key scheme token from your server's REST API docs; the key is then sent as `Authorization: <scheme> <key>`. |
39
+ | Cert fingerprint | Optional. SHA-256 (64 hex) or SHA-1 (40 hex) of the **server** certificate, colons optional. When set, the server's self-signed cert is accepted only if it matches; the check runs before any request is written, so the key is never sent to an impostor. A mismatch error prints the actual fingerprint. |
40
+ | Verify server certificate | Normal CA check when no fingerprint is set. |
41
+ | Force links onto this host:port | The server builds `href`s from its own machine name. With this on, followed links keep path + query but use your configured host/port. With it off, a link that would downgrade `https` to `http` is refused. |
42
+ | Client cert / key / PFX | Only if the REST Client item on the server has a thumbprint (pinned client certificate). |
43
+
44
+ Get the server fingerprint with:
45
+
46
+ ```sh
47
+ openssl s_client -connect rest-server:8904 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
48
+ ```
49
+
50
+ ## List nodes (access groups / cardholders / items)
51
+
52
+ Discovers the URL from `GET /api` (`features.accessGroups.accessGroups`, `features.cardholders.cardholders`, `features.items.items`), requests with `sort=id&top=1000`, and follows `next.href` until done.
53
+
54
+ **Output 1**
55
+
56
+ ```js
57
+ msg.payload = { results: [...all pages merged], count, pageCount }
58
+ msg.pages = [ raw page 1, raw page 2, ... ] // unmodified server bodies (toggle)
59
+ msg.request = { href, params }
60
+ msg.contextKey
61
+ ```
62
+
63
+ **Global context**: `global.<key>` = results array, `global.<key>_updated` = ISO time. Defaults: `rest_accessGroups`, `rest_cardholders`, `rest_items`. Any context store can be picked.
64
+
65
+ **Per-message overrides**: `msg.query` (object or `"a=1&b=2"`), `msg.fields`, `msg.top`, `msg.maxPages`, `msg.contextKey`.
66
+
67
+ Useful settings: cardholders `fields=defaults,cards,accessGroups`; items `query=type=11` (use the item types list to find ids). A trigger while a download is running is ignored and reported as `busy` on the status output.
68
+
69
+ ## Item status node
70
+
71
+ Implements the REST Server status-subscription loop:
72
+
73
+ 1. `POST features.items.updates.href` with `{"itemIds":[...]}` → current status of every item (output, `phase:"subscribe"`)
74
+ 2. `GET next.href` – update-wait (long poll, server returns after ~50 s if nothing changes)
75
+ 3. Updates arrive → output (`phase:"update"`), then a new update-wait
76
+ 4. Update-wait times out (empty `updates`) → another update-wait (optionally output with `phase:"timeout"`; the status output is not used, only the node badge)
77
+ 5. `404` → the server dropped the subscription (>30 s gap or server restart) → re-POST immediately
78
+ 6. Any other error → error output, exponential back-off (5 s → 60 s), re-subscribe. Runs until stopped.
79
+
80
+ **Input** – one trigger starts it:
81
+
82
+ - `msg.payload = "508"` / `"508,526"` / `["508","526"]` / `{itemIds:[...]}` / array of item objects with `id` (so you can wire straight from a filtered `get items` result)
83
+ - `msg.itemIds` takes precedence over `payload`
84
+ - a new trigger with ids replaces the running subscription
85
+ - `msg.payload = "stop"` or `msg.stop = true` stops it
86
+
87
+ **Output 1**
88
+
89
+ ```js
90
+ msg.payload = { updates: [{ id, status, statusText, statusFlags }], next: { href } } // full server response
91
+ msg.updates = msg.payload.updates
92
+ msg.phase = "subscribe" | "update" | "timeout"
93
+ msg.itemIds, msg.seq, msg.statusCode, msg.url
94
+ ```
95
+
96
+ Optional global key keeps a map `id → latest status` (+ `updated` time).
97
+
98
+ Delay between waits defaults to 1 s and is capped at 20 s so the next GET always lands inside the server's 30 s window.
99
+
100
+ Requires REST Server v8.30+ and the **RESTStatus** licence. Before 9.50 the first update-wait repeats the POST results; that is passed through as the server sends it.
101
+
102
+ ## Status output states
103
+
104
+ `requesting`, `done`, `busy` (lists) · `subscribing`, `subscribed`, `update`, `resubscribing`, `retrying`, `stopped` (item status) · `error`.
105
+
106
+ The "waiting" and "timed out" states of each long poll are shown on the node badge only, not sent on output 2, so the status log isn't flooded every 50 s.
107
+
108
+ ## Test
109
+
110
+ ```sh
111
+ npm test
112
+ ```
113
+
114
+ Spins up a self-signed HTTPS mock REST Server and a minimal Node-RED harness; covers fingerprint pin/mismatch, Basic and custom auth schemes, link-downgrade refusal, paging, href rewrite, global context, the update-wait loop, 404 re-subscribe, stop/replace, and recovery after the server drops.
@@ -0,0 +1,290 @@
1
+ [
2
+ {
3
+ "id": "rest_tab",
4
+ "type": "tab",
5
+ "label": "REST Server"
6
+ },
7
+ {
8
+ "z": "rest_tab",
9
+ "id": "dbg_resp",
10
+ "type": "debug",
11
+ "name": "responses",
12
+ "active": true,
13
+ "tosidebar": true,
14
+ "complete": "true",
15
+ "targetType": "msg",
16
+ "x": 760,
17
+ "y": 100,
18
+ "wires": []
19
+ },
20
+ {
21
+ "z": "rest_tab",
22
+ "id": "dbg_status",
23
+ "type": "debug",
24
+ "name": "status (all nodes)",
25
+ "active": true,
26
+ "tosidebar": true,
27
+ "complete": "payload",
28
+ "targetType": "msg",
29
+ "x": 780,
30
+ "y": 300,
31
+ "wires": []
32
+ },
33
+ {
34
+ "z": "rest_tab",
35
+ "id": "dbg_err",
36
+ "type": "debug",
37
+ "name": "errors (all nodes)",
38
+ "active": true,
39
+ "tosidebar": true,
40
+ "complete": "payload",
41
+ "targetType": "msg",
42
+ "x": 780,
43
+ "y": 360,
44
+ "wires": []
45
+ },
46
+ {
47
+ "z": "rest_tab",
48
+ "id": "inj_ag",
49
+ "type": "inject",
50
+ "name": "refresh ag",
51
+ "props": [
52
+ {
53
+ "p": "payload"
54
+ }
55
+ ],
56
+ "repeat": "",
57
+ "once": false,
58
+ "payload": "",
59
+ "payloadType": "date",
60
+ "x": 140,
61
+ "y": 60,
62
+ "wires": [
63
+ [
64
+ "n_ag"
65
+ ]
66
+ ]
67
+ },
68
+ {
69
+ "z": "rest_tab",
70
+ "id": "n_ag",
71
+ "type": "tdn-rest-access-groups",
72
+ "name": "get access groups",
73
+ "server": "rest_srv",
74
+ "contextKey": "rest_accessGroups",
75
+ "contextStore": "",
76
+ "top": 1000,
77
+ "sort": "id",
78
+ "fields": "",
79
+ "query": "",
80
+ "maxPages": 0,
81
+ "includePages": true,
82
+ "x": 400,
83
+ "y": 60,
84
+ "wires": [
85
+ [
86
+ "dbg_resp"
87
+ ],
88
+ [
89
+ "dbg_status"
90
+ ],
91
+ [
92
+ "dbg_err"
93
+ ]
94
+ ]
95
+ },
96
+ {
97
+ "z": "rest_tab",
98
+ "id": "inj_ch",
99
+ "type": "inject",
100
+ "name": "refresh ch",
101
+ "props": [
102
+ {
103
+ "p": "payload"
104
+ }
105
+ ],
106
+ "repeat": "",
107
+ "once": false,
108
+ "payload": "",
109
+ "payloadType": "date",
110
+ "x": 140,
111
+ "y": 120,
112
+ "wires": [
113
+ [
114
+ "n_ch"
115
+ ]
116
+ ]
117
+ },
118
+ {
119
+ "z": "rest_tab",
120
+ "id": "n_ch",
121
+ "type": "tdn-rest-cardholders",
122
+ "name": "get cardholders",
123
+ "server": "rest_srv",
124
+ "contextKey": "rest_cardholders",
125
+ "contextStore": "",
126
+ "top": 1000,
127
+ "sort": "id",
128
+ "fields": "",
129
+ "query": "",
130
+ "maxPages": 0,
131
+ "includePages": true,
132
+ "x": 400,
133
+ "y": 120,
134
+ "wires": [
135
+ [
136
+ "dbg_resp"
137
+ ],
138
+ [
139
+ "dbg_status"
140
+ ],
141
+ [
142
+ "dbg_err"
143
+ ]
144
+ ]
145
+ },
146
+ {
147
+ "z": "rest_tab",
148
+ "id": "inj_it",
149
+ "type": "inject",
150
+ "name": "refresh it",
151
+ "props": [
152
+ {
153
+ "p": "payload"
154
+ }
155
+ ],
156
+ "repeat": "",
157
+ "once": false,
158
+ "payload": "",
159
+ "payloadType": "date",
160
+ "x": 140,
161
+ "y": 180,
162
+ "wires": [
163
+ [
164
+ "n_it"
165
+ ]
166
+ ]
167
+ },
168
+ {
169
+ "z": "rest_tab",
170
+ "id": "n_it",
171
+ "type": "tdn-rest-items",
172
+ "name": "get items",
173
+ "server": "rest_srv",
174
+ "contextKey": "rest_items",
175
+ "contextStore": "",
176
+ "top": 1000,
177
+ "sort": "id",
178
+ "fields": "",
179
+ "query": "",
180
+ "maxPages": 0,
181
+ "includePages": true,
182
+ "x": 400,
183
+ "y": 180,
184
+ "wires": [
185
+ [
186
+ "dbg_resp"
187
+ ],
188
+ [
189
+ "dbg_status"
190
+ ],
191
+ [
192
+ "dbg_err"
193
+ ]
194
+ ]
195
+ },
196
+ {
197
+ "z": "rest_tab",
198
+ "id": "inj_sub",
199
+ "type": "inject",
200
+ "name": "watch 508,526",
201
+ "props": [
202
+ {
203
+ "p": "payload"
204
+ }
205
+ ],
206
+ "payload": "508,526",
207
+ "payloadType": "str",
208
+ "x": 150,
209
+ "y": 260,
210
+ "wires": [
211
+ [
212
+ "n_st"
213
+ ]
214
+ ]
215
+ },
216
+ {
217
+ "z": "rest_tab",
218
+ "id": "inj_stop",
219
+ "type": "inject",
220
+ "name": "stop",
221
+ "props": [
222
+ {
223
+ "p": "payload"
224
+ }
225
+ ],
226
+ "payload": "stop",
227
+ "payloadType": "str",
228
+ "x": 130,
229
+ "y": 300,
230
+ "wires": [
231
+ [
232
+ "n_st"
233
+ ]
234
+ ]
235
+ },
236
+ {
237
+ "z": "rest_tab",
238
+ "id": "n_st",
239
+ "type": "tdn-rest-item-status",
240
+ "name": "door status",
241
+ "server": "rest_srv",
242
+ "pollDelay": 1000,
243
+ "retryDelay": 5000,
244
+ "maxRetryDelay": 60000,
245
+ "emitTimeouts": false,
246
+ "contextKey": "rest_itemStatus",
247
+ "contextStore": "",
248
+ "x": 400,
249
+ "y": 280,
250
+ "wires": [
251
+ [
252
+ "dbg_upd"
253
+ ],
254
+ [
255
+ "dbg_status"
256
+ ],
257
+ [
258
+ "dbg_err"
259
+ ]
260
+ ]
261
+ },
262
+ {
263
+ "z": "rest_tab",
264
+ "id": "dbg_upd",
265
+ "type": "debug",
266
+ "name": "status updates",
267
+ "active": true,
268
+ "tosidebar": true,
269
+ "complete": "updates",
270
+ "targetType": "msg",
271
+ "x": 760,
272
+ "y": 240,
273
+ "wires": []
274
+ },
275
+ {
276
+ "id": "rest_srv",
277
+ "type": "tdn-rest-server",
278
+ "name": "REST Server",
279
+ "url": "192.168.1.10",
280
+ "port": 8904,
281
+ "authScheme": "",
282
+ "fingerprint": "",
283
+ "rejectUnauthorized": true,
284
+ "rewriteHrefs": true,
285
+ "timeout": 30000,
286
+ "certPath": "",
287
+ "keyPath": "",
288
+ "pfxPath": ""
289
+ }
290
+ ]