Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf06547063 | |||
| 620dc2f6e2 | |||
| 00d78077b0 | |||
| 23d2d03912 | |||
| 142c0a65e6 | |||
| b9aacea1cb | |||
| 76e653b7ee | |||
| 35d862ebd3 | |||
| 581b3209ab | |||
| 6855ace642 | |||
| 5033d48013 | |||
| 4c53836a13 | |||
| 10a4fd8328 | |||
| 98f7637683 | |||
| 0df8e81da4 | |||
| 30b1894f37 | |||
| fbbdb6e766 | |||
| 5a1488ebd5 | |||
| 794f044dda | |||
| a197afe8aa | |||
| 1061b93b29 | |||
| 6528a59fc1 | |||
| f6a169b5a5 | |||
| d04135cc1c | |||
| 546047a050 | |||
| 16153e5d82 | |||
| fd73d5068c | |||
| e72859a44a |
@@ -63,6 +63,10 @@ module.exports = {
|
||||
name: "setImmediate",
|
||||
message: "Use setTimeout instead.",
|
||||
},
|
||||
{
|
||||
name: "global",
|
||||
message: "Use globalThis instead.",
|
||||
},
|
||||
],
|
||||
|
||||
"import/no-restricted-paths": [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
/spec/*/webrtc @matrix-org/element-call-reviewers
|
||||
/spec/*/matrixrtc @matrix-org/element-call-reviewers
|
||||
|
||||
/src/crypto-api @matrix-org/element-crypto-web-reviewers
|
||||
/src/crypto @matrix-org/element-crypto-web-reviewers
|
||||
/src/rust-crypto @matrix-org/element-crypto-web-reviewers
|
||||
/spec/integ/crypto @matrix-org/element-crypto-web-reviewers
|
||||
|
||||
@@ -49,9 +49,6 @@ jobs:
|
||||
git checkout develop
|
||||
git merge -X ours master
|
||||
|
||||
- name: Run post-merge-master script to revert package.json fields
|
||||
run: ./.action-repo/scripts/release/post-merge-master.sh
|
||||
|
||||
- name: Reset dependencies
|
||||
if: inputs.dependencies
|
||||
run: |
|
||||
|
||||
@@ -136,7 +136,9 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Bump package.json version
|
||||
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
run: |
|
||||
yarn version --no-git-tag-version --new-version "${VERSION#v}"
|
||||
git add package.json
|
||||
|
||||
- name: Add to CHANGELOG.md
|
||||
if: inputs.final
|
||||
@@ -158,11 +160,6 @@ jobs:
|
||||
env:
|
||||
RELEASE_NOTES: ${{ steps.draft-release.outputs.body }}
|
||||
|
||||
- name: Run pre-release script to update package.json fields
|
||||
run: |
|
||||
./.action-repo/scripts/release/pre-release.sh
|
||||
git add package.json
|
||||
|
||||
- name: Commit changes
|
||||
run: git commit -m "$VERSION"
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ jobs:
|
||||
tag: next
|
||||
ignore-scripts: false
|
||||
|
||||
- name: Check npm package was published
|
||||
if: steps.npm-publish.outputs.id == ''
|
||||
run: exit 1
|
||||
|
||||
- name: 🎖️ Add `latest` dist-tag to final releases
|
||||
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
|
||||
run: npm dist-tag add "$release" latest
|
||||
|
||||
@@ -26,16 +26,6 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
Changes in [34.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.12.0) (2024-11-19)
|
||||
====================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Deprecate `MatrixClient.getKeyBackupVersion` ([#4505](https://github.com/matrix-org/matrix-js-sdk/pull/4505)). Contributed by @florianduros.
|
||||
* Deprecate unused callbacks in `CryptoCallbacks` ([#4501](https://github.com/matrix-org/matrix-js-sdk/pull/4501)). Contributed by @florianduros.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Handle M\_MAX\_DELAY\_EXCEEDED errors ([#4511](https://github.com/matrix-org/matrix-js-sdk/pull/4511)). Contributed by @AndrewFerr.
|
||||
* Allow configuration of MatrixRTC timers when calling joinRoomSession() ([#4510](https://github.com/matrix-org/matrix-js-sdk/pull/4510)). Contributed by @hughns.
|
||||
* When state says you've left ongoing call, rejoin ([#4342](https://github.com/matrix-org/matrix-js-sdk/pull/4342)). Contributed by @AndrewFerr.
|
||||
* Remove redundant type arguments in function call ([#4507](https://github.com/matrix-org/matrix-js-sdk/pull/4507)). Contributed by @AndrewFerr.
|
||||
* MatrixRTCSession: handle rate limit errors ([#4494](https://github.com/matrix-org/matrix-js-sdk/pull/4494)). Contributed by @AndrewFerr.
|
||||
* Send/receive error details with widgets ([#4492](https://github.com/matrix-org/matrix-js-sdk/pull/4492)). Contributed by @AndrewFerr.
|
||||
* Capture HTTP error response headers \& handle Retry-After header (MSC4041) ([#4471](https://github.com/matrix-org/matrix-js-sdk/pull/4471)). Contributed by @AndrewFerr.
|
||||
* Add RoomWidgetClient.sendToDeviceViaWidgetApi() ([#4475](https://github.com/matrix-org/matrix-js-sdk/pull/4475)). Contributed by @hughns.
|
||||
|
||||
|
||||
Changes in [34.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v34.11.1) (2024-11-12)
|
||||
====================================================================================================
|
||||
# Security
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "34.11.1",
|
||||
"version": "34.12.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -58,12 +58,12 @@
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
|
||||
for i in main typings browser
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# For the published and dist versions of the package,
|
||||
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
|
||||
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
|
||||
# needing to build before linting or testing.
|
||||
|
||||
for i in main typings browser
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
|
||||
fi
|
||||
done
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fsProm = require("fs/promises");
|
||||
|
||||
const PKGJSON = "package.json";
|
||||
|
||||
async function main() {
|
||||
const pkgJson = JSON.parse(await fsProm.readFile(PKGJSON, "utf8"));
|
||||
for (const field of ["main", "typings"]) {
|
||||
if (pkgJson["matrix_lib_" + field] !== undefined) {
|
||||
pkgJson[field] = pkgJson["matrix_lib_" + field];
|
||||
}
|
||||
}
|
||||
|
||||
await fsProm.writeFile(PKGJSON, JSON.stringify(pkgJson, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1 +0,0 @@
|
||||
switch_package_to_release.cjs
|
||||
+1
-1
@@ -66,7 +66,7 @@ export class TestClient implements IE2EKeyReceiver, ISyncResponder {
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: this.httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
...options,
|
||||
};
|
||||
if (!fullOptions.cryptoStore) {
|
||||
|
||||
@@ -91,7 +91,6 @@ import {
|
||||
OnlySignedDevicesIsolationMode,
|
||||
} from "../../../src/crypto-api";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import {
|
||||
createOlmAccount,
|
||||
createOlmSession,
|
||||
@@ -106,6 +105,7 @@ import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
|
||||
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
|
||||
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -136,7 +136,7 @@ async function expectSendRoomKey(
|
||||
recipientOlmAccount: Olm.Account,
|
||||
recipientOlmSession: Olm.Session | null = null,
|
||||
): Promise<Olm.InboundGroupSession> {
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
const testRecipientKey = JSON.parse(recipientOlmAccount.identity_keys())["curve25519"];
|
||||
|
||||
function onSendRoomKey(content: any): Olm.InboundGroupSession {
|
||||
@@ -219,7 +219,7 @@ async function expectSendMegolmMessage(
|
||||
}
|
||||
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
// currently we use libolm to implement the crypto in the tests, so need it to be present.
|
||||
logger.warn("not running megolm tests: Olm not present");
|
||||
return;
|
||||
@@ -230,7 +230,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
const newBackendOnly = backend !== "rust-sdk" ? test.skip : test;
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
let testOlmAccount = {} as unknown as Olm.Account;
|
||||
let testSenderKey = "";
|
||||
@@ -1897,13 +1897,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
const p2pSession = new globalThis.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
"readonly",
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
|
||||
@@ -2045,13 +2045,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
const aliceOtks = await keyReceiver.awaitOneTimeKeyUpload();
|
||||
const aliceOtkId = Object.keys(aliceOtks)[0];
|
||||
const aliceOtk = aliceOtks[aliceOtkId];
|
||||
const p2pSession = new global.Olm.Session();
|
||||
const p2pSession = new globalThis.Olm.Session();
|
||||
await beccaTestClient.client.crypto!.cryptoStore.doTxn(
|
||||
"readonly",
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
beccaTestClient.client.crypto!.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
account.unpickle(beccaTestClient.client.crypto!.olmDevice.pickleKey, pickledAccount!);
|
||||
p2pSession.create_outbound(account, keyReceiver.getDeviceKey(), aliceOtk.key);
|
||||
@@ -3138,11 +3138,11 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
// Import a new key that should be uploaded
|
||||
const newKey = testData.MEGOLM_SESSION_DATA;
|
||||
|
||||
const awaitKeyUploaded = new Promise<IKeyBackup>((resolve) => {
|
||||
const awaitKeyUploaded = new Promise<KeyBackup>((resolve) => {
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/v3/room_keys/keys",
|
||||
(url, request) => {
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
resolve(uploadPayload);
|
||||
return {
|
||||
status: 200,
|
||||
|
||||
@@ -42,10 +42,10 @@ import {
|
||||
} from "../../test-utils/test-utils";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keybackup";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { defer, IDeferred } from "../../../src/utils";
|
||||
import { DecryptionFailureCode } from "../../../src/crypto-api";
|
||||
import { KeyBackup } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const ROOM_ID = testData.TEST_ROOM_ID;
|
||||
|
||||
@@ -91,7 +91,7 @@ function mockUploadEmitter(
|
||||
},
|
||||
};
|
||||
}
|
||||
const uploadPayload: IKeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
const uploadPayload: KeyBackup = JSON.parse(request.body?.toString() ?? "{}");
|
||||
let count = 0;
|
||||
for (const [roomId, value] of Object.entries(uploadPayload.rooms)) {
|
||||
for (const sessionId of Object.keys(value.sessions)) {
|
||||
|
||||
@@ -85,15 +85,15 @@ export function bootstrapCrossSigningTestOlmAccount(
|
||||
deviceId: string,
|
||||
keyBackupInfo: KeyBackupInfo[] = [],
|
||||
): Partial<IDownloadKeyResult> {
|
||||
const olmAliceMSK = new global.Olm.PkSigning();
|
||||
const olmAliceMSK = new globalThis.Olm.PkSigning();
|
||||
const masterPrivkey = olmAliceMSK.generate_seed();
|
||||
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
|
||||
|
||||
const olmAliceUSK = new global.Olm.PkSigning();
|
||||
const olmAliceUSK = new globalThis.Olm.PkSigning();
|
||||
const userPrivkey = olmAliceUSK.generate_seed();
|
||||
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
|
||||
|
||||
const olmAliceSSK = new global.Olm.PkSigning();
|
||||
const olmAliceSSK = new globalThis.Olm.PkSigning();
|
||||
const sskPrivkey = olmAliceSSK.generate_seed();
|
||||
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
|
||||
|
||||
@@ -181,7 +181,7 @@ export async function createOlmSession(
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
|
||||
|
||||
beforeAll(async () => {
|
||||
// we use the libolm primitives in the test, so init the Olm library
|
||||
await global.Olm.init();
|
||||
await globalThis.Olm.init();
|
||||
});
|
||||
|
||||
// load the rust library. This can take a few seconds on a slow GH worker.
|
||||
@@ -264,7 +264,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
@@ -358,7 +358,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
|
||||
|
||||
// The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept'
|
||||
// We use the Curve25519, HMAC and HKDF implementations in libolm, for now
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage);
|
||||
|
||||
sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key");
|
||||
|
||||
@@ -67,7 +67,7 @@ function getSyncResponse(roomMembers: string[]) {
|
||||
}
|
||||
|
||||
describe("DeviceList management:", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("not running deviceList tests: Olm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: undefined,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
@@ -135,7 +135,7 @@ describe("MatrixClient opts", function () {
|
||||
let client: MatrixClient;
|
||||
beforeEach(function () {
|
||||
client = new MatrixClient({
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store: new MemoryStore() as IStore,
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
|
||||
@@ -577,7 +577,7 @@ describe("MatrixClient syncing", () => {
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||
const member = client!.getRoom(roomOne)!.getMember(userC)!;
|
||||
expect(member.name).toEqual("The Boss");
|
||||
expect(member.getAvatarUrl("home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
expect(member.getAvatarUrl("https://home.server.url", 1, 1, "", false, false)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2406,7 +2406,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
it("should emit ClientEvent.Room when invited while using indexeddb crypto store", async () => {
|
||||
const idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
cryptoStore: new IndexedDBCryptoStore(global.indexedDB, "tests"),
|
||||
cryptoStore: new IndexedDBCryptoStore(globalThis.indexedDB, "tests"),
|
||||
});
|
||||
const idbHttpBackend = idbTestClient.httpBackend;
|
||||
const idbClient = idbTestClient.client;
|
||||
@@ -2486,7 +2486,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
let idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
@@ -2558,7 +2558,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
|
||||
idbTestClient = new TestClient(selfUserId, "DEVICE", selfAccessToken, undefined, {
|
||||
store: new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
indexedDB: globalThis.indexedDB,
|
||||
dbName: "test",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -266,7 +266,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should abort if device doesn't come up by timeout", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
@@ -319,7 +319,7 @@ describe("MSC4108SignInWithQR", () => {
|
||||
});
|
||||
|
||||
it("should not send secrets if user cancels", async () => {
|
||||
jest.spyOn(global, "setTimeout").mockImplementation((fn) => {
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation((fn) => {
|
||||
fn();
|
||||
// TODO: mock timers properly
|
||||
return -1 as any;
|
||||
|
||||
@@ -560,7 +560,7 @@ export const CRYPTO_BACKENDS: Record<string, InitCrypto> = {};
|
||||
export type InitCrypto = (_: MatrixClient) => Promise<void>;
|
||||
|
||||
CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
|
||||
if (global.Olm) {
|
||||
if (globalThis.Olm) {
|
||||
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
|
||||
}
|
||||
|
||||
|
||||
@@ -582,11 +582,11 @@ export class MockCallFeed {
|
||||
}
|
||||
|
||||
export function installWebRTCMocks() {
|
||||
global.navigator = {
|
||||
globalThis.navigator = {
|
||||
mediaDevices: new MockMediaDevices().typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
global.window = {
|
||||
globalThis.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
@@ -596,13 +596,13 @@ export function installWebRTCMocks() {
|
||||
getUserMedia: () => new MockMediaStream("local_stream"),
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
globalThis.document = {};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.AudioContext = MockAudioContext;
|
||||
globalThis.AudioContext = MockAudioContext;
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpReceiver = {
|
||||
globalThis.RTCRtpReceiver = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
@@ -610,7 +610,7 @@ export function installWebRTCMocks() {
|
||||
};
|
||||
|
||||
// @ts-ignore Mock
|
||||
global.RTCRtpSender = {
|
||||
globalThis.RTCRtpSender = {
|
||||
getCapabilities: jest.fn<RTCRtpCapabilities, [string]>().mockReturnValue({
|
||||
codecs: [],
|
||||
headerExtensions: [],
|
||||
|
||||
@@ -22,12 +22,12 @@ import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
// keep to reset the fetch function after using MockHttpBackend
|
||||
// @ts-ignore private property
|
||||
const realAutoDiscoveryFetch: typeof global.fetch = AutoDiscovery.fetchFn;
|
||||
const realAutoDiscoveryFetch: typeof globalThis.fetch = AutoDiscovery.fetchFn;
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
const httpBackend = new MockHttpBackend();
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof global.fetch);
|
||||
AutoDiscovery.setFetchFn(httpBackend.fetchFn as typeof globalThis.fetch);
|
||||
return httpBackend;
|
||||
};
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = undefined;
|
||||
|
||||
global.atob = NodeBuffer.atob;
|
||||
global.btoa = NodeBuffer.btoa;
|
||||
globalThis.atob = NodeBuffer.atob;
|
||||
globalThis.btoa = NodeBuffer.btoa;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,9 +38,9 @@ describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
Buffer = origBuffer;
|
||||
// @ts-ignore
|
||||
global.atob = undefined;
|
||||
globalThis.atob = undefined;
|
||||
// @ts-ignore
|
||||
global.btoa = undefined;
|
||||
globalThis.btoa = undefined;
|
||||
});
|
||||
|
||||
it("Should decode properly encoded data", () => {
|
||||
|
||||
@@ -29,10 +29,10 @@ describe("Beacon content helpers", () => {
|
||||
describe("makeBeaconInfoContent()", () => {
|
||||
const mockDateNow = 123456789;
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(mockDateNow);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(mockDateNow);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
it("create fully defined event content", () => {
|
||||
expect(makeBeaconInfoContent(1234, true, "nice beacon_info", LocationAssetType.Pin)).toEqual({
|
||||
|
||||
@@ -28,7 +28,7 @@ import * as testData from "../test-utils/test-data";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import type { DeviceInfoMap } from "../../src/crypto/DeviceList";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
function awaitEvent(emitter: EventEmitter, event: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -44,13 +44,13 @@ badKey[0] ^= 1;
|
||||
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should throw if no callback is provided", async () => {
|
||||
@@ -80,7 +80,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
// check that the pkSigning object corresponds to the pubKey
|
||||
const signature = pkSigning.sign("message");
|
||||
const util = new global.Olm.Utility();
|
||||
const util = new globalThis.Olm.Utility();
|
||||
try {
|
||||
util.ed25519_verify(pubKey, "message", signature);
|
||||
} finally {
|
||||
@@ -199,7 +199,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function () {
|
||||
* it's not possible to get one in normal execution unless you hack as we do here.
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new IndexedDBCryptoStore(undefined!, "tests")],
|
||||
[
|
||||
"MemoryCryptoStore",
|
||||
|
||||
@@ -43,10 +43,10 @@ const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get("m.megolm.v1.aes-sha2
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("MegolmDecryption", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ describe("MegolmDecryption", function () {
|
||||
describe("receives some keys:", function () {
|
||||
let groupSession: OutboundGroupSession;
|
||||
beforeEach(async function () {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession = new globalThis.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
|
||||
@@ -47,13 +47,13 @@ function alwaysSucceed<T>(promise: Promise<T>): Promise<T | void> {
|
||||
}
|
||||
|
||||
describe("OlmDevice", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice: OlmDevice;
|
||||
|
||||
@@ -33,7 +33,7 @@ import { CryptoStore } from "../../../src/crypto/store/base";
|
||||
import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm";
|
||||
import { IKeyBackupInfo } from "../../../src/crypto/keybackup";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get("m.megolm.v1.aes-sha2")!;
|
||||
|
||||
@@ -155,7 +155,7 @@ function makeTestClient(cryptoStore: CryptoStore) {
|
||||
}
|
||||
|
||||
describe("MegolmBackup", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
@@ -205,14 +205,14 @@ describe("MegolmBackup", function () {
|
||||
// clobber the setTimeout function to run 100x faster.
|
||||
// ideally we would use lolex, but we have no oportunity
|
||||
// to tick the clock between the first try and the retry.
|
||||
const realSetTimeout = global.setTimeout;
|
||||
jest.spyOn(global, "setTimeout").mockImplementation(function (f, n) {
|
||||
const realSetTimeout = globalThis.setTimeout;
|
||||
jest.spyOn(globalThis, "setTimeout").mockImplementation(function (f, n) {
|
||||
return realSetTimeout(f!, n! / 100);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
jest.spyOn(global, "setTimeout").mockRestore();
|
||||
jest.spyOn(globalThis, "setTimeout").mockRestore();
|
||||
});
|
||||
|
||||
test("fail if crypto not enabled", async () => {
|
||||
|
||||
@@ -84,13 +84,13 @@ async function makeTestClient(
|
||||
}
|
||||
|
||||
describe("Cross Signing", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function () {
|
||||
@@ -369,10 +369,10 @@ describe("Cross Signing", function () {
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -488,7 +488,7 @@ describe("Cross Signing", function () {
|
||||
};
|
||||
await alice.crypto!.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
const bobOlmAccount = new globalThis.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
|
||||
const bobDeviceUnsigned = {
|
||||
@@ -622,10 +622,10 @@ describe("Cross Signing", function () {
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -688,10 +688,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
|
||||
const bobSigning = new global.Olm.PkSigning();
|
||||
const bobSigning = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey = bobSigning.generate_seed();
|
||||
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
|
||||
const bobSSK: CrossSigningKeyInfo = {
|
||||
@@ -755,10 +755,10 @@ describe("Cross Signing", function () {
|
||||
expect(bobDeviceTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// Alice downloads new SSK for Bob
|
||||
const bobMasterSigning2 = new global.Olm.PkSigning();
|
||||
const bobMasterSigning2 = new globalThis.Olm.PkSigning();
|
||||
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
|
||||
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
|
||||
const bobSigning2 = new global.Olm.PkSigning();
|
||||
const bobSigning2 = new globalThis.Olm.PkSigning();
|
||||
const bobPrivkey2 = bobSigning2.generate_seed();
|
||||
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
|
||||
const bobSSK2: CrossSigningKeyInfo = {
|
||||
@@ -905,10 +905,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -980,10 +980,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -1040,10 +1040,10 @@ describe("Cross Signing", function () {
|
||||
alice.uploadKeySignatures = async () => ({ failures: {} });
|
||||
|
||||
// Generate Alice's SSK etc
|
||||
const aliceMasterSigning = new global.Olm.PkSigning();
|
||||
const aliceMasterSigning = new globalThis.Olm.PkSigning();
|
||||
const aliceMasterPrivkey = aliceMasterSigning.generate_seed();
|
||||
const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey);
|
||||
const aliceSigning = new global.Olm.PkSigning();
|
||||
const aliceSigning = new globalThis.Olm.PkSigning();
|
||||
const alicePrivkey = aliceSigning.generate_seed();
|
||||
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
|
||||
const aliceSSK: CrossSigningKeyInfo = {
|
||||
@@ -1088,12 +1088,12 @@ describe("Cross Signing", function () {
|
||||
});
|
||||
|
||||
describe("userHasCrossSigningKeys", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function resetCrossSigningKeys(
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey(): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
|
||||
@@ -19,16 +19,16 @@ import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { DEHYDRATION_ALGORITHM } from "../../../src/crypto/dehydration";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("Dehydration", () => {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running dehydration unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should rehydrate a dehydrated device", async () => {
|
||||
|
||||
@@ -51,7 +51,7 @@ const requests = [
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("Outgoing room key requests [%s]", function (name, dbFactory) {
|
||||
|
||||
@@ -69,20 +69,20 @@ function sign<T extends IObject | ICurve25519AuthData>(
|
||||
}
|
||||
|
||||
describe("Secrets", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running megolm backup unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function () {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signing = new globalThis.Olm.PkSigning();
|
||||
const signingKey = signing.generate_seed();
|
||||
const signingPubKey = signing.init_with_seed(signingKey);
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("Secrets", function () {
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function () {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
const bob: MatrixClient = await makeTestClient(
|
||||
|
||||
@@ -20,7 +20,7 @@ import { IndexedDBCryptoStore, LocalStorageCryptoStore, MemoryCryptoStore } from
|
||||
import { CryptoStore, MigrationState, SESSION_BATCH_SIZE } from "../../../../src/crypto/store/base";
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["IndexedDBCryptoStore", () => new IndexedDBCryptoStore(globalThis.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
|
||||
["MemoryCryptoStore", () => new MemoryCryptoStore()],
|
||||
])("CryptoStore tests for %s", function (name, dbFactory) {
|
||||
|
||||
@@ -27,21 +27,21 @@ describe("IndexedDBCryptoStore", () => {
|
||||
|
||||
it("Should be true if there is a legacy database", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("Should be true if there is a legacy database in non migrated state", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(MigrationState.NOT_STARTED);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -54,18 +54,18 @@ describe("IndexedDBCryptoStore", () => {
|
||||
])("Exists and Migration state is %s", (migrationState) => {
|
||||
it("Should be false if migration has started", async () => {
|
||||
// should detect a store that is not migrated
|
||||
const store = new IndexedDBCryptoStore(global.indexedDB, "tests");
|
||||
const store = new IndexedDBCryptoStore(globalThis.indexedDB, "tests");
|
||||
await store.startup();
|
||||
await store.setMigrationState(migrationState);
|
||||
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should be false if there is no legacy database", async () => {
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(global.indexedDB, "tests");
|
||||
const result = await IndexedDBCryptoStore.existsAndIsNotMigrated(globalThis.indexedDB, "tests");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
@@ -17,10 +17,10 @@ limitations under the License.
|
||||
import "../../../olm-loader";
|
||||
import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
describe("QR code verification", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients } from "./util";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request integration tests with crypto layer", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ import { MatrixClient } from "../../../../src";
|
||||
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
const Olm = globalThis.Olm;
|
||||
|
||||
let ALICE_DEVICES: Record<string, IDevice>;
|
||||
let BOB_DEVICES: Record<string, IDevice>;
|
||||
|
||||
describe("SAS verification", function () {
|
||||
if (!global.Olm) {
|
||||
if (!globalThis.Olm) {
|
||||
logger.warn("Not running device verification unit tests: libolm not present");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function () {
|
||||
return global.Olm.init();
|
||||
return globalThis.Olm.init();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
|
||||
@@ -29,12 +29,12 @@ describe("sha256", () => {
|
||||
});
|
||||
|
||||
it("throws if webcrypto is not available", async () => {
|
||||
const oldCrypto = global.crypto;
|
||||
const oldCrypto = globalThis.crypto;
|
||||
try {
|
||||
global.crypto = {} as any;
|
||||
globalThis.crypto = {} as any;
|
||||
await expect(sha256("test")).rejects.toThrow();
|
||||
} finally {
|
||||
global.crypto = oldCrypto;
|
||||
globalThis.crypto = oldCrypto;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+112
-3
@@ -30,12 +30,13 @@ import {
|
||||
ITurnServer,
|
||||
IRoomEvent,
|
||||
IOpenIDCredentials,
|
||||
WidgetApiResponseError,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { createRoomWidgetClient, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import { createRoomWidgetClient, MatrixError, MsgType, UpdateDelayedEventAction } from "../../src/matrix";
|
||||
import { MatrixClient, ClientEvent, ITurnServer as IClientTurnServer } from "../../src/client";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { ICapabilities } from "../../src/embedded";
|
||||
import { ICapabilities, RoomWidgetClient } from "../../src/embedded";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { ToDeviceBatch } from "../../src/models/ToDeviceMessage";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
@@ -90,7 +91,11 @@ class MockWidgetApi extends EventEmitter {
|
||||
public getTurnServers = jest.fn(() => []);
|
||||
public sendContentLoaded = jest.fn();
|
||||
|
||||
public transport = { reply: jest.fn() };
|
||||
public transport = {
|
||||
reply: jest.fn(),
|
||||
send: jest.fn(),
|
||||
sendComplete: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
declare module "../../src/types" {
|
||||
@@ -187,6 +192,46 @@ describe("RoomWidgetClient", () => {
|
||||
.map((e) => e.getEffectiveEvent()),
|
||||
).toEqual([event]);
|
||||
});
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to send");
|
||||
widgetApi.transport.send.mockRejectedValue(error);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it("handles widget errors with Matrix API error response data", async () => {
|
||||
const errorStatusCode = 400;
|
||||
const errorUrl = "http://example.org";
|
||||
const errorData = {
|
||||
errcode: "M_BAD_JSON",
|
||||
error: "Invalid body",
|
||||
};
|
||||
|
||||
const widgetError = new WidgetApiResponseError("failed to send", {
|
||||
matrix_api_error: {
|
||||
http_status: errorStatusCode,
|
||||
http_headers: {},
|
||||
url: errorUrl,
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
|
||||
widgetApi.transport.send.mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({ sendEvent: ["org.matrix.rageshake_request"] });
|
||||
widgetApi.sendRoomEvent.mockImplementation(widgetApi.transport.send);
|
||||
|
||||
await expect(
|
||||
client.sendEvent("!1:example.org", "org.matrix.rageshake_request", { request_id: 123 }),
|
||||
).rejects.toThrow(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayed events", () => {
|
||||
@@ -493,6 +538,23 @@ describe("RoomWidgetClient", () => {
|
||||
["@bob:example.org"]: { ["bobDesktop"]: { hello: "bob!" } },
|
||||
};
|
||||
|
||||
const encryptedContentMap = new Map<string, Map<string, object>>([
|
||||
["@alice:example.org", new Map([["aliceMobile", { hello: "alice!" }]])],
|
||||
["@bob:example.org", new Map([["bobDesktop", { hello: "bob!" }]])],
|
||||
]);
|
||||
|
||||
it("sends unencrypted (sendToDeviceViaWidgetApi)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi(
|
||||
"org.example.foo",
|
||||
false,
|
||||
unencryptedContentMap,
|
||||
);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", false, expectedRequestData);
|
||||
});
|
||||
|
||||
it("sends unencrypted (sendToDevice)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
@@ -534,6 +596,17 @@ describe("RoomWidgetClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sends encrypted (sendToDeviceViaWidgetApi)", async () => {
|
||||
await makeClient({ sendToDevice: ["org.example.foo"] });
|
||||
expect(widgetApi.requestCapabilityToSendToDevice).toHaveBeenCalledWith("org.example.foo");
|
||||
|
||||
await (client as RoomWidgetClient).sendToDeviceViaWidgetApi("org.example.foo", true, encryptedContentMap);
|
||||
expect(widgetApi.sendToDevice).toHaveBeenCalledWith("org.example.foo", true, {
|
||||
"@alice:example.org": { aliceMobile: { hello: "alice!" } },
|
||||
"@bob:example.org": { bobDesktop: { hello: "bob!" } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encrypted: false, title: "unencrypted" },
|
||||
{ encrypted: true, title: "encrypted" },
|
||||
@@ -570,6 +643,42 @@ describe("RoomWidgetClient", () => {
|
||||
await makeClient({});
|
||||
expect(await client.getOpenIdToken()).toStrictEqual(testOIDCToken);
|
||||
});
|
||||
|
||||
it("handles widget errors with generic error data", async () => {
|
||||
const error = new Error("failed to get token");
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(error);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
await expect(client.getOpenIdToken()).rejects.toThrow(error);
|
||||
});
|
||||
|
||||
it("handles widget errors with Matrix API error response data", async () => {
|
||||
const errorStatusCode = 400;
|
||||
const errorUrl = "http://example.org";
|
||||
const errorData = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Bad request",
|
||||
};
|
||||
|
||||
const widgetError = new WidgetApiResponseError("failed to get token", {
|
||||
matrix_api_error: {
|
||||
http_status: errorStatusCode,
|
||||
http_headers: {},
|
||||
url: errorUrl,
|
||||
response: errorData,
|
||||
},
|
||||
});
|
||||
const matrixError = new MatrixError(errorData, errorStatusCode, errorUrl);
|
||||
|
||||
widgetApi.transport.sendComplete.mockRejectedValue(widgetError);
|
||||
|
||||
await makeClient({});
|
||||
widgetApi.requestOpenIDConnectToken.mockImplementation(widgetApi.transport.sendComplete as any);
|
||||
|
||||
await expect(client.getOpenIdToken()).rejects.toThrow(matrixError);
|
||||
});
|
||||
});
|
||||
|
||||
it("gets TURN servers", async () => {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixError } from "../../../src";
|
||||
|
||||
type IErrorJson = MatrixError["data"];
|
||||
|
||||
describe("MatrixError", () => {
|
||||
let headers: Headers;
|
||||
|
||||
beforeEach(() => {
|
||||
headers = new Headers({ "Content-Type": "application/json" });
|
||||
});
|
||||
|
||||
function makeMatrixError(httpStatus: number, data: IErrorJson, url?: string): MatrixError {
|
||||
return new MatrixError(data, httpStatus, url, undefined, headers);
|
||||
}
|
||||
|
||||
it("should accept absent retry time from rate-limit error", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(null);
|
||||
});
|
||||
|
||||
it("should retrieve retry_after_ms from rate-limit error", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(150000);
|
||||
});
|
||||
|
||||
it("should ignore retry_after_ms if errcode is not M_LIMIT_EXCEEDED", () => {
|
||||
const err = makeMatrixError(429, { errcode: "M_UNKNOWN", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
expect(err.getRetryAfterMs()).toEqual(null);
|
||||
});
|
||||
|
||||
it("should retrieve numeric Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should retrieve Date Retry-After header from rate-limit error", () => {
|
||||
headers.set("Retry-After", `${new Date(160000).toUTCString()}`);
|
||||
jest.spyOn(globalThis.Date, "now").mockImplementationOnce(() => 100000);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 150000 });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// prefer Retry-After header over retry_after_ms
|
||||
expect(err.getRetryAfterMs()).toEqual(60000);
|
||||
});
|
||||
|
||||
it("should prefer M_FORBIDDEN errcode over HTTP status code 429", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(429, { errcode: "M_FORBIDDEN" });
|
||||
expect(err.isRateLimitError()).toBe(false);
|
||||
// retrieve Retry-After header even for non-M_LIMIT_EXCEEDED errors
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should prefer M_LIMIT_EXCEEDED errcode over HTTP status code 400", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const err = makeMatrixError(400, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(err.isRateLimitError()).toBe(true);
|
||||
// retrieve Retry-After header even for non-429 errors
|
||||
expect(err.getRetryAfterMs()).toEqual(120000);
|
||||
});
|
||||
|
||||
it("should reject invalid Retry-After header", () => {
|
||||
for (const invalidValue of ["-1", "1.23", new Date(0).toString()]) {
|
||||
headers.set("Retry-After", invalidValue);
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(() => err.getRetryAfterMs()).toThrow(
|
||||
"value is not a valid HTTP-date or non-negative decimal integer",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should reject too-large Retry-After header", () => {
|
||||
headers.set("Retry-After", "1" + Array(500).fill("0").join(""));
|
||||
const err = makeMatrixError(429, { errcode: "M_LIMIT_EXCEEDED" });
|
||||
expect(() => err.getRetryAfterMs()).toThrow("integer value is too large");
|
||||
});
|
||||
|
||||
describe("can be converted to data compatible with the widget api", () => {
|
||||
it("from default values", () => {
|
||||
const matrixError = new MatrixError();
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: 400,
|
||||
http_headers: {},
|
||||
url: "",
|
||||
response: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown message",
|
||||
},
|
||||
};
|
||||
|
||||
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
|
||||
});
|
||||
|
||||
it("from non-default values", () => {
|
||||
headers.set("Retry-After", "120");
|
||||
const statusCode = 429;
|
||||
const data = {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
error: "Request is rate-limited.",
|
||||
retry_after_ms: 120000,
|
||||
};
|
||||
const url = "http://example.net";
|
||||
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {
|
||||
"content-type": "application/json",
|
||||
"retry-after": "120",
|
||||
},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
expect(matrixError.asWidgetApiErrorData()).toEqual(widgetApiErrorData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("can be created from data received from the widget api", () => {
|
||||
it("from minimal data", () => {
|
||||
const statusCode = 400;
|
||||
const data = {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Something went wrong.",
|
||||
};
|
||||
const url = "";
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
headers.delete("Content-Type");
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
|
||||
});
|
||||
|
||||
it("from more data", () => {
|
||||
const statusCode = 429;
|
||||
const data = {
|
||||
errcode: "M_LIMIT_EXCEEDED",
|
||||
error: "Request is rate-limited.",
|
||||
retry_after_ms: 120000,
|
||||
};
|
||||
const url = "http://example.net";
|
||||
|
||||
const widgetApiErrorData = {
|
||||
http_status: statusCode,
|
||||
http_headers: {
|
||||
"content-type": "application/json",
|
||||
"retry-after": "120",
|
||||
},
|
||||
url,
|
||||
response: data,
|
||||
};
|
||||
|
||||
headers.set("Retry-After", "120");
|
||||
const matrixError = makeMatrixError(statusCode, data, url);
|
||||
|
||||
expect(MatrixError.fromWidgetApiErrorData(widgetApiErrorData)).toEqual(matrixError);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,11 +60,11 @@ describe("FetchHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should fall back to global fetch if fetchFn not provided", () => {
|
||||
global.fetch = jest.fn();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
globalThis.fetch = jest.fn();
|
||||
expect(globalThis.fetch).not.toHaveBeenCalled();
|
||||
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix });
|
||||
api.fetch("test");
|
||||
expect(global.fetch).toHaveBeenCalled();
|
||||
expect(globalThis.fetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update identity server base url", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -41,12 +41,13 @@ describe("MatrixHttpApi", () => {
|
||||
setRequestHeader: jest.fn(),
|
||||
onreadystatechange: undefined,
|
||||
getResponseHeader: jest.fn(),
|
||||
getAllResponseHeaders: jest.fn(),
|
||||
} as unknown as XMLHttpRequest;
|
||||
// We stub out XHR here as it is not available in JSDOM
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
globalThis.XMLHttpRequest = jest.fn().mockReturnValue(xhr);
|
||||
// @ts-ignore
|
||||
global.XMLHttpRequest.DONE = DONE;
|
||||
globalThis.XMLHttpRequest.DONE = DONE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -59,7 +60,7 @@ describe("MatrixHttpApi", () => {
|
||||
});
|
||||
|
||||
it("should fall back to `fetch` where xhr is unavailable", () => {
|
||||
global.XMLHttpRequest = undefined!;
|
||||
globalThis.XMLHttpRequest = undefined!;
|
||||
const fetchFn = jest.fn().mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({}) });
|
||||
const api = new MatrixHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
|
||||
upload = api.uploadContent({} as File);
|
||||
@@ -171,7 +172,10 @@ describe("MatrixHttpApi", () => {
|
||||
xhr.readyState = DONE;
|
||||
xhr.responseText = '{"errcode": "M_NOT_FOUND", "error": "Not found"}';
|
||||
xhr.status = 404;
|
||||
mocked(xhr.getResponseHeader).mockReturnValue("application/json");
|
||||
mocked(xhr.getResponseHeader).mockImplementation((name) =>
|
||||
name.toLowerCase() === "content-type" ? "application/json" : null,
|
||||
);
|
||||
mocked(xhr.getAllResponseHeaders).mockReturnValue("content-type: application/json\r\n");
|
||||
// @ts-ignore
|
||||
xhr.onreadystatechange?.(new Event("test"));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -86,13 +86,28 @@ describe("anySignal", () => {
|
||||
});
|
||||
|
||||
describe("parseErrorResponse", () => {
|
||||
let headers: Headers;
|
||||
const xhrHeaderMethods = {
|
||||
getResponseHeader: (name: string) => headers.get(name),
|
||||
getAllResponseHeaders: () => {
|
||||
let allHeaders = "";
|
||||
headers.forEach((value, key) => {
|
||||
allHeaders += `${key.toLowerCase()}: ${value}\r\n`;
|
||||
});
|
||||
return allHeaders;
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
headers = new Headers();
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -108,14 +123,11 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -131,13 +143,12 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR with urls", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
responseURL: "https://example.com",
|
||||
getResponseHeader(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -154,15 +165,12 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch with urls", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
url: "https://example.com",
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "application/json" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -178,6 +186,66 @@ describe("parseErrorResponse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("with HTTP headers", () => {
|
||||
function addHeaders(headers: Headers) {
|
||||
headers.set("Age", "0");
|
||||
headers.set("Date", "Thu, 01 Jan 1970 00:00:00 GMT"); // value contains colons
|
||||
headers.set("x-empty", "");
|
||||
headers.set("x-multi", "1");
|
||||
headers.append("x-multi", "2");
|
||||
}
|
||||
|
||||
function compareHeaders(expectedHeaders: Headers, otherHeaders: Headers | undefined) {
|
||||
expect(new Map(otherHeaders)).toEqual(new Map(expectedHeaders));
|
||||
}
|
||||
|
||||
it("should resolve HTTP Errors from XHR with headers", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse({
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest) as HTTPError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve HTTP Errors from fetch with headers", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse({
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response) as HTTPError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from XHR with headers", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse(
|
||||
{
|
||||
...xhrHeaderMethods,
|
||||
status: 500,
|
||||
} as XMLHttpRequest,
|
||||
'{"errcode": "TEST"}',
|
||||
) as MatrixError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
|
||||
it("should resolve Matrix Errors from fetch with headers", () => {
|
||||
headers.set("Content-Type", "application/json");
|
||||
addHeaders(headers);
|
||||
const err = parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
) as MatrixError;
|
||||
compareHeaders(headers, err.httpHeaders);
|
||||
});
|
||||
});
|
||||
|
||||
it("should set a sensible default error message on MatrixError", () => {
|
||||
let err = new MatrixError();
|
||||
expect(err.message).toEqual("MatrixError: Unknown message");
|
||||
@@ -188,14 +256,11 @@ describe("parseErrorResponse", () => {
|
||||
});
|
||||
|
||||
it("should handle no type gracefully", () => {
|
||||
// No Content-Type header
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
@@ -203,31 +268,38 @@ describe("parseErrorResponse", () => {
|
||||
).toStrictEqual(new HTTPError("Server returned 500 error", 500));
|
||||
});
|
||||
|
||||
it("should handle invalid type gracefully", () => {
|
||||
it("should handle empty type gracefully", () => {
|
||||
headers.set("Content-Type", " ");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? " " : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new Error("Error parsing Content-Type ' ': TypeError: invalid media type"));
|
||||
).toStrictEqual(new Error("Error parsing Content-Type '': TypeError: argument string is required"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
it("should handle invalid type gracefully", () => {
|
||||
headers.set("Content-Type", "unknown");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers: {
|
||||
get(name: string): string | null {
|
||||
return name === "Content-Type" ? "text/plain" : null;
|
||||
},
|
||||
},
|
||||
headers,
|
||||
status: 500,
|
||||
} as Response,
|
||||
'{"errcode": "TEST"}',
|
||||
),
|
||||
).toStrictEqual(new Error("Error parsing Content-Type 'unknown': TypeError: invalid media type"));
|
||||
});
|
||||
|
||||
it("should handle plaintext errors", () => {
|
||||
headers.set("Content-Type", "text/plain");
|
||||
expect(
|
||||
parseErrorResponse(
|
||||
{
|
||||
headers,
|
||||
status: 418,
|
||||
} as Response,
|
||||
"I'm a teapot",
|
||||
|
||||
@@ -467,19 +467,109 @@ describe("MatrixRTCSession", () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("uses membershipExpiryTimeout from join config", async () => {
|
||||
const realSetTimeout = setTimeout;
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{
|
||||
memberships: [
|
||||
{
|
||||
application: "m.call",
|
||||
scope: "m.room",
|
||||
call_id: "",
|
||||
device_id: "AAAAAAA",
|
||||
expires: 60000,
|
||||
expires_ts: Date.now() + 60000,
|
||||
foci_active: [mockFocus],
|
||||
|
||||
membershipID: expect.stringMatching(".*"),
|
||||
},
|
||||
],
|
||||
},
|
||||
"@alice:example.org",
|
||||
);
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("non-legacy calls", () => {
|
||||
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
||||
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
|
||||
|
||||
async function testJoin(useOwnedStateEvents: boolean): Promise<void> {
|
||||
const realSetTimeout = setTimeout;
|
||||
if (useOwnedStateEvents) {
|
||||
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3779.default");
|
||||
mockRoom.getVersion = jest.fn().mockReturnValue("org.matrix.msc3757.default");
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
sess!.joinRoomSession([activeFocusConfig], activeFocus, { useLegacyMemberEvents: false });
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
|
||||
// preparing the delayed disconnect should handle the delay being too long
|
||||
const sendDelayedStateExceedAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError({
|
||||
"errcode": "M_UNKNOWN",
|
||||
"org.matrix.msc4140.errcode": "M_MAX_DELAY_EXCEEDED",
|
||||
"org.matrix.msc4140.max_delay": 7500,
|
||||
});
|
||||
sendDelayedStateMock.mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
// preparing the delayed disconnect should handle ratelimiting
|
||||
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
|
||||
sendDelayedStateMock.mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
// setting the membership state should handle ratelimiting (also with a retry-after value)
|
||||
const sendStateEventAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError(
|
||||
{ errcode: "M_LIMIT_EXCEEDED" },
|
||||
429,
|
||||
undefined,
|
||||
undefined,
|
||||
new Headers({ "Retry-After": "1" }),
|
||||
);
|
||||
sendStateEventMock.mockImplementationOnce(() => {
|
||||
resolve();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
// needed to advance the mock timers properly
|
||||
const scheduledDelayDisconnection = new Promise<void>((resolve) => {
|
||||
const originalFn: () => void = (sess as any).scheduleDelayDisconnection;
|
||||
(sess as any).scheduleDelayDisconnection = jest.fn(() => {
|
||||
originalFn.call(sess);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
|
||||
useLegacyMemberEvents: false,
|
||||
membershipServerSideExpiryTimeout: 9000,
|
||||
});
|
||||
|
||||
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000);
|
||||
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
|
||||
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500);
|
||||
|
||||
await sendDelayedStateAttempt;
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
await sentStateEvent;
|
||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||
mockRoom!.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
@@ -493,9 +583,10 @@ describe("MatrixRTCSession", () => {
|
||||
} satisfies SessionMembershipData,
|
||||
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
|
||||
);
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
await sentDelayedState;
|
||||
|
||||
// should have prepared the heartbeat to keep delaying the leave event while still connected
|
||||
await scheduledDelayDisconnection;
|
||||
// should have tried updating the delayed leave to test that it wasn't replaced by own state
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
// should update delayed disconnect
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("Beacon", () => {
|
||||
|
||||
const advanceDateAndTime = (ms: number) => {
|
||||
// bc liveness check uses Date.now we have to advance this mock
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(Date.now() + ms);
|
||||
// then advance time for the interval by the same amount
|
||||
jest.advanceTimersByTime(ms);
|
||||
};
|
||||
@@ -108,11 +108,11 @@ describe("Beacon", () => {
|
||||
);
|
||||
|
||||
// back to 'now'
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("creates beacon from event", () => {
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("oidc authorization", () => {
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
global.TextEncoder = TextEncoder;
|
||||
globalThis.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("Pushers", () => {
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
@@ -256,7 +256,7 @@ describe.each([[StoreType.Memory], [StoreType.IndexedDB]])("queueToDevice (%s st
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
store,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("Read receipt", () => {
|
||||
userId: "@user:server",
|
||||
baseUrl: "https://my.home.server",
|
||||
accessToken: "my.access.token",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
});
|
||||
client.isGuest = () => false;
|
||||
client.supportsThreads = () => true;
|
||||
|
||||
@@ -53,8 +53,8 @@ describe("realtime-callbacks", function () {
|
||||
|
||||
it("should set 'this' to the global object", function () {
|
||||
let passed = false;
|
||||
const callback = function (this: typeof global) {
|
||||
expect(this).toBe(global); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
const callback = function (this: typeof globalThis) {
|
||||
expect(this).toBe(globalThis); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @typescript-eslint/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ describe("OutgoingRequestProcessor", () => {
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
fetchFn: httpBackend.fetchFn as typeof globalThis.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ import fetchMock from "fetch-mock-jest";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi, TypedEventEmitter } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
import { CryptoEvent, KeyBackupSession } from "../../../src/crypto-api/index.ts";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import * as TestData from "../../test-utils/test-data";
|
||||
import { IKeyBackup } from "../../../src/crypto/backup";
|
||||
import { IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { RustBackupManager } from "../../../src/rust-crypto/backup";
|
||||
import { RustBackupManager, KeyBackup } from "../../../src/rust-crypto/backup";
|
||||
|
||||
describe("Upload keys to backup", () => {
|
||||
/** The backup manager under test */
|
||||
@@ -27,7 +25,7 @@ describe("Upload keys to backup", () => {
|
||||
|
||||
let idGenerator = 0;
|
||||
function mockBackupRequest(keyCount: number): RustSdkCryptoJs.KeysBackupRequest {
|
||||
const requestBody: IKeyBackup = {
|
||||
const requestBody: KeyBackup = {
|
||||
rooms: {
|
||||
"!room1:server": {
|
||||
sessions: {},
|
||||
@@ -35,7 +33,7 @@ describe("Upload keys to backup", () => {
|
||||
},
|
||||
};
|
||||
for (let i = 0; i < keyCount; i++) {
|
||||
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as IKeyBackupSession;
|
||||
requestBody.rooms["!room1:server"].sessions["session" + i] = {} as KeyBackupSession;
|
||||
}
|
||||
return {
|
||||
id: "id" + idGenerator++,
|
||||
|
||||
@@ -794,7 +794,7 @@ describe("SyncAccumulator", function () {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
jest.spyOn(globalThis.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("should copy summary properties", function () {
|
||||
@@ -851,11 +851,11 @@ describe("SyncAccumulator", function () {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(startingTs);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs);
|
||||
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(startingTs + delta);
|
||||
jest.spyOn(globalThis.Date, "now").mockReturnValue(startingTs + delta);
|
||||
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual(
|
||||
|
||||
@@ -119,9 +119,9 @@ describe("Call", function () {
|
||||
const errorListener = () => {};
|
||||
|
||||
beforeEach(function () {
|
||||
prevNavigator = global.navigator;
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
prevNavigator = globalThis.navigator;
|
||||
prevDocument = globalThis.document;
|
||||
prevWindow = globalThis.window;
|
||||
|
||||
installWebRTCMocks();
|
||||
|
||||
@@ -159,9 +159,9 @@ describe("Call", function () {
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
|
||||
client.stop();
|
||||
global.navigator = prevNavigator;
|
||||
global.window = prevWindow;
|
||||
global.document = prevDocument;
|
||||
globalThis.navigator = prevNavigator;
|
||||
globalThis.window = prevWindow;
|
||||
globalThis.document = prevDocument;
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
@@ -788,17 +788,17 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
it("should return false if window or document are undefined", () => {
|
||||
global.window = undefined!;
|
||||
globalThis.window = undefined!;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
global.window = prevWindow;
|
||||
global.document = undefined!;
|
||||
globalThis.window = prevWindow;
|
||||
globalThis.document = undefined!;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if RTCPeerConnection throws", () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
globalThis.window = {};
|
||||
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
@@ -810,11 +810,11 @@ describe("Call", function () {
|
||||
"should return false if RTCPeerConnection & RTCSessionDescription " +
|
||||
"& RTCIceCandidate & mediaDevices are unavailable",
|
||||
() => {
|
||||
global.window.RTCPeerConnection = undefined!;
|
||||
global.window.RTCSessionDescription = undefined!;
|
||||
global.window.RTCIceCandidate = undefined!;
|
||||
globalThis.window.RTCPeerConnection = undefined!;
|
||||
globalThis.window.RTCSessionDescription = undefined!;
|
||||
globalThis.window.RTCIceCandidate = undefined!;
|
||||
// @ts-ignore - writing to a read-only property as we are simulating faulty browsers
|
||||
global.navigator.mediaDevices = undefined;
|
||||
globalThis.navigator.mediaDevices = undefined;
|
||||
expect(supportsMatrixCall()).toBe(false);
|
||||
},
|
||||
);
|
||||
@@ -1305,7 +1305,7 @@ describe("Call", function () {
|
||||
});
|
||||
|
||||
it("removes RTX codec from screen sharing transcievers", async () => {
|
||||
mocked(global.RTCRtpSender.getCapabilities).mockReturnValue({
|
||||
mocked(globalThis.RTCRtpSender.getCapabilities).mockReturnValue({
|
||||
codecs: [
|
||||
{ mimeType: "video/rtx", clockRate: 90000 },
|
||||
{ mimeType: "video/somethingelse", clockRate: 90000 },
|
||||
@@ -1816,8 +1816,8 @@ describe("Call", function () {
|
||||
|
||||
it("should emit IceFailed error on the successor call if RTCPeerConnection throws", async () => {
|
||||
// @ts-ignore - writing to window as we are simulating browser edge-cases
|
||||
global.window = {};
|
||||
Object.defineProperty(global.window, "RTCPeerConnection", {
|
||||
globalThis.window = {};
|
||||
Object.defineProperty(globalThis.window, "RTCPeerConnection", {
|
||||
get: () => {
|
||||
throw Error("Secure mode, naaah!");
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("Media Handler", function () {
|
||||
beforeEach(() => {
|
||||
mockMediaDevices = new MockMediaDevices();
|
||||
|
||||
global.navigator = {
|
||||
globalThis.navigator = {
|
||||
mediaDevices: mockMediaDevices.typed(),
|
||||
} as unknown as Navigator;
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ describe("GroupCallStats", () => {
|
||||
|
||||
it("doing nothing if process already running", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.start();
|
||||
@@ -146,8 +146,8 @@ describe("GroupCallStats", () => {
|
||||
});
|
||||
it("finish stats process if was started", async () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(global, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(global, "clearInterval");
|
||||
jest.spyOn(globalThis, "setInterval").mockReturnValue(22);
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
stats.start();
|
||||
expect(setInterval).toHaveBeenCalledTimes(1);
|
||||
stats.stop();
|
||||
@@ -155,7 +155,7 @@ describe("GroupCallStats", () => {
|
||||
});
|
||||
|
||||
it("do nothing if stats process was not started", async () => {
|
||||
jest.spyOn(global, "clearInterval");
|
||||
jest.spyOn(globalThis, "clearInterval");
|
||||
stats.stop();
|
||||
expect(clearInterval).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Vendored
+2
-2
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
// this is needed to tell TS about globalThis.Olm
|
||||
import "@matrix-org/olm";
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
// use `number` as the return type in all cases for global.set{Interval,Timeout},
|
||||
// use `number` as the return type in all cases for globalThis.set{Interval,Timeout},
|
||||
// so we don't accidentally use the methods on NodeJS.Timeout - they only exist in a subset of environments.
|
||||
// The overload for clear{Interval,Timeout} is resolved as expected.
|
||||
// We use `ReturnType<typeof setTimeout>` in the code to be agnostic of if this definition gets loaded.
|
||||
|
||||
@@ -414,16 +414,16 @@ export class AutoDiscovery {
|
||||
}
|
||||
}
|
||||
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
private static fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
return globalThis.fetch(resource, options);
|
||||
}
|
||||
|
||||
private static fetchFn?: typeof global.fetch;
|
||||
private static fetchFn?: typeof globalThis.fetch;
|
||||
|
||||
public static setFetchFn(fetchFn: typeof global.fetch): void {
|
||||
public static setFetchFn(fetchFn: typeof globalThis.fetch): void {
|
||||
AutoDiscovery.fetchFn = fetchFn;
|
||||
}
|
||||
|
||||
|
||||
+21
-16
@@ -307,7 +307,7 @@ export interface ICreateClientOpts {
|
||||
* The function to invoke for HTTP requests.
|
||||
* Most supported environments have a global `fetch` registered to which this will fall back.
|
||||
*/
|
||||
fetchFn?: typeof global.fetch;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
|
||||
userId?: string;
|
||||
|
||||
@@ -1581,11 +1581,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.callEventHandler = undefined;
|
||||
this.groupCallEventHandler = undefined;
|
||||
|
||||
global.clearInterval(this.checkTurnServersIntervalID);
|
||||
globalThis.clearInterval(this.checkTurnServersIntervalID);
|
||||
this.checkTurnServersIntervalID = undefined;
|
||||
|
||||
if (this.clientWellKnownIntervalID !== undefined) {
|
||||
global.clearInterval(this.clientWellKnownIntervalID);
|
||||
globalThis.clearInterval(this.clientWellKnownIntervalID);
|
||||
}
|
||||
|
||||
this.toDeviceMessageQueue.stop();
|
||||
@@ -1625,7 +1625,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return;
|
||||
}
|
||||
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
const deviceData = getDeviceResult.device_data;
|
||||
if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
|
||||
@@ -1781,7 +1781,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const deleteRustSdkStore = async (): Promise<void> => {
|
||||
let indexedDB: IDBFactory;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
indexedDB = globalThis.indexedDB;
|
||||
if (!indexedDB) return; // No indexedDB support
|
||||
} catch {
|
||||
// No indexedDB support
|
||||
@@ -1799,7 +1799,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
resolve(0);
|
||||
};
|
||||
req.onerror = (e): void => {
|
||||
// In private browsing, Firefox has a global.indexedDB, but attempts to delete an indexeddb
|
||||
// In private browsing, Firefox has a globalThis.indexedDB, but attempts to delete an indexeddb
|
||||
// (even a non-existent one) fail with "DOMException: A mutation operation was attempted on a
|
||||
// database that did not allow mutations."
|
||||
//
|
||||
@@ -3351,6 +3351,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* uploaded a key backup version using an algorithm I don't understand.
|
||||
*
|
||||
* @returns Information object from API, or null if no backup is present on the server.
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.getActiveSessionBackupVersion}.
|
||||
*/
|
||||
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
|
||||
let res: IKeyBackupInfo;
|
||||
@@ -5796,16 +5798,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Promise which resolves: `{}` an empty object.
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public forget(roomId: string, deleteRoom = true): Promise<{}> {
|
||||
const promise = this.membershipChange(roomId, undefined, "forget");
|
||||
if (!deleteRoom) {
|
||||
return promise;
|
||||
}
|
||||
return promise.then((response) => {
|
||||
public async forget(roomId: string, deleteRoom = true): Promise<{}> {
|
||||
// API returns an empty object
|
||||
const path = utils.encodeUri("/rooms/$room_id/forget", {
|
||||
$room_id: roomId,
|
||||
});
|
||||
const response = await this.http.authedRequest<{}>(Method.Post, path);
|
||||
if (deleteRoom) {
|
||||
this.store.removeRoom(roomId);
|
||||
this.emit(ClientEvent.DeleteRoom, roomId);
|
||||
return response;
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -5846,7 +5849,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
private membershipChange(
|
||||
roomId: string,
|
||||
userId: string | undefined,
|
||||
membership: Membership | "forget",
|
||||
membership: Membership,
|
||||
reason?: string,
|
||||
): Promise<{}> {
|
||||
// API returns an empty object
|
||||
@@ -7548,7 +7551,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
if ((<HTTPError>err).httpStatus === 403) {
|
||||
// We got a 403, so there's no point in looping forever.
|
||||
this.logger.info("TURN access unavailable for this account: stopping credentials checks");
|
||||
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
|
||||
if (this.checkTurnServersIntervalID !== null) {
|
||||
globalThis.clearInterval(this.checkTurnServersIntervalID);
|
||||
}
|
||||
this.checkTurnServersIntervalID = undefined;
|
||||
this.emit(ClientEvent.TurnServersError, <HTTPError>err, true); // fatal
|
||||
} else {
|
||||
|
||||
+11
-1
@@ -274,7 +274,13 @@ export interface CryptoApi {
|
||||
isCrossSigningReady(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the ID of one of the user's cross-signing keys.
|
||||
* Get the ID of one of the user's cross-signing keys, if both private and matching
|
||||
* public parts of that key are available (ie. cached in the local crypto store).
|
||||
*
|
||||
* The public part may not be available if a `/keys/query` request has not yet been
|
||||
* performed, or if the device that created the keys failed to publish them.
|
||||
*
|
||||
* If either part of the keypair is not available, this will return `null`.
|
||||
*
|
||||
* @param type - The type of key to get the ID of. One of `CrossSigningKey.Master`, `CrossSigningKey.SelfSigning`,
|
||||
* or `CrossSigningKey.UserSigning`. Defaults to `CrossSigningKey.Master`.
|
||||
@@ -930,8 +936,11 @@ export interface CrossSigningStatus {
|
||||
* Crypto callbacks provided by the application
|
||||
*/
|
||||
export interface CryptoCallbacks extends SecretStorageCallbacks {
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
getCrossSigningKey?: (keyType: string, pubKey: string) => Promise<Uint8Array | null>;
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
shouldUpgradeDeviceVerifications?: (users: Record<string, any>) => Promise<string[]>;
|
||||
/**
|
||||
* Called by {@link CryptoApi#bootstrapSecretStorage}
|
||||
@@ -956,6 +965,7 @@ export interface CryptoCallbacks extends SecretStorageCallbacks {
|
||||
checkFunc: (key: Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
|
||||
/** @deprecated: unused with the Rust crypto stack. */
|
||||
getBackupKey?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ export class CrossSigningInfo {
|
||||
|
||||
function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined {
|
||||
if (!key) return;
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signing = new globalThis.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(key);
|
||||
if (gotPubkey === expectedPubkey) {
|
||||
return [gotPubkey, signing];
|
||||
@@ -307,7 +307,7 @@ export class CrossSigningInfo {
|
||||
|
||||
try {
|
||||
if (level & CrossSigningLevel.MASTER) {
|
||||
masterSigning = new global.Olm.PkSigning();
|
||||
masterSigning = new globalThis.Olm.PkSigning();
|
||||
privateKeys.master = masterSigning.generate_seed();
|
||||
masterPub = masterSigning.init_with_seed(privateKeys.master);
|
||||
keys.master = {
|
||||
@@ -322,7 +322,7 @@ export class CrossSigningInfo {
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.SELF_SIGNING) {
|
||||
const sskSigning = new global.Olm.PkSigning();
|
||||
const sskSigning = new globalThis.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.self_signing = sskSigning.generate_seed();
|
||||
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
|
||||
@@ -340,7 +340,7 @@ export class CrossSigningInfo {
|
||||
}
|
||||
|
||||
if (level & CrossSigningLevel.USER_SIGNING) {
|
||||
const uskSigning = new global.Olm.PkSigning();
|
||||
const uskSigning = new globalThis.Olm.PkSigning();
|
||||
try {
|
||||
privateKeys.user_signing = uskSigning.generate_seed();
|
||||
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
|
||||
|
||||
+11
-11
@@ -167,7 +167,7 @@ export class OlmDevice {
|
||||
* @returns The version of Olm.
|
||||
*/
|
||||
public static getOlmVersion(): [number, number, number] {
|
||||
return global.Olm.get_library_version();
|
||||
return globalThis.Olm.get_library_version();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +186,7 @@ export class OlmDevice {
|
||||
*/
|
||||
public async init({ pickleKey, fromExportedDevice }: IInitOpts = {}): Promise<void> {
|
||||
let e2eKeys;
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
|
||||
try {
|
||||
if (fromExportedDevice) {
|
||||
@@ -268,7 +268,7 @@ export class OlmDevice {
|
||||
*/
|
||||
private getAccount(txn: unknown, func: (account: Account) => void): void {
|
||||
this.cryptoStore.getAccount(txn, (pickledAccount: string | null) => {
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
try {
|
||||
account.unpickle(this.pickleKey, pickledAccount!);
|
||||
func(account);
|
||||
@@ -352,7 +352,7 @@ export class OlmDevice {
|
||||
sessionInfo: ISessionInfo,
|
||||
func: (unpickledSessionInfo: IUnpickledSessionInfo) => void,
|
||||
): void {
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
try {
|
||||
session.unpickle(this.pickleKey, sessionInfo.session!);
|
||||
const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session });
|
||||
@@ -390,7 +390,7 @@ export class OlmDevice {
|
||||
* @internal
|
||||
*/
|
||||
private getUtility<T>(func: (utility: Utility) => T): T {
|
||||
const utility = new global.Olm.Utility();
|
||||
const utility = new globalThis.Olm.Utility();
|
||||
try {
|
||||
return func(utility);
|
||||
} finally {
|
||||
@@ -517,7 +517,7 @@ export class OlmDevice {
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this.getAccount(txn, (account: Account) => {
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
try {
|
||||
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
|
||||
newSessionId = session.session_id();
|
||||
@@ -567,7 +567,7 @@ export class OlmDevice {
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT, IndexedDBCryptoStore.STORE_SESSIONS],
|
||||
(txn) => {
|
||||
this.getAccount(txn, (account: Account) => {
|
||||
const session = new global.Olm.Session();
|
||||
const session = new globalThis.Olm.Session();
|
||||
try {
|
||||
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
|
||||
account.remove_one_time_keys(session);
|
||||
@@ -889,7 +889,7 @@ export class OlmDevice {
|
||||
throw new Error("Unknown outbound group session " + sessionId);
|
||||
}
|
||||
|
||||
const session = new global.Olm.OutboundGroupSession();
|
||||
const session = new globalThis.Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this.pickleKey, pickled);
|
||||
return func(session);
|
||||
@@ -904,7 +904,7 @@ export class OlmDevice {
|
||||
* @returns sessionId for the outbound session.
|
||||
*/
|
||||
public createOutboundGroupSession(): string {
|
||||
const session = new global.Olm.OutboundGroupSession();
|
||||
const session = new globalThis.Olm.OutboundGroupSession();
|
||||
try {
|
||||
session.create();
|
||||
this.saveOutboundGroupSession(session);
|
||||
@@ -966,7 +966,7 @@ export class OlmDevice {
|
||||
sessionData: InboundGroupSessionData,
|
||||
func: (session: InboundGroupSession) => T,
|
||||
): T {
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
const session = new globalThis.Olm.InboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this.pickleKey, sessionData.session);
|
||||
return func(session);
|
||||
@@ -1068,7 +1068,7 @@ export class OlmDevice {
|
||||
existingSessionData: InboundGroupSessionData | null,
|
||||
) => {
|
||||
// new session.
|
||||
const session = new global.Olm.InboundGroupSession();
|
||||
const session = new globalThis.Olm.InboundGroupSession();
|
||||
try {
|
||||
if (exportFormat) {
|
||||
session.import_session(sessionKey);
|
||||
|
||||
@@ -666,13 +666,13 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
if (!authData || !("public_key" in authData)) {
|
||||
throw new Error("auth_data missing required information");
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
const publicKey = new globalThis.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
return new Curve25519(authData as ICurve25519AuthData, publicKey, getKey);
|
||||
}
|
||||
|
||||
public static async prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: Partial<ICurve25519AuthData> = {};
|
||||
if (!key) {
|
||||
@@ -685,7 +685,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
authData.private_key_iterations = derivation.iterations;
|
||||
authData.public_key = decryption.init_with_private_key(derivation.key);
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
const publicKey = new globalThis.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
|
||||
return [decryption.get_private_key(), authData as AuthData];
|
||||
@@ -716,7 +716,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
sessions: Record<string, IKeyBackupSession<Curve25519SessionData>>,
|
||||
): Promise<IMegolmSessionData[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
try {
|
||||
const backupPubKey = decryption.init_with_private_key(privKey);
|
||||
|
||||
@@ -748,7 +748,7 @@ export class Curve25519 implements BackupAlgorithm {
|
||||
}
|
||||
|
||||
public async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
let pubKey: string;
|
||||
try {
|
||||
pubKey = decryption.init_with_private_key(key);
|
||||
|
||||
@@ -68,7 +68,7 @@ export class DehydrationManager {
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
const now = Date.now();
|
||||
const delay = Math.max(1, time + oneweek - now);
|
||||
this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), delay);
|
||||
this.timeoutId = globalThis.setTimeout(this.dehydrateDevice.bind(this), delay);
|
||||
}
|
||||
},
|
||||
"dehydration",
|
||||
@@ -97,7 +97,7 @@ export class DehydrationManager {
|
||||
if (!key) {
|
||||
// unsetting the key -- cancel any pending dehydration task
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
globalThis.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
// clear storage
|
||||
@@ -135,7 +135,7 @@ export class DehydrationManager {
|
||||
}
|
||||
this.inProgress = true;
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
globalThis.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
@@ -155,7 +155,7 @@ export class DehydrationManager {
|
||||
|
||||
logger.log("Creating account");
|
||||
// create the account and all the necessary keys
|
||||
const account = new global.Olm.Account();
|
||||
const account = new globalThis.Olm.Account();
|
||||
account.create();
|
||||
const e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
@@ -255,7 +255,7 @@ export class DehydrationManager {
|
||||
logger.log("Done dehydrating");
|
||||
|
||||
// dehydrate again in a week
|
||||
this.timeoutId = global.setTimeout(this.dehydrateDevice.bind(this), oneweek);
|
||||
this.timeoutId = globalThis.setTimeout(this.dehydrateDevice.bind(this), oneweek);
|
||||
|
||||
return deviceId;
|
||||
} finally {
|
||||
@@ -265,7 +265,7 @@ export class DehydrationManager {
|
||||
|
||||
public stop(): void {
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
globalThis.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -537,7 +537,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
*/
|
||||
public async init({ exportedOlmDevice, pickleKey }: IInitOpts = {}): Promise<void> {
|
||||
logger.log("Crypto: initialising Olm...");
|
||||
await global.Olm.init();
|
||||
await globalThis.Olm.init();
|
||||
logger.log(
|
||||
exportedOlmDevice
|
||||
? "Crypto: initialising Olm device from exported device..."
|
||||
@@ -668,7 +668,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
public async createRecoveryKeyFromPassphrase(password?: string): Promise<IRecoveryKey> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const decryption = new globalThis.Olm.PkDecryption();
|
||||
try {
|
||||
if (password) {
|
||||
const derivation = await keyFromPassphrase(password);
|
||||
@@ -1252,7 +1252,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public checkSecretStoragePrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
|
||||
let decryption: PkDecryption | null = null;
|
||||
try {
|
||||
decryption = new global.Olm.PkDecryption();
|
||||
decryption = new globalThis.Olm.PkDecryption();
|
||||
const gotPubkey = decryption.init_with_private_key(privateKey);
|
||||
// make sure it agrees with the given pubkey
|
||||
return gotPubkey === expectedPublicKey;
|
||||
@@ -1354,7 +1354,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
public checkCrossSigningPrivateKey(privateKey: Uint8Array, expectedPublicKey: string): boolean {
|
||||
let signing: PkSigning | null = null;
|
||||
try {
|
||||
signing = new global.Olm.PkSigning();
|
||||
signing = new globalThis.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(privateKey);
|
||||
// make sure it agrees with the given pubkey
|
||||
return gotPubkey === expectedPublicKey;
|
||||
|
||||
@@ -471,7 +471,7 @@ export async function verifySignature(
|
||||
export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
|
||||
let createdKey = false;
|
||||
if (key instanceof Uint8Array) {
|
||||
const keyObj = new global.Olm.PkSigning();
|
||||
const keyObj = new globalThis.Olm.PkSigning();
|
||||
pubKey = keyObj.init_with_seed(key);
|
||||
key = keyObj;
|
||||
createdKey = true;
|
||||
@@ -506,7 +506,7 @@ export function pkVerify(obj: IObject, pubKey: string, userId: string): void {
|
||||
throw new Error("No signature");
|
||||
}
|
||||
const signature = obj.signatures[userId][keyId];
|
||||
const util = new global.Olm.Utility();
|
||||
const util = new globalThis.Olm.Utility();
|
||||
const sigs = obj.signatures;
|
||||
delete obj.signatures;
|
||||
const unsigned = obj.unsigned;
|
||||
|
||||
@@ -210,7 +210,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
|
||||
);
|
||||
|
||||
try {
|
||||
return new LocalStorageCryptoStore(global.localStorage);
|
||||
return new LocalStorageCryptoStore(globalThis.localStorage);
|
||||
} catch (e) {
|
||||
logger.warn(`unable to open localStorage: falling back to in-memory store: ${e}`);
|
||||
return new MemoryCryptoStore();
|
||||
|
||||
@@ -242,8 +242,8 @@ export class SAS extends Base {
|
||||
}
|
||||
|
||||
protected doVerification = async (): Promise<void> => {
|
||||
await global.Olm.init();
|
||||
olmutil = olmutil || new global.Olm.Utility();
|
||||
await globalThis.Olm.init();
|
||||
olmutil = olmutil || new globalThis.Olm.Utility();
|
||||
|
||||
// make sure user's keys are downloaded
|
||||
await this.baseApis.downloadKeys([this.userId]);
|
||||
@@ -369,7 +369,7 @@ export class SAS extends Base {
|
||||
const keyAgreement = content.key_agreement_protocol;
|
||||
const macMethod = content.message_authentication_code;
|
||||
const hashCommitment = content.commitment;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
try {
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this.send(EventType.KeyVerificationKey, {
|
||||
@@ -411,7 +411,7 @@ export class SAS extends Base {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
const olmSAS = new globalThis.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
await this.send(EventType.KeyVerificationAccept, {
|
||||
|
||||
@@ -17,12 +17,17 @@ limitations under the License.
|
||||
import {
|
||||
WidgetApi,
|
||||
WidgetApiToWidgetAction,
|
||||
WidgetApiResponseError,
|
||||
MatrixCapabilities,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiAcknowledgeResponseData,
|
||||
ISendEventToWidgetActionRequest,
|
||||
ISendToDeviceToWidgetActionRequest,
|
||||
ISendEventFromWidgetResponseData,
|
||||
IWidgetApiRequestData,
|
||||
WidgetApiAction,
|
||||
IWidgetApiResponse,
|
||||
IWidgetApiResponseData,
|
||||
} from "matrix-widget-api";
|
||||
|
||||
import { MatrixEvent, IEvent, IContent, EventStatus } from "./models/event.ts";
|
||||
@@ -45,6 +50,7 @@ import {
|
||||
} from "./client.ts";
|
||||
import { SyncApi, SyncState } from "./sync.ts";
|
||||
import { SlidingSyncSdk } from "./sliding-sync-sdk.ts";
|
||||
import { MatrixError } from "./http-api/errors.ts";
|
||||
import { User } from "./models/user.ts";
|
||||
import { Room } from "./models/room.ts";
|
||||
import { ToDeviceBatch, ToDevicePayload } from "./models/ToDeviceMessage.ts";
|
||||
@@ -147,6 +153,33 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
) {
|
||||
super(opts);
|
||||
|
||||
const transportSend = this.widgetApi.transport.send.bind(this.widgetApi.transport);
|
||||
this.widgetApi.transport.send = async <
|
||||
T extends IWidgetApiRequestData,
|
||||
R extends IWidgetApiResponseData = IWidgetApiAcknowledgeResponseData,
|
||||
>(
|
||||
action: WidgetApiAction,
|
||||
data: T,
|
||||
): Promise<R> => {
|
||||
try {
|
||||
return await transportSend(action, data);
|
||||
} catch (error) {
|
||||
processAndThrow(error);
|
||||
}
|
||||
};
|
||||
|
||||
const transportSendComplete = this.widgetApi.transport.sendComplete.bind(this.widgetApi.transport);
|
||||
this.widgetApi.transport.sendComplete = async <T extends IWidgetApiRequestData, R extends IWidgetApiResponse>(
|
||||
action: WidgetApiAction,
|
||||
data: T,
|
||||
): Promise<R> => {
|
||||
try {
|
||||
return await transportSendComplete(action, data);
|
||||
} catch (error) {
|
||||
processAndThrow(error);
|
||||
}
|
||||
};
|
||||
|
||||
this.widgetApiReady = new Promise<void>((resolve) => this.widgetApi.once("ready", resolve));
|
||||
|
||||
// Request capabilities for the functionality this client needs to support
|
||||
@@ -420,6 +453,27 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
await this.widgetApi.sendToDevice((payload as { type: string }).type, true, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to a specific list of devices via the widget API. Optionally encrypts the event.
|
||||
*
|
||||
* If you are using a full MatrixClient you would be calling {@link MatrixClient.getCrypto().encryptToDeviceMessages()} followed
|
||||
* by {@link MatrixClient.queueToDevice}.
|
||||
*
|
||||
* However, this is combined into a single step when running as an embedded widget client. So, we expose this method for those
|
||||
* that need it.
|
||||
*
|
||||
* @param eventType - Type of the event to send.
|
||||
* @param encrypted - Whether the event should be encrypted.
|
||||
* @param contentMap - The content to send. Map from user_id to device_id to content object.
|
||||
*/
|
||||
public async sendToDeviceViaWidgetApi(
|
||||
eventType: string,
|
||||
encrypted: boolean,
|
||||
contentMap: SendToDeviceContentMap,
|
||||
): Promise<void> {
|
||||
await this.widgetApi.sendToDevice(eventType, encrypted, recursiveMapToObject(contentMap));
|
||||
}
|
||||
|
||||
// Overridden since we get TURN servers automatically over the widget API,
|
||||
// and this method would otherwise complain about missing an access token
|
||||
public async checkTurnServers(): Promise<boolean> {
|
||||
@@ -502,3 +556,11 @@ export class RoomWidgetClient extends MatrixClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processAndThrow(error: unknown): never {
|
||||
if (error instanceof WidgetApiResponseError && error.data.matrix_api_error) {
|
||||
throw MatrixError.fromWidgetApiErrorData(error.data.matrix_api_error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+115
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IMatrixApiError as IWidgetMatrixError } from "matrix-widget-api";
|
||||
|
||||
import { IUsageLimit } from "../@types/partials.ts";
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
|
||||
@@ -28,14 +30,53 @@ interface IErrorJson extends Partial<IUsageLimit> {
|
||||
* specific to HTTP responses.
|
||||
* @param msg - The error message to include.
|
||||
* @param httpStatus - The HTTP response status code.
|
||||
* @param httpHeaders - The HTTP response headers.
|
||||
*/
|
||||
export class HTTPError extends Error {
|
||||
public constructor(
|
||||
msg: string,
|
||||
public readonly httpStatus?: number,
|
||||
public readonly httpHeaders?: Headers,
|
||||
) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this error was due to rate-limiting on the server side (and should therefore be retried after a delay).
|
||||
*
|
||||
* If this returns `true`, {@link getRetryAfterMs} can be called to retrieve the server-side
|
||||
* recommendation for the retry period.
|
||||
*
|
||||
* @returns Whether this error is due to rate-limiting.
|
||||
*/
|
||||
public isRateLimitError(): boolean {
|
||||
return this.httpStatus === 429;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The recommended delay in milliseconds to wait before retrying
|
||||
* the request that triggered this error, or null if no delay is recommended.
|
||||
* @throws Error if the recommended delay is an invalid value.
|
||||
* @see {@link safeGetRetryAfterMs} for a version of this check that doesn't throw.
|
||||
*/
|
||||
public getRetryAfterMs(): number | null {
|
||||
const retryAfter = this.httpHeaders?.get("Retry-After");
|
||||
if (retryAfter != null) {
|
||||
if (/^\d+$/.test(retryAfter)) {
|
||||
const ms = Number.parseInt(retryAfter) * 1000;
|
||||
if (!Number.isFinite(ms)) {
|
||||
throw new Error("Retry-After header integer value is too large");
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
const date = new Date(retryAfter);
|
||||
if (date.toUTCString() !== retryAfter) {
|
||||
throw new Error("Retry-After header value is not a valid HTTP-date or non-negative decimal integer");
|
||||
}
|
||||
return date.getTime() - Date.now();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MatrixError extends HTTPError {
|
||||
@@ -49,12 +90,14 @@ export class MatrixError extends HTTPError {
|
||||
* information specific to the standard Matrix error response.
|
||||
* @param errorJson - The Matrix error JSON returned from the homeserver.
|
||||
* @param httpStatus - The numeric HTTP status code given
|
||||
* @param httpHeaders - The HTTP response headers given
|
||||
*/
|
||||
public constructor(
|
||||
errorJson: IErrorJson = {},
|
||||
public readonly httpStatus?: number,
|
||||
httpStatus?: number,
|
||||
public url?: string,
|
||||
public event?: MatrixEvent,
|
||||
httpHeaders?: Headers,
|
||||
) {
|
||||
let message = errorJson.error || "Unknown message";
|
||||
if (httpStatus) {
|
||||
@@ -63,11 +106,80 @@ export class MatrixError extends HTTPError {
|
||||
if (url) {
|
||||
message = `${message} (${url})`;
|
||||
}
|
||||
super(`MatrixError: ${message}`, httpStatus);
|
||||
super(`MatrixError: ${message}`, httpStatus, httpHeaders);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.data = errorJson;
|
||||
}
|
||||
|
||||
public isRateLimitError(): boolean {
|
||||
return (
|
||||
this.errcode === "M_LIMIT_EXCEEDED" ||
|
||||
((this.errcode === "M_UNKNOWN" || this.errcode === undefined) && super.isRateLimitError())
|
||||
);
|
||||
}
|
||||
|
||||
public getRetryAfterMs(): number | null {
|
||||
const headerValue = super.getRetryAfterMs();
|
||||
if (headerValue !== null) {
|
||||
return headerValue;
|
||||
}
|
||||
// Note: retry_after_ms is deprecated as of spec version v1.10
|
||||
if (this.errcode === "M_LIMIT_EXCEEDED" && "retry_after_ms" in this.data) {
|
||||
if (!Number.isInteger(this.data.retry_after_ms)) {
|
||||
throw new Error("retry_after_ms is not an integer");
|
||||
}
|
||||
return this.data.retry_after_ms;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns this error expressed as a JSON payload
|
||||
* for use by Widget API error responses.
|
||||
*/
|
||||
public asWidgetApiErrorData(): IWidgetMatrixError {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.httpHeaders) {
|
||||
for (const [name, value] of this.httpHeaders) {
|
||||
headers[name] = value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
http_status: this.httpStatus ?? 400,
|
||||
http_headers: headers,
|
||||
url: this.url ?? "",
|
||||
response: {
|
||||
errcode: this.errcode ?? "M_UNKNOWN",
|
||||
error: this.data.error ?? "Unknown message",
|
||||
...this.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a new {@link MatrixError} from a JSON payload
|
||||
* received from Widget API error responses.
|
||||
*/
|
||||
public static fromWidgetApiErrorData(data: IWidgetMatrixError): MatrixError {
|
||||
return new MatrixError(data.response, data.http_status, data.url, undefined, new Headers(data.http_headers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The recommended delay in milliseconds to wait before retrying
|
||||
* the request that triggered {@link error}, or {@link defaultMs} if the
|
||||
* error was not due to rate-limiting or if no valid delay is recommended.
|
||||
*/
|
||||
export function safeGetRetryAfterMs(error: unknown, defaultMs: number): number {
|
||||
if (!(error instanceof HTTPError) || !error.isRateLimitError()) {
|
||||
return defaultMs;
|
||||
}
|
||||
try {
|
||||
return error.getRetryAfterMs() ?? defaultMs;
|
||||
} catch {
|
||||
return defaultMs;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -53,11 +53,11 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
public fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
if (this.opts.fetchFn) {
|
||||
return this.opts.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
return globalThis.fetch(resource, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,8 +60,8 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
} as Upload;
|
||||
const deferred = defer<UploadResponse>();
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
const xhr = new global.XMLHttpRequest();
|
||||
if (globalThis.XMLHttpRequest) {
|
||||
const xhr = new globalThis.XMLHttpRequest();
|
||||
|
||||
const timeoutFn = function (): void {
|
||||
xhr.abort();
|
||||
@@ -73,7 +73,7 @@ export class MatrixHttpApi<O extends IHttpOpts> extends FetchHttpApi<O> {
|
||||
|
||||
xhr.onreadystatechange = function (): void {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
case globalThis.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(timeoutTimer);
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export type AccessTokens = {
|
||||
*/
|
||||
export type TokenRefreshFunction = (refreshToken: string) => Promise<AccessTokens>;
|
||||
export interface IHttpOpts {
|
||||
fetchFn?: typeof global.fetch;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
|
||||
+25
-24
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,7 @@ import { parse as parseContentType, ParsedMediaType } from "content-type";
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { ConnectionError, HTTPError, MatrixError } from "./errors.ts";
|
||||
import { ConnectionError, HTTPError, MatrixError, safeGetRetryAfterMs } from "./errors.ts";
|
||||
|
||||
// Ponyfill for https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
|
||||
export function timeoutSignal(ms: number): AbortSignal {
|
||||
@@ -72,24 +72,38 @@ export function anySignal(signals: AbortSignal[]): {
|
||||
* @returns
|
||||
*/
|
||||
export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error {
|
||||
const httpHeaders = isXhr(response)
|
||||
? new Headers(
|
||||
response
|
||||
.getAllResponseHeaders()
|
||||
.trim()
|
||||
.split(/[\r\n]+/)
|
||||
.map((header): [string, string] => {
|
||||
const colonIdx = header.indexOf(":");
|
||||
return [header.substring(0, colonIdx), header.substring(colonIdx + 1)];
|
||||
}),
|
||||
)
|
||||
: response.headers;
|
||||
|
||||
let contentType: ParsedMediaType | null;
|
||||
try {
|
||||
contentType = getResponseContentType(response);
|
||||
contentType = getResponseContentType(httpHeaders);
|
||||
} catch (e) {
|
||||
return <Error>e;
|
||||
}
|
||||
|
||||
if (contentType?.type === "application/json" && body) {
|
||||
return new MatrixError(
|
||||
JSON.parse(body),
|
||||
response.status,
|
||||
isXhr(response) ? response.responseURL : response.url,
|
||||
undefined,
|
||||
httpHeaders,
|
||||
);
|
||||
}
|
||||
if (contentType?.type === "text/plain") {
|
||||
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status);
|
||||
return new HTTPError(`Server returned ${response.status} error: ${body}`, response.status, httpHeaders);
|
||||
}
|
||||
return new HTTPError(`Server returned ${response.status} error`, response.status);
|
||||
return new HTTPError(`Server returned ${response.status} error`, response.status, httpHeaders);
|
||||
}
|
||||
|
||||
function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest {
|
||||
@@ -97,7 +111,7 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* extract the Content-Type header from the response object, and
|
||||
* extract the Content-Type header from response headers, and
|
||||
* parse it to a `{type, parameters}` object.
|
||||
*
|
||||
* returns null if no content-type header could be found.
|
||||
@@ -105,15 +119,9 @@ function isXhr(response: XMLHttpRequest | Response): response is XMLHttpRequest
|
||||
* @param response - response object
|
||||
* @returns parsed content-type header, or null if not found
|
||||
*/
|
||||
function getResponseContentType(response: XMLHttpRequest | Response): ParsedMediaType | null {
|
||||
let contentType: string | null;
|
||||
if (isXhr(response)) {
|
||||
contentType = response.getResponseHeader("Content-Type");
|
||||
} else {
|
||||
contentType = response.headers.get("Content-Type");
|
||||
}
|
||||
|
||||
if (!contentType) return null;
|
||||
function getResponseContentType(headers: Headers): ParsedMediaType | null {
|
||||
const contentType = headers.get("Content-Type");
|
||||
if (contentType === null) return null;
|
||||
|
||||
try {
|
||||
return parseContentType(contentType);
|
||||
@@ -188,12 +196,5 @@ export function calculateRetryBackoff(err: any, attempts: number, retryConnectio
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
|
||||
return 1000 * Math.pow(2, attempts);
|
||||
return safeGetRetryAfterMs(err, 1000 * Math.pow(2, attempts));
|
||||
}
|
||||
|
||||
+2
-2
@@ -16,10 +16,10 @@ limitations under the License.
|
||||
|
||||
import * as matrixcs from "./matrix.ts";
|
||||
|
||||
if (global.__js_sdk_entrypoint) {
|
||||
if (globalThis.__js_sdk_entrypoint) {
|
||||
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
|
||||
}
|
||||
global.__js_sdk_entrypoint = true;
|
||||
globalThis.__js_sdk_entrypoint = true;
|
||||
|
||||
export * from "./matrix.ts";
|
||||
export default matrixcs;
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ function amendClientOpts(opts: ICreateClientOpts): ICreateClientOpts {
|
||||
opts.store =
|
||||
opts.store ??
|
||||
new MemoryStore({
|
||||
localStorage: global.localStorage,
|
||||
localStorage: globalThis.localStorage,
|
||||
});
|
||||
opts.scheduler = opts.scheduler ?? new MatrixScheduler();
|
||||
opts.cryptoStore = opts.cryptoStore ?? cryptoStoreFactory();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -34,7 +34,7 @@ import { randomString, secureRandomBase64Url } from "../randomstring.ts";
|
||||
import { EncryptionKeysEventContent } from "./types.ts";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
||||
import { KnownMembership } from "../@types/membership.ts";
|
||||
import { MatrixError } from "../http-api/errors.ts";
|
||||
import { HTTPError, MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall.ts";
|
||||
@@ -42,20 +42,6 @@ import { sleep } from "../utils.ts";
|
||||
|
||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||
|
||||
const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000;
|
||||
const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event
|
||||
const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000;
|
||||
const UPDATE_ENCRYPTION_KEY_THROTTLE = 3000;
|
||||
|
||||
// A delay after a member leaves before we create and publish a new key, because people
|
||||
// tend to leave calls at the same time
|
||||
const MAKE_KEY_DELAY = 3000;
|
||||
// The delay between creating and sending a new key and starting to encrypt with it. This gives others
|
||||
// a chance to receive the new key to minimise the chance they don't get media they can't decrypt.
|
||||
// The total time between a member leaving and the call switching to new keys is therefore
|
||||
// MAKE_KEY_DELAY + SEND_KEY_DELAY
|
||||
const USE_KEY_DELAY = 5000;
|
||||
|
||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
|
||||
|
||||
@@ -87,12 +73,15 @@ export type MatrixRTCSessionEventHandlerMap = {
|
||||
participantId: string,
|
||||
) => void;
|
||||
};
|
||||
|
||||
export interface JoinSessionConfig {
|
||||
/** If true, generate and share a media key for this participant,
|
||||
/**
|
||||
* If true, generate and share a media key for this participant,
|
||||
* and emit MatrixRTCSessionEvent.EncryptionKeyChanged when
|
||||
* media keys for other participants become available.
|
||||
*/
|
||||
manageMediaKeys?: boolean;
|
||||
|
||||
/** Lets you configure how the events for the session are formatted.
|
||||
* - legacy: use one event with a membership array.
|
||||
* - MSC4143: use one event per membership (with only one membership per event)
|
||||
@@ -100,7 +89,64 @@ export interface JoinSessionConfig {
|
||||
* `CallMembershipDataLegacy` and `SessionMembershipData`
|
||||
*/
|
||||
useLegacyMemberEvents?: boolean;
|
||||
|
||||
/**
|
||||
* The timeout (in milliseconds) after we joined the call, that our membership should expire
|
||||
* unless we have explicitly updated it.
|
||||
*/
|
||||
membershipExpiryTimeout?: number;
|
||||
|
||||
/**
|
||||
* The period (in milliseconds) with which we check that our membership event still exists on the
|
||||
* server. If it is not found we create it again.
|
||||
*/
|
||||
memberEventCheckPeriod?: number;
|
||||
|
||||
/**
|
||||
* The minimum delay (in milliseconds) after which we will retry sending the membership event if it
|
||||
* failed to send.
|
||||
*/
|
||||
callMemberEventRetryDelayMinimum?: number;
|
||||
|
||||
/**
|
||||
* The jitter (in milliseconds) which is added to callMemberEventRetryDelayMinimum before retrying
|
||||
* sending the membership event. e.g. if this is set to 1000, then a random delay of between 0 and 1000
|
||||
* milliseconds will be added.
|
||||
*/
|
||||
callMemberEventRetryJitter?: number;
|
||||
|
||||
/**
|
||||
* The minimum time (in milliseconds) between each attempt to send encryption key(s).
|
||||
* e.g. if this is set to 1000, then we will send at most one key event every second.
|
||||
*/
|
||||
updateEncryptionKeyThrottle?: number;
|
||||
|
||||
/**
|
||||
* The delay (in milliseconds) after a member leaves before we create and publish a new key, because people
|
||||
* tend to leave calls at the same time.
|
||||
*/
|
||||
makeKeyDelay?: number;
|
||||
|
||||
/**
|
||||
* The delay (in milliseconds) between creating and sending a new key and starting to encrypt with it. This
|
||||
* gives other a chance to receive the new key to minimise the chance they don't get media they can't decrypt.
|
||||
* The total time between a member leaving and the call switching to new keys is therefore:
|
||||
* makeKeyDelay + useKeyDelay
|
||||
*/
|
||||
useKeyDelay?: number;
|
||||
|
||||
/**
|
||||
* The timeout (in milliseconds) after which the server will consider the membership to have expired if it
|
||||
* has not received a keep-alive from the client.
|
||||
*/
|
||||
membershipServerSideExpiryTimeout?: number;
|
||||
|
||||
/**
|
||||
* The period (in milliseconds) that the client will send membership keep-alives to the server.
|
||||
*/
|
||||
membershipKeepAlivePeriod?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
|
||||
* This class doesn't deal with media at all, just membership & properties of a session.
|
||||
@@ -109,10 +155,57 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
// The session Id of the call, this is the call_id of the call Member event.
|
||||
private _callId: string | undefined;
|
||||
|
||||
// How many ms after we joined the call, that our membership should expire, or undefined
|
||||
// if we're not yet joined
|
||||
private relativeExpiry: number | undefined;
|
||||
|
||||
// undefined means not yet joined
|
||||
private joinConfig?: JoinSessionConfig;
|
||||
|
||||
private get membershipExpiryTimeout(): number {
|
||||
return this.joinConfig?.membershipExpiryTimeout ?? 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
private get memberEventCheckPeriod(): number {
|
||||
return this.joinConfig?.memberEventCheckPeriod ?? 2 * 60 * 1000;
|
||||
}
|
||||
|
||||
private get callMemberEventRetryDelayMinimum(): number {
|
||||
return this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000;
|
||||
}
|
||||
|
||||
private get updateEncryptionKeyThrottle(): number {
|
||||
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
|
||||
}
|
||||
|
||||
private get makeKeyDelay(): number {
|
||||
return this.joinConfig?.makeKeyDelay ?? 3_000;
|
||||
}
|
||||
|
||||
private get useKeyDelay(): number {
|
||||
return this.joinConfig?.useKeyDelay ?? 5_000;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the server disallows the configured {@link membershipServerSideExpiryTimeout},
|
||||
* this stores a delay that the server does allow.
|
||||
*/
|
||||
private membershipServerSideExpiryTimeoutOverride?: number;
|
||||
|
||||
private get membershipServerSideExpiryTimeout(): number {
|
||||
return (
|
||||
this.membershipServerSideExpiryTimeoutOverride ??
|
||||
this.joinConfig?.membershipServerSideExpiryTimeout ??
|
||||
8_000
|
||||
);
|
||||
}
|
||||
|
||||
private get membershipKeepAlivePeriod(): number {
|
||||
return this.joinConfig?.membershipKeepAlivePeriod ?? 5_000;
|
||||
}
|
||||
|
||||
private get callMemberEventRetryJitter(): number {
|
||||
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
|
||||
}
|
||||
|
||||
// An identifier for our membership of the call. This will allow us to easily recognise
|
||||
// whether a membership was sent by this session or is stale from some other time.
|
||||
// It also forces our membership events to be unique, because otherwise we could try
|
||||
@@ -320,7 +413,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
|
||||
this.ownFocusActive = fociActive;
|
||||
this.ownFociPreferred = fociPreferred;
|
||||
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME;
|
||||
this.joinConfig = joinConfig;
|
||||
this.relativeExpiry = this.membershipExpiryTimeout;
|
||||
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
||||
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
|
||||
this.membershipId = randomString(5);
|
||||
@@ -373,6 +467,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
this.setNewKeyTimeouts.clear();
|
||||
|
||||
logger.info(`Leaving call session in room ${this.room.roomId}`);
|
||||
this.joinConfig = undefined;
|
||||
this.relativeExpiry = undefined;
|
||||
this.ownFocusActive = undefined;
|
||||
this.manageMediaKeys = false;
|
||||
@@ -515,7 +610,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
||||
}
|
||||
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
|
||||
}, USE_KEY_DELAY);
|
||||
}, this.useKeyDelay);
|
||||
this.setNewKeyTimeouts.add(useKeyTimeout);
|
||||
} else {
|
||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
||||
@@ -554,11 +649,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
|
||||
if (
|
||||
this.lastEncryptionKeyUpdateRequest &&
|
||||
this.lastEncryptionKeyUpdateRequest + UPDATE_ENCRYPTION_KEY_THROTTLE > Date.now()
|
||||
this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now()
|
||||
) {
|
||||
logger.info("Last encryption key event sent too recently: postponing");
|
||||
if (this.keysEventUpdateTimeout === undefined) {
|
||||
this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, UPDATE_ENCRYPTION_KEY_THROTTLE);
|
||||
this.keysEventUpdateTimeout = setTimeout(
|
||||
this.sendEncryptionKeysEvent,
|
||||
this.updateEncryptionKeyThrottle,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -630,7 +728,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
this.client.cancelPendingEvent(matrixError.event);
|
||||
}
|
||||
if (this.keysEventUpdateTimeout === undefined) {
|
||||
const resendDelay = matrixError.data?.retry_after_ms ?? 5000;
|
||||
const resendDelay = safeGetRetryAfterMs(matrixError, 5000);
|
||||
logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error);
|
||||
this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, resendDelay);
|
||||
} else {
|
||||
@@ -772,6 +870,12 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
if (changed) {
|
||||
logger.info(`Memberships for call in room ${this.room.roomId} have changed: emitting`);
|
||||
this.emit(MatrixRTCSessionEvent.MembershipsChanged, oldMemberships, this.memberships);
|
||||
|
||||
if (this.isJoined() && !this.memberships.some(this.isMyMembership)) {
|
||||
logger.warn("Missing own membership: force re-join");
|
||||
// TODO: Should this be awaited? And is there anything to tell the focus?
|
||||
this.triggerCallMembershipEventUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.manageMediaKeys && this.isJoined() && this.makeNewKeyTimeout === undefined) {
|
||||
@@ -793,7 +897,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
|
||||
if (anyLeft) {
|
||||
logger.debug(`Member(s) have left: queueing sender key rotation`);
|
||||
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY);
|
||||
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
|
||||
} else if (anyJoined) {
|
||||
logger.debug(`New member(s) have joined: re-sending keys`);
|
||||
this.requestSendCurrentKey();
|
||||
@@ -881,9 +985,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
if (!myPrevMembership) return true;
|
||||
|
||||
const expiryTime = myPrevMembership.getMsUntilExpiry();
|
||||
if (expiryTime !== undefined && expiryTime < MEMBERSHIP_EXPIRY_TIME / 2) {
|
||||
if (expiryTime !== undefined && expiryTime < this.membershipExpiryTimeout / 2) {
|
||||
// ...or if the expiry time needs bumping
|
||||
this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME;
|
||||
this.relativeExpiry! += this.membershipExpiryTimeout;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -898,7 +1002,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* Makes a new membership list given the old list alonng with this user's previous membership event
|
||||
* Makes a new membership list given the old list along with this user's previous membership event
|
||||
* (if any) and this device's previous membership (if any)
|
||||
*/
|
||||
private makeNewLegacyMemberships(
|
||||
@@ -942,6 +1046,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
}
|
||||
|
||||
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
||||
// TODO: Should this await on a shared promise?
|
||||
if (this.updateCallMembershipRunning) {
|
||||
this.needCallMembershipUpdate = true;
|
||||
return;
|
||||
@@ -1003,7 +1108,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
}
|
||||
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
|
||||
// nothing to do - reschedule the check again
|
||||
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
|
||||
this.memberEventTimeout = setTimeout(
|
||||
this.triggerCallMembershipEventUpdate,
|
||||
this.memberEventCheckPeriod,
|
||||
);
|
||||
return;
|
||||
}
|
||||
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
|
||||
@@ -1023,47 +1131,60 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
// check periodically to see if we need to refresh our member event
|
||||
this.memberEventTimeout = setTimeout(
|
||||
this.triggerCallMembershipEventUpdate,
|
||||
MEMBER_EVENT_CHECK_PERIOD,
|
||||
this.memberEventCheckPeriod,
|
||||
);
|
||||
}
|
||||
} else if (this.isJoined()) {
|
||||
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
|
||||
const prepareDelayedDisconnection = async (): Promise<void> => {
|
||||
try {
|
||||
// TODO: If delayed event times out, re-join!
|
||||
const res = await this.client._unstable_sendDelayedStateEvent(
|
||||
this.room.roomId,
|
||||
{
|
||||
delay: 8000,
|
||||
},
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{}, // leave event
|
||||
stateKey,
|
||||
const res = await resendIfRateLimited(() =>
|
||||
this.client._unstable_sendDelayedStateEvent(
|
||||
this.room.roomId,
|
||||
{
|
||||
delay: this.membershipServerSideExpiryTimeout,
|
||||
},
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{}, // leave event
|
||||
stateKey,
|
||||
),
|
||||
);
|
||||
this.disconnectDelayId = res.delay_id;
|
||||
} catch (e) {
|
||||
// TODO: Retry if rate-limited
|
||||
if (
|
||||
e instanceof MatrixError &&
|
||||
e.errcode === "M_UNKNOWN" &&
|
||||
e.data["org.matrix.msc4140.errcode"] === "M_MAX_DELAY_EXCEEDED"
|
||||
) {
|
||||
const maxDelayAllowed = e.data["org.matrix.msc4140.max_delay"];
|
||||
if (
|
||||
typeof maxDelayAllowed === "number" &&
|
||||
this.membershipServerSideExpiryTimeout > maxDelayAllowed
|
||||
) {
|
||||
this.membershipServerSideExpiryTimeoutOverride = maxDelayAllowed;
|
||||
return prepareDelayedDisconnection();
|
||||
}
|
||||
}
|
||||
logger.error("Failed to prepare delayed disconnection event:", e);
|
||||
}
|
||||
};
|
||||
await prepareDelayedDisconnection();
|
||||
// Send join event _after_ preparing the delayed disconnection event
|
||||
await this.client.sendStateEvent(
|
||||
this.room.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
newContent,
|
||||
stateKey,
|
||||
await resendIfRateLimited(() =>
|
||||
this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey),
|
||||
);
|
||||
// If sending state cancels your own delayed state, prepare another delayed state
|
||||
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
|
||||
if (this.disconnectDelayId !== undefined) {
|
||||
try {
|
||||
await this.client._unstable_updateDelayedEvent(
|
||||
this.disconnectDelayId,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
const knownDisconnectDelayId = this.disconnectDelayId;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(
|
||||
knownDisconnectDelayId,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// TODO: Make embedded client include errcode, and retry only if not M_NOT_FOUND (or rate-limited)
|
||||
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
|
||||
this.disconnectDelayId = undefined;
|
||||
await prepareDelayedDisconnection();
|
||||
@@ -1076,29 +1197,33 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
let sentDelayedDisconnect = false;
|
||||
if (this.disconnectDelayId !== undefined) {
|
||||
try {
|
||||
await this.client._unstable_updateDelayedEvent(
|
||||
this.disconnectDelayId,
|
||||
UpdateDelayedEventAction.Send,
|
||||
const knownDisconnectDelayId = this.disconnectDelayId;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(
|
||||
knownDisconnectDelayId,
|
||||
UpdateDelayedEventAction.Send,
|
||||
),
|
||||
);
|
||||
sentDelayedDisconnect = true;
|
||||
} catch (e) {
|
||||
// TODO: Retry if rate-limited
|
||||
logger.error("Failed to send our delayed disconnection event:", e);
|
||||
}
|
||||
this.disconnectDelayId = undefined;
|
||||
}
|
||||
if (!sentDelayedDisconnect) {
|
||||
await this.client.sendStateEvent(
|
||||
this.room.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{},
|
||||
this.makeMembershipStateKey(localUserId, localDeviceId),
|
||||
await resendIfRateLimited(() =>
|
||||
this.client.sendStateEvent(
|
||||
this.room.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{},
|
||||
this.makeMembershipStateKey(localUserId, localDeviceId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
logger.info("Sent updated call member event.");
|
||||
} catch (e) {
|
||||
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000;
|
||||
const resendDelay = this.callMemberEventRetryDelayMinimum + Math.random() * this.callMemberEventRetryJitter;
|
||||
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
|
||||
await sleep(resendDelay);
|
||||
await this.triggerCallMembershipEventUpdate();
|
||||
@@ -1106,15 +1231,17 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
}
|
||||
|
||||
private scheduleDelayDisconnection(): void {
|
||||
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000);
|
||||
this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod);
|
||||
}
|
||||
|
||||
private readonly delayDisconnection = async (): Promise<void> => {
|
||||
try {
|
||||
await this.client._unstable_updateDelayedEvent(this.disconnectDelayId!, UpdateDelayedEventAction.Restart);
|
||||
const knownDisconnectDelayId = this.disconnectDelayId!;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(knownDisconnectDelayId, UpdateDelayedEventAction.Restart),
|
||||
);
|
||||
this.scheduleDelayDisconnection();
|
||||
} catch (e) {
|
||||
// TODO: Retry if rate-limited
|
||||
logger.error("Failed to delay our disconnection event:", e);
|
||||
}
|
||||
};
|
||||
@@ -1162,3 +1289,31 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
||||
this.sendEncryptionKeysEvent(newKeyIndex);
|
||||
};
|
||||
}
|
||||
|
||||
async function resendIfRateLimited<T>(func: () => Promise<T>, numRetriesAllowed: number = 1): Promise<T> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await func();
|
||||
} catch (e) {
|
||||
if (numRetriesAllowed > 0 && e instanceof HTTPError && e.isRateLimitError()) {
|
||||
numRetriesAllowed--;
|
||||
let resendDelay: number;
|
||||
const defaultMs = 5000;
|
||||
try {
|
||||
resendDelay = e.getRetryAfterMs() ?? defaultMs;
|
||||
logger.info(`Rate limited by server, retrying in ${resendDelay}ms`);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Error while retrieving a rate-limit retry delay, retrying after default delay of ${defaultMs}`,
|
||||
e,
|
||||
);
|
||||
resendDelay = defaultMs;
|
||||
}
|
||||
await sleep(resendDelay);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-29
@@ -2858,22 +2858,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some events to this room. This can include state events, message
|
||||
* events and typing notifications. These events are treated as "live" so
|
||||
* they will go to the end of the timeline.
|
||||
*
|
||||
* @param events - A list of events to add.
|
||||
* @param addLiveEventOptions - addLiveEvent options
|
||||
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
|
||||
*/
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
|
||||
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||
}
|
||||
|
||||
// sanity check that the live timeline is still live
|
||||
private assertTimelineSetsAreLive(): void {
|
||||
for (let i = 0; i < this.timelineSets.length; i++) {
|
||||
const liveTimeline = this.timelineSets[i].getLiveTimeline();
|
||||
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
|
||||
@@ -2890,6 +2875,25 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add some events to this room. This can include state events, message
|
||||
* events and typing notifications. These events are treated as "live" so
|
||||
* they will go to the end of the timeline.
|
||||
*
|
||||
* @param events - A list of events to add.
|
||||
* @param addLiveEventOptions - addLiveEvent options
|
||||
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
|
||||
*/
|
||||
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
|
||||
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
|
||||
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
|
||||
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
|
||||
}
|
||||
|
||||
// sanity check that the live timeline is still live
|
||||
this.assertTimelineSetsAreLive();
|
||||
|
||||
const threadRoots = this.findThreadRoots(events);
|
||||
const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
|
||||
@@ -2916,11 +2920,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
let { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
|
||||
event,
|
||||
neighbouringEvents,
|
||||
threadRoots,
|
||||
);
|
||||
let {
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
threadId = "",
|
||||
} = this.eventShouldLiveIn(event, neighbouringEvents, threadRoots);
|
||||
|
||||
if (!shouldLiveInThread && !shouldLiveInRoom && event.isRelation()) {
|
||||
try {
|
||||
@@ -2935,20 +2939,20 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
event.setUnsigned(unsigned);
|
||||
}
|
||||
|
||||
({ shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
|
||||
event,
|
||||
neighbouringEvents,
|
||||
threadRoots,
|
||||
));
|
||||
({
|
||||
shouldLiveInRoom,
|
||||
shouldLiveInThread,
|
||||
threadId = "",
|
||||
} = this.eventShouldLiveIn(event, neighbouringEvents, threadRoots));
|
||||
} catch (e) {
|
||||
logger.error("Failed to load parent event of unhandled relation", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
|
||||
eventsByThread[threadId ?? ""] = [];
|
||||
if (shouldLiveInThread && !eventsByThread[threadId]) {
|
||||
eventsByThread[threadId] = [];
|
||||
}
|
||||
eventsByThread[threadId ?? ""]?.push(event);
|
||||
eventsByThread[threadId]?.push(event);
|
||||
|
||||
if (shouldLiveInRoom) {
|
||||
this.addLiveEvent(event, options);
|
||||
|
||||
@@ -33,7 +33,7 @@ const TIMER_CHECK_PERIOD_MS = 1000;
|
||||
// counter, for making up ids to return from setTimeout
|
||||
let count = 0;
|
||||
|
||||
// the key for our callback with the real global.setTimeout
|
||||
// the key for our callback with the real globalThis.setTimeout
|
||||
let realCallbackKey: NodeJS.Timeout | number;
|
||||
|
||||
type Callback = {
|
||||
@@ -114,10 +114,10 @@ export function clearTimeout(key: number): void {
|
||||
}
|
||||
}
|
||||
|
||||
// use the real global.setTimeout to schedule a callback to runCallbacks.
|
||||
// use the real globalThis.setTimeout to schedule a callback to runCallbacks.
|
||||
function scheduleRealCallback(): void {
|
||||
if (realCallbackKey) {
|
||||
global.clearTimeout(realCallbackKey as NodeJS.Timeout);
|
||||
globalThis.clearTimeout(realCallbackKey as NodeJS.Timeout);
|
||||
}
|
||||
|
||||
const first = callbackList[0];
|
||||
@@ -131,7 +131,7 @@ function scheduleRealCallback(): void {
|
||||
const delayMs = Math.min(first.runAt - timestamp, TIMER_CHECK_PERIOD_MS);
|
||||
|
||||
debuglog("scheduleRealCallback: now:", timestamp, "delay:", delayMs);
|
||||
realCallbackKey = global.setTimeout(runCallbacks, delayMs);
|
||||
realCallbackKey = globalThis.setTimeout(runCallbacks, delayMs);
|
||||
}
|
||||
|
||||
function runCallbacks(): void {
|
||||
@@ -158,7 +158,7 @@ function runCallbacks(): void {
|
||||
|
||||
for (const cb of callbacksToRun) {
|
||||
try {
|
||||
cb.func.apply(global, cb.params);
|
||||
cb.func.apply(globalThis, cb.params);
|
||||
} catch (e) {
|
||||
logger.error("Uncaught exception in callback function", e);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class MSC4108RendezvousSession {
|
||||
public url?: string;
|
||||
private readonly client?: MatrixClient;
|
||||
private readonly fallbackRzServer?: string;
|
||||
private readonly fetchFn?: typeof global.fetch;
|
||||
private readonly fetchFn?: typeof globalThis.fetch;
|
||||
private readonly onFailure?: RendezvousFailureListener;
|
||||
private etag?: string;
|
||||
private expiresAt?: Date;
|
||||
@@ -42,7 +42,7 @@ export class MSC4108RendezvousSession {
|
||||
url,
|
||||
fetchFn,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
url: string;
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export class MSC4108RendezvousSession {
|
||||
fallbackRzServer,
|
||||
fetchFn,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
client?: MatrixClient;
|
||||
fallbackRzServer?: string;
|
||||
@@ -64,7 +64,7 @@ export class MSC4108RendezvousSession {
|
||||
client,
|
||||
fallbackRzServer,
|
||||
}: {
|
||||
fetchFn?: typeof global.fetch;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
onFailure?: RendezvousFailureListener;
|
||||
url?: string;
|
||||
client?: MatrixClient;
|
||||
@@ -91,11 +91,11 @@ export class MSC4108RendezvousSession {
|
||||
return this._cancelled;
|
||||
}
|
||||
|
||||
private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof global.fetch> {
|
||||
private fetch(resource: URL | string, options?: RequestInit): ReturnType<typeof globalThis.fetch> {
|
||||
if (this.fetchFn) {
|
||||
return this.fetchFn(resource, options);
|
||||
}
|
||||
return global.fetch(resource, options);
|
||||
return globalThis.fetch(resource, options);
|
||||
}
|
||||
|
||||
private async getPostEndpoint(): Promise<string | undefined> {
|
||||
|
||||
@@ -50,7 +50,7 @@ export class KeyClaimManager {
|
||||
* Given a list of users, attempt to ensure that we have Olm Sessions active with each of their devices
|
||||
*
|
||||
* If we don't have an active olm session, we will claim a one-time key and start one.
|
||||
*
|
||||
* @param logger - logger to use
|
||||
* @param userList - list of userIDs to claim
|
||||
*/
|
||||
public ensureSessionsForUsers(logger: LogSpan, userList: Array<UserId>): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -370,15 +370,17 @@ export class PerSessionKeyBackupDownloader {
|
||||
// Notice that this request will be lost if instead the backup got out of sync (updated from other session).
|
||||
throw new KeyDownloadError(KeyDownloadErrorCode.MISSING_DECRYPTION_KEY);
|
||||
}
|
||||
if (errCode == "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = e.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
this.logger.info(`Rate limited by server, waiting ${waitTime}ms`);
|
||||
throw new KeyDownloadRateLimitError(waitTime);
|
||||
} else {
|
||||
// apply the default backoff time
|
||||
throw new KeyDownloadRateLimitError(KEY_BACKUP_BACKOFF);
|
||||
if (e.isRateLimitError()) {
|
||||
let waitTime: number | undefined;
|
||||
try {
|
||||
waitTime = e.getRetryAfterMs() ?? undefined;
|
||||
} catch (error) {
|
||||
this.logger.warn("Error while retrieving a rate-limit retry delay", error);
|
||||
}
|
||||
if (waitTime && waitTime > 0) {
|
||||
this.logger.info(`Rate limited by server, waiting ${waitTime}ms`);
|
||||
}
|
||||
throw new KeyDownloadRateLimitError(waitTime ?? KEY_BACKUP_BACKOFF);
|
||||
}
|
||||
}
|
||||
throw new KeyDownloadError(KeyDownloadErrorCode.NETWORK_ERROR);
|
||||
|
||||
+24
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 - 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
KeyBackupInfo,
|
||||
KeyBackupSession,
|
||||
Curve25519SessionData,
|
||||
KeyBackupRoomSessions,
|
||||
} from "../crypto-api/keybackup.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
|
||||
@@ -34,8 +35,6 @@ import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { BackupDecryptor } from "../common-crypto/CryptoBackend.ts";
|
||||
import { ImportRoomKeyProgressData, ImportRoomKeysOpts, CryptoEvent } from "../crypto-api/index.ts";
|
||||
import { IKeyBackupInfo } from "../crypto/keybackup.ts";
|
||||
import { IKeyBackup } from "../crypto/backup.ts";
|
||||
import { AESEncryptedSecretStoragePayload } from "../@types/AESEncryptedSecretStoragePayload.ts";
|
||||
|
||||
/** Authentification of the backup info, depends on algorithm */
|
||||
@@ -458,12 +457,19 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
this.backupKeysLoopRunning = false;
|
||||
this.checkKeyBackupAndEnable(true);
|
||||
return;
|
||||
} else if (errCode == "M_LIMIT_EXCEEDED") {
|
||||
} else if (err.isRateLimitError()) {
|
||||
// wait for that and then continue?
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime > 0) {
|
||||
await sleep(waitTime);
|
||||
continue;
|
||||
try {
|
||||
const waitTime = err.getRetryAfterMs();
|
||||
if (waitTime && waitTime > 0) {
|
||||
await sleep(waitTime);
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
"Backup: An error occurred while retrieving a rate-limit retry delay",
|
||||
error,
|
||||
);
|
||||
} // else go to the normal backoff
|
||||
}
|
||||
}
|
||||
@@ -488,7 +494,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
|
||||
* @returns The number of keys in the backup request.
|
||||
*/
|
||||
private keysCountInBatch(batch: RustSdkCryptoJs.KeysBackupRequest): number {
|
||||
const parsedBody: IKeyBackup = JSON.parse(batch.body);
|
||||
const parsedBody: KeyBackup = JSON.parse(batch.body);
|
||||
let count = 0;
|
||||
for (const { sessions } of Object.values(parsedBody.rooms)) {
|
||||
count += Object.keys(sessions).length;
|
||||
@@ -653,7 +659,7 @@ export class RustBackupDecryptor implements BackupDecryptor {
|
||||
|
||||
export async function requestKeyBackupVersion(
|
||||
http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
): Promise<IKeyBackupInfo | null> {
|
||||
): Promise<KeyBackupInfo | null> {
|
||||
try {
|
||||
return await http.authedRequest<KeyBackupInfo>(Method.Get, "/room_keys/version", undefined, undefined, {
|
||||
prefix: ClientPrefix.V3,
|
||||
@@ -679,3 +685,11 @@ export type RustBackupCryptoEventMap = {
|
||||
[CryptoEvent.KeyBackupFailed]: (errCode: string) => void;
|
||||
[CryptoEvent.KeyBackupDecryptionKeyCached]: (version: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Response from GET `/room_keys/keys` endpoint.
|
||||
* See https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3room_keyskeys
|
||||
*/
|
||||
export interface KeyBackup {
|
||||
rooms: Record<string, { sessions: KeyBackupRoomSessions }>;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm";
|
||||
import { RustCrypto } from "./rust-crypto.ts";
|
||||
import { IHttpOpts, MatrixHttpApi } from "../http-api/index.ts";
|
||||
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
||||
import { ICryptoCallbacks } from "../crypto/index.ts";
|
||||
import { Logger } from "../logger.ts";
|
||||
import { CryptoStore, MigrationState } from "../crypto/store/base.ts";
|
||||
import {
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
migrateLegacyLocalTrustIfNeeded,
|
||||
migrateRoomSettingsFromLegacyCrypto,
|
||||
} from "./libolm_migration.ts";
|
||||
import { CryptoCallbacks } from "../crypto-api/index.ts";
|
||||
|
||||
/**
|
||||
* Create a new `RustCrypto` implementation
|
||||
@@ -55,7 +55,7 @@ export async function initRustCrypto(args: {
|
||||
secretStorage: ServerSideSecretStorage;
|
||||
|
||||
/** Crypto callbacks provided by the application. */
|
||||
cryptoCallbacks: ICryptoCallbacks;
|
||||
cryptoCallbacks: CryptoCallbacks;
|
||||
|
||||
/**
|
||||
* The prefix to use on the indexeddbs created by rust-crypto.
|
||||
@@ -145,7 +145,7 @@ async function initOlmMachine(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
secretStorage: ServerSideSecretStorage,
|
||||
cryptoCallbacks: ICryptoCallbacks,
|
||||
cryptoCallbacks: CryptoCallbacks,
|
||||
storeHandle: StoreHandle,
|
||||
legacyCryptoStore?: CryptoStore,
|
||||
): Promise<RustCrypto> {
|
||||
|
||||
@@ -757,7 +757,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#boostrapCrossSigning}
|
||||
* Implementation of {@link CryptoApi#bootstrapCrossSigning}
|
||||
*/
|
||||
public async bootstrapCrossSigning(opts: BootstrapCrossSigningOpts): Promise<void> {
|
||||
await this.crossSigningIdentity.bootstrapCrossSigning(opts);
|
||||
@@ -953,7 +953,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link Crypto.CryptoApi.getEncryptionInfoForEvent}.
|
||||
* Implementation of {@link CryptoApi#getEncryptionInfoForEvent}.
|
||||
*/
|
||||
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
|
||||
return this.eventDecryptor.getEncryptionInfoForEvent(event);
|
||||
@@ -1214,7 +1214,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Determine if a key backup can be trusted.
|
||||
*
|
||||
* Implementation of {@link Crypto.CryptoApi.isKeyBackupTrusted}.
|
||||
* Implementation of {@link CryptoApi#isKeyBackupTrusted}.
|
||||
*/
|
||||
public async isKeyBackupTrusted(info: KeyBackupInfo): Promise<BackupTrustInfo> {
|
||||
return await this.backupManager.isKeyBackupTrusted(info);
|
||||
@@ -1223,7 +1223,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Force a re-check of the key backup and enable/disable it as appropriate.
|
||||
*
|
||||
* Implementation of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
|
||||
* Implementation of {@link CryptoApi#checkKeyBackupAndEnable}.
|
||||
*/
|
||||
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
|
||||
return await this.backupManager.checkKeyBackupAndEnable(true);
|
||||
|
||||
+5
-5
@@ -694,7 +694,7 @@ export class SyncApi {
|
||||
this.running = true;
|
||||
this.abortController = new AbortController();
|
||||
|
||||
global.window?.addEventListener?.("online", this.onOnline, false);
|
||||
globalThis.window?.addEventListener?.("online", this.onOnline, false);
|
||||
|
||||
if (this.client.isGuest()) {
|
||||
// no push rules for guests, no access to POST filter for guests.
|
||||
@@ -779,10 +779,10 @@ export class SyncApi {
|
||||
public stop(): void {
|
||||
debuglog("SyncApi.stop");
|
||||
// It is necessary to check for the existance of
|
||||
// global.window AND global.window.removeEventListener.
|
||||
// Some platforms (e.g. React Native) register global.window,
|
||||
// but do not have global.window.removeEventListener.
|
||||
global.window?.removeEventListener?.("online", this.onOnline, false);
|
||||
// globalThis.window AND globalThis.window.removeEventListener.
|
||||
// Some platforms (e.g. React Native) register globalThis.window,
|
||||
// but do not have globalThis.window.removeEventListener.
|
||||
globalThis.window?.removeEventListener?.("online", this.onOnline, false);
|
||||
this.running = false;
|
||||
this.abortController?.abort();
|
||||
if (this.keepAliveTimer) {
|
||||
|
||||
@@ -4940,10 +4940,10 @@ matrix-mock-request@^2.5.0:
|
||||
dependencies:
|
||||
expect "^28.1.0"
|
||||
|
||||
matrix-widget-api@^1.8.2:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.9.0.tgz#884136b405bd3c56e4ea285095c9e01ec52b6b1f"
|
||||
integrity sha512-au8mqralNDqrEvaVAkU37bXOb8I9SCe+ACdPk11QWw58FKstVq31q2wRz+qWA6J+42KJ6s1DggWbG/S3fEs3jw==
|
||||
matrix-widget-api@^1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55"
|
||||
integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw==
|
||||
dependencies:
|
||||
"@types/events" "^3.0.0"
|
||||
events "^3.2.0"
|
||||
@@ -6486,10 +6486,10 @@ url-parse@^1.5.3:
|
||||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
uuid@10:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
|
||||
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
|
||||
uuid@11:
|
||||
version "11.0.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.2.tgz#a8d68ba7347d051e7ea716cc8dcbbab634d66875"
|
||||
integrity sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==
|
||||
|
||||
uuid@8.3.2:
|
||||
version "8.3.2"
|
||||
|
||||
Reference in New Issue
Block a user