node-red-contrib-tdn-eventsub 0.2.3
Node-RED nodes for an access-control REST Server API: access groups, cardholders and items lists to global context, plus continuous item status subscriptions.
node-red-contrib-tdn-eventsub
Node-RED nodes for an access-control REST Server API.
| Node | Endpoint (under the base path) | Mode |
|---|---|---|
get cardholders |
/cardholders, /cardholders/{id} |
request/reply |
get access groups |
/access_groups, /access_groups/{id}/cardholders |
request/reply |
get items |
/items, /items/{id} |
request/reply |
get personal data fields |
/personal_data_fields, /personal_data_fields/{id} |
request/reply |
get event groups |
/events/groups, /events/groups/{id} (event types) |
request/reply |
item status |
POST /items/updates, then long-poll |
continuous |
event updates |
/events/updates, long-poll |
continuous |
Every node has a trigger input and three outputs:
- Response: the full server response in
msg. - Status: one message per successful server response, for wiring to a shared log or dashboard.
- Error: every non-success, whether an HTTP error, a network failure, a certificate problem or bad input. Errors never appear on the status output.
No runtime dependencies. Node 16+, Node-RED 3+.
Install
cd ~/.node-red
npm install node-red-contrib-tdn-eventsub@latest
Restart Node-RED. Nodes appear under TDN REST. Import the example via Import → Examples → node-red-contrib-tdn-eventsub.
Server address
Every request goes to <protocol>://<host>:<port><basePath><endpoint>, for example:
http://localhost:80/api/cardholders
https://192.168.0.20:8904/api/access_groups
https://testserver:8911/api/items
The address is read from global context at every request, key rest_server (configurable in the server config node):
global.rest_server = {
host: "192.168.0.20", // hostname or IP (a full URL like "https://testserver:8911/api" also works)
port: 8904,
basePath: "/api",
protocol: "https" // or "http"
}
Any field that's missing falls back to the server config node. A plain string value is treated as the host or URL. You can change the value with a change node to switch servers without redeploying. The protocol is decided in this order: protocol, then the scheme in host, then the config node's Plain HTTP checkbox, then https.
Server config
| Field | Notes |
|---|---|
| Global key | Default rest_server. Leave blank to ignore global context. |
| Host / IP, Port, Base path | Fallbacks when global context doesn't set them. Defaults: port 8904, base path /api. |
| API key | Optional. Stored as a Node-RED credential and treated as opaque. When it's set, it is always used. When it's blank, each node reads a credentials object. |
| Send key as | Authorization header (default) or API key header. |
| Auth scheme | Authorization header only. Blank (the default) sends Authorization: Basic base64(":" + key), which server v9.0+ accepts. 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>. |
| Header name | API key header only. Sent as <header name>: <key>; default X-API-Key. |
| Plain HTTP (insecure) | Uses unencrypted http:// when nothing else sets the protocol. Certificate settings are ignored and the key travels in clear text, so use it for bench testing only. |
| Cert fingerprint | Optional. The SHA-256 (64 hex) or SHA-1 (40 hex) fingerprint of the server certificate; colons are optional. When set, a self-signed certificate is accepted only if it matches. The check runs before any request is written, so the key is never sent to an impostor. |
| Verify server certificate | Normal CA check when no fingerprint is set. |
| Force links onto this host:port | Paging and subscription links (next.href) are built from the server's own machine name. With this option on, they keep their path and query but use your host and port. With it off, a link that would downgrade https to http is refused. |
| Client cert / key / PFX | Only needed if the REST Client item on the server has a thumbprint (pinned client certificate). |
Get the server fingerprint with:
openssl s_client -connect rest-server:8904 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
Credentials object
If the server config has no API key stored in Node-RED, every node takes the key from its Credentials property. This is a typedInput pointing at msg, flow, global or env; the default is msg.credentials. The value is either a bare key string or:
{ apiKey: "XXXX-...", authType: "authorization" | "header", authScheme: "", headerName: "X-API-Key" }
- Missing fields fall back to the server config.
- A stored key always takes precedence.
- A credentials property on the msg is deleted before the msg is passed on.
- Subscriptions read the credentials once per trigger.
List nodes
Input
| msg | Request |
|---|---|
| no id, or an inject timestamp or boolean | list: GET <endpoint> with top, sort and fields; every page is followed and merged |
an id: msg.id, a string or number msg.payload, or msg.payload.id |
details: GET <endpoint>/{id}. For access groups this is the group's members instead: GET /access_groups/{id}/cardholders |
msg.action set |
takes precedence over the defaults above |
msg.action |
Request | Stored in global context |
|---|---|---|
list |
GET <endpoint> (all pages) |
<key> (array) and <key>_updated |
get |
GET <endpoint>/{id} |
<key>_byId[id] |
members (access groups) |
GET /access_groups/{id}/cardholders (all pages) |
<key>_members[id] |
groups (cardholders) |
GET /cardholders/{id}?fields=accessGroups |
<key>_groups[id] |
create |
POST <endpoint>, body msg.payload |
none (msg.location = new resource) |
update |
PATCH <endpoint>/{id}, body msg.payload |
none |
delete |
DELETE <endpoint>/{id} (only with an explicit action) |
removes <key>_byId[id] |
Default keys are rest_cardholders, rest_accessGroups, rest_items, rest_pdfs and rest_eventGroups. Any context store can be picked. Id responses go into maps, so they never overwrite the full list.
Per-message overrides: msg.query (an object or "a=1&b=2"), msg.fields, msg.top, msg.maxPages and msg.contextKey.
Requests are queued and run one at a time, up to the Queue limit (default 100).
Output 1
msg.payload // list/members: { results: [...all pages], count, pageCount }; details/writes: the server's body
msg.pages // raw page bodies (toggle)
msg.action, msg.id, msg.statusCode, msg.location, msg.request, msg.contextKey
Item status node
POST /items/updateswith{"itemIds":[...]}returns the current status of every item. It is sent on output 1 withphase:"subscribe".GET next.hrefis the update-wait: a long poll that the server answers on a change, or after about 50 s.- When updates arrive they're output with
phase:"update", and the next update-wait goes out immediately. Empty update-waits only update the node badge (or use the Also output empty responses option).
Input is "508", "508,526", an array, {itemIds:[...]}, or item objects with id. msg.itemIds and then msg.id take precedence. A new trigger replaces the running subscription. payload:"stop", action:"stop" or msg.stop=true stops it.
There's an optional global map of item id → latest status. The delay between waits is capped at 20 s, so each request stays inside the server's 30 s window.
This needs REST Server v8.30+ and the RESTStatus licence.
Event updates node
GET /events/updates?<filter> long-polls for new events. Each response's updates.href (or next.href) is followed immediately, indefinitely.
- Filter: set it in the node (
group=23&type=20001&source=508) or inmsg.queryat start. Use get event groups to find group and type ids. - Output 1:
msg.payload(the full response),msg.events,msg.phase,msg.seqandmsg.query. - Global context (optional): keeps the last N events.
- Start on deploy: an option that takes credentials from flow, global or env.
- Stopping: same as item status.
Failures and back-off
| Failure | List nodes | Item status / event updates |
|---|---|---|
| 401 wrong API key | error, no retry; the rest of the queue is dropped (one EDROPPED error) so a bad key can't trigger the server's failed-login alarms |
error, polling stops |
| Certificate / TLS (pin mismatch, CA, client cert, not TLS) | error, no retry; queue dropped | error, polling stops |
| 403 licence or privilege | error, no retry | error, polling stops |
| 404 | error, no retry | on the start endpoint: error, polling stops. On a followed link (expired subscription): error, then an immediate re-subscribe |
| other 4xx | error, no retry | error, polling stops |
| 5xx, 408, 429, timeout, network | GET/DELETE retried Retries times (default 3) at delay × 2ⁿ (5 s, 10 s, 20 s…, capped); each attempt is an error with attempt and retryInMs. POST/PATCH are never retried, to avoid duplicates |
error, back-off 5 s → 60 s, re-subscribe, indefinitely |
A stopped subscription restarts on the next trigger or on redeploy. Error payloads carry category (fatal, transient or expired) and, for subscriptions, stopped: true when polling has ended.
Status output
Status messages are { topic: "status", payload: { state, text, node, name, type, time, ... } }:
- lists:
response(one per successful HTTP response, withstatusCode,method,urlandpage), thendone; - item status:
subscribedandupdate; - event updates:
update; - subscriptions:
stoppedon a stop input.
Empty long-poll returns (about every 50 s) show on the node badge only.
Test
npm test
This spins up a mock REST Server (self-signed HTTPS, plain HTTP, and a TLS echo endpoint) and a minimal Node-RED harness. It covers:
- addressing and the global-context override;
- every list node and action, paging, the queue, back-off, and the hard-stop rules;
- the credentials object and both auth header modes;
- certificate pinning;
- the item-status and events long-poll loops, including expiry, stop/replace and recovery.
After changing a list-node definition, regenerate the editor HTML with node tools/gen-lists-html.js.