Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b316613f6 | |||
| 387b3485ae | |||
| 9f6073478f | |||
| b47c87f909 | |||
| c66850e897 | |||
| 2766146c49 | |||
| 61497c9a8f | |||
| 5981feeb44 | |||
| 51218ddc1d | |||
| b907433d38 | |||
| 32c0b81332 | |||
| 634b8ebbb4 | |||
| 4ab8066e1f | |||
| 13dccb3d71 | |||
| ef1f5bf232 | |||
| a03e3dd501 | |||
| 3cfad3cdeb | |||
| 60c715d5df | |||
| c2942ddbc7 | |||
| 2d7fdde7ed | |||
| cf34e90cb4 | |||
| f0fa4d2cc8 | |||
| c52e4b6329 | |||
| 9451b55985 | |||
| 39714bfe6f | |||
| f5f6100b1e | |||
| ecd700a36e | |||
| ea7042efb9 | |||
| 04a6c4e6c4 | |||
| 0329824cab | |||
| 3351c4f57a | |||
| e70a1a1eff | |||
| 1a5af9d8e3 | |||
| 258f157ebc | |||
| dfb079a76f | |||
| 858155e0ef | |||
| 946a1cef0f | |||
| 6c1fdbb7e9 | |||
| 8cd7c96496 | |||
| cc9d530bcf | |||
| cabf6da6a7 | |||
| 895a82efcd |
@@ -0,0 +1,31 @@
|
||||
# Triggers after the "Downstream artifacts" build has finished, to run the
|
||||
# cypress tests (with access to repo secrets)
|
||||
|
||||
name: matrix-react-sdk Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build downstream artifacts"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
name: Cypress
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@develop
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
secrets:
|
||||
# secrets are not automatically shared with called workflows, so share the cypress dashboard key, and the Kiwi login details
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
TCMS_USERNAME: ${{ secrets.TCMS_USERNAME }}
|
||||
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
|
||||
with:
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
rust-crypto: true
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Build downstream artifacts
|
||||
on:
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
||||
# For now at least, we don't run this or the cypress-tests against pushes
|
||||
# to develop or master.
|
||||
#
|
||||
# Note that if we later choose to do so, we'll need to find a way to stop
|
||||
# the results in Cypress Cloud from clobbering those from the 'develop'
|
||||
# branch of matrix-react-sdk.
|
||||
#
|
||||
#push:
|
||||
# branches: [develop, master]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
build-element-web:
|
||||
name: Build element-web
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@develop
|
||||
with:
|
||||
matrix-js-sdk-sha: ${{ github.sha }}
|
||||
react-sdk-repository: matrix-org/matrix-react-sdk
|
||||
@@ -24,3 +24,6 @@ out
|
||||
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
|
||||
# This file is also autogenerated
|
||||
/spec/test-utils/test-data/index.ts
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
Changes in [26.1.0-rc1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.1.0-rc1) (2023-06-13)
|
||||
==========================================================================================================
|
||||
|
||||
## 🦖 Deprecations
|
||||
* Introduce a new `Crypto.Verifier` interface, and deprecate direct access to `VerificationBase`, `SAS` and `ReciprocateQRCode` ([\#3414](https://github.com/matrix-org/matrix-js-sdk/pull/3414)).
|
||||
|
||||
## ✨ Features
|
||||
* Add `rust-crypto#isCrossSigningReady` implementation ([\#3462](https://github.com/matrix-org/matrix-js-sdk/pull/3462)). Contributed by @florianduros.
|
||||
* OIDC: Validate `m.authentication` configuration ([\#3419](https://github.com/matrix-org/matrix-js-sdk/pull/3419)). Contributed by @kerryarchibald.
|
||||
* ElementR: Add `CryptoApi.getCrossSigningStatus` ([\#3452](https://github.com/matrix-org/matrix-js-sdk/pull/3452)). Contributed by @florianduros.
|
||||
* Extend stats summary with call device and user count based on room state ([\#3424](https://github.com/matrix-org/matrix-js-sdk/pull/3424)). Contributed by @toger5.
|
||||
* Update MSC3912 implementation to use `with_rel_type` instead of `with_relations` ([\#3420](https://github.com/matrix-org/matrix-js-sdk/pull/3420)).
|
||||
* Export thread-related types from SDK ([\#3447](https://github.com/matrix-org/matrix-js-sdk/pull/3447)). Contributed by @stas-demydiuk.
|
||||
* Use correct /v3 prefix for /refresh ([\#3016](https://github.com/matrix-org/matrix-js-sdk/pull/3016)). Contributed by @davidisaaclee.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix thread list being ordered based on all updates ([\#3458](https://github.com/matrix-org/matrix-js-sdk/pull/3458)). Fixes vector-im/element-web#25522.
|
||||
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
|
||||
* use cli.canSupport to determine intentional mentions support ([\#3445](https://github.com/matrix-org/matrix-js-sdk/pull/3445)). Fixes vector-im/element-web#25497. Contributed by @kerryarchibald.
|
||||
* Make sliding sync linearize processing of sync requests ([\#3442](https://github.com/matrix-org/matrix-js-sdk/pull/3442)).
|
||||
* Fix edge cases around 2nd order relations and threads ([\#3437](https://github.com/matrix-org/matrix-js-sdk/pull/3437)).
|
||||
|
||||
Changes in [26.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.1) (2023-06-09)
|
||||
==================================================================================================
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
* Fix: handle `baseUrl` with trailing slash in `fetch.getUrl` ([\#3455](https://github.com/matrix-org/matrix-js-sdk/pull/3455)). Fixes vector-im/element-web#25526. Contributed by @kerryarchibald.
|
||||
|
||||
Changes in [26.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v26.0.0) (2023-06-06)
|
||||
==================================================================================================
|
||||
|
||||
|
||||
+9
-1
@@ -23,11 +23,19 @@ const config: Config = {
|
||||
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts}"],
|
||||
coverageReporters: ["text-summary", "lcov"],
|
||||
testResultsProcessor: "@casualbot/jest-sonar-reporter",
|
||||
|
||||
// Always print out a summary if there are any failing tests. Normally
|
||||
// a summary is only printed if there are more than 20 test *suites*.
|
||||
reporters: [["default", { summaryThreshold: 0 }]],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
|
||||
const reporters: Config["reporters"] = [
|
||||
["github-actions", { silent: false }],
|
||||
// as above: always show a summary if there were any failing tests.
|
||||
["summary", { summaryThreshold: 0 }],
|
||||
];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "26.0.0",
|
||||
"version": "26.1.0-rc1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -55,7 +55,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.9",
|
||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
@@ -101,13 +101,13 @@
|
||||
"debug": "^4.3.4",
|
||||
"docdash": "^2.0.0",
|
||||
"domexception": "^4.0.0",
|
||||
"eslint": "8.40.0",
|
||||
"eslint": "8.41.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"eslint-plugin-jsdoc": "^44.0.0",
|
||||
"eslint-plugin-jsdoc": "^45.0.0",
|
||||
"eslint-plugin-matrix-org": "^1.0.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.17",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 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 fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../test-utils/test-utils";
|
||||
import { createClient, MatrixClient, UIAuthCallback } from "../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
it("publishes keys if none were yet published", async () => {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
|
||||
await makeRequest(authDict);
|
||||
};
|
||||
|
||||
// now bootstrap cross signing, and check it resolves successfully
|
||||
await aliceClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: uiaCallback,
|
||||
});
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
Copyright 2023 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 fetchMock from "fetch-mock-jest";
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||
import { createClient, MatrixClient, IAuthDict, UIAuthCallback } from "../../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
|
||||
/**
|
||||
* Integration tests for cross-signing functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
/**
|
||||
* Mock the requests needed to set up cross signing
|
||||
*
|
||||
* Return `{}` for `GET _matrix/client/r0/user/:userId/account_data/:type` request
|
||||
* Return `{}` for `POST _matrix/client/v3/keys/signatures/upload` request (named `upload-sigs` for fetchMock check)
|
||||
* Return `{}` for `POST /_matrix/client/(unstable|v3)/keys/device_signing/upload` request (named `upload-keys` for fetchMock check)
|
||||
*/
|
||||
function mockSetupCrossSigningRequests(): void {
|
||||
// have account_data requests return an empty object
|
||||
fetchMock.get("express:/_matrix/client/r0/user/:userId/account_data/:type", {});
|
||||
|
||||
// we expect a request to upload signatures for our device ...
|
||||
fetchMock.post({ url: "path:/_matrix/client/v3/keys/signatures/upload", name: "upload-sigs" }, {});
|
||||
|
||||
// ... and one to upload the cross-signing keys (with UIA)
|
||||
fetchMock.post(
|
||||
// legacy crypto uses /unstable/; /v3/ is correct
|
||||
{
|
||||
url: new RegExp("/_matrix/client/(unstable|v3)/keys/device_signing/upload"),
|
||||
name: "upload-keys",
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cross-signing keys, publish the keys
|
||||
* Mock and bootstrap all the required steps
|
||||
*
|
||||
* @param authDict - The parameters to as the `auth` dict in the key upload request.
|
||||
* @see https://spec.matrix.org/v1.6/client-server-api/#authentication-types
|
||||
*/
|
||||
async function bootstrapCrossSigning(authDict: IAuthDict): Promise<void> {
|
||||
const uiaCallback: UIAuthCallback<void> = async (makeRequest) => {
|
||||
await makeRequest(authDict);
|
||||
};
|
||||
|
||||
// now bootstrap cross signing, and check it resolves successfully
|
||||
await aliceClient.getCrypto()?.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: uiaCallback,
|
||||
});
|
||||
}
|
||||
|
||||
describe("bootstrapCrossSigning (before initialsync completes)", () => {
|
||||
it("publishes keys if none were yet published", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
// check the cross-signing keys upload
|
||||
expect(fetchMock.called("upload-keys")).toBeTruthy();
|
||||
const [, keysOpts] = fetchMock.lastCall("upload-keys")!;
|
||||
const keysBody = JSON.parse(keysOpts!.body as string);
|
||||
expect(keysBody.auth).toEqual(authDict); // check uia dict was passed
|
||||
// there should be a key of each type
|
||||
// master key is signed by the device
|
||||
expect(keysBody).toHaveProperty(`master_key.signatures.[${TEST_USER_ID}].[ed25519:${TEST_DEVICE_ID}]`);
|
||||
const masterKeyId = Object.keys(keysBody.master_key.keys)[0];
|
||||
// ssk and usk are signed by the master key
|
||||
expect(keysBody).toHaveProperty(`self_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
expect(keysBody).toHaveProperty(`user_signing_key.signatures.[${TEST_USER_ID}].[${masterKeyId}]`);
|
||||
const sskId = Object.keys(keysBody.self_signing_key.keys)[0];
|
||||
|
||||
// check the publish call
|
||||
expect(fetchMock.called("upload-sigs")).toBeTruthy();
|
||||
const [, sigsOpts] = fetchMock.lastCall("upload-sigs")!;
|
||||
const body = JSON.parse(sigsOpts!.body as string);
|
||||
// there should be a signature for our device, by our self-signing key.
|
||||
expect(body).toHaveProperty(
|
||||
`[${TEST_USER_ID}].[${TEST_DEVICE_ID}].signatures.[${TEST_USER_ID}].[${sskId}]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCrossSigningStatus()", () => {
|
||||
it("should return correct values without bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be unavailable
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: false,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: false, userSigningKey: false, selfSigningKey: false },
|
||||
});
|
||||
});
|
||||
|
||||
it("should return correct values after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
// provide a UIA callback, so that the cross-signing keys are uploaded
|
||||
const authDict = { type: "test" };
|
||||
await bootstrapCrossSigning(authDict);
|
||||
|
||||
const crossSigningStatus = await aliceClient.getCrypto()!.getCrossSigningStatus();
|
||||
|
||||
// Expect the cross signing keys to be available
|
||||
expect(crossSigningStatus).toStrictEqual({
|
||||
publicKeysOnDevice: true,
|
||||
privateKeysInSecretStorage: false,
|
||||
privateKeysCachedLocally: { masterKey: true, userSigningKey: true, selfSigningKey: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCrossSigningReady()", () => {
|
||||
it("should return false if cross-signing is not bootstrapped", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true after bootstrapping cross-signing", async () => {
|
||||
mockSetupCrossSigningRequests();
|
||||
await bootstrapCrossSigning({ type: "test" });
|
||||
|
||||
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
|
||||
|
||||
expect(isCrossSigningReady).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,11 @@ import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { MockResponse, MockResponseFunction } from "fetch-mock";
|
||||
|
||||
import type { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
import type { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
createClient,
|
||||
IClaimOTKsResult,
|
||||
@@ -43,13 +43,14 @@ import {
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomStateEvent,
|
||||
} from "../../src/matrix";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder, SyncResponder } from "../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../src/utils";
|
||||
import { downloadDeviceToJsDevice } from "../../src/rust-crypto/device-converter";
|
||||
import { flushPromises } from "../test-utils/flushPromises";
|
||||
} from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
import { E2EKeyReceiver, IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { ISyncResponder, SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import { escapeRegExp } from "../../../src/utils";
|
||||
import { downloadDeviceToJsDevice } from "../../../src/rust-crypto/device-converter";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -419,12 +420,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
|
||||
async function startClientAndAwaitFirstSync(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(aliceClient.getUserId() + ": starting");
|
||||
|
||||
const homeserverUrl = aliceClient.getHomeserverUrl();
|
||||
fetchMock.get(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] });
|
||||
fetchMock.get(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.post(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
|
||||
// we let the client do a very basic initial sync, which it needs before
|
||||
// it will upload one-time keys.
|
||||
@@ -16,12 +16,12 @@ limitations under the License.
|
||||
|
||||
import { Account } from "@matrix-org/olm";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { decodeRecoveryKey } from "../../src/crypto/recoverykey";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../src/crypto/keybackup";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IEvent } from "../../src";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { decodeRecoveryKey } from "../../../src/crypto/recoverykey";
|
||||
import { IKeyBackupInfo, IKeyBackupSession } from "../../../src/crypto/keybackup";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { IEvent } from "../../../src";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
|
||||
const ROOM_ID = "!ROOM:ID";
|
||||
|
||||
@@ -26,16 +26,16 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import "../olm-loader";
|
||||
import "../../olm-loader";
|
||||
|
||||
import type { Session } from "@matrix-org/olm";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../src/@types/crypto";
|
||||
import { logger } from "../../src/logger";
|
||||
import * as testUtils from "../test-utils/test-utils";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
import type { IDeviceKeys, IOneTimeKey } from "../../../src/@types/crypto";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as testUtils from "../../test-utils/test-utils";
|
||||
import { TestClient } from "../../TestClient";
|
||||
import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../../src/client";
|
||||
import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../../src/matrix";
|
||||
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
|
||||
|
||||
let aliTestClient: TestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
|
||||
import { createClient } from "../../src";
|
||||
import { createClient } from "../../../src";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
Copyright 2023 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 fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
|
||||
import { createClient, MatrixClient } from "../../../src";
|
||||
import { ShowQrCodeCallbacks, ShowSasCallbacks, Verifier, VerifierEvent } from "../../../src/crypto-api/verification";
|
||||
import { escapeRegExp } from "../../../src/utils";
|
||||
import { CRYPTO_BACKENDS, InitCrypto } from "../../test-utils/test-utils";
|
||||
import { SyncResponder } from "../../test-utils/SyncResponder";
|
||||
import {
|
||||
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
|
||||
SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
SIGNED_TEST_DEVICE_DATA,
|
||||
TEST_DEVICE_ID,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
TEST_USER_ID,
|
||||
} from "../../test-utils/test-data";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
import {
|
||||
Phase,
|
||||
VerificationRequest,
|
||||
VerificationRequestEvent,
|
||||
} from "../../../src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
jest.useFakeTimers();
|
||||
|
||||
let previousCrypto: Crypto | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
// Stub out global.crypto
|
||||
previousCrypto = global["crypto"];
|
||||
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: {
|
||||
getRandomValues: function <T extends Uint8Array>(array: T): T {
|
||||
array.fill(0x12);
|
||||
return array;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// restore the original global.crypto
|
||||
afterAll(() => {
|
||||
if (previousCrypto === undefined) {
|
||||
// @ts-ignore deleting a non-optional property. It *is* optional really.
|
||||
delete global.crypto;
|
||||
} else {
|
||||
Object.defineProperty(global, "crypto", {
|
||||
value: previousCrypto,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for verification functionality.
|
||||
*
|
||||
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
|
||||
* to provide the most effective integration tests possible.
|
||||
*/
|
||||
describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => {
|
||||
// oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the
|
||||
// Rust backend. Once we have full support in the rust sdk, it will go away.
|
||||
const oldBackendOnly = backend === "rust-sdk" ? test.skip : test;
|
||||
|
||||
/** the client under test */
|
||||
let aliceClient: MatrixClient;
|
||||
|
||||
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
|
||||
let syncResponder: SyncResponder;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = false;
|
||||
|
||||
const homeserverUrl = "https://alice-server.com";
|
||||
aliceClient = createClient({
|
||||
baseUrl: homeserverUrl,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: "device_under_test",
|
||||
});
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
aliceClient.startClient();
|
||||
});
|
||||
|
||||
oldBackendOnly("Outgoing verification: can verify another device via SAS", async () => {
|
||||
// expect requests to download our own keys
|
||||
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||
device_keys: {
|
||||
[TEST_USER_ID]: {
|
||||
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
let [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
|
||||
]);
|
||||
const transactionId = request.transactionId;
|
||||
expect(transactionId).toBeDefined();
|
||||
expect(request.phase).toEqual(Phase.Requested);
|
||||
expect(request.roomId).toBeUndefined();
|
||||
|
||||
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.sas.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready...
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.ready",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.sas.v1"],
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(Phase.Ready);
|
||||
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);
|
||||
|
||||
// ... and picks a method with m.key.verification.start
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
method: "m.sas.v1",
|
||||
transaction_id: transactionId,
|
||||
hashes: ["sha256"],
|
||||
key_agreement_protocols: ["curve25519"],
|
||||
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
|
||||
short_authentication_string: ["emoji"],
|
||||
},
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(Phase.Started);
|
||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||
|
||||
// there should now be a verifier
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const verificationPromise = verifier.verify();
|
||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||
jest.advanceTimersByTime(10);
|
||||
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519");
|
||||
expect(toDeviceMessage.short_authentication_string).toEqual(["emoji"]);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// 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();
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.key",
|
||||
content: {
|
||||
transaction_id: transactionId,
|
||||
key: olmSAS.get_pubkey(),
|
||||
},
|
||||
});
|
||||
|
||||
// alice responds with a 'key' ...
|
||||
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
const aliceDevicePubKeyBase64 = toDeviceMessage.key;
|
||||
olmSAS.set_their_key(aliceDevicePubKeyBase64);
|
||||
|
||||
// ... and the client is notified to show the emoji
|
||||
const showSas = await new Promise<ShowSasCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowSas, resolve);
|
||||
});
|
||||
|
||||
// `getShowSasCallbacks` is an alternative way to get the callbacks
|
||||
expect(verifier.getShowSasCallbacks()).toBe(showSas);
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// user confirms that the emoji match, and alice sends a 'mac'
|
||||
[requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]);
|
||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// the dummy device also confirms that the emoji match, and sends a mac
|
||||
const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`;
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.mac",
|
||||
content: {
|
||||
keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`),
|
||||
transaction_id: transactionId,
|
||||
mac: {
|
||||
[`ed25519:${TEST_DEVICE_ID}`]: calculateMAC(
|
||||
olmSAS,
|
||||
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
|
||||
`${macInfoBase}ed25519:${TEST_DEVICE_ID}`,
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(Phase.Done);
|
||||
|
||||
// we're done with the temporary keypair
|
||||
olmSAS.free();
|
||||
});
|
||||
|
||||
oldBackendOnly(
|
||||
"Outgoing verification: can verify another device via QR code with an untrusted cross-signing key",
|
||||
async () => {
|
||||
// expect requests to download our own keys
|
||||
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
|
||||
device_keys: {
|
||||
[TEST_USER_ID]: {
|
||||
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
|
||||
},
|
||||
},
|
||||
...SIGNED_CROSS_SIGNING_KEYS_DATA,
|
||||
});
|
||||
|
||||
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
|
||||
//
|
||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||
|
||||
// have alice initiate a verification. She should send a m.key.verification.request
|
||||
const [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
aliceClient.requestVerification(TEST_USER_ID, [TEST_DEVICE_ID]),
|
||||
]);
|
||||
const transactionId = request.transactionId;
|
||||
|
||||
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||
expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1");
|
||||
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
|
||||
expect(toDeviceMessage.methods).toContain("m.reciprocate.v1");
|
||||
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
|
||||
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
|
||||
|
||||
// The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.ready",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
methods: ["m.qr_code.scan.v1"],
|
||||
transaction_id: transactionId,
|
||||
},
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(Phase.Ready);
|
||||
|
||||
// we should now have QR data we can display
|
||||
const qrCodeBuffer = request.getQRCodeBytes()!;
|
||||
expect(qrCodeBuffer).toBeTruthy();
|
||||
|
||||
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
|
||||
expect(qrCodeBuffer.subarray(0, 6).toString("latin1")).toEqual("MATRIX");
|
||||
expect(qrCodeBuffer.readUint8(6)).toEqual(0x02); // version
|
||||
expect(qrCodeBuffer.readUint8(7)).toEqual(0x02); // mode
|
||||
const txnIdLen = qrCodeBuffer.readUint16BE(8);
|
||||
expect(qrCodeBuffer.subarray(10, 10 + txnIdLen).toString("utf-8")).toEqual(transactionId);
|
||||
// Alice's device's public key comes next, but we have nothing to do with it here.
|
||||
// const aliceDevicePubKey = qrCodeBuffer.subarray(10 + txnIdLen, 32 + 10 + txnIdLen);
|
||||
expect(qrCodeBuffer.subarray(42 + txnIdLen, 32 + 42 + txnIdLen)).toEqual(
|
||||
Buffer.from(MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64, "base64"),
|
||||
);
|
||||
const sharedSecret = qrCodeBuffer.subarray(74 + txnIdLen);
|
||||
|
||||
// the dummy device "scans" the displayed QR code and acknowledges it with a "m.key.verification.start"
|
||||
returnToDeviceMessageFromSync({
|
||||
type: "m.key.verification.start",
|
||||
content: {
|
||||
from_device: TEST_DEVICE_ID,
|
||||
method: "m.reciprocate.v1",
|
||||
transaction_id: transactionId,
|
||||
secret: encodeUnpaddedBase64(sharedSecret),
|
||||
},
|
||||
});
|
||||
await waitForVerificationRequestChanged(request);
|
||||
expect(request.phase).toEqual(Phase.Started);
|
||||
expect(request.chosenMethod).toEqual("m.reciprocate.v1");
|
||||
|
||||
// there should now be a verifier
|
||||
const verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull();
|
||||
|
||||
// ... which we call .verify on, which emits a ShowReciprocateQr event
|
||||
const verificationPromise = verifier.verify();
|
||||
const reciprocateQRCodeCallbacks = await new Promise<ShowQrCodeCallbacks>((resolve) => {
|
||||
verifier.once(VerifierEvent.ShowReciprocateQr, resolve);
|
||||
});
|
||||
|
||||
// getReciprocateQrCodeCallbacks() is an alternative way to get the callbacks
|
||||
expect(verifier.getReciprocateQrCodeCallbacks()).toBe(reciprocateQRCodeCallbacks);
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
|
||||
// Alice confirms she is happy
|
||||
reciprocateQRCodeCallbacks.confirm();
|
||||
|
||||
// that should satisfy Alice, who should reply with a 'done'
|
||||
await expectSendToDeviceMessage("m.key.verification.done");
|
||||
|
||||
// ... and the whole thing should be done!
|
||||
await verificationPromise;
|
||||
expect(request.phase).toEqual(Phase.Done);
|
||||
},
|
||||
);
|
||||
|
||||
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||
ev.sender ??= TEST_USER_ID;
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Wait for the client under test to send a to-device message of the given type.
|
||||
*
|
||||
* @param msgtype - type of to-device message we expect
|
||||
* @returns A Promise which resolves with the body of the HTTP request
|
||||
*/
|
||||
function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }> {
|
||||
return new Promise((resolve) => {
|
||||
fetchMock.putOnce(
|
||||
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp(msgtype)}`),
|
||||
(url: string, opts: RequestInit): MockResponse => {
|
||||
resolve(JSON.parse(opts.body as string));
|
||||
return {};
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** wait for the verification request to emit a 'Change' event */
|
||||
function waitForVerificationRequestChanged(request: VerificationRequest): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
request.once(VerificationRequestEvent.Change, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/** Perform a MAC calculation on the given data
|
||||
*
|
||||
* Does an HKDR and HMAC as defined by the matrix spec (https://spec.matrix.org/v1.7/client-server-api/#mac-calculation,
|
||||
* as amended by https://github.com/matrix-org/matrix-spec/issues/1553).
|
||||
*
|
||||
* @param olmSAS
|
||||
* @param input
|
||||
* @param info
|
||||
*/
|
||||
function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
|
||||
const mac = olmSAS.calculate_mac_fixed_base64(input, info);
|
||||
//console.info(`Test MAC: input:'${input}, info: '${info}' -> '${mac}`);
|
||||
return mac;
|
||||
}
|
||||
|
||||
function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
|
||||
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
|
||||
}
|
||||
@@ -1274,6 +1274,7 @@ describe("MatrixClient event timelines", function () {
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD_REPLY.event_id,
|
||||
THREAD_REPLY2.getId(),
|
||||
THREAD_ROOT_REACTION.getId(),
|
||||
THREAD_REPLY3.getId(),
|
||||
]);
|
||||
});
|
||||
@@ -1322,7 +1323,7 @@ describe("MatrixClient event timelines", function () {
|
||||
request.respond(200, function () {
|
||||
return {
|
||||
original_event: root,
|
||||
chunk: [replies],
|
||||
chunk: replies,
|
||||
// no next batch as this is the oldest end of the timeline
|
||||
};
|
||||
});
|
||||
@@ -1452,7 +1453,7 @@ describe("MatrixClient event timelines", function () {
|
||||
expect(room.getPendingEvents()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should handle thread updates by reordering the thread list", async () => {
|
||||
it("should handle new thread replies by reordering the thread list", async () => {
|
||||
// Test data for a second thread
|
||||
const THREAD2_ROOT = utils.mkEvent({
|
||||
room: roomId,
|
||||
@@ -1479,7 +1480,7 @@ describe("MatrixClient event timelines", function () {
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply",
|
||||
"body": "thread2 reply",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
@@ -1499,7 +1500,7 @@ describe("MatrixClient event timelines", function () {
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread reply",
|
||||
"body": "thread reply2",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
@@ -1571,6 +1572,7 @@ describe("MatrixClient event timelines", function () {
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY2]);
|
||||
await httpBackend.flushAllExpected();
|
||||
@@ -1582,6 +1584,134 @@ describe("MatrixClient event timelines", function () {
|
||||
THREAD_ROOT.event_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not reorder the thread list on other thread updates", async () => {
|
||||
// Test data for a second thread
|
||||
const THREAD2_ROOT = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "thread root",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
"io.element.thread": {
|
||||
//"latest_event": undefined,
|
||||
count: 1,
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
const THREAD2_REPLY = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"body": "thread2 reply",
|
||||
"msgtype": "m.text",
|
||||
"m.relates_to": {
|
||||
// We can't use the const here because we change server support mode for test
|
||||
rel_type: "io.element.thread",
|
||||
event_id: THREAD_ROOT.event_id,
|
||||
},
|
||||
},
|
||||
event: false,
|
||||
});
|
||||
|
||||
// @ts-ignore we know this is a defined path for THREAD ROOT
|
||||
THREAD2_ROOT.unsigned["m.relations"]["io.element.thread"].latest_event = THREAD2_REPLY;
|
||||
|
||||
const THREAD_REPLY_REACTION = utils.mkEvent({
|
||||
room: roomId,
|
||||
user: userId,
|
||||
type: "m.reaction",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: THREAD_REPLY.event_id,
|
||||
key: "🪿",
|
||||
},
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
THREAD_REPLY_REACTION.localTimestamp += 1000;
|
||||
|
||||
// Modified thread root event containing latest thread reply in its unsigned
|
||||
const THREAD_ROOT_UPDATED = {
|
||||
...THREAD_ROOT,
|
||||
unsigned: {
|
||||
...THREAD_ROOT.unsigned,
|
||||
"m.relations": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"],
|
||||
"io.element.thread": {
|
||||
...THREAD_ROOT.unsigned!["m.relations"]!["io.element.thread"],
|
||||
count: 2,
|
||||
latest_event: THREAD_REPLY,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Response with test data for the thread list request
|
||||
const threadsResponse = {
|
||||
chunk: [THREAD2_ROOT, THREAD_ROOT],
|
||||
state: [],
|
||||
next_batch: RANDOM_TOKEN as string | null,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
client.clientOpts.threadSupport = true;
|
||||
Thread.setServerSideSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideListSupport(FeatureSupport.Stable);
|
||||
Thread.setServerSideFwdPaginationSupport(FeatureSupport.Stable);
|
||||
|
||||
await client.stopClient(); // we don't need the client to be syncing at this time
|
||||
const room = client.getRoom(roomId)!;
|
||||
|
||||
// Set up room threads
|
||||
const timelineSets = await room!.createThreadsTimelineSets();
|
||||
expect(timelineSets).not.toBeNull();
|
||||
respondToThreads(threadsResponse);
|
||||
respondToThreads(threadsResponse);
|
||||
respondToEvent(THREAD_ROOT);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
respondToThread(THREAD_ROOT, [THREAD_REPLY]);
|
||||
respondToThread(THREAD2_ROOT, [THREAD2_REPLY]);
|
||||
await flushHttp(room.fetchRoomThreads());
|
||||
const threadIds = room.getThreads().map((thread) => thread.id);
|
||||
expect(threadIds).toContain(THREAD_ROOT.event_id);
|
||||
expect(threadIds).toContain(THREAD2_ROOT.event_id);
|
||||
const [allThreads] = timelineSets!;
|
||||
const timeline = allThreads.getLiveTimeline()!;
|
||||
// Test threads are in chronological order
|
||||
expect(timeline.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD2_ROOT.event_id,
|
||||
]);
|
||||
|
||||
// Test adding a second event to the first thread
|
||||
const thread = room.getThread(THREAD_ROOT.event_id!)!;
|
||||
thread.initialEventsFetched = true;
|
||||
const prom = emitPromise(room, ThreadEvent.Update);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD_ROOT_UPDATED);
|
||||
respondToEvent(THREAD2_ROOT);
|
||||
await room.addLiveEvents([THREAD_REPLY_REACTION]);
|
||||
await httpBackend.flushAllExpected();
|
||||
await prom;
|
||||
expect(thread.length).toBe(2);
|
||||
// Test thread order is unchanged
|
||||
expect(timeline!.getEvents().map((it) => it.event.event_id)).toEqual([
|
||||
THREAD_ROOT.event_id,
|
||||
THREAD2_ROOT.event_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without server compatibility", function () {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { fail } from "assert";
|
||||
|
||||
import { SlidingSync, SlidingSyncEvent, MSC3575RoomData, SlidingSyncState, Extension } from "../../src/sliding-sync";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { IRoomEvent, IStateEvent } from "../../src/sync-accumulator";
|
||||
import { IRoomEvent, IStateEvent } from "../../src";
|
||||
import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { IStoredClientOpts } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2023 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 fetchMock from "fetch-mock-jest";
|
||||
|
||||
/**
|
||||
* Mock out the endpoints that the js-sdk calls when we call `MatrixClient.start()`.
|
||||
*
|
||||
* @param homeserverUrl - the homeserver url for the client under test
|
||||
*/
|
||||
export function mockInitialApiRequests(homeserverUrl: string) {
|
||||
fetchMock.getOnce(new URL("/_matrix/client/versions", homeserverUrl).toString(), { versions: ["r0.5.0"] });
|
||||
fetchMock.getOnce(new URL("/_matrix/client/r0/pushrules/", homeserverUrl).toString(), {});
|
||||
fetchMock.postOnce(new URL("/_matrix/client/r0/user/%40alice%3Alocalhost/filter", homeserverUrl).toString(), {
|
||||
filter_id: "fid",
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/env
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
#!/bin/env python
|
||||
#
|
||||
# Copyright 2023 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.
|
||||
|
||||
"""
|
||||
This file is a Python script to generate test data for crypto tests.
|
||||
|
||||
To run it:
|
||||
|
||||
python -m venv env
|
||||
./env/bin/pip install cryptography canonicaljson
|
||||
./env/bin/python generate-test-data.py > index.ts
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
|
||||
# input data
|
||||
TEST_USER_ID = "@alice:localhost"
|
||||
TEST_DEVICE_ID = "test_device"
|
||||
# any 32-byte string can be an ed25519 private key.
|
||||
TEST_DEVICE_PRIVATE_KEY_BYTES = b"deadbeefdeadbeefdeadbeefdeadbeef"
|
||||
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"doyouspeakwhaaaaaaaaaaaaaaaaaale"
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"useruseruseruseruseruseruseruser"
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES = b"selfselfselfselfselfselfselfself"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
TEST_DEVICE_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_public_key = encode_base64(
|
||||
private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
device_data = {
|
||||
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
"device_id": TEST_DEVICE_ID,
|
||||
"keys": {
|
||||
f"curve25519:{TEST_DEVICE_ID}": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
f"ed25519:{TEST_DEVICE_ID}": b64_public_key,
|
||||
},
|
||||
"signatures": {TEST_USER_ID: {}},
|
||||
"user_id": TEST_USER_ID,
|
||||
}
|
||||
|
||||
device_data["signatures"][TEST_USER_ID][f"ed25519:{TEST_DEVICE_ID}"] = sign_json(
|
||||
device_data, private_key
|
||||
)
|
||||
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
|
||||
print(
|
||||
f"""\
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import {{ IDeviceKeys }} from "../../../src/@types/crypto";
|
||||
import {{ IDownloadKeyResult }} from "../../../src";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
export const TEST_USER_ID = "{TEST_USER_ID}";
|
||||
export const TEST_DEVICE_ID = "{TEST_DEVICE_ID}";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "{b64_public_key}";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {json.dumps(device_data, indent=4)};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "{b64_master_public_key}";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
json.dumps(build_cross_signing_keys_data(), indent=4)
|
||||
};
|
||||
""",
|
||||
end="",
|
||||
)
|
||||
|
||||
|
||||
def build_cross_signing_keys_data() -> dict:
|
||||
"""Build the signed cross-signing-keys data for return from /keys/query"""
|
||||
master_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
MASTER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_master_public_key = encode_base64(
|
||||
master_private_key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
|
||||
)
|
||||
self_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
SELF_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_self_signing_public_key = encode_base64(
|
||||
self_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
user_signing_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(
|
||||
USER_CROSS_SIGNING_PRIVATE_KEY_BYTES
|
||||
)
|
||||
b64_user_signing_public_key = encode_base64(
|
||||
user_signing_private_key.public_key().public_bytes(
|
||||
Encoding.Raw, PublicFormat.Raw
|
||||
)
|
||||
)
|
||||
# create without signatures initially
|
||||
cross_signing_keys_data = {
|
||||
"master_keys": {
|
||||
TEST_USER_ID: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_master_public_key}": b64_master_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"usage": ["master"],
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
TEST_USER_ID: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_self_signing_public_key}": b64_self_signing_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"usage": ["self_signing"],
|
||||
},
|
||||
},
|
||||
"user_signing_keys": {
|
||||
TEST_USER_ID: {
|
||||
"keys": {
|
||||
f"ed25519:{b64_user_signing_public_key}": b64_user_signing_public_key,
|
||||
},
|
||||
"user_id": TEST_USER_ID,
|
||||
"usage": ["user_signing"],
|
||||
},
|
||||
},
|
||||
}
|
||||
# sign the sub-keys with the master
|
||||
for k in ["self_signing_keys", "user_signing_keys"]:
|
||||
to_sign = cross_signing_keys_data[k][TEST_USER_ID]
|
||||
sig = sign_json(to_sign, master_private_key)
|
||||
to_sign["signatures"] = {
|
||||
TEST_USER_ID: {f"ed25519:{b64_master_public_key}": sig}
|
||||
}
|
||||
|
||||
return cross_signing_keys_data
|
||||
|
||||
|
||||
def encode_base64(input_bytes: bytes) -> str:
|
||||
"""Encode with unpadded base64"""
|
||||
output_bytes = base64.b64encode(input_bytes)
|
||||
output_string = output_bytes.decode("ascii")
|
||||
return output_string.rstrip("=")
|
||||
|
||||
|
||||
def sign_json(json_object: dict, private_key: ed25519.Ed25519PrivateKey) -> str:
|
||||
"""
|
||||
Sign the given json object
|
||||
|
||||
Returns the base64-encoded signature of signing `input` following the Matrix
|
||||
JSON signature algorithm [1]
|
||||
|
||||
[1]: https://spec.matrix.org/v1.7/appendices/#signing-details
|
||||
"""
|
||||
signatures = json_object.pop("signatures", {})
|
||||
unsigned = json_object.pop("unsigned", None)
|
||||
|
||||
signature = private_key.sign(encode_canonical_json(json_object))
|
||||
signature_base64 = encode_base64(signature)
|
||||
|
||||
json_object["signatures"] = signatures
|
||||
if unsigned is not None:
|
||||
json_object["unsigned"] = unsigned
|
||||
|
||||
return signature_base64
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,84 @@
|
||||
/* Test data for cryptography tests
|
||||
*
|
||||
* Do not edit by hand! This file is generated by `./generate-test-data.py`
|
||||
*/
|
||||
|
||||
import { IDeviceKeys } from "../../../src/@types/crypto";
|
||||
import { IDownloadKeyResult } from "../../../src";
|
||||
|
||||
/* eslint-disable comma-dangle */
|
||||
|
||||
export const TEST_USER_ID = "@alice:localhost";
|
||||
export const TEST_DEVICE_ID = "test_device";
|
||||
|
||||
/** The base64-encoded public ed25519 key for this device */
|
||||
export const TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64 = "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU";
|
||||
|
||||
/** Signed device data, suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_TEST_DEVICE_DATA: IDeviceKeys = {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "test_device",
|
||||
"keys": {
|
||||
"curve25519:test_device": "F4uCNNlcbRvc7CfBz95ZGWBvY1ALniG1J8+6rhVoKS0",
|
||||
"ed25519:test_device": "YI/7vbGVLpGdYtuceQR8MSsKB/QjgfMXM1xqnn+0NWU"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:test_device": "LmQC/yAUZJmkxZ+3L0nEwvtVWOzjqQqADWBhk+C47SPaFYHeV+E291mgXaSCJVeGltX+HC49Aw7nb6ga7sw0Aw"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** base64-encoded public master cross-signing key */
|
||||
export const MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64 = "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY";
|
||||
|
||||
/** Signed cross-signing keys data, also suitable for returning from a `/keys/query` call */
|
||||
export const SIGNED_CROSS_SIGNING_KEYS_DATA: Partial<IDownloadKeyResult> = {
|
||||
"master_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY": "aU2+2CyXQTCuDcmWW0EL2bhJ6PdjFW2LbAsbHqf02AY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "XfhYEhZmOs8BJdb3viatILBZ/bElsHXEW28V4tIaY5CxrBR0YOym3yZHWmRmypXessHZAKOhZn3yBMXzdajyCw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@alice:localhost": {
|
||||
"keys": {
|
||||
"ed25519:g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY": "g5TC/zjQXyZYuDLZv7a41z5fFVrXpYPypG//AFQj8hY"
|
||||
},
|
||||
"user_id": "@alice:localhost",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"signatures": {
|
||||
"@alice:localhost": {
|
||||
"ed25519:J+5An10v1vzZpAXTYFokD1/PEVccFnLC61EfRXit0UY": "6AkD1XM2H0/ebgP9oBdMKNeft7uxsrb0XN1CsjjHgeZCvCTMmv3BHlLiT/Hzy4fe8H+S1tr484dcXN/PIdnfDA"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -745,3 +745,78 @@ export const REMOTE_SFU_DESCRIPTION =
|
||||
"a=sctp-port:5000\n" +
|
||||
"a=ice-ufrag:obZwzAcRtxwuozPZ\n" +
|
||||
"a=ice-pwd:TWXNaPeyKTTvRLyIQhWHfHlZHJjtcoKs";
|
||||
|
||||
export const groupCallParticipantsFourOtherDevices = new Map([
|
||||
[
|
||||
new RoomMember("roomId0", "user1"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId0",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId1",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId2",
|
||||
{
|
||||
sessionId: "2",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
new RoomMember("roomId0", "user2"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId3",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"deviceId4",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
export const groupCallParticipantsOneOtherDevice = new Map([
|
||||
[
|
||||
new RoomMember("roomId1", "thisMember"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId0",
|
||||
{
|
||||
sessionId: "0",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
new RoomMember("roomId1", "opponentMember"),
|
||||
new Map([
|
||||
[
|
||||
"deviceId1",
|
||||
{
|
||||
sessionId: "1",
|
||||
screensharing: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ limitations under the License.
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
import { OidcDiscoveryError } from "../../src/oidc/validate";
|
||||
|
||||
describe("AutoDiscovery", function () {
|
||||
const getHttpBackend = (): MockHttpBackend => {
|
||||
@@ -368,7 +369,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " + "m.homeserver", function () {
|
||||
it("should return SUCCESS when .well-known has a verifiably accurate base_url for m.homeserver", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -397,6 +398,10 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -434,6 +439,54 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS with authentication error when authentication config is invalid", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://chat.example.org/_matrix/client/versions");
|
||||
})
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
base_url: "https://chat.example.org/",
|
||||
},
|
||||
"m.authentication": {
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
AutoDiscovery.findClientConfig("example.org").then((conf) => {
|
||||
const expected = {
|
||||
"m.homeserver": {
|
||||
state: "SUCCESS",
|
||||
error: null,
|
||||
base_url: "https://chat.example.org",
|
||||
},
|
||||
"m.identity_server": {
|
||||
state: "PROMPT",
|
||||
error: null,
|
||||
base_url: null,
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "FAIL_ERROR",
|
||||
error: OidcDiscoveryError.Misconfigured,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -625,7 +678,7 @@ describe("AutoDiscovery", function () {
|
||||
},
|
||||
);
|
||||
|
||||
it("should return SUCCESS when the identity server configuration is " + "verifiably accurate", function () {
|
||||
it("should return SUCCESS when the identity server configuration is verifiably accurate", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -664,6 +717,10 @@ describe("AutoDiscovery", function () {
|
||||
error: null,
|
||||
base_url: "https://identity.example.org",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
@@ -671,7 +728,7 @@ describe("AutoDiscovery", function () {
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return SUCCESS and preserve non-standard keys from the " + ".well-known response", function () {
|
||||
it("should return SUCCESS and preserve non-standard keys from the .well-known response", function () {
|
||||
const httpBackend = getHttpBackend();
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/client/versions")
|
||||
@@ -716,6 +773,10 @@ describe("AutoDiscovery", function () {
|
||||
"org.example.custom.property": {
|
||||
cupcakes: "yes",
|
||||
},
|
||||
"m.authentication": {
|
||||
state: "IGNORE",
|
||||
error: OidcDiscoveryError.NotSupported,
|
||||
},
|
||||
};
|
||||
|
||||
expect(conf).toEqual(expected);
|
||||
|
||||
@@ -142,13 +142,6 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
describe("addEventToTimeline", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("Adds event to timeline", () => {
|
||||
const liveTimeline = eventTimelineSet.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -167,6 +160,15 @@ describe("EventTimelineSet", () => {
|
||||
eventTimelineSet.addEventToTimeline(messageEvent, liveTimeline, true, false);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventToTimeline (thread timeline)", () => {
|
||||
let thread: Thread;
|
||||
|
||||
beforeEach(() => {
|
||||
(client.supportsThreads as jest.Mock).mockReturnValue(true);
|
||||
thread = new Thread("!thread_id:server", messageEvent, { room, client });
|
||||
});
|
||||
|
||||
it("should not add an event to a timeline that does not belong to the timelineSet", () => {
|
||||
const eventTimelineSet2 = new EventTimelineSet(room);
|
||||
@@ -197,7 +199,14 @@ describe("EventTimelineSet", () => {
|
||||
const liveTimeline = eventTimelineSetForThread.getLiveTimeline();
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(messageEvent, liveTimeline, {
|
||||
const normalMessage = utils.mkMessage({
|
||||
room: roomId,
|
||||
user: userA,
|
||||
msg: "Hello!",
|
||||
event: true,
|
||||
});
|
||||
|
||||
eventTimelineSetForThread.addEventToTimeline(normalMessage, liveTimeline, {
|
||||
toStartOfTimeline: true,
|
||||
});
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
@@ -336,7 +345,9 @@ describe("EventTimelineSet", () => {
|
||||
});
|
||||
|
||||
it("should return true if the timeline set is not for a thread and the event is a thread root", () => {
|
||||
const thread = new Thread(messageEvent.getId()!, messageEvent, { room, client });
|
||||
const eventTimelineSet = new EventTimelineSet(room, {}, client);
|
||||
messageEvent.setThread(thread);
|
||||
expect(eventTimelineSet.canContain(messageEvent)).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { FetchHttpApi } from "../../../src/http-api/fetch";
|
||||
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
|
||||
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { QueryDict } from "../../../src/utils";
|
||||
|
||||
describe("FetchHttpApi", () => {
|
||||
const baseUrl = "http://baseUrl";
|
||||
@@ -235,4 +236,58 @@ describe("FetchHttpApi", () => {
|
||||
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUrl()", () => {
|
||||
const localBaseUrl = "http://baseurl";
|
||||
const baseUrlWithTrailingSlash = "http://baseurl/";
|
||||
const makeApi = (thisBaseUrl = baseUrl): FetchHttpApi<any> => {
|
||||
const fetchFn = jest.fn();
|
||||
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
return new FetchHttpApi(emitter, { baseUrl: thisBaseUrl, prefix, fetchFn });
|
||||
};
|
||||
|
||||
type TestParams = {
|
||||
path: string;
|
||||
queryParams?: QueryDict;
|
||||
prefix?: string;
|
||||
baseUrl?: string;
|
||||
};
|
||||
type TestCase = [TestParams, string];
|
||||
const queryParams: QueryDict = {
|
||||
test1: 99,
|
||||
test2: ["a", "b"],
|
||||
};
|
||||
const testPrefix = "/just/testing";
|
||||
const testUrl = "http://justtesting.com";
|
||||
const testUrlWithTrailingSlash = "http://justtesting.com/";
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
[{ path: "/terms" }, `${localBaseUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", queryParams }, `${localBaseUrl}${prefix}/terms?test1=99&test2=a&test2=b`],
|
||||
[{ path: "/terms", prefix: testPrefix }, `${localBaseUrl}${testPrefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrl }, `${testUrl}${prefix}/terms`],
|
||||
[{ path: "/terms", baseUrl: testUrlWithTrailingSlash }, `${testUrl}${prefix}/terms`],
|
||||
[
|
||||
{ path: "/terms", queryParams, prefix: testPrefix, baseUrl: testUrl },
|
||||
`${testUrl}${testPrefix}/terms?test1=99&test2=a&test2=b`,
|
||||
],
|
||||
];
|
||||
const runTests = (fetchBaseUrl: string) => {
|
||||
it.each<TestCase>(testCases)(
|
||||
"creates url with params %s",
|
||||
({ path, queryParams, prefix, baseUrl }, result) => {
|
||||
const api = makeApi(fetchBaseUrl);
|
||||
|
||||
expect(api.getUrl(path, queryParams, prefix, baseUrl)).toEqual(new URL(result));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
describe("when fetch.opts.baseUrl does not have a trailing slash", () => {
|
||||
runTests(localBaseUrl);
|
||||
});
|
||||
describe("when fetch.opts.baseUrl does have a trailing slash", () => {
|
||||
runTests(baseUrlWithTrailingSlash);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { ClientPrefix, MatrixClient } from "../../src";
|
||||
import { SSOAction } from "../../src/@types/auth";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
function createExampleMatrixClient(): MatrixClient {
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://example.com",
|
||||
});
|
||||
}
|
||||
|
||||
describe("Login request", function () {
|
||||
let client: TestClient;
|
||||
|
||||
@@ -57,3 +66,84 @@ describe("SSO login URL", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshToken", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
it("requests the correctly-prefixed /refresh endpoint when server correctly accepts /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
const response = {
|
||||
access_token: "access_token",
|
||||
refresh_token: "refresh_token",
|
||||
expires_in_ms: 30000,
|
||||
};
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), response);
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
|
||||
throw new Error("/v1/refresh unexpectedly called");
|
||||
});
|
||||
|
||||
const refreshResult = await client.refreshToken("initial_refresh_token");
|
||||
expect(refreshResult).toEqual(response);
|
||||
});
|
||||
|
||||
it("falls back to /v1 when server does not recognized /v3 refresh", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
const response = {
|
||||
access_token: "access_token",
|
||||
refresh_token: "refresh_token",
|
||||
expires_in_ms: 30000,
|
||||
};
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), response);
|
||||
|
||||
const refreshResult = await client.refreshToken("initial_refresh_token");
|
||||
expect(refreshResult).toEqual(response);
|
||||
});
|
||||
|
||||
it("re-raises M_UNRECOGNIZED exceptions from /v1", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
|
||||
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ errcode: "M_UNRECOGNIZED" });
|
||||
});
|
||||
|
||||
it("re-raises non-M_UNRECOGNIZED exceptions from /v3", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), 429);
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), () => {
|
||||
throw new Error("/v1/refresh unexpectedly called");
|
||||
});
|
||||
|
||||
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
|
||||
});
|
||||
|
||||
it("re-raises non-M_UNRECOGNIZED exceptions from /v1", async () => {
|
||||
const client = createExampleMatrixClient();
|
||||
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V3).toString(), {
|
||||
status: 400,
|
||||
body: { errcode: "M_UNRECOGNIZED" },
|
||||
});
|
||||
fetchMock.postOnce(client.http.getUrl("/refresh", undefined, ClientPrefix.V1).toString(), 429);
|
||||
|
||||
expect(client.refreshToken("initial_refresh_token")).rejects.toMatchObject({ httpStatus: 429 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
MSC3912_RELATION_BASED_REDACTIONS_PROP,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
@@ -181,9 +180,7 @@ describe("MatrixClient", function () {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
const unstableFeatures: Record<string, boolean> = {
|
||||
"org.matrix.msc3440.stable": true,
|
||||
};
|
||||
let unstableFeatures: Record<string, boolean> = {};
|
||||
|
||||
// items are popped off when processed and block if no items left.
|
||||
let httpLookups: HttpLookup[] = [];
|
||||
@@ -342,6 +339,12 @@ describe("MatrixClient", function () {
|
||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
|
||||
// set unstableFeatures to a defined state before each test
|
||||
unstableFeatures = {
|
||||
"org.matrix.msc3440.stable": true,
|
||||
};
|
||||
|
||||
makeClient();
|
||||
|
||||
// set reasonable working defaults
|
||||
@@ -1373,10 +1376,10 @@ describe("MatrixClient", function () {
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
|
||||
describe("when calling with with_relations", () => {
|
||||
describe("when calling with 'with_rel_types'", () => {
|
||||
const eventId = "$event42:example.org";
|
||||
|
||||
it("should raise an error if server has no support for relation based redactions", async () => {
|
||||
it("should raise an error if the server has no support for relation based redactions", async () => {
|
||||
// load supported features
|
||||
await client.getVersions();
|
||||
|
||||
@@ -1384,7 +1387,7 @@ describe("MatrixClient", function () {
|
||||
|
||||
expect(() => {
|
||||
client.redactEvent(roomId, eventId, txnId, {
|
||||
with_relations: [RelationType.Reference],
|
||||
with_rel_types: [RelationType.Reference],
|
||||
});
|
||||
}).toThrow(
|
||||
new Error(
|
||||
@@ -1394,34 +1397,30 @@ describe("MatrixClient", function () {
|
||||
);
|
||||
});
|
||||
|
||||
describe("and the server supports relation based redactions (unstable)", () => {
|
||||
beforeEach(async () => {
|
||||
unstableFeatures["org.matrix.msc3912"] = true;
|
||||
// load supported features
|
||||
await client.getVersions();
|
||||
});
|
||||
it("and the server has unstable support for relation based redactions, it should send 'org.matrix.msc3912.with_relations' in the request body", async () => {
|
||||
unstableFeatures["org.matrix.msc3912"] = true;
|
||||
// load supported features
|
||||
await client.getVersions();
|
||||
|
||||
it("should send with_relations in the request body", async () => {
|
||||
const txnId = client.makeTxnId();
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PUT",
|
||||
path:
|
||||
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
|
||||
`/${encodeURIComponent(txnId)}`,
|
||||
expectBody: {
|
||||
reason: "redaction test",
|
||||
[MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: [RelationType.Reference],
|
||||
},
|
||||
data: { event_id: eventId },
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PUT",
|
||||
path:
|
||||
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
|
||||
`/${encodeURIComponent(txnId)}`,
|
||||
expectBody: {
|
||||
reason: "redaction test",
|
||||
["org.matrix.msc3912.with_relations"]: ["m.reference"],
|
||||
},
|
||||
];
|
||||
data: { event_id: eventId },
|
||||
},
|
||||
];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId, {
|
||||
reason: "redaction test",
|
||||
with_relations: [RelationType.Reference],
|
||||
});
|
||||
await client.redactEvent(roomId, eventId, txnId, {
|
||||
reason: "redaction test",
|
||||
with_rel_types: [RelationType.Reference],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
Copyright 2023 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 { M_AUTHENTICATION } from "../../../src";
|
||||
import { logger } from "../../../src/logger";
|
||||
import {
|
||||
OidcDiscoveryError,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "../../../src/oidc/validate";
|
||||
|
||||
describe("validateWellKnownAuthentication()", () => {
|
||||
const baseWk = {
|
||||
"m.homeserver": {
|
||||
base_url: "https://hs.org",
|
||||
},
|
||||
};
|
||||
it("should throw not supported error when wellKnown has no m.authentication section", () => {
|
||||
expect(() => validateWellKnownAuthentication(baseWk)).toThrow(OidcDiscoveryError.NotSupported);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication issuer is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: { url: "test.com" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is not a string", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: { url: "test" },
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should throw misconfigured error when authentication account is false", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: false,
|
||||
},
|
||||
};
|
||||
expect(() => validateWellKnownAuthentication(wk)).toThrow(OidcDiscoveryError.Misconfigured);
|
||||
});
|
||||
|
||||
it("should return valid config when wk uses stable m.authentication", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk)).toEqual({
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid config when m.authentication account is missing", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk)).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove unexpected properties", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.stable!]: {
|
||||
issuer: "test.com",
|
||||
somethingElse: "test",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk)).toEqual({
|
||||
issuer: "test.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid config when wk uses unstable prefix for m.authentication", () => {
|
||||
const wk = {
|
||||
...baseWk,
|
||||
[M_AUTHENTICATION.unstable!]: {
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
},
|
||||
};
|
||||
expect(validateWellKnownAuthentication(wk)).toEqual({
|
||||
issuer: "test.com",
|
||||
account: "account.com",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateOIDCIssuerWellKnown", () => {
|
||||
const validWk: any = {
|
||||
authorization_endpoint: "https://test.org/authorize",
|
||||
token_endpoint: "https://authorize.org/token",
|
||||
registration_endpoint: "https://authorize.org/regsiter",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
};
|
||||
beforeEach(() => {
|
||||
// stub to avoid console litter
|
||||
jest.spyOn(logger, "error")
|
||||
.mockClear()
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it("should throw OP support error when wellKnown is not an object", () => {
|
||||
expect(() => {
|
||||
validateOIDCIssuerWellKnown([]);
|
||||
}).toThrow(OidcDiscoveryError.OpSupport);
|
||||
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
||||
});
|
||||
|
||||
it("should log all errors before throwing", () => {
|
||||
expect(() => {
|
||||
validateOIDCIssuerWellKnown({
|
||||
...validWk,
|
||||
authorization_endpoint: undefined,
|
||||
response_types_supported: [],
|
||||
});
|
||||
}).toThrow(OidcDiscoveryError.OpSupport);
|
||||
expect(logger.error).toHaveBeenCalledWith("OIDC issuer configuration: authorization_endpoint is invalid");
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"OIDC issuer configuration: response_types_supported is invalid. code is required.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return validated issuer config", () => {
|
||||
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
|
||||
authorizationEndpoint: validWk.authorization_endpoint,
|
||||
tokenEndpoint: validWk.token_endpoint,
|
||||
registrationEndpoint: validWk.registration_endpoint,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return validated issuer config without registrationendpoint", () => {
|
||||
const wk = { ...validWk };
|
||||
delete wk.registration_endpoint;
|
||||
expect(validateOIDCIssuerWellKnown(wk)).toEqual({
|
||||
authorizationEndpoint: validWk.authorization_endpoint,
|
||||
tokenEndpoint: validWk.token_endpoint,
|
||||
registrationEndpoint: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
type TestCase = [string, any];
|
||||
it.each<TestCase>([
|
||||
["authorization_endpoint", undefined],
|
||||
["authorization_endpoint", { not: "a string" }],
|
||||
["token_endpoint", undefined],
|
||||
["token_endpoint", { not: "a string" }],
|
||||
["registration_endpoint", { not: "a string" }],
|
||||
["response_types_supported", undefined],
|
||||
["response_types_supported", "not an array"],
|
||||
["response_types_supported", ["doesnt include code"]],
|
||||
["grant_types_supported", undefined],
|
||||
["grant_types_supported", "not an array"],
|
||||
["grant_types_supported", ["doesnt include authorization_code"]],
|
||||
["code_challenge_methods_supported", undefined],
|
||||
["code_challenge_methods_supported", "not an array"],
|
||||
["code_challenge_methods_supported", ["doesnt include S256"]],
|
||||
])("should throw OP support error when %s is %s", (key, value) => {
|
||||
const wk = {
|
||||
...validWk,
|
||||
[key]: value,
|
||||
};
|
||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcDiscoveryError.OpSupport);
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,19 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import * as utils from "../test-utils/test-utils";
|
||||
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
|
||||
import { EventType, RoomState } from "../../src";
|
||||
import {
|
||||
createClient,
|
||||
EventType,
|
||||
MatrixClient,
|
||||
RoomState,
|
||||
UNSTABLE_MSC2666_MUTUAL_ROOMS,
|
||||
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
||||
UNSTABLE_MSC2666_SHARED_ROOMS,
|
||||
} from "../../src";
|
||||
|
||||
describe("RoomMember", function () {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -481,3 +491,125 @@ describe("RoomMember", function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MutualRooms", () => {
|
||||
let client: MatrixClient;
|
||||
const HS_URL = "https://example.com";
|
||||
const TEST_USER_ID = "@alice:localhost";
|
||||
const TEST_DEVICE_ID = "xzcvb";
|
||||
const QUERIED_USER = "@user:example.com";
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
fetchMock.config.warnOnFallback = true;
|
||||
|
||||
client = createClient({
|
||||
baseUrl: HS_URL,
|
||||
userId: TEST_USER_ID,
|
||||
accessToken: "akjgkrgjs",
|
||||
deviceId: TEST_DEVICE_ID,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await client.stopClient();
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
function enableFeature(feature: string) {
|
||||
const mapping: Record<string, boolean> = {};
|
||||
|
||||
mapping[feature] = true;
|
||||
|
||||
fetchMock.get(`${HS_URL}/_matrix/client/versions`, {
|
||||
unstable_features: mapping,
|
||||
versions: ["v1.1"],
|
||||
});
|
||||
}
|
||||
|
||||
it("supports the initial MSC version (shared rooms)", async () => {
|
||||
enableFeature(UNSTABLE_MSC2666_SHARED_ROOMS);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/shared_rooms/:user_id", (rawUrl) => {
|
||||
const segments = rawUrl.split("/");
|
||||
const lastSegment = decodeURIComponent(segments[segments.length - 1]);
|
||||
|
||||
expect(lastSegment).toEqual(QUERIED_USER);
|
||||
|
||||
return {
|
||||
joined: ["!test:example.com"],
|
||||
};
|
||||
});
|
||||
|
||||
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
|
||||
|
||||
expect(rooms).toEqual(["!test:example.com"]);
|
||||
});
|
||||
|
||||
it("supports the renaming MSC version (mutual rooms)", async () => {
|
||||
enableFeature(UNSTABLE_MSC2666_MUTUAL_ROOMS);
|
||||
|
||||
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms/:user_id", (rawUrl) => {
|
||||
const segments = rawUrl.split("/");
|
||||
const lastSegment = decodeURIComponent(segments[segments.length - 1]);
|
||||
|
||||
expect(lastSegment).toEqual(QUERIED_USER);
|
||||
|
||||
return {
|
||||
joined: ["!test2:example.com"],
|
||||
};
|
||||
});
|
||||
|
||||
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
|
||||
|
||||
expect(rooms).toEqual(["!test2:example.com"]);
|
||||
});
|
||||
|
||||
describe("can work the latest MSC version (query mutual rooms)", () => {
|
||||
beforeEach(() => {
|
||||
enableFeature(UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS);
|
||||
});
|
||||
|
||||
it("works with a simple response", async () => {
|
||||
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => {
|
||||
const url = new URL(rawUrl);
|
||||
|
||||
expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER);
|
||||
|
||||
return {
|
||||
joined: ["!test3:example.com"],
|
||||
};
|
||||
});
|
||||
|
||||
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
|
||||
|
||||
expect(rooms).toEqual(["!test3:example.com"]);
|
||||
});
|
||||
|
||||
it("works with a paginated response", async () => {
|
||||
fetchMock.get("express:/_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms", (rawUrl) => {
|
||||
const url = new URL(rawUrl);
|
||||
|
||||
expect(url.searchParams.get("user_id")).toEqual(QUERIED_USER);
|
||||
|
||||
const token = url.searchParams.get("batch_token");
|
||||
|
||||
if (token == "yahaha") {
|
||||
return {
|
||||
joined: ["!korok:example.com"],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
joined: ["!rock:example.com"],
|
||||
next_batch_token: "yahaha",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const rooms = await client._unstable_getSharedRooms(QUERIED_USER);
|
||||
|
||||
expect(rooms).toEqual(["!rock:example.com", "!korok:example.com"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
import { ImportRoomKeysOpts } from "../../../src/crypto-api";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
@@ -40,16 +41,59 @@ const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
describe(".exportRoomKeys", () => {
|
||||
describe(".importRoomKeys and .exportRoomKeys", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
rustCrypto = await makeTestRustCrypto();
|
||||
});
|
||||
|
||||
it("should return a list", async () => {
|
||||
it("should import and export keys", async () => {
|
||||
const someRoomKeys = [
|
||||
{
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: "!cLDYAnjpiQXIrSwngM:localhost:8480",
|
||||
sender_key: "C9FMqTD20C0VaGWE/aSImkimuE6HDa/RyYj5gRUg3gY",
|
||||
session_id: "iGQG5GaP1/B3dSH6zCQDQqrNuotrtQjVC7w1OsUDwbg",
|
||||
session_key:
|
||||
"AQAAAADaCbP2gdOy8jrhikjploKgSBaFSJ5rvHcziaADbwNEzeCSrfuAUlXvCvxik8kU+MfCHIi5arN2M7UM5rGKdzkHnkReoIByFkeMdbjKWk5SFpVQexcM74eDhBGj+ICkQqOgApfnEbSswrmreB0+MhHHyLStwW5fy5f8A9QW1sbPuohkBuRmj9fwd3Uh+swkA0KqzbqLa7UI1Qu8NTrFA8G4",
|
||||
sender_claimed_keys: {
|
||||
ed25519: "RSq0Xw0RR0DeqlJ/j3qrF5qbN0D96fKk8lz9kZJlG9k",
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
},
|
||||
{
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: "!cLDYAnjpiQXIrSwngM:localhost:8480",
|
||||
sender_key: "C9FMqTD20C0VaGWE/aSImkimuE6HDa/RyYj5gRUg3gY",
|
||||
session_id: "P/Jy9Tog4CMtLseeS4Fe2AEXZov3k6cibcop/uyhr78",
|
||||
session_key:
|
||||
"AQAAAAATyAVm0c9c9DW9Od72MxvfSDYoysBw3C6yMJ3bYuTmssHN7yNGm59KCtKeFp2Y5qO7lvUmwOfSTvTASUb7HViE7Lt+Bvp5WiMTJ2Pv6m+N12ihyowV5lgtKFWI18Wxd0AugMTVQRwjBK6aMobf86NXWD2hiKm3N6kWbC0PXmqV7T/ycvU6IOAjLS7HnkuBXtgBF2aL95OnIm3KKf7soa+/",
|
||||
sender_claimed_keys: {
|
||||
ed25519: "RSq0Xw0RR0DeqlJ/j3qrF5qbN0D96fKk8lz9kZJlG9k",
|
||||
},
|
||||
forwarding_curve25519_key_chain: [],
|
||||
},
|
||||
];
|
||||
let importTotal = 0;
|
||||
const opt: ImportRoomKeysOpts = {
|
||||
progressCallback: (stage) => {
|
||||
importTotal = stage.total;
|
||||
},
|
||||
};
|
||||
await rustCrypto.importRoomKeys(someRoomKeys, opt);
|
||||
|
||||
expect(importTotal).toBe(2);
|
||||
|
||||
const keys = await rustCrypto.exportRoomKeys();
|
||||
expect(Array.isArray(keys)).toBeTruthy();
|
||||
expect(keys.length).toBe(2);
|
||||
|
||||
const aSession = someRoomKeys[0];
|
||||
|
||||
const exportedKey = keys.find((k) => k.session_id == aSession.session_id);
|
||||
|
||||
expect(aSession).toStrictEqual(exportedKey);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,11 +137,6 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("isCrossSigningReady", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.isCrossSigningReady()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("getCrossSigningKeyId", async () => {
|
||||
const rustCrypto = await makeTestRustCrypto();
|
||||
await expect(rustCrypto.getCrossSigningKeyId()).resolves.toBe(null);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2023 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 { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
|
||||
import { ServerSideSecretStorage } from "../../../src/secret-storage";
|
||||
|
||||
describe("secret-storage", () => {
|
||||
describe("secretStorageContainsCrossSigningKeys", () => {
|
||||
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockReturnValue(false),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false when there is no shared secret storage key between master, user signing and self signing keys", async () => {
|
||||
const secretStorage = {
|
||||
isStored: (type: string) => {
|
||||
// Return different storage keys
|
||||
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
|
||||
else return { secretStorageKey2: {} };
|
||||
},
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false when the secret storage key for the master key is only shared by the user-signing key", async () => {
|
||||
const secretStorage = {
|
||||
isStored: (type: string) => {
|
||||
// Return different storage keys
|
||||
if (type === "m.cross_signing.master" || type === "m.cross_signing.user_signing") {
|
||||
return { secretStorageKey: {} };
|
||||
} else {
|
||||
return { secretStorageKey2: {} };
|
||||
}
|
||||
},
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
|
||||
const secretStorage = {
|
||||
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,25 +18,25 @@ import { mocked } from "jest-mock";
|
||||
|
||||
import { EventType, GroupCallIntent, GroupCallType, MatrixCall, MatrixEvent, Room, RoomMember } from "../../../src";
|
||||
import { RoomStateEvent } from "../../../src/models/room-state";
|
||||
import { GroupCall, GroupCallEvent, GroupCallState } from "../../../src/webrtc/groupCall";
|
||||
import { GroupCall, GroupCallEvent, GroupCallState, GroupCallStatsReportEvent } from "../../../src/webrtc/groupCall";
|
||||
import { IMyDevice, MatrixClient } from "../../../src/client";
|
||||
import {
|
||||
FAKE_CONF_ID,
|
||||
FAKE_DEVICE_ID_1,
|
||||
FAKE_DEVICE_ID_2,
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_SESSION_ID_1,
|
||||
FAKE_SESSION_ID_2,
|
||||
FAKE_USER_ID_1,
|
||||
FAKE_USER_ID_2,
|
||||
FAKE_USER_ID_3,
|
||||
installWebRTCMocks,
|
||||
MockCallFeed,
|
||||
MockCallMatrixClient,
|
||||
MockMatrixCall,
|
||||
MockMediaStream,
|
||||
MockMediaStreamTrack,
|
||||
MockRTCPeerConnection,
|
||||
MockMatrixCall,
|
||||
FAKE_ROOM_ID,
|
||||
FAKE_USER_ID_1,
|
||||
FAKE_CONF_ID,
|
||||
FAKE_DEVICE_ID_2,
|
||||
FAKE_SESSION_ID_2,
|
||||
FAKE_USER_ID_2,
|
||||
FAKE_DEVICE_ID_1,
|
||||
FAKE_SESSION_ID_1,
|
||||
FAKE_USER_ID_3,
|
||||
} from "../../test-utils/webrtc";
|
||||
import { SDPStreamMetadataKey, SDPStreamMetadataPurpose } from "../../../src/webrtc/callEventTypes";
|
||||
import { sleep } from "../../../src/utils";
|
||||
@@ -44,6 +44,9 @@ import { CallEventHandlerEvent } from "../../../src/webrtc/callEventHandler";
|
||||
import { CallFeed } from "../../../src/webrtc/callFeed";
|
||||
import { CallEvent, CallState } from "../../../src/webrtc/call";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { CallFeedReport } from "../../../src/webrtc/stats/statsReport";
|
||||
import { CallFeedStatsReporter } from "../../../src/webrtc/stats/callFeedStatsReporter";
|
||||
import { StatsReportEmitter } from "../../../src/webrtc/stats/statsReportEmitter";
|
||||
|
||||
const FAKE_STATE_EVENTS = [
|
||||
{
|
||||
@@ -1726,4 +1729,89 @@ describe("Group Call", function () {
|
||||
expect(start).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("as stats event listener and a CallFeedReport was triggered", () => {
|
||||
let groupCall: GroupCall;
|
||||
let reportEmitter: StatsReportEmitter;
|
||||
const report: CallFeedReport = {} as CallFeedReport;
|
||||
beforeEach(async () => {
|
||||
CallFeedStatsReporter.expandCallFeedReport = jest.fn().mockReturnValue(report);
|
||||
const typedMockClient = new MockCallMatrixClient(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, FAKE_SESSION_ID_1);
|
||||
const mockClient = typedMockClient.typed();
|
||||
const room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
|
||||
room.currentState.members[FAKE_USER_ID_1] = {
|
||||
userId: FAKE_USER_ID_1,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.members[FAKE_USER_ID_2] = {
|
||||
userId: FAKE_USER_ID_2,
|
||||
membership: "join",
|
||||
} as unknown as RoomMember;
|
||||
room.currentState.getStateEvents = jest.fn().mockImplementation(mockGetStateEvents());
|
||||
groupCall = await createAndEnterGroupCall(mockClient, room);
|
||||
reportEmitter = groupCall.getGroupCallStats().reports;
|
||||
});
|
||||
|
||||
it("should not extends with feed stats if no call exists", async () => {
|
||||
const testPromise = new Promise<void>((done) => {
|
||||
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith({}, [], "from-call-feed");
|
||||
done();
|
||||
});
|
||||
});
|
||||
const report: CallFeedReport = {} as CallFeedReport;
|
||||
reportEmitter.emitCallFeedReport(report);
|
||||
await testPromise;
|
||||
});
|
||||
|
||||
it("and a CallFeedReport was triggered then it should extends with local feed", async () => {
|
||||
const localCallFeed = {} as CallFeed;
|
||||
groupCall.localCallFeed = localCallFeed;
|
||||
|
||||
const testPromise = new Promise<void>((done) => {
|
||||
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
|
||||
report,
|
||||
[localCallFeed],
|
||||
"from-local-feed",
|
||||
);
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
|
||||
report,
|
||||
[],
|
||||
"from-call-feed",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
const report: CallFeedReport = {} as CallFeedReport;
|
||||
reportEmitter.emitCallFeedReport(report);
|
||||
await testPromise;
|
||||
});
|
||||
|
||||
it("and a CallFeedReport was triggered then it should extends with remote feed", async () => {
|
||||
const localCallFeed = {} as CallFeed;
|
||||
groupCall.localCallFeed = localCallFeed;
|
||||
// @ts-ignore Suppress error because access to private property
|
||||
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
|
||||
report.callId = call.callId;
|
||||
const feeds = call.getFeeds();
|
||||
const testPromise = new Promise<void>((done) => {
|
||||
groupCall.on(GroupCallStatsReportEvent.CallFeedStats, () => {
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
|
||||
report,
|
||||
[localCallFeed],
|
||||
"from-local-feed",
|
||||
);
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport).toHaveBeenCalledWith(
|
||||
report,
|
||||
feeds,
|
||||
"from-call-feed",
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
reportEmitter.emitCallFeedReport(report);
|
||||
await testPromise;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CallFeedStatsReporter should builds CallFeedReport 1`] = `
|
||||
{
|
||||
"callFeeds": [],
|
||||
"callId": "CALL_ID",
|
||||
"opponentMemberId": "USER_ID",
|
||||
"transceiver": [
|
||||
{
|
||||
"currentDirection": "sendonly",
|
||||
"direction": "sendrecv",
|
||||
"mid": "0",
|
||||
"receiver": {
|
||||
"constrainDeviceId": "constrainDeviceId-receiver_audio_0",
|
||||
"enabled": true,
|
||||
"id": "receiver_audio_0",
|
||||
"kind": "audio",
|
||||
"label": "receiver",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-receiver_audio_0",
|
||||
},
|
||||
"sender": {
|
||||
"constrainDeviceId": "constrainDeviceId-sender_audio_0",
|
||||
"enabled": true,
|
||||
"id": "sender_audio_0",
|
||||
"kind": "audio",
|
||||
"label": "sender",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-sender_audio_0",
|
||||
},
|
||||
},
|
||||
{
|
||||
"currentDirection": "sendrecv",
|
||||
"direction": "recvonly",
|
||||
"mid": "1",
|
||||
"receiver": {
|
||||
"constrainDeviceId": "constrainDeviceId-receiver_video_1",
|
||||
"enabled": true,
|
||||
"id": "receiver_video_1",
|
||||
"kind": "video",
|
||||
"label": "receiver",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-receiver_video_1",
|
||||
},
|
||||
"sender": {
|
||||
"constrainDeviceId": "constrainDeviceId-sender_video_1",
|
||||
"enabled": true,
|
||||
"id": "sender_video_1",
|
||||
"kind": "video",
|
||||
"label": "sender",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-sender_video_1",
|
||||
},
|
||||
},
|
||||
{
|
||||
"currentDirection": "recvonly",
|
||||
"direction": "recvonly",
|
||||
"mid": "2",
|
||||
"receiver": {
|
||||
"constrainDeviceId": "constrainDeviceId-receiver_video_2",
|
||||
"enabled": true,
|
||||
"id": "receiver_video_2",
|
||||
"kind": "video",
|
||||
"label": "receiver",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-receiver_video_2",
|
||||
},
|
||||
"sender": null,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`CallFeedStatsReporter should extends CallFeedReport with call feeds 1`] = `
|
||||
[
|
||||
{
|
||||
"audio": {
|
||||
"constrainDeviceId": "constrainDeviceId-video-1",
|
||||
"enabled": true,
|
||||
"id": "video-1",
|
||||
"kind": "video",
|
||||
"label": "--",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-video-1",
|
||||
},
|
||||
"isAudioMuted": true,
|
||||
"isVideoMuted": false,
|
||||
"prefix": "unknown",
|
||||
"purpose": undefined,
|
||||
"stream": "stream-1",
|
||||
"type": "local",
|
||||
"video": {
|
||||
"constrainDeviceId": "constrainDeviceId-audio-1",
|
||||
"enabled": true,
|
||||
"id": "audio-1",
|
||||
"kind": "audio",
|
||||
"label": "--",
|
||||
"muted": false,
|
||||
"readyState": "live",
|
||||
"settingDeviceId": "settingDeviceId-audio-1",
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2023 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 { CallFeedStatsReporter } from "../../../../src/webrtc/stats/callFeedStatsReporter";
|
||||
import { CallFeedReport } from "../../../../src/webrtc/stats/statsReport";
|
||||
import { CallFeed } from "../../../../src/webrtc/callFeed";
|
||||
|
||||
const CALL_ID = "CALL_ID";
|
||||
const USER_ID = "USER_ID";
|
||||
describe("CallFeedStatsReporter", () => {
|
||||
let rtcSpy: RTCPeerConnection;
|
||||
beforeEach(() => {
|
||||
rtcSpy = {} as RTCPeerConnection;
|
||||
rtcSpy.getTransceivers = jest.fn().mockReturnValue(buildTransceiverMocks());
|
||||
});
|
||||
|
||||
describe("should", () => {
|
||||
it("builds CallFeedReport", async () => {
|
||||
expect(CallFeedStatsReporter.buildCallFeedReport(CALL_ID, USER_ID, rtcSpy)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("extends CallFeedReport with call feeds", async () => {
|
||||
const feed = buildCallFeedMock("1");
|
||||
const callFeedList: CallFeed[] = [feed];
|
||||
const report = {
|
||||
callId: "callId",
|
||||
opponentMemberId: "opponentMemberId",
|
||||
transceiver: [],
|
||||
callFeeds: [],
|
||||
} as CallFeedReport;
|
||||
|
||||
expect(CallFeedStatsReporter.expandCallFeedReport(report, callFeedList).callFeeds).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
const buildTransceiverMocks = (): RTCRtpTransceiver[] => {
|
||||
const trans1 = {
|
||||
mid: "0",
|
||||
direction: "sendrecv",
|
||||
currentDirection: "sendonly",
|
||||
sender: buildSenderMock("sender_audio_0", "audio"),
|
||||
receiver: buildReceiverMock("receiver_audio_0", "audio"),
|
||||
} as RTCRtpTransceiver;
|
||||
const trans2 = {
|
||||
mid: "1",
|
||||
direction: "recvonly",
|
||||
currentDirection: "sendrecv",
|
||||
sender: buildSenderMock("sender_video_1", "video"),
|
||||
receiver: buildReceiverMock("receiver_video_1", "video"),
|
||||
} as RTCRtpTransceiver;
|
||||
const trans3 = {
|
||||
mid: "2",
|
||||
direction: "recvonly",
|
||||
currentDirection: "recvonly",
|
||||
sender: { track: null } as RTCRtpSender,
|
||||
receiver: buildReceiverMock("receiver_video_2", "video"),
|
||||
} as RTCRtpTransceiver;
|
||||
return [trans1, trans2, trans3];
|
||||
};
|
||||
|
||||
const buildSenderMock = (id: string, kind: "audio" | "video"): RTCRtpSender => {
|
||||
const track = buildTrackMock(id, kind);
|
||||
return {
|
||||
track,
|
||||
} as RTCRtpSender;
|
||||
};
|
||||
|
||||
const buildReceiverMock = (id: string, kind: "audio" | "video"): RTCRtpReceiver => {
|
||||
const track = buildTrackMock(id, kind);
|
||||
return {
|
||||
track,
|
||||
} as RTCRtpReceiver;
|
||||
};
|
||||
|
||||
const buildTrackMock = (id: string, kind: "audio" | "video"): MediaStreamTrack => {
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
enabled: true,
|
||||
label: "--",
|
||||
muted: false,
|
||||
readyState: "live",
|
||||
getSettings: () => ({ deviceId: `settingDeviceId-${id}` }),
|
||||
getConstraints: () => ({ deviceId: `constrainDeviceId-${id}` }),
|
||||
} as MediaStreamTrack;
|
||||
};
|
||||
|
||||
const buildCallFeedMock = (id: string, isLocal = true): CallFeed => {
|
||||
const stream = {
|
||||
id: `stream-${id}`,
|
||||
getAudioTracks(): MediaStreamTrack[] {
|
||||
return [buildTrackMock(`video-${id}`, "video")];
|
||||
},
|
||||
getVideoTracks(): MediaStreamTrack[] {
|
||||
return [buildTrackMock(`audio-${id}`, "audio")];
|
||||
},
|
||||
} as MediaStream;
|
||||
return {
|
||||
stream,
|
||||
isLocal: () => isLocal,
|
||||
isVideoMuted: () => false,
|
||||
isAudioMuted: () => true,
|
||||
} as CallFeed;
|
||||
};
|
||||
});
|
||||
@@ -17,6 +17,8 @@ limitations under the License.
|
||||
import { CallStatsReportGatherer } from "../../../../src/webrtc/stats/callStatsReportGatherer";
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import { MediaSsrcHandler } from "../../../../src/webrtc/stats/media/mediaSsrcHandler";
|
||||
import { currentChromeReport, prevChromeReport } from "../../../test-utils/webrtcReports";
|
||||
import { MockRTCPeerConnection } from "../../../test-utils/webrtc";
|
||||
|
||||
const CALL_ID = "CALL_ID";
|
||||
const USER_ID = "USER_ID";
|
||||
@@ -28,6 +30,7 @@ describe("CallStatsReportGatherer", () => {
|
||||
beforeEach(() => {
|
||||
rtcSpy = { getStats: () => new Promise<RTCStatsReport>(() => null) } as RTCPeerConnection;
|
||||
rtcSpy.addEventListener = jest.fn();
|
||||
rtcSpy.getTransceivers = jest.fn().mockReturnValue([]);
|
||||
emitter = new StatsReportEmitter();
|
||||
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
|
||||
});
|
||||
@@ -145,6 +148,70 @@ describe("CallStatsReportGatherer", () => {
|
||||
});
|
||||
expect(collector.getActive()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("should not only produce a call summary stat but also", () => {
|
||||
const wantedSummaryReport = {
|
||||
isFirstCollection: false,
|
||||
audioTrackSummary: {
|
||||
concealedAudio: 0,
|
||||
count: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
muted: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
receivedAudioMedia: 0,
|
||||
receivedMedia: 0,
|
||||
receivedVideoMedia: 0,
|
||||
videoTrackSummary: {
|
||||
concealedAudio: 0,
|
||||
count: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
muted: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
};
|
||||
beforeEach(() => {
|
||||
rtcSpy = new MockRTCPeerConnection() as unknown as RTCPeerConnection;
|
||||
collector = new CallStatsReportGatherer(CALL_ID, USER_ID, rtcSpy, emitter);
|
||||
const getStats = jest.spyOn(rtcSpy, "getStats");
|
||||
|
||||
const previous = prevChromeReport as unknown as RTCStatsReport;
|
||||
previous.get = (id: string) => {
|
||||
return prevChromeReport.find((data) => data.id === id);
|
||||
};
|
||||
// @ts-ignore
|
||||
collector.previousStatsReport = previous;
|
||||
|
||||
const current = currentChromeReport as unknown as RTCStatsReport;
|
||||
current.get = (id: string) => {
|
||||
return currentChromeReport.find((data) => data.id === id);
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
getStats.mockResolvedValue(current);
|
||||
});
|
||||
|
||||
it("emit byteSentStatsReport", async () => {
|
||||
const emitByteSendReport = jest.spyOn(emitter, "emitByteSendReport");
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(actual).toEqual(wantedSummaryReport);
|
||||
expect(emitByteSendReport).toHaveBeenCalled();
|
||||
});
|
||||
it("emit emitConnectionStatsReport", async () => {
|
||||
const emitConnectionStatsReport = jest.spyOn(emitter, "emitConnectionStatsReport");
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(actual).toEqual(wantedSummaryReport);
|
||||
expect(emitConnectionStatsReport).toHaveBeenCalled();
|
||||
});
|
||||
it("emit callFeedStatsReport", async () => {
|
||||
const emitCallFeedReport = jest.spyOn(emitter, "emitCallFeedReport");
|
||||
const actual = await collector.processStats("GROUP_CALL_ID", "LOCAL_USER_ID");
|
||||
expect(actual).toEqual(wantedSummaryReport);
|
||||
expect(emitCallFeedReport).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("on signal state change event", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
CallFeedReport,
|
||||
ConnectionStatsReport,
|
||||
StatsReport,
|
||||
SummaryStatsReport,
|
||||
@@ -62,4 +63,16 @@ describe("StatsReportEmitter", () => {
|
||||
emitter.emitSummaryStatsReport(report);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit and receive CallFeedReports", async () => {
|
||||
const report = {} as CallFeedReport;
|
||||
return new Promise((resolve, _) => {
|
||||
emitter.on(StatsReport.CALL_FEED_REPORT, (r) => {
|
||||
expect(r).toBe(report);
|
||||
resolve(null);
|
||||
return;
|
||||
});
|
||||
emitter.emitCallFeedReport(report);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
import { SummaryStatsReportGatherer } from "../../../../src/webrtc/stats/summaryStatsReportGatherer";
|
||||
import { StatsReportEmitter } from "../../../../src/webrtc/stats/statsReportEmitter";
|
||||
import { groupCallParticipantsFourOtherDevices } from "../../../test-utils/webrtc";
|
||||
|
||||
describe("SummaryStatsReportGatherer", () => {
|
||||
let reporter: SummaryStatsReportGatherer;
|
||||
@@ -584,9 +585,119 @@ describe("SummaryStatsReportGatherer", () => {
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 3,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
it("should report missing peer connections", async () => {
|
||||
const summary = [
|
||||
{
|
||||
isFirstCollection: true,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 20,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 0,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
isFirstCollection: false,
|
||||
receivedMedia: 1,
|
||||
receivedAudioMedia: 1,
|
||||
receivedVideoMedia: 1,
|
||||
audioTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 5,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
videoTrackSummary: {
|
||||
count: 1,
|
||||
muted: 0,
|
||||
maxJitter: 0,
|
||||
maxPacketLoss: 40,
|
||||
concealedAudio: 0,
|
||||
totalAudio: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
reporter.build(summary);
|
||||
expect(emitter.emitSummaryStatsReport).toHaveBeenCalledWith({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 2,
|
||||
percentageConcealedAudio: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("extend Summary Stats Report", () => {
|
||||
it("should extend the report with the appropriate data based on a user map", async () => {
|
||||
const summary = {
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
};
|
||||
SummaryStatsReportGatherer.extendSummaryReport(summary, groupCallParticipantsFourOtherDevices);
|
||||
expect(summary).toStrictEqual({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
opponentUsersInCall: 1,
|
||||
opponentDevicesInCall: 4,
|
||||
diffDevicesToPeerConnections: 0,
|
||||
ratioPeerConnectionToDevices: 1,
|
||||
});
|
||||
});
|
||||
it("should extend the report data based on a user map", async () => {
|
||||
const summary = {
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
};
|
||||
SummaryStatsReportGatherer.extendSummaryReport(summary, new Map());
|
||||
expect(summary).toStrictEqual({
|
||||
percentageReceivedMedia: 1,
|
||||
percentageReceivedAudioMedia: 1,
|
||||
percentageReceivedVideoMedia: 1,
|
||||
maxJitter: 2,
|
||||
maxPacketLoss: 40,
|
||||
peerConnections: 4,
|
||||
percentageConcealedAudio: 0,
|
||||
opponentUsersInCall: 0,
|
||||
opponentDevicesInCall: 0,
|
||||
diffDevicesToPeerConnections: -4,
|
||||
ratioPeerConnectionToDevices: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -168,11 +168,11 @@ export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix
|
||||
export const UNSTABLE_MSC2716_MARKER = new UnstableValue("m.room.marker", "org.matrix.msc2716.marker");
|
||||
|
||||
/**
|
||||
* Name of the "with_relations" request property for relation based redactions.
|
||||
* Name of the request property for relation based redactions.
|
||||
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
|
||||
*/
|
||||
export const MSC3912_RELATION_BASED_REDACTIONS_PROP = new UnstableValue(
|
||||
"with_relations",
|
||||
"with_rel_types",
|
||||
"org.matrix.msc3912.with_relations",
|
||||
);
|
||||
|
||||
|
||||
@@ -48,17 +48,16 @@ export interface IJoinRoomOpts {
|
||||
export interface IRedactOpts {
|
||||
reason?: string;
|
||||
/**
|
||||
* Whether events related to the redacted event should be redacted.
|
||||
*
|
||||
* If specified, then any events which relate to the event being redacted with
|
||||
* any of the relationship types listed will also be redacted.
|
||||
* Provide a "*" list item to tell the server to redact relations of any type.
|
||||
*
|
||||
* <b>Raises an Error if the server does not support it.</b>
|
||||
* Check for server-side support before using this param with
|
||||
* <code>client.canSupport.get(Feature.RelationBasedRedactions)</code>.
|
||||
* {@link https://github.com/matrix-org/matrix-spec-proposals/pull/3912}
|
||||
*/
|
||||
with_relations?: Array<RelationType | string>;
|
||||
with_rel_types?: Array<RelationType | "*">;
|
||||
}
|
||||
|
||||
export interface ISendEventResponse {
|
||||
|
||||
+75
-5
@@ -15,9 +15,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IClientWellKnown, IWellKnownConfig } from "./client";
|
||||
import { IClientWellKnown, IWellKnownConfig, IDelegatedAuthConfig, IServerVersions, M_AUTHENTICATION } from "./client";
|
||||
import { logger } from "./logger";
|
||||
import { MatrixError, Method, timeoutSignal } from "./http-api";
|
||||
import {
|
||||
OidcDiscoveryError,
|
||||
ValidatedIssuerConfig,
|
||||
validateOIDCIssuerWellKnown,
|
||||
validateWellKnownAuthentication,
|
||||
} from "./oidc/validate";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -42,14 +48,18 @@ enum AutoDiscoveryError {
|
||||
InvalidJson = "Invalid JSON",
|
||||
}
|
||||
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error"> {
|
||||
interface AutoDiscoveryState {
|
||||
state: AutoDiscoveryAction;
|
||||
error?: IWellKnownConfig["error"] | null;
|
||||
}
|
||||
interface WellKnownConfig extends Omit<IWellKnownConfig, "error">, AutoDiscoveryState {}
|
||||
|
||||
interface DelegatedAuthConfig extends IDelegatedAuthConfig, ValidatedIssuerConfig, AutoDiscoveryState {}
|
||||
|
||||
export interface ClientConfig extends Omit<IClientWellKnown, "m.homeserver" | "m.identity_server"> {
|
||||
"m.homeserver": WellKnownConfig;
|
||||
"m.identity_server": WellKnownConfig;
|
||||
"m.authentication"?: DelegatedAuthConfig | AutoDiscoveryState;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +180,7 @@ export class AutoDiscovery {
|
||||
}
|
||||
|
||||
// Step 3: Make sure the homeserver URL points to a homeserver.
|
||||
const hsVersions = await this.fetchWellKnownObject(`${hsUrl}/_matrix/client/versions`);
|
||||
const hsVersions = await this.fetchWellKnownObject<IServerVersions>(`${hsUrl}/_matrix/client/versions`);
|
||||
if (!hsVersions?.raw?.["versions"]) {
|
||||
logger.error("Invalid /versions response");
|
||||
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
|
||||
@@ -256,10 +266,67 @@ export class AutoDiscovery {
|
||||
}
|
||||
});
|
||||
|
||||
const authConfig = await this.validateDiscoveryAuthenticationConfig(wellknown);
|
||||
clientConfig[M_AUTHENTICATION.stable!] = authConfig;
|
||||
|
||||
// Step 8: Give the config to the caller (finally)
|
||||
return Promise.resolve(clientConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate delegated auth configuration
|
||||
* - m.authentication config is present and valid
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* When successful, DelegatedAuthConfig will be returned with endpoints used for delegated auth
|
||||
* Any errors are caught, and AutoDiscoveryState returned with error
|
||||
* @param wellKnown - configuration object as returned
|
||||
* by the .well-known auto-discovery endpoint
|
||||
* @returns Config or failure result
|
||||
*/
|
||||
public static async validateDiscoveryAuthenticationConfig(
|
||||
wellKnown: IClientWellKnown,
|
||||
): Promise<DelegatedAuthConfig | AutoDiscoveryState> {
|
||||
try {
|
||||
const homeserverAuthenticationConfig = validateWellKnownAuthentication(wellKnown);
|
||||
|
||||
const issuerOpenIdConfigUrl = `${this.sanitizeWellKnownUrl(
|
||||
homeserverAuthenticationConfig.issuer,
|
||||
)}/.well-known/openid-configuration`;
|
||||
const issuerWellKnown = await this.fetchWellKnownObject<unknown>(issuerOpenIdConfigUrl);
|
||||
|
||||
if (issuerWellKnown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("Failed to fetch issuer openid configuration");
|
||||
throw new Error(OidcDiscoveryError.General);
|
||||
}
|
||||
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown.raw);
|
||||
|
||||
const delegatedAuthConfig: DelegatedAuthConfig = {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
error: null,
|
||||
...homeserverAuthenticationConfig,
|
||||
...validatedIssuerConfig,
|
||||
};
|
||||
return delegatedAuthConfig;
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message as unknown as OidcDiscoveryError;
|
||||
const errorType = Object.values(OidcDiscoveryError).includes(errorMessage)
|
||||
? errorMessage
|
||||
: OidcDiscoveryError.General;
|
||||
|
||||
const state =
|
||||
errorType === OidcDiscoveryError.NotSupported
|
||||
? AutoDiscoveryAction.IGNORE
|
||||
: AutoDiscoveryAction.FAIL_ERROR;
|
||||
|
||||
return {
|
||||
state,
|
||||
error: errorType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to automatically discover client configuration information
|
||||
* prior to logging in. Such information includes the homeserver URL
|
||||
@@ -308,7 +375,8 @@ export class AutoDiscovery {
|
||||
|
||||
// Step 1: Actually request the .well-known JSON file and make sure it
|
||||
// at least has a homeserver definition.
|
||||
const wellknown = await this.fetchWellKnownObject(`https://${domain}/.well-known/matrix/client`);
|
||||
const domainWithProtocol = domain.includes("://") ? domain : `https://${domain}`;
|
||||
const wellknown = await this.fetchWellKnownObject(`${domainWithProtocol}/.well-known/matrix/client`);
|
||||
if (!wellknown || wellknown.action !== AutoDiscoveryAction.SUCCESS) {
|
||||
logger.error("No response or error when parsing .well-known");
|
||||
if (wellknown.reason) logger.error(wellknown.reason);
|
||||
@@ -412,7 +480,9 @@ export class AutoDiscovery {
|
||||
* @returns Promise which resolves to the returned state.
|
||||
* @internal
|
||||
*/
|
||||
private static async fetchWellKnownObject(url: string): Promise<IWellKnownConfig> {
|
||||
private static async fetchWellKnownObject<T = IWellKnownConfig>(
|
||||
url: string,
|
||||
): Promise<IWellKnownConfig<Partial<T>>> {
|
||||
let response: Response;
|
||||
|
||||
try {
|
||||
|
||||
+119
-67
@@ -103,13 +103,7 @@ import { MatrixScheduler } from "./scheduler";
|
||||
import { BeaconEvent, BeaconEventHandlerMap } from "./models/beacon";
|
||||
import { IAuthData, IAuthDict } from "./interactive-auth";
|
||||
import { IMinimalEvent, IRoomEvent, IStateEvent } from "./sync-accumulator";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
} from "./crypto/api";
|
||||
import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./crypto/api";
|
||||
import { EventTimelineSet } from "./models/event-timeline-set";
|
||||
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest";
|
||||
import { VerificationBase as Verification } from "./crypto/verification/Base";
|
||||
@@ -207,7 +201,7 @@ import { LocalNotificationSettings } from "./@types/local_notifications";
|
||||
import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature";
|
||||
import { CryptoBackend } from "./common-crypto/CryptoBackend";
|
||||
import { RUST_SDK_STORE_PREFIX } from "./rust-crypto/constants";
|
||||
import { BootstrapCrossSigningOpts, CryptoApi } from "./crypto-api";
|
||||
import { BootstrapCrossSigningOpts, CryptoApi, ImportRoomKeysOpts } from "./crypto-api";
|
||||
import { DeviceInfoMap } from "./crypto/DeviceList";
|
||||
import {
|
||||
AddSecretStorageKeyOpts,
|
||||
@@ -469,11 +463,6 @@ export interface IStartClientOpts {
|
||||
* @experimental
|
||||
*/
|
||||
slidingSync?: SlidingSync;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
intentionalMentions?: boolean;
|
||||
}
|
||||
|
||||
export interface IStoredClientOpts extends IStartClientOpts {}
|
||||
@@ -500,6 +489,10 @@ export interface IMSC3882GetLoginTokenCapability extends ICapability {}
|
||||
|
||||
export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token");
|
||||
|
||||
export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
|
||||
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
|
||||
export const UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS = "uk.half-shot.msc2666.query_mutual_rooms";
|
||||
|
||||
/**
|
||||
* A representation of the capabilities advertised by a homeserver as defined by
|
||||
* [Capabilities negotiation](https://spec.matrix.org/v1.6/client-server-api/#get_matrixclientv3capabilities).
|
||||
@@ -593,8 +586,8 @@ export interface IClientWellKnown {
|
||||
[M_AUTHENTICATION.name]?: IDelegatedAuthConfig; // MSC2965
|
||||
}
|
||||
|
||||
export interface IWellKnownConfig {
|
||||
raw?: IClientWellKnown;
|
||||
export interface IWellKnownConfig<T = IClientWellKnown> {
|
||||
raw?: T;
|
||||
action?: AutoDiscoveryAction;
|
||||
reason?: string;
|
||||
error?: Error | string;
|
||||
@@ -3196,14 +3189,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* Import a list of room keys previously exported by exportRoomKeys
|
||||
*
|
||||
* @param keys - a list of session export objects
|
||||
* @param opts - options object
|
||||
*
|
||||
* @returns a promise which resolves when the keys have been imported
|
||||
*
|
||||
* @deprecated Prefer {@link CryptoApi.importRoomKeys | `CryptoApi.importRoomKeys`}:
|
||||
* ```javascript
|
||||
* await client.getCrypto()?.importRoomKeys([..]);
|
||||
* ```
|
||||
*/
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts?: IImportRoomKeysOpts): Promise<void> {
|
||||
if (!this.crypto) {
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
|
||||
if (!this.cryptoBackend) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this.crypto.importRoomKeys(keys, opts);
|
||||
return this.cryptoBackend.importRoomKeys(keys, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3811,7 +3810,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
algorithm.free();
|
||||
}
|
||||
|
||||
await this.importRoomKeys(keys, {
|
||||
await this.getCrypto()?.importRoomKeys(keys, {
|
||||
progressCallback,
|
||||
untrusted,
|
||||
source: "backup",
|
||||
@@ -4594,10 +4593,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* @param txnId - transaction id. One will be made up if not supplied.
|
||||
* @param opts - Options to pass on, may contain `reason` and `with_relations` (MSC3912)
|
||||
* @param opts - Redact options
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Rejects: with an error response.
|
||||
* @throws Error if called with `with_relations` (MSC3912) but the server does not support it.
|
||||
* @throws Error if called with `with_rel_types` (MSC3912) but the server does not support it.
|
||||
* Callers should check whether the server supports MSC3912 via `MatrixClient.canSupport`.
|
||||
*/
|
||||
public redactEvent(
|
||||
@@ -4627,34 +4626,30 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
threadId = null;
|
||||
}
|
||||
const reason = opts?.reason;
|
||||
const content: IContent = { reason };
|
||||
|
||||
if (
|
||||
opts?.with_relations &&
|
||||
this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported
|
||||
) {
|
||||
throw new Error(
|
||||
"Server does not support relation based redactions " +
|
||||
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
|
||||
);
|
||||
if (opts?.with_rel_types !== undefined) {
|
||||
if (this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Unsupported) {
|
||||
throw new Error(
|
||||
"Server does not support relation based redactions " +
|
||||
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId ${threadId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const withRelTypesPropName =
|
||||
this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Stable
|
||||
? MSC3912_RELATION_BASED_REDACTIONS_PROP.stable!
|
||||
: MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!;
|
||||
|
||||
content[withRelTypesPropName] = opts.with_rel_types;
|
||||
}
|
||||
|
||||
const withRelations = opts?.with_relations
|
||||
? {
|
||||
[this.canSupport.get(Feature.RelationBasedRedactions) === ServerSupport.Stable
|
||||
? MSC3912_RELATION_BASED_REDACTIONS_PROP.stable!
|
||||
: MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: opts?.with_relations,
|
||||
}
|
||||
: {};
|
||||
|
||||
return this.sendCompleteEvent(
|
||||
roomId,
|
||||
threadId,
|
||||
{
|
||||
type: EventType.RoomRedaction,
|
||||
content: {
|
||||
...withRelations,
|
||||
reason,
|
||||
},
|
||||
content,
|
||||
redacts: eventId,
|
||||
},
|
||||
txnId as string,
|
||||
@@ -7148,29 +7143,75 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of room IDs in common with another user
|
||||
* Gets a set of room IDs in common with another user.
|
||||
*
|
||||
* Note: This endpoint is unstable, and can throw an `Error`.
|
||||
* Check progress on [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666) for more details.
|
||||
*
|
||||
* @param userId - The userId to check.
|
||||
* @returns Promise which resolves to a set of rooms
|
||||
* @returns Promise which resolves to an array of rooms
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
// TODO: on spec release, rename this to getMutualRooms
|
||||
// eslint-disable-next-line
|
||||
public async _unstable_getSharedRooms(userId: string): Promise<string[]> {
|
||||
const sharedRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666");
|
||||
const mutualRoomsSupport = await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666.mutual_rooms");
|
||||
// Initial variant of the MSC
|
||||
const sharedRoomsSupport = await this.doesServerSupportUnstableFeature(UNSTABLE_MSC2666_SHARED_ROOMS);
|
||||
|
||||
if (!sharedRoomsSupport && !mutualRoomsSupport) {
|
||||
throw Error("Server does not support mutual_rooms API");
|
||||
}
|
||||
// Newer variant that renamed shared rooms to mutual rooms
|
||||
const mutualRoomsSupport = await this.doesServerSupportUnstableFeature(UNSTABLE_MSC2666_MUTUAL_ROOMS);
|
||||
|
||||
const path = utils.encodeUri(
|
||||
`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`,
|
||||
{ $userId: userId },
|
||||
// Latest variant that changed from path elements to query elements
|
||||
const queryMutualRoomsSupport = await this.doesServerSupportUnstableFeature(
|
||||
UNSTABLE_MSC2666_QUERY_MUTUAL_ROOMS,
|
||||
);
|
||||
|
||||
const res = await this.http.authedRequest<{ joined: string[] }>(Method.Get, path, undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable,
|
||||
});
|
||||
return res.joined;
|
||||
if (!sharedRoomsSupport && !mutualRoomsSupport && !queryMutualRoomsSupport) {
|
||||
throw Error("Server does not support the Mutual Rooms API");
|
||||
}
|
||||
|
||||
let path;
|
||||
let query;
|
||||
|
||||
// Cascading unstable support switching.
|
||||
if (queryMutualRoomsSupport) {
|
||||
path = "/uk.half-shot.msc2666/user/mutual_rooms";
|
||||
query = { user_id: userId };
|
||||
} else {
|
||||
path = utils.encodeUri(
|
||||
`/uk.half-shot.msc2666/user/${mutualRoomsSupport ? "mutual_rooms" : "shared_rooms"}/$userId`,
|
||||
{ $userId: userId },
|
||||
);
|
||||
query = {};
|
||||
}
|
||||
|
||||
// Accumulated rooms
|
||||
const rooms: string[] = [];
|
||||
let token = null;
|
||||
|
||||
do {
|
||||
const tokenQuery: Record<string, string> = {};
|
||||
if (token != null && queryMutualRoomsSupport) {
|
||||
tokenQuery["batch_token"] = token;
|
||||
}
|
||||
|
||||
const res = await this.http.authedRequest<{
|
||||
joined: string[];
|
||||
next_batch_token?: string;
|
||||
}>(Method.Get, path, { ...query, ...tokenQuery }, undefined, {
|
||||
prefix: ClientPrefix.Unstable,
|
||||
});
|
||||
|
||||
rooms.push(...res.joined);
|
||||
|
||||
if (res.next_batch_token !== undefined) {
|
||||
token = res.next_batch_token;
|
||||
} else {
|
||||
token = null;
|
||||
}
|
||||
} while (token != null);
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -7689,16 +7730,27 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Rejects with an error response.
|
||||
*/
|
||||
public refreshToken(refreshToken: string): Promise<IRefreshTokenResponse> {
|
||||
return this.http.authedRequest(
|
||||
Method.Post,
|
||||
"/refresh",
|
||||
undefined,
|
||||
{ refresh_token: refreshToken },
|
||||
{
|
||||
prefix: ClientPrefix.V1,
|
||||
inhibitLogoutEmit: true, // we don't want to cause logout loops
|
||||
},
|
||||
);
|
||||
const performRefreshRequestWithPrefix = (prefix: ClientPrefix): Promise<IRefreshTokenResponse> =>
|
||||
this.http.authedRequest(
|
||||
Method.Post,
|
||||
"/refresh",
|
||||
undefined,
|
||||
{ refresh_token: refreshToken },
|
||||
{
|
||||
prefix,
|
||||
inhibitLogoutEmit: true, // we don't want to cause logout loops
|
||||
},
|
||||
);
|
||||
|
||||
// First try with the (specced) /v3/ prefix.
|
||||
// However, before Synapse 1.72.0, Synapse incorrectly required a /v1/ prefix, so we fall
|
||||
// back to that if the request fails, for backwards compatibility.
|
||||
return performRefreshRequestWithPrefix(ClientPrefix.V3).catch((e) => {
|
||||
if (e.errcode === "M_UNRECOGNIZED") {
|
||||
return performRefreshRequestWithPrefix(ClientPrefix.V1);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9574,11 +9626,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
|
||||
/**
|
||||
* A helper to determine intentional mentions support
|
||||
* @returns a boolean to determine if intentional mentions are enabled
|
||||
* @returns a boolean to determine if intentional mentions are enabled on the server
|
||||
* @experimental
|
||||
*/
|
||||
public supportsIntentionalMentions(): boolean {
|
||||
return this.clientOpts?.intentionalMentions || false;
|
||||
return this.canSupport.get(Feature.IntentionalMentions) !== ServerSupport.Unsupported;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -82,6 +82,15 @@ export interface CryptoApi {
|
||||
*/
|
||||
exportRoomKeys(): Promise<IMegolmSessionData[]>;
|
||||
|
||||
/**
|
||||
* Import a list of room keys previously exported by exportRoomKeys
|
||||
*
|
||||
* @param keys - a list of session export objects
|
||||
* @param opts - options object
|
||||
* @returns a promise which resolves once the keys have been imported
|
||||
*/
|
||||
importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the device information for the given list of users.
|
||||
*
|
||||
@@ -185,6 +194,13 @@ export interface CryptoApi {
|
||||
* @returns True if secret storage is ready to be used on this device
|
||||
*/
|
||||
isSecretStorageReady(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get the status of our cross-signing keys.
|
||||
*
|
||||
* @returns The current status of cross-signing keys: whether we have public and private keys cached locally, and whether the private keys are in secret storage.
|
||||
*/
|
||||
getCrossSigningStatus(): Promise<CrossSigningStatus>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,4 +278,49 @@ export class DeviceVerificationStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Room key import progress report.
|
||||
* Used when calling {@link CryptoApi#importRoomKeys} as the parameter of
|
||||
* the progressCallback. Used to display feedback.
|
||||
*/
|
||||
export interface ImportRoomKeyProgressData {
|
||||
stage: string; // TODO: Enum
|
||||
successes: number;
|
||||
failures: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options object for {@link CryptoApi#importRoomKeys}.
|
||||
*/
|
||||
export interface ImportRoomKeysOpts {
|
||||
/** Reports ongoing progress of the import process. Can be used for feedback. */
|
||||
progressCallback?: (stage: ImportRoomKeyProgressData) => void;
|
||||
// TODO, the rust SDK will always such imported keys as untrusted
|
||||
untrusted?: boolean;
|
||||
source?: String; // TODO: Enum (backup, file, ??)
|
||||
}
|
||||
|
||||
export * from "./crypto-api/verification";
|
||||
|
||||
/**
|
||||
* The result of a call to {@link CryptoApi.getCrossSigningStatus}.
|
||||
*/
|
||||
export interface CrossSigningStatus {
|
||||
/**
|
||||
* True if the public master, self signing and user signing keys are available on this device.
|
||||
*/
|
||||
publicKeysOnDevice: boolean;
|
||||
/**
|
||||
* True if the private keys are stored in the secret storage.
|
||||
*/
|
||||
privateKeysInSecretStorage: boolean;
|
||||
/**
|
||||
* True if the private keys are stored locally.
|
||||
*/
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: boolean;
|
||||
selfSigningKey: boolean;
|
||||
userSigningKey: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,8 +15,67 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||
|
||||
/** Events emitted by `Verifier`. */
|
||||
/**
|
||||
* A `Verifier` is responsible for performing the verification using a particular method, such as via QR code or SAS
|
||||
* (emojis).
|
||||
*
|
||||
* A verifier object can be created by calling `VerificationRequest.beginVerification`; one is also created
|
||||
* automatically when a `m.key.verification.start` event is received for an existing VerificationRequest.
|
||||
*
|
||||
* Once a verifier object is created, the verification can be started by calling the {@link Verifier#verify} method.
|
||||
*/
|
||||
export interface Verifier extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> {
|
||||
/**
|
||||
* Returns true if the verification has been cancelled, either by us or the other side.
|
||||
*/
|
||||
get hasBeenCancelled(): boolean;
|
||||
|
||||
/**
|
||||
* The ID of the other user in the verification process.
|
||||
*/
|
||||
get userId(): string;
|
||||
|
||||
/**
|
||||
* Start the key verification, if it has not already been started.
|
||||
*
|
||||
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
|
||||
* if the other side has already sent a start event.
|
||||
*
|
||||
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
|
||||
* or times out.
|
||||
*/
|
||||
verify(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Cancel a verification.
|
||||
*
|
||||
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
|
||||
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
|
||||
*
|
||||
* @param e - the reason for the cancellation.
|
||||
*/
|
||||
cancel(e: Error): void;
|
||||
|
||||
/**
|
||||
* Get the details for an SAS verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
|
||||
* the SAS matches.
|
||||
*/
|
||||
getShowSasCallbacks(): ShowSasCallbacks | null;
|
||||
|
||||
/**
|
||||
* Get the details for reciprocating QR code verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
|
||||
* already scanned our QR code), and we are waiting for the user to confirm.
|
||||
*/
|
||||
getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null;
|
||||
}
|
||||
|
||||
/** Events emitted by {@link Verifier} */
|
||||
export enum VerifierEvent {
|
||||
/**
|
||||
* The verification has been cancelled, by us or the other side.
|
||||
@@ -29,7 +88,7 @@ export enum VerifierEvent {
|
||||
/**
|
||||
* SAS data has been exchanged and should be displayed to the user.
|
||||
*
|
||||
* The payload is the {@link ShowQrCodeCallbacks} object.
|
||||
* The payload is the {@link ShowSasCallbacks} object.
|
||||
*/
|
||||
ShowSas = "show_sas",
|
||||
|
||||
|
||||
+6
-14
@@ -20,6 +20,12 @@ import type { AddSecretStorageKeyOpts } from "../secret-storage";
|
||||
|
||||
/* re-exports for backwards compatibility. */
|
||||
export { CrossSigningKey } from "../crypto-api";
|
||||
|
||||
export type {
|
||||
ImportRoomKeyProgressData as IImportOpts,
|
||||
ImportRoomKeysOpts as IImportRoomKeysOpts,
|
||||
} from "../crypto-api";
|
||||
|
||||
export type {
|
||||
AddSecretStorageKeyOpts as IAddSecretStorageKeyOpts,
|
||||
PassphraseInfo as IPassphraseInfo,
|
||||
@@ -100,17 +106,3 @@ export interface ICreateSecretStorageOpts {
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface IImportOpts {
|
||||
stage: string; // TODO: Enum
|
||||
successes: number;
|
||||
failures: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface IImportRoomKeysOpts {
|
||||
/** called with an object that has a "stage" param */
|
||||
progressCallback?: (stage: IImportOpts) => void;
|
||||
untrusted?: boolean;
|
||||
source?: string; // TODO: Enum
|
||||
}
|
||||
|
||||
+32
-9
@@ -35,13 +35,7 @@ import * as algorithms from "./algorithms";
|
||||
import { createCryptoStoreCacheCallbacks, CrossSigningInfo, DeviceTrustLevel, UserTrustLevel } from "./CrossSigning";
|
||||
import { EncryptionSetupBuilder } from "./EncryptionSetup";
|
||||
import { SecretStorage as LegacySecretStorage } from "./SecretStorage";
|
||||
import {
|
||||
CrossSigningKey,
|
||||
ICreateSecretStorageOpts,
|
||||
IEncryptedEventInfo,
|
||||
IImportRoomKeysOpts,
|
||||
IRecoveryKey,
|
||||
} from "./api";
|
||||
import { CrossSigningKey, ICreateSecretStorageOpts, IEncryptedEventInfo, IRecoveryKey } from "./api";
|
||||
import { OutgoingRoomKeyRequestManager } from "./OutgoingRoomKeyRequestManager";
|
||||
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
|
||||
import { VerificationBase } from "./verification/Base";
|
||||
@@ -93,7 +87,12 @@ import {
|
||||
ServerSideSecretStorageImpl,
|
||||
} from "../secret-storage";
|
||||
import { ISecretRequest } from "./SecretSharing";
|
||||
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
|
||||
import {
|
||||
BootstrapCrossSigningOpts,
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeysOpts,
|
||||
} from "../crypto-api";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { deviceInfoToDevice } from "./device-converter";
|
||||
|
||||
@@ -744,6 +743,30 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
return !!(secretStorageKeyInAccount && privateKeysInStorage && sessionBackupInStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||
*/
|
||||
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
||||
const publicKeysOnDevice = Boolean(this.crossSigningInfo.getId());
|
||||
const privateKeysInSecretStorage = Boolean(
|
||||
await this.crossSigningInfo.isStoredInSecretStorage(this.secretStorage),
|
||||
);
|
||||
const cacheCallbacks = this.crossSigningInfo.getCacheCallbacks();
|
||||
const masterKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("master"));
|
||||
const selfSigningKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("self_signing"));
|
||||
const userSigningKey = Boolean(await cacheCallbacks.getCrossSigningKeyCache?.("user_signing"));
|
||||
|
||||
return {
|
||||
publicKeysOnDevice,
|
||||
privateKeysInSecretStorage,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey,
|
||||
selfSigningKey,
|
||||
userSigningKey,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap cross-signing by creating keys if needed. If everything is already
|
||||
* set up, then no changes are made, so this is safe to run to ensure
|
||||
@@ -2828,7 +2851,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
* @param keys - a list of session export objects
|
||||
* @returns a promise which resolves once the keys have been imported
|
||||
*/
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: IImportRoomKeysOpts = {}): Promise<void> {
|
||||
public importRoomKeys(keys: IMegolmSessionData[], opts: ImportRoomKeysOpts = {}): Promise<void> {
|
||||
let successes = 0;
|
||||
let failures = 0;
|
||||
const total = keys.length;
|
||||
|
||||
@@ -29,7 +29,13 @@ import { IVerificationChannel } from "./request/Channel";
|
||||
import { MatrixClient } from "../../client";
|
||||
import { VerificationRequest } from "./request/VerificationRequest";
|
||||
import { TypedEventEmitter } from "../../models/typed-event-emitter";
|
||||
import { VerifierEvent, VerifierEventHandlerMap } from "../../crypto-api/verification";
|
||||
import {
|
||||
ShowQrCodeCallbacks,
|
||||
ShowSasCallbacks,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
VerifierEventHandlerMap,
|
||||
} from "../../crypto-api/verification";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
@@ -51,14 +57,18 @@ export type VerificationEventHandlerMap = {
|
||||
[VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
|
||||
};
|
||||
|
||||
/** @deprecated Avoid referencing this class directly; instead use {@link Crypto.Verifier}. */
|
||||
// The type parameters of VerificationBase are no longer used, but we need some placeholders to maintain
|
||||
// backwards compatibility with applications that reference the class.
|
||||
export class VerificationBase<
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Events extends string = VerifierEvent,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Arguments = VerifierEventHandlerMap,
|
||||
> extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Events extends string = VerifierEvent,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Arguments = VerifierEventHandlerMap,
|
||||
>
|
||||
extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap>
|
||||
implements Verifier
|
||||
{
|
||||
private cancelled = false;
|
||||
private _done = false;
|
||||
private promise: Promise<void> | null = null;
|
||||
@@ -373,4 +383,24 @@ export class VerificationBase<
|
||||
public get events(): string[] | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for an SAS verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
|
||||
* the SAS matches.
|
||||
*/
|
||||
public getShowSasCallbacks(): ShowSasCallbacks | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details for reciprocating QR code verification, if one is in progress
|
||||
*
|
||||
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
|
||||
* already scanned our QR code), and we are waiting for the user to confirm.
|
||||
*/
|
||||
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export type QrCodeEvent = VerifierEvent;
|
||||
/** @deprecated use VerifierEvent */
|
||||
export const QrCodeEvent = VerifierEvent;
|
||||
|
||||
/** @deprecated Avoid referencing this class directly; instead use {@link Crypto.Verifier}. */
|
||||
export class ReciprocateQRCode extends Base {
|
||||
public reciprocateQREvent?: ShowQrCodeCallbacks;
|
||||
|
||||
@@ -119,6 +120,10 @@ export class ReciprocateQRCode extends Base {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
|
||||
return this.reciprocateQREvent ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
|
||||
@@ -219,6 +219,7 @@ export type SasEvent = VerifierEvent;
|
||||
/** @deprecated use VerifierEvent */
|
||||
export const SasEvent = VerifierEvent;
|
||||
|
||||
/** @deprecated Avoid referencing this class directly; instead use {@link Crypto.Verifier}. */
|
||||
export class SAS extends Base {
|
||||
private waitingForAccept?: boolean;
|
||||
public ourSASPubKey?: string;
|
||||
@@ -480,4 +481,8 @@ export class SAS extends Base {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getShowSasCallbacks(): ShowSasCallbacks | null {
|
||||
return this.sasEvent ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +163,22 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique ID for this verification request.
|
||||
*
|
||||
* An ID isn't assigned until the first message is sent, so this may be `undefined` in the early phases.
|
||||
*/
|
||||
public get transactionId(): string | undefined {
|
||||
return this.channel.transactionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* For an in-room verification, the ID of the room.
|
||||
*/
|
||||
public get roomId(): string | undefined {
|
||||
return this.channel.roomId;
|
||||
}
|
||||
|
||||
public get invalid(): boolean {
|
||||
return this.phase === PHASE_UNSENT;
|
||||
}
|
||||
@@ -257,11 +273,23 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** Only set after a .ready if the other party can scan a QR code */
|
||||
/** Only set after a .ready if the other party can scan a QR code
|
||||
*
|
||||
* @deprecated Prefer `getQRCodeBytes`.
|
||||
*/
|
||||
public get qrCodeData(): QRCodeData | null {
|
||||
return this._qrCodeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||
*
|
||||
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
|
||||
*/
|
||||
public getQRCodeBytes(): Buffer | undefined {
|
||||
return this._qrCodeData?.getBuffer();
|
||||
}
|
||||
|
||||
/** Checks whether the other party supports a given verification method.
|
||||
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
|
||||
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
|
||||
@@ -347,6 +375,11 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
|
||||
return this.channel.userId!;
|
||||
}
|
||||
|
||||
/** The device id of the other party in this request, for requests happening over to-device messages only. */
|
||||
public get otherDeviceId(): string | undefined {
|
||||
return this.channel.deviceId;
|
||||
}
|
||||
|
||||
public get isSelfVerification(): boolean {
|
||||
return this.client.getUserId() === this.otherUserId;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export enum Feature {
|
||||
RelationBasedRedactions = "RelationBasedRedactions",
|
||||
AccountDataDeletion = "AccountDataDeletion",
|
||||
RelationsRecursion = "RelationsRecursion",
|
||||
IntentionalMentions = "IntentionalMentions",
|
||||
}
|
||||
|
||||
type FeatureSupportCondition = {
|
||||
@@ -60,6 +61,9 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||
[Feature.RelationsRecursion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3981"],
|
||||
},
|
||||
[Feature.IntentionalMentions]: {
|
||||
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
|
||||
},
|
||||
};
|
||||
|
||||
export async function buildFeatureSupportMap(versions: IServerVersions): Promise<Map<Feature, ServerSupport>> {
|
||||
|
||||
@@ -298,11 +298,15 @@ export class FetchHttpApi<O extends IHttpOpts> {
|
||||
* @param path - The HTTP path <b>after</b> the supplied prefix e.g. "/createRoom".
|
||||
* @param queryParams - A dict of query params (these will NOT be urlencoded).
|
||||
* @param prefix - The full prefix to use e.g. "/_matrix/client/v2_alpha", defaulting to this.opts.prefix.
|
||||
* @param baseUrl - The baseUrl to use e.g. "https://matrix.org/", defaulting to this.opts.baseUrl.
|
||||
* @param baseUrl - The baseUrl to use e.g. "https://matrix.org", defaulting to this.opts.baseUrl.
|
||||
* @returns URL
|
||||
*/
|
||||
public getUrl(path: string, queryParams?: QueryDict, prefix?: string, baseUrl?: string): URL {
|
||||
const url = new URL((baseUrl ?? this.opts.baseUrl) + (prefix ?? this.opts.prefix) + path);
|
||||
const baseUrlWithFallback = baseUrl ?? this.opts.baseUrl;
|
||||
const baseUrlWithoutTrailingSlash = baseUrlWithFallback.endsWith("/")
|
||||
? baseUrlWithFallback.slice(0, -1)
|
||||
: baseUrlWithFallback;
|
||||
const url = new URL(baseUrlWithoutTrailingSlash + (prefix ?? this.opts.prefix) + path);
|
||||
if (queryParams) {
|
||||
encodeParams(queryParams, url.searchParams);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export * from "./models/event-timeline-set";
|
||||
export * from "./models/poll";
|
||||
export * from "./models/room-member";
|
||||
export * from "./models/room-state";
|
||||
export * from "./models/thread";
|
||||
export * from "./models/typed-event-emitter";
|
||||
export * from "./models/user";
|
||||
export * from "./models/device";
|
||||
|
||||
+13
-4
@@ -34,7 +34,7 @@ import {
|
||||
import { Crypto } from "../crypto";
|
||||
import { deepSortedObjectEntries, internaliseString } from "../utils";
|
||||
import { RoomMember } from "./room-member";
|
||||
import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||
import { Thread, ThreadEvent, ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
|
||||
import { IActionsObject } from "../pushprocessor";
|
||||
import { TypedReEmitter } from "../ReEmitter";
|
||||
import { MatrixError } from "../http-api";
|
||||
@@ -579,9 +579,18 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
const relatesTo = this.getWireContent()?.["m.relates_to"];
|
||||
if (relatesTo?.rel_type === THREAD_RELATION_TYPE.name) {
|
||||
return relatesTo.event_id;
|
||||
} else {
|
||||
return this.getThread()?.id || this.threadId;
|
||||
}
|
||||
if (this.thread) {
|
||||
return this.thread.id;
|
||||
}
|
||||
if (this.threadId !== undefined) {
|
||||
return this.threadId;
|
||||
}
|
||||
const unsigned = this.getUnsigned();
|
||||
if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") {
|
||||
return unsigned[UNSIGNED_THREAD_ID_FIELD.name];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,7 +602,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
// Bundled relationships only returned when the sync response is limited
|
||||
// hence us having to check both bundled relation and inspect the thread
|
||||
// model
|
||||
return !!threadDetails || this.getThread()?.id === this.getId();
|
||||
return !!threadDetails || this.threadRootId === this.getId();
|
||||
}
|
||||
|
||||
public get replyEventId(): string | undefined {
|
||||
|
||||
+10
-21
@@ -49,7 +49,7 @@ import { BeaconEvent, BeaconEventHandlerMap } from "./beacon";
|
||||
import {
|
||||
Thread,
|
||||
ThreadEvent,
|
||||
EventHandlerMap as ThreadHandlerMap,
|
||||
ThreadEventHandlerMap as ThreadHandlerMap,
|
||||
FILTER_RELATED_BY_REL_TYPES,
|
||||
THREAD_RELATION_TYPE,
|
||||
FILTER_RELATED_BY_SENDERS,
|
||||
@@ -1957,7 +1957,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
this.on(ThreadEvent.NewReply, this.onThreadNewReply);
|
||||
this.on(ThreadEvent.NewReply, this.onThreadReply);
|
||||
this.on(ThreadEvent.Delete, this.onThreadDelete);
|
||||
this.threadsReady = true;
|
||||
}
|
||||
@@ -2055,7 +2055,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
}
|
||||
}
|
||||
|
||||
private onThreadNewReply(thread: Thread): void {
|
||||
private onThreadReply(thread: Thread): void {
|
||||
this.updateThreadRootEvents(thread, false, true);
|
||||
}
|
||||
|
||||
@@ -2113,12 +2113,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
};
|
||||
}
|
||||
|
||||
// A thread relation is always only shown in a thread
|
||||
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
// A thread relation (1st and 2nd order) is always only shown in a thread
|
||||
const threadRootId = event.threadRootId;
|
||||
if (threadRootId != undefined) {
|
||||
return {
|
||||
shouldLiveInRoom: false,
|
||||
shouldLiveInThread: true,
|
||||
threadId: event.threadRootId,
|
||||
threadId: threadRootId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2149,15 +2150,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
};
|
||||
}
|
||||
|
||||
const unsigned = event.getUnsigned();
|
||||
if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") {
|
||||
return {
|
||||
shouldLiveInRoom: false,
|
||||
shouldLiveInThread: true,
|
||||
threadId: unsigned[UNSIGNED_THREAD_ID_FIELD.name],
|
||||
};
|
||||
}
|
||||
|
||||
// We've exhausted all scenarios,
|
||||
// we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
|
||||
// adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts
|
||||
@@ -2890,12 +2882,9 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
private findThreadRoots(events: MatrixEvent[]): Set<string> {
|
||||
const threadRoots = new Set<string>();
|
||||
for (const event of events) {
|
||||
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
|
||||
threadRoots.add(event.relationEventId ?? "");
|
||||
}
|
||||
const unsigned = event.getUnsigned();
|
||||
if (typeof unsigned[UNSIGNED_THREAD_ID_FIELD.name] === "string") {
|
||||
threadRoots.add(unsigned[UNSIGNED_THREAD_ID_FIELD.name]!);
|
||||
const threadRootId = event.threadRootId;
|
||||
if (threadRootId != undefined) {
|
||||
threadRoots.add(threadRootId);
|
||||
}
|
||||
}
|
||||
return threadRoots;
|
||||
|
||||
@@ -38,15 +38,20 @@ export enum ThreadEvent {
|
||||
Delete = "Thread.delete",
|
||||
}
|
||||
|
||||
type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset;
|
||||
export type ThreadEmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset;
|
||||
|
||||
export type EventHandlerMap = {
|
||||
export type ThreadEventHandlerMap = {
|
||||
[ThreadEvent.Update]: (thread: Thread) => void;
|
||||
[ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
|
||||
[ThreadEvent.ViewThread]: () => void;
|
||||
[ThreadEvent.Delete]: (thread: Thread) => void;
|
||||
} & EventTimelineSetHandlerMap;
|
||||
|
||||
/**
|
||||
* @deprecated please use ThreadEventHandlerMap instead
|
||||
*/
|
||||
export type EventHandlerMap = ThreadEventHandlerMap;
|
||||
|
||||
interface IThreadOpts {
|
||||
room: Room;
|
||||
client: MatrixClient;
|
||||
@@ -70,7 +75,7 @@ export function determineFeatureSupport(stable: boolean, unstable: boolean): Fea
|
||||
}
|
||||
}
|
||||
|
||||
export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerMap> {
|
||||
public static hasServerSideSupport = FeatureSupport.None;
|
||||
public static hasServerSideListSupport = FeatureSupport.None;
|
||||
public static hasServerSideFwdPaginationSupport = FeatureSupport.None;
|
||||
@@ -83,7 +88,7 @@ export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
|
||||
|
||||
private _currentUserParticipated = false;
|
||||
|
||||
private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
|
||||
private reEmitter: TypedReEmitter<ThreadEmittedEvents, ThreadEventHandlerMap>;
|
||||
|
||||
private lastEvent: MatrixEvent | undefined;
|
||||
private replyCount = 0;
|
||||
|
||||
@@ -89,6 +89,24 @@ export class TypedEventEmitter<
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `emit` but calls all listeners within a `Promise.all` and returns the promise chain
|
||||
* @param event - The name of the event to emit
|
||||
* @param args - Arguments to pass to the listener
|
||||
* @returns `true` if the event had listeners, `false` otherwise.
|
||||
*/
|
||||
public async emitPromised<T extends Events>(
|
||||
event: T,
|
||||
...args: Parameters<SuperclassArguments[T]>
|
||||
): Promise<boolean>;
|
||||
public async emitPromised<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): Promise<boolean>;
|
||||
public async emitPromised<T extends Events>(event: T, ...args: any[]): Promise<boolean> {
|
||||
const listeners = this.listeners(event);
|
||||
return Promise.allSettled(listeners.map((l) => l(...args))).then(() => {
|
||||
return listeners.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of listeners listening to the event named `event`.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 2023 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 { IClientWellKnown, IDelegatedAuthConfig, M_AUTHENTICATION } from "../client";
|
||||
import { logger } from "../logger";
|
||||
|
||||
export enum OidcDiscoveryError {
|
||||
NotSupported = "OIDC authentication not supported",
|
||||
Misconfigured = "OIDC is misconfigured",
|
||||
General = "Something went wrong with OIDC discovery",
|
||||
OpSupport = "Configured OIDC OP does not support required functions",
|
||||
}
|
||||
|
||||
export type ValidatedIssuerConfig = {
|
||||
authorizationEndpoint: string;
|
||||
tokenEndpoint: string;
|
||||
registrationEndpoint?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates MSC2965 m.authentication config
|
||||
* Returns valid configuration
|
||||
* @param wellKnown - client well known as returned from ./well-known/client/matrix
|
||||
* @returns config - when present and valid
|
||||
* @throws when config is not found or invalid
|
||||
*/
|
||||
export const validateWellKnownAuthentication = (wellKnown: IClientWellKnown): IDelegatedAuthConfig => {
|
||||
const authentication = M_AUTHENTICATION.findIn<IDelegatedAuthConfig>(wellKnown);
|
||||
|
||||
if (!authentication) {
|
||||
throw new Error(OidcDiscoveryError.NotSupported);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof authentication.issuer === "string" &&
|
||||
(!authentication.hasOwnProperty("account") || typeof authentication.account === "string")
|
||||
) {
|
||||
return {
|
||||
issuer: authentication.issuer,
|
||||
account: authentication.account,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(OidcDiscoveryError.Misconfigured);
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
const requiredStringProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
|
||||
if (!wellKnown[key] || !optionalStringProperty(wellKnown, key)) {
|
||||
logger.error(`OIDC issuer configuration: ${key} is invalid`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const optionalStringProperty = (wellKnown: Record<string, unknown>, key: string): boolean => {
|
||||
if (!!wellKnown[key] && typeof wellKnown[key] !== "string") {
|
||||
logger.error(`OIDC issuer configuration: ${key} is invalid`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, value: any): boolean => {
|
||||
const array = wellKnown[key];
|
||||
if (!array || !Array.isArray(array) || !array.includes(value)) {
|
||||
logger.error(`OIDC issuer configuration: ${key} is invalid. ${value} is required.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates issue `.well-known/openid-configuration`
|
||||
* As defined in RFC5785 https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
* validates that OP is compatible with Element's OIDC flow
|
||||
* @param wellKnown - json object
|
||||
* @returns valid issuer config
|
||||
* @throws Error - when issuer config is not found or is invalid
|
||||
*/
|
||||
export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuerConfig => {
|
||||
if (!isRecord(wellKnown)) {
|
||||
logger.error("Issuer configuration not found or malformed");
|
||||
throw new Error(OidcDiscoveryError.OpSupport);
|
||||
}
|
||||
|
||||
const isInvalid = [
|
||||
requiredStringProperty(wellKnown, "authorization_endpoint"),
|
||||
requiredStringProperty(wellKnown, "token_endpoint"),
|
||||
optionalStringProperty(wellKnown, "registration_endpoint"),
|
||||
requiredArrayValue(wellKnown, "response_types_supported", "code"),
|
||||
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
|
||||
].some((isValid) => !isValid);
|
||||
|
||||
if (!isInvalid) {
|
||||
return {
|
||||
authorizationEndpoint: wellKnown["authorization_endpoint"],
|
||||
tokenEndpoint: wellKnown["token_endpoint"],
|
||||
registrationEndpoint: wellKnown["registration_endpoint"],
|
||||
} as ValidatedIssuerConfig;
|
||||
}
|
||||
|
||||
logger.error("Issuer configuration not valid");
|
||||
throw new Error(OidcDiscoveryError.OpSupport);
|
||||
};
|
||||
@@ -30,13 +30,20 @@ import { RoomEncryptor } from "./RoomEncryptor";
|
||||
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||
import { KeyClaimManager } from "./KeyClaimManager";
|
||||
import { MapWithDefault } from "../utils";
|
||||
import { BootstrapCrossSigningOpts, DeviceVerificationStatus } from "../crypto-api";
|
||||
import {
|
||||
BootstrapCrossSigningOpts,
|
||||
CrossSigningStatus,
|
||||
DeviceVerificationStatus,
|
||||
ImportRoomKeyProgressData,
|
||||
ImportRoomKeysOpts,
|
||||
} from "../crypto-api";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client";
|
||||
import { Device, DeviceMap } from "../models/device";
|
||||
import { ServerSideSecretStorage } from "../secret-storage";
|
||||
import { CrossSigningKey } from "../crypto/api";
|
||||
import { CrossSigningIdentity } from "./CrossSigningIdentity";
|
||||
import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
@@ -71,13 +78,13 @@ export class RustCrypto implements CryptoBackend {
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
|
||||
/** The local user's User ID. */
|
||||
_userId: string,
|
||||
private readonly userId: string,
|
||||
|
||||
/** The local user's Device ID. */
|
||||
_deviceId: string,
|
||||
|
||||
/** Interface to server-side secret storage */
|
||||
_secretStorage: ServerSideSecretStorage,
|
||||
private readonly secretStorage: ServerSideSecretStorage,
|
||||
) {
|
||||
this.outgoingRequestProcessor = new OutgoingRequestProcessor(olmMachine, http);
|
||||
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
|
||||
@@ -206,8 +213,22 @@ export class RustCrypto implements CryptoBackend {
|
||||
}
|
||||
|
||||
public async exportRoomKeys(): Promise<IMegolmSessionData[]> {
|
||||
// TODO
|
||||
return [];
|
||||
const raw = await this.olmMachine.exportRoomKeys(() => true);
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
public async importRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
|
||||
// TODO when backup support will be added we would need to expose the `from_backup` flag in the bindings
|
||||
const jsonKeys = JSON.stringify(keys);
|
||||
await this.olmMachine.importRoomKeys(jsonKeys, (progress: BigInt, total: BigInt) => {
|
||||
const importOpt: ImportRoomKeyProgressData = {
|
||||
total: Number(total),
|
||||
successes: Number(progress),
|
||||
stage: "load_keys",
|
||||
failures: 0,
|
||||
};
|
||||
opts?.progressCallback?.(importOpt);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +346,15 @@ export class RustCrypto implements CryptoBackend {
|
||||
* Implementation of {@link CryptoApi#isCrossSigningReady}
|
||||
*/
|
||||
public async isCrossSigningReady(): Promise<boolean> {
|
||||
return false;
|
||||
const { publicKeysOnDevice, privateKeysInSecretStorage, privateKeysCachedLocally } =
|
||||
await this.getCrossSigningStatus();
|
||||
const hasKeysInCache =
|
||||
Boolean(privateKeysCachedLocally.masterKey) &&
|
||||
Boolean(privateKeysCachedLocally.selfSigningKey) &&
|
||||
Boolean(privateKeysCachedLocally.userSigningKey);
|
||||
|
||||
// The cross signing is ready if the public and private keys are available
|
||||
return publicKeysOnDevice && (hasKeysInCache || privateKeysInSecretStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,6 +379,32 @@ export class RustCrypto implements CryptoBackend {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#getCrossSigningStatus}
|
||||
*/
|
||||
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
|
||||
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.olmMachine.getIdentity(
|
||||
new RustSdkCryptoJs.UserId(this.userId),
|
||||
);
|
||||
const publicKeysOnDevice =
|
||||
Boolean(userIdentity?.masterKey) &&
|
||||
Boolean(userIdentity?.selfSigningKey) &&
|
||||
Boolean(userIdentity?.userSigningKey);
|
||||
const privateKeysInSecretStorage = await secretStorageContainsCrossSigningKeys(this.secretStorage);
|
||||
const crossSigningStatus: RustSdkCryptoJs.CrossSigningStatus | null =
|
||||
await this.olmMachine.crossSigningStatus();
|
||||
|
||||
return {
|
||||
publicKeysOnDevice,
|
||||
privateKeysInSecretStorage,
|
||||
privateKeysCachedLocally: {
|
||||
masterKey: Boolean(crossSigningStatus?.hasMaster),
|
||||
userSigningKey: Boolean(crossSigningStatus?.hasUserSigning),
|
||||
selfSigningKey: Boolean(crossSigningStatus?.hasSelfSigning),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2023 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 { ServerSideSecretStorage } from "../secret-storage";
|
||||
|
||||
/**
|
||||
* Check that the private cross signing keys (master, self signing, user signing) are stored in the secret storage and encrypted with the same secret storage key.
|
||||
*
|
||||
* @param secretStorage - The secret store using account data
|
||||
* @returns True if the cross-signing keys are all stored and encrypted with the same secret storage key.
|
||||
*/
|
||||
export async function secretStorageContainsCrossSigningKeys(secretStorage: ServerSideSecretStorage): Promise<boolean> {
|
||||
// Check if the master cross-signing key is stored in secret storage
|
||||
const secretStorageMasterKeys = await secretStorage.isStored("m.cross_signing.master");
|
||||
|
||||
// Master key not stored
|
||||
if (!secretStorageMasterKeys) return false;
|
||||
|
||||
// Get the user signing keys stored into the secret storage
|
||||
const secretStorageUserSigningKeys = (await secretStorage.isStored(`m.cross_signing.user_signing`)) || {};
|
||||
// Get the self signing keys stored into the secret storage
|
||||
const secretStorageSelfSigningKeys = (await secretStorage.isStored(`m.cross_signing.self_signing`)) || {};
|
||||
|
||||
// Check that one of the secret storage keys used to encrypt the master key was also used to encrypt the user-signing and self-signing keys
|
||||
return Object.keys(secretStorageMasterKeys).some(
|
||||
(secretStorageKey) =>
|
||||
secretStorageUserSigningKeys[secretStorageKey] && secretStorageSelfSigningKeys[secretStorageKey],
|
||||
);
|
||||
}
|
||||
@@ -376,7 +376,7 @@ export class SlidingSyncSdk {
|
||||
});
|
||||
}
|
||||
|
||||
private onRoomData(roomId: string, roomData: MSC3575RoomData): void {
|
||||
private async onRoomData(roomId: string, roomData: MSC3575RoomData): Promise<void> {
|
||||
let room = this.client.store.getRoom(roomId);
|
||||
if (!room) {
|
||||
if (!roomData.initial) {
|
||||
@@ -385,7 +385,7 @@ export class SlidingSyncSdk {
|
||||
}
|
||||
room = _createAndReEmitRoom(this.client, roomId, this.opts);
|
||||
}
|
||||
this.processRoomData(this.client, room, roomData);
|
||||
await this.processRoomData(this.client, room!, roomData);
|
||||
}
|
||||
|
||||
private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error): void {
|
||||
|
||||
+6
-6
@@ -326,7 +326,7 @@ export enum SlidingSyncEvent {
|
||||
}
|
||||
|
||||
export type SlidingSyncEventHandlerMap = {
|
||||
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void;
|
||||
[SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => Promise<void> | void;
|
||||
[SlidingSyncEvent.Lifecycle]: (
|
||||
state: SlidingSyncState,
|
||||
resp: MSC3575SlidingSyncResponse | null,
|
||||
@@ -567,14 +567,14 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
* @param roomId - The room which received some data.
|
||||
* @param roomData - The raw sliding sync response JSON.
|
||||
*/
|
||||
private invokeRoomDataListeners(roomId: string, roomData: MSC3575RoomData): void {
|
||||
private async invokeRoomDataListeners(roomId: string, roomData: MSC3575RoomData): Promise<void> {
|
||||
if (!roomData.required_state) {
|
||||
roomData.required_state = [];
|
||||
}
|
||||
if (!roomData.timeline) {
|
||||
roomData.timeline = [];
|
||||
}
|
||||
this.emit(SlidingSyncEvent.RoomData, roomId, roomData);
|
||||
await this.emitPromised(SlidingSyncEvent.RoomData, roomId, roomData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -923,9 +923,9 @@ export class SlidingSync extends TypedEventEmitter<SlidingSyncEvent, SlidingSync
|
||||
}
|
||||
this.onPreExtensionsResponse(resp.extensions);
|
||||
|
||||
Object.keys(resp.rooms).forEach((roomId) => {
|
||||
this.invokeRoomDataListeners(roomId, resp!.rooms[roomId]);
|
||||
});
|
||||
for (const roomId in resp.rooms) {
|
||||
await this.invokeRoomDataListeners(roomId, resp!.rooms[roomId]);
|
||||
}
|
||||
|
||||
const listKeysWithUpdates: Set<string> = new Set();
|
||||
if (!doNotUpdateList) {
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ export enum CallErrorCode {
|
||||
const VOIP_PROTO_VERSION = "1";
|
||||
|
||||
/** The fallback ICE server to use for STUN or TURN protocols. */
|
||||
const FALLBACK_ICE_SERVER = "stun:turn.matrix.org";
|
||||
export const FALLBACK_ICE_SERVER = "stun:turn.matrix.org";
|
||||
|
||||
/** The length of time a call can be ringing for. */
|
||||
const CALL_TIMEOUT_MS = 60 * 1000; // ms
|
||||
|
||||
+40
-2
@@ -25,7 +25,15 @@ import { GroupCallEventHandlerEvent } from "./groupCallEventHandler";
|
||||
import { IScreensharingOpts } from "./mediaHandler";
|
||||
import { mapsEqual } from "../utils";
|
||||
import { GroupCallStats } from "./stats/groupCallStats";
|
||||
import { ByteSentStatsReport, ConnectionStatsReport, StatsReport, SummaryStatsReport } from "./stats/statsReport";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
CallFeedReport,
|
||||
ConnectionStatsReport,
|
||||
StatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "./stats/statsReport";
|
||||
import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer";
|
||||
import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter";
|
||||
|
||||
export enum GroupCallIntent {
|
||||
Ring = "m.ring",
|
||||
@@ -96,12 +104,17 @@ export enum GroupCallStatsReportEvent {
|
||||
ConnectionStats = "GroupCall.connection_stats",
|
||||
ByteSentStats = "GroupCall.byte_sent_stats",
|
||||
SummaryStats = "GroupCall.summary_stats",
|
||||
CallFeedStats = "GroupCall.call_feed_stats",
|
||||
}
|
||||
|
||||
/**
|
||||
* The final report-events that get consumed by client.
|
||||
*/
|
||||
export type GroupCallStatsReportEventHandlerMap = {
|
||||
[GroupCallStatsReportEvent.ConnectionStats]: (report: GroupCallStatsReport<ConnectionStatsReport>) => void;
|
||||
[GroupCallStatsReportEvent.ByteSentStats]: (report: GroupCallStatsReport<ByteSentStatsReport>) => void;
|
||||
[GroupCallStatsReportEvent.SummaryStats]: (report: GroupCallStatsReport<SummaryStatsReport>) => void;
|
||||
[GroupCallStatsReportEvent.CallFeedStats]: (report: GroupCallStatsReport<CallFeedReport>) => void;
|
||||
};
|
||||
|
||||
export enum GroupCallErrorCode {
|
||||
@@ -110,7 +123,9 @@ export enum GroupCallErrorCode {
|
||||
PlaceCallFailed = "place_call_failed",
|
||||
}
|
||||
|
||||
export interface GroupCallStatsReport<T extends ConnectionStatsReport | ByteSentStatsReport | SummaryStatsReport> {
|
||||
export interface GroupCallStatsReport<
|
||||
T extends ConnectionStatsReport | ByteSentStatsReport | SummaryStatsReport | CallFeedReport,
|
||||
> {
|
||||
report: T;
|
||||
}
|
||||
|
||||
@@ -269,17 +284,37 @@ export class GroupCall extends TypedEventEmitter<
|
||||
}
|
||||
|
||||
private onConnectionStats = (report: ConnectionStatsReport): void => {
|
||||
// Final emit of the summary event, to be consumed by the client
|
||||
this.emit(GroupCallStatsReportEvent.ConnectionStats, { report });
|
||||
};
|
||||
|
||||
private onByteSentStats = (report: ByteSentStatsReport): void => {
|
||||
// Final emit of the summary event, to be consumed by the client
|
||||
this.emit(GroupCallStatsReportEvent.ByteSentStats, { report });
|
||||
};
|
||||
|
||||
private onSummaryStats = (report: SummaryStatsReport): void => {
|
||||
SummaryStatsReportGatherer.extendSummaryReport(report, this.participants);
|
||||
// Final emit of the summary event, to be consumed by the client
|
||||
this.emit(GroupCallStatsReportEvent.SummaryStats, { report });
|
||||
};
|
||||
|
||||
private onCallFeedReport = (report: CallFeedReport): void => {
|
||||
if (this.localCallFeed) {
|
||||
report = CallFeedStatsReporter.expandCallFeedReport(report, [this.localCallFeed], "from-local-feed");
|
||||
}
|
||||
|
||||
const callFeeds: CallFeed[] = [];
|
||||
this.forEachCall((call) => {
|
||||
if (call.callId === report.callId) {
|
||||
call.getFeeds().forEach((f) => callFeeds.push(f));
|
||||
}
|
||||
});
|
||||
|
||||
report = CallFeedStatsReporter.expandCallFeedReport(report, callFeeds, "from-call-feed");
|
||||
this.emit(GroupCallStatsReportEvent.CallFeedStats, { report });
|
||||
};
|
||||
|
||||
public async create(): Promise<GroupCall> {
|
||||
this.creationTs = Date.now();
|
||||
this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this);
|
||||
@@ -1595,6 +1630,8 @@ export class GroupCall extends TypedEventEmitter<
|
||||
});
|
||||
|
||||
if (this.state === GroupCallState.Entered) this.placeOutgoingCalls();
|
||||
|
||||
// Update the participants stored in the stats object
|
||||
};
|
||||
|
||||
private onStateChanged = (newState: GroupCallState, oldState: GroupCallState): void => {
|
||||
@@ -1632,6 +1669,7 @@ export class GroupCall extends TypedEventEmitter<
|
||||
this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
|
||||
this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
|
||||
this.stats.reports.on(StatsReport.SUMMARY_STATS, this.onSummaryStats);
|
||||
this.stats.reports.on(StatsReport.CALL_FEED_REPORT, this.onCallFeedReport);
|
||||
}
|
||||
return this.stats;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
Copyright 2023 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 { CallFeedReport, CallFeedStats, TrackStats, TransceiverStats } from "./statsReport";
|
||||
import { CallFeed } from "../callFeed";
|
||||
|
||||
export class CallFeedStatsReporter {
|
||||
public static buildCallFeedReport(callId: string, opponentMemberId: string, pc: RTCPeerConnection): CallFeedReport {
|
||||
const rtpTransceivers = pc.getTransceivers();
|
||||
const transceiver: TransceiverStats[] = [];
|
||||
const callFeeds: CallFeedStats[] = [];
|
||||
|
||||
rtpTransceivers.forEach((t) => {
|
||||
const sender = t.sender?.track ? CallFeedStatsReporter.buildTrackStats(t.sender.track, "sender") : null;
|
||||
const receiver = CallFeedStatsReporter.buildTrackStats(t.receiver.track, "receiver");
|
||||
transceiver.push({
|
||||
mid: t.mid == null ? "null" : t.mid,
|
||||
direction: t.direction,
|
||||
currentDirection: t.currentDirection == null ? "null" : t.currentDirection,
|
||||
sender,
|
||||
receiver,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
callId,
|
||||
opponentMemberId,
|
||||
transceiver,
|
||||
callFeeds,
|
||||
};
|
||||
}
|
||||
|
||||
private static buildTrackStats(track: MediaStreamTrack, label = "--"): TrackStats {
|
||||
const settingDeviceId = track.getSettings()?.deviceId;
|
||||
const constrainDeviceId = track.getConstraints()?.deviceId;
|
||||
|
||||
return {
|
||||
id: track.id,
|
||||
kind: track.kind,
|
||||
settingDeviceId: settingDeviceId ? settingDeviceId : "unknown",
|
||||
constrainDeviceId: constrainDeviceId ? constrainDeviceId : "unknown",
|
||||
muted: track.muted,
|
||||
enabled: track.enabled,
|
||||
readyState: track.readyState,
|
||||
label,
|
||||
} as TrackStats;
|
||||
}
|
||||
|
||||
public static expandCallFeedReport(
|
||||
report: CallFeedReport,
|
||||
callFeeds: CallFeed[],
|
||||
prefix = "unknown",
|
||||
): CallFeedReport {
|
||||
if (!report.callFeeds) {
|
||||
report.callFeeds = [];
|
||||
}
|
||||
callFeeds.forEach((feed) => {
|
||||
const audioTracks = feed.stream.getAudioTracks();
|
||||
const videoTracks = feed.stream.getVideoTracks();
|
||||
const audio =
|
||||
audioTracks.length > 0
|
||||
? CallFeedStatsReporter.buildTrackStats(feed.stream.getAudioTracks()[0], feed.purpose)
|
||||
: null;
|
||||
const video =
|
||||
videoTracks.length > 0
|
||||
? CallFeedStatsReporter.buildTrackStats(feed.stream.getVideoTracks()[0], feed.purpose)
|
||||
: null;
|
||||
const feedStats = {
|
||||
stream: feed.stream.id,
|
||||
type: feed.isLocal() ? "local" : "remote",
|
||||
audio,
|
||||
video,
|
||||
purpose: feed.purpose,
|
||||
prefix,
|
||||
isVideoMuted: feed.isVideoMuted(),
|
||||
isAudioMuted: feed.isAudioMuted(),
|
||||
} as CallFeedStats;
|
||||
report.callFeeds.push(feedStats);
|
||||
});
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import { TrackStatsBuilder } from "./trackStatsBuilder";
|
||||
import { ConnectionStatsReportBuilder } from "./connectionStatsReportBuilder";
|
||||
import { ValueFormatter } from "./valueFormatter";
|
||||
import { CallStatsReportSummary } from "./callStatsReportSummary";
|
||||
import { logger } from "../../logger";
|
||||
import { CallFeedStatsReporter } from "./callFeedStatsReporter";
|
||||
|
||||
export class CallStatsReportGatherer {
|
||||
private isActive = true;
|
||||
@@ -62,10 +64,11 @@ export class CallStatsReportGatherer {
|
||||
.then((report) => {
|
||||
// @ts-ignore
|
||||
this.currentStatsReport = typeof report?.result === "function" ? report.result() : report;
|
||||
|
||||
try {
|
||||
this.processStatsReport(groupCallId, localUserId);
|
||||
} catch (error) {
|
||||
this.isActive = false;
|
||||
this.handleError(error);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@@ -161,6 +164,9 @@ export class CallStatsReportGatherer {
|
||||
});
|
||||
|
||||
this.emitter.emitByteSendReport(byteSentStatsReport);
|
||||
this.emitter.emitCallFeedReport(
|
||||
CallFeedStatsReporter.buildCallFeedReport(this.callId, this.opponentMemberId, this.pc),
|
||||
);
|
||||
this.processAndEmitConnectionStatsReport();
|
||||
}
|
||||
|
||||
@@ -172,8 +178,9 @@ export class CallStatsReportGatherer {
|
||||
return this.isActive;
|
||||
}
|
||||
|
||||
private handleError(_: any): void {
|
||||
private handleError(error: any): void {
|
||||
this.isActive = false;
|
||||
logger.warn(`CallStatsReportGatherer ${this.callId} processStatsReport fails and set to inactive ${error}`);
|
||||
}
|
||||
|
||||
private processAndEmitConnectionStatsReport(): void {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { CallStatsReportGatherer } from "./callStatsReportGatherer";
|
||||
import { StatsReportEmitter } from "./statsReportEmitter";
|
||||
import { CallStatsReportSummary } from "./callStatsReportSummary";
|
||||
import { SummaryStatsReportGatherer } from "./summaryStatsReportGatherer";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export class GroupCallStats {
|
||||
private timer: undefined | ReturnType<typeof setTimeout>;
|
||||
@@ -75,7 +76,11 @@ export class GroupCallStats {
|
||||
summary.push(c.processStats(this.groupCallId, this.userId));
|
||||
});
|
||||
|
||||
Promise.all(summary).then((s: Awaited<CallStatsReportSummary>[]) => this.summaryStatsReportGatherer.build(s));
|
||||
Promise.all(summary)
|
||||
.then((s: Awaited<CallStatsReportSummary>[]) => this.summaryStatsReportGatherer.build(s))
|
||||
.catch((err) => {
|
||||
logger.error("Could not build summary stats report", err);
|
||||
});
|
||||
}
|
||||
|
||||
public setInterval(interval: number): void {
|
||||
|
||||
@@ -20,19 +20,22 @@ import { Resolution } from "./media/mediaTrackStats";
|
||||
|
||||
export enum StatsReport {
|
||||
CONNECTION_STATS = "StatsReport.connection_stats",
|
||||
CALL_FEED_REPORT = "StatsReport.call_feed_report",
|
||||
BYTE_SENT_STATS = "StatsReport.byte_sent_stats",
|
||||
SUMMARY_STATS = "StatsReport.summary_stats",
|
||||
}
|
||||
|
||||
export type TrackID = string;
|
||||
export type ByteSend = number;
|
||||
|
||||
/// ByteSentStatsReport ################################################################################################
|
||||
export interface ByteSentStatsReport extends Map<TrackID, ByteSend> {
|
||||
callId?: string;
|
||||
opponentMemberId?: string;
|
||||
// is a map: `local trackID` => byte send
|
||||
}
|
||||
|
||||
export type TrackID = string;
|
||||
export type ByteSend = number;
|
||||
|
||||
/// ConnectionStatsReport ##############################################################################################
|
||||
export interface ConnectionStatsReport {
|
||||
callId?: string;
|
||||
opponentMemberId?: string;
|
||||
@@ -68,6 +71,7 @@ export interface CodecMap {
|
||||
remote: Map<TrackID, string>;
|
||||
}
|
||||
|
||||
/// SummaryStatsReport #################################################################################################
|
||||
export interface SummaryStatsReport {
|
||||
/**
|
||||
* Aggregated the information for percentage of received media
|
||||
@@ -83,4 +87,47 @@ export interface SummaryStatsReport {
|
||||
maxPacketLoss: number;
|
||||
percentageConcealedAudio: number;
|
||||
peerConnections: number;
|
||||
opponentUsersInCall?: number;
|
||||
opponentDevicesInCall?: number;
|
||||
diffDevicesToPeerConnections?: number;
|
||||
ratioPeerConnectionToDevices?: number;
|
||||
// Todo: Decide if we want an index (or a timestamp) of this report in relation to the group call, to help differenciate when issues occur and ignore/track initial connection delays.
|
||||
}
|
||||
|
||||
/// CallFeedReport #####################################################################################################
|
||||
export interface CallFeedReport {
|
||||
callId: string;
|
||||
opponentMemberId: string;
|
||||
transceiver: TransceiverStats[];
|
||||
callFeeds: CallFeedStats[];
|
||||
}
|
||||
|
||||
export interface CallFeedStats {
|
||||
stream: string;
|
||||
type: "remote" | "local";
|
||||
audio: TrackStats | null;
|
||||
video: TrackStats | null;
|
||||
purpose: string;
|
||||
prefix: string;
|
||||
isVideoMuted: boolean;
|
||||
isAudioMuted: boolean;
|
||||
}
|
||||
|
||||
export interface TransceiverStats {
|
||||
readonly mid: string;
|
||||
readonly sender: TrackStats | null;
|
||||
readonly receiver: TrackStats | null;
|
||||
readonly direction: string;
|
||||
readonly currentDirection: string;
|
||||
}
|
||||
|
||||
export interface TrackStats {
|
||||
readonly id: string;
|
||||
readonly kind: "audio" | "video";
|
||||
readonly settingDeviceId: string;
|
||||
readonly constrainDeviceId: string;
|
||||
readonly muted: boolean;
|
||||
readonly enabled: boolean;
|
||||
readonly readyState: "ended" | "live";
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,18 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { TypedEventEmitter } from "../../models/typed-event-emitter";
|
||||
import { ByteSentStatsReport, ConnectionStatsReport, StatsReport, SummaryStatsReport } from "./statsReport";
|
||||
import {
|
||||
ByteSentStatsReport,
|
||||
CallFeedReport,
|
||||
ConnectionStatsReport,
|
||||
StatsReport,
|
||||
SummaryStatsReport,
|
||||
} from "./statsReport";
|
||||
|
||||
export type StatsReportHandlerMap = {
|
||||
[StatsReport.BYTE_SENT_STATS]: (report: ByteSentStatsReport) => void;
|
||||
[StatsReport.CONNECTION_STATS]: (report: ConnectionStatsReport) => void;
|
||||
[StatsReport.CALL_FEED_REPORT]: (report: CallFeedReport) => void;
|
||||
[StatsReport.SUMMARY_STATS]: (report: SummaryStatsReport) => void;
|
||||
};
|
||||
|
||||
@@ -32,6 +39,10 @@ export class StatsReportEmitter extends TypedEventEmitter<StatsReport, StatsRepo
|
||||
this.emit(StatsReport.CONNECTION_STATS, report);
|
||||
}
|
||||
|
||||
public emitCallFeedReport(report: CallFeedReport): void {
|
||||
this.emit(StatsReport.CALL_FEED_REPORT, report);
|
||||
}
|
||||
|
||||
public emitSummaryStatsReport(report: SummaryStatsReport): void {
|
||||
this.emit(StatsReport.SUMMARY_STATS, report);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ limitations under the License.
|
||||
import { StatsReportEmitter } from "./statsReportEmitter";
|
||||
import { CallStatsReportSummary } from "./callStatsReportSummary";
|
||||
import { SummaryStatsReport } from "./statsReport";
|
||||
import { ParticipantState } from "../groupCall";
|
||||
import { RoomMember } from "../../matrix";
|
||||
|
||||
interface CallStatsReportSummaryCounter {
|
||||
receivedAudio: number;
|
||||
@@ -31,9 +33,12 @@ export class SummaryStatsReportGatherer {
|
||||
// webrtcStats as basement all the calculation are 0. We don't want track the 0 stats.
|
||||
const summary = allSummary.filter((s) => !s.isFirstCollection);
|
||||
const summaryTotalCount = summary.length;
|
||||
// For counting the peer connections we also want to consider the ignored summaries
|
||||
const peerConnectionsCount = allSummary.length;
|
||||
if (summaryTotalCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const summaryCounter: CallStatsReportSummaryCounter = {
|
||||
receivedAudio: 0,
|
||||
receivedVideo: 0,
|
||||
@@ -65,11 +70,33 @@ export class SummaryStatsReportGatherer {
|
||||
? (summaryCounter.concealedAudio / summaryCounter.totalAudio).toFixed(decimalPlaces)
|
||||
: 0,
|
||||
),
|
||||
peerConnections: summaryTotalCount,
|
||||
peerConnections: peerConnectionsCount,
|
||||
} as SummaryStatsReport;
|
||||
this.emitter.emitSummaryStatsReport(report);
|
||||
}
|
||||
|
||||
public static extendSummaryReport(
|
||||
report: SummaryStatsReport,
|
||||
callParticipants: Map<RoomMember, Map<string, ParticipantState>>,
|
||||
): void {
|
||||
// Calculate the actual number of devices based on the participants state event
|
||||
// (this is used, to compare the expected participant count from the room state with the acutal peer connections)
|
||||
// const devices = callParticipants.()
|
||||
const devices: [string, ParticipantState][] = [];
|
||||
const users: [RoomMember, Map<string, ParticipantState>][] = [];
|
||||
for (const userEntry of callParticipants) {
|
||||
users.push(userEntry);
|
||||
for (const device of userEntry[1]) {
|
||||
devices.push(device);
|
||||
}
|
||||
}
|
||||
report.opponentDevicesInCall = Math.max(0, devices.length - 1);
|
||||
report.opponentUsersInCall = Math.max(0, users.length - 1);
|
||||
report.diffDevicesToPeerConnections = Math.max(0, devices.length - 1) - report.peerConnections;
|
||||
report.ratioPeerConnectionToDevices =
|
||||
Math.max(0, devices.length - 1) == 0 ? 0 : report.peerConnections / (devices.length - 1);
|
||||
}
|
||||
|
||||
private countTrackListReceivedMedia(counter: CallStatsReportSummaryCounter, stats: CallStatsReportSummary): void {
|
||||
let hasReceivedAudio = false;
|
||||
let hasReceivedVideo = false;
|
||||
|
||||
@@ -1055,7 +1055,7 @@
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@es-joy/jsdoccomment@~0.39.3":
|
||||
"@es-joy/jsdoccomment@~0.39.4":
|
||||
version "0.39.4"
|
||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz#6b8a62e9b3077027837728818d3c4389a898b392"
|
||||
integrity sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==
|
||||
@@ -1091,10 +1091,10 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.40.0.tgz#3ba73359e11f5a7bd3e407f70b3528abfae69cec"
|
||||
integrity sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==
|
||||
"@eslint/js@8.41.0":
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.41.0.tgz#080321c3b68253522f7646b55b577dd99d2950b3"
|
||||
integrity sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.8":
|
||||
version "0.11.8"
|
||||
@@ -1385,7 +1385,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
|
||||
|
||||
"@jridgewell/source-map@^0.3.2":
|
||||
"@jridgewell/source-map@^0.3.3":
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda"
|
||||
integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==
|
||||
@@ -1426,14 +1426,13 @@
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.9":
|
||||
version "0.1.0-alpha.9"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.9.tgz#00bc266781502641a661858a5a521dd4d95275fc"
|
||||
integrity sha512-g5cjpFwA9h0CbEGoAqNVI2QcyDsbI8FHoLo9+OXWHIezEKITsSv78mc5ilIwN+2YpmVlH0KNeQWTHw4vi0BMnw==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.10":
|
||||
version "0.1.0-alpha.10"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
||||
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
||||
|
||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||
version "3.2.14"
|
||||
uid acd96c00a881d0f462e1f97a56c73742c8dbc984
|
||||
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz#acd96c00a881d0f462e1f97a56c73742c8dbc984"
|
||||
|
||||
"@microsoft/tsdoc-config@0.16.2":
|
||||
@@ -1709,9 +1708,9 @@
|
||||
integrity sha512-dgMN+syt1xb7Hk8LU6AODOfPlvz5z1CbXpPuJE5ZrX9STfBOIXF09pEB8N7a97WT9dbngt3ksDCm6GW6yMrxfQ==
|
||||
|
||||
"@types/debug@^4.1.7":
|
||||
version "4.1.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
|
||||
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
|
||||
integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
|
||||
dependencies:
|
||||
"@types/ms" "*"
|
||||
|
||||
@@ -1754,9 +1753,9 @@
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^29.0.0":
|
||||
version "29.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47"
|
||||
integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==
|
||||
version "29.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.2.tgz#86b4afc86e3a8f3005b297ed8a72494f89e6395b"
|
||||
integrity sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==
|
||||
dependencies:
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
@@ -1771,9 +1770,9 @@
|
||||
parse5 "^7.0.0"
|
||||
|
||||
"@types/json-schema@^7.0.9":
|
||||
version "7.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
|
||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
@@ -1786,14 +1785,14 @@
|
||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||
|
||||
"@types/node@*":
|
||||
version "20.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.1.5.tgz#e94b604c67fc408f215fcbf3bd84d4743bf7f710"
|
||||
integrity sha512-IvGD1CD/nego63ySR7vrAKEX3AJTcmrAN2kn+/sDNLi1Ff5kBzDeEdqWDplK+0HAEoLYej137Sk0cUU8OLOlMg==
|
||||
version "20.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
|
||||
integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
|
||||
|
||||
"@types/node@18":
|
||||
version "18.16.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.10.tgz#9b16d918f4f6fec6cae4af34283a91d555b81519"
|
||||
integrity sha512-sMo3EngB6QkMBlB9rBe1lFdKSLqljyWPPWv6/FzSxh/IDlyVWSzE9RiF4eAuerQHybrWdqBgAGb03PM89qOasA==
|
||||
version "18.16.16"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.16.tgz#3b64862856c7874ccf7439e6bab872d245c86d8e"
|
||||
integrity sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.1"
|
||||
@@ -1801,9 +1800,9 @@
|
||||
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
|
||||
|
||||
"@types/prettier@^2.1.5":
|
||||
version "2.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0"
|
||||
integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f"
|
||||
integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==
|
||||
|
||||
"@types/retry@0.12.0":
|
||||
version "0.12.0"
|
||||
@@ -1853,14 +1852,14 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.45.0":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.6.tgz#a350faef1baa1e961698240f922d8de1761a9e2b"
|
||||
integrity sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.8.tgz#1e7a3e5318ece22251dfbc5c9c6feeb4793cc509"
|
||||
integrity sha512-JDMOmhXteJ4WVKOiHXGCoB96ADWg9q7efPWHRViT/f09bA8XOMLAVHHju3l0MkZnG1izaWXYmgvQcUjTRcpShQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.59.6"
|
||||
"@typescript-eslint/type-utils" "5.59.6"
|
||||
"@typescript-eslint/utils" "5.59.6"
|
||||
"@typescript-eslint/scope-manager" "5.59.8"
|
||||
"@typescript-eslint/type-utils" "5.59.8"
|
||||
"@typescript-eslint/utils" "5.59.8"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
@@ -1869,13 +1868,13 @@
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/parser@^5.45.0":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.6.tgz#bd36f71f5a529f828e20b627078d3ed6738dbb40"
|
||||
integrity sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.8.tgz#60cbb00671d86cf746044ab797900b1448188567"
|
||||
integrity sha512-AnR19RjJcpjoeGojmwZtCwBX/RidqDZtzcbG3xHrmz0aHHoOcbWnpDllenRDmDvsV0RQ6+tbb09/kyc+UT9Orw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.59.6"
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/typescript-estree" "5.59.6"
|
||||
"@typescript-eslint/scope-manager" "5.59.8"
|
||||
"@typescript-eslint/types" "5.59.8"
|
||||
"@typescript-eslint/typescript-estree" "5.59.8"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.59.6":
|
||||
@@ -1886,13 +1885,21 @@
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
"@typescript-eslint/visitor-keys" "5.59.6"
|
||||
|
||||
"@typescript-eslint/type-utils@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.6.tgz#37c51d2ae36127d8b81f32a0a4d2efae19277c48"
|
||||
integrity sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==
|
||||
"@typescript-eslint/scope-manager@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.8.tgz#ff4ad4fec6433647b817c4a7d4b4165d18ea2fa8"
|
||||
integrity sha512-/w08ndCYI8gxGf+9zKf1vtx/16y8MHrZs5/tnjHhMLNSixuNcJavSX4wAiPf4aS5x41Es9YPCn44MIe4cxIlig==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.59.6"
|
||||
"@typescript-eslint/utils" "5.59.6"
|
||||
"@typescript-eslint/types" "5.59.8"
|
||||
"@typescript-eslint/visitor-keys" "5.59.8"
|
||||
|
||||
"@typescript-eslint/type-utils@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.8.tgz#aa6c029a9d7706d26bbd25eb4666398781df6ea2"
|
||||
integrity sha512-+5M518uEIHFBy3FnyqZUF3BMP+AXnYn4oyH8RF012+e7/msMY98FhGL5SrN29NQ9xDgvqCgYnsOiKp1VjZ/fpA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.59.8"
|
||||
"@typescript-eslint/utils" "5.59.8"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
@@ -1901,6 +1908,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.6.tgz#5a6557a772af044afe890d77c6a07e8c23c2460b"
|
||||
integrity sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==
|
||||
|
||||
"@typescript-eslint/types@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.8.tgz#212e54414733618f5d0fd50b2da2717f630aebf8"
|
||||
integrity sha512-+uWuOhBTj/L6awoWIg0BlWy0u9TyFpCHrAuQ5bNfxDaZ1Ppb3mx6tUigc74LHcbHpOHuOTOJrBoAnhdHdaea1w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.59.6":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz#2fb80522687bd3825504925ea7e1b8de7bb6251b"
|
||||
@@ -1914,7 +1926,34 @@
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.59.6", "@typescript-eslint/utils@^5.10.0":
|
||||
"@typescript-eslint/typescript-estree@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.8.tgz#801a7b1766481629481b3b0878148bd7a1f345d7"
|
||||
integrity sha512-Jy/lPSDJGNow14vYu6IrW790p7HIf/SOV1Bb6lZ7NUkLc2iB2Z9elESmsaUtLw8kVqogSbtLH9tut5GCX1RLDg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.8"
|
||||
"@typescript-eslint/visitor-keys" "5.59.8"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.8.tgz#34d129f35a2134c67fdaf024941e8f96050dca2b"
|
||||
integrity sha512-Tr65630KysnNn9f9G7ROF3w1b5/7f6QVCJ+WK9nhIocWmx9F+TmCAcglF26Vm7z8KCTwoKcNEBZrhlklla3CKg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.59.8"
|
||||
"@typescript-eslint/types" "5.59.8"
|
||||
"@typescript-eslint/typescript-estree" "5.59.8"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0":
|
||||
version "5.59.6"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.6.tgz#82960fe23788113fc3b1f9d4663d6773b7907839"
|
||||
integrity sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==
|
||||
@@ -1936,6 +1975,14 @@
|
||||
"@typescript-eslint/types" "5.59.6"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.59.8":
|
||||
version "5.59.8"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.8.tgz#aa6a7ef862add919401470c09e1609392ef3cc40"
|
||||
integrity sha512-pJhi2ms0x0xgloT7xYabil3SGGlojNNKjK/q6dB3Ey0uJLMjK2UDGJvHieiyJVW/7C3KI+Z4Q3pEHkm4ejA+xQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.59.8"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
JSONStream@^1.0.3:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
|
||||
@@ -2008,7 +2055,7 @@ acorn@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.1.0, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.8.0, acorn@^8.8.1:
|
||||
acorn@^8.1.0, acorn@^8.4.1, acorn@^8.8.0, acorn@^8.8.1, acorn@^8.8.2:
|
||||
version "8.8.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||
@@ -3576,12 +3623,12 @@ eslint-plugin-jest@^27.1.6:
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
|
||||
eslint-plugin-jsdoc@^44.0.0:
|
||||
version "44.2.4"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-44.2.4.tgz#0bdc163771504ec7330414eda6a7dbae67156ddb"
|
||||
integrity sha512-/EMMxCyRh1SywhCb66gAqoGX4Yv6Xzc4bsSkF1AiY2o2+bQmGMQ05QZ5+JjHbdFTPDZY9pfn+DsSNP0a5yQpIg==
|
||||
eslint-plugin-jsdoc@^45.0.0:
|
||||
version "45.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-45.0.0.tgz#6be84e4842a7138cc571a907ea9c31c42eaac5c0"
|
||||
integrity sha512-l2+Jcs/Ps7oFA+SWY+0sweU/e5LgricnEl6EsDlyRTF5y0+NWL1y9Qwz9PHwHAxtdJq6lxPjEQWmYLMkvhzD4g==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.39.3"
|
||||
"@es-joy/jsdoccomment" "~0.39.4"
|
||||
are-docs-informative "^0.0.2"
|
||||
comment-parser "1.3.1"
|
||||
debug "^4.3.4"
|
||||
@@ -3656,15 +3703,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
|
||||
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
|
||||
|
||||
eslint@8.40.0:
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.40.0.tgz#a564cd0099f38542c4e9a2f630fa45bf33bc42a4"
|
||||
integrity sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==
|
||||
eslint@8.41.0:
|
||||
version "8.41.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.41.0.tgz#3062ca73363b4714b16dbc1e60f035e6134b6f1c"
|
||||
integrity sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@eslint/eslintrc" "^2.0.3"
|
||||
"@eslint/js" "8.40.0"
|
||||
"@eslint/js" "8.41.0"
|
||||
"@humanwhocodes/config-array" "^0.11.8"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
"@nodelib/fs.walk" "^1.2.8"
|
||||
@@ -3684,13 +3731,12 @@ eslint@8.40.0:
|
||||
find-up "^5.0.0"
|
||||
glob-parent "^6.0.2"
|
||||
globals "^13.19.0"
|
||||
grapheme-splitter "^1.0.4"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
is-glob "^4.0.0"
|
||||
is-path-inside "^3.0.3"
|
||||
js-sdsl "^4.1.4"
|
||||
js-yaml "^4.1.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.4.1"
|
||||
@@ -4131,15 +4177,15 @@ glob-to-regexp@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
|
||||
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
|
||||
|
||||
glob@^10.0.0:
|
||||
version "10.2.4"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.4.tgz#f5bf7ddb080e3e9039b148a9e2aef3d5ebfc0a25"
|
||||
integrity sha512-fDboBse/sl1oXSLhIp0FcCJgzW9KmhC/q8ULTKC82zc+DL3TL7FNb8qlt5qqXN53MsKEUSIcb+7DLmEygOE5Yw==
|
||||
glob@^10.2.5:
|
||||
version "10.2.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.6.tgz#1e27edbb3bbac055cb97113e27a066c100a4e5e1"
|
||||
integrity sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA==
|
||||
dependencies:
|
||||
foreground-child "^3.1.0"
|
||||
jackspeak "^2.0.3"
|
||||
minimatch "^9.0.0"
|
||||
minipass "^5.0.0 || ^6.0.0"
|
||||
minimatch "^9.0.1"
|
||||
minipass "^5.0.0 || ^6.0.2"
|
||||
path-scurry "^1.7.0"
|
||||
|
||||
glob@^7.1.0, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0:
|
||||
@@ -4221,6 +4267,11 @@ grapheme-splitter@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
graphemer@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
|
||||
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
|
||||
|
||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa"
|
||||
@@ -4760,9 +4811,9 @@ istanbul-reports@^3.1.3, istanbul-reports@^3.1.4:
|
||||
istanbul-lib-report "^3.0.0"
|
||||
|
||||
jackspeak@^2.0.3:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.0.tgz#497cbaedc902ec3f31d5d61be804d2364ff9ddad"
|
||||
integrity sha512-r5XBrqIJfwRIjRt/Xr5fv9Wh09qyhHfKnYddDlpM+ibRR20qrYActpCAgU6U+d53EOEjzkvxPMVHSlgR7leXrQ==
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6"
|
||||
integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==
|
||||
dependencies:
|
||||
"@isaacs/cliui" "^8.0.2"
|
||||
optionalDependencies:
|
||||
@@ -5205,11 +5256,6 @@ jju@~1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
|
||||
integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==
|
||||
|
||||
js-sdsl@^4.1.4:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
|
||||
integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==
|
||||
|
||||
js-stringify@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
|
||||
@@ -5499,9 +5545,9 @@ lru-cache@^6.0.0:
|
||||
yallist "^4.0.0"
|
||||
|
||||
lru-cache@^9.1.1:
|
||||
version "9.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1"
|
||||
integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835"
|
||||
integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==
|
||||
|
||||
lru-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
@@ -5675,6 +5721,13 @@ minimatch@^9.0.0:
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253"
|
||||
integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566"
|
||||
@@ -5685,10 +5738,10 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
"minipass@^5.0.0 || ^6.0.0":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.1.tgz#315417c259cb32a1b2fc530c0e7f55c901a60a6d"
|
||||
integrity sha512-Tenl5QPpgozlOGBiveNYHg2f6y+VpxsXRoIHFUVJuSmTonXRAE6q9b8Mp/O46762/2AlW4ye4Nkyvx0fgWDKbw==
|
||||
"minipass@^5.0.0 || ^6.0.2":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81"
|
||||
integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==
|
||||
|
||||
mkdirp-classic@^0.5.2:
|
||||
version "0.5.3"
|
||||
@@ -6059,12 +6112,12 @@ path-platform@~0.11.15:
|
||||
integrity sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==
|
||||
|
||||
path-scurry@^1.7.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.9.1.tgz#838566bb22e38feaf80ecd49ae06cd12acd782ee"
|
||||
integrity sha512-UgmoiySyjFxP6tscZDgWGEAgsW5ok8W3F5CJDnnH2pozwSTGE6eH7vwTotMwATWA2r5xqdkKdxYPkwlJjAI/3g==
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.9.2.tgz#90f9d296ac5e37e608028e28a447b11d385b3f63"
|
||||
integrity sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==
|
||||
dependencies:
|
||||
lru-cache "^9.1.1"
|
||||
minipass "^5.0.0 || ^6.0.0"
|
||||
minipass "^5.0.0 || ^6.0.2"
|
||||
|
||||
path-to-regexp@^2.2.1:
|
||||
version "2.4.0"
|
||||
@@ -6665,11 +6718,11 @@ rimraf@^3.0.2:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.0.tgz#5bda14e410d7e4dd522154891395802ce032c2cb"
|
||||
integrity sha512-Jf9llaP+RvaEVS5nPShYFhtXIrb3LRKP281ib3So0KkeZKo2wIKyq0Re7TOSwanasA423PSr6CCIL4bP6T040g==
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0"
|
||||
integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==
|
||||
dependencies:
|
||||
glob "^10.0.0"
|
||||
glob "^10.2.5"
|
||||
|
||||
ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||
version "2.0.2"
|
||||
@@ -7034,9 +7087,9 @@ string_decoder@~1.1.1:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2"
|
||||
integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
|
||||
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
@@ -7143,12 +7196,12 @@ tapable@^2.2.0:
|
||||
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
|
||||
|
||||
terser@^5.5.1:
|
||||
version "5.17.4"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.4.tgz#b0c2d94897dfeba43213ed5f90ed117270a2c696"
|
||||
integrity sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==
|
||||
version "5.17.7"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.7.tgz#2a8b134826fe179b711969fd9d9a0c2479b2a8c3"
|
||||
integrity sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
"@jridgewell/source-map" "^0.3.3"
|
||||
acorn "^8.8.2"
|
||||
commander "^2.20.0"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user