mqtt-plus 1.1.3 → 1.2.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +49 -8
  3. package/dst-stage1/mqtt-plus-auth.d.ts +2 -1
  4. package/dst-stage1/mqtt-plus-auth.js +15 -7
  5. package/dst-stage1/mqtt-plus-base.js +3 -1
  6. package/dst-stage1/mqtt-plus-codec.d.ts +4 -3
  7. package/dst-stage1/mqtt-plus-codec.js +20 -19
  8. package/dst-stage1/mqtt-plus-encode.d.ts +1 -0
  9. package/dst-stage1/mqtt-plus-encode.js +3 -1
  10. package/dst-stage1/mqtt-plus-event.js +27 -20
  11. package/dst-stage1/mqtt-plus-meta.d.ts +1 -1
  12. package/dst-stage1/mqtt-plus-meta.js +2 -0
  13. package/dst-stage1/mqtt-plus-msg.js +110 -88
  14. package/dst-stage1/mqtt-plus-service.d.ts +4 -4
  15. package/dst-stage1/mqtt-plus-service.js +46 -42
  16. package/dst-stage1/mqtt-plus-sink.d.ts +3 -0
  17. package/dst-stage1/mqtt-plus-sink.js +80 -33
  18. package/dst-stage1/mqtt-plus-source.d.ts +6 -6
  19. package/dst-stage1/mqtt-plus-source.js +109 -42
  20. package/dst-stage1/mqtt-plus-trace.d.ts +2 -1
  21. package/dst-stage1/mqtt-plus-trace.js +1 -1
  22. package/dst-stage1/mqtt-plus-util.d.ts +4 -3
  23. package/dst-stage1/mqtt-plus-util.js +33 -21
  24. package/dst-stage2/mqtt-plus.cjs.js +428 -259
  25. package/dst-stage2/mqtt-plus.esm.js +425 -259
  26. package/dst-stage2/mqtt-plus.umd.js +12 -12
  27. package/etc/vite.mts +4 -1
  28. package/package.json +9 -6
  29. package/src/mqtt-plus-auth.ts +20 -11
  30. package/src/mqtt-plus-base.ts +3 -1
  31. package/src/mqtt-plus-codec.ts +18 -17
  32. package/src/mqtt-plus-encode.ts +11 -8
  33. package/src/mqtt-plus-event.ts +28 -21
  34. package/src/mqtt-plus-meta.ts +6 -4
  35. package/src/mqtt-plus-msg.ts +124 -92
  36. package/src/mqtt-plus-service.ts +56 -49
  37. package/src/mqtt-plus-sink.ts +83 -36
  38. package/src/mqtt-plus-source.ts +118 -49
  39. package/src/mqtt-plus-trace.ts +1 -1
  40. package/src/mqtt-plus-util.ts +38 -29
  41. package/tst/mqtt-plus-mosquitto.ts +1 -1
  42. package/tst/mqtt-plus.spec.ts +8 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
  ChangeLog
3
3
  =========
4
4
 
5
+ 1.2.0 (2026-02-06)
6
+ ------------------
7
+
8
+ - IMPROVEMENT: use Valibot for more robust object validation
9
+ - IMPROVEMENT: support concurrent operations
10
+ - IMPROVEMENT: improve chunk sending
11
+ - IMPROVEMENT: improve buffer handling and decoding
12
+ - IMPROVEMENT: use derived keys to have enough entropy
13
+ - IMPROVEMENT: report failures and log errors on dispatching messages
14
+ - IMPROVEMENT: await subscribes and super calls in dispatching messages
15
+ - IMPROVEMENT: log failing destroy operations
16
+ - BUGFIX: fix share option handling in event()
17
+ - BUGFIX: fix error handling
18
+ - BUGFIX: fix return values of promises
19
+ - CLEANUP: improve type safety (use unknown type, remove Awaited type)
20
+ - CLEANUP: simplify code by using closures and conversions
21
+ - CLEANUP: various code cleanups (rename variables, reduce whitespaces)
22
+ - UPDATE: upgrade NPM dependencies
23
+
24
+ 1.1.4 (2026-02-02)
25
+ ------------------
26
+
27
+ - CLEANUP: various cleanups
28
+
5
29
  1.1.3 (2026-02-02)
6
30
  ------------------
7
31
 
package/README.md CHANGED
@@ -238,6 +238,40 @@ The **MQTT+** API provides the following functionalities:
238
238
  Call this method when the instance is no longer needed.
239
239
  The companion MQTT.js instance has to be destroyed separately.
240
240
 
241
+ - **Event Handling**:<br/>
242
+
243
+ /* listen for error or log events */
244
+ on(event: "error", callback: (error: Error) => void): void
245
+ on(event: "log", callback: (log: LogEvent) => void): void
246
+
247
+ /* remove error or log event listener */
248
+ off(event: "error", callback: (error: Error) => void): void
249
+ off(event: "log", callback: (log: LogEvent) => void): void
250
+
251
+ MQTT+ emits `error` and `log` events for monitoring and debugging.
252
+
253
+ - The `on()` method registers an event listener.
254
+ The `"error"` event is emitted when an error occurs during
255
+ message processing, subscription, or publishing.
256
+ The `"log"` event is emitted for informational and debug-level
257
+ messages with a `LogEvent` object containing `timestamp`, `level`,
258
+ `msg`, and optional `data` fields.
259
+
260
+ - The `off()` method removes a previously registered event listener.
261
+
262
+ - The `LogEvent` object provides `resolve()` for resolving lazy
263
+ promise-based fields and `toString()` for rendering log entries
264
+ as formatted strings.
265
+
266
+ Example:
267
+
268
+ mqttp.on("error", (err) => {
269
+ console.error("MQTT+ error:", err.message)
270
+ })
271
+ mqttp.on("log", (log) => {
272
+ console.log(log.toString())
273
+ })
274
+
241
275
  - **Authentication**:<br/>
242
276
 
243
277
  /* store server-side secret credential */
@@ -282,8 +316,11 @@ The **MQTT+** API provides the following functionalities:
282
316
  /* set meta information by key */
283
317
  meta(key: string, value: any): void
284
318
 
319
+ /* retrieve meta information by key */
320
+ meta(key: string): any
321
+
285
322
  /* delete meta information by key */
286
- meta(key: string): void
323
+ meta(key: string, value: null): void
287
324
 
288
325
  MQTT+ allows attaching persistent meta-data to an instance that is
289
326
  automatically included in all outgoing messages. This is useful for
@@ -291,8 +328,9 @@ The **MQTT+** API provides the following functionalities:
291
328
  identity to every request.
292
329
 
293
330
  - The `meta()` method manages instance-level meta-data:
294
- called with a key only, deletes the meta-data entry for that key;
295
- called with a key and value, sets the meta-data entry.
331
+ called with a key only, retrieves the meta-data entry for that key;
332
+ called with a key and non-null value, sets the meta-data entry;
333
+ called with a key and `null`, deletes the meta-data entry.
296
334
 
297
335
  - Instance-level meta-data set via `meta()` is merged with any per-request
298
336
  `meta` option passed to `emit()`, `call()`, `push()`, or `fetch()`.
@@ -309,8 +347,11 @@ The **MQTT+** API provides the following functionalities:
309
347
  mqttp.meta("clientVersion", "1.0.0")
310
348
  mqttp.meta("environment", "production")
311
349
 
350
+ /* client: retrieve a metadata entry */
351
+ const environment = mqttp.meta("environment")
352
+
312
353
  /* client: delete a metadata entry */
313
- mqttp.meta("environment")
354
+ mqttp.meta("environment", null)
314
355
 
315
356
  /* client: per-request metadata (merged with instance-level) */
316
357
  mqttp.call({ name: "example/hello", params: [ "world" ], meta: { requestId: "123" } })
@@ -590,14 +631,14 @@ The **MQTT+** API provides the following functionalities:
590
631
  event: string,
591
632
  params: any[],
592
633
  receiver?: string,
593
- options?: MQTT::IClientSubscribeOptions,
634
+ options?: MQTT::IClientPublishOptions,
594
635
  meta?: Record<string, any>
595
636
  }): void
596
637
  emit({
597
638
  event: string,
598
639
  params: any[],
599
640
  receiver?: string,
600
- options?: MQTT::IClientSubscribeOptions,
641
+ options?: MQTT::IClientPublishOptions,
601
642
  meta?: Record<string, any>,
602
643
  dry: true
603
644
  }): { topic: string, payload: string | Uint8Array, options: IClientPublishOptions }
@@ -695,7 +736,7 @@ The **MQTT+** API provides the following functionalities:
695
736
  name: string,
696
737
  params: any[],
697
738
  receiver?: string,
698
- options?: MQTT::IClientSubscribeOptions,
739
+ options?: MQTT::IClientPublishOptions,
699
740
  meta?: Record<string, any>
700
741
  }): Promise<{
701
742
  stream: Readable,
@@ -974,7 +1015,7 @@ pattern read example/client/+/sink-push-request/%c
974
1015
  pattern write example/client/+/sink-push-response/%c
975
1016
  pattern read example/client/+/sink-push-chunk/%c
976
1017
 
977
- # ==== server/autenticated ACL ====
1018
+ # ==== server/authenticated ACL ====
978
1019
 
979
1020
  user example
980
1021
 
@@ -6,7 +6,7 @@ export type AuthOption = AuthRole | {
6
6
  mode: AuthMode;
7
7
  roles: AuthRole[];
8
8
  };
9
- export type TokenPayload = {
9
+ type TokenPayload = {
10
10
  roles: AuthRole[];
11
11
  id?: string;
12
12
  };
@@ -21,3 +21,4 @@ export declare class AuthTrait<T extends APISchema = APISchema> extends MetaTrai
21
21
  private validateToken;
22
22
  protected authenticated(clientId: string | undefined, tokens: string[] | undefined, option: AuthOption): Promise<boolean>;
23
23
  }
24
+ export {};
@@ -24,6 +24,8 @@
24
24
  /* external requirements */
25
25
  import { SignJWT } from "jose/jwt/sign";
26
26
  import { jwtVerify } from "jose/jwt/verify";
27
+ import * as pbkdf2 from "@stablelib/pbkdf2";
28
+ import * as sha256 from "@stablelib/sha256";
27
29
  import { MetaTrait } from "./mqtt-plus-meta";
28
30
  /* authentication trait */
29
31
  export class AuthTrait extends MetaTrait {
@@ -35,7 +37,13 @@ export class AuthTrait extends MetaTrait {
35
37
  }
36
38
  /* store server-side secret credential */
37
39
  credential(credential) {
38
- this._credential = credential;
40
+ /* sanity check argument */
41
+ if (credential.length === 0)
42
+ throw new Error("credential must not be empty");
43
+ /* use a derived key with minimum length of 32 for JWT HS256 */
44
+ const pw = new TextEncoder().encode(credential);
45
+ const st = new TextEncoder().encode("mqtt-plus");
46
+ this._credential = pbkdf2.deriveKey(sha256.SHA256, pw, st, 100000, 32);
39
47
  }
40
48
  /* issue client-side token on server-side */
41
49
  async issue(payload) {
@@ -43,8 +51,7 @@ export class AuthTrait extends MetaTrait {
43
51
  throw new Error("credential has to be provided before issuing tokens");
44
52
  const jwt = new SignJWT(payload);
45
53
  jwt.setProtectedHeader({ alg: "HS256", typ: "JWT" });
46
- const key = new TextEncoder().encode(this._credential);
47
- const token = await jwt.sign(key);
54
+ const token = await jwt.sign(this._credential);
48
55
  return token;
49
56
  }
50
57
  authenticate(token, remove) {
@@ -59,8 +66,7 @@ export class AuthTrait extends MetaTrait {
59
66
  async validateToken(token) {
60
67
  if (this._credential === null)
61
68
  throw new Error("credential has to be provided before validating tokens");
62
- const key = new TextEncoder().encode(this._credential);
63
- const result = await jwtVerify(token, key).catch(() => null);
69
+ const result = await jwtVerify(token, this._credential).catch(() => null);
64
70
  return result?.payload ?? null;
65
71
  }
66
72
  /* check whether request is authenticated */
@@ -77,9 +83,11 @@ export class AuthTrait extends MetaTrait {
77
83
  mode = option.mode;
78
84
  roles = option.roles;
79
85
  }
80
- /* iterate over all roles and try to authenticate token (first-match) */
86
+ /* iterate over all roles and try to authenticate token (first-match, max 8) */
81
87
  if (tokens !== undefined) {
82
- for (const token of tokens) {
88
+ for (const token of tokens.slice(0, 8)) {
89
+ if (token.length > 8192)
90
+ continue;
83
91
  const payload = await this.validateToken(token);
84
92
  if (payload === null)
85
93
  continue;
@@ -154,7 +154,9 @@ export class BaseTrait extends TraceTrait {
154
154
  }
155
155
  this.log("debug", `received from MQTT topic "${topic}"`, { message: parsed });
156
156
  /* dispatch to trait handlers */
157
- this._dispatchMessage(topic, parsed).catch(() => { });
157
+ this._dispatchMessage(topic, parsed).catch((err) => {
158
+ this.error(err, `dispatching message from MQTT topic "${topic}" failed`);
159
+ });
158
160
  }
159
161
  /* dispatch parsed message to appropriate handler
160
162
  (base implementation, to be overridden in sub-traits) */
@@ -3,10 +3,10 @@ import { APIOptions, OptionsTrait } from "./mqtt-plus-options";
3
3
  export declare class JSONX {
4
4
  private static uint8ArrayToBase64;
5
5
  private static base64ToUint8Array;
6
- static stringify(obj: any): string;
7
- static parse(json: string): any;
6
+ static stringify(obj: unknown): string;
7
+ static parse(json: string): unknown;
8
8
  }
9
- export default class Codec {
9
+ declare class Codec {
10
10
  private type;
11
11
  private types;
12
12
  private tags;
@@ -18,3 +18,4 @@ export declare class CodecTrait<T extends APISchema = APISchema> extends Options
18
18
  protected codec: Codec;
19
19
  constructor(options?: Partial<APIOptions>);
20
20
  }
21
+ export {};
@@ -22,19 +22,16 @@
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
24
  /* external requirements */
25
+ import { Buffer } from "node:buffer";
25
26
  import * as CBOR from "cbor2";
26
27
  import { OptionsTrait } from "./mqtt-plus-options";
27
28
  /* JSON encode/decode with Uint8Array support */
28
29
  export class JSONX {
29
30
  static uint8ArrayToBase64(arr) {
30
- return btoa(String.fromCharCode(...arr));
31
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString("base64");
31
32
  }
32
33
  static base64ToUint8Array(base64) {
33
- const binary = atob(base64);
34
- const arr = new Uint8Array(binary.length);
35
- for (let i = 0; i < binary.length; i++)
36
- arr[i] = binary.charCodeAt(i);
37
- return arr;
34
+ return new Uint8Array(Buffer.from(base64, "base64"));
38
35
  }
39
36
  static stringify(obj) {
40
37
  return JSON.stringify(obj, (_, value) => value instanceof Uint8Array
@@ -48,7 +45,7 @@ export class JSONX {
48
45
  }
49
46
  }
50
47
  /* the encoder/decoder abstraction */
51
- export default class Codec {
48
+ class Codec {
52
49
  constructor(type) {
53
50
  this.type = type;
54
51
  this.types = new CBOR.TypeEncoderMap();
@@ -68,42 +65,46 @@ export default class Codec {
68
65
  try {
69
66
  result = CBOR.encode(data, { types: this.types });
70
67
  }
71
- catch (_ex) {
72
- throw new Error("failed to encode CBOR format");
68
+ catch (ex) {
69
+ throw new Error("failed to encode CBOR format", { cause: ex });
73
70
  }
74
71
  }
75
72
  else if (this.type === "json") {
76
73
  try {
77
74
  result = JSONX.stringify(data);
78
75
  }
79
- catch (_ex) {
80
- throw new Error("failed to encode JSON format");
76
+ catch (ex) {
77
+ throw new Error("failed to encode JSON format", { cause: ex });
81
78
  }
82
79
  }
83
80
  else
84
- throw new Error("invalid format");
81
+ throw new Error(`invalid format "${this.type}"`);
85
82
  return result;
86
83
  }
87
84
  decode(data) {
88
85
  let result;
89
- if (this.type === "cbor" && data instanceof Uint8Array) {
86
+ if (this.type === "cbor") {
87
+ if (!(data instanceof Uint8Array))
88
+ throw new Error("failed to decode CBOR format (data type is not Uint8Array)");
90
89
  try {
91
90
  result = CBOR.decode(data, { tags: this.tags });
92
91
  }
93
- catch (_ex) {
94
- throw new Error("failed to decode CBOR format");
92
+ catch (ex) {
93
+ throw new Error("failed to decode CBOR format", { cause: ex });
95
94
  }
96
95
  }
97
- else if (this.type === "json" && typeof data === "string") {
96
+ else if (this.type === "json") {
97
+ if (typeof data !== "string")
98
+ throw new Error("failed to decode JSON format (data type is not string)");
98
99
  try {
99
100
  result = JSONX.parse(data);
100
101
  }
101
- catch (_ex) {
102
- throw new Error("failed to decode JSON format");
102
+ catch (ex) {
103
+ throw new Error("failed to decode JSON format", { cause: ex });
103
104
  }
104
105
  }
105
106
  else
106
- throw new Error("invalid format or wrong data type");
107
+ throw new Error(`invalid format "${this.type}"`);
107
108
  return result;
108
109
  }
109
110
  }
@@ -1,3 +1,4 @@
1
+ import { Buffer } from "node:buffer";
1
2
  import { APISchema } from "./mqtt-plus-api";
2
3
  import { CodecTrait } from "./mqtt-plus-codec";
3
4
  export declare class EncodeTrait<T extends APISchema = APISchema> extends CodecTrait<T> {
@@ -21,6 +21,8 @@
21
21
  ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
22
  ** SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
23
  */
24
+ /* built-in requirements */
25
+ import { Buffer } from "node:buffer";
24
26
  import { CodecTrait } from "./mqtt-plus-codec";
25
27
  /* encoding trait */
26
28
  export class EncodeTrait extends CodecTrait {
@@ -43,7 +45,7 @@ export class EncodeTrait extends CodecTrait {
43
45
  }
44
46
  buf2arr(data, cons) {
45
47
  let arr;
46
- if (cons === Buffer)
48
+ if (typeof Buffer !== "undefined" && cons === Buffer)
47
49
  arr = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
48
50
  else if (cons === Uint8Array)
49
51
  arr = data;
@@ -44,6 +44,7 @@ export class EventTrait extends AuthTrait {
44
44
  name = nameOrConfig.name;
45
45
  callback = nameOrConfig.callback;
46
46
  options = nameOrConfig.options ?? {};
47
+ share = nameOrConfig.share;
47
48
  auth = nameOrConfig.auth;
48
49
  }
49
50
  else {
@@ -73,16 +74,17 @@ export class EventTrait extends AuthTrait {
73
74
  auth
74
75
  });
75
76
  /* provide a registration for subsequent destruction */
76
- const self = this;
77
77
  const registration = {
78
- async destroy() {
79
- if (!self.events.has(name))
78
+ destroy: async () => {
79
+ if (!this.events.has(name))
80
80
  throw new Error(`destroy: event "${name}" not registered`);
81
- self.events.delete(name);
81
+ this.events.delete(name);
82
82
  return Promise.all([
83
- self._unsubscribeTopic(topicB),
84
- self._unsubscribeTopic(topicD)
85
- ]).then(() => { });
83
+ this._unsubscribeTopic(topicB),
84
+ this._unsubscribeTopic(topicD)
85
+ ]).then(() => { }).catch((err) => {
86
+ this.error(err, `destroy: failed to unsubscribe from topics for event "${name}"`);
87
+ });
86
88
  }
87
89
  };
88
90
  return registration;
@@ -110,11 +112,11 @@ export class EventTrait extends AuthTrait {
110
112
  params = args;
111
113
  }
112
114
  /* generate unique request id */
113
- const rid = nanoid();
115
+ const requestId = nanoid();
114
116
  /* generate encoded message */
115
117
  const auth = this.authenticate();
116
118
  const metaStore = this.metaStore(meta);
117
- const request = this.msg.makeEventEmission(rid, event, params, this.options.id, receiver, auth, metaStore);
119
+ const request = this.msg.makeEventEmission(requestId, event, params, this.options.id, receiver, auth, metaStore);
118
120
  const message = this.codec.encode(request);
119
121
  /* generate corresponding MQTT topic */
120
122
  const topic = this.options.topicMake(event, "event-emission", receiver);
@@ -124,17 +126,21 @@ export class EventTrait extends AuthTrait {
124
126
  return { topic, payload: message, options: { qos: 0, ...options } };
125
127
  else
126
128
  /* publish message to MQTT topic */
127
- this._publishToTopic(topic, message, { qos: 0, ...options }).catch(() => { });
129
+ this._publishToTopic(topic, message, { qos: 0, ...options }).catch((err) => {
130
+ this.error(err, `emitting event "${event}" failed`);
131
+ });
128
132
  }
129
133
  /* dispatch message (Event pattern handling) */
130
134
  async _dispatchMessage(topic, parsed) {
131
- super._dispatchMessage(topic, parsed);
135
+ await super._dispatchMessage(topic, parsed);
132
136
  const topicMatch = this.options.topicMatch(topic);
133
137
  if (topicMatch !== null
134
138
  && topicMatch.operation === "event-emission"
135
139
  && parsed instanceof EventEmission) {
136
140
  /* just deliver event */
137
141
  const name = parsed.name;
142
+ if (topicMatch.name !== name)
143
+ throw new Error(`event name mismatch between topic "${topicMatch.name}" and payload "${name}"`);
138
144
  const handler = this.events.get(name);
139
145
  const params = parsed.params ?? [];
140
146
  const info = { sender: parsed.sender ?? "" };
@@ -142,16 +148,17 @@ export class EventTrait extends AuthTrait {
142
148
  info.receiver = parsed.receiver;
143
149
  if (parsed.meta)
144
150
  info.meta = parsed.meta;
145
- if (handler?.auth)
151
+ if (handler === undefined)
152
+ throw new Error(`handler for event "${name}" not found`);
153
+ if (handler.auth)
146
154
  info.authenticated = await this.authenticated(parsed.sender, parsed.auth, handler.auth);
147
- if (info.authenticated !== undefined && !info.authenticated)
148
- this.error(new Error(`authentication on event "${name}" failed`));
149
- else
150
- Promise.resolve()
151
- .then(() => handler?.callback?.(...params, info))
152
- .catch((err) => {
153
- this.error(err);
154
- });
155
+ Promise.resolve().then(() => {
156
+ if (info.authenticated !== undefined && !info.authenticated)
157
+ throw new Error(`authentication on event "${name}" failed`);
158
+ return handler.callback(...params, info);
159
+ }).catch((err) => {
160
+ this.error(err, `handler for event "${name}" failed`);
161
+ });
155
162
  }
156
163
  }
157
164
  }
@@ -3,7 +3,7 @@ import { BaseTrait } from "./mqtt-plus-base";
3
3
  export declare class MetaTrait<T extends APISchema = APISchema> extends BaseTrait<T> {
4
4
  private _meta;
5
5
  meta(): Record<string, any>;
6
- meta(key: string): void;
6
+ meta(key: string): any;
7
7
  meta(key: string, value: any): void;
8
8
  protected metaStore(extra?: Record<string, any>): Record<string, any> | undefined;
9
9
  }
@@ -32,6 +32,8 @@ export class MetaTrait extends BaseTrait {
32
32
  meta(key, value) {
33
33
  if (key === undefined)
34
34
  return Object.fromEntries(this._meta);
35
+ else if (arguments.length === 1)
36
+ return this._meta.get(key);
35
37
  else if (value === undefined || value === null)
36
38
  this._meta.delete(key);
37
39
  else