@nanobpm/nano-ide-trigger-mqtt 1.0.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/README.md +154 -0
- package/driver.ts +214 -0
- package/nano-ide.ext.json +32 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# MQTT trigger — `@nanobpm/nano-ide-trigger-mqtt`
|
|
2
|
+
|
|
3
|
+
> Turn any MQTT message into a running process. Subscribe to a broker topic and
|
|
4
|
+
> let each message **start a process** or **correlate into a running one** — the
|
|
5
|
+
> *"when this happens…"* half of a Zapier-style automation, for your own apps.
|
|
6
|
+
|
|
7
|
+
This is a pack on the **`nano-ide-trigger-*` marketplace axis** for the Nano /
|
|
8
|
+
Urban RAD console. Installing it adds a new trigger source **kind** — `mqtt` —
|
|
9
|
+
that any App can declare. The pack ships a small out-of-process **driver** that
|
|
10
|
+
the runtime auto-launches and supervises; the driver only *produces* events, and
|
|
11
|
+
the runtime owns the durable inbox, dispatch, retry, and lifecycle (ADR 0025).
|
|
12
|
+
|
|
13
|
+
## Why you'd want it
|
|
14
|
+
|
|
15
|
+
MQTT is the lingua franca of IoT and home automation. With this pack an Urban App
|
|
16
|
+
can react to anything that speaks MQTT:
|
|
17
|
+
|
|
18
|
+
- **Home automation** — Home Assistant, Zigbee2MQTT, Tasmota, ESPHome, Shelly…
|
|
19
|
+
a door opens, a sensor crosses a threshold, a button is pressed → kick off a
|
|
20
|
+
process.
|
|
21
|
+
- **Industrial / edge** — PLCs and gateways publishing telemetry over MQTT/MQTTS.
|
|
22
|
+
- **Your own devices** — an ESP32 publishing `{ "value": 21.4 }` becomes a typed
|
|
23
|
+
event your process reasons about.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
Install it into your Urban workspace's extensions like any other pack — from the
|
|
28
|
+
console **Extensions** marketplace (search "MQTT"), or from the CLI:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
c8ctl load plugin @nanobpm/nano-ide-trigger-mqtt
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Once installed, `mqtt` shows up as a recognised source kind in the console
|
|
35
|
+
**Triggers** panel, and you can declare triggers of `type: "mqtt"`.
|
|
36
|
+
|
|
37
|
+
## Use it in an App
|
|
38
|
+
|
|
39
|
+
Declare a trigger of type `mqtt` in your `nano.app.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"triggers": [
|
|
44
|
+
{
|
|
45
|
+
"id": "room-temp",
|
|
46
|
+
"type": "mqtt",
|
|
47
|
+
"config": {
|
|
48
|
+
"url": "mqtt://localhost:1883",
|
|
49
|
+
"topics": "home/+/temperature",
|
|
50
|
+
"qos": "1"
|
|
51
|
+
},
|
|
52
|
+
"connection": "broker",
|
|
53
|
+
"action": {
|
|
54
|
+
"start": "handle-reading",
|
|
55
|
+
"variables": "{ room: split(body.topic, \"/\")[2], celsius: body.payload.value }"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"connections": {
|
|
60
|
+
"broker": {
|
|
61
|
+
"type": "mqtt",
|
|
62
|
+
"username": "{{ env.MQTT_USER }}",
|
|
63
|
+
"password": "{{ env.MQTT_PASS }}"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Each received message is delivered to the App as the event **body**:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"topic": "home/kitchen/temperature",
|
|
74
|
+
"payload": { "value": 21.4 },
|
|
75
|
+
"ts": "2026-07-24T00:00:00.000Z"
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- `topic` — the concrete topic the message arrived on (wildcards resolved).
|
|
80
|
+
- `payload` — the message payload parsed as JSON when possible, else the raw
|
|
81
|
+
UTF-8 string.
|
|
82
|
+
- `ts` — ISO-8601 receive time.
|
|
83
|
+
|
|
84
|
+
Reference these in the action's FEEL (`variables` / `correlationKey`), e.g.
|
|
85
|
+
`body.payload.value`, `split(body.topic, "/")[2]`.
|
|
86
|
+
|
|
87
|
+
## Configuration
|
|
88
|
+
|
|
89
|
+
| field | required | default | notes |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| `url` | yes\* | `mqtt://localhost:1883` | Broker URL. Schemes: `mqtt://`, `mqtts://` (TLS), `ws://`, `wss://`. A `connection.url` overrides this. |
|
|
92
|
+
| `topics` | yes | — | Topic filter(s) to subscribe to. Comma-separate several. MQTT wildcards `+` (single level) and `#` (multi level) are allowed, e.g. `home/+/temperature`, `sensors/#`. |
|
|
93
|
+
| `qos` | no | `1` | Subscription QoS: `0` (at most once), `1` (at least once), `2` (exactly once). |
|
|
94
|
+
|
|
95
|
+
\* A broker URL is required, but it may come from either `config.url` or the
|
|
96
|
+
referenced `connection.url`.
|
|
97
|
+
|
|
98
|
+
### Credentials
|
|
99
|
+
|
|
100
|
+
Put credentials in a `connections[]` entry (referenced by the trigger's
|
|
101
|
+
`connection`), **never inline** in `config` — secrets should be env templates
|
|
102
|
+
(`{{ env.VAR }}`) the host resolves. The connection object may supply:
|
|
103
|
+
|
|
104
|
+
| key | notes |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `url` | Broker URL; overrides `config.url`. |
|
|
107
|
+
| `username` / `password` | Broker credentials. |
|
|
108
|
+
| `clientId` | Fixed MQTT client id (otherwise the library picks one). |
|
|
109
|
+
|
|
110
|
+
## Delivery semantics
|
|
111
|
+
|
|
112
|
+
The driver POSTs every received message to the trigger ingress with a **stable
|
|
113
|
+
idempotency key**, and the runtime's inbox is **at-least-once**: a message that
|
|
114
|
+
is briefly un-POSTable (ingress hiccup) is retried with backoff, and a retried
|
|
115
|
+
delivery is collapsed rather than double-processed. Design your process actions
|
|
116
|
+
to tolerate the occasional duplicate (idempotent starts / correlation keys).
|
|
117
|
+
|
|
118
|
+
MQTT itself only redelivers **retained** messages on reconnect; live messages
|
|
119
|
+
published while the driver was down are not replayed.
|
|
120
|
+
|
|
121
|
+
## How it runs
|
|
122
|
+
|
|
123
|
+
The runtime launches `driver.ts` as a supervised child process — **Node ≥ 22.6**
|
|
124
|
+
with `--experimental-strip-types` (the default), or **Deno** — passing the
|
|
125
|
+
trigger's context in the environment:
|
|
126
|
+
|
|
127
|
+
| env var | value |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `NANOBPMN_HOOK_URL` | the ingress endpoint the driver POSTs events to |
|
|
130
|
+
| `NANOBPMN_TRIGGER_CONFIG` | JSON of the trigger's `config` |
|
|
131
|
+
| `NANOBPMN_TRIGGER_CONNECTION` | JSON of the referenced connection, or `null` |
|
|
132
|
+
| `NANOBPMN_WEBHOOK_TOKEN` | shared secret presented as `X-Webhook-Token` (if the trigger sets `auth`) |
|
|
133
|
+
| `NANOBPMN_PROJECT` / `NANOBPMN_TRIGGER_ID` / `NANOBPMN_TRIGGER_TYPE` | identity, for logs |
|
|
134
|
+
|
|
135
|
+
On crash the driver is restarted with capped backoff; on App stop it receives
|
|
136
|
+
`SIGTERM` and disconnects cleanly. Its stdout/stderr stream into the App's
|
|
137
|
+
trigger log, so `[mqtt] connected` / `subscribed` / errors are visible in the
|
|
138
|
+
console.
|
|
139
|
+
|
|
140
|
+
## Troubleshooting
|
|
141
|
+
|
|
142
|
+
- **No events arriving** — check the App's trigger log for `[mqtt] connected` and
|
|
143
|
+
`[mqtt] subscribed: <topic>@<qos>`. If you see `reconnecting…` in a loop, the
|
|
144
|
+
broker URL or credentials are wrong.
|
|
145
|
+
- **`no topics configured`** — set `config.topics` (comma-separated); the driver
|
|
146
|
+
refuses to start without at least one.
|
|
147
|
+
- **`ingress rejected event (401/403)`** — the trigger declares an `auth` secret
|
|
148
|
+
but the env var it names is unset or mismatched.
|
|
149
|
+
- **Payload arrives as a string, not an object** — the message wasn't valid JSON;
|
|
150
|
+
`body.payload` is then the raw text. Publish JSON to get a structured object.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
Apache-2.0.
|
package/driver.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// MQTT trigger source driver (nano-ide-trigger-mqtt, ADR 0025 §6 / phase 4).
|
|
2
|
+
//
|
|
3
|
+
// Auto-launched and supervised by the Urban runtime while an App that declares
|
|
4
|
+
// an `mqtt` trigger is running. It subscribes to the configured topics and
|
|
5
|
+
// POSTs each received message to the trigger ingress (the universal emit
|
|
6
|
+
// endpoint); the runtime owns the durable inbox, dispatch, and retry. This
|
|
7
|
+
// process only produces events.
|
|
8
|
+
//
|
|
9
|
+
// Runtime contract (env, set by the host — see extensions.rs / ADR 0025 §6):
|
|
10
|
+
// NANOBPMN_HOOK_URL POST events here
|
|
11
|
+
// NANOBPMN_TRIGGER_CONFIG JSON of the trigger's `config` ({ url, topics, qos })
|
|
12
|
+
// NANOBPMN_TRIGGER_CONNECTION JSON of the referenced connection, or "null"
|
|
13
|
+
// NANOBPMN_WEBHOOK_TOKEN shared secret to present as X-Webhook-Token (if set)
|
|
14
|
+
// NANOBPMN_TRIGGER_ID / _TYPE / NANOBPMN_PROJECT identity (for logs)
|
|
15
|
+
//
|
|
16
|
+
// Runs on Node >=22.6 (`--experimental-strip-types`, the host default) or Deno.
|
|
17
|
+
// Written in erasable TypeScript so Node can strip the types without a build.
|
|
18
|
+
|
|
19
|
+
import mqtt from "mqtt";
|
|
20
|
+
import type { IClientOptions, IClientSubscribeOptions } from "mqtt";
|
|
21
|
+
|
|
22
|
+
/** Read an env var portably across Node (`process.env`) and Deno (`Deno.env`). */
|
|
23
|
+
function env(name: string): string | undefined {
|
|
24
|
+
const g = globalThis as { Deno?: { env: { get(k: string): string | undefined } }; process?: { env: Record<string, string | undefined> } };
|
|
25
|
+
if (g.Deno) return g.Deno.env.get(name);
|
|
26
|
+
return g.process?.env?.[name];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function log(msg: string): void {
|
|
30
|
+
// stdout is streamed to the App's trigger log by the supervisor.
|
|
31
|
+
console.log(`[mqtt] ${msg}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fail(msg: string): never {
|
|
35
|
+
console.error(`[mqtt] ${msg}`);
|
|
36
|
+
const g = globalThis as { Deno?: { exit(code: number): never }; process?: { exit(code: number): never } };
|
|
37
|
+
(g.Deno ?? g.process)?.exit(1);
|
|
38
|
+
throw new Error(msg);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface Config {
|
|
42
|
+
url?: string;
|
|
43
|
+
topics?: string | string[];
|
|
44
|
+
qos?: number | string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Connection {
|
|
48
|
+
url?: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
password?: string;
|
|
51
|
+
clientId?: string;
|
|
52
|
+
// Any extra keys are passed through to the mqtt client options untouched.
|
|
53
|
+
[k: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hookUrl = env("NANOBPMN_HOOK_URL");
|
|
57
|
+
if (!hookUrl) fail("NANOBPMN_HOOK_URL is not set; refusing to start");
|
|
58
|
+
|
|
59
|
+
const token = env("NANOBPMN_WEBHOOK_TOKEN");
|
|
60
|
+
|
|
61
|
+
let config: Config = {};
|
|
62
|
+
try {
|
|
63
|
+
config = JSON.parse(env("NANOBPMN_TRIGGER_CONFIG") || "{}") as Config;
|
|
64
|
+
} catch {
|
|
65
|
+
fail("NANOBPMN_TRIGGER_CONFIG is not valid JSON");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let connection: Connection = {};
|
|
69
|
+
try {
|
|
70
|
+
const raw = JSON.parse(env("NANOBPMN_TRIGGER_CONNECTION") || "null");
|
|
71
|
+
if (raw && typeof raw === "object") connection = raw as Connection;
|
|
72
|
+
} catch {
|
|
73
|
+
fail("NANOBPMN_TRIGGER_CONNECTION is not valid JSON");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const brokerUrl = connection.url || config.url || "mqtt://localhost:1883";
|
|
77
|
+
const qos = clampQos(config.qos);
|
|
78
|
+
const topics = parseTopics(config.topics);
|
|
79
|
+
if (topics.length === 0) fail("no topics configured; set config.topics (comma-separated)");
|
|
80
|
+
|
|
81
|
+
function clampQos(v: number | string | undefined): 0 | 1 | 2 {
|
|
82
|
+
const n = typeof v === "string" ? parseInt(v, 10) : v;
|
|
83
|
+
return n === 0 || n === 2 ? n : 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseTopics(v: string | string[] | undefined): string[] {
|
|
87
|
+
if (Array.isArray(v)) return v.map((t) => String(t).trim()).filter(Boolean);
|
|
88
|
+
if (typeof v === "string") return v.split(",").map((t) => t.trim()).filter(Boolean);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A driver-run id + monotonic sequence gives every emitted event a stable
|
|
93
|
+
// idempotency key, so a POST retried after a transient failure is collapsed by
|
|
94
|
+
// the inbox (ADR 0025 §2 step 1) rather than double-delivered.
|
|
95
|
+
const runId = `${Date.now().toString(36)}-${Math.floor(Math.random() * 1e6).toString(36)}`;
|
|
96
|
+
let seq = 0;
|
|
97
|
+
|
|
98
|
+
const utf8 = new TextDecoder("utf-8");
|
|
99
|
+
|
|
100
|
+
async function emit(topic: string, payloadRaw: Uint8Array): Promise<void> {
|
|
101
|
+
const idem = `${runId}-${(seq++).toString(36)}`;
|
|
102
|
+
const text = utf8.decode(payloadRaw);
|
|
103
|
+
let payload: unknown = text;
|
|
104
|
+
// Convenience: surface JSON payloads as objects so the App's FEEL can read
|
|
105
|
+
// `body.payload.field` directly; non-JSON stays a string.
|
|
106
|
+
try {
|
|
107
|
+
payload = JSON.parse(text);
|
|
108
|
+
} catch {
|
|
109
|
+
/* keep raw string */
|
|
110
|
+
}
|
|
111
|
+
const body = JSON.stringify({ topic, payload, ts: new Date().toISOString() });
|
|
112
|
+
const headers: Record<string, string> = {
|
|
113
|
+
"content-type": "application/json",
|
|
114
|
+
"idempotency-key": idem,
|
|
115
|
+
};
|
|
116
|
+
if (token) headers["x-webhook-token"] = token;
|
|
117
|
+
|
|
118
|
+
// Bounded retry so a brief ingress hiccup doesn't drop a message; the stable
|
|
119
|
+
// idempotency key makes the retry safe.
|
|
120
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(hookUrl as string, { method: "POST", headers, body });
|
|
123
|
+
if (res.ok) return;
|
|
124
|
+
// 401/403 => auth misconfigured; retrying won't help.
|
|
125
|
+
if (res.status === 401 || res.status === 403) {
|
|
126
|
+
log(`ingress rejected event (${res.status}); check the trigger's auth secret`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
log(`ingress returned ${res.status} for ${topic} (attempt ${attempt})`);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
log(`POST failed for ${topic} (attempt ${attempt}): ${(e as Error).message}`);
|
|
132
|
+
}
|
|
133
|
+
await sleep(Math.min(250 * 2 ** (attempt - 1), 4000));
|
|
134
|
+
}
|
|
135
|
+
log(`giving up on event for ${topic} after retries`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function sleep(ms: number): Promise<void> {
|
|
139
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const RESERVED_CONN_KEYS = new Set(["url", "username", "password", "clientId"]);
|
|
143
|
+
const options: IClientOptions = {
|
|
144
|
+
reconnectPeriod: 2000,
|
|
145
|
+
connectTimeout: 30000,
|
|
146
|
+
};
|
|
147
|
+
// Pass any extra connection keys straight through to the mqtt client options
|
|
148
|
+
// (e.g. `rejectUnauthorized`, `ca`, `keepalive`), excluding the ones we map
|
|
149
|
+
// explicitly and `url` (used to dial, not an option).
|
|
150
|
+
for (const [k, v] of Object.entries(connection)) {
|
|
151
|
+
if (!RESERVED_CONN_KEYS.has(k) && v !== undefined) (options as Record<string, unknown>)[k] = v;
|
|
152
|
+
}
|
|
153
|
+
if (connection.username) options.username = connection.username;
|
|
154
|
+
if (connection.password) options.password = connection.password;
|
|
155
|
+
if (connection.clientId) options.clientId = connection.clientId;
|
|
156
|
+
|
|
157
|
+
// Redact any userinfo (user:pass@) so credentials embedded in the URL never
|
|
158
|
+
// reach the trigger log.
|
|
159
|
+
function redact(u: string): string {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = new URL(u);
|
|
162
|
+
if (parsed.username || parsed.password) {
|
|
163
|
+
parsed.username = parsed.username ? "***" : "";
|
|
164
|
+
parsed.password = parsed.password ? "***" : "";
|
|
165
|
+
}
|
|
166
|
+
return parsed.toString();
|
|
167
|
+
} catch {
|
|
168
|
+
return u.replace(/\/\/[^@/]*@/, "//***@");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log(`connecting to ${redact(brokerUrl)}; topics=[${topics.join(", ")}] qos=${qos}`);
|
|
173
|
+
const client = mqtt.connect(brokerUrl, options);
|
|
174
|
+
|
|
175
|
+
client.on("connect", () => {
|
|
176
|
+
log("connected");
|
|
177
|
+
client.subscribe(topics, { qos } as IClientSubscribeOptions, (err, granted) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
log(`subscribe failed: ${err.message}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const g = (granted ?? []).map((x) => `${x.topic}@${x.qos}`).join(", ");
|
|
183
|
+
log(`subscribed: ${g || "(none granted)"}`);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
client.on("message", (topic, payload) => {
|
|
188
|
+
void emit(topic, payload);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
client.on("reconnect", () => log("reconnecting…"));
|
|
192
|
+
client.on("error", (err) => log(`client error: ${err.message}`));
|
|
193
|
+
client.on("close", () => log("connection closed"));
|
|
194
|
+
|
|
195
|
+
// Terminate cleanly when the supervisor sends SIGTERM/SIGINT (kill on App stop),
|
|
196
|
+
// under both Node (`process.on`) and Deno (`Deno.addSignalListener`).
|
|
197
|
+
const shutdown = () => {
|
|
198
|
+
log("shutting down");
|
|
199
|
+
client.end(true, {}, () => {
|
|
200
|
+
const g = globalThis as { Deno?: { exit(code: number): never }; process?: { exit(code: number): never } };
|
|
201
|
+
(g.Deno ?? g.process)?.exit(0);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
const g = globalThis as {
|
|
205
|
+
process?: { on(ev: string, cb: () => void): void };
|
|
206
|
+
Deno?: { addSignalListener(sig: string, cb: () => void): void };
|
|
207
|
+
};
|
|
208
|
+
if (g.Deno?.addSignalListener) {
|
|
209
|
+
g.Deno.addSignalListener("SIGTERM", shutdown);
|
|
210
|
+
g.Deno.addSignalListener("SIGINT", shutdown);
|
|
211
|
+
} else if (g.process) {
|
|
212
|
+
g.process.on("SIGTERM", shutdown);
|
|
213
|
+
g.process.on("SIGINT", shutdown);
|
|
214
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nano-ide-trigger-mqtt",
|
|
3
|
+
"kind": "trigger",
|
|
4
|
+
"displayName": "MQTT",
|
|
5
|
+
"triggerSources": [
|
|
6
|
+
{
|
|
7
|
+
"kind": "mqtt",
|
|
8
|
+
"displayName": "MQTT (subscribe)",
|
|
9
|
+
"transport": "webhook",
|
|
10
|
+
"driver": "driver.ts",
|
|
11
|
+
"configFields": [
|
|
12
|
+
{
|
|
13
|
+
"key": "url",
|
|
14
|
+
"label": "Broker URL",
|
|
15
|
+
"description": "MQTT broker URL, e.g. mqtt://localhost:1883, mqtts://host:8883, or ws://host:9001.",
|
|
16
|
+
"default": "mqtt://localhost:1883"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"key": "topics",
|
|
20
|
+
"label": "Topics",
|
|
21
|
+
"description": "Topic filter(s) to subscribe to. Comma-separate several; MQTT wildcards + (single level) and # (multi level) are allowed, e.g. home/+/temperature, sensors/#."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"key": "qos",
|
|
25
|
+
"label": "QoS",
|
|
26
|
+
"description": "Subscription quality of service: 0 (at most once), 1 (at least once), or 2 (exactly once). Defaults to 1.",
|
|
27
|
+
"default": "1"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nanobpm/nano-ide-trigger-mqtt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MQTT trigger source pack for the Nano/Urban RAD console: subscribe to broker topics and drive process instances from messages (ADR 0025).",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": ["nano-ide-ext", "nano-ide-trigger", "nanobpm", "mqtt", "iot", "home-automation"],
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"repository": { "type": "git", "url": "https://github.com/jwulf/nano-ide.git", "directory": "packages/trigger-mqtt" },
|
|
10
|
+
"files": ["nano-ide.ext.json", "driver.ts", "README.md"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"mqtt": "^5.10.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|