mqtt-plus 1.4.2 → 1.4.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/AGENTS.md +23 -13
- package/CHANGELOG.md +11 -0
- package/README.md +18 -3
- package/doc/mqtt-plus-internals.md +362 -42
- package/dst-stage1/mqtt-plus-auth.d.ts +1 -0
- package/dst-stage1/mqtt-plus-subscription.js +1 -1
- package/dst-stage2/mqtt-plus.cjs.js +1 -1
- package/dst-stage2/mqtt-plus.esm.js +1 -1
- package/package.json +1 -1
- package/src/mqtt-plus-auth.ts +1 -1
- package/src/mqtt-plus-subscription.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -107,26 +107,36 @@ Each trait lives in its own file: `src/mqtt-plus-<trait>.ts`.
|
|
|
107
107
|
|
|
108
108
|
### Documentation
|
|
109
109
|
|
|
110
|
-
The `doc/` directory contains D2 diagram sources
|
|
111
|
-
|
|
110
|
+
The `doc/` directory contains Markdown documentation, D2 diagram sources,
|
|
111
|
+
and generated SVG files:
|
|
112
112
|
|
|
113
|
-
- `doc/mqtt-plus-
|
|
114
|
-
- `doc/mqtt-plus-
|
|
115
|
-
- `doc/mqtt-plus-
|
|
116
|
-
- `doc/mqtt-plus-comm
|
|
117
|
-
- `doc/
|
|
113
|
+
- `doc/mqtt-plus-api.md` — public API reference
|
|
114
|
+
- `doc/mqtt-plus-architecture.{d2,svg,md}` — architecture overview (diagram + docs)
|
|
115
|
+
- `doc/mqtt-plus-broker-setup.md` — MQTT broker setup guide
|
|
116
|
+
- `doc/mqtt-plus-comm.md` — communication patterns overview
|
|
117
|
+
- `doc/mqtt-plus-comm-event-emission.{d2,svg}` — Event Emission pattern diagram
|
|
118
|
+
- `doc/mqtt-plus-comm-service-call.{d2,svg}` — Service Call pattern diagram
|
|
119
|
+
- `doc/mqtt-plus-comm-sink-push.{d2,svg}` — Sink Push pattern diagram
|
|
120
|
+
- `doc/mqtt-plus-comm-source-fetch.{d2,svg}` — Source Fetch pattern diagram
|
|
121
|
+
- `doc/mqtt-plus-internals.md` — internal implementation details
|
|
118
122
|
|
|
119
|
-
Regenerate with `npm start build-doc` (requires the `etc/d2.mts` helper script).
|
|
123
|
+
Regenerate diagrams with `npm start build-doc` (requires the `etc/d2.mts` helper script).
|
|
120
124
|
|
|
121
125
|
### Tests
|
|
122
126
|
|
|
123
127
|
Test files live in `tst/`:
|
|
124
128
|
|
|
125
|
-
| File
|
|
126
|
-
|
|
127
|
-
| `tst/mqtt-plus.
|
|
128
|
-
| `tst/mqtt-plus-mosquitto.ts`
|
|
129
|
-
| `tst/
|
|
129
|
+
| File | Role |
|
|
130
|
+
|-----------------------------------|------|
|
|
131
|
+
| `tst/mqtt-plus-0-fixture.ts` | Shared test fixture setup (broker, MQTTp instances, etc.) |
|
|
132
|
+
| `tst/mqtt-plus-0-mosquitto.ts` | Helper for starting/stopping the Mosquitto MQTT broker |
|
|
133
|
+
| `tst/mqtt-plus-1-api.spec.ts` | API type and endpoint definition tests |
|
|
134
|
+
| `tst/mqtt-plus-2-event.spec.ts` | Event Emission pattern tests |
|
|
135
|
+
| `tst/mqtt-plus-3-service.spec.ts` | Service Call / RPC pattern tests |
|
|
136
|
+
| `tst/mqtt-plus-4-sink.spec.ts` | Sink Push pattern tests |
|
|
137
|
+
| `tst/mqtt-plus-5-source.spec.ts` | Source Fetch pattern tests |
|
|
138
|
+
| `tst/mqtt-plus-6-misc.spec.ts` | Miscellaneous / edge-case tests |
|
|
139
|
+
| `tst/tsc.json` | TypeScript configuration for the test directory |
|
|
130
140
|
|
|
131
141
|
### Type System
|
|
132
142
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
ChangeLog
|
|
3
3
|
=========
|
|
4
4
|
|
|
5
|
+
1.4.4 (2026-02-21)
|
|
6
|
+
------------------
|
|
7
|
+
|
|
8
|
+
- CLEANUP: cleanup documentation
|
|
9
|
+
|
|
10
|
+
1.4.3 (2026-02-21)
|
|
11
|
+
------------------
|
|
12
|
+
|
|
13
|
+
- IMPROVEMENT: allow JWT expirations
|
|
14
|
+
- DOCUMENTATION: document more internals
|
|
15
|
+
|
|
5
16
|
1.4.2 (2026-02-21)
|
|
6
17
|
------------------
|
|
7
18
|
|
package/README.md
CHANGED
|
@@ -117,15 +117,29 @@ mqtt.on("connect", async () => {
|
|
|
117
117
|
Documentation
|
|
118
118
|
-------------
|
|
119
119
|
|
|
120
|
+
Main documentation:
|
|
121
|
+
|
|
120
122
|
- [**Communication Patterns**](doc/mqtt-plus-comm.md)
|
|
121
123
|
- [**Application Programming Interface (API)**](doc/mqtt-plus-api.md)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
|
|
125
|
+
Additional auxilliary documentation:
|
|
126
|
+
|
|
127
|
+
- [Extra: Architecture Overview](doc/mqtt-plus-architecture.md)
|
|
128
|
+
- [Extra: Internal Protocol](doc/mqtt-plus-internals.md)
|
|
124
129
|
- [Extra: Broker Setup](doc/mqtt-plus-broker-setup.md)
|
|
125
130
|
|
|
126
131
|
Notice
|
|
127
132
|
------
|
|
128
133
|
|
|
134
|
+
> [!Note]
|
|
135
|
+
> **MQTT+** and its peer dependency **MQTT** provide a powerful
|
|
136
|
+
> functionality, but are not small in size. **MQTT+** is 3.500 LoC
|
|
137
|
+
> and 75 KB in size (ESM and CJS format). When bundled with all its
|
|
138
|
+
> dependencies, it is 220 KB in size (UMD format). Its peer dependency
|
|
139
|
+
> **MQTT.js** is 370 KB (ESM and CJS format) and 860 KB (UMD format) in
|
|
140
|
+
> size. For a Node.js application, this usually doesn't matter. For a
|
|
141
|
+
> HTML5 SPA it matters more, but usually is still acceptable.
|
|
142
|
+
|
|
129
143
|
> [!Note]
|
|
130
144
|
> **MQTT+** is still somewhat similar to and originally derived from the weaker
|
|
131
145
|
> [MQTT-JSON-RPC](https://github.com/rse/mqtt-json-rpc) library of the same
|
|
@@ -139,7 +153,7 @@ Notice
|
|
|
139
153
|
License
|
|
140
154
|
-------
|
|
141
155
|
|
|
142
|
-
Copyright
|
|
156
|
+
Copyright © 2018-2026 Dr. Ralf S. Engelschall (http://engelschall.com/)
|
|
143
157
|
|
|
144
158
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
145
159
|
a copy of this software and associated documentation files (the
|
|
@@ -159,3 +173,4 @@ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
|
159
173
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
160
174
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
161
175
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
176
|
+
|
|
@@ -1,59 +1,380 @@
|
|
|
1
1
|
|
|
2
2
|
MQTT+ Internals
|
|
3
|
-
|
|
3
|
+
===============
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Overview
|
|
6
|
+
--------
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
**MQTT+** implements a message-oriented protocol on top of standard MQTT.
|
|
9
|
+
Each MQTT+ instance is identified by a unique **peer ID** (a NanoID).
|
|
10
|
+
All messages are encoded as structured objects and transported as MQTT
|
|
11
|
+
payloads on well-defined MQTT topics. The protocol supports four
|
|
12
|
+
communication patterns: **Event Emission**, **Service Call**, **Sink
|
|
13
|
+
Push**, and **Source Fetch**.
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
Message Encoding
|
|
16
|
+
----------------
|
|
17
|
+
|
|
18
|
+
Messages are encoded using one of two codecs, selected at instance creation:
|
|
19
|
+
|
|
20
|
+
- **CBOR** (default): Binary encoding via the `cbor2` library.
|
|
21
|
+
`Uint8Array` and `Buffer` values are encoded natively.
|
|
22
|
+
MQTT payloads are `Uint8Array`.
|
|
23
|
+
|
|
24
|
+
- **JSON**: Text encoding via a custom `JSONX` serializer.
|
|
25
|
+
`Uint8Array` values are encoded as `{ "__Uint8Array": "<base64>" }`.
|
|
26
|
+
MQTT payloads are UTF-8 strings.
|
|
27
|
+
|
|
28
|
+
Message Base Structure
|
|
29
|
+
----------------------
|
|
30
|
+
|
|
31
|
+
Every MQTT+ message shares a common base structure:
|
|
32
|
+
|
|
33
|
+
| Field | Type | Description |
|
|
34
|
+
|------------|-------------------|------------------------------------------------|
|
|
35
|
+
| `version` | `string` | Protocol version identifier, format `MQTT+/X.X` (e.g. `MQTT+/1.4`). Must match between peers. |
|
|
36
|
+
| `type` | `string` | One of the 11 message types (see below). |
|
|
37
|
+
| `id` | `string` | NanoID correlating requests with their responses. |
|
|
38
|
+
| `sender` | `string?` | NanoID of the sending peer. |
|
|
39
|
+
| `receiver` | `string?` | NanoID of the intended receiving peer. |
|
|
40
|
+
|
|
41
|
+
The `version` field is checked on every incoming message. A mismatch
|
|
42
|
+
causes the message to be rejected.
|
|
43
|
+
|
|
44
|
+
Message Types
|
|
45
|
+
-------------
|
|
46
|
+
|
|
47
|
+
The protocol defines 11 message types, grouped by communication pattern:
|
|
48
|
+
|
|
49
|
+
### Event Emission (1 message type)
|
|
50
|
+
|
|
51
|
+
| Type | Direction | Purpose |
|
|
52
|
+
|--------------------|-----------|-------------------------------|
|
|
53
|
+
| `event-emission` | one-way | Fire-and-forget event notification |
|
|
54
|
+
|
|
55
|
+
### Service Call (2 message types)
|
|
56
|
+
|
|
57
|
+
| Type | Direction | Purpose |
|
|
58
|
+
|--------------------------|-------------|--------------------------|
|
|
59
|
+
| `service-call-request` | caller -> callee | RPC request with parameters |
|
|
60
|
+
| `service-call-response` | callee -> caller | RPC result or error |
|
|
61
|
+
|
|
62
|
+
### Sink Push (4 message types)
|
|
63
|
+
|
|
64
|
+
| Type | Direction | Purpose |
|
|
65
|
+
|----------------------|------------------|-------------------------------|
|
|
66
|
+
| `sink-push-request` | pusher -> sink | Initiate data push |
|
|
67
|
+
| `sink-push-response` | sink -> pusher | Acknowledge (ack) or reject (nak) |
|
|
68
|
+
| `sink-push-chunk` | pusher -> sink | Transfer a data chunk |
|
|
69
|
+
| `sink-push-credit` | sink -> pusher | Replenish flow control credit |
|
|
70
|
+
|
|
71
|
+
### Source Fetch (4 message types)
|
|
72
|
+
|
|
73
|
+
| Type | Direction | Purpose |
|
|
74
|
+
|-------------------------|--------------------|-------------------------------|
|
|
75
|
+
| `source-fetch-request` | fetcher -> source | Initiate data fetch |
|
|
76
|
+
| `source-fetch-response` | source -> fetcher | Acknowledge (ack) or reject (nak) |
|
|
77
|
+
| `source-fetch-chunk` | source -> fetcher | Transfer a data chunk |
|
|
78
|
+
| `source-fetch-credit` | fetcher -> source | Replenish flow control credit |
|
|
79
|
+
|
|
80
|
+
Message Fields by Type
|
|
81
|
+
----------------------
|
|
82
|
+
|
|
83
|
+
### `event-emission`
|
|
84
|
+
|
|
85
|
+
| Field | Type | Required | Description |
|
|
86
|
+
|----------|------------------------|----------|-------------------------------|
|
|
87
|
+
| `name` | `string` | yes | Endpoint name |
|
|
88
|
+
| `params` | `any[]` | no | Event parameters (max 64) |
|
|
89
|
+
| `auth` | `string[]` | no | JWT tokens (max 8, each max 8192 chars) |
|
|
90
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata (non-array object) |
|
|
91
|
+
|
|
92
|
+
### `service-call-request`
|
|
93
|
+
|
|
94
|
+
| Field | Type | Required | Description |
|
|
95
|
+
|----------|------------------------|----------|-------------------------------|
|
|
96
|
+
| `name` | `string` | yes | Service endpoint name |
|
|
97
|
+
| `params` | `any[]` | no | Call parameters (max 64) |
|
|
98
|
+
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
99
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
18
100
|
|
|
19
|
-
|
|
20
|
-
remote service named `example/hello` with parameters `"world"` and `42` via...
|
|
101
|
+
### `service-call-response`
|
|
21
102
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
103
|
+
| Field | Type | Required | Description |
|
|
104
|
+
|----------|-----------|----------|--------------------------------------|
|
|
105
|
+
| `result` | `any` | no | Return value on success |
|
|
106
|
+
| `error` | `string` | no | Error message on failure |
|
|
107
|
+
|
|
108
|
+
Exactly one of `result` or `error` is present.
|
|
109
|
+
|
|
110
|
+
### `sink-push-request`
|
|
111
|
+
|
|
112
|
+
| Field | Type | Required | Description |
|
|
113
|
+
|----------|------------------------|----------|-------------------------------|
|
|
114
|
+
| `name` | `string` | yes | Sink endpoint name |
|
|
115
|
+
| `params` | `any[]` | no | Push parameters (max 64) |
|
|
116
|
+
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
117
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
118
|
+
|
|
119
|
+
### `sink-push-response`
|
|
120
|
+
|
|
121
|
+
| Field | Type | Required | Description |
|
|
122
|
+
|----------|------------------------|----------|-------------------------------|
|
|
123
|
+
| `name` | `string` | yes | Sink endpoint name |
|
|
124
|
+
| `error` | `string` | no | Error message (nak) or absent (ack) |
|
|
125
|
+
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
126
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
127
|
+
| `credit` | `integer` | no | Initial flow control credit (min 1) |
|
|
128
|
+
|
|
129
|
+
### `sink-push-chunk`
|
|
130
|
+
|
|
131
|
+
| Field | Type | Required | Description |
|
|
132
|
+
|---------|--------------|----------|------------------------------------|
|
|
133
|
+
| `name` | `string` | yes | Sink endpoint name |
|
|
134
|
+
| `chunk` | `Uint8Array` | no | Data chunk payload |
|
|
135
|
+
| `error` | `string` | no | Error message (aborts the stream) |
|
|
136
|
+
| `final` | `boolean` | no | `true` on the last chunk |
|
|
137
|
+
|
|
138
|
+
### `sink-push-credit`
|
|
139
|
+
|
|
140
|
+
| Field | Type | Required | Description |
|
|
141
|
+
|----------|-----------|----------|-------------------------------------|
|
|
142
|
+
| `name` | `string` | yes | Sink endpoint name |
|
|
143
|
+
| `credit` | `integer` | yes | Number of additional credits (min 1)|
|
|
144
|
+
|
|
145
|
+
### `source-fetch-request`
|
|
146
|
+
|
|
147
|
+
| Field | Type | Required | Description |
|
|
148
|
+
|----------|------------------------|----------|-------------------------------|
|
|
149
|
+
| `name` | `string` | yes | Source endpoint name |
|
|
150
|
+
| `params` | `any[]` | no | Fetch parameters (max 64) |
|
|
151
|
+
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
152
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
153
|
+
| `credit` | `integer` | no | Initial flow control credit (min 1) |
|
|
154
|
+
|
|
155
|
+
### `source-fetch-response`
|
|
156
|
+
|
|
157
|
+
| Field | Type | Required | Description |
|
|
158
|
+
|----------|------------------------|----------|-------------------------------|
|
|
159
|
+
| `name` | `string` | yes | Source endpoint name |
|
|
160
|
+
| `error` | `string` | no | Error message (nak) or absent (ack) |
|
|
161
|
+
| `auth` | `string[]` | no | JWT tokens (max 8) |
|
|
162
|
+
| `meta` | `Record<string, any>` | no | Arbitrary metadata |
|
|
163
|
+
|
|
164
|
+
### `source-fetch-chunk`
|
|
165
|
+
|
|
166
|
+
| Field | Type | Required | Description |
|
|
167
|
+
|---------|--------------|----------|------------------------------------|
|
|
168
|
+
| `name` | `string` | yes | Source endpoint name |
|
|
169
|
+
| `chunk` | `Uint8Array` | no | Data chunk payload |
|
|
170
|
+
| `error` | `string` | no | Error message (aborts the stream) |
|
|
171
|
+
| `final` | `boolean` | no | `true` on the last chunk |
|
|
172
|
+
|
|
173
|
+
### `source-fetch-credit`
|
|
174
|
+
|
|
175
|
+
| Field | Type | Required | Description |
|
|
176
|
+
|----------|-----------|----------|-------------------------------------|
|
|
177
|
+
| `name` | `string` | yes | Source endpoint name |
|
|
178
|
+
| `credit` | `integer` | yes | Number of additional credits (min 1)|
|
|
179
|
+
|
|
180
|
+
MQTT Topic Structure
|
|
181
|
+
--------------------
|
|
182
|
+
|
|
183
|
+
MQTT+ maps messages to MQTT topics using the pattern:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
{name}/{operation}/{peerId}
|
|
26
187
|
```
|
|
27
188
|
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
189
|
+
- **`name`**: The endpoint name (e.g. `example/hello`).
|
|
190
|
+
- **`operation`**: The message type (e.g. `service-call-request`).
|
|
191
|
+
- **`peerId`**: Either the target peer's NanoID (for directed messages) or
|
|
192
|
+
`any` (for broadcast messages).
|
|
193
|
+
|
|
194
|
+
### Broadcast Topics (Requests)
|
|
195
|
+
|
|
196
|
+
Request messages are published to broadcast topics when no specific
|
|
197
|
+
receiver is targeted:
|
|
198
|
+
|
|
199
|
+
| Pattern | Operation | Purpose |
|
|
200
|
+
|----------------------------|-------------------------|------------------------|
|
|
201
|
+
| `{name}/event-emission/any` | `event-emission` | Broadcast event |
|
|
202
|
+
| `{name}/event-emission/{peerId}` | `event-emission` | Directed event |
|
|
203
|
+
| `$share/{share}/{name}/service-call-request/any` | `service-call-request` | Shared service request |
|
|
204
|
+
| `$share/{share}/{name}/source-fetch-request/any` | `source-fetch-request` | Shared fetch request |
|
|
205
|
+
| `$share/{share}/{name}/sink-push-request/any` | `sink-push-request` | Shared push request |
|
|
206
|
+
|
|
207
|
+
Service, source, and sink requests use **MQTT shared subscriptions**
|
|
208
|
+
(`$share/{group}/...`) to distribute load across multiple handlers
|
|
209
|
+
(default group: `"default"`). Event emissions do *not* use shared
|
|
210
|
+
subscriptions by default (all registered handlers receive the event).
|
|
211
|
+
|
|
212
|
+
### Direct Topics (Responses and Chunks)
|
|
213
|
+
|
|
214
|
+
Response messages, chunks, and credits are sent to peer-specific topics:
|
|
215
|
+
|
|
216
|
+
| Pattern | Operation |
|
|
217
|
+
|--------------------------------------------------|---------------------------|
|
|
218
|
+
| `{name}/service-call-response/{clientId}` | `service-call-response` |
|
|
219
|
+
| `{name}/sink-push-response/{clientId}` | `sink-push-response` |
|
|
220
|
+
| `{name}/sink-push-chunk/{sinkId}` | `sink-push-chunk` |
|
|
221
|
+
| `{name}/sink-push-credit/{pusherId}` | `sink-push-credit` |
|
|
222
|
+
| `{name}/source-fetch-response/{clientId}` | `source-fetch-response` |
|
|
223
|
+
| `{name}/source-fetch-chunk/{clientId}` | `source-fetch-chunk` |
|
|
224
|
+
| `{name}/source-fetch-credit/{sourceId}` | `source-fetch-credit` |
|
|
225
|
+
|
|
226
|
+
The `{clientId}` is the `sender` field from the corresponding request
|
|
227
|
+
message, ensuring responses are routed back to the originating peer only.
|
|
228
|
+
|
|
229
|
+
### Topic Customization
|
|
230
|
+
|
|
231
|
+
The topic structure is fully customizable through the `topicMake` and
|
|
232
|
+
`topicMatch` options at instance creation time.
|
|
233
|
+
|
|
234
|
+
MQTT QoS Levels
|
|
235
|
+
---------------
|
|
236
|
+
|
|
237
|
+
| Communication Pattern | QoS | Rationale |
|
|
238
|
+
|-----------------------|-----|-----------------------------------|
|
|
239
|
+
| Event Emission | 0 | Best-effort, fire-and-forget |
|
|
240
|
+
| Service Call | 2 | Exactly-once for reliable RPC |
|
|
241
|
+
| Sink Push | 2 | Exactly-once for reliable data transfer |
|
|
242
|
+
| Source Fetch | 2 | Exactly-once for reliable data transfer |
|
|
243
|
+
|
|
244
|
+
Credit-Based Flow Control
|
|
245
|
+
-------------------------
|
|
246
|
+
|
|
247
|
+
Sink Push and Source Fetch patterns use a **credit-based flow control**
|
|
248
|
+
mechanism to prevent the data producer from overwhelming the consumer.
|
|
249
|
+
|
|
250
|
+
### How It Works
|
|
251
|
+
|
|
252
|
+
1. The **consumer** grants an initial number of credits (default: 4)
|
|
253
|
+
to the **producer** (via the response message or request message).
|
|
254
|
+
2. Each chunk sent by the producer **consumes one credit**.
|
|
255
|
+
3. When credits are exhausted, the producer **blocks** (waits).
|
|
256
|
+
4. As the consumer processes chunks, it sends **credit messages** to
|
|
257
|
+
replenish the producer's credit, unblocking it.
|
|
258
|
+
5. The chunk size is configurable (default: 16 KB).
|
|
259
|
+
|
|
260
|
+
### Configuration
|
|
261
|
+
|
|
262
|
+
| Option | Default | Description |
|
|
263
|
+
|---------------|-----------|------------------------------------------|
|
|
264
|
+
| `chunkSize` | `16384` | Maximum bytes per chunk (16 KB) |
|
|
265
|
+
| `chunkCredit` | `4` | Number of chunks allowed in-flight |
|
|
266
|
+
|
|
267
|
+
Setting `chunkCredit` to `0` disables flow control entirely.
|
|
268
|
+
|
|
269
|
+
### Direction of Credit
|
|
270
|
+
|
|
271
|
+
| Pattern | Credit Sender | Credit Receiver | Credit Message Type |
|
|
272
|
+
|--------------|---------------|-----------------|-------------------------|
|
|
273
|
+
| Sink Push | Sink | Pusher | `sink-push-credit` |
|
|
274
|
+
| Source Fetch | Fetcher | Source | `source-fetch-credit` |
|
|
275
|
+
|
|
276
|
+
Authentication
|
|
277
|
+
--------------
|
|
278
|
+
|
|
279
|
+
MQTT+ provides optional JWT-based authentication and role-based
|
|
280
|
+
authorization on any endpoint.
|
|
281
|
+
|
|
282
|
+
### Setup
|
|
283
|
+
|
|
284
|
+
1. The **server** sets a shared secret via `credential(secret)`.
|
|
285
|
+
The secret is derived into a 256-bit key using PBKDF2-SHA256
|
|
286
|
+
(600,000 iterations).
|
|
287
|
+
|
|
288
|
+
2. The server **issues JWT tokens** via `issue({ roles, id?, exp? })`,
|
|
289
|
+
signed with HS256.
|
|
290
|
+
|
|
291
|
+
3. The **client** stores tokens via `authenticate(token)`.
|
|
292
|
+
|
|
293
|
+
### Token Transmission
|
|
294
|
+
|
|
295
|
+
When a client sends a request (`event-emission`, `service-call-request`,
|
|
296
|
+
`sink-push-request`, or `source-fetch-request`), all stored JWT tokens
|
|
297
|
+
are included in the `auth` field (max 8 tokens, each max 8192 characters).
|
|
298
|
+
|
|
299
|
+
### Validation
|
|
300
|
+
|
|
301
|
+
On the server side, the handler validates the tokens against
|
|
302
|
+
the configured credential and required roles:
|
|
303
|
+
|
|
304
|
+
- The token must be a valid HS256-signed JWT.
|
|
305
|
+
- If the token payload contains an `id` field, it must match the `sender`
|
|
306
|
+
peer ID of the request.
|
|
307
|
+
- If the token payload contains an `exp` field, the token must not be
|
|
308
|
+
expired.
|
|
309
|
+
- The token's `roles` array must contain at least one of the
|
|
310
|
+
required roles.
|
|
311
|
+
|
|
312
|
+
### Authentication Modes
|
|
313
|
+
|
|
314
|
+
| Mode | Behavior |
|
|
315
|
+
|------------|----------------------------------------------------|
|
|
316
|
+
| `require` | Request is rejected if no valid token is found. |
|
|
317
|
+
| `optional` | Request passes even if no valid token is found. |
|
|
318
|
+
|
|
319
|
+
Message Dispatching
|
|
320
|
+
-------------------
|
|
321
|
+
|
|
322
|
+
### Request Dispatching
|
|
323
|
+
|
|
324
|
+
Incoming request messages are dispatched based on the combination of
|
|
325
|
+
their `type` and `name` fields. The dispatch key is
|
|
326
|
+
`{operation}:{name}` (e.g. `service-call-request:example/hello`).
|
|
327
|
+
|
|
328
|
+
### Response Dispatching
|
|
329
|
+
|
|
330
|
+
Incoming response messages are dispatched based on the combination
|
|
331
|
+
of their `type` and `id` fields. The dispatch key is
|
|
332
|
+
`{operation}:{requestId}` (e.g. `service-call-response:vwLzfQDu2uEeOdOfIlT42`).
|
|
333
|
+
This ensures responses are correlated to their originating requests.
|
|
334
|
+
|
|
335
|
+
Timeouts
|
|
336
|
+
--------
|
|
337
|
+
|
|
338
|
+
All bi-directional patterns (Service Call, Sink Push, Source Fetch) are
|
|
339
|
+
guarded by a configurable timeout (default: 10 seconds). If no response
|
|
340
|
+
or progress is received within the timeout, the operation is aborted
|
|
341
|
+
with a timeout error. For streaming patterns, each chunk or credit
|
|
342
|
+
message resets the timeout.
|
|
343
|
+
|
|
344
|
+
Error Handling
|
|
345
|
+
--------------
|
|
346
|
+
|
|
347
|
+
Errors are communicated in two ways, depending on timing:
|
|
348
|
+
|
|
349
|
+
1. **Before data transfer starts**: The response message carries an
|
|
350
|
+
`error` field (nak response).
|
|
351
|
+
|
|
352
|
+
2. **During data transfer**: A chunk message carries an `error` field
|
|
353
|
+
and `final: true`, terminating the stream.
|
|
354
|
+
|
|
355
|
+
Example Message Exchange
|
|
356
|
+
------------------------
|
|
357
|
+
|
|
358
|
+
A service call to `example/hello` with parameters `"world"` and `42`:
|
|
359
|
+
|
|
360
|
+
**Request** (published to `example/hello/service-call-request/any`):
|
|
31
361
|
|
|
32
362
|
```json
|
|
33
363
|
{
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
364
|
+
"version": "MQTT+/1.4",
|
|
365
|
+
"type": "service-call-request",
|
|
366
|
+
"id": "vwLzfQDu2uEeOdOfIlT42",
|
|
367
|
+
"name": "example/hello",
|
|
368
|
+
"params": [ "world", 42 ],
|
|
369
|
+
"sender": "2IBMSk0NPnrz1AeTERoea"
|
|
39
370
|
}
|
|
40
371
|
```
|
|
41
372
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
mqttp.service("example/hello", (a1, a2) => {
|
|
46
|
-
return `${a1}:${a2}`
|
|
47
|
-
})
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
...and then its result, in the above `mqttp.call()` example `"world:42"`, is then
|
|
51
|
-
sent back as the following success response
|
|
52
|
-
message to the temporary (client-specific) MQTT topic
|
|
53
|
-
`example/hello/service-call-response/2IBMSk0NPnrz1AeTERoea`:
|
|
373
|
+
**Response** (published to `example/hello/service-call-response/2IBMSk0NPnrz1AeTERoea`):
|
|
54
374
|
|
|
55
375
|
```json
|
|
56
376
|
{
|
|
377
|
+
"version": "MQTT+/1.4",
|
|
57
378
|
"type": "service-call-response",
|
|
58
379
|
"id": "vwLzfQDu2uEeOdOfIlT42",
|
|
59
380
|
"result": "world:42",
|
|
@@ -62,7 +383,6 @@ message to the temporary (client-specific) MQTT topic
|
|
|
62
383
|
}
|
|
63
384
|
```
|
|
64
385
|
|
|
65
|
-
The `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`id` is used for correlating the response to the request only.
|
|
386
|
+
The `id` field correlates the response to the request. The `sender`
|
|
387
|
+
field in the request is used as the peer-specific suffix in the response
|
|
388
|
+
topic, ensuring only the caller receives the response.
|
|
@@ -118,7 +118,7 @@ export class SubscriptionTrait extends BaseTrait {
|
|
|
118
118
|
super(...arguments);
|
|
119
119
|
this.subscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic));
|
|
120
120
|
}
|
|
121
|
-
/* destroy
|
|
121
|
+
/* destroy subscription trait */
|
|
122
122
|
async destroy() {
|
|
123
123
|
await this.subscriptions.flush();
|
|
124
124
|
await super.destroy();
|
|
@@ -1131,7 +1131,7 @@ class SubscriptionTrait extends BaseTrait {
|
|
|
1131
1131
|
super(...arguments);
|
|
1132
1132
|
this.subscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic));
|
|
1133
1133
|
}
|
|
1134
|
-
/* destroy
|
|
1134
|
+
/* destroy subscription trait */
|
|
1135
1135
|
async destroy() {
|
|
1136
1136
|
await this.subscriptions.flush();
|
|
1137
1137
|
await super.destroy();
|
|
@@ -1110,7 +1110,7 @@ class SubscriptionTrait extends BaseTrait {
|
|
|
1110
1110
|
super(...arguments);
|
|
1111
1111
|
this.subscriptions = new RefCountedSubscription((topic, options) => this._subscribeTopic(topic, options), (topic) => this._unsubscribeTopic(topic));
|
|
1112
1112
|
}
|
|
1113
|
-
/* destroy
|
|
1113
|
+
/* destroy subscription trait */
|
|
1114
1114
|
async destroy() {
|
|
1115
1115
|
await this.subscriptions.flush();
|
|
1116
1116
|
await super.destroy();
|
package/package.json
CHANGED
package/src/mqtt-plus-auth.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { MetaTrait } from "./mqtt-plus-meta"
|
|
|
36
36
|
export type AuthMode = "require" | "optional"
|
|
37
37
|
export type AuthRole = string
|
|
38
38
|
export type AuthOption = AuthRole | { mode: AuthMode, roles: AuthRole[] }
|
|
39
|
-
type TokenPayload = { roles: AuthRole[], id?: string }
|
|
39
|
+
type TokenPayload = { roles: AuthRole[], id?: string, exp?: number }
|
|
40
40
|
|
|
41
41
|
/* authentication trait */
|
|
42
42
|
export class AuthTrait<T extends APISchema = APISchema> extends MetaTrait<T> {
|
|
@@ -133,7 +133,7 @@ export class SubscriptionTrait<T extends APISchema = APISchema> extends BaseTrai
|
|
|
133
133
|
(topic) => this._unsubscribeTopic(topic)
|
|
134
134
|
)
|
|
135
135
|
|
|
136
|
-
/* destroy
|
|
136
|
+
/* destroy subscription trait */
|
|
137
137
|
override async destroy () {
|
|
138
138
|
await this.subscriptions.flush()
|
|
139
139
|
await super.destroy()
|