From 717af632de0b2de2ddb90c3063ca0dc2ac9f4167 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 22 Sep 2026 19:29:38 +0000 Subject: [PATCH] 1.5.1: broker errors as events that never crash the host (error only when listened to; connect/offline/reconnect/close; connected flag), configurable broker host + mqtt options + log hook, Alexa.ChangeReport via Device.getChangeReport (/changeReport), SceneController discovery + sendSceneResponse, addCapability options (proactivelyReported), unregisterDevice/clearDevices/getDevices/disconnect, discover/directive events, promise-returning quiet send(); tests on an in-process broker (aedes); readme Co-Authored-By: Claude Fable 5.1 --- dist/Alex2Node.js | 163 ++-- dist/AlexaErrorResponse.js | 12 +- dist/AlexaInterface.js | 9 +- dist/AlexaStatusMessage.js | 73 +- dist/Device.js | 42 +- dist/index.js | 7 +- package-lock.json | 1491 +++++++++++++++++++++++++++++++++++- package.json | 16 +- readme.md | 37 +- src/Alex2Node.ts | 191 +++-- src/AlexaErrorResponse.ts | 17 +- src/AlexaInterface.ts | 8 +- src/AlexaStatusMessage.ts | 71 +- src/Device.ts | 44 +- src/index.ts | 15 +- test/alex2node.test.js | 109 +++ 16 files changed, 2080 insertions(+), 225 deletions(-) create mode 100644 test/alex2node.test.js diff --git a/dist/Alex2Node.js b/dist/Alex2Node.js index f32cff7..883b3d9 100644 --- a/dist/Alex2Node.js +++ b/dist/Alex2Node.js @@ -3,12 +3,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_HOST = void 0; const mqtt_1 = __importDefault(require("mqtt")); const Device_1 = __importDefault(require("./Device")); const events_1 = require("events"); const DisplayCategory_1 = require("./DisplayCategory"); +exports.DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883"; +/** + * The Alexa-to-MQTT bridge: one broker connection for a user's root topic, a set of registered devices, discovery and + * directive dispatch. + * + * Events (all optional to listen to - since 1.5.1 a broker outage never throws out of the library): + * "connect" connected (or reconnected) and subscribed to /# + * "offline" the connection dropped; mqtt.js reconnects on its own (reconnectPeriod, default 1 s) + * "reconnect" a reconnect attempt starts + * "close" the connection closed + * "error" (err) a connection or publish error. Emitted ONLY when a listener is attached (1.4.0 emitted it + * unconditionally, and Node kills a process that has an unhandled "error" event) + * "discover" (n) a discovery request was answered with n devices + * "directive" (info) a directive was dispatched to a device: { endpointId, namespace, name } + */ class Alex2MQTT extends events_1.EventEmitter { - constructor(username, password, rootTopic, debugLogging) { + constructor(username, password, rootTopic, debugLogging = false, options = {}) { super(); // Initialize EventEmitter this.username = username; this.password = password; @@ -16,62 +32,73 @@ class Alex2MQTT extends events_1.EventEmitter { this.debugLogging = debugLogging; this.client = null; this.devices = []; - this.MqttHost = "mqtt://Alex2MQTT.stormysdream.club"; + /** true while the broker connection is up. */ + this.connected = false; + /** ISO time of the last discovery request answered, null before the first. */ + this.lastDiscoveryAt = null; + this.options = options || {}; + this.MqttHost = this.options.host || exports.DEFAULT_HOST; + } + log(message, detail) { + if (this.options.log) + this.options.log(message, detail); + else if (this.debugLogging) { + if (detail === undefined) + console.log(`[Alex2Node.ts] ${message}`); + else + console.log(`[Alex2Node.ts] ${message}`, detail); + } + } + /** Emit "error" only when somebody listens: an unhandled "error" event would crash the host process. */ + fail(err) { + this.log("error: " + err.message); + if (this.listenerCount("error") > 0) + this.emit("error", err); } connect() { - const options = { - username: this.username, - password: this.password, - }; + if (this.client) + return; + const options = Object.assign({ username: this.username, password: this.password, reconnectPeriod: 1000, connectTimeout: 10000 }, (this.options.mqtt || {})); this.client = mqtt_1.default.connect(this.MqttHost, options); this.client.on("connect", () => { - if (this.debugLogging) { - console.log("[Alex2Node.ts] Connected to MQTT broker"); - } + this.connected = true; + this.log("Connected to MQTT broker"); this.client.subscribe(this.rootTopic + "/#", (err) => { - if (this.debugLogging && !err) { - console.log("[Alex2Node.ts] Subscribed to topics"); - } if (err) { - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); + this.fail(err); + return; } + this.log("Subscribed to topics"); + this.emit("connect"); }); }); + this.client.on("offline", () => { this.connected = false; this.log("offline"); this.emit("offline"); }); + this.client.on("reconnect", () => { this.log("reconnecting"); this.emit("reconnect"); }); + this.client.on("close", () => { this.connected = false; this.emit("close"); }); + this.client.on("error", (err) => { this.connected = false; this.fail(err); }); this.client.on("message", (topic, message) => { - if (this.debugLogging) { - console.log(`[Alex2Node.ts] MQTT Message Received`); - console.log(` Topic: ${topic}`); - console.log(` Payload: ${message.toString()}`); - } + this.log(`MQTT Message Received`, { topic, payload: message.toString() }); if (topic == `${this.rootTopic}/discover`) { - if (this.debugLogging) { - console.log("[Alex2Node.ts] Discovery request received, getting device json..."); - } + this.log("Discovery request received, getting device json..."); const deviceArray = this.devices.map((device) => device.getJSON()); - if (deviceArray.length > 0) { - this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => { - if (err) { - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); - } - else if (this.debugLogging) { - console.log(`[Alex2Node.ts] Discovery payloads published to ${topic + "_r"}`); - console.log(JSON.stringify(deviceArray, null, 2)); - } - }); - } + this.lastDiscoveryAt = new Date().toISOString(); + this.client.publish(topic + "_r", JSON.stringify(deviceArray), (err) => { + if (err) { + this.fail(err); + return; + } + this.log(`Discovery payloads published to ${topic + "_r"}`, deviceArray); + this.emit("discover", deviceArray.length); + }); } else if (topic.split("/").length == 3) { - const [root, endpointId, directiveType] = topic.split("/"); + const [, endpointId, directiveType] = topic.split("/"); if (directiveType != "alexaDirective") { return; } const device = this.devices.find((d) => d.endpointId === endpointId); if (!device) { - if (this.debugLogging) { - console.warn(`[Alex2Node.ts] No device found for endpointId: ${endpointId}`); - } + this.log(`No device found for endpointId: ${endpointId}`); return; } let payload; @@ -79,15 +106,14 @@ class Alex2MQTT extends events_1.EventEmitter { payload = JSON.parse(message.toString()); } catch (e) { - console.error(`[Alex2Node.ts] Failed to parse payload JSON`, e); + this.log(`Failed to parse payload JSON`, e); return; } - if (payload.header && - payload.header.namespace === "Alexa" && - payload.header.name === "ReportState") { - if (this.debugLogging) { - console.log(`[Alex2Node.ts] ReportState directive for device ${endpointId}`); - } + if (!payload || !payload.header) + return; + this.emit("directive", { endpointId, namespace: payload.header.namespace, name: payload.header.name }); + if (payload.header.namespace === "Alexa" && payload.header.name === "ReportState") { + this.log(`ReportState directive for device ${endpointId}`); device.emit("ReportState", payload); } else { @@ -95,21 +121,23 @@ class Alex2MQTT extends events_1.EventEmitter { } } }); - this.client.on("error", (err) => { - if (this.debugLogging) { - console.error("[Alex2Node.ts] Connection failed:", err); - } - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); + } + /** Close the broker connection (resolves once closed). The devices stay registered; connect() again reuses them. */ + disconnect() { + return new Promise((resolve) => { + const c = this.client; + if (!c) + return resolve(); + this.client = null; + this.connected = false; + c.end(true, {}, () => resolve()); }); } registerDevice(name, endpointId, displayCategory) { if (!this.client) { throw new Error("Must call connect before creating devices"); } - if (this.debugLogging) { - console.log(`[Alex2Node.ts] Creating new device with endpoint: ${endpointId}`); - } + this.log(`Creating new device with endpoint: ${endpointId}`); const normalizedCategory = displayCategory === null ? [DisplayCategory_1.DisplayCategory.LIGHT] : Array.isArray(displayCategory) @@ -119,5 +147,32 @@ class Alex2MQTT extends events_1.EventEmitter { this.devices.push(device); return device; } + /** Forget a device (its listeners with it). Returns false when there was none. */ + unregisterDevice(endpointId) { + const i = this.devices.findIndex((d) => d.endpointId === endpointId); + if (i === -1) + return false; + this.devices[i].removeAllListeners(); + this.devices.splice(i, 1); + return true; + } + /** Forget every device. */ + clearDevices() { + for (const d of this.devices) + d.removeAllListeners(); + this.devices = []; + } + getDevices() { + return this.devices.slice(); + } + getDevice(endpointId) { + return this.devices.find((d) => d.endpointId === endpointId); + } + getRootTopic() { + return this.rootTopic; + } + getHost() { + return this.MqttHost; + } } exports.default = Alex2MQTT; diff --git a/dist/AlexaErrorResponse.js b/dist/AlexaErrorResponse.js index 40577f5..5c31483 100644 --- a/dist/AlexaErrorResponse.js +++ b/dist/AlexaErrorResponse.js @@ -106,19 +106,17 @@ class AlexaErrorResponse { this.event.payload = Object.assign({ type, message }, otherParams); } + toJSON() { + return { event: this.event }; + } send(sendAsync = false) { const payload = { event: this.event, }; const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too const payloadStr = JSON.stringify(payload); - this.mqttClient.publish(topic, payloadStr, (err) => { - if (err) { - console.error("[AlexaErrorResponse] Failed to publish status message:", err); - } - else { - console.log(`[AlexaErrorResponse] Sent message to ${topic}`); - } + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, payloadStr, (err) => (err ? reject(err) : resolve(topic))); }); } } diff --git a/dist/AlexaInterface.js b/dist/AlexaInterface.js index 0febeab..1b97c71 100644 --- a/dist/AlexaInterface.js +++ b/dist/AlexaInterface.js @@ -244,6 +244,7 @@ class AlexaInterface { case AlexaInterfaceType.REMOTE_VIDEO_PLAYER: case AlexaInterfaceType.RTC_SESSION_CONTROLLER: case AlexaInterfaceType.SCENE_CONTROLLER: + return []; // scenes have no reportable properties: Activate/Deactivate answer with ActivationStarted (Device.sendSceneResponse) case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER: case AlexaInterfaceType.SEEK_CONTROLLER: case AlexaInterfaceType.SIMPLE_EVENT_SOURCE: @@ -276,7 +277,13 @@ class AlexaInterface { supported: this.getProps().map(name => ({ name })), }, }; - if (this.type == AlexaInterfaceType.THERMOSTAT_CONTROLLER) { + if (this.type == AlexaInterfaceType.SCENE_CONTROLLER) { + // Alexa.SceneController v3 carries no properties block: supportsDeactivation + proactivelyReported at the top level + delete doc.properties; + doc.supportsDeactivation = true; + doc.proactivelyReported = this.proactivelyReported; + } + else if (this.type == AlexaInterfaceType.THERMOSTAT_CONTROLLER) { doc.configuration = { "supportedModes": ["HEAT", "COOL", "AUTO", "OFF"], "supportsScheduling": false diff --git a/dist/AlexaStatusMessage.js b/dist/AlexaStatusMessage.js index 64dbeba..38f4bc6 100644 --- a/dist/AlexaStatusMessage.js +++ b/dist/AlexaStatusMessage.js @@ -28,20 +28,29 @@ var TemperatureSensorScale; TemperatureSensorScale["FAHRENHEIT"] = "FAHRENHEIT"; })(TemperatureSensorScale || (exports.TemperatureSensorScale = TemperatureSensorScale = {})); class AlexaStatusMessage { - constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false) { + constructor(correlationToken, rootTopic, endpointId, mqttClient, isResponse = false, isDeferred = false, changeCause = null) { this.context = { properties: [] }; + /** ChangeReport only: the properties that changed (payload.change.properties); the rest go to context. */ + this.changeProps = []; + this.changeCause = null; + this.target = "context"; this.rootTopic = rootTopic; this.endpointId = endpointId; this.mqttClient = mqttClient; this.isDeferred = isDeferred; + this.changeCause = changeCause; + if (changeCause) + this.target = "change"; this.event = { header: { namespace: "Alexa", - name: isDeferred - ? "DeferredResponse" - : isResponse - ? "Response" - : "StateReport", + name: changeCause + ? "ChangeReport" + : isDeferred + ? "DeferredResponse" + : isResponse + ? "Response" + : "StateReport", payloadVersion: "3", messageId: this.generateMessageId(), correlationToken, @@ -68,9 +77,33 @@ class AlexaStatusMessage { }; if (instance) prop.instance = instance; - this.context.properties.push(prop); + (this.target === "change" ? this.changeProps : this.context.properties).push(prop); return this; } + /** ChangeReport: the add*Prop calls that follow describe what CHANGED (the default for a change report). */ + changed() { + this.target = "change"; + return this; + } + /** ChangeReport: the add*Prop calls that follow describe the other, unchanged properties (context). */ + unchanged() { + this.target = "context"; + return this; + } + /** True for a ChangeReport (Device.getChangeReport). */ + isChangeReport() { + return this.changeCause !== null; + } + /** The message as it will be published (for tests and logging). */ + toJSON() { + if (this.changeCause) { + return { + event: Object.assign(Object.assign({}, this.event), { payload: { change: { cause: { type: this.changeCause }, properties: this.changeProps } } }), + context: this.context, + }; + } + return { context: this.isDeferred ? null : this.context, event: this.event }; + } addModeControllerProp(instance, value, uncertaintyInMs = 0) { return this.addProperty(AlexaInterface_1.AlexaInterfaceType.MODE_CONTROLLER, "mode", value, uncertaintyInMs, instance); } @@ -125,20 +158,18 @@ class AlexaStatusMessage { this.context.properties.push(prop); return this; } - send(sendAsync) { - const payload = { - context: this.isDeferred ? null : this.context, - event: this.event, - }; - const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too - const payloadStr = JSON.stringify(payload); - this.mqttClient.publish(topic, payloadStr, (err) => { - if (err) { - console.error("[AlexaStatusMessage] Failed to publish status message:", err); - } - else { - console.log(`[AlexaStatusMessage] Sent message to ${topic}`); - } + /** + * Publish: a Response/StateReport to //alexaResponce (sendAsync: deferredResponse), a ChangeReport + * to /changeReport (Alex2MQTT adds the user's token and posts it to the Alexa event gateway). Resolves with + * the topic; rejects on a publish error (1.4.0 only logged). + */ + send(sendAsync = false) { + const payloadStr = JSON.stringify(this.toJSON()); + const topic = this.changeCause + ? `${this.rootTopic}/changeReport` + : `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, payloadStr, (err) => (err ? reject(err) : resolve(topic))); }); } } diff --git a/dist/Device.js b/dist/Device.js index e2abf6b..b151148 100644 --- a/dist/Device.js +++ b/dist/Device.js @@ -4,6 +4,7 @@ const AlexaInterface_1 = require("./AlexaInterface"); const DisplayCategory_1 = require("./DisplayCategory"); const events_1 = require("events"); const AlexaStatusMessage_1 = require("./AlexaStatusMessage"); +const uuid_1 = require("uuid"); const AlexaErrorResponse_1 = require("./AlexaErrorResponse"); class Device extends events_1.EventEmitter { constructor(mqttClient, rootTopic, name, endpointId, displayCategory, description = "Alexa to Node.js bridge", manufacturerName = "Alex2Node", manufacturer = "Alex2Node", model = "Alex2Node_v1.0.0") { @@ -47,6 +48,35 @@ class Device extends events_1.EventEmitter { getStatusMessage(correlationToken, isResponse = false, isDeferred = false) { return new AlexaStatusMessage_1.AlexaStatusMessage(correlationToken, this.rootTopic, this.endpointId, this.mqttClient, isResponse, isDeferred); } + /** + * A proactive Alexa.ChangeReport (1.5.1): add the changed properties (the default target), optionally + * .unchanged() then the others, and .send() - it goes to /changeReport, which Alex2MQTT forwards to the Alexa + * event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported. + */ + getChangeReport(cause = "PHYSICAL_INTERACTION") { + return new AlexaStatusMessage_1.AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, false, false, cause); + } + /** + * Alexa.SceneController: answer an Activate / Deactivate directive with ActivationStarted / DeactivationStarted + * (1.5.1). Resolves with the topic published to. + */ + sendSceneResponse(correlationToken, activated, cause = "VOICE_INTERACTION", sendAsync = false) { + const payload = { + context: {}, + event: { + header: { namespace: "Alexa.SceneController", name: activated ? "ActivationStarted" : "DeactivationStarted", messageId: (0, uuid_1.v4)(), correlationToken, payloadVersion: "3" }, + endpoint: { endpointId: this.endpointId }, + payload: { cause: { type: cause }, timestamp: new Date().toISOString() }, + }, + }; + const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, JSON.stringify(payload), (err) => (err ? reject(err) : resolve(topic))); + }); + } + getCapabilities() { + return this.capabilities.slice(); + } setManufacturerName(name) { this.manufacturerName = name; } @@ -68,14 +98,10 @@ class Device extends events_1.EventEmitter { getSoftwareVersion() { return this.softwareVersion; } - addCapability(type) { - // const existing = this.capabilities.find( - // (iface) => iface.getType() === type - // ); - // if (existing) { - // return existing; - // } - const newCapability = new AlexaInterface_1.AlexaInterface(type); + /** Add a capability. 1.5.1: options.retrievable / proactivelyReported / instance (a change report needs proactivelyReported). */ + addCapability(type, options = {}) { + var _a, _b, _c; + const newCapability = new AlexaInterface_1.AlexaInterface(type, (_a = options.retrievable) !== null && _a !== void 0 ? _a : true, (_b = options.proactivelyReported) !== null && _b !== void 0 ? _b : false, (_c = options.instance) !== null && _c !== void 0 ? _c : ""); this.capabilities.push(newCapability); // console.log( // `Created new interface with type: ${newCapability.getTypeString()}` diff --git a/dist/index.js b/dist/index.js index 12e62bf..a0dcefa 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3,10 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.Alex2MQTT = void 0; +exports.AlexaErrorResponse = exports.AlexaErrorType = exports.ThermostatMode = exports.TemperatureSensorScale = exports.EndpointHealth = exports.PowerController = exports.AlexaStatusMessage = exports.DisplayCategory = exports.AlexaActions = exports.ActionMapping = exports.AlexaInterfaceType = exports.AlexaInterface = exports.Device = exports.DEFAULT_HOST = exports.Alex2MQTT = void 0; var Alex2Node_1 = require("./Alex2Node"); Object.defineProperty(exports, "Alex2MQTT", { enumerable: true, get: function () { return __importDefault(Alex2Node_1).default; } }); +Object.defineProperty(exports, "DEFAULT_HOST", { enumerable: true, get: function () { return Alex2Node_1.DEFAULT_HOST; } }); +var Device_1 = require("./Device"); +Object.defineProperty(exports, "Device", { enumerable: true, get: function () { return __importDefault(Device_1).default; } }); var AlexaInterface_1 = require("./AlexaInterface"); +Object.defineProperty(exports, "AlexaInterface", { enumerable: true, get: function () { return AlexaInterface_1.AlexaInterface; } }); Object.defineProperty(exports, "AlexaInterfaceType", { enumerable: true, get: function () { return AlexaInterface_1.AlexaInterfaceType; } }); var ActionMapping_1 = require("./ActionMapping"); Object.defineProperty(exports, "ActionMapping", { enumerable: true, get: function () { return ActionMapping_1.ActionMapping; } }); @@ -14,6 +18,7 @@ Object.defineProperty(exports, "AlexaActions", { enumerable: true, get: function var DisplayCategory_1 = require("./DisplayCategory"); Object.defineProperty(exports, "DisplayCategory", { enumerable: true, get: function () { return DisplayCategory_1.DisplayCategory; } }); var AlexaStatusMessage_1 = require("./AlexaStatusMessage"); +Object.defineProperty(exports, "AlexaStatusMessage", { enumerable: true, get: function () { return AlexaStatusMessage_1.AlexaStatusMessage; } }); Object.defineProperty(exports, "PowerController", { enumerable: true, get: function () { return AlexaStatusMessage_1.PowerController; } }); Object.defineProperty(exports, "EndpointHealth", { enumerable: true, get: function () { return AlexaStatusMessage_1.EndpointHealth; } }); Object.defineProperty(exports, "TemperatureSensorScale", { enumerable: true, get: function () { return AlexaStatusMessage_1.TemperatureSensorScale; } }); diff --git a/package-lock.json b/package-lock.json index 85b8e73..9250360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24 +1,336 @@ { "name": "alex2node", - "version": "1.0.0", + "version": "1.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "alex2node", - "version": "1.0.0", - "license": "ISC", + "version": "1.5.1", + "license": "MIT", "dependencies": { - "dotenv": "^16.5.0" + "mqtt": "^5.10.3", + "uuid": "^11.0.3" }, "devDependencies": { + "aedes": "^0.51.3", + "dotenv": "^16.5.0", "typescript": "^5.8.3" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/aedes": { + "version": "0.51.3", + "resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.3.tgz", + "integrity": "sha512-aQfiI9w3RbqnowNCdcGMmCtxBFXN9bhJFcuZm24U5/NU06V3MCl42jWK2GUnu8rOypR2Ahi/aEcgq3w7CMcycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "aedes-packet": "^3.0.0", + "aedes-persistence": "^9.1.2", + "end-of-stream": "^1.4.4", + "fastfall": "^1.5.1", + "fastparallel": "^2.4.1", + "fastseries": "^2.0.0", + "hyperid": "^3.2.0", + "mqemitter": "^6.0.0", + "mqtt-packet": "^9.0.0", + "retimer": "^4.0.0", + "reusify": "^1.0.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/aedes" + } + }, + "node_modules/aedes-packet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz", + "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mqtt-packet": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/aedes-packet/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/aedes-packet/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/aedes-packet/node_modules/mqtt-packet": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz", + "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/aedes-packet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/aedes-persistence": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz", + "integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aedes-packet": "^3.0.0", + "qlobber": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/aedes/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, "engines": { "node": ">=12" }, @@ -26,6 +338,457 @@ "url": "https://dotenvx.com" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "node_modules/fastseries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz", + "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/hyperid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz", + "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + } + }, + "node_modules/hyperid/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/hyperid/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mqemitter": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.2.tgz", + "integrity": "sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==", + "dev": true, + "license": "ISC", + "dependencies": { + "fastparallel": "^2.4.1", + "qlobber": "^8.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mqemitter/node_modules/qlobber": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz", + "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/mqtt": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.16.0.tgz", + "integrity": "sha512-G0VsEtuJz5sJUgZJaaADMVfW7SIFPzkIuHR+d6rk+tBeSwdCfd9BaIRycRfd2AbyFDEmLlFM+SDQeti/+IbOnw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/qlobber": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz", + "integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/retimer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz", + "integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "worker-timers": "^7.0.75" + } + }, + "node_modules/retimer/node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/retimer/node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/retimer/node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/retimer/node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -38,19 +801,737 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.34", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.34.tgz", + "integrity": "sha512-WXL+Dqsm0G6dnC66rQsvM3tPT2adhbqSirWUCZGglkALOr8ocS0KlpU0iuKbjflJOlu3ydZikMEVrYnHOM9suw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } } }, "dependencies": { + "@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==" + }, + "@types/node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", + "requires": { + "undici-types": "~8.9.0" + } + }, + "@types/readable-stream": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", + "integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "aedes": { + "version": "0.51.3", + "resolved": "https://registry.npmjs.org/aedes/-/aedes-0.51.3.tgz", + "integrity": "sha512-aQfiI9w3RbqnowNCdcGMmCtxBFXN9bhJFcuZm24U5/NU06V3MCl42jWK2GUnu8rOypR2Ahi/aEcgq3w7CMcycg==", + "dev": true, + "requires": { + "aedes-packet": "^3.0.0", + "aedes-persistence": "^9.1.2", + "end-of-stream": "^1.4.4", + "fastfall": "^1.5.1", + "fastparallel": "^2.4.1", + "fastseries": "^2.0.0", + "hyperid": "^3.2.0", + "mqemitter": "^6.0.0", + "mqtt-packet": "^9.0.0", + "retimer": "^4.0.0", + "reusify": "^1.0.4", + "uuid": "^10.0.0" + }, + "dependencies": { + "uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true + } + } + }, + "aedes-packet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz", + "integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==", + "dev": true, + "requires": { + "mqtt-packet": "^7.0.0" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "mqtt-packet": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz", + "integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==", + "dev": true, + "requires": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "aedes-persistence": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-9.1.2.tgz", + "integrity": "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ==", + "dev": true, + "requires": { + "aedes-packet": "^3.0.0", + "qlobber": "^7.0.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "requires": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "broker-factory": { + "version": "3.1.15", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz", + "integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==", + "requires": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "requires": { + "ms": "^2.1.3" + } + }, "dotenv": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==" + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "requires": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + } + }, + "fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "dev": true, + "requires": { + "reusify": "^1.0.0" + } + }, + "fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "dev": true, + "requires": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, + "fastseries": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-2.0.0.tgz", + "integrity": "sha512-XBU9RXeoYc2/VnvMhplAxEmZLfIk7cvTBu+xwoBuTI8pL19E03cmca17QQycKIdxgwCeFA/a4u27gv1h3ya5LQ==", + "dev": true + }, + "help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" + }, + "hyperid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hyperid/-/hyperid-3.3.0.tgz", + "integrity": "sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "uuid": "^8.3.2", + "uuid-parse": "^1.1.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + } + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-address": { + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", + "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==" + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mqemitter": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-6.0.2.tgz", + "integrity": "sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==", + "dev": true, + "requires": { + "fastparallel": "^2.4.1", + "qlobber": "^8.0.1" + }, + "dependencies": { + "qlobber": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz", + "integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==", + "dev": true + } + } + }, + "mqtt": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.16.0.tgz", + "integrity": "sha512-G0VsEtuJz5sJUgZJaaADMVfW7SIFPzkIuHR+d6rk+tBeSwdCfd9BaIRycRfd2AbyFDEmLlFM+SDQeti/+IbOnw==", + "requires": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + } + }, + "mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "requires": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "qlobber": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/qlobber/-/qlobber-7.0.1.tgz", + "integrity": "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg==", + "dev": true + }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "retimer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/retimer/-/retimer-4.0.0.tgz", + "integrity": "sha512-fZIVtvbOsQsxNSDhpdPOX4lx5Ss2ni+S72AUBitARpFhtA3UzrAjQ6gDtypB2/+l7L+1VQgAgpvAKY66mElH0w==", + "dev": true, + "requires": { + "worker-timers": "^7.0.75" + }, + "dependencies": { + "fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + } + }, + "worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + } + } + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "requires": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + } + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true + }, + "undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==" + }, + "uuid-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uuid-parse/-/uuid-parse-1.1.0.tgz", + "integrity": "sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A==", + "dev": true + }, + "worker-factory": { + "version": "7.0.50", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz", + "integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==", + "requires": { + "@babel/runtime": "^7.29.7", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "worker-timers": { + "version": "8.0.34", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.34.tgz", + "integrity": "sha512-WXL+Dqsm0G6dnC66rQsvM3tPT2adhbqSirWUCZGglkALOr8ocS0KlpU0iuKbjflJOlu3ydZikMEVrYnHOM9suw==", + "requires": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.18", + "worker-timers-worker": "^9.0.15" + } + }, + "worker-timers-broker": { + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz", + "integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==", + "requires": { + "@babel/runtime": "^7.29.7", + "broker-factory": "^3.1.15", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.15" + } + }, + "worker-timers-worker": { + "version": "9.0.15", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz", + "integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==", + "requires": { + "@babel/runtime": "^7.29.7", + "tslib": "^2.8.1", + "worker-factory": "^7.0.50" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "requires": {} + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true } } } diff --git a/package.json b/package.json index 9706fa3..3003ac9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "alex2node", - "version": "1.5.0", + "version": "1.5.1", "description": "A Node.js library for creating Alexa-compatible devices using MQTT, based on Alex2MQTT.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -12,7 +12,8 @@ "type": "commonjs", "scripts": { "build": "tsc", - "prepare": "npm run build" + "prepare": "npm run build", + "test": "node --test test/*.test.js" }, "keywords": [ "alexa", @@ -34,11 +35,16 @@ }, "homepage": "https://git.stormysdream.club/apps/Alex2Node#readme", "devDependencies": { - "typescript": "^5.8.3", - "dotenv": "^16.5.0" + "aedes": "^0.51.3", + "dotenv": "^16.5.0", + "typescript": "^5.8.3" }, "dependencies": { "mqtt": "^5.10.3", "uuid": "^11.0.3" - } + }, + "files": [ + "dist", + "readme.md" + ] } diff --git a/readme.md b/readme.md index ba801fc..2303e8f 100644 --- a/readme.md +++ b/readme.md @@ -13,6 +13,41 @@ For more details on how to configure Alex2MQTT, visit [Alex2MQTT Documentation]( - Easily configure devices and their capabilities via simple API. +## What is new in 1.5.1 + +- **A broker outage no longer crashes your process.** 1.4.0 emitted `error` unconditionally, and Node terminates a + process that has an unhandled `error` event, so every reconnect failure killed the host. 1.5.1 emits `error` only + when you listen for it, keeps reconnecting on its own (mqtt.js, 1 s), and reports `connect`, `offline`, `reconnect` + and `close`. `bridge.connected` says where you stand. +- **Configurable broker.** `new Alex2MQTT(user, pass, rootTopic, debug, { host: "mqtt://broker:1883", mqtt: { ... } })` + (the default is still the public Alex2MQTT broker). `options.mqtt` is merged over the mqtt.js client options; + `options.log` replaces the console output. +- **Proactive state (ChangeReport).** `device.getChangeReport(cause)` builds an `Alexa.ChangeReport`: the + `add*Prop` calls describe what changed, `.unchanged()` switches the following calls to the context, `.send()` + publishes it to `/changeReport`, which Alex2MQTT forwards to the Alexa event gateway with your account's + token. Register the capability with `{ proactivelyReported: true }` so Alexa accepts it. +- **Scenes.** `Alexa.SceneController` discovers correctly (`supportsDeactivation`), and + `device.sendSceneResponse(correlationToken, activated)` answers Activate / Deactivate with ActivationStarted / + DeactivationStarted. +- **Device management.** `unregisterDevice(endpointId)`, `clearDevices()`, `getDevices()`, `getDevice(id)`, + `disconnect()`; `discover` (device count) and `directive` ({ endpointId, namespace, name }) events; + `lastDiscoveryAt`. `addCapability(type, { retrievable, proactivelyReported, instance })`. +- `send()` on every message returns a promise (resolves with the topic) and no longer logs to the console. +- Tests: `npm test` runs the library against an in-process MQTT broker (aedes, a dev dependency). + +```js +const { Alex2MQTT, AlexaInterfaceType, DisplayCategory, PowerController } = require("alex2node"); +const bridge = new Alex2MQTT(user, pass, rootTopic, false, { host: process.env.MQTT_URL }); +bridge.on("error", (e) => console.warn("broker:", e.message)); // optional - without it the error is swallowed, never thrown +bridge.on("connect", () => console.log("connected")); +bridge.connect(); +const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT); +lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true }); +lamp.on("Event", (directive) => { /* ... act, then: */ lamp.getStatusMessage(directive.header.correlationToken, true).addPowerControllerProp(PowerController.ON).send(); }); +// later, when the light changes locally: +lamp.getChangeReport("PHYSICAL_INTERACTION").addPowerControllerProp(PowerController.OFF).send(); +``` + ## Quick Start ### Installation @@ -54,7 +89,7 @@ const username = process.env.MQTT_USERNAME; const password = process.env.MQTT_PASSWORD; const rootTopic = process.env.MQTT_ROOT_TOPIC; -const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); +const alex2NodeClient = new Alex2MQTT(username, password, rootTopic, false); // 1.5.1: add { host } as a fifth argument for another broker alex2NodeClient.connect(); // Connect to the MQTT broker ``` diff --git a/src/Alex2Node.ts b/src/Alex2Node.ts index dc3b821..60c5125 100644 --- a/src/Alex2Node.ts +++ b/src/Alex2Node.ts @@ -1,122 +1,143 @@ -import mqtt, { MqttClient } from "mqtt"; +import mqtt, { MqttClient, IClientOptions } from "mqtt"; import Device from "./Device"; import { EventEmitter } from "events"; import { DisplayCategory } from "./DisplayCategory"; +/** Optional settings for the bridge (1.5.1). Everything has the 1.4.0 behaviour as its default. */ +export interface Alex2MQTTOptions { + /** The broker URL. Default: the public Alex2MQTT broker, mqtt://Alex2MQTT.stormysdream.club:1883. */ + host?: string; + /** Extra mqtt.js client options (reconnectPeriod, connectTimeout, clientId, ...). Merged over the defaults. */ + mqtt?: IClientOptions; + /** Where log lines go. Default: console (only when debugLogging is on). */ + log?: (message: string, detail?: unknown) => void; +} + +export const DEFAULT_HOST = "mqtt://Alex2MQTT.stormysdream.club:1883"; + +/** + * The Alexa-to-MQTT bridge: one broker connection for a user's root topic, a set of registered devices, discovery and + * directive dispatch. + * + * Events (all optional to listen to - since 1.5.1 a broker outage never throws out of the library): + * "connect" connected (or reconnected) and subscribed to /# + * "offline" the connection dropped; mqtt.js reconnects on its own (reconnectPeriod, default 1 s) + * "reconnect" a reconnect attempt starts + * "close" the connection closed + * "error" (err) a connection or publish error. Emitted ONLY when a listener is attached (1.4.0 emitted it + * unconditionally, and Node kills a process that has an unhandled "error" event) + * "discover" (n) a discovery request was answered with n devices + * "directive" (info) a directive was dispatched to a device: { endpointId, namespace, name } + */ class Alex2MQTT extends EventEmitter { private client: MqttClient | null = null; private devices: Device[] = []; - private MqttHost = "mqtt://Alex2MQTT.stormysdream.club"; + private MqttHost: string; + private options: Alex2MQTTOptions; + /** true while the broker connection is up. */ + public connected = false; + /** ISO time of the last discovery request answered, null before the first. */ + public lastDiscoveryAt: string | null = null; constructor( private username: string, private password: string, private rootTopic: string, - private debugLogging: boolean + private debugLogging: boolean = false, + options: Alex2MQTTOptions = {} ) { super(); // Initialize EventEmitter + this.options = options || {}; + this.MqttHost = this.options.host || DEFAULT_HOST; + } + + private log(message: string, detail?: unknown): void { + if (this.options.log) this.options.log(message, detail); + else if (this.debugLogging) { + if (detail === undefined) console.log(`[Alex2Node.ts] ${message}`); + else console.log(`[Alex2Node.ts] ${message}`, detail); + } + } + /** Emit "error" only when somebody listens: an unhandled "error" event would crash the host process. */ + private fail(err: Error): void { + this.log("error: " + err.message); + if (this.listenerCount("error") > 0) this.emit("error", err); } connect(): void { - const options = { + if (this.client) return; + const options: IClientOptions = { username: this.username, password: this.password, + reconnectPeriod: 1000, + connectTimeout: 10000, + ...(this.options.mqtt || {}), }; this.client = mqtt.connect(this.MqttHost, options); this.client.on("connect", () => { - if (this.debugLogging) { - console.log("[Alex2Node.ts] Connected to MQTT broker"); - } - + this.connected = true; + this.log("Connected to MQTT broker"); this.client!.subscribe(this.rootTopic + "/#", (err) => { - if (this.debugLogging && !err) { - console.log("[Alex2Node.ts] Subscribed to topics"); - } - if (err) { - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); - } + if (err) { this.fail(err); return; } + this.log("Subscribed to topics"); + this.emit("connect"); }); }); + this.client.on("offline", () => { this.connected = false; this.log("offline"); this.emit("offline"); }); + this.client.on("reconnect", () => { this.log("reconnecting"); this.emit("reconnect"); }); + this.client.on("close", () => { this.connected = false; this.emit("close"); }); + this.client.on("error", (err: Error) => { this.connected = false; this.fail(err); }); this.client.on("message", (topic: string, message: Buffer) => { - if (this.debugLogging) { - console.log(`[Alex2Node.ts] MQTT Message Received`); - console.log(` Topic: ${topic}`); - console.log(` Payload: ${message.toString()}`); - } + this.log(`MQTT Message Received`, { topic, payload: message.toString() }); if (topic == `${this.rootTopic}/discover`) { - if (this.debugLogging) { - console.log( - "[Alex2Node.ts] Discovery request received, getting device json..." - ); - } + this.log("Discovery request received, getting device json..."); const deviceArray = this.devices.map((device) => device.getJSON()); - - if (deviceArray.length > 0) { - this.client!.publish( - topic + "_r", - JSON.stringify(deviceArray), - (err) => { - if (err) { - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); - } else if (this.debugLogging) { - console.log( - `[Alex2Node.ts] Discovery payloads published to ${ - topic + "_r" - }` - ); - console.log(JSON.stringify(deviceArray, null, 2)); - } - } - ); - } + this.lastDiscoveryAt = new Date().toISOString(); + this.client!.publish(topic + "_r", JSON.stringify(deviceArray), (err) => { + if (err) { this.fail(err); return; } + this.log(`Discovery payloads published to ${topic + "_r"}`, deviceArray); + this.emit("discover", deviceArray.length); + }); } else if (topic.split("/").length == 3) { - const [root, endpointId, directiveType] = topic.split("/"); + const [, endpointId, directiveType] = topic.split("/"); if (directiveType != "alexaDirective") { return; } const device = this.devices.find((d) => d.endpointId === endpointId); if (!device) { - if (this.debugLogging) { - console.warn( - `[Alex2Node.ts] No device found for endpointId: ${endpointId}` - ); - } + this.log(`No device found for endpointId: ${endpointId}`); return; } let payload; try { payload = JSON.parse(message.toString()); } catch (e) { - console.error(`[Alex2Node.ts] Failed to parse payload JSON`, e); + this.log(`Failed to parse payload JSON`, e); return; } - if ( - payload.header && - payload.header.namespace === "Alexa" && - payload.header.name === "ReportState" - ) { - if (this.debugLogging) { - console.log( - `[Alex2Node.ts] ReportState directive for device ${endpointId}` - ); - } + if (!payload || !payload.header) return; + this.emit("directive", { endpointId, namespace: payload.header.namespace, name: payload.header.name }); + if (payload.header.namespace === "Alexa" && payload.header.name === "ReportState") { + this.log(`ReportState directive for device ${endpointId}`); device.emit("ReportState", payload); } else { device.emit("Event", payload, payload.header.namespace); } } }); - this.client.on("error", (err: Error) => { - if (this.debugLogging) { - console.error("[Alex2Node.ts] Connection failed:", err); - } - console.error("[Alex2Node.ts] MQTT connection error:", err); - this.emit("error", err); + } + + /** Close the broker connection (resolves once closed). The devices stay registered; connect() again reuses them. */ + disconnect(): Promise { + return new Promise((resolve) => { + const c = this.client; + if (!c) return resolve(); + this.client = null; + this.connected = false; + c.end(true, {}, () => resolve()); }); } @@ -128,17 +149,13 @@ class Alex2MQTT extends EventEmitter { if (!this.client) { throw new Error("Must call connect before creating devices"); } - if (this.debugLogging) { - console.log( - `[Alex2Node.ts] Creating new device with endpoint: ${endpointId}` - ); - } + this.log(`Creating new device with endpoint: ${endpointId}`); const normalizedCategory = displayCategory === null ? [DisplayCategory.LIGHT] : Array.isArray(displayCategory) ? displayCategory - : [displayCategory||DisplayCategory.LIGHT]; + : [displayCategory || DisplayCategory.LIGHT]; const device = new Device( this.client!, @@ -150,6 +167,32 @@ class Alex2MQTT extends EventEmitter { this.devices.push(device); return device; } + + /** Forget a device (its listeners with it). Returns false when there was none. */ + unregisterDevice(endpointId: string): boolean { + const i = this.devices.findIndex((d) => d.endpointId === endpointId); + if (i === -1) return false; + this.devices[i].removeAllListeners(); + this.devices.splice(i, 1); + return true; + } + /** Forget every device. */ + clearDevices(): void { + for (const d of this.devices) d.removeAllListeners(); + this.devices = []; + } + getDevices(): Device[] { + return this.devices.slice(); + } + getDevice(endpointId: string): Device | undefined { + return this.devices.find((d) => d.endpointId === endpointId); + } + getRootTopic(): string { + return this.rootTopic; + } + getHost(): string { + return this.MqttHost; + } } export default Alex2MQTT; diff --git a/src/AlexaErrorResponse.ts b/src/AlexaErrorResponse.ts index ee845df..1f065c5 100644 --- a/src/AlexaErrorResponse.ts +++ b/src/AlexaErrorResponse.ts @@ -125,7 +125,10 @@ export class AlexaErrorResponse { }; } - send(sendAsync = false) { + toJSON(): { event: any } { + return { event: this.event }; + } + send(sendAsync = false): Promise { const payload = { event: this.event, }; @@ -133,16 +136,8 @@ export class AlexaErrorResponse { sendAsync ? "deferredResponse" : "alexaResponce" }`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too const payloadStr = JSON.stringify(payload); - - this.mqttClient.publish(topic, payloadStr, (err) => { - if (err) { - console.error( - "[AlexaErrorResponse] Failed to publish status message:", - err - ); - } else { - console.log(`[AlexaErrorResponse] Sent message to ${topic}`); - } + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, payloadStr, (err) => (err ? reject(err) : resolve(topic))); }); } } diff --git a/src/AlexaInterface.ts b/src/AlexaInterface.ts index 31fac2d..8140450 100644 --- a/src/AlexaInterface.ts +++ b/src/AlexaInterface.ts @@ -256,6 +256,7 @@ export class AlexaInterface { case AlexaInterfaceType.REMOTE_VIDEO_PLAYER: case AlexaInterfaceType.RTC_SESSION_CONTROLLER: case AlexaInterfaceType.SCENE_CONTROLLER: + return []; // scenes have no reportable properties: Activate/Deactivate answer with ActivationStarted (Device.sendSceneResponse) case AlexaInterfaceType.SECURITY_PANEL_CONTROLLER: case AlexaInterfaceType.SEEK_CONTROLLER: case AlexaInterfaceType.SIMPLE_EVENT_SOURCE: @@ -290,7 +291,12 @@ export class AlexaInterface { supported: this.getProps().map(name => ({ name })), }, }; - if(this.type==AlexaInterfaceType.THERMOSTAT_CONTROLLER){ + if (this.type == AlexaInterfaceType.SCENE_CONTROLLER) { + // Alexa.SceneController v3 carries no properties block: supportsDeactivation + proactivelyReported at the top level + delete doc.properties; + doc.supportsDeactivation = true; + doc.proactivelyReported = this.proactivelyReported; + } else if(this.type==AlexaInterfaceType.THERMOSTAT_CONTROLLER){ doc.configuration={ "supportedModes": ["HEAT", "COOL", "AUTO", "OFF"], "supportsScheduling": false diff --git a/src/AlexaStatusMessage.ts b/src/AlexaStatusMessage.ts index 7855730..301887f 100644 --- a/src/AlexaStatusMessage.ts +++ b/src/AlexaStatusMessage.ts @@ -47,8 +47,15 @@ interface ContextProperty { instance?: string; } +/** Why a ChangeReport is sent (Alexa.ChangeReport payload.change.cause.type). */ +export type ChangeCause = "APP_INTERACTION" | "PHYSICAL_INTERACTION" | "PERIODIC_POLL" | "RULE_TRIGGER" | "VOICE_INTERACTION"; + export class AlexaStatusMessage { private context: { properties: ContextProperty[] } = { properties: [] }; + /** ChangeReport only: the properties that changed (payload.change.properties); the rest go to context. */ + private changeProps: ContextProperty[] = []; + private changeCause: ChangeCause | null = null; + private target: "context" | "change" = "context"; private event: { header: DirectiveHeader; endpoint: DirectiveEndpoint; @@ -65,16 +72,21 @@ export class AlexaStatusMessage { endpointId: string, mqttClient: MqttClient, isResponse: boolean = false, - isDeferred: boolean = false + isDeferred: boolean = false, + changeCause: ChangeCause | null = null ) { this.rootTopic = rootTopic; this.endpointId = endpointId; this.mqttClient = mqttClient; this.isDeferred = isDeferred; + this.changeCause = changeCause; + if (changeCause) this.target = "change"; this.event = { header: { namespace: "Alexa", - name: isDeferred + name: changeCause + ? "ChangeReport" + : isDeferred ? "DeferredResponse" : isResponse ? "Response" @@ -112,9 +124,33 @@ export class AlexaStatusMessage { uncertaintyInMilliseconds, }; if (instance) prop.instance = instance; - this.context.properties.push(prop); + (this.target === "change" ? this.changeProps : this.context.properties).push(prop); return this; } + /** ChangeReport: the add*Prop calls that follow describe what CHANGED (the default for a change report). */ + public changed(): this { + this.target = "change"; + return this; + } + /** ChangeReport: the add*Prop calls that follow describe the other, unchanged properties (context). */ + public unchanged(): this { + this.target = "context"; + return this; + } + /** True for a ChangeReport (Device.getChangeReport). */ + public isChangeReport(): boolean { + return this.changeCause !== null; + } + /** The message as it will be published (for tests and logging). */ + public toJSON(): { event: any; context: { properties: ContextProperty[] } | null } { + if (this.changeCause) { + return { + event: { ...this.event, payload: { change: { cause: { type: this.changeCause }, properties: this.changeProps } } }, + context: this.context, + }; + } + return { context: this.isDeferred ? null : this.context, event: this.event }; + } public addModeControllerProp(instance: string, value: string, uncertaintyInMs = 0): this { return this.addProperty( AlexaInterfaceType.MODE_CONTROLLER, @@ -250,23 +286,18 @@ export class AlexaStatusMessage { return this; } - public send(sendAsync: boolean): void { - const payload = { - context: this.isDeferred ? null : this.context, - event: this.event, - }; - const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync?"deferredResponse":"alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too - const payloadStr = JSON.stringify(payload); - - this.mqttClient.publish(topic, payloadStr, (err) => { - if (err) { - console.error( - "[AlexaStatusMessage] Failed to publish status message:", - err - ); - } else { - console.log(`[AlexaStatusMessage] Sent message to ${topic}`); - } + /** + * Publish: a Response/StateReport to //alexaResponce (sendAsync: deferredResponse), a ChangeReport + * to /changeReport (Alex2MQTT adds the user's token and posts it to the Alexa event gateway). Resolves with + * the topic; rejects on a publish error (1.4.0 only logged). + */ + public send(sendAsync: boolean = false): Promise { + const payloadStr = JSON.stringify(this.toJSON()); + const topic = this.changeCause + ? `${this.rootTopic}/changeReport` + : `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; //Yes this should be response but it is incorrect in both Alex2MQTT and Alex2ESP so for consistency is is wrong here too + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, payloadStr, (err) => (err ? reject(err) : resolve(topic))); }); } } diff --git a/src/Device.ts b/src/Device.ts index 8a1871d..2532fa5 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -2,7 +2,8 @@ import { AlexaInterface, AlexaInterfaceType } from "./AlexaInterface"; import { DisplayCategory } from "./DisplayCategory"; import { EventEmitter } from "events"; import { MqttClient } from "mqtt"; -import { AlexaStatusMessage } from "./AlexaStatusMessage"; +import { AlexaStatusMessage, ChangeCause } from "./AlexaStatusMessage"; +import { v4 as uuidv4 } from "uuid"; import { AlexaErrorResponse } from "./AlexaErrorResponse"; class Device extends EventEmitter { @@ -72,6 +73,35 @@ class Device extends EventEmitter { isDeferred ); } + /** + * A proactive Alexa.ChangeReport (1.5.1): add the changed properties (the default target), optionally + * .unchanged() then the others, and .send() - it goes to /changeReport, which Alex2MQTT forwards to the Alexa + * event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported. + */ + getChangeReport(cause: ChangeCause = "PHYSICAL_INTERACTION"): AlexaStatusMessage { + return new AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, false, false, cause); + } + /** + * Alexa.SceneController: answer an Activate / Deactivate directive with ActivationStarted / DeactivationStarted + * (1.5.1). Resolves with the topic published to. + */ + sendSceneResponse(correlationToken: string, activated: boolean, cause: ChangeCause = "VOICE_INTERACTION", sendAsync = false): Promise { + const payload = { + context: {}, + event: { + header: { namespace: "Alexa.SceneController", name: activated ? "ActivationStarted" : "DeactivationStarted", messageId: uuidv4(), correlationToken, payloadVersion: "3" }, + endpoint: { endpointId: this.endpointId }, + payload: { cause: { type: cause }, timestamp: new Date().toISOString() }, + }, + }; + const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`; + return new Promise((resolve, reject) => { + this.mqttClient.publish(topic, JSON.stringify(payload), (err) => (err ? reject(err) : resolve(topic))); + }); + } + getCapabilities(): AlexaInterface[] { + return this.capabilities.slice(); + } setManufacturerName(name: string): void { this.manufacturerName = name; } @@ -100,15 +130,9 @@ class Device extends EventEmitter { return this.softwareVersion; } - addCapability(type: AlexaInterfaceType): AlexaInterface { - // const existing = this.capabilities.find( - // (iface) => iface.getType() === type - // ); - // if (existing) { - // return existing; - // } - - const newCapability = new AlexaInterface(type); + /** Add a capability. 1.5.1: options.retrievable / proactivelyReported / instance (a change report needs proactivelyReported). */ + addCapability(type: AlexaInterfaceType, options: { retrievable?: boolean; proactivelyReported?: boolean; instance?: string } = {}): AlexaInterface { + const newCapability = new AlexaInterface(type, options.retrievable ?? true, options.proactivelyReported ?? false, options.instance ?? ""); this.capabilities.push(newCapability); // console.log( diff --git a/src/index.ts b/src/index.ts index f381872..7c93aca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,9 @@ -export { default as Alex2MQTT } from './Alex2Node'; -export { AlexaInterfaceType } from './AlexaInterface'; -export { ActionMapping, AlexaActions } from './ActionMapping'; -export {DisplayCategory} from "./DisplayCategory"; -export { PowerController,EndpointHealth,TemperatureSensorScale,ThermostatMode } from './AlexaStatusMessage'; -export { AlexaErrorType,AlexaErrorResponse} from './AlexaErrorResponse'; +export { default as Alex2MQTT, DEFAULT_HOST } from "./Alex2Node"; +export type { Alex2MQTTOptions } from "./Alex2Node"; +export { default as Device } from "./Device"; +export { AlexaInterface, AlexaInterfaceType } from "./AlexaInterface"; +export { ActionMapping, AlexaActions } from "./ActionMapping"; +export { DisplayCategory } from "./DisplayCategory"; +export { AlexaStatusMessage, PowerController, EndpointHealth, TemperatureSensorScale, ThermostatMode } from "./AlexaStatusMessage"; +export type { ChangeCause } from "./AlexaStatusMessage"; +export { AlexaErrorType, AlexaErrorResponse } from "./AlexaErrorResponse"; diff --git a/test/alex2node.test.js b/test/alex2node.test.js new file mode 100644 index 0000000..ecc1bb2 --- /dev/null +++ b/test/alex2node.test.js @@ -0,0 +1,109 @@ +"use strict"; +// Alex2Node against an in-process MQTT broker (aedes): the 1.4.0 contract (discovery on /discover -> /discover_r, +// directives on //alexaDirective -> Response on .../alexaResponce) and the 1.5.1 additions: a configurable +// broker, connection errors as events that never crash the host, ChangeReport on /changeReport, scene responses, +// unregister. +const { test, after } = require("node:test"); +const assert = require("node:assert/strict"); +const net = require("node:net"); +const aedes = require("aedes"); +const mqtt = require("mqtt"); +const { Alex2MQTT, AlexaInterfaceType, DisplayCategory, PowerController, EndpointHealth, TemperatureSensorScale, DEFAULT_HOST } = require("../dist/index.js"); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const until = async (fn, ms = 3000, what = "condition") => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await fn()) return true; await sleep(15); } throw new Error(`timeout waiting for ${what}`); }; +const cleanups = []; +after(async () => { for (const c of cleanups.reverse()) { try { await c(); } catch { /* gone */ } } }); + +async function broker() { + const a = aedes(); + const server = net.createServer(a.handle); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + const port = server.address().port; + cleanups.push(() => new Promise((r) => { a.close(() => server.close(() => r())); })); + return { a, port, url: `mqtt://127.0.0.1:${port}` }; +} +async function watcher(url, filter) { + const c = mqtt.connect(url); const got = []; + await new Promise((r, j) => { c.once("connect", r); c.once("error", j); }); + await new Promise((r) => c.subscribe(filter, r)); + c.on("message", (t, m) => { let p; try { p = JSON.parse(m.toString()); } catch { p = m.toString(); } got.push({ topic: t, payload: p }); }); + cleanups.push(() => new Promise((r) => c.end(true, {}, r))); + return { c, got, publish: (t, p) => c.publish(t, typeof p === "string" ? p : JSON.stringify(p)) }; +} + +test("defaults: the 1.4.0 host stays the default; options.host overrides it", () => { + const a = new Alex2MQTT("u", "p", "root"); + assert.equal(a.getHost(), DEFAULT_HOST); assert.equal(DEFAULT_HOST, "mqtt://Alex2MQTT.stormysdream.club:1883"); + assert.equal(new Alex2MQTT("u", "p", "root", false, { host: "mqtt://127.0.0.1:1" }).getHost(), "mqtt://127.0.0.1:1"); +}); + +test("discovery + directive + ReportState round trip on a local broker", async () => { + const b = await broker(); + const alexa = await watcher(b.url, "root/#"); + const bridge = new Alex2MQTT("u", "p", "root", false, { host: b.url }); + const events = []; bridge.on("discover", (n) => events.push(["discover", n])); bridge.on("directive", (d) => events.push(["directive", d.name])); + bridge.connect(); + await until(() => bridge.connected, 3000, "connect"); + cleanups.push(() => bridge.disconnect()); + const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT); + lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true }); + lamp.addCapability(AlexaInterfaceType.BRIGHTNESS_CONTROLLER); + const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER); + scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER); + lamp.on("Event", (directive, ns) => { lamp.getStatusMessage(directive.header.correlationToken, true).addHealthProp(EndpointHealth.OK).addPowerControllerProp(directive.header.name === "TurnOn" ? PowerController.ON : PowerController.OFF).addBrightnessControllerProp(100).send(false); }); + lamp.on("ReportState", (payload) => { lamp.getStatusMessage(payload.header.correlationToken).addPowerControllerProp(PowerController.OFF).addBrightnessControllerProp(0).send(false); }); + scene.on("Event", (directive) => { scene.sendSceneResponse(directive.header.correlationToken, directive.header.name === "Activate"); }); + alexa.publish("root/discover", { namespace: "Alexa.Discovery", name: "Discover" }); + await until(() => alexa.got.some((m) => m.topic === "root/discover_r"), 3000, "discovery reply"); + const disc = alexa.got.find((m) => m.topic === "root/discover_r").payload; + assert.equal(disc.length, 2); + assert.equal(disc[0].friendlyName, "Dining Room Light"); assert.deepEqual(disc[0].displayCategories, ["LIGHT"]); + const power = disc[0].capabilities.find((c) => c.interface === "Alexa.PowerController"); assert.equal(power.properties.proactivelyReported, true); assert.deepEqual(power.properties.supported, [{ name: "powerState" }]); + const sc = disc[1].capabilities.find((c) => c.interface === "Alexa.SceneController"); assert.equal(sc.supportsDeactivation, true); assert.equal(sc.version, "3"); assert.equal(sc.properties, undefined); + assert.deepEqual(events[0], ["discover", 2]); assert.equal(bridge.lastDiscoveryAt !== null, true); + alexa.publish("root/5020AA/alexaDirective", { header: { namespace: "Alexa.PowerController", name: "TurnOn", correlationToken: "ct1", payloadVersion: "3", messageId: "m1" }, endpoint: { endpointId: "5020AA" }, payload: {} }); + await until(() => alexa.got.some((m) => m.topic === "root/5020AA/alexaResponce"), 3000, "directive response"); + const resp = alexa.got.find((m) => m.topic === "root/5020AA/alexaResponce").payload; + assert.equal(resp.event.header.name, "Response"); assert.equal(resp.event.header.correlationToken, "ct1"); assert.equal(resp.context.properties.find((p) => p.name === "powerState").value, "ON"); + assert.deepEqual(events[1], ["directive", "TurnOn"]); + alexa.publish("root/5020AA/alexaDirective", { header: { namespace: "Alexa", name: "ReportState", correlationToken: "ct2", payloadVersion: "3", messageId: "m2" }, endpoint: { endpointId: "5020AA" }, payload: {} }); + await until(() => alexa.got.filter((m) => m.topic === "root/5020AA/alexaResponce").length === 2, 3000, "state report"); + assert.equal(alexa.got.filter((m) => m.topic === "root/5020AA/alexaResponce")[1].payload.event.header.name, "StateReport"); + alexa.publish("root/scene-1/alexaDirective", { header: { namespace: "Alexa.SceneController", name: "Activate", correlationToken: "ct3", payloadVersion: "3", messageId: "m3" }, endpoint: { endpointId: "scene-1" }, payload: {} }); + await until(() => alexa.got.some((m) => m.topic === "root/scene-1/alexaResponce"), 3000, "scene response"); + const sr = alexa.got.find((m) => m.topic === "root/scene-1/alexaResponce").payload; + assert.equal(sr.event.header.namespace, "Alexa.SceneController"); assert.equal(sr.event.header.name, "ActivationStarted"); assert.equal(sr.event.payload.cause.type, "VOICE_INTERACTION"); assert.ok(sr.event.payload.timestamp); + // change report: changed props in payload.change, the rest in context, on /changeReport + const topic = await lamp.getChangeReport("PHYSICAL_INTERACTION").addPowerControllerProp(PowerController.OFF).unchanged().addBrightnessControllerProp(0).send(); + assert.equal(topic, "root/changeReport"); + await until(() => alexa.got.some((m) => m.topic === "root/changeReport"), 3000, "change report"); + const cr = alexa.got.find((m) => m.topic === "root/changeReport").payload; + assert.equal(cr.event.header.name, "ChangeReport"); assert.equal(cr.event.endpoint.endpointId, "5020AA"); assert.equal(cr.event.payload.change.cause.type, "PHYSICAL_INTERACTION"); + assert.deepEqual(cr.event.payload.change.properties.map((p) => [p.name, p.value]), [["powerState", "OFF"]]); assert.deepEqual(cr.context.properties.map((p) => [p.name, p.value]), [["brightness", 0]]); + // thermostat props convert to celsius as 1.4.0 did + const st = lamp.getStatusMessage("x").addTemperatureSensorProp(TemperatureSensorScale.FAHRENHEIT, 68).addThermostatControllerProp("lowerSetpoint", TemperatureSensorScale.FAHRENHEIT, 68).toJSON(); + assert.equal(Math.round(st.context.properties[0].value.value * 10) / 10, 20); assert.equal(st.context.properties[0].value.scale, "CELSIUS"); + // unregister: the device no longer answers + assert.equal(bridge.unregisterDevice("scene-1"), true); assert.equal(bridge.unregisterDevice("scene-1"), false); assert.equal(bridge.getDevices().length, 1); + alexa.publish("root/discover", {}); + await until(() => alexa.got.filter((m) => m.topic === "root/discover_r").length === 2, 3000, "second discovery"); + assert.equal(alexa.got.filter((m) => m.topic === "root/discover_r")[1].payload.length, 1); + await bridge.disconnect(); assert.equal(bridge.connected, false); +}); + +test("a dead broker: 'offline'/'error' events when listened to, and NO crash when nobody listens (1.4.0 killed the host)", async () => { + const dead = new Alex2MQTT("u", "p", "root", false, { host: "mqtt://127.0.0.1:1", mqtt: { reconnectPeriod: 50, connectTimeout: 200 } }); + dead.connect(); cleanups.push(() => dead.disconnect()); + await sleep(300); + assert.equal(dead.connected, false, "still alive, not connected"); + const seen = []; + const listened = new Alex2MQTT("u", "p", "root", false, { host: "mqtt://127.0.0.1:1", mqtt: { reconnectPeriod: 50, connectTimeout: 200 } }); + listened.on("error", (e) => seen.push(e.code || e.message)); listened.on("offline", () => seen.push("offline")); listened.on("reconnect", () => seen.push("reconnect")); + listened.connect(); cleanups.push(() => listened.disconnect()); + await until(() => seen.includes("ECONNREFUSED") && seen.includes("reconnect"), 3000, "error + reconnect events"); +}); + +test("registerDevice before connect throws (unchanged from 1.4.0)", () => { + assert.throws(() => new Alex2MQTT("u", "p", "root").registerDevice("x", "1", null), /connect before/); +});