Skip to content

Commit 09c67d9

Browse files
authored
Merge branch 'develop' into dbkr/hydra
2 parents a4ac412 + c4e1e07 commit 09c67d9

25 files changed

+1265
-330
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
Changes in [37.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.12.0) (2025-07-29)
2+
====================================================================================================
3+
## 🦖 Deprecations
4+
5+
* Deprecate non-functional `IJoinRoomOpts.syncRoom` ([#4913](https://github.com/matrix-org/matrix-js-sdk/pull/4913)). Contributed by @richvdh.
6+
7+
## ✨ Features
8+
9+
* Custom abort timeout logic for restarting delayed events that is compatible with the widget api ([#4927](https://github.com/matrix-org/matrix-js-sdk/pull/4927)). Contributed by @toger5.
10+
* Allow sending notification events when starting a call ([#4826](https://github.com/matrix-org/matrix-js-sdk/pull/4826)). Contributed by @robintown.
11+
12+
## 🐛 Bug Fixes
13+
14+
* Fix deep import incompatibility (#4924) ([#4925](https://github.com/matrix-org/matrix-js-sdk/pull/4925)). Contributed by @toriningen.
15+
* Fix more incorrect logger use ([#4904](https://github.com/matrix-org/matrix-js-sdk/pull/4904)). Contributed by @richvdh.
16+
17+
118
Changes in [37.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v37.11.0) (2025-07-15)
219
====================================================================================================
320
## ✨ Features

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "matrix-js-sdk",
3-
"version": "37.11.0",
3+
"version": "37.12.0",
44
"description": "Matrix Client-Server SDK for Javascript",
55
"engines": {
66
"node": ">=22.0.0"
@@ -49,7 +49,7 @@
4949
],
5050
"dependencies": {
5151
"@babel/runtime": "^7.12.5",
52-
"@matrix-org/matrix-sdk-crypto-wasm": "^15.0.0",
52+
"@matrix-org/matrix-sdk-crypto-wasm": "^15.1.0",
5353
"another-json": "^0.2.0",
5454
"bs58": "^6.0.0",
5555
"content-type": "^1.0.4",

spec/integ/crypto/cross-signing.spec.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ describe("cross-signing", () => {
137137
const authDict = { type: "test" };
138138
await bootstrapCrossSigning(authDict);
139139

140-
// check the cross-signing keys upload
141-
expect(fetchMock.called("upload-keys")).toBeTruthy();
142-
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
140+
// check that the cross-signing keys have been uploaded
141+
expect(fetchMock.called("upload-cross-signing-keys")).toBeTruthy();
142+
const [, keysOpts] = fetchMock.lastCall("upload-cross-signing-keys")!;
143143
const keysBody = JSON.parse(keysOpts!.body as string);
144144
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
145145
// there should be a key of each type
@@ -225,9 +225,6 @@ describe("cross-signing", () => {
225225
await aliceClient.startClient();
226226
await syncPromise(aliceClient);
227227

228-
// we expect a request to upload signatures for our device ...
229-
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
230-
231228
// we expect the UserTrustStatusChanged event to be fired after the cross signing keys import
232229
const userTrustStatusChangedPromise = new Promise<string>((resolve) =>
233230
aliceClient.on(CryptoEvent.UserTrustStatusChanged, resolve),
@@ -420,15 +417,18 @@ describe("cross-signing", () => {
420417
return new Promise<any>((resolve) => {
421418
fetchMock.post(
422419
{
423-
url: new RegExp("/_matrix/client/v3/keys/device_signing/upload"),
424-
name: "upload-keys",
420+
url: new URL(
421+
"/_matrix/client/v3/keys/device_signing/upload",
422+
aliceClient.getHomeserverUrl(),
423+
).toString(),
424+
name: "upload-cross-signing-keys",
425425
},
426426
(url, options) => {
427427
const content = JSON.parse(options.body as string);
428428
resolve(content);
429429
return {};
430430
},
431-
// Override the routes define in `mockSetupCrossSigningRequests`
431+
// Override the route defined in E2EKeyReceiver
432432
{ overwriteRoutes: true },
433433
);
434434
});

spec/integ/crypto/device-dehydration.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,6 @@ async function initializeSecretStorage(
181181
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
182182
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
183183
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
184-
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
185-
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
186184
const accountData: Map<string, object> = new Map();
187185
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
188186
const name = url.split("/").pop()!;
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*
2+
Copyright 2025 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import "fake-indexeddb/auto";
18+
import fetchMock from "fetch-mock-jest";
19+
import mkDebug from "debug";
20+
21+
import {
22+
createClient,
23+
DebugLogger,
24+
EventType,
25+
type IContent,
26+
KnownMembership,
27+
type MatrixClient,
28+
MsgType,
29+
} from "../../../src";
30+
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver.ts";
31+
import { SyncResponder } from "../../test-utils/SyncResponder.ts";
32+
import { mockInitialApiRequests, mockSetupCrossSigningRequests } from "../../test-utils/mockEndpoints.ts";
33+
import { getSyncResponse, mkEventCustom, syncPromise } from "../../test-utils/test-utils.ts";
34+
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder.ts";
35+
import { flushPromises } from "../../test-utils/flushPromises.ts";
36+
import { E2EOTKClaimResponder } from "../../test-utils/E2EOTKClaimResponder.ts";
37+
import { escapeRegExp } from "../../../src/utils.ts";
38+
39+
const debug = mkDebug("matrix-js-sdk:history-sharing");
40+
41+
// load the rust library. This can take a few seconds on a slow GH worker.
42+
beforeAll(async () => {
43+
// eslint-disable-next-line @typescript-eslint/no-require-imports
44+
const RustSdkCryptoJs = await require("@matrix-org/matrix-sdk-crypto-wasm");
45+
await RustSdkCryptoJs.initAsync();
46+
}, 10000);
47+
48+
afterEach(() => {
49+
// reset fake-indexeddb after each test, to make sure we don't leak connections
50+
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
51+
// eslint-disable-next-line no-global-assign
52+
indexedDB = new IDBFactory();
53+
});
54+
55+
const ROOM_ID = "!room:example.com";
56+
const ALICE_HOMESERVER_URL = "https://alice-server.com";
57+
const BOB_HOMESERVER_URL = "https://bob-server.com";
58+
59+
async function createAndInitClient(homeserverUrl: string, userId: string) {
60+
mockInitialApiRequests(homeserverUrl, userId);
61+
62+
const client = createClient({
63+
baseUrl: homeserverUrl,
64+
userId: userId,
65+
accessToken: "akjgkrgjs",
66+
deviceId: "xzcvb",
67+
logger: new DebugLogger(mkDebug(`matrix-js-sdk:${userId}`)),
68+
});
69+
70+
await client.initRustCrypto({ cryptoDatabasePrefix: userId });
71+
await client.startClient();
72+
await client.getCrypto()!.bootstrapCrossSigning({ setupNewCrossSigning: true });
73+
return client;
74+
}
75+
76+
describe("History Sharing", () => {
77+
let aliceClient: MatrixClient;
78+
let aliceSyncResponder: SyncResponder;
79+
let bobClient: MatrixClient;
80+
let bobSyncResponder: SyncResponder;
81+
82+
beforeEach(async () => {
83+
// anything that we don't have a specific matcher for silently returns a 404
84+
fetchMock.catch(404);
85+
fetchMock.config.warnOnFallback = false;
86+
mockSetupCrossSigningRequests();
87+
88+
const aliceId = "@alice:localhost";
89+
const bobId = "@bob:xyz";
90+
91+
const aliceKeyReceiver = new E2EKeyReceiver(ALICE_HOMESERVER_URL, "alice-");
92+
const aliceKeyResponder = new E2EKeyResponder(ALICE_HOMESERVER_URL);
93+
const aliceKeyClaimResponder = new E2EOTKClaimResponder(ALICE_HOMESERVER_URL);
94+
aliceSyncResponder = new SyncResponder(ALICE_HOMESERVER_URL);
95+
96+
const bobKeyReceiver = new E2EKeyReceiver(BOB_HOMESERVER_URL, "bob-");
97+
const bobKeyResponder = new E2EKeyResponder(BOB_HOMESERVER_URL);
98+
bobSyncResponder = new SyncResponder(BOB_HOMESERVER_URL);
99+
100+
aliceKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
101+
aliceKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
102+
bobKeyResponder.addKeyReceiver(aliceId, aliceKeyReceiver);
103+
bobKeyResponder.addKeyReceiver(bobId, bobKeyReceiver);
104+
105+
aliceClient = await createAndInitClient(ALICE_HOMESERVER_URL, aliceId);
106+
bobClient = await createAndInitClient(BOB_HOMESERVER_URL, bobId);
107+
108+
aliceKeyClaimResponder.addKeyReceiver(bobId, bobClient.deviceId!, bobKeyReceiver);
109+
110+
aliceSyncResponder.sendOrQueueSyncResponse({});
111+
await syncPromise(aliceClient);
112+
113+
bobSyncResponder.sendOrQueueSyncResponse({});
114+
await syncPromise(bobClient);
115+
});
116+
117+
test("Room keys are successfully shared on invite", async () => {
118+
// Alice is in an encrypted room
119+
const syncResponse = getSyncResponse([aliceClient.getSafeUserId()], ROOM_ID);
120+
aliceSyncResponder.sendOrQueueSyncResponse(syncResponse);
121+
await syncPromise(aliceClient);
122+
123+
// ... and she sends an event
124+
const msgProm = expectSendRoomEvent(ALICE_HOMESERVER_URL, "m.room.encrypted");
125+
await aliceClient.sendEvent(ROOM_ID, EventType.RoomMessage, { msgtype: MsgType.Text, body: "Hi!" });
126+
const sentMessage = await msgProm;
127+
debug(`Alice sent encrypted room event: ${JSON.stringify(sentMessage)}`);
128+
129+
// Now, Alice invites Bob
130+
const uploadProm = new Promise<Uint8Array>((resolve) => {
131+
fetchMock.postOnce(new URL("/_matrix/media/v3/upload", ALICE_HOMESERVER_URL).toString(), (url, request) => {
132+
const body = request.body as Uint8Array;
133+
debug(`Alice uploaded blob of length ${body.length}`);
134+
resolve(body);
135+
return { content_uri: "mxc://alice-server/here" };
136+
});
137+
});
138+
const toDeviceMessageProm = expectSendToDeviceMessage(ALICE_HOMESERVER_URL, "m.room.encrypted");
139+
// POST https://alice-server.com/_matrix/client/v3/rooms/!room%3Aexample.com/invite
140+
fetchMock.postOnce(`${ALICE_HOMESERVER_URL}/_matrix/client/v3/rooms/${encodeURIComponent(ROOM_ID)}/invite`, {});
141+
await aliceClient.invite(ROOM_ID, bobClient.getSafeUserId(), { shareEncryptedHistory: true });
142+
const uploadedBlob = await uploadProm;
143+
const sentToDeviceRequest = await toDeviceMessageProm;
144+
debug(`Alice sent encrypted to-device events: ${JSON.stringify(sentToDeviceRequest)}`);
145+
const bobToDeviceMessage = sentToDeviceRequest[bobClient.getSafeUserId()][bobClient.deviceId!];
146+
expect(bobToDeviceMessage).toBeDefined();
147+
148+
// Bob receives the to-device event and the room invite
149+
const inviteEvent = mkEventCustom({
150+
type: "m.room.member",
151+
sender: aliceClient.getSafeUserId(),
152+
state_key: bobClient.getSafeUserId(),
153+
content: { membership: KnownMembership.Invite },
154+
});
155+
bobSyncResponder.sendOrQueueSyncResponse({
156+
rooms: { invite: { [ROOM_ID]: { invite_state: { events: [inviteEvent] } } } },
157+
to_device: {
158+
events: [
159+
{
160+
type: "m.room.encrypted",
161+
sender: aliceClient.getSafeUserId(),
162+
content: bobToDeviceMessage,
163+
},
164+
],
165+
},
166+
});
167+
await syncPromise(bobClient);
168+
169+
const room = bobClient.getRoom(ROOM_ID);
170+
expect(room).toBeTruthy();
171+
expect(room?.getMyMembership()).toEqual(KnownMembership.Invite);
172+
173+
fetchMock.postOnce(`${BOB_HOMESERVER_URL}/_matrix/client/v3/join/${encodeURIComponent(ROOM_ID)}`, {
174+
room_id: ROOM_ID,
175+
});
176+
fetchMock.getOnce(
177+
`begin:${BOB_HOMESERVER_URL}/_matrix/client/v1/media/download/alice-server/here`,
178+
{ body: uploadedBlob },
179+
{ sendAsJson: false },
180+
);
181+
await bobClient.joinRoom(ROOM_ID, { acceptSharedHistory: true });
182+
183+
// Bob receives, should be able to decrypt, the megolm message
184+
const bobSyncResponse = getSyncResponse([aliceClient.getSafeUserId(), bobClient.getSafeUserId()], ROOM_ID);
185+
bobSyncResponse.rooms.join[ROOM_ID].timeline.events.push(
186+
mkEventCustom({
187+
type: "m.room.encrypted",
188+
sender: aliceClient.getSafeUserId(),
189+
content: sentMessage,
190+
event_id: "$event_id",
191+
}) as any,
192+
);
193+
bobSyncResponder.sendOrQueueSyncResponse(bobSyncResponse);
194+
await syncPromise(bobClient);
195+
196+
const bobRoom = bobClient.getRoom(ROOM_ID);
197+
const event = bobRoom!.getLastLiveEvent()!;
198+
expect(event.getId()).toEqual("$event_id");
199+
await event.getDecryptionPromise();
200+
expect(event.getType()).toEqual("m.room.message");
201+
expect(event.getContent().body).toEqual("Hi!");
202+
});
203+
204+
afterEach(async () => {
205+
bobClient.stopClient();
206+
aliceClient.stopClient();
207+
await flushPromises();
208+
});
209+
});
210+
211+
function expectSendRoomEvent(homeserverUrl: string, msgtype: string): Promise<IContent> {
212+
return new Promise<IContent>((resolve) => {
213+
fetchMock.putOnce(
214+
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/rooms/[^/]*/send/${escapeRegExp(msgtype)}/`),
215+
(url, request) => {
216+
const content = JSON.parse(request.body as string);
217+
resolve(content);
218+
return { event_id: "$event_id" };
219+
},
220+
{ name: "sendRoomEvent" },
221+
);
222+
});
223+
}
224+
225+
function expectSendToDeviceMessage(
226+
homeserverUrl: string,
227+
msgtype: string,
228+
): Promise<Record<string, Record<string, object>>> {
229+
return new Promise((resolve) => {
230+
fetchMock.putOnce(
231+
new RegExp(`^${escapeRegExp(homeserverUrl)}/_matrix/client/v3/sendToDevice/${escapeRegExp(msgtype)}/`),
232+
(url: string, opts: RequestInit) => {
233+
const body = JSON.parse(opts.body as string);
234+
resolve(body.messages);
235+
return {};
236+
},
237+
);
238+
});
239+
}

0 commit comments

Comments
 (0)