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.
This commit is contained in:
2026-05-22 14:40:00 -06:00
parent 68e63e5def
commit ebcb1eab81
19 changed files with 2528 additions and 536 deletions
+113
View File
@@ -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);
}