openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,079 lines (1,078 loc) • 46.8 kB
JavaScript
import { M as resolveTimestampMsToIsoString, S as resolveDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import "./number-runtime-DBLVDypr.js";
import { t as formatMatrixErrorMessage } from "./errors-C4iaVh6O.js";
import { t as ensureMatrixCryptoRuntime } from "./deps-D5b5VIwf.js";
import { n as LogService, r as noop } from "./logger-LOBoLzE6.js";
import { a as FDBFactory, c as FDBDatabase, d as FDBRecord, f as FDBIndex, g as FDBKeyRange, h as FDBCursor, i as fakeIndexedDB, l as FDBTransaction, m as FDBCursorWithValue, o as FDBVersionChangeEvent, p as FDBRequest, r as restoreIdbFromDisk, s as FDBOpenDBRequest, t as persistIdbToDisk, u as FDBObjectStore } from "./idb-persistence-Cdbd_6KM.js";
import { n as isRepairableSecretStorageAccessError } from "./recovery-key-store-HVlfPvzT.js";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
import { MatrixEventEvent } from "matrix-js-sdk/lib/matrix.js";
import { VerificationMethod } from "matrix-js-sdk/lib/types.js";
import { CryptoEvent } from "matrix-js-sdk/lib/crypto-api/CryptoEvent.js";
import { DecryptionFailureCode } from "matrix-js-sdk/lib/crypto-api/index.js";
import { VerificationPhase, VerificationRequestEvent, VerifierEvent } from "matrix-js-sdk/lib/crypto-api/verification.js";
//#region node_modules/fake-indexeddb/auto/index.mjs
var globalVar = typeof window !== "undefined" ? window : typeof WorkerGlobalScope !== "undefined" ? self : typeof global !== "undefined" ? global : Function("return this;")();
const createPropertyDescriptor = (value) => {
return {
value,
enumerable: false,
configurable: true,
writable: true
};
};
Object.defineProperties(globalVar, {
indexedDB: createPropertyDescriptor(fakeIndexedDB),
IDBCursor: createPropertyDescriptor(FDBCursor),
IDBCursorWithValue: createPropertyDescriptor(FDBCursorWithValue),
IDBDatabase: createPropertyDescriptor(FDBDatabase),
IDBFactory: createPropertyDescriptor(FDBFactory),
IDBIndex: createPropertyDescriptor(FDBIndex),
IDBKeyRange: createPropertyDescriptor(FDBKeyRange),
IDBObjectStore: createPropertyDescriptor(FDBObjectStore),
IDBOpenDBRequest: createPropertyDescriptor(FDBOpenDBRequest),
IDBRecord: createPropertyDescriptor(FDBRecord),
IDBRequest: createPropertyDescriptor(FDBRequest),
IDBTransaction: createPropertyDescriptor(FDBTransaction),
IDBVersionChangeEvent: createPropertyDescriptor(FDBVersionChangeEvent)
});
//#endregion
//#region extensions/matrix/src/matrix/sdk/verification-status.ts
function isMatrixDeviceLocallyVerified(status) {
return status?.localVerified === true;
}
function isMatrixDeviceOwnerVerified(status) {
return status?.crossSigningVerified === true;
}
function isMatrixDeviceVerifiedInCurrentClient(status) {
return status?.isVerified?.() === true || isMatrixDeviceLocallyVerified(status) || isMatrixDeviceOwnerVerified(status);
}
//#endregion
//#region extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts
const CROSS_SIGNING_PUBLICATION_WAIT_MS = 5e3;
var MatrixCryptoBootstrapper = class {
constructor(deps) {
this.deps = deps;
this.verificationHandlerRegistered = false;
}
async bootstrap(crypto, options = {}) {
const strict = options.strict === true;
const deferSecretStorageBootstrapUntilAfterCrossSigning = options.forceResetCrossSigning === true;
this.registerVerificationRequestHandler(crypto);
if (!deferSecretStorageBootstrapUntilAfterCrossSigning) await this.bootstrapSecretStorage(crypto, {
strict,
allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true
});
let crossSigning = await this.bootstrapCrossSigning(crypto, {
forceResetCrossSigning: options.forceResetCrossSigning === true,
allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false,
allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true,
strict
});
await this.bootstrapSecretStorage(crypto, {
strict,
allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true
});
if (deferSecretStorageBootstrapUntilAfterCrossSigning) crossSigning = await this.bootstrapCrossSigning(crypto, {
forceResetCrossSigning: false,
allowAutomaticCrossSigningReset: false,
allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey === true,
strict
});
const ownDeviceVerified = await this.ensureOwnDeviceTrust(crypto, { strict });
return {
crossSigningReady: crossSigning.ready,
crossSigningPublished: crossSigning.published,
ownDeviceVerified
};
}
createSigningKeysUiAuthCallback(params) {
return async (makeRequest) => {
try {
return await makeRequest(null);
} catch {
try {
return await makeRequest({ type: "m.login.dummy" });
} catch {
if (!params.password?.trim()) throw new Error("Matrix cross-signing key upload requires UIA; provide matrix.password for m.login.password fallback");
return await makeRequest({
type: "m.login.password",
identifier: {
type: "m.id.user",
user: params.userId
},
password: params.password
});
}
}
};
}
async bootstrapCrossSigning(crypto, options) {
const userId = await this.deps.getUserId();
const authUploadDeviceSigningKeys = this.createSigningKeysUiAuthCallback({
userId,
password: this.deps.getPassword?.()
});
const hasPublishedCrossSigningKeys = async () => {
if (typeof crypto.userHasCrossSigningKeys !== "function") return true;
try {
return await crypto.userHasCrossSigningKeys(userId, true);
} catch {
return false;
}
};
const refreshPublishedCrossSigningKeys = async () => {
if (typeof crypto.userHasCrossSigningKeys !== "function") return;
try {
await crypto.userHasCrossSigningKeys(userId, true);
} catch {}
};
const isCrossSigningReady = async () => {
if (typeof crypto.isCrossSigningReady !== "function") return true;
try {
return await crypto.isCrossSigningReady();
} catch {
return false;
}
};
const finalize = async () => {
const ready = await isCrossSigningReady();
const published = ready ? await waitForPublishedCrossSigningKeys() : await hasPublishedCrossSigningKeys();
if (ready && published) {
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
return {
ready,
published
};
}
const message = "Cross-signing bootstrap finished but server keys are still not published";
LogService.warn("MatrixClientLite", message);
if (options.strict) throw new Error(message);
return {
ready,
published
};
};
const waitForPublishedCrossSigningKeys = async () => {
const startedAt = Date.now();
do {
if (await hasPublishedCrossSigningKeys()) return true;
await setTimeout$1(250);
} while (Date.now() - startedAt < CROSS_SIGNING_PUBLICATION_WAIT_MS);
return false;
};
if (options.forceResetCrossSigning) {
const resetCrossSigning = async () => {
await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys
});
};
try {
await resetCrossSigning();
await this.trustFreshOwnIdentity(crypto);
} catch (err) {
if (options.allowSecretStorageRecreateWithoutRecoveryKey && isRepairableSecretStorageAccessError(err)) {
LogService.warn("MatrixClientLite", "Forced cross-signing reset could not unlock secret storage; recreating secret storage and retrying.");
try {
await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
allowSecretStorageRecreateWithoutRecoveryKey: true,
forceNewSecretStorage: true
});
await resetCrossSigning();
await this.trustFreshOwnIdentity(crypto);
} catch (repairErr) {
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", repairErr);
if (options.strict) throw repairErr instanceof Error ? repairErr : new Error(String(repairErr));
return {
ready: false,
published: false
};
}
return await finalize();
}
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", err);
if (options.strict) throw err instanceof Error ? err : new Error(String(err));
return {
ready: false,
published: false
};
}
return await finalize();
}
try {
await refreshPublishedCrossSigningKeys();
await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys });
} catch (err) {
if (options.allowSecretStorageRecreateWithoutRecoveryKey && isRepairableSecretStorageAccessError(err)) {
LogService.warn("MatrixClientLite", "Cross-signing bootstrap could not unlock secret storage; recreating secret storage during explicit bootstrap and retrying.");
await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, {
allowSecretStorageRecreateWithoutRecoveryKey: true,
forceNewSecretStorage: true
});
await crypto.bootstrapCrossSigning({ authUploadDeviceSigningKeys });
} else if (!options.allowAutomaticCrossSigningReset) {
LogService.warn("MatrixClientLite", "Initial cross-signing bootstrap failed and automatic reset is disabled:", err);
return {
ready: false,
published: false
};
} else {
LogService.warn("MatrixClientLite", "Initial cross-signing bootstrap failed, trying reset:", err);
try {
await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys
});
} catch (resetErr) {
LogService.warn("MatrixClientLite", "Failed to bootstrap cross-signing:", resetErr);
if (options.strict) throw resetErr instanceof Error ? resetErr : new Error(String(resetErr));
return {
ready: false,
published: false
};
}
}
}
const firstPassReady = await isCrossSigningReady();
const firstPassPublished = await hasPublishedCrossSigningKeys();
if (firstPassReady && firstPassPublished) {
LogService.info("MatrixClientLite", "Cross-signing bootstrap complete");
return {
ready: true,
published: true
};
}
if (!options.allowAutomaticCrossSigningReset) return {
ready: firstPassReady,
published: firstPassPublished
};
try {
await crypto.bootstrapCrossSigning({
setupNewCrossSigning: true,
authUploadDeviceSigningKeys
});
await this.trustFreshOwnIdentity(crypto);
} catch (err) {
LogService.warn("MatrixClientLite", "Fallback cross-signing bootstrap failed:", err);
if (options.strict) throw err instanceof Error ? err : new Error(String(err));
return {
ready: false,
published: false
};
}
return await finalize();
}
async trustFreshOwnIdentity(crypto) {
const ownIdentity = typeof crypto.getOwnIdentity === "function" ? await crypto.getOwnIdentity().catch(() => void 0) : void 0;
if (!ownIdentity) return;
try {
if (typeof ownIdentity.isVerified === "function" && ownIdentity.isVerified()) return;
await ownIdentity.verify?.();
} finally {
ownIdentity.free?.();
}
}
async bootstrapSecretStorage(crypto, options) {
try {
await this.deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey(crypto, { allowSecretStorageRecreateWithoutRecoveryKey: options.allowSecretStorageRecreateWithoutRecoveryKey });
LogService.info("MatrixClientLite", "Secret storage bootstrap complete");
} catch (err) {
LogService.warn("MatrixClientLite", "Failed to bootstrap secret storage:", err);
if (options.strict) throw err instanceof Error ? err : new Error(String(err));
}
}
registerVerificationRequestHandler(crypto) {
if (this.verificationHandlerRegistered) return;
this.verificationHandlerRegistered = true;
crypto.on(CryptoEvent.VerificationRequestReceived, (request) => {
const verificationRequest = request;
try {
this.deps.verificationManager.trackVerificationRequest(verificationRequest);
} catch (err) {
LogService.warn("MatrixClientLite", `Failed to track verification request from ${verificationRequest.otherUserId}:`, err);
}
});
this.deps.decryptBridge.bindCryptoRetrySignals(crypto);
LogService.info("MatrixClientLite", "Verification request handler registered");
}
async ensureOwnDeviceTrust(crypto, options) {
const deviceId = this.deps.getDeviceId()?.trim();
if (!deviceId) return null;
const userId = await this.deps.getUserId();
if (isMatrixDeviceOwnerVerified(typeof crypto.getDeviceVerificationStatus === "function" ? await crypto.getDeviceVerificationStatus(userId, deviceId).catch(() => null) : null)) return true;
if (typeof crypto.setDeviceVerified === "function") await crypto.setDeviceVerified(userId, deviceId, true);
if (typeof crypto.crossSignDevice === "function") {
if (typeof crypto.isCrossSigningReady === "function" ? await crypto.isCrossSigningReady() : true) await crypto.crossSignDevice(deviceId);
}
const verified = isMatrixDeviceOwnerVerified(typeof crypto.getDeviceVerificationStatus === "function" ? await crypto.getDeviceVerificationStatus(userId, deviceId).catch(() => null) : null);
if (!verified && options.strict) throw new Error(`Matrix own device ${deviceId} does not have full Matrix identity trust after bootstrap`);
return verified;
}
};
//#endregion
//#region extensions/matrix/src/matrix/sdk/crypto-facade.ts
let matrixCryptoNodeRuntimePromise = null;
async function loadMatrixCryptoNodeRuntime() {
matrixCryptoNodeRuntimePromise ??= import("./crypto-node.runtime.js").catch((error) => {
matrixCryptoNodeRuntimePromise = null;
throw error;
});
return await matrixCryptoNodeRuntimePromise;
}
async function loadMatrixCryptoNodeBindings() {
await ensureMatrixCryptoRuntime();
return (await loadMatrixCryptoNodeRuntime()).loadMatrixCryptoNodeBindings();
}
function trackInProgressToDeviceVerifications(deps) {
const crypto = deps.client.getCrypto();
const userId = deps.client.getUserId();
if (!userId || typeof crypto?.getVerificationRequestsToDeviceInProgress !== "function") return;
for (const request of crypto.getVerificationRequestsToDeviceInProgress(userId)) deps.verificationManager.trackVerificationRequest(request);
}
function createMatrixCryptoFacade(deps) {
return {
prepare: async (_joinedRooms) => {},
updateSyncData: async (_toDeviceMessages, _otkCounts, _unusedFallbackKeyAlgs, _changedDeviceLists, _leftDeviceLists) => {},
isRoomEncrypted: async (roomId) => {
if (deps.client.getRoom(roomId)?.hasEncryptionStateEvent()) return true;
try {
const event = await deps.getRoomStateEvent(roomId, "m.room.encryption", "");
return typeof event.algorithm === "string" && event.algorithm.length > 0;
} catch {
return false;
}
},
requestOwnUserVerification: async () => {
const crypto = deps.client.getCrypto();
return await deps.verificationManager.requestOwnUserVerification(crypto);
},
encryptMedia: async (buffer) => {
const { Attachment } = await loadMatrixCryptoNodeBindings();
const encrypted = Attachment.encrypt(new Uint8Array(buffer));
const mediaInfoJson = encrypted.mediaEncryptionInfo;
if (!mediaInfoJson) throw new Error("Matrix media encryption failed: missing media encryption info");
const parsed = JSON.parse(mediaInfoJson);
return {
buffer: Buffer.from(encrypted.encryptedData),
file: {
key: parsed.key,
iv: parsed.iv,
hashes: parsed.hashes,
v: parsed.v
}
};
},
decryptMedia: async (file, opts) => {
const { Attachment, EncryptedAttachment } = await loadMatrixCryptoNodeBindings();
const encrypted = await deps.downloadContent(file.url, opts);
const metadata = {
url: file.url,
key: file.key,
iv: file.iv,
hashes: file.hashes,
v: file.v
};
const attachment = new EncryptedAttachment(new Uint8Array(encrypted), JSON.stringify(metadata));
const decrypted = Attachment.decrypt(attachment);
return Buffer.from(decrypted);
},
getRecoveryKey: async () => {
return deps.recoveryKeyStore.getRecoveryKeySummary();
},
listVerifications: async () => {
trackInProgressToDeviceVerifications(deps);
return deps.verificationManager.listVerifications();
},
ensureVerificationDmTracked: async ({ roomId, userId }) => {
const crypto = deps.client.getCrypto();
const request = typeof crypto?.findVerificationRequestDMInProgress === "function" ? crypto.findVerificationRequestDMInProgress(roomId, userId) : void 0;
if (!request) return null;
return deps.verificationManager.trackVerificationRequest(request);
},
requestVerification: async (params) => {
const crypto = deps.client.getCrypto();
return await deps.verificationManager.requestVerification(crypto, params);
},
acceptVerification: async (id) => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.acceptVerification(id);
},
cancelVerification: async (id, params) => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.cancelVerification(id, params);
},
startVerification: async (id, method = "sas") => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.startVerification(id, method);
},
generateVerificationQr: async (id) => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.generateVerificationQr(id);
},
scanVerificationQr: async (id, qrDataBase64) => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.scanVerificationQr(id, qrDataBase64);
},
confirmVerificationSas: async (id) => {
trackInProgressToDeviceVerifications(deps);
return await deps.verificationManager.confirmVerificationSas(id);
},
mismatchVerificationSas: async (id) => {
trackInProgressToDeviceVerifications(deps);
return deps.verificationManager.mismatchVerificationSas(id);
},
confirmVerificationReciprocateQr: async (id) => {
trackInProgressToDeviceVerifications(deps);
return deps.verificationManager.confirmVerificationReciprocateQr(id);
},
getVerificationSas: async (id) => {
trackInProgressToDeviceVerifications(deps);
return deps.verificationManager.getVerificationSas(id);
}
};
}
//#endregion
//#region extensions/matrix/src/matrix/sdk/decrypt-bridge.ts
const MATRIX_DECRYPT_RETRY_BASE_DELAY_MS = 1500;
const MATRIX_DECRYPT_RETRY_MAX_DELAY_MS = 3e4;
const MATRIX_DECRYPT_RETRY_MAX_ATTEMPTS = 8;
function resolveDecryptRetryKey(roomId, eventId) {
if (!roomId || !eventId) return null;
return `${roomId}|${eventId}`;
}
function isDecryptionFailure(event) {
return typeof event.isDecryptionFailure === "function" && event.isDecryptionFailure();
}
function getDecryptionFailureReason(event) {
const reason = event.decryptionFailureReason;
return typeof reason === "string" && reason in DecryptionFailureCode ? reason : null;
}
function shouldRetryDecryptionFailure(event) {
if (!isDecryptionFailure(event)) return false;
const reason = getDecryptionFailureReason(event);
if (!reason) return true;
return reason === DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID || reason === DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX || reason === DecryptionFailureCode.UNKNOWN_ERROR;
}
var MatrixDecryptBridge = class {
constructor(deps) {
this.deps = deps;
this.trackedEncryptedEvents = /* @__PURE__ */ new WeakSet();
this.decryptedMessageDedupe = /* @__PURE__ */ new Map();
this.decryptRetries = /* @__PURE__ */ new Map();
this.failedDecryptionsNotified = /* @__PURE__ */ new Set();
this.exhaustedDecryptRetries = /* @__PURE__ */ new Set();
this.activeRetryRuns = 0;
this.retryIdleResolvers = /* @__PURE__ */ new Set();
this.cryptoRetrySignalsBound = false;
}
shouldEmitUnencryptedMessage(roomId, eventId) {
if (!eventId) return true;
const key = `${roomId}|${eventId}`;
if (this.decryptedMessageDedupe.get(key) === void 0) return true;
this.decryptedMessageDedupe.delete(key);
return false;
}
attachEncryptedEvent(event, roomId) {
if (this.trackedEncryptedEvents.has(event)) return;
this.trackedEncryptedEvents.add(event);
event.on(MatrixEventEvent.Decrypted, (decryptedEvent, err) => {
this.handleEncryptedEventDecrypted({
roomId,
encryptedEvent: event,
decryptedEvent,
err
});
});
if (shouldRetryDecryptionFailure(event)) {
const eventId = this.deps.toRaw(event).event_id || event.getId() || "";
this.scheduleDecryptRetry({
event,
roomId,
eventId
});
}
}
retryPendingNow(reason) {
const pending = Array.from(this.decryptRetries.entries());
if (pending.length === 0) return;
LogService.debug("MatrixClientLite", `Retrying pending decryptions due to ${reason}`);
for (const [retryKey, state] of pending) {
if (state.timer) {
clearTimeout(state.timer);
state.timer = null;
}
if (state.inFlight) continue;
this.runDecryptRetry(retryKey).catch(noop);
}
}
bindCryptoRetrySignals(crypto) {
if (!crypto || this.cryptoRetrySignalsBound) return;
this.cryptoRetrySignalsBound = true;
const trigger = (reason) => {
this.retryPendingNow(reason);
};
crypto.on(CryptoEvent.KeyBackupDecryptionKeyCached, () => {
trigger("crypto.keyBackupDecryptionKeyCached");
});
crypto.on(CryptoEvent.RehydrationCompleted, () => {
trigger("dehydration.RehydrationCompleted");
});
crypto.on(CryptoEvent.DevicesUpdated, () => {
trigger("crypto.devicesUpdated");
});
crypto.on(CryptoEvent.KeysChanged, () => {
trigger("crossSigning.keysChanged");
});
}
stop() {
for (const retryKey of this.decryptRetries.keys()) this.clearDecryptRetry(retryKey);
}
async drainPendingDecryptions(reason) {
for (let attempts = 0; attempts < MATRIX_DECRYPT_RETRY_MAX_ATTEMPTS; attempts += 1) {
if (this.decryptRetries.size === 0) return;
this.retryPendingNow(reason);
await this.waitForActiveRetryRunsToFinish();
if (!Array.from(this.decryptRetries.values()).some((state) => state.timer || state.inFlight)) return;
}
}
handleEncryptedEventDecrypted(params) {
const decryptedRoomId = params.decryptedEvent.getRoomId() || params.roomId;
const decryptedRaw = this.deps.toRaw(params.decryptedEvent);
const retryEventId = decryptedRaw.event_id || params.encryptedEvent.getId() || "";
const retryKey = resolveDecryptRetryKey(decryptedRoomId, retryEventId);
if (params.err) {
this.emitFailedDecryptionOnce(retryKey, decryptedRoomId, decryptedRaw, params.err);
if (shouldRetryDecryptionFailure(params.decryptedEvent)) this.scheduleDecryptRetry({
event: params.encryptedEvent,
roomId: decryptedRoomId,
eventId: retryEventId
});
else if (retryKey) this.clearDecryptRetry(retryKey);
return;
}
if (isDecryptionFailure(params.decryptedEvent)) {
this.emitFailedDecryptionOnce(retryKey, decryptedRoomId, decryptedRaw, /* @__PURE__ */ new Error("Matrix event failed to decrypt"));
if (shouldRetryDecryptionFailure(params.decryptedEvent)) this.scheduleDecryptRetry({
event: params.encryptedEvent,
roomId: decryptedRoomId,
eventId: retryEventId
});
else if (retryKey) this.clearDecryptRetry(retryKey);
return;
}
if (retryKey) this.clearDecryptRetry(retryKey);
this.rememberDecryptedMessage(decryptedRoomId, decryptedRaw.event_id);
this.deps.emitDecryptedEvent(decryptedRoomId, decryptedRaw);
this.deps.emitMessage(decryptedRoomId, decryptedRaw);
}
emitFailedDecryptionOnce(retryKey, roomId, event, error) {
if (retryKey) {
if (this.failedDecryptionsNotified.has(retryKey)) return;
this.failedDecryptionsNotified.add(retryKey);
}
this.deps.emitFailedDecryption(roomId, event, error);
}
scheduleDecryptRetry(params) {
const retryKey = resolveDecryptRetryKey(params.roomId, params.eventId);
if (!retryKey) return;
const existing = this.decryptRetries.get(retryKey);
if (this.exhaustedDecryptRetries.has(retryKey)) return;
if (existing?.timer || existing?.inFlight) return;
const attempts = (existing?.attempts ?? 0) + 1;
if (attempts > MATRIX_DECRYPT_RETRY_MAX_ATTEMPTS) {
const retry = this.decryptRetries.get(retryKey);
if (retry?.timer) clearTimeout(retry.timer);
this.decryptRetries.delete(retryKey);
this.exhaustedDecryptRetries.add(retryKey);
LogService.debug("MatrixClientLite", `Giving up decryption retry for ${params.eventId} in ${params.roomId} after ${attempts - 1} attempts`);
return;
}
const delayMs = Math.min(MATRIX_DECRYPT_RETRY_BASE_DELAY_MS * 2 ** (attempts - 1), MATRIX_DECRYPT_RETRY_MAX_DELAY_MS);
const next = {
event: params.event,
roomId: params.roomId,
eventId: params.eventId,
attempts,
inFlight: false,
timer: null
};
next.timer = setTimeout(() => {
this.runDecryptRetry(retryKey).catch(noop);
}, delayMs);
this.decryptRetries.set(retryKey, next);
}
async runDecryptRetry(retryKey) {
const state = this.decryptRetries.get(retryKey);
if (!state || state.inFlight) return;
state.inFlight = true;
state.timer = null;
this.activeRetryRuns += 1;
if (!(typeof this.deps.client.decryptEventIfNeeded === "function")) {
this.clearDecryptRetry(retryKey);
this.activeRetryRuns = Math.max(0, this.activeRetryRuns - 1);
this.resolveRetryIdleIfNeeded();
return;
}
try {
await this.deps.client.decryptEventIfNeeded?.(state.event, { isRetry: true });
} catch {} finally {
state.inFlight = false;
this.activeRetryRuns = Math.max(0, this.activeRetryRuns - 1);
this.resolveRetryIdleIfNeeded();
}
if (this.decryptRetries.get(retryKey) !== state) return;
if (isDecryptionFailure(state.event)) {
if (!shouldRetryDecryptionFailure(state.event)) {
this.clearDecryptRetry(retryKey);
return;
}
this.scheduleDecryptRetry(state);
return;
}
this.clearDecryptRetry(retryKey);
const raw = this.deps.toRaw(state.event);
this.rememberDecryptedMessage(state.roomId, raw.event_id);
this.deps.emitDecryptedEvent(state.roomId, raw);
this.deps.emitMessage(state.roomId, raw);
}
clearDecryptRetry(retryKey) {
const state = this.decryptRetries.get(retryKey);
if (state?.timer) clearTimeout(state.timer);
this.decryptRetries.delete(retryKey);
this.exhaustedDecryptRetries.delete(retryKey);
this.failedDecryptionsNotified.delete(retryKey);
}
rememberDecryptedMessage(roomId, eventId) {
if (!eventId) return;
const now = Date.now();
this.pruneDecryptedMessageDedupe(now);
this.decryptedMessageDedupe.set(`${roomId}|${eventId}`, now);
}
pruneDecryptedMessageDedupe(now) {
const ttlMs = 3e4;
for (const [key, createdAt] of this.decryptedMessageDedupe) if (now - createdAt > ttlMs) this.decryptedMessageDedupe.delete(key);
const maxEntries = 2048;
while (this.decryptedMessageDedupe.size > maxEntries) {
const oldest = this.decryptedMessageDedupe.keys().next().value;
if (oldest === void 0) break;
this.decryptedMessageDedupe.delete(oldest);
}
}
async waitForActiveRetryRunsToFinish() {
if (this.activeRetryRuns === 0) return;
await new Promise((resolve) => {
this.retryIdleResolvers.add(resolve);
if (this.activeRetryRuns === 0) {
this.retryIdleResolvers.delete(resolve);
resolve();
}
});
}
resolveRetryIdleIfNeeded() {
if (this.activeRetryRuns !== 0) return;
for (const resolve of this.retryIdleResolvers) resolve();
this.retryIdleResolvers.clear();
}
};
//#endregion
//#region extensions/matrix/src/matrix/sdk/verification-manager.ts
const MATRIX_VERIFICATION_PHASES = new Set([
-1,
VerificationPhase.Unsent,
VerificationPhase.Requested,
VerificationPhase.Ready,
VerificationPhase.Started,
VerificationPhase.Cancelled,
VerificationPhase.Done
]);
function isMatrixVerificationPhase(value) {
return typeof value === "number" && MATRIX_VERIFICATION_PHASES.has(value);
}
const MAX_TRACKED_VERIFICATION_SESSIONS = 256;
const TERMINAL_SESSION_RETENTION_MS = 1440 * 60 * 1e3;
const SAS_AUTO_CONFIRM_DELAY_MS = 3e4;
var MatrixVerificationManager = class {
constructor(opts = {}) {
this.opts = opts;
this.verificationSessions = /* @__PURE__ */ new Map();
this.verificationSessionCounter = 0;
this.trackedVerificationRequests = /* @__PURE__ */ new WeakSet();
this.trackedVerificationVerifiers = /* @__PURE__ */ new WeakSet();
this.summaryListeners = /* @__PURE__ */ new Set();
}
readRequestValue(_request, reader, fallback) {
try {
return reader();
} catch {
return fallback;
}
}
readVerificationPhase(request, fallback) {
const phase = this.readRequestValue(request, () => request.phase, fallback);
return isMatrixVerificationPhase(phase) ? phase : fallback;
}
readVerificationRequestIdentity(request) {
return {
transactionId: this.readRequestValue(request, () => request.transactionId?.trim() ?? "", ""),
roomId: this.readRequestValue(request, () => request.roomId ?? "", ""),
otherUserId: this.readRequestValue(request, () => request.otherUserId, ""),
otherDeviceId: this.readRequestValue(request, () => request.otherDeviceId ?? "", ""),
isSelfVerification: this.readRequestValue(request, () => request.isSelfVerification, false),
initiatedByMe: this.readRequestValue(request, () => request.initiatedByMe, false)
};
}
isSameLogicalVerificationRequest(left, right) {
const leftIdentity = this.readVerificationRequestIdentity(left);
const rightIdentity = this.readVerificationRequestIdentity(right);
return leftIdentity.transactionId !== "" && leftIdentity.transactionId === rightIdentity.transactionId && leftIdentity.roomId === rightIdentity.roomId && leftIdentity.otherUserId === rightIdentity.otherUserId && this.isSameOptionalIdentityValue(leftIdentity.otherDeviceId, rightIdentity.otherDeviceId) && leftIdentity.isSelfVerification === rightIdentity.isSelfVerification && leftIdentity.initiatedByMe === rightIdentity.initiatedByMe;
}
isSameOptionalIdentityValue(left, right) {
return left === "" || right === "" || left === right;
}
pruneVerificationSessions(nowMs) {
for (const [id, session] of this.verificationSessions) {
const phase = this.readVerificationPhase(session.request, -1);
if ((phase === VerificationPhase.Done || phase === VerificationPhase.Cancelled) && nowMs - session.updatedAtMs > TERMINAL_SESSION_RETENTION_MS) this.verificationSessions.delete(id);
}
if (this.verificationSessions.size <= MAX_TRACKED_VERIFICATION_SESSIONS) return;
const sortedByAge = Array.from(this.verificationSessions.entries()).toSorted((a, b) => a[1].updatedAtMs - b[1].updatedAtMs);
const overflow = this.verificationSessions.size - MAX_TRACKED_VERIFICATION_SESSIONS;
for (let i = 0; i < overflow; i += 1) {
const entry = sortedByAge[i];
if (entry) this.verificationSessions.delete(entry[0]);
}
}
getVerificationPhaseName(phase) {
switch (phase) {
case VerificationPhase.Unsent: return "unsent";
case VerificationPhase.Requested: return "requested";
case VerificationPhase.Ready: return "ready";
case VerificationPhase.Started: return "started";
case VerificationPhase.Cancelled: return "cancelled";
case VerificationPhase.Done: return "done";
default: return `unknown(${phase})`;
}
}
emitVerificationSummary(session) {
const summary = this.buildVerificationSummary(session);
for (const listener of this.summaryListeners) listener(summary);
}
touchVerificationSession(session) {
session.updatedAtMs = resolveDateTimestampMs(Date.now());
this.emitVerificationSummary(session);
}
clearSasAutoConfirmTimer(session) {
if (!session.sasAutoConfirmTimer) return;
clearTimeout(session.sasAutoConfirmTimer);
session.sasAutoConfirmTimer = void 0;
}
buildVerificationSummary(session) {
const request = session.request;
const phase = this.readVerificationPhase(request, VerificationPhase.Requested);
const accepting = this.readRequestValue(request, () => request.accepting, false);
const declining = this.readRequestValue(request, () => request.declining, false);
const pending = this.readRequestValue(request, () => request.pending, false);
const methodsRaw = this.readRequestValue(request, () => request.methods, []);
const methods = Array.isArray(methodsRaw) ? methodsRaw.filter((entry) => typeof entry === "string") : [];
const sasCallbacks = session.sasCallbacks ?? session.activeVerifier?.getShowSasCallbacks();
if (sasCallbacks) session.sasCallbacks = sasCallbacks;
const canAccept = phase < VerificationPhase.Ready && !accepting && !declining;
return {
id: session.id,
transactionId: this.readRequestValue(request, () => request.transactionId, void 0),
roomId: this.readRequestValue(request, () => request.roomId, void 0),
otherUserId: this.readRequestValue(request, () => request.otherUserId, "unknown"),
otherDeviceId: this.readRequestValue(request, () => request.otherDeviceId, void 0),
isSelfVerification: this.readRequestValue(request, () => request.isSelfVerification, false),
initiatedByMe: this.readRequestValue(request, () => request.initiatedByMe, false),
phase,
phaseName: this.getVerificationPhaseName(phase),
pending,
methods,
chosenMethod: this.readRequestValue(request, () => request.chosenMethod ?? null, null),
canAccept,
hasSas: Boolean(sasCallbacks),
sas: sasCallbacks ? {
decimal: sasCallbacks.sas.decimal,
emoji: sasCallbacks.sas.emoji
} : void 0,
hasReciprocateQr: Boolean(session.reciprocateQrCallbacks),
completed: phase === VerificationPhase.Done,
error: session.error,
createdAt: resolveTimestampMsToIsoString(session.createdAtMs),
updatedAt: resolveTimestampMsToIsoString(session.updatedAtMs)
};
}
findVerificationSession(id) {
const direct = this.verificationSessions.get(id);
if (direct) return direct;
const transactionMatches = Array.from(this.verificationSessions.values()).filter((session) => {
return this.readRequestValue(session.request, () => session.request.transactionId?.trim(), "") === id;
});
if (transactionMatches.length === 1) return transactionMatches[0];
if (transactionMatches.length > 1) throw new Error(`Matrix verification request id is ambiguous for transaction ${id}; use the verification id instead`);
throw new Error(`Matrix verification request not found: ${id}`);
}
ensureVerificationRequestTracked(session) {
const requestObj = session.request;
if (this.trackedVerificationRequests.has(requestObj)) return;
this.trackedVerificationRequests.add(requestObj);
session.request.on(VerificationRequestEvent.Change, () => {
this.touchVerificationSession(session);
this.maybeAutoAcceptInboundRequest(session);
const verifier = this.readRequestValue(session.request, () => session.request.verifier, null);
if (verifier) this.attachVerifierToVerificationSession(session, verifier);
this.maybeAutoStartInboundSas(session);
});
}
maybeAutoAcceptInboundRequest(session) {
if (session.acceptRequested) return;
const request = session.request;
const isSelfVerification = this.readRequestValue(request, () => request.isSelfVerification, false);
const initiatedByMe = this.readRequestValue(request, () => request.initiatedByMe, false);
const phase = this.readVerificationPhase(request, VerificationPhase.Requested);
const accepting = this.readRequestValue(request, () => request.accepting, false);
const declining = this.readRequestValue(request, () => request.declining, false);
if (isSelfVerification || initiatedByMe) return;
if (phase !== VerificationPhase.Requested || accepting || declining) return;
session.acceptRequested = true;
request.accept().then(() => {
this.touchVerificationSession(session);
}).catch((err) => {
session.acceptRequested = false;
session.error = formatMatrixErrorMessage(err);
this.touchVerificationSession(session);
});
}
maybeAutoStartInboundSas(session) {
if (session.activeVerifier || session.verifyStarted || session.startRequested) return;
if (this.readRequestValue(session.request, () => session.request.initiatedByMe, true)) return;
if (!this.readRequestValue(session.request, () => session.request.isSelfVerification, false)) return;
const phase = this.readVerificationPhase(session.request, VerificationPhase.Requested);
if (phase < VerificationPhase.Ready || phase >= VerificationPhase.Cancelled) return;
const methodsRaw = this.readRequestValue(session.request, () => session.request.methods, []);
const methods = Array.isArray(methodsRaw) ? methodsRaw.filter((entry) => typeof entry === "string") : [];
const chosenMethod = this.readRequestValue(session.request, () => session.request.chosenMethod, null);
if (!(methods.includes(VerificationMethod.Sas) || chosenMethod === VerificationMethod.Sas)) return;
session.startRequested = true;
session.request.startVerification(VerificationMethod.Sas).then((verifier) => {
this.attachVerifierToVerificationSession(session, verifier);
this.touchVerificationSession(session);
}).catch(() => {
session.startRequested = false;
});
}
attachVerifierToVerificationSession(session, verifier) {
session.activeVerifier = verifier;
this.touchVerificationSession(session);
const maybeSas = verifier.getShowSasCallbacks();
if (maybeSas) {
session.sasCallbacks = maybeSas;
this.maybeAutoConfirmSas(session);
}
const maybeReciprocateQr = verifier.getReciprocateQrCodeCallbacks();
if (maybeReciprocateQr) session.reciprocateQrCallbacks = maybeReciprocateQr;
const verifierObj = verifier;
if (this.trackedVerificationVerifiers.has(verifierObj)) {
this.ensureVerificationStarted(session);
return;
}
this.trackedVerificationVerifiers.add(verifierObj);
verifier.on(VerifierEvent.ShowSas, (sas) => {
session.sasCallbacks = sas;
this.touchVerificationSession(session);
this.maybeAutoConfirmSas(session);
});
verifier.on(VerifierEvent.ShowReciprocateQr, (qr) => {
session.reciprocateQrCallbacks = qr;
this.touchVerificationSession(session);
});
verifier.on(VerifierEvent.Cancel, (err) => {
this.clearSasAutoConfirmTimer(session);
session.error = formatMatrixErrorMessage(err);
this.touchVerificationSession(session);
});
this.ensureVerificationStarted(session);
}
maybeAutoConfirmSas(session) {
if (session.sasAutoConfirmStarted || session.sasAutoConfirmTimer) return;
if (this.readRequestValue(session.request, () => session.request.initiatedByMe, true)) return;
const callbacks = session.sasCallbacks ?? session.activeVerifier?.getShowSasCallbacks();
if (!callbacks) return;
session.sasCallbacks = callbacks;
session.sasAutoConfirmTimer = setTimeout(() => {
session.sasAutoConfirmTimer = void 0;
if (this.readVerificationPhase(session.request, VerificationPhase.Requested) >= VerificationPhase.Cancelled) return;
session.sasAutoConfirmStarted = true;
this.confirmSasForSession(session, callbacks, { trustOwnDevice: true }).then(() => {
this.touchVerificationSession(session);
}).catch((err) => {
session.error = formatMatrixErrorMessage(err);
this.touchVerificationSession(session);
});
}, SAS_AUTO_CONFIRM_DELAY_MS);
}
async confirmSasForSession(session, callbacks, opts = { trustOwnDevice: true }) {
await callbacks.confirm();
if (opts.trustOwnDevice) await this.trustOwnDeviceAfterConfirmedSas(session);
}
ensureVerificationStarted(session) {
if (!session.activeVerifier || session.verifyStarted) return;
session.verifyStarted = true;
session.verifyPromise = session.activeVerifier.verify().then(() => {
this.touchVerificationSession(session);
}).catch((err) => {
session.error = formatMatrixErrorMessage(err);
this.touchVerificationSession(session);
});
}
async trustOwnDeviceAfterConfirmedSas(session) {
if (!this.readRequestValue(session.request, () => session.request.isSelfVerification, false)) return;
const deviceId = this.readRequestValue(session.request, () => session.request.otherDeviceId?.trim(), "");
if (!deviceId || !this.opts.trustOwnDeviceAfterSas) return;
await this.opts.trustOwnDeviceAfterSas(deviceId);
}
onSummaryChanged(listener) {
this.summaryListeners.add(listener);
return () => {
this.summaryListeners.delete(listener);
};
}
trackVerificationRequest(request) {
this.pruneVerificationSessions(Date.now());
const requestObj = request;
for (const existing of this.verificationSessions.values()) if (existing.request === requestObj) {
this.touchVerificationSession(existing);
return this.buildVerificationSummary(existing);
}
if (this.readVerificationRequestIdentity(request).transactionId) {
for (const existing of this.verificationSessions.values()) if (this.isSameLogicalVerificationRequest(existing.request, request)) {
existing.request = request;
this.ensureVerificationRequestTracked(existing);
const verifier = this.readRequestValue(request, () => request.verifier, null);
if (verifier) this.attachVerifierToVerificationSession(existing, verifier);
this.touchVerificationSession(existing);
return this.buildVerificationSummary(existing);
}
}
const now = resolveDateTimestampMs(Date.now());
const session = {
id: `verification-${++this.verificationSessionCounter}`,
request,
createdAtMs: now,
updatedAtMs: now,
verifyStarted: false,
startRequested: false,
acceptRequested: false,
sasAutoConfirmStarted: false
};
this.verificationSessions.set(session.id, session);
this.ensureVerificationRequestTracked(session);
this.maybeAutoAcceptInboundRequest(session);
const verifier = this.readRequestValue(request, () => request.verifier, null);
if (verifier) this.attachVerifierToVerificationSession(session, verifier);
this.maybeAutoStartInboundSas(session);
this.emitVerificationSummary(session);
return this.buildVerificationSummary(session);
}
async requestOwnUserVerification(crypto) {
if (!crypto) return null;
const request = await crypto.requestOwnUserVerification();
if (!request) return null;
return this.trackVerificationRequest(request);
}
listVerifications() {
this.pruneVerificationSessions(Date.now());
return Array.from(this.verificationSessions.values()).map((session) => this.buildVerificationSummary(session)).toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}
async requestVerification(crypto, params) {
if (!crypto) throw new Error("Matrix crypto is not available");
let request;
if (params.ownUser) request = await crypto.requestOwnUserVerification();
else if (params.userId && params.deviceId && crypto.requestDeviceVerification) request = await crypto.requestDeviceVerification(params.userId, params.deviceId);
else if (params.userId && params.roomId && crypto.requestVerificationDM) request = await crypto.requestVerificationDM(params.userId, params.roomId);
else throw new Error("Matrix verification request requires one of: ownUser, userId+deviceId, or userId+roomId");
if (!request) throw new Error("Matrix verification request could not be created");
return this.trackVerificationRequest(request);
}
async acceptVerification(id) {
const session = this.findVerificationSession(id);
await session.request.accept();
this.touchVerificationSession(session);
return this.buildVerificationSummary(session);
}
async cancelVerification(id, params) {
const session = this.findVerificationSession(id);
await session.request.cancel(params);
this.touchVerificationSession(session);
return this.buildVerificationSummary(session);
}
async startVerification(id, method = "sas") {
const session = this.findVerificationSession(id);
if (method !== "sas") throw new Error("Matrix startVerification currently supports only SAS directly");
const verifier = await session.request.startVerification(VerificationMethod.Sas);
this.attachVerifierToVerificationSession(session, verifier);
this.ensureVerificationStarted(session);
return this.buildVerificationSummary(session);
}
async generateVerificationQr(id) {
const qr = await this.findVerificationSession(id).request.generateQRCode();
if (!qr) throw new Error("Matrix verification QR data is not available yet");
return { qrDataBase64: Buffer.from(qr).toString("base64") };
}
async scanVerificationQr(id, qrDataBase64) {
const session = this.findVerificationSession(id);
const trimmed = qrDataBase64.trim();
if (!trimmed) throw new Error("Matrix verification QR payload is required");
const qrBytes = Buffer.from(trimmed, "base64");
if (qrBytes.length === 0) throw new Error("Matrix verification QR payload is invalid base64");
const verifier = await session.request.scanQRCode(new Uint8ClampedArray(qrBytes));
this.attachVerifierToVerificationSession(session, verifier);
this.ensureVerificationStarted(session);
return this.buildVerificationSummary(session);
}
async confirmVerificationSas(id) {
const session = this.findVerificationSession(id);
const callbacks = session.sasCallbacks ?? session.activeVerifier?.getShowSasCallbacks();
if (!callbacks) throw new Error("Matrix SAS confirmation is not available for this verification request");
this.clearSasAutoConfirmTimer(session);
session.sasCallbacks = callbacks;
session.sasAutoConfirmStarted = true;
await this.confirmSasForSession(session, callbacks);
if (session.verifyPromise) await session.verifyPromise;
this.touchVerificationSession(session);
return this.buildVerificationSummary(session);
}
mismatchVerificationSas(id) {
const session = this.findVerificationSession(id);
const callbacks = session.sasCallbacks ?? session.activeVerifier?.getShowSasCallbacks();
if (!callbacks) throw new Error("Matrix SAS mismatch is not available for this verification request");
this.clearSasAutoConfirmTimer(session);
session.sasCallbacks = callbacks;
callbacks.mismatch();
this.touchVerificationSession(session);
return this.buildVerificationSummary(session);
}
confirmVerificationReciprocateQr(id) {
const session = this.findVerificationSession(id);
const callbacks = session.reciprocateQrCallbacks ?? session.activeVerifier?.getReciprocateQrCodeCallbacks();
if (!callbacks) throw new Error("Matrix reciprocate-QR confirmation is not available for this verification request");
session.reciprocateQrCallbacks = callbacks;
callbacks.confirm();
this.touchVerificationSession(session);
return this.buildVerificationSummary(session);
}
getVerificationSas(id) {
const session = this.findVerificationSession(id);
const callbacks = session.sasCallbacks ?? session.activeVerifier?.getShowSasCallbacks();
if (!callbacks) throw new Error("Matrix SAS data is not available for this verification request");
session.sasCallbacks = callbacks;
return {
decimal: callbacks.sas.decimal,
emoji: callbacks.sas.emoji
};
}
};
//#endregion
export { MatrixCryptoBootstrapper, MatrixDecryptBridge, MatrixVerificationManager, createMatrixCryptoFacade, isMatrixDeviceOwnerVerified, isMatrixDeviceVerifiedInCurrentClient, persistIdbToDisk, restoreIdbFromDisk };