From 58bf2dcb54742d867e41252890661feaf94fef9c Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 17 Jan 2026 14:56:05 -0700 Subject: [PATCH 01/18] Set version to 0.9.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c4095d..0a008d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-matrix-chat", - "version": "0.9.1", + "version": "0.9.2", "description": "Matrix chat server client for Node-RED", "dependencies": { "abort-controller": "^3.0.0", From 68e63e5def975763cf370f6de44cfc2e5b31a9d5 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Thu, 19 Feb 2026 22:45:31 -0700 Subject: [PATCH 02/18] Upgrade from matrix-js-sdk v34 to v34.13.0 --- package-lock.json | 98 +++++++++++++++------------- package.json | 9 ++- src/matrix-crypt-file.js | 4 +- src/matrix-server-config.js | 124 +++++++++++++++--------------------- 4 files changed, 111 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index adefe96..198204c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-red-contrib-matrix-chat", - "version": "0.8.0", + "version": "0.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-red-contrib-matrix-chat", - "version": "0.8.0", + "version": "0.9.2", "license": "SEE LICENSE FILE", "dependencies": { "abort-controller": "^3.0.0", @@ -15,7 +15,7 @@ "got": "^12.0.2", "image-size": "^1.0.2", "isomorphic-webcrypto": "^2.3.8", - "matrix-js-sdk": "34.11.1", + "matrix-js-sdk": "^34.13.0", "mime": "^3.0.0", "node-fetch": "^3.3.0", "node-localstorage": "^2.2.1", @@ -25,7 +25,7 @@ "utf8": "^3.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -4191,6 +4191,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz", "integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==", + "license": "Apache-2.0", "engines": { "node": ">= 10" } @@ -4198,7 +4199,8 @@ "node_modules/@matrix-org/olm": { "version": "3.2.15", "resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz", - "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==" + "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==", + "license": "Apache-2.0" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -6620,9 +6622,10 @@ "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, "node_modules/base-x": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", - "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==", + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -6824,6 +6827,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", "dependencies": { "base-x": "^5.0.0" } @@ -10629,9 +10633,9 @@ } }, "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "engines": { "node": ">= 0.6.0" }, @@ -10713,9 +10717,10 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "34.11.1", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz", - "integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==", + "version": "34.13.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.13.0.tgz", + "integrity": "sha512-AAU8ZdCawca+7ucQfdcC3LA85OtCTV7QeqcjvKt/ZZhU3xL9VoawuoRQ+4R6H8KZnqyJmT4j7bdeC0jG4qcqLg==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", @@ -10726,33 +10731,34 @@ "jwt-decode": "^4.0.0", "loglevel": "^1.7.1", "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.8.2", + "matrix-widget-api": "^1.10.0", "oidc-client-ts": "^3.0.1", "p-retry": "4", "sdp-transform": "^2.14.1", "unhomoglyph": "^1.0.6", - "uuid": "10" + "uuid": "11" }, "engines": { "node": ">=20.0.0" } }, "node_modules/matrix-js-sdk/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/matrix-widget-api": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz", - "integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", + "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", "dependencies": { "@types/events": "^3.0.0", "events": "^3.2.0" @@ -13767,9 +13773,9 @@ } }, "node_modules/sdp-transform": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", - "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", "bin": { "sdp-verify": "checker.js" } @@ -20333,9 +20339,9 @@ "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, "base-x": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", - "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", + "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==" }, "base64-js": { "version": "1.5.1", @@ -23413,9 +23419,9 @@ } }, "loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==" + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==" }, "loose-envify": { "version": "1.4.0", @@ -23477,9 +23483,9 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "matrix-js-sdk": { - "version": "34.11.1", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.11.1.tgz", - "integrity": "sha512-rDbIUIqEsN/pbHb6haBQmjxxgeb9G3Df2IhPPOotUbX6R1KseA8yJ6TAY0YySM2zVaBV3yZ6dnKWexF/uWvZfA==", + "version": "34.13.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.13.0.tgz", + "integrity": "sha512-AAU8ZdCawca+7ucQfdcC3LA85OtCTV7QeqcjvKt/ZZhU3xL9VoawuoRQ+4R6H8KZnqyJmT4j7bdeC0jG4qcqLg==", "requires": { "@babel/runtime": "^7.12.5", "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", @@ -23490,25 +23496,25 @@ "jwt-decode": "^4.0.0", "loglevel": "^1.7.1", "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.8.2", + "matrix-widget-api": "^1.10.0", "oidc-client-ts": "^3.0.1", "p-retry": "4", "sdp-transform": "^2.14.1", "unhomoglyph": "^1.0.6", - "uuid": "10" + "uuid": "11" }, "dependencies": { "uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==" } } }, "matrix-widget-api": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.13.1.tgz", - "integrity": "sha512-mkOHUVzaN018TCbObfGOSaMW2GoUxOfcxNNlTVx5/HeMk3OSQPQM0C9oEME5Liiv/dBUoSrEB64V8wF7e/gb1w==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz", + "integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==", "requires": { "@types/events": "^3.0.0", "events": "^3.2.0" @@ -25867,9 +25873,9 @@ } }, "sdp-transform": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", - "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", + "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==" }, "semver": { "version": "7.6.2", diff --git a/package.json b/package.json index 0a008d9..527815e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "got": "^12.0.2", "image-size": "^1.0.2", "isomorphic-webcrypto": "^2.3.8", - "matrix-js-sdk": "34.11.1", + "matrix-js-sdk": "^34.13.0", "mime": "^3.0.0", "node-fetch": "^3.3.0", "node-localstorage": "^2.2.1", @@ -55,7 +55,7 @@ } }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "keywords": [ "node-red", @@ -72,5 +72,8 @@ "name": "Skylar Sadlier", "url": "https://skylar.tech" }, - "license": "SEE LICENSE FILE" + "license": "SEE LICENSE FILE", + "engines": { + "node": ">=18.0.0" + } } diff --git a/src/matrix-crypt-file.js b/src/matrix-crypt-file.js index 779a1c8..a6236cf 100644 --- a/src/matrix-crypt-file.js +++ b/src/matrix-crypt-file.js @@ -58,11 +58,11 @@ module.exports = function(RED) { RED.nodes.registerType("matrix-decrypt-file", MatrixDecryptFile); function atob(a) { - return new Buffer.from(a, 'base64').toString('binary'); + return Buffer.from(a, 'base64').toString('binary'); } function btoa(b) { - return new Buffer.from(b).toString('base64'); + return Buffer.from(b).toString('base64'); } // the following was taken & modified from https://github.com/matrix-org/browser-encrypt-attachment/blob/master/index.js diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index f3acc44..d69cd49 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -1,51 +1,32 @@ +const {RelationType, TimelineWindow} = require("matrix-js-sdk"); + global.Olm = require('olm'); const fs = require("fs-extra"); -let RelationType, sdk, LocalStorageCryptoStore, RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore; - -(async () => { - const mod = await import("matrix-js-sdk"); - RelationType = mod.RelationType; - // matrix-js-sdk doesn't export a default – the top-level export is the same object: - sdk = mod; - - RoomEvent = mod.RoomEvent; - RoomMemberEvent = mod.RoomMemberEvent; - HttpApiEvent = mod.HttpApiEvent; - ClientEvent = mod.ClientEvent; - MemoryStore = mod.MemoryStore; - - // For LocalStorageCryptoStore, specify the file extension for Node 20+: - const cmod = await import("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store.js"); - LocalStorageCryptoStore = cmod.LocalStorageCryptoStore; -})(); - +const sdk = require("matrix-js-sdk"); const { resolve } = require('path'); const { LocalStorage } = require('node-localstorage'); +const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store'); +const {RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore} = require("matrix-js-sdk"); require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it if (!globalThis.fetch) { // polyfill fetch if we don't have it if (!globalThis.fetch) { import('node-fetch').then(({ default: fetch, Headers, Request, Response }) => { - Object.assign(globalThis, { fetch, Headers, Request, Response }); - }); + Object.assign(globalThis, { fetch, Headers, Request, Response }) + }) } } module.exports = function(RED) { // disable logging if set to "off" let loggingSettings = RED.settings.get('logging'); - if ( + if( typeof loggingSettings.console !== 'undefined' && typeof loggingSettings.console.level !== 'undefined' && ['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0 ) { - import('matrix-js-sdk/lib/logger.js') - .then(({ logger }) => { - logger.disableAll(); - }) - .catch((err) => { - console.error("Error loading logger module:", err); - }); + const { logger } = require('matrix-js-sdk/lib/logger'); + logger.disableAll(); } function MatrixFolderNameFromUserId(name) { @@ -74,18 +55,17 @@ module.exports = function(RED) { this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.e2ee = n.enableE2ee || false; this.globalAccess = n.global; - this.allowUnknownDevices = n.allowUnknownDevices || false; this.initializedAt = new Date(); node.initialSyncLimit = 25; - // Keep track of all consumers of this node to catch errors + // Keep track of all consumers of this node to be able to catch errors node.register = function(consumerNode) { node.users[consumerNode.id] = consumerNode; }; node.deregister = function(consumerNode) { delete node.users[consumerNode.id]; }; - + if(!this.userId) { node.log("Matrix connection failed: missing user ID in configuration."); return; @@ -118,7 +98,7 @@ module.exports = function(RED) { device_id = this.matrixClient.getDeviceId(); if(!device_id && node.enableE2ee) { - node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.", {}); + node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.", {}) } else { if(!stored_device_id || stored_device_id !== device_id) { node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); @@ -135,7 +115,7 @@ module.exports = function(RED) { node.matrixClient.setDeviceDetails(device_id, { display_name: node.deviceLabel }).then( - function() {}, + function(response) {}, function(error) { node.error("Failed to set device label: " + error, {}); } @@ -173,6 +153,7 @@ module.exports = function(RED) { }), userId: this.userId, deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined + // verificationMethods: ["m.sas.v1"] }); node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`); @@ -198,7 +179,7 @@ module.exports = function(RED) { if(node.globalAccess) { try { node.context().global.set('matrixClient["'+node.userId+'"]', undefined); - } catch(e) { + } catch(e){ node.error(e.message, {}); } } @@ -212,15 +193,15 @@ module.exports = function(RED) { node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) { if (toStartOfTimeline) { node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result"); - return; + return; // ignore paginated results } if (!data || !data.liveEvent) { node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message"); - return; + return; // ignore old message (we only want live events) } if(node.initializedAt > event.getDate()) { node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init"); - return; + return; // skip events that occurred before our client initialized } try { @@ -395,6 +376,7 @@ module.exports = function(RED) { } }); + node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){ // Example if user auth token incorrect: // { @@ -417,7 +399,6 @@ module.exports = function(RED) { node.log("Initializing crypto..."); await node.matrixClient.initCrypto(); node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices - node.matrixClient.getCrypto().globalErrorOnUnknownDevices = !node.allowUnknownDevices; } node.log("Connecting to Matrix server..."); await node.matrixClient.startClient({ @@ -467,7 +448,7 @@ module.exports = function(RED) { node.error("Auth check failed: " + err, {}); } } - ); + ) })(); } } @@ -492,19 +473,17 @@ module.exports = function(RED) { deviceId = req.body.deviceId || undefined, displayName = req.body.displayName || undefined; - (async () => { - const mod = await import("matrix-js-sdk"); - const matrixClient = mod.createClient({ - baseUrl: baseUrl, - deviceId: deviceId, - timelineSupport: true, - localTimeoutMs: '30000' - }); + const matrixClient = sdk.createClient({ + baseUrl: baseUrl, + deviceId: deviceId, + timelineSupport: true, + localTimeoutMs: '30000' + }); + matrixClient.timelineSupport = true; - matrixClient.timelineSupport = true; - - matrixClient.login('m.login.password', { + matrixClient.login( + 'm.login.password', { identifier: { type: 'm.id.user', user: userId, @@ -512,27 +491,23 @@ module.exports = function(RED) { password: password, initial_device_display_name: displayName }) - .then( - function(response) { - res.json({ - 'result': 'ok', - 'token': response.access_token, - 'device_id': response.device_id, - 'user_id': response.user_id, - }); - }, - function(err) { - res.json({ - 'result': 'error', - 'message': err - }); - } - ); - })().catch(err => { - res.json({ result: 'error', message: err.toString() }); - }); - } - ); + .then( + function(response) { + res.json({ + 'result': 'ok', + 'token': response.access_token, + 'device_id': response.device_id, + 'user_id': response.user_id, + }); + }, + function(err) { + res.json({ + 'result': 'error', + 'message': err + }); + } + ); + }); function upgradeDirectoryIfNecessary(node, storageDir) { let oldStorageDir = './matrix-local-storage', @@ -573,6 +548,9 @@ module.exports = function(RED) { } } + /** + * If a device ID is stored we will use that for the client + */ function getStoredDeviceId(localStorage) { let deviceId = localStorage.getItem('my_device_id'); if(deviceId === "null" || !deviceId) { @@ -588,4 +566,4 @@ module.exports = function(RED) { localStorage.setItem('my_device_id', deviceId); return true; } -}; \ No newline at end of file +} From ebcb1eab81287d8a98af5410257280442588e114 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Fri, 22 May 2026 14:40:00 -0600 Subject: [PATCH 03/18] Upgrade to matrix-js-sdk 41.5.0; add device verification Upgrades matrix-js-sdk from 34.13.0 to 41.5.0. This crosses the v37 removal of the legacy libolm crypto stack, so E2EE is migrated to the Rust crypto implementation. Also adds device verification, cross-signing setup, and authenticated media support. Dependencies - Bump matrix-js-sdk ^34.13.0 -> ^41.5.0; require Node.js >= 22. - Drop the `olm` dependency (legacy crypto only); add `fake-indexeddb`. Rust crypto - Replace initCrypto() with initRustCrypto(); the legacy crypto stack was removed upstream in v37. - Add src/matrix-crypto-store.js: the Rust crypto store requires IndexedDB, absent in Node.js, so it is backed by fake-indexeddb and snapshotted to disk (rust-crypto-store.v8) to survive restarts. - Migrate existing libolm crypto state into the Rust store on first run, and discard the stored crypto state when the device ID changes. Homeserver discovery - Resolve the homeserver via .well-known, so a delegating domain (e.g. example.org) works as the configured server URL. Cross-signing & secure backup - Add a secured /matrix-chat/secure-backup admin endpoint and a modal dialog on the server config node: check status, unlock an existing secure backup with its recovery key, or reset and create a new one. Device verification (new nodes) - matrix-verification: event source emitting verification requests and phase changes, with on-node filters (phase, initiated by, type, self-verification, user allowlist, room). - matrix-verification-action: request, accept, start SAS, confirm, mismatch, or cancel an in-flight verification. Authenticated media - matrix-receive and matrix-crypt-file use the authenticated media endpoints, send a bearer token via msg.headers, and fall back between the v3 and v1 media endpoints on a 404. Fixes - Surface connection/auth errors in the log; node.error() calls were passed an empty msg object, which routed the error and suppressed console logging. - matrix-get-user: await getProfileInfo()/getPresence(). - matrix-invite-room: pass the reason as the third invite() argument (the removed callback parameter was shifting it out). - Guard the verification handlers so a throwing SDK getter cannot crash Node-RED. Docs - Add the device-verification example flow; update the READMEs and node help, correcting stale claims that device verification, secure backup, and encrypted file uploads were unsupported. --- README.md | 19 +- examples/README.md | 21 + examples/device-verification-flow.json | 548 ++++++++++++ examples/device-verification-flow.png | Bin 0 -> 142087 bytes package-lock.json | 195 ++--- package.json | 15 +- src/matrix-crypt-file.html | 12 +- src/matrix-crypt-file.js | 55 +- src/matrix-crypto-store.js | 175 ++++ src/matrix-get-user.js | 8 +- src/matrix-invite-room.js | 5 +- src/matrix-receive.html | 7 +- src/matrix-receive.js | 36 +- src/matrix-server-config.html | 329 ++++++-- src/matrix-server-config.js | 1062 +++++++++++++++++------- src/matrix-verification-action.html | 126 +++ src/matrix-verification-action.js | 139 ++++ src/matrix-verification.html | 199 +++++ src/matrix-verification.js | 113 +++ 19 files changed, 2528 insertions(+), 536 deletions(-) create mode 100644 examples/device-verification-flow.json create mode 100644 examples/device-verification-flow.png create mode 100644 src/matrix-crypto-store.js create mode 100644 src/matrix-verification-action.html create mode 100644 src/matrix-verification-action.js create mode 100644 src/matrix-verification.html create mode 100644 src/matrix-verification.js diff --git a/README.md b/README.md index 55e0f04..04d8b44 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech Supported functionality in this package includes: -- **End-to-end encryption (E2EE)** - - [Work in progress](#end-to-end-encryption-notes) - - Alternative: Use [Pantalaimon](https://github.com/matrix-org/pantalaimon) for E2EE key synchronization across sessions +- **End-to-end encryption (E2EE)** — send and receive encrypted messages (see the [encryption notes](#end-to-end-encryption-notes)) +- **Cross-signing & secure backup** — interactive setup from the server config node so the bot's own device shows as verified +- **Device verification** — flow-driven SAS (emoji) verification via the `matrix-verification` and `matrix-verification-action` nodes - **Receive events** from rooms: Messages, reactions, images, audio, locations, files, encrypted or unencrypted - **Fetch/modify room state**: Update room settings - **Paginate room history** -- **Send files** (encryption support for files coming soon) +- **Send files** to rooms, encrypted or unencrypted - **Send/edit messages** (supports plain text and HTML formats) - **Send typing notifications** - **Delete events** (messages, reactions, etc.) @@ -33,6 +33,8 @@ These features allow you to easily build bots, set up chat relays, or even admin ### Installing +**Requires Node.js 22 or newer** (this is a requirement of the bundled `matrix-js-sdk`). + Install through Node-RED's UI by searching for `node-red-contrib-matrix-chat`, or use the following command inside your Node-RED directory: ```bash @@ -51,11 +53,12 @@ You're not limited to just the nodes we've created. Enable global access in your ### End-to-End Encryption Notes -- This module doesn't handle encryption key synchronization between devices. It’s recommended to use the bot exclusively in Node-RED to prevent issues with E2EE messages. -- **Storage:** Keys for E2EE are saved in a folder called `matrix-client-storage` within your Node-RED directory. Back up this folder regularly! If lost, you won’t be able to decrypt messages from E2EE rooms. +- E2EE uses the Rust crypto stack from `matrix-js-sdk`. The first time a bot starts after upgrading from an older version, any existing (legacy libolm) crypto state is migrated automatically. +- **Storage:** E2EE state is saved in a folder called `matrix-client-storage` within your Node-RED directory. Each account's Rust crypto store is persisted there as `rust-crypto-store.v8` (snapshotted on shutdown and every 5 minutes). Back up this folder regularly! If lost, you won’t be able to decrypt messages from E2EE rooms. - To move your bot to a different installation, migrate this folder and ensure the old and new clients don't run simultaneously. - -Interested in helping? Contributions to finalize E2EE support are welcome! +- It’s simplest to dedicate the account to the bot and run it only within Node-RED. The account can also be signed in elsewhere — if so, verify those sessions against the bot (see below) so they trust each other and share keys. +- **Cross-signing & secure backup:** open the server config node and use the **Set up secure backup & cross-signing** button. It checks the account and lets you unlock an existing secure backup with its recovery key, or create a fresh one — after which the bot's own device is cross-signed and shows as verified to others. +- **Device verification:** the `matrix-verification` node emits verification requests and phase changes, and `matrix-verification-action` accepts, starts, confirms, or cancels them — so you can build your own approval flow (e.g. emailing the SAS emoji for a human to confirm). See the [device verification example](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#device-verification). ### Registering a User diff --git a/examples/README.md b/examples/README.md index 8521c45..223d3d0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -431,6 +431,27 @@ Downloads received files/images. If the file is encrypted, it will decrypt it fo +### Device Verification + +
+Handle device verification (SAS / emoji) + +[View JSON](device-verification-flow.json) + +An end-to-end example of interactive device verification. The `matrix-verification` node emits every verification request and phase change; the flow routes by phase, automatically **accepts** incoming requests and **starts SAS**, then surfaces the SAS emoji so a human can compare it. Inject nodes let you **confirm** or **reject** the match, and there are paths to have the bot **request** verification of a specific user's device, or a user in a room. + +Requires end-to-end encryption to be enabled on the server config node. For the bot's own device to be trusted by others, also set up cross-signing via the **Set up secure backup & cross-signing** button on the server config node. + +**Instructions:** + +1. Import the flow and set the Matrix server config on each matrix node. +2. Replace the `@CHANGE_ME:example.org` / `CHANGE_ME` placeholders in the "Verify a user" inject nodes if you want to use the bot-initiated paths. +3. To verify the bot from another client, start a verification with it, watch the debug sidebar for the `sas` event, compare the emoji, then click the **Confirm SAS match** inject. + +![device-verification-flow.png](device-verification-flow.png) + +
+ ### Deprecated
diff --git a/examples/device-verification-flow.json b/examples/device-verification-flow.json new file mode 100644 index 0000000..e916ed2 --- /dev/null +++ b/examples/device-verification-flow.json @@ -0,0 +1,548 @@ +[ + { + "id": "7158964bd67edc52", + "type": "group", + "z": "vtest", + "name": "Example verification flow", + "style": { + "label": true + }, + "nodes": [ + "40c105c38054d6db", + "83f785d52a61009a", + "d51bab8cbf5f247c", + "2e543533d49b467c" + ], + "x": 88, + "y": 73, + "w": 1044, + "h": 754 + }, + { + "id": "40c105c38054d6db", + "type": "group", + "z": "vtest", + "g": "7158964bd67edc52", + "name": "Verification request handling", + "style": { + "label": true + }, + "nodes": [ + "mv_all", + "dbg_events", + "sw_phase", + "act_accept", + "act_start", + "chg_savevid", + "dbg_sas", + "dbg_done", + "dbg_cancelled", + "dbg_err" + ], + "x": 114, + "y": 99, + "w": 992, + "h": 342 + }, + { + "id": "mv_all", + "type": "matrix-verification", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "All verifications", + "server": null, + "phaseRequested": true, + "phaseReady": true, + "phaseStarted": true, + "phaseSas": true, + "phaseDone": true, + "phaseCancelled": true, + "initiatedBy": "any", + "verificationType": "any", + "selfVerification": "any", + "userFilter": "", + "roomFilter": "", + "x": 220, + "y": 180, + "wires": [ + [ + "dbg_events", + "sw_phase" + ] + ] + }, + { + "id": "dbg_events", + "type": "debug", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "all verification events", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 500, + "y": 140, + "wires": [] + }, + { + "id": "sw_phase", + "type": "switch", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "route by phase", + "property": "phase", + "propertyType": "msg", + "rules": [ + { + "t": "eq", + "v": "requested", + "vt": "str" + }, + { + "t": "eq", + "v": "ready", + "vt": "str" + }, + { + "t": "eq", + "v": "sas", + "vt": "str" + }, + { + "t": "eq", + "v": "done", + "vt": "str" + }, + { + "t": "eq", + "v": "cancelled", + "vt": "str" + } + ], + "checkall": "true", + "outputs": 5, + "x": 460, + "y": 220, + "wires": [ + [ + "act_accept" + ], + [ + "act_start" + ], + [ + "chg_savevid" + ], + [ + "dbg_done" + ], + [ + "dbg_cancelled" + ] + ] + }, + { + "id": "act_accept", + "type": "matrix-verification-action", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "Accept", + "server": null, + "mode": "accept", + "x": 700, + "y": 180, + "wires": [ + [], + [ + "dbg_err" + ] + ] + }, + { + "id": "act_start", + "type": "matrix-verification-action", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "Start SAS", + "server": null, + "mode": "start", + "x": 700, + "y": 230, + "wires": [ + [], + [ + "dbg_err" + ] + ] + }, + { + "id": "chg_savevid", + "type": "change", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "save verificationId", + "rules": [ + { + "t": "set", + "p": "verificationId", + "pt": "flow", + "to": "verificationId", + "tot": "msg" + } + ], + "x": 710, + "y": 290, + "wires": [ + [ + "dbg_sas" + ] + ] + }, + { + "id": "dbg_sas", + "type": "debug", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "SAS emoji (msg.sas)", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 960, + "y": 290, + "wires": [] + }, + { + "id": "dbg_done", + "type": "debug", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "verification done", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 710, + "y": 350, + "wires": [] + }, + { + "id": "dbg_cancelled", + "type": "debug", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "verification cancelled", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 730, + "y": 400, + "wires": [] + }, + { + "id": "dbg_err", + "type": "debug", + "z": "vtest", + "g": "40c105c38054d6db", + "name": "action errors", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 940, + "y": 200, + "wires": [] + }, + { + "id": "83f785d52a61009a", + "type": "group", + "z": "vtest", + "g": "7158964bd67edc52", + "name": "Confirm or reject last verification request", + "style": { + "label": true + }, + "nodes": [ + "inj_confirm", + "chg_vid_c", + "act_confirm", + "inj_reject", + "chg_vid_r", + "act_mismatch", + "dbg_result" + ], + "x": 114, + "y": 459, + "w": 982, + "h": 142 + }, + { + "id": "inj_confirm", + "type": "inject", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "Confirm SAS match", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 250, + "y": 500, + "wires": [ + [ + "chg_vid_c" + ] + ] + }, + { + "id": "chg_vid_c", + "type": "change", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "verificationId from flow", + "rules": [ + { + "t": "set", + "p": "verificationId", + "pt": "msg", + "to": "verificationId", + "tot": "flow" + } + ], + "x": 500, + "y": 500, + "wires": [ + [ + "act_confirm" + ] + ] + }, + { + "id": "act_confirm", + "type": "matrix-verification-action", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "Confirm SAS", + "server": null, + "mode": "confirm", + "x": 750, + "y": 500, + "wires": [ + [ + "dbg_result" + ], + [ + "dbg_err" + ] + ] + }, + { + "id": "inj_reject", + "type": "inject", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "Reject SAS (mismatch)", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 260, + "y": 560, + "wires": [ + [ + "chg_vid_r" + ] + ] + }, + { + "id": "chg_vid_r", + "type": "change", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "verificationId from flow", + "rules": [ + { + "t": "set", + "p": "verificationId", + "pt": "msg", + "to": "verificationId", + "tot": "flow" + } + ], + "x": 500, + "y": 560, + "wires": [ + [ + "act_mismatch" + ] + ] + }, + { + "id": "act_mismatch", + "type": "matrix-verification-action", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "Reject (mismatch)", + "server": null, + "mode": "mismatch", + "x": 760, + "y": 560, + "wires": [ + [ + "dbg_result" + ], + [ + "dbg_err" + ] + ] + }, + { + "id": "dbg_result", + "type": "debug", + "z": "vtest", + "g": "83f785d52a61009a", + "name": "action result", + "active": true, + "tosidebar": true, + "complete": "true", + "x": 980, + "y": 530, + "wires": [] + }, + { + "id": "d51bab8cbf5f247c", + "type": "group", + "z": "vtest", + "g": "7158964bd67edc52", + "name": "Request verification with specific user & device", + "style": { + "label": true + }, + "nodes": [ + "inj_request", + "act_request" + ], + "x": 114, + "y": 619, + "w": 512, + "h": 82 + }, + { + "id": "inj_request", + "type": "inject", + "z": "vtest", + "g": "d51bab8cbf5f247c", + "name": "Verify a user & device", + "props": [ + { + "p": "userId", + "v": "@CHANGE_ME:example.org", + "vt": "str" + }, + { + "p": "deviceId", + "v": "CHANGE_ME", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "", + "x": 260, + "y": 660, + "wires": [ + [ + "act_request" + ] + ] + }, + { + "id": "act_request", + "type": "matrix-verification-action", + "z": "vtest", + "g": "d51bab8cbf5f247c", + "name": "Request verification", + "server": null, + "mode": "request", + "x": 510, + "y": 660, + "wires": [ + [ + "dbg_result" + ], + [ + "dbg_err" + ] + ] + }, + { + "id": "2e543533d49b467c", + "type": "group", + "z": "vtest", + "g": "7158964bd67edc52", + "name": "Request verification with specific user & room", + "style": { + "label": true + }, + "nodes": [ + "f7c043d39780b9a4", + "b2807fd5125b56b4" + ], + "x": 114, + "y": 719, + "w": 512, + "h": 82 + }, + { + "id": "f7c043d39780b9a4", + "type": "inject", + "z": "vtest", + "g": "2e543533d49b467c", + "name": "Verify a user & room", + "props": [ + { + "p": "userId", + "v": "@CHANGE_ME:example.org", + "vt": "str" + }, + { + "p": "topic", + "vt": "str" + } + ], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "topic": "CHANGE_ME", + "x": 250, + "y": 760, + "wires": [ + [ + "b2807fd5125b56b4" + ] + ] + }, + { + "id": "b2807fd5125b56b4", + "type": "matrix-verification-action", + "z": "vtest", + "g": "2e543533d49b467c", + "name": "Request verification", + "server": null, + "mode": "request", + "x": 510, + "y": 760, + "wires": [ + [ + "dbg_result" + ], + [ + "dbg_err" + ] + ] + } +] \ No newline at end of file diff --git a/examples/device-verification-flow.png b/examples/device-verification-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..b167c92601637329058602cbf773ce962848e093 GIT binary patch literal 142087 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

LNvfJ=H15${E>&JD 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

QD_q>sCk1;1-O)qX|hj#j+wf6lRI( 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 C10-w! literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 198204c..4a18ee0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,22 +10,22 @@ "license": "SEE LICENSE FILE", "dependencies": { "abort-controller": "^3.0.0", + "fake-indexeddb": "^6.2.5", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^11.1.0", "got": "^12.0.2", "image-size": "^1.0.2", "isomorphic-webcrypto": "^2.3.8", - "matrix-js-sdk": "^34.13.0", + "matrix-js-sdk": "^41.5.0", "mime": "^3.0.0", "node-fetch": "^3.3.0", "node-localstorage": "^2.2.1", - "olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download", "sharp": "^0.33.4", "tmp": "^0.2.1", "utf8": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" } }, "node_modules/@ampproject/remapping": { @@ -4188,20 +4188,14 @@ } }, "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz", - "integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==", + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", + "integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==", "license": "Apache-2.0", "engines": { - "node": ">= 10" + "node": ">= 18" } }, - "node_modules/@matrix-org/olm": { - "version": "3.2.15", - "resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz", - "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==", - "license": "Apache-2.0" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6037,11 +6031,6 @@ "optional": true, "peer": true }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -8288,6 +8277,15 @@ } } }, + "node_modules/fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -9337,6 +9335,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10717,42 +10727,27 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "node_modules/matrix-js-sdk": { - "version": "34.13.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.13.0.tgz", - "integrity": "sha512-AAU8ZdCawca+7ucQfdcC3LA85OtCTV7QeqcjvKt/ZZhU3xL9VoawuoRQ+4R6H8KZnqyJmT4j7bdeC0jG4qcqLg==", + "version": "41.5.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz", + "integrity": "sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", - "@matrix-org/olm": "3.2.15", + "@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0", "another-json": "^0.2.0", "bs58": "^6.0.0", "content-type": "^1.0.4", "jwt-decode": "^4.0.0", - "loglevel": "^1.7.1", + "loglevel": "^1.9.2", "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.10.0", + "matrix-widget-api": "^1.16.1", "oidc-client-ts": "^3.0.1", - "p-retry": "4", - "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6", - "uuid": "11" + "p-retry": "8", + "sdp-transform": "^3.0.0", + "unhomoglyph": "^1.0.6" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/matrix-js-sdk/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" + "node": ">=22.0.0" } }, "node_modules/matrix-widget-api": { @@ -12298,13 +12293,6 @@ "node": ">=18" } }, - "node_modules/olm": { - "name": "@matrix-org/olm", - "version": "3.2.15", - "resolved": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download", - "integrity": "sha512-5j5CLplrEodeqGMHEzwDZz6UlqWHyR8c1+tCpCj/yrIJ3V4kYbQVySUhrPNIz2k6j7Ief8GiNxxArjXfuGLfgg==", - "license": "Apache-2.0" - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -12559,15 +12547,18 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", + "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", + "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "is-network-error": "^1.3.0" }, "engines": { - "node": ">=8" + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -13680,14 +13671,6 @@ "node": ">=4" } }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -13773,9 +13756,10 @@ } }, "node_modules/sdp-transform": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", - "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", + "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==", + "license": "MIT", "bin": { "sdp-verify": "checker.js" } @@ -18417,14 +18401,9 @@ } }, "@matrix-org/matrix-sdk-crypto-wasm": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz", - "integrity": "sha512-CtPoNcoRW6ehwxpRQAksG3tR+NJ7k4DV02nMFYTDwQtie1V4R8OTY77BjEIs97NOblhtS26jU8m1lWsOBEz0Og==" - }, - "@matrix-org/olm": { - "version": "3.2.15", - "resolved": "https://registry.npmjs.org/@matrix-org/olm/-/olm-3.2.15.tgz", - "integrity": "sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q==" + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", + "integrity": "sha512-9a4feyt8QLysARu7PHKaRWT+wcCd+IYH074LXp9QK5WqfN4zUXueRhiSSMNT18Bm+8q3sBR/4zxDxOSDR0M8Kg==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -19841,11 +19820,6 @@ "optional": true, "peer": true }, - "@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" - }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -21631,6 +21605,11 @@ "base64-js": "^1.3.0" } }, + "fake-indexeddb": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", + "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==" + }, "fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -22427,6 +22406,11 @@ } } }, + "is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==" + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -23483,32 +23467,23 @@ "integrity": "sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==" }, "matrix-js-sdk": { - "version": "34.13.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-34.13.0.tgz", - "integrity": "sha512-AAU8ZdCawca+7ucQfdcC3LA85OtCTV7QeqcjvKt/ZZhU3xL9VoawuoRQ+4R6H8KZnqyJmT4j7bdeC0jG4qcqLg==", + "version": "41.5.0", + "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-41.5.0.tgz", + "integrity": "sha512-CK3h+qQJ4wkVEUgEWc5MdLjccXyiFqncCC53P+auqOhnX2U6tAFsRfnbML1QQiKIsFMzqTrAnF/4a5LUUOIeXg==", "requires": { "@babel/runtime": "^7.12.5", - "@matrix-org/matrix-sdk-crypto-wasm": "^9.0.0", - "@matrix-org/olm": "3.2.15", + "@matrix-org/matrix-sdk-crypto-wasm": "^18.2.0", "another-json": "^0.2.0", "bs58": "^6.0.0", "content-type": "^1.0.4", "jwt-decode": "^4.0.0", - "loglevel": "^1.7.1", + "loglevel": "^1.9.2", "matrix-events-sdk": "0.0.1", - "matrix-widget-api": "^1.10.0", + "matrix-widget-api": "^1.16.1", "oidc-client-ts": "^3.0.1", - "p-retry": "4", - "sdp-transform": "^2.14.1", - "unhomoglyph": "^1.0.6", - "uuid": "11" - }, - "dependencies": { - "uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==" - } + "p-retry": "8", + "sdp-transform": "^3.0.0", + "unhomoglyph": "^1.0.6" } }, "matrix-widget-api": { @@ -24730,10 +24705,6 @@ "jwt-decode": "^4.0.0" } }, - "olm": { - "version": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download", - "integrity": "sha512-5j5CLplrEodeqGMHEzwDZz6UlqWHyR8c1+tCpCj/yrIJ3V4kYbQVySUhrPNIz2k6j7Ief8GiNxxArjXfuGLfgg==" - }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -24927,12 +24898,11 @@ } }, "p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-8.0.0.tgz", + "integrity": "sha512-kFVqH1HxOHp8LupNsOys7bSV09VYTRLxarH/mokO4Rqhk6wGi70E0jh4VzvVGXfEVNggHoHLAMWsQqHyU1Ey9A==", "requires": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "is-network-error": "^1.3.0" } }, "p-try": { @@ -25804,11 +25774,6 @@ "signal-exit": "^3.0.2" } }, - "retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -25873,9 +25838,9 @@ } }, "sdp-transform": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.15.0.tgz", - "integrity": "sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-3.0.0.tgz", + "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==" }, "semver": { "version": "7.6.2", diff --git a/package.json b/package.json index 527815e..8bb2505 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,16 @@ "description": "Matrix chat server client for Node-RED", "dependencies": { "abort-controller": "^3.0.0", + "fake-indexeddb": "^6.2.5", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^11.1.0", "got": "^12.0.2", "image-size": "^1.0.2", "isomorphic-webcrypto": "^2.3.8", - "matrix-js-sdk": "^34.13.0", + "matrix-js-sdk": "^41.5.0", "mime": "^3.0.0", "node-fetch": "^3.3.0", "node-localstorage": "^2.2.1", - "olm": "https://gitlab.matrix.org/matrix-org/olm/-/package_files/2572/download", "sharp": "^0.33.4", "tmp": "^0.2.1", "utf8": "^3.0.0" @@ -51,11 +51,13 @@ "matrix-whois-user": "src/matrix-whois-user.js", "matrix-paginate-room": "src/matrix-paginate-room.js", "matrix-get-event": "src/matrix-get-event.js", - "matrix-event-relations": "src/matrix-event-relations.js" + "matrix-event-relations": "src/matrix-event-relations.js", + "matrix-verification": "src/matrix-verification.js", + "matrix-verification-action": "src/matrix-verification-action.js" } }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "keywords": [ "node-red", @@ -72,8 +74,5 @@ "name": "Skylar Sadlier", "url": "https://skylar.tech" }, - "license": "SEE LICENSE FILE", - "engines": { - "node": ">=18.0.0" - } + "license": "SEE LICENSE FILE" } diff --git a/src/matrix-crypt-file.html b/src/matrix-crypt-file.html index ae73715..65a7a3b 100644 --- a/src/matrix-crypt-file.html +++ b/src/matrix-crypt-file.html @@ -43,6 +43,16 @@ string | null

the decoded mxc url.
+ +
msg.headers + object +
+
optional HTTP headers. If provided, they are used when downloading media (for example authenticated media bearer tokens).
+ +
msg.access_token + string +
+
optional Matrix access token. Used as a bearer token if msg.headers.Authorization is not present.

Outputs

@@ -74,4 +84,4 @@
- \ No newline at end of file + diff --git a/src/matrix-crypt-file.js b/src/matrix-crypt-file.js index a6236cf..c15f6db 100644 --- a/src/matrix-crypt-file.js +++ b/src/matrix-crypt-file.js @@ -32,7 +32,9 @@ module.exports = function(RED) { } try{ - let buffer = await got(msg.url).buffer(); + const requestOptions = getRequestOptions(msg); + + let buffer = await downloadBufferWithFallback(got, msg.url, requestOptions); msg.payload = Buffer.from(await decryptAttachment(buffer, msg.content.file)); // handle thumbnail decryption if necessary @@ -41,13 +43,14 @@ module.exports = function(RED) { && msg.thumbnail_url && msg.content.info.thumbnail_file ) { - let thumb_buffer = await got(msg.thumbnail_url).buffer(); + let thumb_buffer = await downloadBufferWithFallback(got, msg.thumbnail_url, requestOptions); msg.thumbnail_payload = Buffer.from(await decryptAttachment(thumb_buffer, msg.content.info.thumbnail_file)); } } catch(error){ node.error(error); msg.error = error; node.send([null, msg]); + return; } msg.filename = msg.content.filename || msg.content.body; @@ -57,6 +60,52 @@ module.exports = function(RED) { } RED.nodes.registerType("matrix-decrypt-file", MatrixDecryptFile); + function getRequestOptions(msg) { + const headers = { ...(msg.headers || {}) }; + if (!headers.Authorization && msg.access_token) { + headers.Authorization = `Bearer ${msg.access_token}`; + } + + return Object.keys(headers).length ? { headers } : {}; + } + + function getMediaEndpointFallbackUrl(url) { + if (typeof url !== "string") { + return null; + } + + if (url.includes("/_matrix/media/v3/download/")) { + return url.replace("/_matrix/media/v3/download/", "/_matrix/client/v1/media/download/"); + } + + if (url.includes("/_matrix/client/v1/media/download/")) { + return url.replace("/_matrix/client/v1/media/download/", "/_matrix/media/v3/download/"); + } + + if (url.includes("/_matrix/media/v3/thumbnail/")) { + return url.replace("/_matrix/media/v3/thumbnail/", "/_matrix/client/v1/media/thumbnail/"); + } + + if (url.includes("/_matrix/client/v1/media/thumbnail/")) { + return url.replace("/_matrix/client/v1/media/thumbnail/", "/_matrix/media/v3/thumbnail/"); + } + + return null; + } + + async function downloadBufferWithFallback(got, url, requestOptions) { + try { + return await got(url, requestOptions).buffer(); + } catch (error) { + const fallbackUrl = getMediaEndpointFallbackUrl(url); + if (error?.response?.statusCode === 404 && fallbackUrl && fallbackUrl !== url) { + return await got(fallbackUrl, requestOptions).buffer(); + } + + throw error; + } + } + function atob(a) { return Buffer.from(a, 'base64').toString('binary'); } @@ -200,4 +249,4 @@ module.exports = function(RED) { } return uint8Array; } -} \ No newline at end of file +} diff --git a/src/matrix-crypto-store.js b/src/matrix-crypto-store.js new file mode 100644 index 0000000..eb74cf3 --- /dev/null +++ b/src/matrix-crypto-store.js @@ -0,0 +1,175 @@ +/** + * Persistence helpers for the matrix-js-sdk Rust crypto store in Node.js. + * + * matrix-js-sdk v37+ removed the legacy (libolm) crypto stack. The Rust crypto + * replacement persists its state (device identity, Olm/megolm sessions, etc.) + * to IndexedDB, which does not exist in Node.js. We provide an in-memory + * IndexedDB via `fake-indexeddb` and snapshot the databases to/from disk so the + * crypto state survives Node-RED restarts. + * + * The `indexeddbshim` package (which can persist to disk directly) is not used + * because it is incompatible with the Rust crypto store migrations + * (see matrix-org/matrix-sdk-crypto-wasm#195). `fake-indexeddb` is spec + * compliant, so snapshotting it through the public IndexedDB API is reliable. + */ +const fs = require('fs-extra'); +const v8 = require('v8'); + +let shimInstalled = false; + +/** + * Install the in-memory IndexedDB shim onto globalThis. Idempotent. Must be + * called before MatrixClient.initRustCrypto(). + */ +function ensureIndexedDBShim() { + if (shimInstalled || globalThis.indexedDB) { + shimInstalled = true; + return; + } + // `fake-indexeddb/auto` assigns indexedDB / IDBKeyRange / etc. onto globalThis. + require('fake-indexeddb/auto'); + shimInstalled = true; +} + +function reqAsync(req) { + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); +} + +function txDone(tx) { + return new Promise((resolve, reject) => { + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + tx.onabort = () => reject(tx.error || new Error('IndexedDB transaction aborted')); + }); +} + +/** + * Restore previously snapshotted IndexedDB databases from `filePath` into the + * in-memory store. No-op if the snapshot does not exist. Databases that are + * already present in memory (e.g. after a Node-RED redeploy that kept the + * process alive) are left untouched so the live state is not clobbered. + * + * Must be called before MatrixClient.initRustCrypto(). + * + * @returns {Promise} true if at least one database was restored. + */ +async function restoreCryptoStore(filePath) { + ensureIndexedDBShim(); + + if (!filePath || !fs.pathExistsSync(filePath)) { + return false; + } + + let databases; + try { + databases = v8.deserialize(fs.readFileSync(filePath)); + } catch (e) { + // Corrupt/unreadable snapshot - start fresh rather than crash. + return false; + } + if (!Array.isArray(databases) || !databases.length) { + return false; + } + + const existing = new Set((await indexedDB.databases()).map((d) => d.name)); + let restored = 0; + + for (const dbSpec of databases) { + if (existing.has(dbSpec.name)) { + continue; // already live in memory - don't overwrite + } + + const openReq = indexedDB.open(dbSpec.name, dbSpec.version); + openReq.onupgradeneeded = () => { + const db = openReq.result; + for (const store of dbSpec.stores) { + if (db.objectStoreNames.contains(store.name)) { + continue; + } + const os = db.createObjectStore(store.name, { + keyPath: store.keyPath || undefined, + autoIncrement: store.autoIncrement, + }); + for (const ix of store.indexes) { + os.createIndex(ix.name, ix.keyPath, { unique: ix.unique, multiEntry: ix.multiEntry }); + } + } + }; + const db = await reqAsync(openReq); + + for (const store of dbSpec.stores) { + if (!store.values.length) { + continue; + } + const tx = db.transaction(store.name, 'readwrite'); + const os = tx.objectStore(store.name); + for (let i = 0; i < store.values.length; i++) { + if (store.keyPath) { + os.put(store.values[i]); + } else { + os.put(store.values[i], store.keys[i]); + } + } + await txDone(tx); + } + db.close(); + restored++; + } + + return restored > 0; +} + +/** + * Snapshot IndexedDB databases to `filePath`. If `dbNamePrefix` is given only + * databases whose name starts with it are written, so multiple Matrix accounts + * sharing one process do not snapshot each other's data. + * + * The write is atomic (temp file + rename). Values are serialized with the V8 + * serializer so typed arrays / Maps inside the crypto store survive intact. + * + * @returns {Promise} true if a snapshot file was written. + */ +async function snapshotCryptoStore(filePath, dbNamePrefix) { + if (!filePath || !globalThis.indexedDB || typeof indexedDB.databases !== 'function') { + return false; + } + + let dbList = await indexedDB.databases(); + if (dbNamePrefix) { + dbList = dbList.filter((d) => typeof d.name === 'string' && d.name.startsWith(dbNamePrefix)); + } + + const out = []; + for (const { name, version } of dbList) { + const db = await reqAsync(indexedDB.open(name, version)); + const stores = []; + for (const storeName of Array.from(db.objectStoreNames)) { + const tx = db.transaction(storeName, 'readonly'); + const os = tx.objectStore(storeName); + const indexes = Array.from(os.indexNames).map((n) => { + const ix = os.index(n); + return { name: n, keyPath: ix.keyPath, unique: ix.unique, multiEntry: ix.multiEntry }; + }); + stores.push({ + name: storeName, + keyPath: os.keyPath, + autoIncrement: os.autoIncrement, + indexes, + values: await reqAsync(os.getAll()), + keys: await reqAsync(os.getAllKeys()), + }); + } + db.close(); + out.push({ name, version, stores }); + } + + const tmp = `${filePath}.tmp`; + fs.writeFileSync(tmp, v8.serialize(out)); + fs.renameSync(tmp, filePath); + return true; +} + +module.exports = { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore }; diff --git a/src/matrix-get-user.js b/src/matrix-get-user.js index 9afa8fc..0f8b595 100644 --- a/src/matrix-get-user.js +++ b/src/matrix-get-user.js @@ -106,14 +106,14 @@ module.exports = function(RED) { let user2 = {}; try { - let profileInfo = node.server.matrixClient.getProfileInfo(userId); - if(Object.keys(profileInfo).length > 0) { + let profileInfo = await node.server.matrixClient.getProfileInfo(userId); + if(profileInfo && Object.keys(profileInfo).length > 0) { user2.displayName = profileInfo.displayname; user2.avatarUrl = profileInfo.avatar_url; } - let presence = node.server.matrixClient.getPresence(userId); - if(Object.keys(presence).length > 0) { + let presence = await node.server.matrixClient.getPresence(userId); + if(presence && Object.keys(presence).length > 0) { user2.currentlyActive = presence.currently_active; user2.lastActiveAgo = presence.last_active_ago; user2.presenceStatusMsg = presence.presence_status_msg; diff --git a/src/matrix-invite-room.js b/src/matrix-invite-room.js index 61c7589..22a7f92 100644 --- a/src/matrix-invite-room.js +++ b/src/matrix-invite-room.js @@ -54,9 +54,10 @@ module.exports = function(RED) { return; } - // we need the status code, so set onlydata to false for this request + // invite(roomId, userId, opts|reason) - the SDK no longer accepts a + // callback argument, so the reason is passed as the 3rd parameter. node.server.matrixClient - .invite(msg.topic, msg.userId, undefined, msg.reason || undefined) + .invite(msg.topic, msg.userId, msg.reason || undefined) .then(function(e){ msg.payload = e; node.send([msg, null]); diff --git a/src/matrix-receive.html b/src/matrix-receive.html index 3274c4a..bfe4a50 100644 --- a/src/matrix-receive.html +++ b/src/matrix-receive.html @@ -227,6 +227,11 @@
msg.content object
the message's content object
+ +
+
msg.headers object | null
+
for media events, includes auth headers (for example Authorization: Bearer ...) used by authed media endpoints.
+
  • msg.type == 'm.text' @@ -358,4 +363,4 @@
  • - \ No newline at end of file + diff --git a/src/matrix-receive.js b/src/matrix-receive.js index 77059c8..45f1e76 100644 --- a/src/matrix-receive.js +++ b/src/matrix-receive.js @@ -47,11 +47,31 @@ module.exports = function(RED) { return; } + const setAuthHeaders = () => { + const accessToken = node.server.matrixClient.getAccessToken?.(); + if (accessToken) { + msg.headers = { + ...(msg.headers || {}), + Authorization: `Bearer ${accessToken}`, + }; + } + }; + const setUrls = (urlKey, encryptedKey) => { const url = msg.encrypted ? msg.content[encryptedKey]?.url : msg.content[urlKey]; if (url) { - msg.url = node.server.matrixClient.mxcUrlToHttp(url); + const authenticatedUrl = node.server.matrixClient.mxcUrlToHttp( + url, + undefined, + undefined, + undefined, + false, + true, + true, + ); + msg.url = authenticatedUrl || node.server.matrixClient.mxcUrlToHttp(url); msg.mxc_url = url; + setAuthHeaders(); } }; @@ -59,8 +79,18 @@ module.exports = function(RED) { const thumbnailFile = msg.content.info?.[infoKey]; const thumbnailUrl = thumbnailFile?.url; if (thumbnailUrl) { - msg.thumbnail_url = node.server.matrixClient.mxcUrlToHttp(thumbnailUrl); + const authenticatedThumbnailUrl = node.server.matrixClient.mxcUrlToHttp( + thumbnailUrl, + undefined, + undefined, + undefined, + false, + true, + true, + ); + msg.thumbnail_url = authenticatedThumbnailUrl || node.server.matrixClient.mxcUrlToHttp(thumbnailUrl); msg.thumbnail_mxc_url = thumbnailUrl; + setAuthHeaders(); } }; @@ -141,4 +171,4 @@ module.exports = function(RED) { }); } RED.nodes.registerType("matrix-receive", MatrixReceiveMessage); -} \ No newline at end of file +} diff --git a/src/matrix-server-config.html b/src/matrix-server-config.html index 60db2f7..7d9a29b 100644 --- a/src/matrix-server-config.html +++ b/src/matrix-server-config.html @@ -31,17 +31,232 @@ accessToken: { type: "password", required: true }, deviceId: { type: "text", required: false }, url: { type: "text", required: true }, + password: { type: "password", required: false }, }, defaults: { name: { value: null }, autoAcceptRoomInvites: { value: true }, enableE2ee: { type: "checkbox", value: true }, global: { type: "checkbox", value: true }, - allowUnknownDevices: { type: "checkbox", value: false } + allowUnknownDevices: { type: "checkbox", value: true } }, icon: "matrix.png", label: function() { return this.name || undefined; + }, + oneditprepare: function() { + const nodeId = this.id; + + // --- Secure backup / cross-signing setup (modal dialog) --- + // The modal is built once and reused across editor sessions; the + // current node id is stored on the overlay so its handlers target + // whichever server config node is being edited. + if (!document.getElementById("matrix-sb-overlay")) { + $('').appendTo("head"); + + $('
    ' + + '
    Secure Backup & Cross-signing' + + '×
    ' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '
    ').appendTo(document.body); + + var sbEsc = function(s) { return $("
    ").text(s == null ? "" : String(s)).html(); }; + var sbClose = function() { $("#matrix-sb-overlay").fadeOut(120); }; + var sbId = function() { return $("#matrix-sb-overlay").data("matrixNodeId"); }; + var sbBtns = function(disabled) { $("#matrix-sb-unlock-btn,#matrix-sb-reset-btn").prop("disabled", disabled); }; + var sbCall = function(body) { + return $.ajax({ + url: "matrix-chat/secure-backup", type: "POST", + contentType: "application/json", data: JSON.stringify(body), + }); + }; + var sbState = function(icon, color, html) { + $("#matrix-sb-state").html('' + html + ''); + }; + var sbResult = function(ok, text, key) { + var h = sbEsc(text); + if (key) { h += '
    ' + sbEsc(key) + '
    '; } + $("#matrix-sb-result").removeClass("ok err").addClass(ok ? "ok" : "err").html(h).show(); + }; + var sbStatus = function() { + $("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-result,#matrix-sb-reset-toggle").hide(); + sbState("fa-spinner fa-spin", "#888", "Checking the account…"); + sbCall({ id: sbId(), action: "status" }).done(function(data) { + if (data.result !== "ok") { + sbState("fa-exclamation-triangle", "#c9302c", "Could not check the account."); + sbResult(false, data.message || "Unknown error"); + return; + } + if (data.crossSigningReady) { + sbState("fa-check-circle", "#3a9a4e", "Cross-signing is set up. The bot's device is cross-signed."); + $("#matrix-sb-reset-toggle").show(); + } else if (data.secretStorageExists) { + sbState("fa-lock", "#d18a1b", "This account has an existing secure backup. Enter its recovery key to set up cross-signing for the bot."); + $("#matrix-sb-recoverykey").val(""); + $("#matrix-sb-unlock,#matrix-sb-reset-toggle").show(); + } else { + sbState("fa-shield", "#888", "No secure backup exists yet. Set one up to enable cross-signing."); + $("#matrix-sb-password").val(""); + $("#matrix-sb-reset").show(); + } + sbBtns(false); + }).fail(function() { + sbState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?"); + }); + }; + + $("#matrix-sb-x,#matrix-sb-close").on("click", sbClose); + $("#matrix-sb-overlay").on("mousedown", function(e) { if (e.target === this) { sbClose(); } }); + $(document).on("keydown.matrixsb", function(e) { + if (e.key === "Escape" && $("#matrix-sb-overlay").is(":visible")) { sbClose(); } + }); + $("#matrix-sb-reset-toggle").on("click", function() { + $(this).hide(); + $("#matrix-sb-password").val(""); + $("#matrix-sb-reset").show(); + }); + $("#matrix-sb-unlock-btn").on("click", function() { + sbBtns(true); + sbState("fa-spinner fa-spin", "#888", "Unlocking secure backup…"); + sbCall({ id: sbId(), action: "unlock", recoveryKey: $("#matrix-sb-recoverykey").val() }) + .done(function(data) { + if (data.result !== "ok") { + sbState("fa-lock", "#d18a1b", "Enter the recovery key to set up cross-signing."); + sbResult(false, data.message); sbBtns(false); return; + } + $("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-reset-toggle").hide(); + sbState("fa-check-circle", "#3a9a4e", "Done."); + sbResult(true, data.message); + }) + .fail(function() { sbResult(false, "Request failed — is Node-RED still running?"); sbBtns(false); }); + }); + $("#matrix-sb-reset-btn").on("click", function() { + sbBtns(true); + sbState("fa-spinner fa-spin", "#888", "Resetting cross-signing & secure backup…"); + sbCall({ id: sbId(), action: "reset", password: $("#matrix-sb-password").val() }) + .done(function(data) { + if (data.result !== "ok") { + sbState("fa-shield", "#d18a1b", "Enter the account password to reset."); + sbResult(false, data.message); sbBtns(false); return; + } + $("#matrix-sb-unlock,#matrix-sb-reset,#matrix-sb-reset-toggle").hide(); + sbState("fa-check-circle", "#3a9a4e", "Reset complete."); + sbResult(true, data.message, data.recoveryKey); + }) + .fail(function() { sbResult(false, "Request failed — is Node-RED still running?"); sbBtns(false); }); + }); + + // expose the status loader so per-session click handlers can call it + $("#matrix-sb-overlay").data("sbStatusFn", sbStatus); + } + + $("#matrix-secure-backup-btn").on("click", function() { + $("#matrix-sb-overlay").data("matrixNodeId", nodeId).fadeIn(120); + $("#matrix-sb-overlay").data("sbStatusFn")(); + }); + + // --- Login: fetch a fresh access token & device id --- + $("#matrix-login-btn").on("click", function() { + function prettyPrintJson(json) { + try { return typeof json === 'object' ? JSON.stringify(json, null, 2) : json; } + catch (error) { return json; } + } + let userId = $("#node-config-input-userId").val(), + userPassword = $("#node-config-input-password").val(), + serverUrl = $("#node-config-input-url").val(); + if (!userId) { alert("User ID is required to fetch access token."); return; } + if (!userPassword) { alert("Password is required to fetch access token."); return; } + if (!serverUrl) { alert("Server URL is required to fetch access token."); return; } + + $("#matrix-login-btn, #matrix-chat-login-error, #matrix-chat-login-success").hide(); + $("#matrix-access-token-loader").show(); + $.ajax({ + type: 'POST', + url: 'matrix-chat/login', + dataType: 'json', + data: { + 'userId': userId, + 'password': userPassword, + 'baseUrl': serverUrl, + 'displayName': $("#node-config-input-deviceLabel").val(), + } + }).then( + function(data) { + if (data.result && data.result === 'ok') { + $("#matrix-chat-login-error").hide(); + $("#matrix-chat-login-success") + .html("Login Successful! Auth Token and Device ID have been set below.") + .show(); + $("#node-config-input-accessToken").val(data.token); + $("#node-config-input-deviceId").val(data.device_id); + } else if (data.result && data.result === 'error') { + $("#matrix-chat-login-success").hide(); + $("#matrix-chat-login-error") + .html(data.message ? ('Failed to login:
    ' + prettyPrintJson(data.message)) : 'Failed to login') + .show(); + } + $("#matrix-login-btn").show(); + $("#matrix-access-token-loader").hide(); + }, + function() { + $("#matrix-chat-login-success").hide(); + $("#matrix-chat-login-error") + .html("Failed to login due to server error communicating with Node-RED") + .show(); + $("#matrix-login-btn").show(); + $("#matrix-access-token-loader").hide(); + } + ); + }); } }); @@ -72,7 +287,10 @@
    - Password is never saved and is only used to fetch an access token using the button below. + Optional. Used to fetch an access token with the button below, and — if you + enable cross-signing — as a fallback when the homeserver requires the account + password to upload signing keys. If set, it is stored (encrypted) with the node's + credentials. Leave blank if you only want to use an access token.
    @@ -145,88 +363,55 @@ Allow sending messages to a room with unknown devices which have not been verified. - +
    + + +
    +
    + Sets up cross-signing so the bot's own device shows as verified. The server + configuration must be deployed and connected first. +
    diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index d69cd49..234cbcd 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -1,12 +1,14 @@ -const {RelationType, TimelineWindow} = require("matrix-js-sdk"); +// matrix-js-sdk is an ES module; load it via dynamic import so this CommonJS +// node keeps working. All SDK-dependent setup awaits this promise. +const sdkPromise = import("matrix-js-sdk"); +// The crypto-api enums (CryptoEvent, VerificationPhase, ...) are not re-exported +// from the package root, so they are imported from the crypto-api subpath. +const cryptoApiPromise = import("matrix-js-sdk/lib/crypto-api/index.js"); -global.Olm = require('olm'); const fs = require("fs-extra"); -const sdk = require("matrix-js-sdk"); const { resolve } = require('path'); const { LocalStorage } = require('node-localstorage'); -const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store'); -const {RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, MemoryStore} = require("matrix-js-sdk"); +const { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore } = require('./matrix-crypto-store'); require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it if (!globalThis.fetch) { // polyfill fetch if we don't have it @@ -17,6 +19,41 @@ if (!globalThis.fetch) { } } +/** + * Resolve the real homeserver base URL for a configured server name / URL. + * + * Uses matrix-js-sdk's built-in .well-known auto-discovery: given e.g. + * "https://example.org" it looks up https://example.org/.well-known/matrix/client + * and returns the homeserver it delegates to (e.g. https://matrix.example.org). + * If there is no .well-known delegation (or discovery fails), the original URL + * is returned unchanged, so explicitly-configured homeserver URLs still work. + */ +async function resolveHomeserverUrl(sdk, configuredUrl) { + if(!configuredUrl) { + return configuredUrl; + } + let domain; + try { + domain = new URL(configuredUrl).host; + } catch(e) { + // not a full URL - treat the value itself as a domain + domain = String(configuredUrl).replace(/^https?:\/\//i, '').replace(/\/.*$/, ''); + } + if(!domain) { + return configuredUrl; + } + try { + const discovery = await sdk.AutoDiscovery.findClientConfig(domain); + const homeserver = discovery['m.homeserver']; + if(homeserver && homeserver.state === sdk.AutoDiscovery.SUCCESS && homeserver.base_url) { + return homeserver.base_url; + } + } catch(e) { + // discovery failed unexpectedly - fall back to the configured URL + } + return configuredUrl; +} + module.exports = function(RED) { // disable logging if set to "off" let loggingSettings = RED.settings.get('logging'); @@ -25,8 +62,9 @@ module.exports = function(RED) { typeof loggingSettings.console.level !== 'undefined' && ['info','debug','trace'].indexOf(loggingSettings.console.level.toLowerCase()) >= 0 ) { - const { logger } = require('matrix-js-sdk/lib/logger'); - logger.disableAll(); + import('matrix-js-sdk/lib/logger.js') + .then(({ logger }) => logger.disableAll()) + .catch(() => { /* logger module path changed - ignore */ }); } function MatrixFolderNameFromUserId(name) { @@ -54,10 +92,25 @@ module.exports = function(RED) { this.url = this.credentials.url; this.autoAcceptRoomInvites = n.autoAcceptRoomInvites; this.e2ee = n.enableE2ee || false; + // Whether to send encrypted messages to devices that have not been + // verified. Undefined (config saved before this option existed) keeps + // the long-standing behaviour of allowing unverified devices. + this.allowUnknownDevices = n.allowUnknownDevices; + // Optional account password (used by the login helper, and as fallback + // user-interactive auth when resetting secure backup / cross-signing). + this.botPassword = this.credentials.password || null; this.globalAccess = n.global; this.initializedAt = new Date(); node.initialSyncLimit = 25; + // Live device-verification state, shared with the matrix-verification + // and matrix-verification-action nodes. Keyed by verification id. + node.verificationRequests = new Map(); // id -> VerificationRequest + node.verificationSas = new Map(); // id -> ShowSasCallbacks + // Cached Secure Secret Storage (4S) key as [keyId, Uint8Array], set by + // the /matrix-chat/secure-backup admin endpoint once unlocked. + node._secretStorageKeyCache = null; + // Keep track of all consumers of this node to be able to catch errors node.register = function(consumerNode) { node.users[consumerNode.id] = consumerNode; @@ -65,7 +118,7 @@ module.exports = function(RED) { node.deregister = function(consumerNode) { delete node.users[consumerNode.id]; }; - + if(!this.userId) { node.log("Matrix connection failed: missing user ID in configuration."); return; @@ -77,10 +130,17 @@ module.exports = function(RED) { let retryStartTimeout = null; + // Rust crypto persistence (see ./matrix-crypto-store.js). Each Matrix + // account gets its own IndexedDB name prefix and on-disk snapshot so + // multiple server-config nodes never collide. + let cryptoDbPrefix = 'mxjssdk-' + MatrixFolderNameFromUserId(this.userId), + cryptoSnapshotPath = null, + cryptoSnapshotInterval = null; + if(!this.credentials.accessToken) { - node.error("Matrix connection failed: missing access token in configuration.", {}); + node.error("Matrix connection failed: missing access token in configuration."); } else if(!this.url) { - node.error("Matrix connection failed: missing server URL in configuration.", {}); + node.error("Matrix connection failed: missing server URL in configuration."); } else { node.setConnected = async function(connected, cb) { if (node.connected !== connected) { @@ -98,7 +158,7 @@ module.exports = function(RED) { device_id = this.matrixClient.getDeviceId(); if(!device_id && node.enableE2ee) { - node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.", {}) + node.error("Failed to auto detect deviceId for this auth token. You will need to manually specify one. You may need to login to create a new deviceId.") } else { if(!stored_device_id || stored_device_id !== device_id) { node.log(`Saving Device ID (old:${stored_device_id} new:${device_id})`); @@ -117,13 +177,13 @@ module.exports = function(RED) { }).then( function(response) {}, function(error) { - node.error("Failed to set device label: " + error, {}); + node.error("Failed to set device label: " + error); } ); } }, function(error) { - node.error("Failed to fetch device: " + error, {}); + node.error("Failed to fetch device: " + error); } ); } @@ -142,25 +202,62 @@ module.exports = function(RED) { }; node.setConnected(false); - fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist - upgradeDirectoryIfNecessary(node, storageDir); - node.matrixClient = sdk.createClient({ - baseUrl: this.url, - accessToken: this.credentials.accessToken, - cryptoStore: new LocalStorageCryptoStore(localStorage), - store: new MemoryStore({ - localStorage: localStorage, - }), - userId: this.userId, - deviceId: (this.deviceId || getStoredDeviceId(localStorage)) || undefined - // verificationMethods: ["m.sas.v1"] - }); + node.isConnected = function() { + return node.connected; + }; - node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`); + // Snapshot the Rust crypto store to disk so E2EE state survives + // restarts. No-op when E2EE is disabled. + async function persistCrypto() { + if(!cryptoSnapshotPath) { + return; + } + try { + await snapshotCryptoStore(cryptoSnapshotPath, cryptoDbPrefix); + } catch(e) { + node.error("Failed to persist Matrix crypto store: " + e); + } + } - // set globally if configured to do so - if(this.globalAccess) { - this.context().global.set('matrixClient["'+this.userId+'"]', node.matrixClient); + // Discard all persisted crypto state for this account. Used when the + // device ID changes - the old crypto store belongs to a device that + // no longer exists and the Rust crypto stack refuses to load it. + async function discardCryptoStore() { + // remove the persisted Rust crypto snapshot + try { + if(cryptoSnapshotPath) { + fs.removeSync(cryptoSnapshotPath); + } + } catch(e) { + node.warn("Could not remove crypto snapshot: " + e); + } + // remove legacy (libolm) crypto data from local storage + try { + for(let i = localStorage.length - 1; i >= 0; i--) { + let key = localStorage.key(i); + if(key && key.indexOf('crypto') === 0) { + localStorage.removeItem(key); + } + } + } catch(e) { + node.warn("Could not clear legacy crypto store: " + e); + } + // drop any in-memory IndexedDB database for this account's crypto store + try { + if(globalThis.indexedDB && typeof indexedDB.databases === 'function') { + let dbs = await indexedDB.databases(); + for(let db of dbs) { + if(db.name && db.name.indexOf(cryptoDbPrefix) === 0) { + await new Promise(function(resolve) { + let req = indexedDB.deleteDatabase(db.name); + req.onsuccess = req.onerror = req.onblocked = function(){ resolve(); }; + }); + } + } + } + } catch(e) { + node.warn("Could not clear in-memory crypto database: " + e); + } } function stopClient() { @@ -172,284 +269,464 @@ module.exports = function(RED) { if(retryStartTimeout) { clearTimeout(retryStartTimeout); } + if(cryptoSnapshotInterval) { + clearInterval(cryptoSnapshotInterval); + cryptoSnapshotInterval = null; + } } node.on('close', function(done) { stopClient(); - if(node.globalAccess) { - try { - node.context().global.set('matrixClient["'+node.userId+'"]', undefined); - } catch(e){ - node.error(e.message, {}); - } - } - done(); - }); - - node.isConnected = function() { - return node.connected; - }; - - node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) { - if (toStartOfTimeline) { - node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result"); - return; // ignore paginated results - } - if (!data || !data.liveEvent) { - node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message"); - return; // ignore old message (we only want live events) - } - if(node.initializedAt > event.getDate()) { - node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init"); - return; // skip events that occurred before our client initialized - } - - try { - await node.matrixClient.decryptEventIfNeeded(event); - } catch (error) { - node.error(error, {}); - return; - } - - const isDmRoom = (room) => { - // Find out if this is a direct message room. - let isDM = !!room.getDMInviter(); - const allMembers = room.currentState.getMembers(); - if (!isDM && allMembers.length <= 2) { - // if not a DM, but there are 2 users only - // double check DM (needed because getDMInviter works only if you were invited, not if you invite) - // hence why we check for each member - if (allMembers.some((m) => m.getDMInviter())) { - return true; + persistCrypto().finally(function() { + if(node.globalAccess) { + try { + node.context().global.set('matrixClient["'+node.userId+'"]', undefined); + } catch(e){ + node.error(e.message); } } - return allMembers.length <= 2 && isDM; + done(); + }); + }); + + fs.ensureDirSync(storageDir); // create storage directory if it doesn't exist + upgradeDirectoryIfNecessary(node, storageDir); + + if(node.e2ee) { + cryptoSnapshotPath = localStorageDir + '/rust-crypto-store.v8'; + } + + setupClient().catch(function(error) { + node.error(error); + }); + + async function setupClient() { + const sdk = await sdkPromise; + const { + RelationType, RoomEvent, RoomMemberEvent, HttpApiEvent, ClientEvent, + MemoryStore, LocalStorageCryptoStore, + } = sdk; + const { + CryptoEvent, VerificationRequestEvent, VerifierEvent, VerificationPhase, + } = await cryptoApiPromise; + + // ---- Device verification ---------------------------------- + // Surface a verification request (and every subsequent phase + // change) to the matrix-verification node as a "Verification.update" + // event. Live request objects are kept in node.verificationRequests + // so the matrix-verification-action node can act on them by id. + function buildVerificationMsg(request, sasShown) { + let phase = sasShown + ? 'sas' + : String(VerificationPhase[request.phase] || 'unknown').toLowerCase(); + let msg = { + verificationId : request.transactionId, + phase : phase, + payload : phase, + userId : request.otherUserId, + deviceId : request.otherDeviceId || null, + topic : request.roomId || null, + isSelfVerification: request.isSelfVerification, + initiatedByMe : request.initiatedByMe, + }; + // chosenMethod is null until a verification method is picked. + // (request.methods is intentionally not used - it is not + // implemented in the Rust crypto stack and always throws.) + try { + msg.chosenMethod = request.chosenMethod || null; + } catch(e) { + msg.chosenMethod = null; + } + let sas = node.verificationSas.get(request.transactionId); + if(sas && sas.sas) { + msg.sas = { + emoji : sas.sas.emoji || null, + decimal: sas.sas.decimal || null, + }; + } + if(request.phase === VerificationPhase.Cancelled) { + msg.cancellationCode = request.cancellationCode || null; + } + return msg; + } + + // Emit a verification update. Never lets an exception escape - + // this runs inside the SDK's synchronous event emission, where an + // uncaught throw would crash Node-RED. + function emitVerificationUpdate(request, sasShown) { + try { + node.emit("Verification.update", buildVerificationMsg(request, sasShown)); + } catch(e) { + node.error("Failed to process verification update: " + e); + } + } + + node.trackVerificationRequest = function(request) { + let id; + try { id = request.transactionId; } catch(e) { id = undefined; } + if(!id) { + // transactionId is only assigned once the first event is + // sent - wait for it before tracking. + const waitForId = function() { + let tid; + try { tid = request.transactionId; } catch(e) { tid = undefined; } + if(tid) { + request.off(VerificationRequestEvent.Change, waitForId); + node.trackVerificationRequest(request); + } + }; + request.on(VerificationRequestEvent.Change, waitForId); + return; + } + if(node.verificationRequests.has(id)) { + return; // already tracked + } + node.verificationRequests.set(id, request); + + let verifierHooked = false; + const onChange = function() { + try { + // Once a verifier exists, hook its SAS event so the + // emoji/decimal can be surfaced to the flow. + const verifier = request.verifier; + if(verifier && !verifierHooked) { + verifierHooked = true; + verifier.on(VerifierEvent.ShowSas, function(sasCallbacks) { + node.verificationSas.set(id, sasCallbacks); + emitVerificationUpdate(request, true); + }); + } + emitVerificationUpdate(request, false); + if(request.phase === VerificationPhase.Done || request.phase === VerificationPhase.Cancelled) { + request.off(VerificationRequestEvent.Change, onChange); + node.verificationRequests.delete(id); + node.verificationSas.delete(id); + } + } catch(e) { + node.error("Verification request handler error: " + e); + } + }; + request.on(VerificationRequestEvent.Change, onChange); + emitVerificationUpdate(request, false); }; - let msg = { - encrypted : event.isEncrypted(), - redacted : event.isRedacted(), - content : event.getContent(), - type : (event.getContent()['msgtype'] || event.getType()) || null, - payload : (event.getContent()['body'] || event.getContent()) || null, - isDM : isDmRoom(room), - isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread, - mentions : event.getContent()["m.mentions"] || null, - userId : event.getSender(), - user : node.matrixClient.getUser(event.getSender()), - topic : event.getRoomId(), - eventId : event.getId(), - event : event, - }; + // Resolve the real homeserver via .well-known discovery so a + // delegating domain (e.g. "example.org") works as the server URL. + const baseUrl = await resolveHomeserverUrl(sdk, node.url); + if(baseUrl !== node.url) { + node.log(`Discovered homeserver ${baseUrl} for ${node.url} via .well-known`); + } - // remove keys from user property that start with an underscore - Object.keys(msg.user).forEach(function (key) { - if (/^_/.test(key)) { - delete msg.user[key]; + let clientOpts = { + baseUrl: baseUrl, + accessToken: node.credentials.accessToken, + store: new MemoryStore({ + localStorage: localStorage, + }), + userId: node.userId, + deviceId: (node.deviceId || getStoredDeviceId(localStorage)) || undefined, + cryptoCallbacks: { + // Supplies the Secure Secret Storage (4S) key to the crypto + // stack once it has been unlocked via the secure-backup + // admin endpoint. Returns null when no key is available. + getSecretStorageKey: async function({ keys }) { + if(node._secretStorageKeyCache) { + const [cachedId, cachedKey] = node._secretStorageKeyCache; + if(keys[cachedId]) { + return [cachedId, cachedKey]; + } + } + return null; + }, + // Caches a newly created 4S key (e.g. after a reset). + cacheSecretStorageKey: function(keyId, keyInfo, key) { + node._secretStorageKeyCache = [keyId, key]; + }, + }, + }; + if(node.e2ee) { + // Provide the legacy (pre-v37 libolm) crypto store so that + // initRustCrypto() can perform a one-time migration of any + // existing crypto state into the Rust crypto store. + clientOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); + } + node.matrixClient = sdk.createClient(clientOpts); + + node.debug(`hasLazyLoadMembersEnabled=${node.matrixClient.hasLazyLoadMembersEnabled()}`); + + // set globally if configured to do so + if(node.globalAccess) { + node.context().global.set('matrixClient["'+node.userId+'"]', node.matrixClient); + } + + node.matrixClient.on(RoomEvent.Timeline, async function(event, room, toStartOfTimeline, removed, data) { + if (toStartOfTimeline) { + node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: paginated result"); + return; // ignore paginated results + } + if (!data || !data.liveEvent) { + node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message"); + return; // ignore old message (we only want live events) + } + if(node.initializedAt > event.getDate()) { + node.log("Ignoring" + (event.isEncrypted() ? ' encrypted' : '') +" timeline event [" + (event.getContent()['msgtype'] || event.getType()) + "]: (" + room.name + ") " + event.getId() + " for reason: old message before init"); + return; // skip events that occurred before our client initialized + } + + try { + await node.matrixClient.decryptEventIfNeeded(event); + } catch (error) { + node.error(error); + return; + } + + const isDmRoom = (room) => { + // Find out if this is a direct message room. + let isDM = !!room.getDMInviter(); + const allMembers = room.currentState.getMembers(); + if (!isDM && allMembers.length <= 2) { + // if not a DM, but there are 2 users only + // double check DM (needed because getDMInviter works only if you were invited, not if you invite) + // hence why we check for each member + if (allMembers.some((m) => m.getDMInviter())) { + return true; + } + } + return allMembers.length <= 2 && isDM; + }; + + let msg = { + encrypted : event.isEncrypted(), + redacted : event.isRedacted(), + content : event.getContent(), + type : (event.getContent()['msgtype'] || event.getType()) || null, + payload : (event.getContent()['body'] || event.getContent()) || null, + isDM : isDmRoom(room), + isThread : event.getContent()?.['m.relates_to']?.rel_type === RelationType.Thread, + mentions : event.getContent()["m.mentions"] || null, + userId : event.getSender(), + user : node.matrixClient.getUser(event.getSender()), + topic : event.getRoomId(), + eventId : event.getId(), + event : event, + }; + + // remove keys from user property that start with an underscore + Object.keys(msg.user).forEach(function (key) { + if (/^_/.test(key)) { + delete msg.user[key]; + } + }); + + node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`); + node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg); + }); + + // handle auto-joining rooms + node.matrixClient.on(RoomMemberEvent.Membership, async function(event, member) { + if(node.initializedAt > event.getDate()) { + return; // skip events that occurred before our client initialized + } + + if (member.membership === "invite" && member.userId === node.userId) { + node.log("Got invite to join room " + member.roomId); + if(node.autoAcceptRoomInvites) { + node.matrixClient.joinRoom(member.roomId).then(function() { + node.log("Automatically accepted invitation to join room " + member.roomId); + }).catch(function(e) { + node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e); + }); + } + + let room = node.matrixClient.getRoom(event.getRoomId()); + node.emit("Room.invite", { + type : 'm.room.member', + userId : event.getSender(), + topic : event.getRoomId(), + topicName : (room ? room.name : null) || null, + event : event, + eventId : event.getId(), + }); } }); - node.log(`Received ${msg.encrypted ? 'encrypted ' : ''}timeline event [${msg.type}]: (${room.name}) ${event.getSender()} :: ${msg.content.body} ${toStartOfTimeline ? ' [PAGINATED]' : ''}`); - node.emit("Room.timeline", event, room, toStartOfTimeline, removed, data, msg); - }); - - /** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - // node.matrixClient.on("crypto.suggestKeyRestore", function(){ - // - // }); - - // node.matrixClient.on("RoomMember.typing", async function(event, member) { - // let isTyping = member.typing; - // let roomId = member.roomId; - // }); - - // node.matrixClient.on("RoomMember.powerLevel", async function(event, member) { - // let newPowerLevel = member.powerLevel; - // let newNormPowerLevel = member.powerLevelNorm; - // let roomId = member.roomId; - // }); - - // node.matrixClient.on("RoomMember.name", async function(event, member) { - // let newName = member.name; - // let roomId = member.roomId; - // }); - - // handle auto-joining rooms - - node.matrixClient.on(RoomMemberEvent.Membership, async function(event, member) { - if(node.initializedAt > event.getDate()) { - return; // skip events that occurred before our client initialized - } - - if (member.membership === "invite" && member.userId === node.userId) { - node.log("Got invite to join room " + member.roomId); - if(node.autoAcceptRoomInvites) { - node.matrixClient.joinRoom(member.roomId).then(function() { - node.log("Automatically accepted invitation to join room " + member.roomId); - }).catch(function(e) { - node.warn("Cannot join room (could be from being kicked/banned) " + member.roomId + ": " + e); + node.matrixClient.on(ClientEvent.Sync, async function(state, prevState, data) { + node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]"); + if(prevState === null && state === "PREPARED" ) { + // Occurs when the initial sync is completed first time. + // This involves setting up filters and obtaining push rules. + node.setConnected(true, function(){ + node.log("Matrix client connected"); + }); + } else if(prevState === null && state === "ERROR") { + // Occurs when the initial sync failed first time. + node.setConnected(false, function(){ + node.error("Failed to connect to Matrix server"); + }); + } else if(prevState === "ERROR" && state === "PREPARED") { + // Occurs when the initial sync succeeds + // after previously failing. + node.setConnected(true, function(){ + node.log("Matrix client connected"); + }); + } else if(prevState === "PREPARED" && state === "SYNCING") { + // Occurs immediately after transitioning to PREPARED. + // Starts listening for live updates rather than catching up. + node.setConnected(true, function(){ + node.log("Matrix client connected"); + }); + } else if(prevState === "SYNCING" && state === "RECONNECTING") { + // Occurs when the live update fails. + node.setConnected(false, function(){ + node.error("Connection to Matrix server lost"); + }); + } else if(prevState === "RECONNECTING" && state === "RECONNECTING") { + // Can occur if the update calls continue to fail, + // but the keepalive calls (to /versions) succeed. + node.setConnected(false, function(){ + node.error("Connection to Matrix server lost"); + }); + } else if(prevState === "RECONNECTING" && state === "ERROR") { + // Occurs when the keepalive call also fails + node.setConnected(false, function(){ + node.error("Connection to Matrix server lost"); + }); + } else if(prevState === "ERROR" && state === "SYNCING") { + // Occurs when the client has performed a + // live update after having previously failed. + node.setConnected(true, function(){ + node.log("Matrix client connected"); + }); + } else if(prevState === "ERROR" && state === "ERROR") { + // Occurs when the client has failed to + // keepalive for a second time or more. + node.setConnected(false, function(){ + node.error("Connection to Matrix server lost"); + }); + } else if(prevState === "SYNCING" && state === "SYNCING") { + // Occurs when the client has performed a live update. + // This is called after processing. + node.setConnected(true, function(){ + node.log("Matrix client connected"); + }); + } else if(state === "STOPPED") { + // Occurs once the client has stopped syncing or + // trying to sync after stopClient has been called. + node.setConnected(false, function(){ + node.error("Connection to Matrix server lost"); }); } - - let room = node.matrixClient.getRoom(event.getRoomId()); - node.emit("Room.invite", { - type : 'm.room.member', - userId : event.getSender(), - topic : event.getRoomId(), - topicName : (room ? room.name : null) || null, - event : event, - eventId : event.getId(), - }); - } - }); - - node.matrixClient.on(ClientEvent.Sync, async function(state, prevState, data) { - node.debug("SYNC [STATE=" + state + "] [PREVSTATE=" + prevState + "]"); - if(prevState === null && state === "PREPARED" ) { - // Occurs when the initial sync is completed first time. - // This involves setting up filters and obtaining push rules. - node.setConnected(true, function(){ - node.log("Matrix client connected"); - }); - } else if(prevState === null && state === "ERROR") { - // Occurs when the initial sync failed first time. - node.setConnected(false, function(){ - node.error("Failed to connect to Matrix server", {}); - }); - } else if(prevState === "ERROR" && state === "PREPARED") { - // Occurs when the initial sync succeeds - // after previously failing. - node.setConnected(true, function(){ - node.log("Matrix client connected"); - }); - } else if(prevState === "PREPARED" && state === "SYNCING") { - // Occurs immediately after transitioning to PREPARED. - // Starts listening for live updates rather than catching up. - node.setConnected(true, function(){ - node.log("Matrix client connected"); - }); - } else if(prevState === "SYNCING" && state === "RECONNECTING") { - // Occurs when the live update fails. - node.setConnected(false, function(){ - node.error("Connection to Matrix server lost", {}); - }); - } else if(prevState === "RECONNECTING" && state === "RECONNECTING") { - // Can occur if the update calls continue to fail, - // but the keepalive calls (to /versions) succeed. - node.setConnected(false, function(){ - node.error("Connection to Matrix server lost", {}); - }); - } else if(prevState === "RECONNECTING" && state === "ERROR") { - // Occurs when the keepalive call also fails - node.setConnected(false, function(){ - node.error("Connection to Matrix server lost", {}); - }); - } else if(prevState === "ERROR" && state === "SYNCING") { - // Occurs when the client has performed a - // live update after having previously failed. - node.setConnected(true, function(){ - node.log("Matrix client connected"); - }); - } else if(prevState === "ERROR" && state === "ERROR") { - // Occurs when the client has failed to - // keepalive for a second time or more. - node.setConnected(false, function(){ - node.error("Connection to Matrix server lost", {}); - }); - } else if(prevState === "SYNCING" && state === "SYNCING") { - // Occurs when the client has performed a live update. - // This is called after processing. - node.setConnected(true, function(){ - node.log("Matrix client connected"); - }); - } else if(state === "STOPPED") { - // Occurs once the client has stopped syncing or - // trying to sync after stopClient has been called. - node.setConnected(false, function(){ - node.error("Connection to Matrix server lost", {}); - }); - } - }); + }); - node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){ - // Example if user auth token incorrect: - // { - // errcode: 'M_UNKNOWN_TOKEN', - // data: { - // errcode: 'M_UNKNOWN_TOKEN', - // error: 'Invalid macaroon passed.', - // soft_logout: false - // }, - // httpStatus: 401 - // } + node.matrixClient.on(HttpApiEvent.SessionLoggedOut, async function(errorObj){ + // Example if user auth token incorrect: + // { + // errcode: 'M_UNKNOWN_TOKEN', + // data: { + // errcode: 'M_UNKNOWN_TOKEN', + // error: 'Invalid macaroon passed.', + // soft_logout: false + // }, + // httpStatus: 401 + // } - node.error("Authentication failure: " + errorObj, {}); - stopClient(); - }); + node.error("Authentication failure: " + errorObj); + stopClient(); + }); - async function run() { - try { - if(node.e2ee){ - node.log("Initializing crypto..."); - await node.matrixClient.initCrypto(); - node.matrixClient.getCrypto().globalBlacklistUnverifiedDevices = false; // prevent errors from unverified devices + // incoming device-verification requests from other users/devices + node.matrixClient.on(CryptoEvent.VerificationRequestReceived, function(request) { + try { + node.log("Received device verification request from " + request.otherUserId); + node.trackVerificationRequest(request); + } catch(e) { + node.error("Failed to handle incoming verification request: " + e); } - node.log("Connecting to Matrix server..."); - await node.matrixClient.startClient({ - initialSyncLimit: node.initialSyncLimit - }); - } catch(error) { - node.error(error, {}); - } - } + }); - // do an authed request and only continue if we don't get an error - // this prevent the matrix client from crashing Node-RED on invalid auth token - (function checkAuthTokenThenStart() { - if(node.matrixClient.clientRunning) { - return; - } - - /** - * We do a /whoami request before starting for a few reasons: - * - validate our auth token - * - make sure auth token belongs to provided node.userId - * - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033) - */ - node.matrixClient.whoami() - .then( - function(data) { - if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) { - node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched.", {}); + async function run() { + try { + if(node.e2ee){ + node.log("Initializing crypto..."); + ensureIndexedDBShim(); + // If the device ID has changed (e.g. a new login), the + // persisted crypto store belongs to the old device and + // cannot be loaded - discard it and start fresh. + // Otherwise restore the previously persisted state. + let effectiveDeviceId = node.matrixClient.getDeviceId(), + storedDeviceId = getStoredDeviceId(localStorage); + if(storedDeviceId && effectiveDeviceId && storedDeviceId !== effectiveDeviceId) { + node.warn(`Device ID changed (${storedDeviceId} -> ${effectiveDeviceId}); discarding the encryption store from the old device.`); + await discardCryptoStore(); + } else { + await restoreCryptoStore(cryptoSnapshotPath); } - if('device_id' in data && data['device_id'] && !node.deviceId) { - // if we have no device_id configured lets use the one - // returned by /whoami for this access_token - node.matrixClient.deviceId = data['device_id']; - } - - // make sure our userId matches the access token's - if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) { - node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`, {}); - return; - } - run().catch((error) => node.error(error)); - }, - function(err) { - // if the error isn't authentication related retry in a little bit - if(err.code !== "M_UNKNOWN_TOKEN") { - retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000); - node.error("Auth check failed: " + err, {}); + await node.matrixClient.initRustCrypto({ + useIndexedDB: true, + cryptoDatabasePrefix: cryptoDbPrefix, + }); + let crypto = node.matrixClient.getCrypto(); + if(crypto) { + // Blacklist (refuse to encrypt to) unverified devices only + // when the user has explicitly unticked "Allow unverified + // devices". Default/undefined allows them, as before. + crypto.globalBlacklistUnverifiedDevices = (node.allowUnknownDevices === false); } + // periodically persist crypto state so it survives an unclean shutdown + cryptoSnapshotInterval = setInterval(persistCrypto, 5 * 60 * 1000); } - ) - })(); + node.log("Connecting to Matrix server..."); + await node.matrixClient.startClient({ + initialSyncLimit: node.initialSyncLimit + }); + } catch(error) { + node.error(error); + } + } + + // do an authed request and only continue if we don't get an error + // this prevent the matrix client from crashing Node-RED on invalid auth token + (function checkAuthTokenThenStart() { + if(node.matrixClient.clientRunning) { + return; + } + + /** + * We do a /whoami request before starting for a few reasons: + * - validate our auth token + * - make sure auth token belongs to provided node.userId + * - fetch device_id if possible (only available on Synapse >= v1.40.0 under MSC2033) + */ + node.matrixClient.whoami() + .then( + function(data) { + if((typeof data['device_id'] === undefined || !data['device_id']) && !node.deviceId && !getStoredDeviceId(localStorage)) { + node.error("/whoami request did not return device_id. You will need to manually set one in your configuration because this cannot be automatically fetched."); + } + if('device_id' in data && data['device_id'] && !node.deviceId) { + // if we have no device_id configured lets use the one + // returned by /whoami for this access_token + node.matrixClient.deviceId = data['device_id']; + } + + // make sure our userId matches the access token's + if(data['user_id'].toLowerCase() !== node.userId.toLowerCase()) { + node.error(`User ID provided is ${node.userId} but token belongs to ${data['user_id']}`); + return; + } + run().catch((error) => node.error(error)); + }, + function(err) { + // if the error isn't authentication related retry in a little bit + if(err.code !== "M_UNKNOWN_TOKEN") { + retryStartTimeout = setTimeout(checkAuthTokenThenStart, 15000); + node.error("Auth check failed: " + err); + } + } + ) + })(); + } } } @@ -459,54 +736,201 @@ module.exports = function(RED) { userId: { type: "text", required: true }, accessToken: { type: "text", required: true }, deviceId: { type: "text", required: false }, - url: { type: "text", required: true } + url: { type: "text", required: true }, + password: { type: "password", required: false } } }); RED.httpAdmin.post( "/matrix-chat/login", RED.auth.needsPermission('flows.write'), - function(req, res) { + async function(req, res) { let userId = req.body.userId || undefined, password = req.body.password || undefined, baseUrl = req.body.baseUrl || undefined, deviceId = req.body.deviceId || undefined, displayName = req.body.displayName || undefined; - const matrixClient = sdk.createClient({ - baseUrl: baseUrl, - deviceId: deviceId, - timelineSupport: true, - localTimeoutMs: '30000' - }); + try { + const sdk = await sdkPromise; + // Resolve .well-known delegation so users can enter their domain. + baseUrl = await resolveHomeserverUrl(sdk, baseUrl); + const matrixClient = sdk.createClient({ + baseUrl: baseUrl, + deviceId: deviceId, + timelineSupport: true, + localTimeoutMs: '30000' + }); - matrixClient.timelineSupport = true; + matrixClient.login( + 'm.login.password', { + identifier: { + type: 'm.id.user', + user: userId, + }, + password: password, + initial_device_display_name: displayName + }) + .then( + function(response) { + res.json({ + 'result': 'ok', + 'token': response.access_token, + 'device_id': response.device_id, + 'user_id': response.user_id, + }); + }, + function(err) { + res.json({ + 'result': 'error', + 'message': err + }); + } + ); + } catch(err) { + res.json({ + 'result': 'error', + 'message': err + }); + } + }); - matrixClient.login( - 'm.login.password', { - identifier: { - type: 'm.id.user', - user: userId, - }, - password: password, - initial_device_display_name: displayName - }) - .then( - function(response) { - res.json({ - 'result': 'ok', - 'token': response.access_token, - 'device_id': response.device_id, - 'user_id': response.user_id, - }); - }, - function(err) { - res.json({ - 'result': 'error', - 'message': err - }); + /** + * Interactive Secure Secret Storage (4S) / cross-signing setup for the + * config editor's "Set up secure backup" button. + * + * Secured with the same flows.write permission as the login endpoint, so it + * is not publicly exposed. Operates on the live, connected client of an + * already-deployed server configuration node (identified by req.body.id). + * + * Actions: + * - status : report connection / cross-signing / secret-storage state + * - unlock : unlock existing 4S with a recovery key/passphrase, then set up + * cross-signing for this device + * - reset : create brand new cross-signing keys and secret storage + * (requires the account password); returns the new recovery key + */ + RED.httpAdmin.post( + "/matrix-chat/secure-backup", + RED.auth.needsPermission('flows.write'), + async function(req, res) { + try { + const serverNode = RED.nodes.getNode(req.body.id); + if(!serverNode || !serverNode.matrixClient) { + return res.json({ result: 'error', message: 'Server configuration not found. Save and deploy the server configuration node first.' }); + } + if(typeof serverNode.isConnected !== 'function' || !serverNode.isConnected()) { + return res.json({ result: 'error', message: 'The Matrix client is not connected. Deploy the server configuration and wait for it to connect, then try again.' }); + } + const crypto = serverNode.matrixClient.getCrypto(); + if(!crypto) { + return res.json({ result: 'error', message: 'End-to-end encryption is not enabled on this server configuration.' }); + } + const secretStorage = serverNode.matrixClient.secretStorage; + const action = req.body.action || 'status'; + + if(action === 'status') { + const defaultKeyId = await secretStorage.getDefaultKeyId(); + return res.json({ + result: 'ok', + crossSigningReady: await crypto.isCrossSigningReady(), + secretStorageReady: await crypto.isSecretStorageReady(), + secretStorageExists: !!defaultKeyId, + }); + } + + if(action === 'unlock') { + const cryptoApi = await cryptoApiPromise; + const recoveryInput = String(req.body.recoveryKey || '').trim(); + if(!recoveryInput) { + return res.json({ result: 'error', message: 'A recovery key or passphrase is required.' }); } - ); + const keyId = await secretStorage.getDefaultKeyId(); + if(!keyId) { + return res.json({ result: 'error', message: 'This account has no secure backup to unlock. Use Reset to create one.' }); + } + const stored = await secretStorage.getKey(keyId); + const keyInfo = stored && stored[1]; + if(!keyInfo) { + return res.json({ result: 'error', message: 'Could not read the secure backup key description from the account.' }); + } + + let keyBytes = null; + try { + keyBytes = cryptoApi.decodeRecoveryKey(recoveryInput.replace(/\s+/g, '')); + } catch(e) { /* not a recovery key - fall back to passphrase */ } + if(!keyBytes && keyInfo.passphrase) { + keyBytes = await cryptoApi.deriveRecoveryKeyFromPassphrase( + recoveryInput, keyInfo.passphrase.salt, keyInfo.passphrase.iterations); + } + if(!keyBytes) { + return res.json({ result: 'error', message: 'Could not read that value as a recovery key or passphrase.' }); + } + if(!(await secretStorage.checkKey(keyBytes, keyInfo))) { + return res.json({ result: 'error', message: 'That recovery key / passphrase is not correct.' }); + } + + serverNode._secretStorageKeyCache = [keyId, keyBytes]; + await crypto.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: async function(makeRequest) { + if(req.body.password) { + await makeRequest({ + type: 'm.login.password', + identifier: { type: 'm.id.user', user: serverNode.userId }, + password: req.body.password, + }); + } else { + await makeRequest(null); + } + }, + }); + try { await crypto.checkKeyBackupAndEnable(); } catch(e) { /* best effort */ } + serverNode.log("Secure backup unlocked; cross-signing set up."); + return res.json({ + result: 'ok', + message: 'Secure backup unlocked. Cross-signing is now set up for this bot.', + crossSigningReady: await crypto.isCrossSigningReady(), + }); + } + + if(action === 'reset') { + const password = req.body.password; + if(!password) { + return res.json({ result: 'error', message: 'The account password is required to reset secure backup.' }); + } + const newKey = await crypto.createRecoveryKeyFromPassphrase(); + // Replace secret storage FIRST. This makes the new 4S key + // (whose private key we hold and cache via cacheSecretStorageKey) + // the default before cross-signing is reset. bootstrapCrossSigning + // exports the new signing keys into whatever 4S is current, so if + // the old 4S were still default it would need the old (unknown) + // key and fail with "getSecretStorageKey callback returned falsey". + await crypto.bootstrapSecretStorage({ + setupNewSecretStorage: true, + createSecretStorageKey: async function() { return newKey; }, + }); + await crypto.bootstrapCrossSigning({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys: async function(makeRequest) { + await makeRequest({ + type: 'm.login.password', + identifier: { type: 'm.id.user', user: serverNode.userId }, + password: password, + }); + }, + }); + serverNode.log("Cross-signing and secure backup were reset."); + return res.json({ + result: 'ok', + message: 'Cross-signing and secure backup have been reset. Store the new recovery key somewhere safe - it is shown only once.', + recoveryKey: newKey.encodedPrivateKey, + }); + } + + return res.json({ result: 'error', message: 'Unknown action: ' + action }); + } catch(error) { + res.json({ result: 'error', message: String(error && error.message || error) }); + } }); function upgradeDirectoryIfNecessary(node, storageDir) { @@ -526,7 +950,7 @@ module.exports = function(RED) { fs.copySync(oldStorageDir, dir); } } catch (err) { - node.error(err, {}); + node.error(err); } }); diff --git a/src/matrix-verification-action.html b/src/matrix-verification-action.html new file mode 100644 index 0000000..7914136 --- /dev/null +++ b/src/matrix-verification-action.html @@ -0,0 +1,126 @@ + + + + + diff --git a/src/matrix-verification-action.js b/src/matrix-verification-action.js new file mode 100644 index 0000000..4688860 --- /dev/null +++ b/src/matrix-verification-action.js @@ -0,0 +1,139 @@ +module.exports = function(RED) { + function MatrixVerificationAction(n) { + RED.nodes.createNode(this, n); + + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.mode = n.mode || "accept"; + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + if (!node.server) { + node.error("No configuration node"); + return; + } + node.server.register(node); + + node.server.on("disconnected", function() { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + node.on("input", async function(msg) { + if (!node.server || !node.server.matrixClient) { + msg.error = "No matrix server selected"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + if (!node.server.isConnected()) { + msg.error = "Matrix server connection is currently closed"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + const crypto = node.server.matrixClient.getCrypto(); + if (!crypto) { + msg.error = "End-to-end encryption is not enabled on the Matrix server config"; + node.error(msg.error, msg); + node.send([null, msg]); + return; + } + + // msg.mode overrides the node's configured mode if provided + const mode = msg.mode || node.mode; + + try { + if (mode === "request") { + // Start a new verification request. + // - msg.userId + msg.deviceId : verify a specific device (to-device) + // - msg.userId + msg.topic : verify a user in a DM room + // - otherwise : verify our own other devices + let request; + if (msg.userId && msg.deviceId) { + request = await crypto.requestDeviceVerification(msg.userId, msg.deviceId); + } else if (msg.userId && msg.topic) { + request = await crypto.requestVerificationDM(msg.userId, msg.topic); + } else { + request = await crypto.requestOwnUserVerification(); + } + + if (typeof node.server.trackVerificationRequest === "function") { + node.server.trackVerificationRequest(request); + } + msg.verificationId = request.transactionId; + node.send([msg, null]); + return; + } + + // Every other mode acts on an existing tracked request. + const request = node.server.verificationRequests.get(msg.verificationId); + if (!request) { + throw new Error(`No active verification found for msg.verificationId '${msg.verificationId}'`); + } + + switch (mode) { + case "accept": + await request.accept(); + break; + + case "start": { + // Begin SAS (emoji) verification. The SAS emoji is delivered + // through the matrix-verification node when it becomes ready. + let verifier = request.verifier; + if (!verifier) { + verifier = await request.startVerification("m.sas.v1"); + } + verifier.verify().catch(function(e) { + node.warn("Verification ended: " + e); + }); + break; + } + + case "confirm": { + const sas = node.server.verificationSas.get(msg.verificationId); + if (!sas) { + throw new Error("This verification has no SAS awaiting confirmation"); + } + await sas.confirm(); + break; + } + + case "mismatch": { + const sas = node.server.verificationSas.get(msg.verificationId); + if (!sas) { + throw new Error("This verification has no SAS awaiting confirmation"); + } + sas.mismatch(); + break; + } + + case "cancel": + await request.cancel(); + break; + + default: + throw new Error("Unknown verification action mode: " + mode); + } + + msg.verificationId = request.transactionId; + node.send([msg, null]); + } catch (e) { + msg.error = String(e && e.message || e); + node.error("Verification action failed: " + msg.error, msg); + node.send([null, msg]); + } + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-verification-action", MatrixVerificationAction); +} diff --git a/src/matrix-verification.html b/src/matrix-verification.html new file mode 100644 index 0000000..93f550e --- /dev/null +++ b/src/matrix-verification.html @@ -0,0 +1,199 @@ + + + + + diff --git a/src/matrix-verification.js b/src/matrix-verification.js new file mode 100644 index 0000000..839e6f2 --- /dev/null +++ b/src/matrix-verification.js @@ -0,0 +1,113 @@ +module.exports = function(RED) { + function MatrixVerification(n) { + RED.nodes.createNode(this, n); + + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + + // Phase filter - emit only the ticked phases. Undefined (config saved + // before these options existed) is treated as ticked, so old nodes + // keep emitting every phase. + this.phases = { + requested: n.phaseRequested !== false, + ready: n.phaseReady !== false, + started: n.phaseStarted !== false, + sas: n.phaseSas !== false, + done: n.phaseDone !== false, + cancelled: n.phaseCancelled !== false, + }; + this.initiatedBy = n.initiatedBy || 'any'; // any | me | notme + this.verificationType = n.verificationType || 'any'; // any | room | device + this.selfVerification = n.selfVerification || 'any'; // any | self | others + this.userFilter = (n.userFilter || '').split(',') + .map(function(s){ return s.trim().toLowerCase(); }) + .filter(Boolean); + this.roomFilter = (n.roomFilter || '').split(',') + .map(function(s){ return s.trim(); }) + .filter(Boolean); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + + if (!node.server) { + node.error("No configuration node"); + return; + } + node.server.register(node); + + // Returns true if a verification update message passes every configured + // filter. All filters AND-combine; each defaults to "pass everything". + function passesFilters(m) { + // phase + if ((m.phase in node.phases) && !node.phases[m.phase]) { + return false; + } + // initiated by + if (node.initiatedBy === 'me' && !m.initiatedByMe) { + return false; + } + if (node.initiatedBy === 'notme' && m.initiatedByMe) { + return false; + } + // verification type - room verifications carry a roomId (msg.topic), + // to-device verifications do not + if (node.verificationType === 'room' && !m.topic) { + return false; + } + if (node.verificationType === 'device' && m.topic) { + return false; + } + // self-verification (the other party is one of the bot's own devices) + if (node.selfVerification === 'self' && !m.isSelfVerification) { + return false; + } + if (node.selfVerification === 'others' && m.isSelfVerification) { + return false; + } + // user id allowlist + if (node.userFilter.length && + (!m.userId || node.userFilter.indexOf(m.userId.toLowerCase()) === -1)) { + return false; + } + // room id filter - only constrains room verifications; device + // verifications have no room and are not affected + if (node.roomFilter.length && m.topic && + node.roomFilter.indexOf(m.topic) === -1) { + return false; + } + return true; + } + + const onConnected = function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }; + const onDisconnected = function() { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }; + const onVerificationUpdate = function(verificationMsg) { + if (!passesFilters(verificationMsg)) { + return; + } + node.status({ fill: "blue", shape: "dot", text: verificationMsg.phase }); + // clone so multiple verification nodes don't share/mutate one object + node.send(RED.util.cloneMessage(verificationMsg)); + }; + + node.server.on("connected", onConnected); + node.server.on("disconnected", onDisconnected); + node.server.on("Verification.update", onVerificationUpdate); + + if (node.server.isConnected && node.server.isConnected()) { + onConnected(); + } + + node.on("close", function() { + node.server.removeListener("connected", onConnected); + node.server.removeListener("disconnected", onDisconnected); + node.server.removeListener("Verification.update", onVerificationUpdate); + node.server.deregister(node); + }); + } + RED.nodes.registerType("matrix-verification", MatrixVerification); +} From 67920840e180ae245307a763ef1d37c22bbb125d Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Fri, 22 May 2026 15:29:43 -0600 Subject: [PATCH 04/18] Add session manager and verification list to the server config editor Adds two interactive admin tools to the matrix-server-config node so device verification and session management can be done from the editor without building a flow. Both open as modal dialogs and are backed by new flows.write-protected admin endpoints. Pending verifications - New "Pending verification requests" button opens a modal listing incoming and in-progress verification requests, refreshed every 5 seconds. - Each entry shows the type (device or room), who it is from, a live age, and a live expiry countdown; the list is capped at the newest 20 with a hidden-count note. - Clicking an entry drives the SAS flow (accept, start, show emoji, confirm/cancel) within the modal. - New /matrix-chat/verification endpoint with list, advance, confirm, mismatch, and cancel actions. - trackVerificationRequest now records a seen-at timestamp and keeps finished requests for two minutes so their outcome can still be reported to the editor. Sessions - New "Manage sessions" button opens a modal listing the account's sessions, modelled on Element's session manager: the current session with a verified / not-verified status box, then other sessions, each with a green or red shield, last activity, and IP address. - Clicking a session shows its details (session ID, last activity, IP), a Rename action, and a password-confirmed Remove. Unverified sessions offer a Verify action that hands off into the verification modal. - New /matrix-chat/sessions endpoint with list, rename, remove, and verify actions. --- src/matrix-server-config.html | 551 ++++++++++++++++++++++++++++++++++ src/matrix-server-config.js | 252 +++++++++++++++- 2 files changed, 801 insertions(+), 2 deletions(-) diff --git a/src/matrix-server-config.html b/src/matrix-server-config.html index 7d9a29b..801e350 100644 --- a/src/matrix-server-config.html +++ b/src/matrix-server-config.html @@ -204,6 +204,539 @@ $("#matrix-sb-overlay").data("sbStatusFn")(); }); + // --- Verification list (modal) --- + // Built once and reused; the node id is stored on the overlay. + if (!document.getElementById("matrix-vl-overlay")) { + $('').appendTo("head"); + + $('
    ' + + '
    Device Verification' + + '×
    ' + + '
    ' + + '
    ' + + '
    Pending verification requests — this list refreshes every 5 seconds. Click a request to verify it.
    ' + + '
    ' + + '' + + '' + + '
    ' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '' + + '
    ').appendTo(document.body); + + var vlListTimer = null, vlTickTimer = null, vlDetailTimer = null; + var vlEsc = function(s) { return $("
    ").text(s == null ? "" : String(s)).html(); }; + var vlId = function() { return $("#matrix-vl-overlay").data("matrixNodeId"); }; + var vlCurId = function() { return $("#matrix-vl-overlay").data("vlCurrentId"); }; + var vlCall = function(body) { + return $.ajax({ + url: "matrix-chat/verification", type: "POST", + contentType: "application/json", data: JSON.stringify(body), + }); + }; + var vlClearTimers = function() { + if(vlListTimer) { clearInterval(vlListTimer); vlListTimer = null; } + if(vlTickTimer) { clearInterval(vlTickTimer); vlTickTimer = null; } + if(vlDetailTimer) { clearInterval(vlDetailTimer); vlDetailTimer = null; } + }; + var vlDur = function(ms) { + var s = Math.max(0, Math.round(ms / 1000)); + if(s < 60) { return s + "s"; } + var m = Math.floor(s / 60), r = s % 60; + return m + "m " + (r < 10 ? "0" : "") + r + "s"; + }; + var vlState = function(icon, color, html) { + $("#matrix-vl-d-state").html('' + html + ''); + }; + + var vlTick = function() { + var now = Date.now(); + $("#matrix-vl-items .matrix-vl-item").each(function() { + var $it = $(this); + var seen = parseInt($it.attr("data-seen"), 10); + var expires = $it.attr("data-expires"); + $it.find(".matrix-vl-age").text(isNaN(seen) ? "" : "age " + vlDur(now - seen)); + if(expires) { + var left = parseInt(expires, 10) - now; + $it.find(".matrix-vl-exp").text(left > 0 ? "expires in " + vlDur(left) : "expired"); + } + }); + }; + var vlRenderList = function(data) { + if(data.result !== "ok") { + $("#matrix-vl-items").html('
    ' + vlEsc(data.message) + '
    '); + $("#matrix-vl-empty,#matrix-vl-more").hide(); + return; + } + var now = Date.now(); + var $items = $("#matrix-vl-items").empty(); + (data.verifications || []).forEach(function(v) { + var typeLabel = v.type === "room" ? "Room verification" : "Device verification"; + var sub = "from " + vlEsc(v.userId || "unknown"); + if(v.type === "device" && v.deviceId) { sub += " · device " + vlEsc(v.deviceId); } + else if(v.type === "room" && v.roomId) { sub += " · in room " + vlEsc(v.roomId); } + if(v.isSelfVerification) { sub += " · your own session"; } + $items.append($('
    ') + .attr("data-vid", v.verificationId) + .attr("data-seen", now - (v.ageMs || 0)) + .attr("data-expires", (v.expiresInMs != null) ? (now + v.expiresInMs) : "") + .html('
    ' + vlEsc(typeLabel) + '
    ' + + '
    ' + sub + '
    ' + + '
    ' + + '
    ')); + }); + $("#matrix-vl-empty").toggle(!(data.verifications || []).length); + if(data.hidden > 0) { + $("#matrix-vl-more").text(data.hidden + " older request" + (data.hidden === 1 ? "" : "s") + " hidden.").show(); + } else { + $("#matrix-vl-more").hide(); + } + vlTick(); + }; + var vlLoadList = function() { + vlCall({ id: vlId(), action: "list" }) + .done(vlRenderList) + .fail(function() { vlRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); }); + }; + var vlShowList = function() { + vlClearTimers(); + $("#matrix-vl-overlay").data("vlConfirmed", false); + $("#matrix-vl-detailview").hide(); + $("#matrix-vl-listview").show(); + $("#matrix-vl-back,#matrix-vl-cancel").hide(); + $("#matrix-vl-items").html('
    Loading…
    '); + $("#matrix-vl-empty,#matrix-vl-more").hide(); + vlLoadList(); + vlListTimer = setInterval(vlLoadList, 5000); + vlTickTimer = setInterval(vlTick, 1000); + }; + + var vlRenderSas = function(sas) { + var $sas = $("#matrix-vl-d-sas").empty(); + if(sas.emoji && sas.emoji.length) { + sas.emoji.forEach(function(pair) { + $sas.append('
    ' + vlEsc(pair[0]) + + '
    ' + vlEsc(pair[1]) + '
    '); + }); + } else if(sas.decimal) { + $sas.append('
    ' + vlEsc(sas.decimal.join(" ")) + '
    '); + } + }; + var vlRenderDetail = function(v) { + var typeLabel = v.type === "room" ? "Room verification" : "Device verification"; + var head = vlEsc(typeLabel); + if(v.userId) { head += " — " + vlEsc(v.userId); } + if(v.deviceId) { head += " (device " + vlEsc(v.deviceId) + ")"; } + $("#matrix-vl-d-head").html(head); + + if(v.phase === "done") { + vlClearTimers(); + vlState("fa-check-circle", "#3a9a4e", "Verified. This session is now verified."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if(v.phase === "cancelled") { + vlClearTimers(); + var why = v.cancellationCode ? (" (" + vlEsc(v.cancellationCode) + ")") : ""; + vlState("fa-times-circle", "#c9302c", "Verification was cancelled" + why + "."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if(v.phase === "gone") { + vlClearTimers(); + vlState("fa-exclamation-triangle", "#888", "This verification is no longer available."); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + $("#matrix-vl-cancel").hide(); + return; + } + if($("#matrix-vl-overlay").data("vlConfirmed")) { + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Waiting for the other device to confirm…"); + return; + } + if(v.sas && (v.sas.emoji || v.sas.decimal)) { + vlState("fa-key", "#d18a1b", "Compare these emoji with the other device, then choose below."); + vlRenderSas(v.sas); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").show(); + return; + } + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Waiting for the verification to start…"); + }; + var vlPollDetail = function() { + vlCall({ id: vlId(), action: "advance", verificationId: vlCurId() }) + .done(function(data) { + if(data.result !== "ok") { + vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message)); + return; + } + vlRenderDetail(data.verification); + }) + .fail(function() { vlState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?"); }); + }; + var vlShowDetail = function(vid) { + vlClearTimers(); + $("#matrix-vl-overlay").data("vlCurrentId", vid).data("vlConfirmed", false); + $("#matrix-vl-listview").hide(); + $("#matrix-vl-detailview").show(); + $("#matrix-vl-d-sas,#matrix-vl-d-actions,#matrix-vl-d-result").hide(); + $("#matrix-vl-d-head").text(""); + vlState("fa-spinner fa-spin", "#888", "Starting verification…"); + $("#matrix-vl-back,#matrix-vl-cancel").show(); + vlPollDetail(); + vlDetailTimer = setInterval(vlPollDetail, 1500); + }; + + var vlClose = function() { vlClearTimers(); $("#matrix-vl-overlay").fadeOut(120); }; + + $("#matrix-vl-x,#matrix-vl-close").on("click", vlClose); + $("#matrix-vl-overlay").on("mousedown", function(e) { if(e.target === this) { vlClose(); } }); + $(document).on("keydown.matrixvl", function(e) { + if(e.key === "Escape" && $("#matrix-vl-overlay").is(":visible")) { vlClose(); } + }); + $("#matrix-vl-back").on("click", vlShowList); + $("#matrix-vl-items").on("click", ".matrix-vl-item", function() { + vlShowDetail($(this).attr("data-vid")); + }); + $("#matrix-vl-cancel").on("click", function() { + vlCall({ id: vlId(), action: "cancel", verificationId: vlCurId() }).always(vlPollDetail); + }); + $("#matrix-vl-d-confirm").on("click", function() { + $("#matrix-vl-overlay").data("vlConfirmed", true); + $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Confirming…"); + vlCall({ id: vlId(), action: "confirm", verificationId: vlCurId() }) + .done(function(data) { + if(data.result !== "ok") { + $("#matrix-vl-overlay").data("vlConfirmed", false); + vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message)); + $("#matrix-vl-d-actions").show(); + } + }); + }); + $("#matrix-vl-d-mismatch").on("click", function() { + $("#matrix-vl-d-actions").hide(); + vlState("fa-spinner fa-spin", "#888", "Cancelling…"); + vlCall({ id: vlId(), action: "mismatch", verificationId: vlCurId() }).always(vlPollDetail); + }); + + $("#matrix-vl-overlay").data("vlShowListFn", vlShowList); + $("#matrix-vl-overlay").data("vlShowDetailFn", vlShowDetail); + } + + $("#matrix-verification-list-btn").on("click", function() { + $("#matrix-vl-overlay").data("matrixNodeId", nodeId).fadeIn(120); + $("#matrix-vl-overlay").data("vlShowListFn")(); + }); + + // --- Sessions (modal) --- + // Built once and reused; the node id is stored on the overlay. + if (!document.getElementById("matrix-ss-overlay")) { + $('').appendTo("head"); + + $('
    ' + + '
    Sessions' + + '×
    ' + + '
    ' + + '
    ' + + '
    Current session
    ' + + '
    ' + + '
    ' + + '
    Other sessions
    ' + + '
    ' + + '' + + '' + + '
    ' + + '' + + '
    ' + + '
    ' + + '' + + '' + + '' + + '
    ').appendTo(document.body); + + var ssEsc = function(s) { return $("
    ").text(s == null ? "" : String(s)).html(); }; + var ssId = function() { return $("#matrix-ss-overlay").data("matrixNodeId"); }; + var ssCall = function(body) { + return $.ajax({ + url: "matrix-chat/sessions", type: "POST", + contentType: "application/json", data: JSON.stringify(body), + }); + }; + var ssRelative = function(ts) { + if(!ts) { return "activity unknown"; } + var days = Math.floor((Date.now() - ts) / 86400000); + if(days <= 0) { return "active today"; } + if(days === 1) { return "active yesterday"; } + if(days < 90) { return "active " + days + " days ago"; } + return "inactive for 90+ days"; + }; + var ssFullDate = function(ts) { + return ts ? new Date(ts).toLocaleString() : "unknown"; + }; + var ssShield = function(verified) { + return ''; + }; + var ssCard = function(d, isCurrent) { + var sub = (d.verified ? 'Verified' : 'Not verified') + + ' · ' + ssRelative(d.lastSeenTs) + + (d.lastSeenIp ? (' · ' + d.lastSeenIp) : ''); + return $('
    ') + .data("device", d).data("isCurrent", isCurrent) + .html(ssShield(d.verified) + + '
    ' + + ssEsc(d.displayName || d.deviceId) + '
    ' + + '
    ' + ssEsc(sub) + '
    ' + + ''); + }; + var ssResult = function(ok, text) { + $("#matrix-ss-d-result").removeClass("ok err").addClass(ok ? "ok" : "err").text(text).show(); + }; + var ssRenderList = function(data) { + if(data.result !== "ok") { + $("#matrix-ss-current").html('
    ' + ssEsc(data.message) + '
    '); + $("#matrix-ss-currentmsg,#matrix-ss-others").empty(); + $("#matrix-ss-others-empty,#matrix-ss-more").hide(); + return; + } + $("#matrix-ss-current").empty().append(ssCard(data.current, true)); + $("#matrix-ss-currentmsg").html(data.current.verified + ? '
    Verified session
    ' + + 'This session is cross-signed and ready for secure messaging.
    ' + : '
    Not verified
    ' + + 'This session is not cross-signed. Use the Set up secure backup & cross-signing ' + + 'button to verify it.
    '); + var $others = $("#matrix-ss-others").empty(); + (data.others || []).forEach(function(d) { $others.append(ssCard(d, false)); }); + $("#matrix-ss-others-empty").toggle(!(data.others || []).length); + if(data.hidden > 0) { + $("#matrix-ss-more").text(data.hidden + " more session" + (data.hidden === 1 ? "" : "s") + " hidden.").show(); + } else { + $("#matrix-ss-more").hide(); + } + }; + var ssShowList = function() { + $("#matrix-ss-detailview").hide(); + $("#matrix-ss-listview").show(); + $("#matrix-ss-back").hide(); + $("#matrix-ss-current").html('
    Loading…
    '); + $("#matrix-ss-currentmsg,#matrix-ss-others").empty(); + $("#matrix-ss-others-empty,#matrix-ss-more").hide(); + ssCall({ id: ssId(), action: "list" }) + .done(ssRenderList) + .fail(function() { ssRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); }); + }; + var ssDetailRow = function(k, v) { + return '
    ' + ssEsc(k) + '' + ssEsc(v) + '
    '; + }; + var ssShowDetail = function(d, isCurrent) { + $("#matrix-ss-overlay").data("ssDevice", d).data("ssIsCurrent", isCurrent); + $("#matrix-ss-listview").hide(); + $("#matrix-ss-detailview").show(); + $("#matrix-ss-back").show(); + $("#matrix-ss-d-result,#matrix-ss-d-removeconfirm").hide(); + $("#matrix-ss-d-name").text(d.displayName || d.deviceId); + $("#matrix-ss-d-status").html(d.verified + ? '
    Verified session
    ' + + 'This session is ready for secure messaging.
    ' + : '
    Not verified
    ' + + (isCurrent + ? 'This session is not cross-signed. Use the Set up secure backup & cross-signing button to verify it.' + : 'Verify this session to confirm it is trusted.') + + '
    '); + $("#matrix-ss-d-details").html( + ssDetailRow("Session ID", d.deviceId) + + ssDetailRow("Last activity", ssFullDate(d.lastSeenTs)) + + ssDetailRow("IP address", d.lastSeenIp || "unknown")); + $("#matrix-ss-d-verifywrap").toggle(!isCurrent && !d.verified); + $("#matrix-ss-d-removewrap").toggle(!isCurrent); + }; + var ssClose = function() { $("#matrix-ss-overlay").fadeOut(120); }; + + $("#matrix-ss-x,#matrix-ss-close").on("click", ssClose); + $("#matrix-ss-overlay").on("mousedown", function(e) { if(e.target === this) { ssClose(); } }); + $(document).on("keydown.matrixss", function(e) { + if(e.key === "Escape" && $("#matrix-ss-overlay").is(":visible")) { ssClose(); } + }); + $("#matrix-ss-back").on("click", ssShowList); + $("#matrix-ss-current,#matrix-ss-others").on("click", ".matrix-ss-item", function() { + ssShowDetail($(this).data("device"), $(this).data("isCurrent")); + }); + $("#matrix-ss-d-rename").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + var name = prompt("Session display name:", d.displayName || ""); + if(name === null) { return; } + ssCall({ id: ssId(), action: "rename", deviceId: d.deviceId, displayName: name }) + .done(function(r) { + if(r.result !== "ok") { ssResult(false, r.message); return; } + d.displayName = name; + $("#matrix-ss-d-name").text(name || d.deviceId); + ssResult(true, "Session renamed."); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + $("#matrix-ss-d-verify").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + $("#matrix-ss-d-result").removeClass("ok err").html(' Starting verification…').show(); + ssCall({ id: ssId(), action: "verify", deviceId: d.deviceId }) + .done(function(r) { + if(r.result !== "ok" || !r.verificationId) { + ssResult(false, r.message || "Could not start verification."); + return; + } + // hand off to the verification modal's detail view + $("#matrix-ss-overlay").fadeOut(120); + $("#matrix-vl-overlay").data("matrixNodeId", ssId()).fadeIn(120); + $("#matrix-vl-overlay").data("vlShowDetailFn")(r.verificationId); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + $("#matrix-ss-d-removelink").on("click", function() { + $("#matrix-ss-d-password").val(""); + $("#matrix-ss-d-removeconfirm").show(); + }); + $("#matrix-ss-d-removeconfirmbtn").on("click", function() { + var d = $("#matrix-ss-overlay").data("ssDevice"); + $("#matrix-ss-d-result").removeClass("ok err").html(' Removing session…').show(); + ssCall({ id: ssId(), action: "remove", deviceId: d.deviceId, password: $("#matrix-ss-d-password").val() }) + .done(function(r) { + if(r.result !== "ok") { ssResult(false, r.message); return; } + ssShowList(); + }) + .fail(function() { ssResult(false, "Request failed."); }); + }); + + $("#matrix-ss-overlay").data("ssShowListFn", ssShowList); + } + + $("#matrix-sessions-btn").on("click", function() { + $("#matrix-ss-overlay").data("matrixNodeId", nodeId).fadeIn(120); + $("#matrix-ss-overlay").data("ssShowListFn")(); + }); + // --- Login: fetch a fresh access token & device id --- $("#matrix-login-btn").on("click", function() { function prettyPrintJson(json) { @@ -372,6 +905,24 @@ Sets up cross-signing so the bot's own device shows as verified. The server configuration must be deployed and connected first.
    + +
    + + +
    +
    + Review and complete incoming device verification requests without building a flow. + The server configuration must be deployed and connected first. +
    + +
    + + +
    +
    + View the account's logged-in sessions, verify them, or remove ones you don't recognize. + The server configuration must be deployed and connected first. +
    + + + + diff --git a/src/matrix-send-location.js b/src/matrix-send-location.js new file mode 100644 index 0000000..3ca4530 --- /dev/null +++ b/src/matrix-send-location.js @@ -0,0 +1,202 @@ +// matrix-js-sdk's main entry does not re-export `makeLocationContent`, so we +// deep-import the content-helpers module. The SDK has no `exports` field in +// its package.json (verified against v41) so subpath imports are stable. +const contentHelpersPromise = import("matrix-js-sdk/lib/content-helpers.js"); + +module.exports = function(RED) { + const VALID_ASSET_TYPES = ["m.self", "m.pin"]; + + function MatrixSendLocation(n) { + RED.nodes.createNode(this, n); + let node = this; + + this.name = n.name; + this.server = RED.nodes.getNode(n.server); + this.roomId = n.roomId; + + // Dynamic inputs: each has a `*Type` (msg | flow | global | str | num) + // and a `*Value` (the property path or literal). Defaults preserve the + // documented per-message names so existing flows keep working. + this.latitudeType = n.latitudeType || "msg"; + this.latitudeValue = n.latitudeValue || "latitude"; + this.longitudeType = n.longitudeType || "msg"; + this.longitudeValue = n.longitudeValue || "longitude"; + this.altitudeType = n.altitudeType || "msg"; + this.altitudeValue = n.altitudeValue || "altitude"; + this.geoUriType = n.geoUriType || "msg"; + this.geoUriValue = n.geoUriValue || "geo_uri"; + this.descriptionType = n.descriptionType || "msg"; + this.descriptionValue = n.descriptionValue || "description"; + this.assetTypeType = n.assetTypeType || "msg"; + this.assetTypeValue = n.assetTypeValue || "assetType"; + this.timestampType = n.timestampType || "msg"; + this.timestampValue = n.timestampValue || "timestamp"; + this.textType = n.textType || "msg"; + this.textValue = n.textValue || "payload"; + + if (!node.server) { + node.warn("No configuration node"); + return; + } + node.server.register(node); + + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + node.server.on("disconnected", function() { + node.status({ fill: "red", shape: "ring", text: "disconnected" }); + }); + node.server.on("connected", function() { + node.status({ fill: "green", shape: "ring", text: "connected" }); + }); + + /** + * Resolve a typed-input pair to its runtime value. + * msg | flow | global - read the property path from that source. + * num - parse the configured literal as a number; + * empty string => undefined. + * str - the configured literal; empty string => undefined. + * bool - "true" => true, anything else => false. + * + * Returning `undefined` from this signals "not provided", which is + * how optional fields opt out. + */ + function getToValue(msg, type, property) { + if (type === "msg") { + return RED.util.getMessageProperty(msg, property); + } + if (type === "flow" || type === "global") { + try { + return RED.util.evaluateNodeProperty(property, type, node, msg); + } catch (e) { + throw new Error("Invalid " + type + " value evaluation for '" + property + "'"); + } + } + if (property === "" || property === undefined || property === null) { + return undefined; + } + if (type === "num") { + const n = Number(property); + return Number.isFinite(n) ? n : undefined; + } + if (type === "bool") { + return property === "true"; + } + // str / default + return property; + } + + function isEmpty(v) { + return v === undefined || v === null || v === ""; + } + + node.on("input", async function(msg) { + if (!node.server || !node.server.matrixClient) { + node.warn("No matrix server selected"); + return; + } + if (!node.server.isConnected()) { + node.error("Matrix server connection is currently closed", msg); + node.send([null, msg]); + return; + } + + msg.topic = node.roomId || msg.topic; + if (!msg.topic) { + node.error("Room must be specified in msg.topic or in configuration", msg); + return; + } + + // Resolve every typed input up-front so a single bad config or + // flow/global lookup surfaces as one clear error. + let rawLat, rawLng, rawAlt, rawGeoUri, description, assetType, rawTimestamp, text; + try { + rawLat = getToValue(msg, node.latitudeType, node.latitudeValue); + rawLng = getToValue(msg, node.longitudeType, node.longitudeValue); + rawAlt = getToValue(msg, node.altitudeType, node.altitudeValue); + rawGeoUri = getToValue(msg, node.geoUriType, node.geoUriValue); + description = getToValue(msg, node.descriptionType, node.descriptionValue); + assetType = getToValue(msg, node.assetTypeType, node.assetTypeValue); + rawTimestamp = getToValue(msg, node.timestampType, node.timestampValue); + text = getToValue(msg, node.textType, node.textValue); + } catch (e) { + node.error(e.message, msg); + node.send([null, msg]); + return; + } + + // Build the geo URI: prefer an explicit geo_uri when supplied; + // otherwise build geo:,[,] from numeric inputs. + let geoUri = isEmpty(rawGeoUri) ? null : String(rawGeoUri); + if (!geoUri) { + const lat = parseFloat(rawLat); + const lng = parseFloat(rawLng); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) { + node.error("Latitude and longitude (numbers) - or a geo_uri - are required", msg); + node.send([null, msg]); + return; + } + if (lat < -90 || lat > 90) { + node.error("Latitude (" + lat + ") is out of range; must be between -90 and 90", msg); + node.send([null, msg]); + return; + } + if (lng < -180 || lng > 180) { + node.error("Longitude (" + lng + ") is out of range; must be between -180 and 180", msg); + node.send([null, msg]); + return; + } + geoUri = "geo:" + lat + "," + lng; + if (!isEmpty(rawAlt)) { + const alt = parseFloat(rawAlt); + if (!Number.isFinite(alt)) { + node.error("Altitude must be a number when provided", msg); + node.send([null, msg]); + return; + } + geoUri = "geo:" + lat + "," + lng + "," + alt; + } + } + + // Asset type defaults to m.self if the resolved value is empty. + if (isEmpty(assetType)) { + assetType = "m.self"; + } + if (VALID_ASSET_TYPES.indexOf(assetType) === -1) { + node.error('Invalid asset type "' + assetType + '"; must be "m.self" or "m.pin"', msg); + node.send([null, msg]); + return; + } + + // Timestamp the location was correct, in ms since the UNIX epoch. + let timestamp = Date.now(); + if (!isEmpty(rawTimestamp)) { + const ts = Number(rawTimestamp); + if (Number.isFinite(ts)) { + timestamp = ts; + } + } + + // makeLocationContent uses `undefined` to mean "generate a default". + const cleanDescription = isEmpty(description) ? undefined : String(description); + const cleanText = isEmpty(text) ? undefined : String(text); + + try { + const { makeLocationContent } = await contentHelpersPromise; + const content = makeLocationContent(cleanText, geoUri, timestamp, cleanDescription, assetType); + const response = await node.server.matrixClient.sendMessage(msg.topic, content); + msg.eventId = response.event_id; + msg.payload = content; + node.send([msg, null]); + } catch (e) { + node.error("Error sending location: " + e, msg); + msg.error = e; + node.send([null, msg]); + } + }); + + node.on("close", function() { + node.server.deregister(node); + }); + } + + RED.nodes.registerType("matrix-send-location", MatrixSendLocation); +}; From af1067a99be0fb2902d4602a7453388f3c337937 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 23 May 2026 13:51:49 -0600 Subject: [PATCH 15/18] Refresh README features and tagline for v1.0.0 Tagline now mentions E2EE; the beta banner is replaced with a permanent issues/release-notes pointer. Features list gains Session management and Send Location, expands Receive events to all 10 handled msgtypes, and notes Markdown alongside HTML in Send & modify messages. --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2288fce..fcc5016 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # node-red-contrib-matrix-chat -[Matrix](https://matrix.org/) chat server client for [Node-RED](https://nodered.org/) +[Matrix](https://matrix.org/) chat client for [Node-RED](https://nodered.org/), with full end-to-end encryption support. -***Currently in beta. Please report any issues in our repository to help us reach a stable, well-tested release. Breaking changes may occur before our first stable release, so be sure to check the changelog before updating.*** +Please report any issues in our [issue tracker](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/issues). Breaking changes between releases are listed in the [release notes](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/releases); check them before upgrading. Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech](https://app.element.io/#/room/#node-red-contrib-matrix-chat:skylar.tech) @@ -11,14 +11,16 @@ Join our public Matrix room for help: [#node-red-contrib-matrix-chat:skylar.tech Supported functionality in this package includes: -- **End-to-end encryption (E2EE)** — send and receive encrypted messages (see the [encryption notes](#end-to-end-encryption-notes)) -- **Cross-signing & secure backup** — interactive setup from the server config node so the bot's own device shows as verified -- **Device verification** — interactive SAS (emoji) verification, either from the server config node or with the `matrix-verification` flow nodes -- **Receive events** from rooms: Messages, reactions, images, audio, locations, files, encrypted or unencrypted +- **End-to-end encryption (E2EE)**: send and receive encrypted messages (see the [encryption notes](#end-to-end-encryption-notes)) +- **Cross-signing & secure backup**: interactive setup from the server config node so the bot's own device shows as verified +- **Device verification**: interactive SAS (emoji) verification, either from the server config node or with the `matrix-verification` flow nodes +- **Session management**: review, verify, rename, or remove the account's sessions from the server config node (Element-style) +- **Receive events** from rooms (encrypted or unencrypted): messages, reactions, emotes, notices, stickers, images, video, audio, locations, files - **Fetch/modify room state**: Update room settings - **Paginate room history** - **Send files** to rooms, encrypted or unencrypted -- **Send/edit messages** (supports plain text and HTML formats) +- **Send & modify messages** (plain text, Markdown, and HTML formats) +- **Send location messages**: produce `m.location` events that match Element's *"Share my location"* wire format - **Send typing notifications** - **Delete events** (messages, reactions, etc.) - **Decrypt files** in E2EE rooms @@ -54,9 +56,9 @@ You're not limited to just the nodes we've created. Enable global access in your ### End-to-End Encryption Notes - E2EE uses the Rust crypto stack from `matrix-js-sdk`. The first time a bot starts after upgrading from an older version, any existing (legacy libolm) crypto state is migrated automatically. -- **Storage:** E2EE state is saved in a folder called `matrix-client-storage` within your Node-RED directory — each account's Rust crypto store is persisted there as `rust-crypto-store.v8` (snapshotted on shutdown and every 5 minutes). Setting up secure backup (below) lets you recover the account's keys even if this folder is lost. -- **Cross-signing & secure backup — strongly recommended:** open the server config node and use the **Set up secure backup & cross-signing** button. It lets you unlock an existing secure backup with its recovery key, or create a fresh one; once done, the bot's own device is cross-signed and shows as verified to others. **Save the recovery key somewhere safe** — it is shown only once, and is the only way to restore the account's encryption keys if the crypto store is ever lost. -- **Device verification:** there are two ways to verify devices — +- **Storage:** E2EE state is saved in a folder called `matrix-client-storage` within your Node-RED directory. Each account's Rust crypto store is persisted there as `rust-crypto-store.v8` (snapshotted on shutdown and every 5 minutes). Setting up secure backup (below) lets you recover the account's keys even if this folder is lost. +- **Cross-signing & secure backup (strongly recommended):** open the server config node and use the **Set up secure backup & cross-signing** button. It lets you unlock an existing secure backup with its recovery key, or create a fresh one; once done, the bot's own device is cross-signed and shows as verified to others. **Save the recovery key somewhere safe.** It is shown only once, and is the only way to restore the account's encryption keys if the crypto store is ever lost. +- **Device verification:** there are two ways to verify devices: - From the server config node, the **Pending verification requests** button opens a list of incoming requests and lets you complete the SAS (emoji) check interactively, no flow required. - Or build your own flow: the `matrix-verification` node emits verification requests and phase changes, and `matrix-verification-action` accepts, starts, confirms, or cancels them (e.g. emailing the SAS emoji for a human to confirm). See the [device verification example](https://github.com/Skylar-Tech/node-red-contrib-matrix-chat/tree/master/examples#device-verification). From 5012c603aa30dee4717d3a21b85b0d373a3fba22 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sat, 23 May 2026 15:46:09 -0600 Subject: [PATCH 16/18] Add Send Location example flow --- examples/README.md | 13 ++++++ examples/send-location-to-room.json | 69 ++++++++++++++++++++++++++++ examples/send-location-to-room.png | Bin 0 -> 9105 bytes 3 files changed, 82 insertions(+) create mode 100644 examples/send-location-to-room.json create mode 100644 examples/send-location-to-room.png diff --git a/examples/README.md b/examples/README.md index 223d3d0..6d606f0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -183,6 +183,19 @@ Any messages containing "delete" will be removed by the client.
    +
    +Send a location to a room + +[View JSON](send-location-to-room.json) + +Sends an `m.location` event (a map pin) for the country of Norway to the configured room. Element and other matrix-react-sdk clients render it as a pin on the map with the label *"Norway"*; clients without map rendering see an auto-generated text fallback. + +Update the `Send Location` node's Room ID to your own room before deploying. The inject node is configured to fire a bare message (no payload, no topic), so the Send Location node falls back to its configured values for everything: asset type `m.pin`, description `Norway`, and geo URI `geo:60.4720,8.4689`. + +![send-location-to-room.png](send-location-to-room.png) + +
    + ### Event Handling
    diff --git a/examples/send-location-to-room.json b/examples/send-location-to-room.json new file mode 100644 index 0000000..e0472be --- /dev/null +++ b/examples/send-location-to-room.json @@ -0,0 +1,69 @@ +[ + { + "id": "222bb5ef43d621b4", + "type": "group", + "z": "f025a8b9fbd1b054", + "name": "Send location to room", + "style": { + "label": true + }, + "nodes": [ + "ee742dfa934b4892", + "1ef540382789ff9d" + ], + "x": 354, + "y": 5279, + "w": 392, + "h": 82 + }, + { + "id": "ee742dfa934b4892", + "type": "matrix-send-location", + "z": "f025a8b9fbd1b054", + "g": "222bb5ef43d621b4", + "name": "", + "server": "", + "roomId": "!example:test.org", + "latitudeType": "msg", + "latitudeValue": "latitude", + "longitudeType": "msg", + "longitudeValue": "longitude", + "altitudeType": "msg", + "altitudeValue": "altitude", + "geoUriType": "str", + "geoUriValue": "geo:60.4720,8.4689", + "descriptionType": "str", + "descriptionValue": "Norway", + "assetTypeType": "str", + "assetTypeValue": "m.pin", + "timestampType": "msg", + "timestampValue": "timestamp", + "textType": "msg", + "textValue": "payload", + "x": 640, + "y": 5320, + "wires": [ + [], + [] + ] + }, + { + "id": "1ef540382789ff9d", + "type": "inject", + "z": "f025a8b9fbd1b054", + "g": "222bb5ef43d621b4", + "name": "", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "onceDelay": 0.1, + "x": 460, + "y": 5320, + "wires": [ + [ + "ee742dfa934b4892" + ] + ] + } +] \ No newline at end of file diff --git a/examples/send-location-to-room.png b/examples/send-location-to-room.png new file mode 100644 index 0000000000000000000000000000000000000000..613cd0363e6e7425b49cadfabffe535a175118e3 GIT binary patch literal 9105 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 MQ`Q2PD?SeSACkC|C;$Ke literal 0 HcmV?d00001 From bc97c564d3990594fe85c210efc22a1c1ec62706 Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Sun, 24 May 2026 14:17:30 -0600 Subject: [PATCH 17/18] Fix libolm->rust crypto migration crash on localStorage stores matrix-js-sdk's LocalStorageCryptoStore.getEndToEndSessionsBatch() returns olm sessions without the deviceKey/sessionId fields, since those live only in the storage key path. initRustCrypto()'s migration then assigns the undefined deviceKey to PickledSession.senderKey and crashes in the WASM setter with "Cannot read properties of undefined (reading 'length')". The IndexedDB backend embeds those fields in the record and is unaffected, which is why this only hit installs that were started on the legacy localStorage crypto store. Patch the instance to re-derive deviceKey/sessionId from the crypto.sessions/ keys before returning a batch, so both the migration and its delete-after-migrate step work. --- src/matrix-crypto-store.js | 75 ++++++++++++++++++++++++++++++++++++- src/matrix-server-config.js | 16 ++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/matrix-crypto-store.js b/src/matrix-crypto-store.js index eb74cf3..55577d3 100644 --- a/src/matrix-crypto-store.js +++ b/src/matrix-crypto-store.js @@ -17,6 +17,13 @@ const v8 = require('v8'); let shimInstalled = false; +// Mirrors matrix-js-sdk's localStorage-crypto-store.js: olm sessions live under +// "crypto.sessions/" and migration batches are capped at 50. +const E2E_PREFIX = 'crypto.'; +const SESSION_KEY_PREFIX = E2E_PREFIX + 'sessions/'; +const SESSION_BATCH_SIZE = 50; +const OLM_BATCH_PATCH_MARKER = Symbol.for('node-red-contrib-matrix-chat.olmSessionBatchPatched'); + /** * Install the in-memory IndexedDB shim onto globalThis. Idempotent. Must be * called before MatrixClient.initRustCrypto(). @@ -172,4 +179,70 @@ async function snapshotCryptoStore(filePath, dbNamePrefix) { return true; } -module.exports = { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore }; +/** + * Patch a LocalStorageCryptoStore instance so its getEndToEndSessionsBatch() + * returns olm sessions with deviceKey/sessionId attached. + * + * Why: matrix-js-sdk's LocalStorageCryptoStore stores olm sessions as + * `{ session, lastReceivedMessageTs }` with the curve25519 deviceKey encoded + * only in the localStorage key ("crypto.sessions/"). On read, + * getEndToEndSessionsBatch() returns the bare session value without injecting + * the deviceKey or sessionId, so initRustCrypto()'s libolm-to-rust migration + * crashes at PickledSession.senderKey = session.deviceKey (undefined). The + * IndexedDB backend stores those fields in the record and is unaffected. + * + * Idempotent and safe to call on a store with no legacy sessions. + */ +function patchLocalStorageCryptoStoreForRustMigration(cryptoStore) { + if (!cryptoStore || cryptoStore[OLM_BATCH_PATCH_MARKER]) { + return cryptoStore; + } + const store = cryptoStore.store; + if (!store || typeof store.length !== 'number' || typeof store.key !== 'function') { + return cryptoStore; + } + cryptoStore.getEndToEndSessionsBatch = async function() { + const result = []; + for (let i = 0; i < store.length; i++) { + const key = store.key(i); + if (!key || !key.startsWith(SESSION_KEY_PREFIX)) { + continue; + } + const deviceKey = key.slice(SESSION_KEY_PREFIX.length); + let sessions; + try { + const raw = store.getItem(key); + sessions = raw ? JSON.parse(raw) : null; + } catch (e) { + sessions = null; + } + if (!sessions || typeof sessions !== 'object') { + continue; + } + for (const [sessionId, val] of Object.entries(sessions)) { + if (val === null || val === undefined) { + continue; + } + // Mirrors LocalStorageCryptoStore._getEndToEndSessions: very old + // entries were stored as bare base64 pickle strings. + const sessionInfo = (typeof val === 'string') + ? { session: val, lastReceivedMessageTs: 0 } + : val; + result.push({ ...sessionInfo, deviceKey, sessionId }); + if (result.length >= SESSION_BATCH_SIZE) { + return result; + } + } + } + return result.length === 0 ? null : result; + }; + cryptoStore[OLM_BATCH_PATCH_MARKER] = true; + return cryptoStore; +} + +module.exports = { + ensureIndexedDBShim, + restoreCryptoStore, + snapshotCryptoStore, + patchLocalStorageCryptoStoreForRustMigration, +}; diff --git a/src/matrix-server-config.js b/src/matrix-server-config.js index 702bc1a..88afc75 100644 --- a/src/matrix-server-config.js +++ b/src/matrix-server-config.js @@ -8,7 +8,12 @@ const cryptoApiPromise = import("matrix-js-sdk/lib/crypto-api/index.js"); const fs = require("fs-extra"); const { resolve } = require('path'); const { LocalStorage } = require('node-localstorage'); -const { ensureIndexedDBShim, restoreCryptoStore, snapshotCryptoStore } = require('./matrix-crypto-store'); +const { + ensureIndexedDBShim, + restoreCryptoStore, + snapshotCryptoStore, + patchLocalStorageCryptoStoreForRustMigration, +} = require('./matrix-crypto-store'); require("abort-controller/polyfill"); // polyfill abort-controller if we don't have it if (!globalThis.fetch) { // polyfill fetch if we don't have it @@ -453,8 +458,13 @@ module.exports = function(RED) { if(node.e2ee) { // Provide the legacy (pre-v37 libolm) crypto store so that // initRustCrypto() can perform a one-time migration of any - // existing crypto state into the Rust crypto store. - clientOpts.cryptoStore = new LocalStorageCryptoStore(localStorage); + // existing crypto state into the Rust crypto store. Patch + // the store because matrix-js-sdk's LocalStorageCryptoStore + // omits deviceKey/sessionId from getEndToEndSessionsBatch(), + // which breaks the libolm->rust olm-session migration. + clientOpts.cryptoStore = patchLocalStorageCryptoStoreForRustMigration( + new LocalStorageCryptoStore(localStorage) + ); } node.matrixClient = sdk.createClient(clientOpts); From f62ec9834b420b7d252f5a529a52c2780660c36c Mon Sep 17 00:00:00 2001 From: Skylar Sadlier Date: Tue, 26 May 2026 13:53:59 -0600 Subject: [PATCH 18/18] Bump got, image-size, mime, node-localstorage, sharp to latest got@15 default export, mime@4 ESM-only dynamic import, image-size@2 split into imageSize/imageSizeFromFile. --- package-lock.json | 1421 ++++++++++++++++++-------------- package.json | 10 +- src/matrix-crypt-file.js | 2 +- src/matrix-synapse-register.js | 2 +- src/matrix-upload-file.js | 19 +- 5 files changed, 816 insertions(+), 638 deletions(-) diff --git a/package-lock.json b/package-lock.json index 675f276..17ab8c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,16 +14,16 @@ "fake-indexeddb": "^6.2.5", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^11.1.0", - "got": "^12.0.2", - "image-size": "^1.0.2", + "got": "^15.0.5", + "image-size": "^2.0.2", "isomorphic-webcrypto": "^2.3.8", "linkifyjs": "^4.3.3", "lodash.escape": "^4.0.1", "matrix-js-sdk": "^41.5.0", - "mime": "^3.0.0", + "mime": "^4.1.0", "node-fetch": "^3.3.0", - "node-localstorage": "^2.2.1", - "sharp": "^0.33.4", + "node-localstorage": "^3.0.5", + "sharp": "^0.34.4", "tmp": "^0.2.1", "utf8": "^3.0.0" }, @@ -2648,9 +2648,10 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -3534,432 +3535,466 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -4190,6 +4225,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, "node_modules/@matrix-org/matrix-sdk-crypto-wasm": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", @@ -5906,6 +5947,12 @@ "react-native": "*" } }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -5949,11 +5996,12 @@ "peer": true }, "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-8.1.0.tgz", + "integrity": "sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==", + "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=22" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -5979,26 +6027,16 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/@types/events": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==" }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -6898,6 +6936,18 @@ "optional": true, "peer": true }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -6996,20 +7046,49 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", + "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" }, "engines": { - "node": ">=14.16" + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/call-bind": { @@ -7132,6 +7211,18 @@ "node": ">=10" } }, + "node_modules/chunk-data": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chunk-data/-/chunk-data-0.1.0.tgz", + "integrity": "sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -7237,22 +7328,11 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -7263,16 +7343,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true }, "node_modules/colorette": { "version": "1.4.0", @@ -7625,25 +7697,15 @@ } }, "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "mimic-response": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7706,14 +7768,6 @@ "node": ">=0.8" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -8605,14 +8659,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "engines": { - "node": ">= 14.17" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -8746,6 +8792,8 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -8826,29 +8874,46 @@ } }, "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/got/-/got-15.0.5.tgz", + "integrity": "sha512-PMIMaZuYUCK43+Z9JWEXea4kkX2b3301m81D5TS6QpfG4PmNyirzEdO/Oa2OHAN4GsjnPfvWCWsshKN2rq4/gQ==", + "license": "MIT", "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", + "@sindresorhus/is": "^8.0.0", + "byte-counter": "^0.1.0", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "cacheable-request": "^13.0.18", + "chunk-data": "^0.1.0", + "decompress-response": "^10.0.0", + "http2-wrapper": "^2.2.1", + "keyv": "^5.6.0", + "lowercase-keys": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^5.6.0", + "uint8array-extras": "^1.5.0" }, "engines": { - "node": ">=14.16" + "node": ">=22" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/got/node_modules/type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8992,9 +9057,10 @@ "peer": true }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" }, "node_modules/http-errors": { "version": "2.0.0", @@ -9024,9 +9090,10 @@ } }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -9104,17 +9171,15 @@ } }, "node_modules/image-size": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", - "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "dependencies": { - "queue": "6.0.2" - }, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", "bin": { "image-size": "bin/image-size.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.x" } }, "node_modules/import-fresh": { @@ -10074,11 +10139,6 @@ "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -10151,11 +10211,12 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/kind-of": { @@ -10711,11 +10772,12 @@ } }, "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-4.0.1.tgz", + "integrity": "sha512-wI9Nui/L8VfADa/cr/7NQruaASk1k23/Uh1khQ02BCVYiiy8F4AhOGnQzJy3Fl/c44GnYSbZHv8g7EcG3kJ1Qg==", + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11606,6 +11668,23 @@ "ms": "2.0.0" } }, + "node_modules/metro/node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/metro/node_modules/metro-react-native-babel-preset": { "version": "0.76.7", "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.7.tgz", @@ -11817,14 +11896,18 @@ } }, "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=10.0.0" + "node": ">=16" } }, "node_modules/mime-db": { @@ -11864,6 +11947,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -12195,24 +12279,40 @@ "peer": true }, "node_modules/node-localstorage": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", - "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-3.0.5.tgz", + "integrity": "sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg==", + "license": "MIT", "dependencies": { - "write-file-atomic": "^1.1.4" + "write-file-atomic": "^5.0.1" }, "engines": { "node": ">=0.12" } }, + "node_modules/node-localstorage/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/node-localstorage/node_modules/write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", "dependencies": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "slide": "^1.1.5" + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/node-releases": { @@ -12247,9 +12347,10 @@ } }, "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -12530,14 +12631,6 @@ "os-tmpdir": "^1.0.0" } }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "engines": { - "node": ">=12.20" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -13213,6 +13306,9 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "inherits": "~2.0.3" } @@ -13241,6 +13337,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -13679,7 +13776,8 @@ "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" }, "node_modules/resolve-from": { "version": "5.0.0", @@ -13692,14 +13790,27 @@ } }, "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/responselike/node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -13813,9 +13924,10 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -13975,48 +14087,54 @@ } }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/sharp/node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -14101,19 +14219,6 @@ "node": ">= 5.10.0" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -14176,14 +14281,6 @@ "optional": true, "peer": true }, - "node_modules/slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", - "engines": { - "node": "*" - } - }, "node_modules/slugify": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", @@ -14530,6 +14627,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -14943,6 +15052,18 @@ "node": ">=0.10.0" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unhomoglyph": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", @@ -17352,9 +17473,9 @@ } }, "@emnapi/runtime": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", - "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "optional": true, "requires": { "tslib": "^2.4.0" @@ -18109,145 +18230,186 @@ "@hapi/hoek": "^9.0.0" } }, + "@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==" + }, "@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "optional": true }, "@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "optional": true }, "@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "optional": true }, "@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "optional": true + }, + "@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "optional": true + }, + "@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", "optional": true }, "@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "optional": true }, "@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "optional": true }, "@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "optional": true, + "requires": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "optional": true, "requires": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "optional": true, "requires": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "optional": true, "requires": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.7.0" } }, + "@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "optional": true + }, "@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "optional": true }, "@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "optional": true }, "@jest/create-cache-key-function": { @@ -18448,6 +18610,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" + }, "@matrix-org/matrix-sdk-crypto-wasm": { "version": "18.3.0", "resolved": "https://registry.npmjs.org/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-18.3.0.tgz", @@ -19749,6 +19916,11 @@ "nullthrows": "^1.1.1" } }, + "@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" + }, "@segment/loosely-validate-event": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", @@ -19792,9 +19964,9 @@ "peer": true }, "@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==" + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-8.1.0.tgz", + "integrity": "sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==" }, "@sinonjs/commons": { "version": "3.0.0", @@ -19816,23 +19988,15 @@ "@sinonjs/commons": "^3.0.0" } }, - "@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "requires": { - "defer-to-connect": "^2.0.1" - } - }, "@types/events": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==" }, "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==" }, "@types/istanbul-lib-coverage": { "version": "2.0.4", @@ -20580,6 +20744,11 @@ "optional": true, "peer": true }, + "byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==" + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -20656,17 +20825,33 @@ "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==" }, "cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", + "version": "13.0.19", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", + "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", "requires": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", + "@types/http-cache-semantics": "^4.2.0", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.6.0", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" + "normalize-url": "^8.1.1", + "responselike": "^4.0.2" + }, + "dependencies": { + "get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "requires": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + } + }, + "is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==" + } } }, "call-bind": { @@ -20745,6 +20930,11 @@ "optional": true, "peer": true }, + "chunk-data": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chunk-data/-/chunk-data-0.1.0.tgz", + "integrity": "sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g==" + }, "ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -20819,19 +21009,11 @@ "shallow-clone": "^3.0.0" } }, - "color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "requires": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, "requires": { "color-name": "~1.1.4" } @@ -20839,16 +21021,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true }, "colorette": { "version": "1.4.0", @@ -21137,18 +21311,11 @@ "peer": true }, "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", "requires": { - "mimic-response": "^3.1.0" - }, - "dependencies": { - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } + "mimic-response": "^4.0.0" } }, "deep-extend": { @@ -21195,11 +21362,6 @@ } } }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" - }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -21903,11 +22065,6 @@ "mime-types": "^2.1.12" } }, - "form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==" - }, "formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -22006,7 +22163,9 @@ "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "optional": true, + "peer": true }, "getenv": { "version": "1.0.0", @@ -22060,21 +22219,32 @@ } }, "got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/got/-/got-15.0.5.tgz", + "integrity": "sha512-PMIMaZuYUCK43+Z9JWEXea4kkX2b3301m81D5TS6QpfG4PmNyirzEdO/Oa2OHAN4GsjnPfvWCWsshKN2rq4/gQ==", "requires": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", + "@sindresorhus/is": "^8.0.0", + "byte-counter": "^0.1.0", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "cacheable-request": "^13.0.18", + "chunk-data": "^0.1.0", + "decompress-response": "^10.0.0", + "http2-wrapper": "^2.2.1", + "keyv": "^5.6.0", + "lowercase-keys": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^5.6.0", + "uint8array-extras": "^1.5.0" + }, + "dependencies": { + "type-fest": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", + "requires": { + "tagged-tag": "^1.0.0" + } + } } }, "graceful-fs": { @@ -22186,9 +22356,9 @@ } }, "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" }, "http-errors": { "version": "2.0.0", @@ -22214,9 +22384,9 @@ } }, "http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -22265,12 +22435,9 @@ "peer": true }, "image-size": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.0.2.tgz", - "integrity": "sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==", - "requires": { - "queue": "6.0.2" - } + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==" }, "import-fresh": { "version": "2.0.0", @@ -23052,11 +23219,6 @@ "optional": true, "peer": true }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -23117,11 +23279,11 @@ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "requires": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "kind-of": { @@ -23490,9 +23652,9 @@ } }, "lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-4.0.1.tgz", + "integrity": "sha512-wI9Nui/L8VfADa/cr/7NQruaASk1k23/Uh1khQ02BCVYiiy8F4AhOGnQzJy3Fl/c44GnYSbZHv8g7EcG3kJ1Qg==" }, "lru-cache": { "version": "5.1.1", @@ -23710,6 +23872,16 @@ "ms": "2.0.0" } }, + "image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "optional": true, + "peer": true, + "requires": { + "queue": "6.0.2" + } + }, "metro-react-native-babel-preset": { "version": "0.76.7", "resolved": "https://registry.npmjs.org/metro-react-native-babel-preset/-/metro-react-native-babel-preset-0.76.7.tgz", @@ -24385,9 +24557,9 @@ } }, "mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==" }, "mime-db": { "version": "1.52.0", @@ -24668,21 +24840,25 @@ "peer": true }, "node-localstorage": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", - "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-3.0.5.tgz", + "integrity": "sha512-GCwtK33iwVXboZWYcqQHu3aRvXEBwmPkAMRBLeaX86ufhqslyUkLGsi4aW3INEfdQYpUB5M9qtYf3eHvAk2VBg==", "requires": { - "write-file-atomic": "^1.1.4" + "write-file-atomic": "^5.0.1" }, "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "slide": "^1.1.5" + "signal-exit": "^4.0.1" } } } @@ -24709,9 +24885,9 @@ "peer": true }, "normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==" + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==" }, "npm-package-arg": { "version": "7.0.0", @@ -24933,11 +25109,6 @@ "os-tmpdir": "^1.0.0" } }, - "p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -25443,6 +25614,8 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "optional": true, + "peer": true, "requires": { "inherits": "~2.0.3" } @@ -25832,11 +26005,18 @@ "peer": true }, "responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", "requires": { "lowercase-keys": "^3.0.0" + }, + "dependencies": { + "lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" + } } }, "restore-cursor": { @@ -25919,9 +26099,9 @@ "integrity": "sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==" }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==" }, "send": { "version": "0.18.0", @@ -26051,38 +26231,43 @@ } }, "sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "requires": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4", - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "@img/colour": "^1.0.0", + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "dependencies": { "detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" } } }, @@ -26153,21 +26338,6 @@ } } }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -26223,11 +26393,6 @@ } } }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==" - }, "slugify": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", @@ -26505,6 +26670,11 @@ "optional": true, "peer": true }, + "tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==" + }, "tar": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", @@ -26820,6 +26990,11 @@ } } }, + "uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==" + }, "unhomoglyph": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/unhomoglyph/-/unhomoglyph-1.0.6.tgz", diff --git a/package.json b/package.json index e4b9b62..69d0055 100644 --- a/package.json +++ b/package.json @@ -8,16 +8,16 @@ "fake-indexeddb": "^6.2.5", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^11.1.0", - "got": "^12.0.2", - "image-size": "^1.0.2", + "got": "^15.0.5", + "image-size": "^2.0.2", "isomorphic-webcrypto": "^2.3.8", "linkifyjs": "^4.3.3", "lodash.escape": "^4.0.1", "matrix-js-sdk": "^41.5.0", - "mime": "^3.0.0", + "mime": "^4.1.0", "node-fetch": "^3.3.0", - "node-localstorage": "^2.2.1", - "sharp": "^0.33.4", + "node-localstorage": "^3.0.5", + "sharp": "^0.34.4", "tmp": "^0.2.1", "utf8": "^3.0.0" }, diff --git a/src/matrix-crypt-file.js b/src/matrix-crypt-file.js index c15f6db..801a55c 100644 --- a/src/matrix-crypt-file.js +++ b/src/matrix-crypt-file.js @@ -9,7 +9,7 @@ module.exports = function(RED) { this.name = n.name; node.on("input", async function (msg) { - const { got } = await import('got'); + const got = (await import('got')).default; if(!msg.type) { node.error('msg.type is required.', msg); diff --git a/src/matrix-synapse-register.js b/src/matrix-synapse-register.js index 2135821..95b0ece 100644 --- a/src/matrix-synapse-register.js +++ b/src/matrix-synapse-register.js @@ -22,7 +22,7 @@ module.exports = function(RED) { } node.on("input", async function (msg) { - const { got } = await import('got'); + const got = (await import('got')).default; if(!msg.payload.username) { node.error("msg.payload.username is required", msg); diff --git a/src/matrix-upload-file.js b/src/matrix-upload-file.js index dd4d2a9..2ed9a2a 100644 --- a/src/matrix-upload-file.js +++ b/src/matrix-upload-file.js @@ -1,7 +1,8 @@ const crypto = require("isomorphic-webcrypto"); const ffmpeg = require('fluent-ffmpeg'); const sharp = require('sharp'); -const getImageSize = require('image-size'); +const { imageSize } = require('image-size'); +const { imageSizeFromFile } = require('image-size/fromFile'); const tmp = require('tmp'); const fs = require('fs'); const path = require('path'); @@ -37,7 +38,7 @@ module.exports = function(RED) { }); async function detectFileType(filename, bufferOrPath, msg) { - const Mime = require('mime'); + const Mime = (await import('mime')).default; let file = Buffer.isBuffer(bufferOrPath) ? filename : bufferOrPath; try { @@ -216,10 +217,10 @@ module.exports = function(RED) { async function addThumbnail(buffer) { try { - let imageSize = getImageSize(Buffer.isBuffer(buffer) ? buffer : buffer.data); + let thumbSize = imageSize(Buffer.isBuffer(buffer) ? buffer : buffer.data); msg.payload.info.thumbnail_info = { - w: imageSize.width, - h: imageSize.height, + w: thumbSize.width, + h: thumbSize.height, size: getFileSize(Buffer.isBuffer(buffer) ? buffer : buffer.data) } let uploadedThumbnail = await node.server.matrixClient.uploadContent( @@ -302,9 +303,11 @@ module.exports = function(RED) { if (msgtype === 'm.image') { // detect size of image try { - let imageSize = getImageSize(bufferOrPath); - msg.payload.info.h = imageSize.height; - msg.payload.info.w = imageSize.width; + let dimensions = Buffer.isBuffer(bufferOrPath) + ? imageSize(bufferOrPath) + : await imageSizeFromFile(bufferOrPath); + msg.payload.info.h = dimensions.height; + msg.payload.info.w = dimensions.width; // Generate thumbnail for image if (node.generateThumbnails) {