diff --git a/packages/nodes-base/nodes/MQTT/GenericFunctions.ts b/packages/nodes-base/nodes/MQTT/GenericFunctions.ts new file mode 100644 index 0000000000..2500af2a16 --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/GenericFunctions.ts @@ -0,0 +1,53 @@ +import { connectAsync, type IClientOptions, type MqttClient } from 'mqtt'; +import { randomString } from 'n8n-workflow'; +import { formatPrivateKey } from '@utils/utilities'; + +interface BaseMqttCredential { + protocol: 'mqtt' | 'mqtts' | 'ws'; + host: string; + port: number; + username: string; + password: string; + clean: boolean; + clientId: string; + passwordless?: boolean; +} + +type NonSslMqttCredential = BaseMqttCredential & { + ssl: false; +}; + +type SslMqttCredential = BaseMqttCredential & { + ssl: true; + ca: string; + cert: string; + key: string; + rejectUnauthorized?: boolean; +}; +export type MqttCredential = NonSslMqttCredential | SslMqttCredential; + +export const createClient = async (credentials: MqttCredential): Promise => { + const { protocol, host, port, clean, clientId, username, password } = credentials; + + const clientOptions: IClientOptions = { + protocol, + host, + port, + clean, + clientId: clientId || `mqttjs_${randomString(8).toLowerCase()}`, + }; + + if (username && password) { + clientOptions.username = username; + clientOptions.password = password; + } + + if (credentials.ssl) { + clientOptions.ca = formatPrivateKey(credentials.ca); + clientOptions.cert = formatPrivateKey(credentials.cert); + clientOptions.key = formatPrivateKey(credentials.key); + clientOptions.rejectUnauthorized = credentials.rejectUnauthorized; + } + + return await connectAsync(clientOptions); +}; diff --git a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts index 2af6354b2f..40674103b9 100644 --- a/packages/nodes-base/nodes/MQTT/Mqtt.node.ts +++ b/packages/nodes-base/nodes/MQTT/Mqtt.node.ts @@ -1,6 +1,6 @@ +import type { IClientPublishOptions } from 'mqtt'; import type { IExecuteFunctions, - ICredentialDataDecryptedObject, ICredentialsDecrypted, ICredentialTestFunctions, INodeCredentialTestResult, @@ -8,10 +8,10 @@ import type { INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { randomString } from 'n8n-workflow'; -import * as mqtt from 'mqtt'; -import { formatPrivateKey } from '@utils/utilities'; +import { createClient, type MqttCredential } from './GenericFunctions'; + +type PublishOption = Pick; export class Mqtt implements INodeType { description: INodeTypeDescription = { @@ -110,63 +110,11 @@ export class Mqtt implements INodeType { this: ICredentialTestFunctions, credential: ICredentialsDecrypted, ): Promise { - const credentials = credential.data as ICredentialDataDecryptedObject; + const credentials = credential.data as unknown as MqttCredential; + try { - const protocol = (credentials.protocol as string) || 'mqtt'; - const host = credentials.host as string; - const brokerUrl = `${protocol}://${host}`; - const port = (credentials.port as number) || 1883; - const clientId = - (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; - const clean = credentials.clean as boolean; - const ssl = credentials.ssl as boolean; - const ca = formatPrivateKey(credentials.ca as string); - const cert = formatPrivateKey(credentials.cert as string); - const key = formatPrivateKey(credentials.key as string); - const rejectUnauthorized = credentials.rejectUnauthorized as boolean; - - let client: mqtt.MqttClient; - - if (!ssl) { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - }; - - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - client = mqtt.connect(brokerUrl, clientOptions); - } else { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - ca, - cert, - key, - rejectUnauthorized, - }; - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - - client = mqtt.connect(brokerUrl, clientOptions); - } - - await new Promise((resolve, reject) => { - client.on('connect', (test) => { - resolve(test); - client.end(); - }); - client.on('error', (error) => { - client.end(); - reject(error); - }); - }); + const client = await createClient(credentials); + client.end(); } catch (error) { return { status: 'Error', @@ -182,87 +130,27 @@ export class Mqtt implements INodeType { }; async execute(this: IExecuteFunctions): Promise { + const credentials = (await this.getCredentials('mqtt')) as unknown as MqttCredential; + const client = await createClient(credentials); + + const publishPromises = []; const items = this.getInputData(); - const length = items.length; - const credentials = await this.getCredentials('mqtt'); - - const protocol = (credentials.protocol as string) || 'mqtt'; - const host = credentials.host as string; - const brokerUrl = `${protocol}://${host}`; - const port = (credentials.port as number) || 1883; - const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; - const clean = credentials.clean as boolean; - const ssl = credentials.ssl as boolean; - const ca = credentials.ca as string; - const cert = credentials.cert as string; - const key = credentials.key as string; - const rejectUnauthorized = credentials.rejectUnauthorized as boolean; - - let client: mqtt.MqttClient; - - if (!ssl) { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - }; - - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - - client = mqtt.connect(brokerUrl, clientOptions); - } else { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - ca, - cert, - key, - rejectUnauthorized, - }; - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - - client = mqtt.connect(brokerUrl, clientOptions); + for (let i = 0; i < items.length; i++) { + const topic = this.getNodeParameter('topic', i) as string; + const options = this.getNodeParameter('options', i) as unknown as PublishOption; + const sendInputData = this.getNodeParameter('sendInputData', i) as boolean; + const message = sendInputData + ? JSON.stringify(items[i].json) + : (this.getNodeParameter('message', i) as string); + publishPromises.push(client.publishAsync(topic, message, options)); } - const sendInputData = this.getNodeParameter('sendInputData', 0) as boolean; + await Promise.all(publishPromises); - const data = await new Promise((resolve, reject) => { - client.on('connect', () => { - for (let i = 0; i < length; i++) { - let message; - const topic = this.getNodeParameter('topic', i) as string; - const options = this.getNodeParameter('options', i); + // wait for the in-flight messages to be acked. + // needed for messages with QoS 1 & 2 + await client.endAsync(); - try { - if (sendInputData) { - message = JSON.stringify(items[i].json); - } else { - message = this.getNodeParameter('message', i) as string; - } - client.publish(topic, message, options); - } catch (e) { - reject(e); - } - } - //wait for the in-flight messages to be acked. - //needed for messages with QoS 1 & 2 - client.end(false, {}, () => { - resolve([items]); - }); - - client.on('error', (e) => { - reject(e); - }); - }); - }); - - return data as INodeExecutionData[][]; + return [items]; } } diff --git a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts index 23a6ddfd1c..f88029c2b1 100644 --- a/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts +++ b/packages/nodes-base/nodes/MQTT/MqttTrigger.node.ts @@ -1,16 +1,22 @@ +import type { ISubscriptionMap } from 'mqtt'; +import type { QoS } from 'mqtt-packet'; import type { ITriggerFunctions, IDataObject, INodeType, INodeTypeDescription, ITriggerResponse, - IDeferredPromise, IRun, } from 'n8n-workflow'; -import { NodeOperationError, randomString } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; -import * as mqtt from 'mqtt'; -import { formatPrivateKey } from '@utils/utilities'; +import { createClient, type MqttCredential } from './GenericFunctions'; + +interface Options { + jsonParseBody: boolean; + onlyMessage: boolean; + parallelProcessing: boolean; +} export class MqttTrigger implements INodeType { description: INodeTypeDescription = { @@ -87,120 +93,64 @@ export class MqttTrigger implements INodeType { }; async trigger(this: ITriggerFunctions): Promise { - const credentials = await this.getCredentials('mqtt'); - const topics = (this.getNodeParameter('topics') as string).split(','); - - const topicsQoS: IDataObject = {}; - - for (const data of topics) { - const [topic, qos] = data.split(':'); - topicsQoS[topic] = qos ? { qos: parseInt(qos, 10) } : { qos: 0 }; - } - - const options = this.getNodeParameter('options') as IDataObject; - const parallelProcessing = this.getNodeParameter('options.parallelProcessing', true) as boolean; - - if (!topics) { + if (!topics?.length) { throw new NodeOperationError(this.getNode(), 'Topics are mandatory!'); } - const protocol = (credentials.protocol as string) || 'mqtt'; - const host = credentials.host as string; - const brokerUrl = `${protocol}://${host}`; - const port = (credentials.port as number) || 1883; - const clientId = (credentials.clientId as string) || `mqttjs_${randomString(8).toLowerCase()}`; - const clean = credentials.clean as boolean; - const ssl = credentials.ssl as boolean; - const ca = formatPrivateKey(credentials.ca as string); - const cert = formatPrivateKey(credentials.cert as string); - const key = formatPrivateKey(credentials.key as string); - const rejectUnauthorized = credentials.rejectUnauthorized as boolean; - - let client: mqtt.MqttClient; - - if (!ssl) { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - }; - - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - - client = mqtt.connect(brokerUrl, clientOptions); - } else { - const clientOptions: mqtt.IClientOptions = { - port, - clean, - clientId, - ca, - cert, - key, - rejectUnauthorized, - }; - if (credentials.username && credentials.password) { - clientOptions.username = credentials.username as string; - clientOptions.password = credentials.password as string; - } - - client = mqtt.connect(brokerUrl, clientOptions); + const topicsQoS: ISubscriptionMap = {}; + for (const data of topics) { + const [topic, qosString] = data.split(':'); + let qos = qosString ? parseInt(qosString, 10) : 0; + if (qos < 0 || qos > 2) qos = 0; + topicsQoS[topic] = { qos: qos as QoS }; } - const manualTriggerFunction = async () => { - await new Promise((resolve, reject) => { - client.on('connect', () => { - client.subscribe(topicsQoS as mqtt.ISubscriptionMap, (error, _granted) => { - if (error) { - reject(error); - } - client.on('message', async (topic: string, message: Buffer | string) => { - let result: IDataObject = {}; + const options = this.getNodeParameter('options') as Options; + const credentials = (await this.getCredentials('mqtt')) as unknown as MqttCredential; + const client = await createClient(credentials); - message = message.toString(); + const parsePayload = (topic: string, payload: Buffer) => { + let message = payload.toString(); - if (options.jsonParseBody) { - try { - message = JSON.parse(message.toString()); - } catch (e) {} - } + if (options.jsonParseBody) { + try { + message = JSON.parse(message); + } catch (e) {} + } - result.message = message; - result.topic = topic; + let result: IDataObject = { message, topic }; - if (options.onlyMessage) { - //@ts-ignore - result = [message as string]; - } + if (options.onlyMessage) { + //@ts-ignore + result = [message]; + } - let responsePromise: IDeferredPromise | undefined; - if (!parallelProcessing) { - responsePromise = await this.helpers.createDeferredPromise(); - } - this.emit([this.helpers.returnJsonArray([result])], undefined, responsePromise); - if (responsePromise) { - await responsePromise.promise(); - } - resolve(true); - }); - }); - }); - - client.on('error', (error) => { - reject(error); - }); - }); + return [this.helpers.returnJsonArray([result])]; }; + const manualTriggerFunction = async () => + await new Promise(async (resolve) => { + client.once('message', (topic, payload) => { + this.emit(parsePayload(topic, payload)); + resolve(); + }); + await client.subscribeAsync(topicsQoS); + }); + if (this.getMode() === 'trigger') { - void manualTriggerFunction(); + const donePromise = !options.parallelProcessing + ? await this.helpers.createDeferredPromise() + : undefined; + client.on('message', async (topic, payload) => { + this.emit(parsePayload(topic, payload), undefined, donePromise); + await donePromise?.promise(); + }); + await client.subscribeAsync(topicsQoS); } async function closeFunction() { - client.end(); + await client.endAsync(); } return { diff --git a/packages/nodes-base/nodes/MQTT/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/MQTT/test/GenericFunctions.test.ts new file mode 100644 index 0000000000..6d68777fe3 --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/test/GenericFunctions.test.ts @@ -0,0 +1,38 @@ +import { MqttClient } from 'mqtt'; +import { mock } from 'jest-mock-extended'; + +import { createClient, type MqttCredential } from '../GenericFunctions'; + +describe('createClient', () => { + const mockConnect = jest.spyOn(MqttClient.prototype, 'connect').mockImplementation(function ( + this: MqttClient, + ) { + setImmediate(() => this.emit('connect', mock())); + return this; + }); + + beforeEach(() => jest.clearAllMocks()); + + it('should create a client with minimal credentials', async () => { + const credentials = mock({ + protocol: 'mqtt', + host: 'localhost', + port: 1883, + clean: true, + clientId: 'testClient', + ssl: false, + }); + const client = await createClient(credentials); + + expect(mockConnect).toBeCalledTimes(1); + expect(client).toBeDefined(); + expect(client).toBeInstanceOf(MqttClient); + expect(client.options).toMatchObject({ + protocol: 'mqtt', + host: 'localhost', + port: 1883, + clean: true, + clientId: 'testClient', + }); + }); +}); diff --git a/packages/nodes-base/nodes/MQTT/test/Mqtt.node.test.ts b/packages/nodes-base/nodes/MQTT/test/Mqtt.node.test.ts new file mode 100644 index 0000000000..67b6202520 --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/test/Mqtt.node.test.ts @@ -0,0 +1,56 @@ +import type { MqttClient } from 'mqtt'; +import { mock } from 'jest-mock-extended'; +import type { ICredentialDataDecryptedObject, IExecuteFunctions } from 'n8n-workflow'; + +import { Mqtt } from '../Mqtt.node'; +import { createClient } from '../GenericFunctions'; + +jest.mock('../GenericFunctions', () => { + const mockMqttClient = mock(); + return { + createClient: jest.fn().mockResolvedValue(mockMqttClient), + }; +}); + +describe('MQTT Node', () => { + const credentials = mock(); + const executeFunctions = mock(); + + beforeEach(() => { + jest.clearAllMocks(); + + executeFunctions.getCredentials.calledWith('mqtt').mockResolvedValue(credentials); + executeFunctions.getInputData.mockReturnValue([{ json: { testing: true } }]); + executeFunctions.getNodeParameter.calledWith('topic', 0).mockReturnValue('test/topic'); + executeFunctions.getNodeParameter.calledWith('options', 0).mockReturnValue({}); + }); + + it('should publish input data', async () => { + executeFunctions.getNodeParameter.calledWith('sendInputData', 0).mockReturnValue(true); + + const result = await new Mqtt().execute.call(executeFunctions); + + expect(result).toEqual([[{ json: { testing: true } }]]); + expect(executeFunctions.getCredentials).toHaveBeenCalledTimes(1); + expect(executeFunctions.getNodeParameter).toHaveBeenCalledTimes(3); + + const mockMqttClient = await createClient(mock()); + expect(mockMqttClient.publishAsync).toHaveBeenCalledWith('test/topic', '{"testing":true}', {}); + expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1); + }); + + it('should publish a custom message', async () => { + executeFunctions.getNodeParameter.calledWith('sendInputData', 0).mockReturnValue(false); + executeFunctions.getNodeParameter.calledWith('message', 0).mockReturnValue('Hello, MQTT!'); + + const result = await new Mqtt().execute.call(executeFunctions); + + expect(result).toEqual([[{ json: { testing: true } }]]); + expect(executeFunctions.getCredentials).toHaveBeenCalledTimes(1); + expect(executeFunctions.getNodeParameter).toHaveBeenCalledTimes(4); + + const mockMqttClient = await createClient(mock()); + expect(mockMqttClient.publishAsync).toHaveBeenCalledWith('test/topic', 'Hello, MQTT!', {}); + expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/nodes-base/nodes/MQTT/test/MqttTrigger.node.test.ts b/packages/nodes-base/nodes/MQTT/test/MqttTrigger.node.test.ts new file mode 100644 index 0000000000..05694b43c6 --- /dev/null +++ b/packages/nodes-base/nodes/MQTT/test/MqttTrigger.node.test.ts @@ -0,0 +1,123 @@ +import type { MqttClient, OnMessageCallback } from 'mqtt'; +import { returnJsonArray } from 'n8n-core'; +import { captor, mock } from 'jest-mock-extended'; +import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow'; + +import { MqttTrigger } from '../MqttTrigger.node'; +import { createClient } from '../GenericFunctions'; + +jest.mock('../GenericFunctions', () => { + const mockMqttClient = mock(); + return { + createClient: jest.fn().mockResolvedValue(mockMqttClient), + }; +}); + +describe('MQTT Trigger Node', () => { + const topic = 'test/topic'; + const payload = Buffer.from('{"testing": true}'); + const credentials = mock(); + const triggerFunctions = mock({ + helpers: { returnJsonArray }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + + triggerFunctions.getCredentials.calledWith('mqtt').mockResolvedValue(credentials); + triggerFunctions.getNodeParameter.calledWith('topics').mockReturnValue(topic); + }); + + it('should emit in manual mode', async () => { + triggerFunctions.getMode.mockReturnValue('manual'); + triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({}); + + const response = await new MqttTrigger().trigger.call(triggerFunctions); + expect(response.manualTriggerFunction).toBeDefined(); + expect(response.closeFunction).toBeDefined(); + + expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1); + expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2); + + // manually trigger the node, like Workflow.runNode does + const triggerPromise = response.manualTriggerFunction!(); + + const mockMqttClient = await createClient(mock()); + expect(mockMqttClient.on).not.toHaveBeenCalled(); + + const onMessageCaptor = captor(); + expect(mockMqttClient.once).toHaveBeenCalledWith('message', onMessageCaptor); + expect(mockMqttClient.subscribeAsync).toHaveBeenCalledWith({ [topic]: { qos: 0 } }); + expect(triggerFunctions.emit).not.toHaveBeenCalled(); + + // simulate a message + const onMessage = onMessageCaptor.value; + onMessage('test/topic', payload, mock()); + expect(triggerFunctions.emit).toHaveBeenCalledWith([ + [{ json: { message: '{"testing": true}', topic } }], + ]); + + // wait for the promise to resolve + await new Promise((resolve) => setImmediate(resolve)); + await expect(triggerPromise).resolves.toEqual(undefined); + + expect(mockMqttClient.endAsync).not.toHaveBeenCalled(); + await response.closeFunction!(); + expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1); + }); + + it('should emit in trigger mode', async () => { + triggerFunctions.getMode.mockReturnValue('trigger'); + triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({}); + + const response = await new MqttTrigger().trigger.call(triggerFunctions); + expect(response.manualTriggerFunction).toBeDefined(); + expect(response.closeFunction).toBeDefined(); + + expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1); + expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2); + + const mockMqttClient = await createClient(mock()); + expect(mockMqttClient.once).not.toHaveBeenCalled(); + + const onMessageCaptor = captor(); + expect(mockMqttClient.on).toHaveBeenCalledWith('message', onMessageCaptor); + expect(mockMqttClient.subscribeAsync).toHaveBeenCalledWith({ [topic]: { qos: 0 } }); + expect(triggerFunctions.emit).not.toHaveBeenCalled(); + + // simulate a message + const onMessage = onMessageCaptor.value; + onMessage('test/topic', payload, mock()); + expect(triggerFunctions.emit).toHaveBeenCalledWith( + [[{ json: { message: '{"testing": true}', topic } }]], + undefined, + undefined, + ); + + expect(mockMqttClient.endAsync).not.toHaveBeenCalled(); + await response.closeFunction!(); + expect(mockMqttClient.endAsync).toHaveBeenCalledTimes(1); + }); + + it('should parse JSON messages when configured', async () => { + triggerFunctions.getMode.mockReturnValue('trigger'); + triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({ + jsonParseBody: true, + }); + + await new MqttTrigger().trigger.call(triggerFunctions); + + const mockMqttClient = await createClient(mock()); + const onMessageCaptor = captor(); + expect(mockMqttClient.on).toHaveBeenCalledWith('message', onMessageCaptor); + + // simulate a message + const onMessage = onMessageCaptor.value; + onMessage('test/topic', payload, mock()); + expect(triggerFunctions.emit).toHaveBeenCalledWith( + [[{ json: { message: { testing: true }, topic } }]], + undefined, + undefined, + ); + }); +}); diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5d4ed97009..8d70c1a5e9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -873,7 +873,7 @@ "minifaker": "1.34.1", "moment-timezone": "0.5.37", "mongodb": "6.3.0", - "mqtt": "5.0.2", + "mqtt": "5.7.2", "mssql": "10.0.2", "mysql2": "3.10.0", "n8n-workflow": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b9b2b9393..d3f513e0e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1456,8 +1456,8 @@ importers: specifier: 6.3.0 version: 6.3.0(gcp-metadata@5.2.0(encoding@0.1.13))(socks@2.7.1) mqtt: - specifier: 5.0.2 - version: 5.0.2 + specifier: 5.7.2 + version: 5.7.2 mssql: specifier: 10.0.2 version: 10.0.2 @@ -2929,6 +2929,10 @@ packages: resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.24.7': + resolution: {integrity: sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==} + engines: {node: '>=6.9.0'} + '@babel/standalone@7.23.6': resolution: {integrity: sha512-+AzS6BZwZdSosrgS/TiGDYLxtlefARKClWgJ4ql//XfmV9KbPWbkEekvbvDRJ8a6qog8E9j3CziHLz5dbIEMyw==} engines: {node: '>=6.9.0'} @@ -6601,9 +6605,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bl@5.0.0: - resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==} - bl@6.0.12: resolution: {integrity: sha512-EnEYHilP93oaOa2MnmNEjAcovPS3JlQZOyzGXi3EyEpPhm9qWvdDp7BmAVEVusGzp8LlwQK56Av+OkDoRjzE0w==} @@ -8193,6 +8194,10 @@ packages: fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + fast-unique-numbers@8.0.13: + resolution: {integrity: sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==} + engines: {node: '>=16.1.0'} + fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true @@ -8736,8 +8741,8 @@ packages: resolution: {integrity: sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==} engines: {node: '>=16.0.0'} - help-me@4.2.0: - resolution: {integrity: sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} hexoid@1.0.0: resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} @@ -9994,6 +9999,10 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -10393,11 +10402,11 @@ packages: socks: optional: true - mqtt-packet@8.2.0: - resolution: {integrity: sha512-21Vo7XdRXUw2qhdTfk8GeOl2jtb8Dkwd4dKxn/epvf37mxTxHodvBJoozTPZGVwh57JXlsh2ChsaxMsAfqxp+A==} + mqtt-packet@9.0.0: + resolution: {integrity: sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==} - mqtt@5.0.2: - resolution: {integrity: sha512-JctWQpxjVVjn5LqAAhfgutMNMbHZf+puG9LnbWga0wRbWW7QVfHPu4Vz2iDSyHfGpW8HWLF+jb01vySDDzIKnQ==} + mqtt@5.7.2: + resolution: {integrity: sha512-b5xIA9J/K1LTubSWKaNYYLxYIusQdip6o9/8bRWad2TelRr8xLifjQt+SnamDAwMp3O6NdvR9E8ae7VMuN02kg==} engines: {node: '>=16.0.0'} hasBin: true @@ -13346,8 +13355,8 @@ packages: vue-component-type-helpers@2.0.19: resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} - vue-component-type-helpers@2.0.21: - resolution: {integrity: sha512-3NaicyZ7N4B6cft4bfb7dOnPbE9CjLcx+6wZWAg5zwszfO4qXRh+U52dN5r5ZZfc6iMaxKCEcoH9CmxxoFZHLg==} + vue-component-type-helpers@2.0.22: + resolution: {integrity: sha512-gPr2Ba7efUwy/Vfbuf735bHSVdN4ycoZUCHfypkI33M9DUH+ieRblLLVM2eImccFYaWNWwEzURx02EgoXDBmaQ==} vue-demi@0.14.5: resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==} @@ -13584,6 +13593,15 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + worker-timers-broker@6.1.8: + resolution: {integrity: sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==} + + worker-timers-worker@7.0.71: + resolution: {integrity: sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==} + + worker-timers@7.1.8: + resolution: {integrity: sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -15889,6 +15907,10 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.24.7': + dependencies: + regenerator-runtime: 0.14.1 + '@babel/standalone@7.23.6': {} '@babel/template@7.22.5': @@ -18976,7 +18998,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.5.2) - vue-component-type-helpers: 2.0.21 + vue-component-type-helpers: 2.0.22 transitivePeerDependencies: - encoding - prettier @@ -20623,12 +20645,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.0 - bl@5.0.0: - dependencies: - buffer: 6.0.3 - inherits: 2.0.4 - readable-stream: 3.6.0 - bl@6.0.12: dependencies: '@types/readable-stream': 4.0.10 @@ -22630,6 +22646,11 @@ snapshots: fast-text-encoding@1.0.6: {} + fast-unique-numbers@8.0.13: + dependencies: + '@babel/runtime': 7.24.7 + tslib: 2.6.2 + fast-xml-parser@4.2.5: dependencies: strnum: 1.0.5 @@ -23294,10 +23315,7 @@ snapshots: helmet@7.1.0: {} - help-me@4.2.0: - dependencies: - glob: 8.1.0 - readable-stream: 3.6.0 + help-me@5.0.0: {} hexoid@1.0.0: {} @@ -24769,6 +24787,8 @@ snapshots: dependencies: tslib: 2.6.2 + lru-cache@10.2.2: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -25159,29 +25179,31 @@ snapshots: gcp-metadata: 5.2.0(encoding@0.1.13) socks: 2.7.1 - mqtt-packet@8.2.0: + mqtt-packet@9.0.0: dependencies: - bl: 5.0.0 + bl: 6.0.12 debug: 4.3.4(supports-color@8.1.1) process-nextick-args: 2.0.1 transitivePeerDependencies: - supports-color - mqtt@5.0.2: + mqtt@5.7.2: dependencies: + '@types/readable-stream': 4.0.10 + '@types/ws': 8.5.10 commist: 3.2.0 concat-stream: 2.0.0 debug: 4.3.4(supports-color@8.1.1) - duplexify: 4.1.2 - help-me: 4.2.0 - lru-cache: 7.18.3 + help-me: 5.0.0 + lru-cache: 10.2.2 minimist: 1.2.8 - mqtt-packet: 8.2.0 + mqtt-packet: 9.0.0 number-allocator: 1.0.14 readable-stream: 4.4.2 reinterval: 1.1.0 rfdc: 1.3.0 split2: 4.2.0 + worker-timers: 7.1.8 ws: 8.17.1 transitivePeerDependencies: - bufferutil @@ -28509,7 +28531,7 @@ snapshots: vue-component-type-helpers@2.0.19: {} - vue-component-type-helpers@2.0.21: {} + vue-component-type-helpers@2.0.22: {} vue-demi@0.14.5(vue@3.4.21(typescript@5.5.2)): dependencies: @@ -28806,6 +28828,25 @@ snapshots: wordwrap@1.0.0: {} + worker-timers-broker@6.1.8: + dependencies: + '@babel/runtime': 7.24.7 + fast-unique-numbers: 8.0.13 + tslib: 2.6.2 + worker-timers-worker: 7.0.71 + + worker-timers-worker@7.0.71: + dependencies: + '@babel/runtime': 7.24.7 + tslib: 2.6.2 + + worker-timers@7.1.8: + dependencies: + '@babel/runtime': 7.24.7 + tslib: 2.6.2 + worker-timers-broker: 6.1.8 + worker-timers-worker: 7.0.71 + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0