diff --git a/extensions/matrix/src/matrix/backup-health.ts b/extensions/matrix/src/matrix/backup-health.ts index 041de1f75c0..63c255197e7 100644 --- a/extensions/matrix/src/matrix/backup-health.ts +++ b/extensions/matrix/src/matrix/backup-health.ts @@ -98,6 +98,7 @@ export function resolveMatrixRoomKeyBackupIssue( export function resolveMatrixRoomKeyBackupReadinessError( backup: MatrixRoomKeyBackupStatusLike, opts: { + allowUntrustedMatchingKey?: boolean; requireServerBackup: boolean; }, ): string | null { @@ -108,6 +109,14 @@ export function resolveMatrixRoomKeyBackupReadinessError( if (issue.code === "ok") { return null; } + if ( + issue.code === "untrusted-signature" && + opts.allowUntrustedMatchingKey === true && + backup.matchesDecryptionKey === true && + backup.decryptionKeyCached === true + ) { + return null; + } if (issue.message) { return `Matrix room key backup is not usable: ${issue.message}.`; } diff --git a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts index 90267af030f..00c740c7a16 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test-helpers.ts @@ -256,11 +256,13 @@ export function createMatrixTextMessageEvent(params: { originServerTs?: number; relatesTo?: RoomMessageEventContent["m.relates_to"]; mentions?: RoomMessageEventContent["m.mentions"]; + unsigned?: MatrixRawEvent["unsigned"]; }): MatrixRawEvent { return createMatrixRoomMessageEvent({ eventId: params.eventId, sender: params.sender, originServerTs: params.originServerTs, + unsigned: params.unsigned, content: { msgtype: "m.text", body: params.body, @@ -274,6 +276,7 @@ export function createMatrixRoomMessageEvent(params: { eventId: string; sender?: string; originServerTs?: number; + unsigned?: MatrixRawEvent["unsigned"]; content: RoomMessageEventContent; }): MatrixRawEvent { return { @@ -282,6 +285,7 @@ export function createMatrixRoomMessageEvent(params: { event_id: params.eventId, origin_server_ts: params.originServerTs ?? Date.now(), content: params.content, + ...(params.unsigned ? { unsigned: params.unsigned } : {}), } as MatrixRawEvent; } diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 2cc1da533b5..eed8b30208b 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -564,6 +564,32 @@ describe("matrix monitor handler pairing account scope", () => { expect(resolveAgentRoute).toHaveBeenCalledTimes(1); }); + it("drops root events that carry a bundled replacement relation", async () => { + const { handler, recordInboundSession } = createMatrixHandlerTestHarness({ + isDirectMessage: false, + mentionRegexes: [/@bot/i], + getMemberDisplayName: async () => "sender", + }); + + await handler( + "!room:example.org", + createMatrixTextMessageEvent({ + eventId: "$edited-root", + body: "@bot please reply", + mentions: { user_ids: ["@bot:example.org"] }, + unsigned: { + "m.relations": { + "m.replace": { + event_id: "$edit", + }, + }, + }, + }), + ); + + expect(recordInboundSession).not.toHaveBeenCalled(); + }); + it("skips media downloads for unmentioned group media messages", async () => { const downloadContent = vi.fn(async () => Buffer.from("image")); const getMemberDisplayName = vi.fn(async () => "sender"); diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 68b13e4d069..f9950169e27 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -169,6 +169,14 @@ function resolveMatrixMentionPrecheckText(params: { return ""; } +function hasBundledMatrixReplacementRelation(event: MatrixRawEvent) { + const relations = event.unsigned?.["m.relations"]; + if (!relations || typeof relations !== "object") { + return false; + } + return relations[RelationType.Replace] !== undefined; +} + function resolveMatrixInboundBodyText(params: { rawBody: string; filename?: string; @@ -500,6 +508,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam if (relates && "rel_type" in relates && relates.rel_type === RelationType.Replace) { return undefined; } + if (hasBundledMatrixReplacementRelation(event)) { + return undefined; + } if (eventId && inboundDeduper) { claimedInboundEvent = inboundDeduper.claimEvent({ roomId, eventId }); if (!claimedInboundEvent) { diff --git a/extensions/matrix/src/matrix/sdk.test.ts b/extensions/matrix/src/matrix/sdk.test.ts index 2f6531d07c0..d54b2855131 100644 --- a/extensions/matrix/src/matrix/sdk.test.ts +++ b/extensions/matrix/src/matrix/sdk.test.ts @@ -1484,6 +1484,38 @@ describe("MatrixClient crypto bootstrapping", () => { expect(status.verified).toBe(false); }); + it("reports peer device trust from the current client", async () => { + const getDeviceVerificationStatus = vi.fn(async () => ({ + isVerified: () => true, + localVerified: true, + crossSigningVerified: false, + signedByOwner: false, + })); + matrixJsClient.getCrypto = vi.fn(() => ({ + on: vi.fn(), + bootstrapCrossSigning: vi.fn(async () => {}), + bootstrapSecretStorage: vi.fn(async () => {}), + requestOwnUserVerification: vi.fn(async () => null), + getDeviceVerificationStatus, + })); + + const client = new MatrixClient("https://matrix.example.org", "token", { + encryption: true, + }); + await client.start(); + + const status = await client.getDeviceVerificationStatus("@peer:example.org", "PEERDEVICE"); + expect(getDeviceVerificationStatus).toHaveBeenCalledWith("@peer:example.org", "PEERDEVICE"); + expect(status).toMatchObject({ + deviceId: "PEERDEVICE", + encryptionEnabled: true, + localVerified: true, + signedByOwner: false, + userId: "@peer:example.org", + verified: true, + }); + }); + it("verifies with a provided recovery key and reports success", async () => { const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1))); expect(encoded).toBeTypeOf("string"); @@ -1885,6 +1917,38 @@ describe("MatrixClient crypto bootstrapping", () => { expect(restoreKeyBackup).toHaveBeenCalledTimes(1); }); + it("restores backup keys when the matching decryption key is cached but signature trust is stale", async () => { + const restoreKeyBackup = vi.fn(async () => ({ imported: 3, total: 3 })); + matrixJsClient.getCrypto = vi.fn(() => ({ + on: vi.fn(), + getActiveSessionBackupVersion: vi.fn(async () => "42"), + getSessionBackupPrivateKey: vi.fn(async () => new Uint8Array([1])), + getKeyBackupInfo: vi.fn(async () => ({ + algorithm: "m.megolm_backup.v1.curve25519-aes-sha2", + auth_data: {}, + version: "42", + })), + isKeyBackupTrusted: vi.fn(async () => ({ + trusted: false, + matchesDecryptionKey: true, + })), + restoreKeyBackup, + })); + + const client = new MatrixClient("https://matrix.example.org", "token", { + encryption: true, + }); + vi.spyOn(client, "doRequest").mockResolvedValue({ version: "42" }); + + const result = await client.restoreRoomKeyBackup(); + expect(result.success).toBe(true); + expect(result.imported).toBe(3); + expect(result.total).toBe(3); + expect(result.backup.trusted).toBe(false); + expect(result.backup.matchesDecryptionKey).toBe(true); + expect(restoreKeyBackup).toHaveBeenCalledTimes(1); + }); + it("activates backup after loading the key from secret storage before restore", async () => { const getActiveSessionBackupVersion = vi .fn() diff --git a/extensions/matrix/src/matrix/sdk.ts b/extensions/matrix/src/matrix/sdk.ts index 62771d14243..59d9b11a1c9 100644 --- a/extensions/matrix/src/matrix/sdk.ts +++ b/extensions/matrix/src/matrix/sdk.ts @@ -83,6 +83,16 @@ export type MatrixOwnDeviceVerificationStatus = { backup: MatrixRoomKeyBackupStatus; }; +export type MatrixDeviceVerificationStatus = { + encryptionEnabled: boolean; + userId: string | null; + deviceId: string | null; + verified: boolean; + localVerified: boolean; + crossSigningVerified: boolean; + signedByOwner: boolean; +}; + export type MatrixRoomKeyBackupStatus = { serverVersion: string | null; activeVersion: string | null; @@ -332,7 +342,7 @@ export class MatrixClient { const runtime = await loadMatrixCryptoRuntime(); this.decryptBridge ??= new runtime.MatrixDecryptBridge({ client: this.client, - toRaw: (event) => matrixEventToRaw(event), + toRaw: (event) => matrixEventToRaw(event, { contentMode: "original" }), emitDecryptedEvent: (roomId, event) => { this.emitter.emit("room.decrypted_event", roomId, event); }, @@ -669,6 +679,7 @@ export class MatrixClient { databasePrefix: this.cryptoDatabasePrefix, }).catch(noop); }, MATRIX_IDB_PERSIST_INTERVAL_MS); + this.idbPersistTimer.unref?.(); } catch (err) { LogService.warn("MatrixClientLite", "Failed to initialize rust crypto:", err); } @@ -1044,44 +1055,61 @@ export class MatrixClient { }; } - async getOwnDeviceVerificationStatus(): Promise { - const recoveryKey = this.recoveryKeyStore.getRecoveryKeySummary(); - const userId = this.client.getUserId() ?? this.selfUserId ?? null; - const deviceId = this.client.getDeviceId()?.trim() || null; - const backup = await this.getRoomKeyBackupStatus(); - + async getDeviceVerificationStatus( + userId: string | null | undefined, + deviceId: string | null | undefined, + ): Promise { + const normalizedUserId = userId?.trim() || null; + const normalizedDeviceId = deviceId?.trim() || null; if (!this.encryptionEnabled) { return { encryptionEnabled: false, - userId, - deviceId, + userId: normalizedUserId, + deviceId: normalizedDeviceId, verified: false, localVerified: false, crossSigningVerified: false, signedByOwner: false, - recoveryKeyStored: Boolean(recoveryKey), - recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null, - recoveryKeyId: recoveryKey?.keyId ?? null, - backupVersion: backup.serverVersion, - backup, }; } const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined; let deviceStatus: MatrixDeviceVerificationStatusLike | null = null; - if (crypto && userId && deviceId && typeof crypto.getDeviceVerificationStatus === "function") { - deviceStatus = await crypto.getDeviceVerificationStatus(userId, deviceId).catch(() => null); + if ( + crypto && + normalizedUserId && + normalizedDeviceId && + typeof crypto.getDeviceVerificationStatus === "function" + ) { + deviceStatus = await crypto + .getDeviceVerificationStatus(normalizedUserId, normalizedDeviceId) + .catch(() => null); } - const { isMatrixDeviceOwnerVerified } = await loadMatrixCryptoRuntime(); + const { isMatrixDeviceVerifiedInCurrentClient } = await loadMatrixCryptoRuntime(); return { encryptionEnabled: true, - userId, - deviceId, - verified: isMatrixDeviceOwnerVerified(deviceStatus), + userId: normalizedUserId, + deviceId: normalizedDeviceId, + verified: isMatrixDeviceVerifiedInCurrentClient(deviceStatus), localVerified: deviceStatus?.localVerified === true, crossSigningVerified: deviceStatus?.crossSigningVerified === true, signedByOwner: deviceStatus?.signedByOwner === true, + }; + } + + async getOwnDeviceVerificationStatus(): Promise { + const recoveryKey = this.recoveryKeyStore.getRecoveryKeySummary(); + const userId = this.client.getUserId() ?? this.selfUserId ?? null; + const deviceId = this.client.getDeviceId()?.trim() || null; + const backup = await this.getRoomKeyBackupStatus(); + const deviceVerification = await this.getDeviceVerificationStatus(userId, deviceId); + const ownerVerified = + deviceVerification.crossSigningVerified || deviceVerification.signedByOwner; + + return { + ...deviceVerification, + verified: ownerVerified, recoveryKeyStored: Boolean(recoveryKey), recoveryKeyCreatedAt: recoveryKey?.createdAt ?? null, recoveryKeyId: recoveryKey?.keyId ?? null, @@ -1211,6 +1239,7 @@ export class MatrixClient { const backup = await this.getRoomKeyBackupStatus(); loadedFromSecretStorage = backup.keyLoadAttempted && !backup.keyLoadError; const backupError = resolveMatrixRoomKeyBackupReadinessError(backup, { + allowUntrustedMatchingKey: true, requireServerBackup: true, }); if (backupError) { @@ -1668,7 +1697,7 @@ export class MatrixClient { return; } - const raw = matrixEventToRaw(event); + const raw = matrixEventToRaw(event, { contentMode: "original" }); const isEncryptedEvent = raw.type === "m.room.encrypted"; this.emitter.emit("room.event", roomId, raw); if (isEncryptedEvent) { diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts index 77b371deff2..99db1115302 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.test.ts @@ -320,6 +320,84 @@ describe("MatrixCryptoBootstrapper", () => { expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).not.toHaveBeenCalled(); }); + it("recreates secret storage and retries a forced reset when stale server SSSS blocks it", async () => { + const bootstrapCrossSigning = vi + .fn<() => Promise>() + .mockRejectedValueOnce(new Error("getSecretStorageKey callback returned falsey")) + .mockResolvedValueOnce(undefined); + const { deps, crypto, bootstrapper } = createBootstrapperHarness({ + bootstrapCrossSigning, + isCrossSigningReady: vi.fn(async () => true), + userHasCrossSigningKeys: vi.fn(async () => true), + getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()), + }); + + await bootstrapper.bootstrap(crypto, { + strict: true, + forceResetCrossSigning: true, + allowSecretStorageRecreateWithoutRecoveryKey: true, + }); + + expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith( + crypto, + { + allowSecretStorageRecreateWithoutRecoveryKey: true, + forceNewSecretStorage: true, + }, + ); + expect(bootstrapCrossSigning).toHaveBeenCalledTimes(3); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + }); + + it("re-exports cross-signing keys after forced reset creates secret storage", async () => { + const bootstrapCrossSigning = vi.fn(async () => {}); + const { deps, crypto, bootstrapper } = createBootstrapperHarness({ + bootstrapCrossSigning, + isCrossSigningReady: vi.fn(async () => true), + userHasCrossSigningKeys: vi.fn(async () => true), + getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()), + }); + + await bootstrapper.bootstrap(crypto, { + strict: true, + forceResetCrossSigning: true, + allowSecretStorageRecreateWithoutRecoveryKey: true, + }); + + expect(deps.recoveryKeyStore.bootstrapSecretStorageWithRecoveryKey).toHaveBeenCalledWith( + crypto, + { + allowSecretStorageRecreateWithoutRecoveryKey: true, + }, + ); + expect(bootstrapCrossSigning).toHaveBeenCalledTimes(2); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + setupNewCrossSigning: true, + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + expect(bootstrapCrossSigning).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + authUploadDeviceSigningKeys: expect.any(Function), + }), + ); + }); + it("fails in strict mode when cross-signing keys are still unpublished", async () => { const deps = createBootstrapperDeps(); const crypto = createCryptoApi({ diff --git a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts index d337b64dfe2..3686800c626 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts @@ -59,7 +59,7 @@ export class MatrixCryptoBootstrapper { options.allowSecretStorageRecreateWithoutRecoveryKey === true, }); } - const crossSigning = await this.bootstrapCrossSigning(crypto, { + let crossSigning = await this.bootstrapCrossSigning(crypto, { forceResetCrossSigning: options.forceResetCrossSigning === true, allowAutomaticCrossSigningReset: options.allowAutomaticCrossSigningReset !== false, allowSecretStorageRecreateWithoutRecoveryKey: @@ -74,6 +74,15 @@ export class MatrixCryptoBootstrapper { 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, @@ -160,12 +169,38 @@ export class MatrixCryptoBootstrapper { }; if (options.forceResetCrossSigning) { - try { + const resetCrossSigning = async (): Promise => { await crypto.bootstrapCrossSigning({ setupNewCrossSigning: true, authUploadDeviceSigningKeys, }); + }; + try { + await resetCrossSigning(); } catch (err) { + const shouldRepairSecretStorage = + options.allowSecretStorageRecreateWithoutRecoveryKey && + isRepairableSecretStorageAccessError(err); + if (shouldRepairSecretStorage) { + 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(); + } 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)); diff --git a/extensions/matrix/src/matrix/sdk/crypto-runtime.ts b/extensions/matrix/src/matrix/sdk/crypto-runtime.ts index a5423fb4b1f..82b98a9c89f 100644 --- a/extensions/matrix/src/matrix/sdk/crypto-runtime.ts +++ b/extensions/matrix/src/matrix/sdk/crypto-runtime.ts @@ -8,4 +8,7 @@ export { MatrixDecryptBridge } from "./decrypt-bridge.js"; export { persistIdbToDisk, restoreIdbFromDisk } from "./idb-persistence.js"; export { MatrixVerificationManager } from "./verification-manager.js"; export type { MatrixVerificationSummary } from "./verification-manager.js"; -export { isMatrixDeviceOwnerVerified } from "./verification-status.js"; +export { + isMatrixDeviceOwnerVerified, + isMatrixDeviceVerifiedInCurrentClient, +} from "./verification-status.js"; diff --git a/extensions/matrix/src/matrix/sdk/event-helpers.test.ts b/extensions/matrix/src/matrix/sdk/event-helpers.test.ts index b3fff8fc52b..b768262cd24 100644 --- a/extensions/matrix/src/matrix/sdk/event-helpers.test.ts +++ b/extensions/matrix/src/matrix/sdk/event-helpers.test.ts @@ -57,4 +57,61 @@ describe("event-helpers", () => { } as unknown as MatrixEvent; expect(matrixEventToRaw(viaRaw).state_key).toBe("@carol:example.org"); }); + + it("serializes current content by default for read APIs", () => { + const event = { + getId: () => "$root", + getSender: () => "@alice:example.org", + getType: () => "m.room.message", + getTs: () => 1000, + getOriginalContent: () => ({ body: "original", msgtype: "m.text" }), + getContent: () => ({ + body: "@bot edited", + "m.mentions": { user_ids: ["@bot:example.org"] }, + msgtype: "m.text", + }), + getUnsigned: () => ({ + "m.relations": { + "m.replace": { event_id: "$edit" }, + }, + }), + } as unknown as MatrixEvent; + + expect(matrixEventToRaw(event)).toMatchObject({ + content: { + body: "@bot edited", + "m.mentions": { user_ids: ["@bot:example.org"] }, + msgtype: "m.text", + }, + }); + }); + + it("can serialize original content for inbound trigger filtering", () => { + const event = { + getId: () => "$root", + getSender: () => "@alice:example.org", + getType: () => "m.room.message", + getTs: () => 1000, + getOriginalContent: () => ({ body: "original", msgtype: "m.text" }), + getContent: () => ({ + body: "@bot edited", + "m.mentions": { user_ids: ["@bot:example.org"] }, + msgtype: "m.text", + }), + getUnsigned: () => ({ + "m.relations": { + "m.replace": { event_id: "$edit" }, + }, + }), + } as unknown as MatrixEvent; + + expect(matrixEventToRaw(event, { contentMode: "original" })).toMatchObject({ + content: { body: "original", msgtype: "m.text" }, + unsigned: { + "m.relations": { + "m.replace": { event_id: "$edit" }, + }, + }, + }); + }); }); diff --git a/extensions/matrix/src/matrix/sdk/event-helpers.ts b/extensions/matrix/src/matrix/sdk/event-helpers.ts index 624924ace0c..994efc882b7 100644 --- a/extensions/matrix/src/matrix/sdk/event-helpers.ts +++ b/extensions/matrix/src/matrix/sdk/event-helpers.ts @@ -1,17 +1,29 @@ import type { MatrixEvent } from "matrix-js-sdk"; import type { MatrixRawEvent } from "./types.js"; -export function matrixEventToRaw(event: MatrixEvent): MatrixRawEvent { +export type MatrixEventContentMode = "current" | "original"; + +export function matrixEventToRaw( + event: MatrixEvent, + opts: { contentMode?: MatrixEventContentMode } = {}, +): MatrixRawEvent { const unsigned = (event.getUnsigned?.() ?? {}) as { age?: number; redacted_because?: unknown; }; + const eventWithOriginalContent = event as { + getOriginalContent?: () => Record; + }; + const content = + opts.contentMode === "original" + ? (eventWithOriginalContent.getOriginalContent?.() ?? event.getContent?.() ?? {}) + : (event.getContent?.() ?? eventWithOriginalContent.getOriginalContent?.() ?? {}); const raw: MatrixRawEvent = { event_id: event.getId() ?? "", sender: event.getSender() ?? "", type: event.getType() ?? "", origin_server_ts: event.getTs() ?? 0, - content: (event.getContent?.() ?? {}) || {}, + content: content || {}, unsigned, }; const stateKey = resolveMatrixStateKey(event); diff --git a/extensions/matrix/src/matrix/sdk/types.ts b/extensions/matrix/src/matrix/sdk/types.ts index a43987117c8..c262fdcf2f5 100644 --- a/extensions/matrix/src/matrix/sdk/types.ts +++ b/extensions/matrix/src/matrix/sdk/types.ts @@ -12,6 +12,7 @@ export type MatrixRawEvent = { content: Record; unsigned?: { age?: number; + "m.relations"?: Record; redacted_because?: unknown; }; state_key?: string; diff --git a/extensions/matrix/test-api.ts b/extensions/matrix/test-api.ts index 396034c820a..f6d9f6d90b6 100644 --- a/extensions/matrix/test-api.ts +++ b/extensions/matrix/test-api.ts @@ -1,2 +1,21 @@ export { matrixPlugin } from "./src/channel.js"; +export { MatrixClient } from "./src/matrix/sdk.js"; +export type { + EncryptedFile, + MatrixDeviceVerificationStatus, + MatrixOwnDeviceDeleteResult, + MatrixOwnDeviceInfo, + MatrixOwnDeviceVerificationStatus, + MatrixRecoveryKeyVerificationResult, + MatrixRawEvent, + MatrixRoomKeyBackupResetResult, + MatrixRoomKeyBackupRestoreResult, + MatrixRoomKeyBackupStatus, + MatrixVerificationBootstrapResult, + MessageEventContent, +} from "./src/matrix/sdk.js"; +export type { + MatrixVerificationMethod, + MatrixVerificationSummary, +} from "./src/matrix/sdk/verification-manager.js"; export { setMatrixRuntime } from "./src/runtime.js";