Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7616bd68 | |||
| 6d7699cb4a | |||
| 6b0d2a4ad3 | |||
| 72519a0eb4 | |||
| 624b8a242e | |||
| 5be104a35c | |||
| c93128ed39 | |||
| cc238c24ab | |||
| 8175683d4b | |||
| 4e54b7aab4 | |||
| a2fd06bdf9 | |||
| 7d8cbd6ef0 | |||
| 44158bc843 | |||
| e4590cac94 | |||
| fd9a44e701 | |||
| df492800b4 | |||
| 161da05972 | |||
| ed397d99ed | |||
| c0e30ceca0 | |||
| 61375ef38a | |||
| e5fda72884 | |||
| acfc619f31 | |||
| 7a0af07bf7 | |||
| ad1afbc45b | |||
| b4ce4ce184 | |||
| ddbc66b905 | |||
| ad1c67e33e | |||
| d473e16df7 | |||
| 806538828e | |||
| 424c258a07 | |||
| ea67d3977e | |||
| 8bffa39eb9 | |||
| f13967aaec | |||
| b496601712 | |||
| ce60162827 | |||
| 1f1d6f0bc8 | |||
| 35fe7bc60a | |||
| b45d51a131 | |||
| a6fd28b03d | |||
| 78e7e2af31 | |||
| 86494c3a96 | |||
| bdd4d82cb3 | |||
| 5babcaf4b3 | |||
| ffbb4716c4 | |||
| 07f97d724f | |||
| 72ee5504d5 | |||
| 9134471dc7 | |||
| f22d5e9d47 | |||
| ffb228bf5a | |||
| bed4e9579e | |||
| 7697338c7e | |||
| 1da26b5cd1 | |||
| 5b85ae491e | |||
| 2024b070b0 | |||
| eef964f07d | |||
| 1e9c119159 | |||
| fd894309f2 | |||
| 75486b72a6 |
@@ -27,7 +27,7 @@ jobs:
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Sonarcloud is done.
|
||||
- uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
run: |
|
||||
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
|
||||
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
reports=$(find coverage -type f -name '*sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
|
||||
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
|
||||
|
||||
- name: "🩻 SonarCloud Scan"
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
- uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||
- uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
steps:
|
||||
- name: Skip SonarCloud on merge queues
|
||||
if: env.ENABLE_COVERAGE == 'false'
|
||||
uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||
uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -1,3 +1,63 @@
|
||||
Changes in [36.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.2.0) (2025-02-11)
|
||||
==================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* [Backport staging] Deprecate parameter and functions using legacy crypto in `models/event.ts` ([#4700](https://github.com/matrix-org/matrix-js-sdk/pull/4700)). Contributed by @RiotRobot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Improve types around Terms ([#4674](https://github.com/matrix-org/matrix-js-sdk/pull/4674)). Contributed by @t3chguy.
|
||||
* Provide more options for starting dehydration ([#4664](https://github.com/matrix-org/matrix-js-sdk/pull/4664)). Contributed by @uhoreg.
|
||||
* Device Dehydration | js-sdk: store/load dehydration key ([#4599](https://github.com/matrix-org/matrix-js-sdk/pull/4599)). Contributed by @BillCarsonFr.
|
||||
* Add unspecced backup disable flag ([#4661](https://github.com/matrix-org/matrix-js-sdk/pull/4661)). Contributed by @dbkr.
|
||||
* Switch OIDC primarily to new `/auth_metadata` API ([#4626](https://github.com/matrix-org/matrix-js-sdk/pull/4626)). Contributed by @t3chguy.
|
||||
* Add `CryptoApi.resetEncryption` ([#4614](https://github.com/matrix-org/matrix-js-sdk/pull/4614)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix topic types ([#4678](https://github.com/matrix-org/matrix-js-sdk/pull/4678)). Contributed by @Half-Shot.
|
||||
* Handle empty m.room.topic ([#4673](https://github.com/matrix-org/matrix-js-sdk/pull/4673)). Contributed by @Half-Shot.
|
||||
* `CryptoApi.resetEncryption` should always create a new key backup ([#4648](https://github.com/matrix-org/matrix-js-sdk/pull/4648)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [36.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.1.0) (2025-01-28)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Deprecate `MatrixClient.login` and replace with `loginRequest` ([#4632](https://github.com/matrix-org/matrix-js-sdk/pull/4632)). Contributed by @richvdh.
|
||||
* Use `SyncCryptoCallback` api instead of legacy crypto in sliding sync ([#4624](https://github.com/matrix-org/matrix-js-sdk/pull/4624)). Contributed by @florianduros.
|
||||
* Distinguish room state and timeline events in embedded clients ([#4574](https://github.com/matrix-org/matrix-js-sdk/pull/4574)). Contributed by @robintown.
|
||||
* Allow setting default secret storage key id to null ([#4615](https://github.com/matrix-org/matrix-js-sdk/pull/4615)). Contributed by @florianduros.
|
||||
* Add authenticated media to getAvatarUrl in room and room-member models ([#4616](https://github.com/matrix-org/matrix-js-sdk/pull/4616)). Contributed by @m004.
|
||||
* Send MSC3981 'recurse' param on `/relations` endpoint on Matrix 1.10 servers ([#4023](https://github.com/matrix-org/matrix-js-sdk/pull/4023)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Revert "Distinguish room state and timeline events in embedded clients (#4574)" ([#4657](https://github.com/matrix-org/matrix-js-sdk/pull/4657)). Contributed by @RiotRobot.
|
||||
* Change randomString et al to be secure ([#4621](https://github.com/matrix-org/matrix-js-sdk/pull/4621)). Contributed by @dbkr.
|
||||
* Fix issue with sentinels being incorrect on m.room.member events ([#4609](https://github.com/matrix-org/matrix-js-sdk/pull/4609)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [36.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v36.0.0) (2025-01-14)
|
||||
==================================================================================================
|
||||
## 🚨 BREAKING CHANGES
|
||||
|
||||
* Remove support for "legacy" MSC3898 group calling in MatrixRTCSession and CallMembership ([#4583](https://github.com/matrix-org/matrix-js-sdk/pull/4583)). Contributed by @toger5.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* MatrixRTC: Implement expiry logic for CallMembership and additional test coverage ([#4587](https://github.com/matrix-org/matrix-js-sdk/pull/4587)). Contributed by @toger5.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Don't retry on 4xx responses ([#4601](https://github.com/matrix-org/matrix-js-sdk/pull/4601)). Contributed by @dbkr.
|
||||
* Upgrade matrix-sdk-crypto-wasm to 12.1.0 ([#4596](https://github.com/matrix-org/matrix-js-sdk/pull/4596)). Contributed by @andybalaam.
|
||||
* Avoid key prompts when resetting crypto ([#4586](https://github.com/matrix-org/matrix-js-sdk/pull/4586)). Contributed by @dbkr.
|
||||
* Handle when aud OIDC claim is an Array ([#4584](https://github.com/matrix-org/matrix-js-sdk/pull/4584)). Contributed by @liamdiprose.
|
||||
* Save the key backup key to 4S during `bootstrapSecretStorage ` ([#4542](https://github.com/matrix-org/matrix-js-sdk/pull/4542)). Contributed by @dbkr.
|
||||
* Only re-prepare MatrixrRTC delayed disconnection event on 404 ([#4575](https://github.com/matrix-org/matrix-js-sdk/pull/4575)). Contributed by @toger5.
|
||||
|
||||
|
||||
Changes in [35.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v35.1.0) (2024-12-18)
|
||||
==================================================================================================
|
||||
This release updates matrix-sdk-crypto-wasm to fix a bug which could prevent loading stored crypto state from storage.
|
||||
|
||||
@@ -6,11 +6,21 @@ export default {
|
||||
"src/types.ts",
|
||||
"src/browser-index.ts",
|
||||
"src/indexeddb-worker.ts",
|
||||
"src/crypto-api/index.ts",
|
||||
"src/testing.ts",
|
||||
"src/matrix.ts",
|
||||
"scripts/**",
|
||||
"spec/**",
|
||||
"release.sh",
|
||||
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
|
||||
"src/**",
|
||||
// XXX: these look entirely unused
|
||||
"src/crypto/aes.ts",
|
||||
"src/crypto/crypto.ts",
|
||||
"src/crypto/recoverykey.ts",
|
||||
// XXX: these should be re-exported by one of the supported exports
|
||||
"src/matrixrtc/index.ts",
|
||||
"src/sliding-sync.ts",
|
||||
"src/webrtc/groupCall.ts",
|
||||
"src/webrtc/stats/media/mediaTrackStats.ts",
|
||||
"src/rendezvous/RendezvousChannel.ts",
|
||||
],
|
||||
project: ["**/*.{js,ts}"],
|
||||
ignore: ["examples/**"],
|
||||
@@ -33,4 +43,6 @@ export default {
|
||||
"dist",
|
||||
],
|
||||
ignoreExportsUsedInFile: true,
|
||||
includeEntryExports: false,
|
||||
exclude: ["enumMembers"],
|
||||
} satisfies KnipConfig;
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "36.0.0-rc.0",
|
||||
"version": "36.2.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -50,7 +50,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^12.1.0",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
|
||||
"@matrix-org/olm": "3.2.15",
|
||||
"another-json": "^0.2.0",
|
||||
"bs58": "^6.0.0",
|
||||
|
||||
@@ -17,11 +17,13 @@ limitations under the License.
|
||||
import "fake-indexeddb/auto";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { ClientEvent, createClient, MatrixClient, MatrixEvent } from "../../../src";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index";
|
||||
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
|
||||
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { emitPromise, EventCounter } from "../../test-utils/test-utils";
|
||||
|
||||
describe("Device dehydration", () => {
|
||||
it("should rehydrate and dehydrate a device", async () => {
|
||||
@@ -40,6 +42,12 @@ describe("Device dehydration", () => {
|
||||
|
||||
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
|
||||
|
||||
const creationEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydratedDeviceCreated);
|
||||
const dehydrationKeyCachedEventCounter = new EventCounter(matrixClient, CryptoEvent.DehydrationKeyCached);
|
||||
const rehydrationStartedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationStarted);
|
||||
const rehydrationCompletedCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationCompleted);
|
||||
const rehydrationProgressCounter = new EventCounter(matrixClient, CryptoEvent.RehydrationProgress);
|
||||
|
||||
// count the number of times the dehydration key gets set
|
||||
let setDehydrationCount = 0;
|
||||
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
|
||||
@@ -74,6 +82,8 @@ describe("Device dehydration", () => {
|
||||
await crypto.startDehydration();
|
||||
|
||||
expect(dehydrationCount).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
|
||||
// a week later, we should have created another dehydrated device
|
||||
const dehydrationPromise = new Promise<void>((resolve, reject) => {
|
||||
@@ -81,7 +91,10 @@ describe("Device dehydration", () => {
|
||||
});
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await dehydrationPromise;
|
||||
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(1);
|
||||
expect(dehydrationCount).toEqual(2);
|
||||
expect(creationEventCounter.counter).toEqual(2);
|
||||
|
||||
// restart dehydration -- rehydrate the device that we created above,
|
||||
// and create a new dehydrated device. We also set `createNewKey`, so
|
||||
@@ -113,6 +126,39 @@ describe("Device dehydration", () => {
|
||||
expect(setDehydrationCount).toEqual(2);
|
||||
expect(eventsResponse.mock.calls).toHaveLength(2);
|
||||
|
||||
expect(rehydrationStartedCounter.counter).toEqual(1);
|
||||
expect(rehydrationCompletedCounter.counter).toEqual(1);
|
||||
expect(creationEventCounter.counter).toEqual(3);
|
||||
expect(rehydrationProgressCounter.counter).toEqual(1);
|
||||
expect(dehydrationKeyCachedEventCounter.counter).toEqual(2);
|
||||
|
||||
// test that if we get an error when we try to rotate, it emits an event
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
});
|
||||
const rotationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.DehydratedDeviceRotationError);
|
||||
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
|
||||
await rotationErrorEventPromise;
|
||||
|
||||
// Restart dehydration, but return an error for GET /dehydrated_device so that rehydration fails.
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 500,
|
||||
body: {
|
||||
errcode: "M_UNKNOWN",
|
||||
error: "Unknown error",
|
||||
},
|
||||
});
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
return {};
|
||||
});
|
||||
const rehydrationErrorEventPromise = emitPromise(matrixClient, CryptoEvent.RehydrationError);
|
||||
await crypto.startDehydration(true);
|
||||
await rehydrationErrorEventPromise;
|
||||
|
||||
matrixClient.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
|
||||
await client!.initLegacyCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -234,7 +234,7 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
|
||||
it("should emit RoomEvent.MyMembership for knock->leave->knock cycles", async () => {
|
||||
await client!.initLegacyCrypto();
|
||||
await client!.initRustCrypto();
|
||||
|
||||
const roomId = "!cycles:example.org";
|
||||
|
||||
@@ -2580,7 +2580,7 @@ describe("MatrixClient syncing (IndexedDB version)", () => {
|
||||
idbHttpBackend.when("GET", "/pushrules/").respond(200, {});
|
||||
idbHttpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
|
||||
await idbClient.initLegacyCrypto();
|
||||
await idbClient.initRustCrypto();
|
||||
|
||||
const roomId = "!invite:example.org";
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
MatrixError,
|
||||
MatrixHttpApi,
|
||||
} from "../../../src";
|
||||
import { mockOpenIdConfiguration } from "../../test-utils/oidc";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled: boolean }): MatrixClient {
|
||||
const baseUrl = "https://example.com";
|
||||
@@ -57,7 +57,7 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
||||
getDomain: () => "example.com",
|
||||
getDevice: jest.fn(),
|
||||
getCrypto: jest.fn(() => crypto),
|
||||
getAuthIssuer: jest.fn().mockResolvedValue({ issuer: "https://issuer/" }),
|
||||
getAuthMetadata: jest.fn().mockResolvedValue(makeDelegatedAuthConfig("https://issuer/", [DEVICE_CODE_SCOPE])),
|
||||
} as unknown as MatrixClient;
|
||||
client.http = new MatrixHttpApi<IHttpOpts & { onlyData: true }>(client, {
|
||||
baseUrl: client.baseUrl,
|
||||
@@ -69,10 +69,6 @@ function makeMockClient(opts: { userId: string; deviceId: string; msc4108Enabled
|
||||
|
||||
describe("MSC4108SignInWithQR", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.get(
|
||||
"https://issuer/.well-known/openid-configuration",
|
||||
mockOpenIdConfiguration("https://issuer/", [DEVICE_CODE_SCOPE]),
|
||||
);
|
||||
fetchMock.get("https://issuer/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
|
||||
@@ -44,6 +44,7 @@ import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
import { defer } from "../../src/utils";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import { SyncCryptoCallbacks } from "../../src/common-crypto/CryptoBackend";
|
||||
|
||||
declare module "../../src/@types/event" {
|
||||
interface AccountDataEvents {
|
||||
@@ -57,6 +58,7 @@ describe("SlidingSyncSdk", () => {
|
||||
let httpBackend: MockHttpBackend | undefined;
|
||||
let sdk: SlidingSyncSdk | undefined;
|
||||
let mockSlidingSync: SlidingSync | undefined;
|
||||
let syncCryptoCallback: SyncCryptoCallbacks | undefined;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
@@ -126,8 +128,9 @@ describe("SlidingSyncSdk", () => {
|
||||
mockSlidingSync = mockifySlidingSync(new SlidingSync("", new Map(), {}, client, 0));
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initLegacyCrypto();
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
await client!.initRustCrypto({ useIndexedDB: false });
|
||||
syncCryptoCallback = client!.getCrypto() as unknown as SyncCryptoCallbacks;
|
||||
syncOpts.cryptoCallbacks = syncCryptoCallback;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/v3/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
@@ -640,13 +643,6 @@ describe("SlidingSyncSdk", () => {
|
||||
ext = findExtension("e2ee");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// needed else we do some async operations in the background which can cause Jest to whine:
|
||||
// "Cannot log after tests are done. Did you forget to wait for something async in your test?"
|
||||
// Attempted to log "Saving device tracking data null"."
|
||||
client!.crypto!.stop();
|
||||
});
|
||||
|
||||
it("gets enabled on the initial request only", () => {
|
||||
expect(ext.onRequest(true)).toEqual({
|
||||
enabled: true,
|
||||
@@ -655,28 +651,28 @@ describe("SlidingSyncSdk", () => {
|
||||
});
|
||||
|
||||
it("can update device lists", () => {
|
||||
client!.crypto!.processDeviceLists = jest.fn();
|
||||
syncCryptoCallback!.processDeviceLists = jest.fn();
|
||||
ext.onResponse({
|
||||
device_lists: {
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
},
|
||||
});
|
||||
expect(client!.crypto!.processDeviceLists).toHaveBeenCalledWith({
|
||||
expect(syncCryptoCallback!.processDeviceLists).toHaveBeenCalledWith({
|
||||
changed: ["@alice:localhost"],
|
||||
left: ["@bob:localhost"],
|
||||
});
|
||||
});
|
||||
|
||||
it("can update OTK counts and unused fallback keys", () => {
|
||||
client!.crypto!.processKeyCounts = jest.fn();
|
||||
syncCryptoCallback!.processKeyCounts = jest.fn();
|
||||
ext.onResponse({
|
||||
device_one_time_keys_count: {
|
||||
signed_curve25519: 42,
|
||||
},
|
||||
device_unused_fallback_key_types: ["signed_curve25519"],
|
||||
});
|
||||
expect(client!.crypto!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
expect(syncCryptoCallback!.processKeyCounts).toHaveBeenCalledWith({ signed_curve25519: 42 }, [
|
||||
"signed_curve25519",
|
||||
]);
|
||||
});
|
||||
|
||||
+1
-39
@@ -14,42 +14,4 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { OidcClientConfig, ValidatedIssuerMetadata } from "../../src";
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns OidcClientConfig
|
||||
*/
|
||||
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
|
||||
const metadata = mockOpenIdConfiguration(issuer);
|
||||
|
||||
return {
|
||||
accountManagementEndpoint: issuer + "account",
|
||||
registrationEndpoint: metadata.registration_endpoint,
|
||||
authorizationEndpoint: metadata.authorization_endpoint,
|
||||
tokenEndpoint: metadata.token_endpoint,
|
||||
metadata,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Useful for mocking <issuer>/.well-known/openid-configuration
|
||||
* @param issuer used as the base for all other urls
|
||||
* @returns ValidatedIssuerMetadata
|
||||
*/
|
||||
export const mockOpenIdConfiguration = (
|
||||
issuer = "https://auth.org/",
|
||||
additionalGrantTypes: string[] = [],
|
||||
): ValidatedIssuerMetadata => ({
|
||||
issuer,
|
||||
revocation_endpoint: issuer + "revoke",
|
||||
token_endpoint: issuer + "token",
|
||||
authorization_endpoint: issuer + "auth",
|
||||
registration_endpoint: issuer + "registration",
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
export { makeDelegatedAuthConfig, mockOpenIdConfiguration } from "../../src/testing.ts";
|
||||
|
||||
@@ -566,6 +566,19 @@ if (globalThis.Olm) {
|
||||
|
||||
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
|
||||
|
||||
/**
|
||||
* Counts the number of times that an event was emitted.
|
||||
*/
|
||||
export class EventCounter {
|
||||
public counter;
|
||||
constructor(emitter: EventEmitter, event: string) {
|
||||
this.counter = 0;
|
||||
emitter.on(event, () => {
|
||||
this.counter++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the fake timers in a loop until the given promise resolves or rejects.
|
||||
*
|
||||
|
||||
@@ -207,6 +207,17 @@ describe("Topic content helpers", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("creates an empty event when the topic is falsey", () => {
|
||||
expect(makeTopicContent(undefined)).toEqual({
|
||||
topic: undefined,
|
||||
[M_TOPIC.name]: [],
|
||||
});
|
||||
expect(makeTopicContent(null)).toEqual({
|
||||
topic: null,
|
||||
[M_TOPIC.name]: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseTopicContent()", () => {
|
||||
@@ -257,5 +268,26 @@ describe("Topic content helpers", () => {
|
||||
html: "<b>pizza</b>",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses legacy event content", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
topic: "pizza",
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses legacy event content when new topic key is invalid", () => {
|
||||
expect(
|
||||
parseTopicContent({
|
||||
"topic": "pizza",
|
||||
"m.topic": {} as any,
|
||||
}),
|
||||
).toEqual({
|
||||
text: "pizza",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
EventTimelineSet,
|
||||
EventType,
|
||||
Filter,
|
||||
KnownMembership,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
@@ -138,6 +139,31 @@ describe("EventTimelineSet", () => {
|
||||
expect(eventsInLiveTimeline.length).toStrictEqual(1);
|
||||
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
|
||||
});
|
||||
|
||||
it("should set event.target after applying the membership state update", () => {
|
||||
const eventTimelineSet = room.getUnfilteredTimelineSet();
|
||||
|
||||
const ev1 = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Invite,
|
||||
user: "@sender:server",
|
||||
skey: userA,
|
||||
event: true,
|
||||
});
|
||||
const ev2 = utils.mkMembership({
|
||||
room: roomId,
|
||||
mship: KnownMembership.Join,
|
||||
user: userA,
|
||||
skey: userA,
|
||||
name: "This is my displayname",
|
||||
event: true,
|
||||
});
|
||||
|
||||
eventTimelineSet.addLiveEvent(ev1, { addToState: true });
|
||||
expect(ev1.target?.name).toBe(userA);
|
||||
eventTimelineSet.addLiveEvent(ev2, { addToState: true });
|
||||
expect(ev2.target?.name).toBe("This is my displayname");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addEventToTimeline", () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth, AuthType } from "../../src/interactive-auth";
|
||||
import { HTTPError, MatrixError } from "../../src/http-api";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -502,7 +502,7 @@ describe("InteractiveAuth", () => {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
const requestEmailToken = jest.fn();
|
||||
const sid = randomString(24);
|
||||
const sid = secureRandomString(24);
|
||||
requestEmailToken.mockImplementation(() => sleep(500, { sid }));
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
|
||||
@@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { logger } from "../../src/logger";
|
||||
import { ClientEvent, IMatrixClientCreateOpts, ITurnServerResponse, MatrixClient, Store } from "../../src/client";
|
||||
@@ -76,6 +77,7 @@ import { SecretStorageKeyDescriptionAesV1, ServerSideSecretStorageImpl } from ".
|
||||
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
|
||||
import { KnownMembership } from "../../src/@types/membership";
|
||||
import { RoomMessageEventContent } from "../../src/@types/events";
|
||||
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -265,13 +267,17 @@ describe("MatrixClient", function () {
|
||||
|
||||
if (next.error) {
|
||||
// eslint-disable-next-line
|
||||
return Promise.reject({
|
||||
errcode: (<MatrixError>next.error).errcode,
|
||||
httpStatus: (<MatrixError>next.error).httpStatus,
|
||||
name: (<MatrixError>next.error).errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
});
|
||||
return Promise.reject(
|
||||
new MatrixError(
|
||||
{
|
||||
errcode: (<MatrixError>next.error).errcode,
|
||||
name: (<MatrixError>next.error).errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error,
|
||||
},
|
||||
(<MatrixError>next.error).httpStatus,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Promise.resolve(next.data);
|
||||
}
|
||||
@@ -3489,6 +3495,63 @@ describe("MatrixClient", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAuthMetadata", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockReset();
|
||||
// This request is made by oidc-client-ts so is not intercepted by httpLookups
|
||||
fetchMock.get("https://auth.org/jwks", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should use unstable prefix", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_metadata`,
|
||||
data: metadata,
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||
},
|
||||
];
|
||||
|
||||
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||
...metadata,
|
||||
signingKeys: [],
|
||||
});
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("should fall back to auth_issuer + openid-configuration", async () => {
|
||||
const metadata = mockOpenIdConfiguration();
|
||||
httpLookups = [
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_metadata`,
|
||||
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: `/auth_issuer`,
|
||||
data: { issuer: metadata.issuer },
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
|
||||
},
|
||||
];
|
||||
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
|
||||
|
||||
await expect(client.getAuthMetadata()).resolves.toEqual({
|
||||
...metadata,
|
||||
signingKeys: [],
|
||||
});
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("identityHashedLookup", () => {
|
||||
it("should return hashed lookup results", async () => {
|
||||
const ID_ACCESS_TOKEN = "hello_id_server_please_let_me_make_a_request";
|
||||
|
||||
@@ -16,10 +16,11 @@ limitations under the License.
|
||||
|
||||
import { encodeBase64, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
|
||||
import { KnownMembership } from "../../../src/@types/membership";
|
||||
import { SessionMembershipData, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
|
||||
import { DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
|
||||
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
import { flushPromises } from "../../test-utils/flushPromises";
|
||||
import { makeMockRoom, makeMockRoomState, membershipTemplate } from "./mocks";
|
||||
|
||||
const mockFocus = { type: "mock" };
|
||||
@@ -97,7 +98,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("safely ignores events with no memberships section", () => {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
const event = {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue({}),
|
||||
@@ -132,7 +133,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
it("safely ignores events with junk memberships section", () => {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
const event = {
|
||||
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
|
||||
getContent: jest.fn().mockReturnValue({ memberships: ["i am a fish"] }),
|
||||
@@ -237,15 +238,13 @@ describe("MatrixRTCSession", () => {
|
||||
async function testSession(membershipData: SessionMembershipData): Promise<void> {
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, makeMockRoom(membershipData));
|
||||
|
||||
const makeNewMembershipMock = jest.spyOn(sess as any, "makeNewMembership");
|
||||
|
||||
sess.joinRoomSession([mockFocus], mockFocus, joinSessionConfig);
|
||||
await Promise.race([sentStateEvent, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
|
||||
expect(makeNewMembershipMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateEventMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
|
||||
expect(sendDelayedStateMock).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
it("sends events", async () => {
|
||||
@@ -323,9 +322,11 @@ describe("MatrixRTCSession", () => {
|
||||
let sendStateEventMock: jest.Mock;
|
||||
let sendDelayedStateMock: jest.Mock;
|
||||
let sendEventMock: jest.Mock;
|
||||
let updateDelayedEventMock: jest.Mock;
|
||||
|
||||
let sentStateEvent: Promise<void>;
|
||||
let sentDelayedState: Promise<void>;
|
||||
let updatedDelayedEvent: Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
sentStateEvent = new Promise((resolve) => {
|
||||
@@ -339,12 +340,15 @@ describe("MatrixRTCSession", () => {
|
||||
};
|
||||
});
|
||||
});
|
||||
updatedDelayedEvent = new Promise((r) => {
|
||||
updateDelayedEventMock = jest.fn(r);
|
||||
});
|
||||
sendEventMock = jest.fn();
|
||||
client.sendStateEvent = sendStateEventMock;
|
||||
client._unstable_sendDelayedStateEvent = sendDelayedStateMock;
|
||||
client.sendEvent = sendEventMock;
|
||||
|
||||
client._unstable_updateDelayedEvent = jest.fn();
|
||||
client._unstable_updateDelayedEvent = updateDelayedEventMock;
|
||||
|
||||
mockRoom = makeMockRoom([]);
|
||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||
@@ -422,7 +426,6 @@ describe("MatrixRTCSession", () => {
|
||||
type: "livekit",
|
||||
},
|
||||
},
|
||||
|
||||
"_@alice:example.org_AAAAAAA",
|
||||
);
|
||||
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
|
||||
@@ -454,6 +457,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`;
|
||||
// preparing the delayed disconnect should handle ratelimiting
|
||||
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
|
||||
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
|
||||
@@ -478,24 +482,18 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// needed to advance the mock timers properly
|
||||
const scheduledDelayDisconnection = new Promise<void>((resolve) => {
|
||||
const originalFn: () => void = (sess as any).scheduleDelayDisconnection;
|
||||
(sess as any).scheduleDelayDisconnection = jest.fn(() => {
|
||||
originalFn.call(sess);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
sess!.joinRoomSession([activeFocusConfig], activeFocus, {
|
||||
membershipServerSideExpiryTimeout: 9000,
|
||||
});
|
||||
|
||||
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 9000);
|
||||
await sendDelayedStateExceedAttempt.then(); // needed to resolve after the send attempt catches
|
||||
expect(sess).toHaveProperty("membershipServerSideExpiryTimeout", 7500);
|
||||
|
||||
await sendDelayedStateAttempt;
|
||||
const callProps = (d: number) => {
|
||||
return [mockRoom!.roomId, { delay: d }, "org.matrix.msc3401.call.member", {}, userStateKey];
|
||||
};
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(1, ...callProps(9000));
|
||||
expect(client._unstable_sendDelayedStateEvent).toHaveBeenNthCalledWith(2, ...callProps(7500));
|
||||
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
await sendStateEventAttempt.then(); // needed to resolve after resendIfRateLimited catches
|
||||
@@ -514,16 +512,18 @@ describe("MatrixRTCSession", () => {
|
||||
foci_preferred: [activeFocusConfig],
|
||||
focus_active: activeFocus,
|
||||
} satisfies SessionMembershipData,
|
||||
`${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`,
|
||||
userStateKey,
|
||||
);
|
||||
await sentDelayedState;
|
||||
|
||||
// should have prepared the heartbeat to keep delaying the leave event while still connected
|
||||
await scheduledDelayDisconnection;
|
||||
// should have tried updating the delayed leave to test that it wasn't replaced by own state
|
||||
await updatedDelayedEvent;
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(1);
|
||||
// should update delayed disconnect
|
||||
|
||||
// ensures that we reach the code that schedules the timeout for the next delay update before we advance the timers.
|
||||
await flushPromises();
|
||||
jest.advanceTimersByTime(5000);
|
||||
// should update delayed disconnect
|
||||
expect(client._unstable_updateDelayedEvent).toHaveBeenCalledTimes(2);
|
||||
|
||||
jest.useRealTimers();
|
||||
@@ -555,7 +555,7 @@ describe("MatrixRTCSession", () => {
|
||||
|
||||
const onMembershipsChanged = jest.fn();
|
||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
expect(onMembershipsChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -568,7 +568,7 @@ describe("MatrixRTCSession", () => {
|
||||
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
|
||||
|
||||
mockRoom.getLiveTimeline().getState = jest.fn().mockReturnValue(makeMockRoomState([], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
expect(onMembershipsChanged).toHaveBeenCalled();
|
||||
});
|
||||
@@ -757,7 +757,7 @@ describe("MatrixRTCSession", () => {
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
// member2 re-joins which should trigger an immediate re-send
|
||||
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
|
||||
@@ -766,7 +766,7 @@ describe("MatrixRTCSession", () => {
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
// but, that immediate resend is throttled so we need to wait a bit
|
||||
jest.advanceTimersByTime(1000);
|
||||
const { keys } = await keysSentPromise2;
|
||||
@@ -819,7 +819,7 @@ describe("MatrixRTCSession", () => {
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
await keysSentPromise2;
|
||||
|
||||
@@ -873,7 +873,7 @@ describe("MatrixRTCSession", () => {
|
||||
sendEventMock.mockClear();
|
||||
|
||||
// these should be a no-op:
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
expect(sendEventMock).toHaveBeenCalledTimes(0);
|
||||
expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
|
||||
} finally {
|
||||
@@ -927,7 +927,7 @@ describe("MatrixRTCSession", () => {
|
||||
sendEventMock.mockClear();
|
||||
|
||||
// this should be a no-op:
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
expect(sendEventMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
// advance time to avoid key throttling
|
||||
@@ -941,7 +941,7 @@ describe("MatrixRTCSession", () => {
|
||||
});
|
||||
|
||||
// this should re-send the key
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
await keysSentPromise2;
|
||||
|
||||
@@ -1004,7 +1004,7 @@ describe("MatrixRTCSession", () => {
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
jest.advanceTimersByTime(10000);
|
||||
|
||||
@@ -1049,7 +1049,7 @@ describe("MatrixRTCSession", () => {
|
||||
);
|
||||
}
|
||||
|
||||
sess!.onMembershipUpdate();
|
||||
sess!.onRTCSessionMemberUpdate();
|
||||
|
||||
// advance time to avoid key throttling
|
||||
jest.advanceTimersByTime(10000);
|
||||
@@ -1090,7 +1090,7 @@ describe("MatrixRTCSession", () => {
|
||||
mockRoom.getLiveTimeline().getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId));
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
realSetTimeout(resolve);
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { EventType, MatrixEvent, Room } from "../../../src";
|
||||
import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||
import { randomString } from "../../../src/randomstring";
|
||||
import { secureRandomString } from "../../../src/randomstring";
|
||||
|
||||
type MembershipData = SessionMembershipData[] | SessionMembershipData | {};
|
||||
|
||||
@@ -41,7 +41,7 @@ export const membershipTemplate: SessionMembershipData = {
|
||||
};
|
||||
|
||||
export function makeMockRoom(membershipData: MembershipData): Room {
|
||||
const roomId = randomString(8);
|
||||
const roomId = secureRandomString(8);
|
||||
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
|
||||
const roomState = makeMockRoomState(membershipData, roomId);
|
||||
const room = {
|
||||
|
||||
@@ -40,8 +40,8 @@ jest.mock("jwt-decode");
|
||||
|
||||
describe("oidc authorization", () => {
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig();
|
||||
const authorizationEndpoint = delegatedAuthConfig.authorizationEndpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.tokenEndpoint;
|
||||
const authorizationEndpoint = delegatedAuthConfig.authorization_endpoint;
|
||||
const tokenEndpoint = delegatedAuthConfig.token_endpoint;
|
||||
const clientId = "xyz789";
|
||||
const baseUrl = "https://test.com";
|
||||
|
||||
@@ -52,10 +52,7 @@ describe("oidc authorization", () => {
|
||||
jest.spyOn(logger, "warn");
|
||||
jest.setSystemTime(now);
|
||||
|
||||
fetchMock.get(
|
||||
delegatedAuthConfig.metadata.issuer + ".well-known/openid-configuration",
|
||||
mockOpenIdConfiguration(),
|
||||
);
|
||||
fetchMock.get(delegatedAuthConfig.issuer + ".well-known/openid-configuration", mockOpenIdConfiguration());
|
||||
globalThis.TextEncoder = TextEncoder;
|
||||
});
|
||||
|
||||
@@ -127,11 +124,9 @@ describe("oidc authorization", () => {
|
||||
it("should generate url with correct parameters", async () => {
|
||||
const nonce = "abc123";
|
||||
|
||||
const metadata = delegatedAuthConfig.metadata;
|
||||
|
||||
const authUrl = new URL(
|
||||
await generateOidcAuthorizationUrl({
|
||||
metadata,
|
||||
metadata: delegatedAuthConfig,
|
||||
homeserverUrl: baseUrl,
|
||||
clientId,
|
||||
redirectUri: baseUrl,
|
||||
@@ -156,11 +151,9 @@ describe("oidc authorization", () => {
|
||||
it("should generate url with create prompt", async () => {
|
||||
const nonce = "abc123";
|
||||
|
||||
const metadata = delegatedAuthConfig.metadata;
|
||||
|
||||
const authUrl = new URL(
|
||||
await generateOidcAuthorizationUrl({
|
||||
metadata,
|
||||
metadata: delegatedAuthConfig,
|
||||
homeserverUrl: baseUrl,
|
||||
clientId,
|
||||
redirectUri: baseUrl,
|
||||
|
||||
@@ -42,13 +42,13 @@ describe("registerOidcClient()", () => {
|
||||
});
|
||||
|
||||
it("should make correct request to register client", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||
});
|
||||
expect(await registerOidcClient(delegatedAuthConfig, metadata)).toEqual(dynamicClientId);
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||
delegatedAuthConfig.registrationEndpoint!,
|
||||
delegatedAuthConfig.registration_endpoint,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
@@ -72,7 +72,7 @@ describe("registerOidcClient()", () => {
|
||||
});
|
||||
|
||||
it("should throw when registration request fails", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||
status: 500,
|
||||
});
|
||||
await expect(() => registerOidcClient(delegatedAuthConfig, metadata)).rejects.toThrow(
|
||||
@@ -81,7 +81,7 @@ describe("registerOidcClient()", () => {
|
||||
});
|
||||
|
||||
it("should throw when registration response is invalid", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
fetchMockJest.post(delegatedAuthConfig.registration_endpoint!, {
|
||||
status: 200,
|
||||
// no clientId in response
|
||||
body: "{}",
|
||||
@@ -96,7 +96,7 @@ describe("registerOidcClient()", () => {
|
||||
registerOidcClient(
|
||||
{
|
||||
...delegatedAuthConfig,
|
||||
registrationEndpoint: undefined,
|
||||
registration_endpoint: undefined,
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
@@ -108,10 +108,7 @@ describe("registerOidcClient()", () => {
|
||||
registerOidcClient(
|
||||
{
|
||||
...delegatedAuthConfig,
|
||||
metadata: {
|
||||
...delegatedAuthConfig.metadata,
|
||||
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
|
||||
},
|
||||
grant_types_supported: [delegatedAuthConfig.grant_types_supported[0]],
|
||||
},
|
||||
metadata,
|
||||
),
|
||||
|
||||
@@ -55,8 +55,8 @@ describe("OidcTokenRefresher", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
|
||||
fetchMock.get(`${config.metadata.issuer}jwks`, {
|
||||
fetchMock.get(`${config.issuer}.well-known/openid-configuration`, config);
|
||||
fetchMock.get(`${config.issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -64,7 +64,7 @@ describe("OidcTokenRefresher", () => {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
fetchMock.post(config.tokenEndpoint, {
|
||||
fetchMock.post(config.token_endpoint, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -81,7 +81,7 @@ describe("OidcTokenRefresher", () => {
|
||||
it("throws when oidc client cannot be initialised", async () => {
|
||||
jest.spyOn(logger, "error");
|
||||
fetchMock.get(
|
||||
`${config.metadata.issuer}.well-known/openid-configuration`,
|
||||
`${config.issuer}.well-known/openid-configuration`,
|
||||
{
|
||||
ok: false,
|
||||
status: 404,
|
||||
@@ -126,7 +126,7 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
const result = await refresher.doRefreshAccessToken("refresh-token");
|
||||
|
||||
expect(fetchMock).toHaveFetched(config.tokenEndpoint, {
|
||||
expect(fetchMock).toHaveFetched(config.token_endpoint, {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
@@ -153,7 +153,7 @@ describe("OidcTokenRefresher", () => {
|
||||
it("should only have one inflight refresh request at once", async () => {
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.tokenEndpoint,
|
||||
config.token_endpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -164,7 +164,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.tokenEndpoint,
|
||||
config.token_endpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
@@ -188,7 +188,7 @@ describe("OidcTokenRefresher", () => {
|
||||
const result2 = await first;
|
||||
|
||||
// only one call to token endpoint
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.tokenEndpoint);
|
||||
expect(fetchMock).toHaveFetchedTimes(1, config.token_endpoint);
|
||||
expect(result1).toEqual({
|
||||
accessToken: "first-new-access-token",
|
||||
refreshToken: "first-new-refresh-token",
|
||||
@@ -208,7 +208,7 @@ describe("OidcTokenRefresher", () => {
|
||||
|
||||
it("should log and rethrow when token refresh fails", async () => {
|
||||
fetchMock.post(
|
||||
config.tokenEndpoint,
|
||||
config.token_endpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -228,7 +228,7 @@ describe("OidcTokenRefresher", () => {
|
||||
// make sure inflight request is cleared after a failure
|
||||
fetchMock
|
||||
.postOnce(
|
||||
config.tokenEndpoint,
|
||||
config.token_endpoint,
|
||||
{
|
||||
status: 503,
|
||||
headers: {
|
||||
@@ -238,7 +238,7 @@ describe("OidcTokenRefresher", () => {
|
||||
{ overwriteRoutes: true },
|
||||
)
|
||||
.postOnce(
|
||||
config.tokenEndpoint,
|
||||
config.token_endpoint,
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
|
||||
@@ -18,17 +18,18 @@ import { mocked } from "jest-mock";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
|
||||
import { logger } from "../../../src/logger";
|
||||
import { validateIdToken, validateOIDCIssuerWellKnown } from "../../../src/oidc/validate";
|
||||
import { ValidatedAuthMetadata, validateIdToken, validateAuthMetadata } from "../../../src/oidc/validate";
|
||||
import { OidcError } from "../../../src/oidc/error";
|
||||
|
||||
jest.mock("jwt-decode");
|
||||
|
||||
describe("validateOIDCIssuerWellKnown", () => {
|
||||
const validWk: any = {
|
||||
const validWk: ValidatedAuthMetadata = {
|
||||
issuer: "https://test.org",
|
||||
authorization_endpoint: "https://test.org/authorize",
|
||||
token_endpoint: "https://authorize.org/token",
|
||||
registration_endpoint: "https://authorize.org/regsiter",
|
||||
revocation_endpoint: "https://authorize.org/regsiter",
|
||||
registration_endpoint: "https://authorize.org/register",
|
||||
revocation_endpoint: "https://authorize.org/revoke",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
@@ -44,14 +45,14 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
|
||||
it("should throw OP support error when wellKnown is not an object", () => {
|
||||
expect(() => {
|
||||
validateOIDCIssuerWellKnown([]);
|
||||
validateAuthMetadata([]);
|
||||
}).toThrow(OidcError.OpSupport);
|
||||
expect(logger.error).toHaveBeenCalledWith("Issuer configuration not found or malformed");
|
||||
});
|
||||
|
||||
it("should log all errors before throwing", () => {
|
||||
expect(() => {
|
||||
validateOIDCIssuerWellKnown({
|
||||
validateAuthMetadata({
|
||||
...validWk,
|
||||
authorization_endpoint: undefined,
|
||||
response_types_supported: [],
|
||||
@@ -62,24 +63,31 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
});
|
||||
|
||||
it("should return validated issuer config", () => {
|
||||
expect(validateOIDCIssuerWellKnown(validWk)).toEqual({
|
||||
authorizationEndpoint: validWk.authorization_endpoint,
|
||||
tokenEndpoint: validWk.token_endpoint,
|
||||
registrationEndpoint: validWk.registration_endpoint,
|
||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
||||
accountManagementEndpoint: "https://authorize.org/account",
|
||||
});
|
||||
expect(validateAuthMetadata(validWk)).toEqual(
|
||||
expect.objectContaining({
|
||||
issuer: validWk.issuer,
|
||||
authorization_endpoint: validWk.authorization_endpoint,
|
||||
token_endpoint: validWk.token_endpoint,
|
||||
registration_endpoint: validWk.registration_endpoint,
|
||||
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
||||
account_management_uri: "https://authorize.org/account",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
accountManagementActionsSupported: ["org.matrix.cross_signing_reset"],
|
||||
accountManagementEndpoint: "https://authorize.org/account",
|
||||
it("should return validated issuer config without registration_endpoint", () => {
|
||||
const { registration_endpoint: _, ...wk } = validWk;
|
||||
expect(validateAuthMetadata(wk)).toEqual({
|
||||
issuer: validWk.issuer,
|
||||
authorization_endpoint: validWk.authorization_endpoint,
|
||||
token_endpoint: validWk.token_endpoint,
|
||||
revocation_endpoint: validWk.revocation_endpoint,
|
||||
registration_endpoint: undefined,
|
||||
account_management_actions_supported: ["org.matrix.cross_signing_reset"],
|
||||
account_management_uri: "https://authorize.org/account",
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
response_types_supported: ["code"],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,7 +114,7 @@ describe("validateOIDCIssuerWellKnown", () => {
|
||||
...validWk,
|
||||
[key]: value,
|
||||
};
|
||||
expect(() => validateOIDCIssuerWellKnown(wk)).toThrow(OidcError.OpSupport);
|
||||
expect(() => validateAuthMetadata(wk)).toThrow(OidcError.OpSupport);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,13 +16,18 @@ limitations under the License.
|
||||
|
||||
import { decodeBase64 } from "../../src/base64";
|
||||
import {
|
||||
randomLowercaseString,
|
||||
randomString,
|
||||
randomUppercaseString,
|
||||
secureRandomString,
|
||||
secureRandomBase64Url,
|
||||
secureRandomStringFrom,
|
||||
LOWERCASE,
|
||||
UPPERCASE,
|
||||
} from "../../src/randomstring";
|
||||
|
||||
describe("Random strings", () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => {
|
||||
const randb641 = secureRandomBase64Url(n);
|
||||
const randb642 = secureRandomBase64Url(n);
|
||||
@@ -33,34 +38,77 @@ describe("Random strings", () => {
|
||||
expect(decoded).toHaveLength(n);
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => {
|
||||
const rand1 = randomString(n);
|
||||
const rand2 = randomString(n);
|
||||
it.each([8, 16, 32])("secureRandomString generates string of %i characters", (n: number) => {
|
||||
const rand1 = secureRandomString(n);
|
||||
const rand2 = secureRandomString(n);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => {
|
||||
const rand1 = randomLowercaseString(n);
|
||||
const rand2 = randomLowercaseString(n);
|
||||
it.each([8, 16, 32])(
|
||||
"secureRandomStringFrom generates lowercase string of %i characters when given lowercase",
|
||||
(n: number) => {
|
||||
const rand1 = secureRandomStringFrom(n, LOWERCASE);
|
||||
const rand2 = secureRandomStringFrom(n, LOWERCASE);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
expect(rand1).toHaveLength(n);
|
||||
|
||||
expect(rand1.toLowerCase()).toEqual(rand1);
|
||||
expect(rand1.toLowerCase()).toEqual(rand1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([8, 16, 32])(
|
||||
"secureRandomStringFrom generates uppercase string of %i characters when given uppercase",
|
||||
(n: number) => {
|
||||
const rand1 = secureRandomStringFrom(n, UPPERCASE);
|
||||
const rand2 = secureRandomStringFrom(n, UPPERCASE);
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
|
||||
expect(rand1.toUpperCase()).toEqual(rand1);
|
||||
},
|
||||
);
|
||||
|
||||
it("throws if given character set less than 2 characters", () => {
|
||||
expect(() => secureRandomStringFrom(8, "a")).toThrow();
|
||||
});
|
||||
|
||||
it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => {
|
||||
const rand1 = randomUppercaseString(n);
|
||||
const rand2 = randomUppercaseString(n);
|
||||
it("throws if given character set more than 256 characters", () => {
|
||||
const charSet = Array.from({ length: 257 }, (_, i) => "a").join("");
|
||||
|
||||
expect(rand1).not.toEqual(rand2);
|
||||
expect(() => secureRandomStringFrom(8, charSet)).toThrow();
|
||||
});
|
||||
|
||||
expect(rand1).toHaveLength(n);
|
||||
it("throws if given length less than 1", () => {
|
||||
expect(() => secureRandomStringFrom(0, "abc")).toThrow();
|
||||
});
|
||||
|
||||
expect(rand1.toUpperCase()).toEqual(rand1);
|
||||
it("throws if given length more than 32768", () => {
|
||||
expect(() => secureRandomStringFrom(32769, "abc")).toThrow();
|
||||
});
|
||||
|
||||
it("asks for more entropy if given entropy is unusable", () => {
|
||||
// This is testing the internal implementation details of the function rather
|
||||
// than strictly the public API. The intention is to have some assertion that
|
||||
// the rejection sampling to make the distribution even over all possible characters
|
||||
// is doing what it's supposed to do.
|
||||
|
||||
// mock once to fill with 255 the first time: 255 should be unusable because
|
||||
// we give 10 possible characters below and 256 is not evenly divisible by 10, so
|
||||
// this should force it to call for more entropy.
|
||||
jest.spyOn(globalThis.crypto, "getRandomValues").mockImplementationOnce((arr) => {
|
||||
if (arr === null) throw new Error("Buffer is null");
|
||||
new Uint8Array(arr.buffer).fill(255);
|
||||
return arr;
|
||||
});
|
||||
|
||||
secureRandomStringFrom(8, "0123456789");
|
||||
expect(globalThis.crypto.getRandomValues).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,40 @@ describe("RoomMember", function () {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return unauthenticated media URL if useAuthentication is not set", function () {
|
||||
member.events.member = utils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.member",
|
||||
skey: userA,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
membership: KnownMembership.Join,
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false);
|
||||
// Check for unauthenticated media prefix
|
||||
expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return authenticated media URL if useAuthentication=true", function () {
|
||||
member.events.member = utils.mkEvent({
|
||||
event: true,
|
||||
type: "m.room.member",
|
||||
skey: userA,
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
membership: KnownMembership.Join,
|
||||
avatar_url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
const url = member.getAvatarUrl(hsUrl, 1, 1, "", false, false, true);
|
||||
// Check for authenticated media prefix
|
||||
expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setPowerLevelEvent", function () {
|
||||
|
||||
@@ -299,6 +299,48 @@ describe("Room", function () {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
expect(url).toEqual(null);
|
||||
});
|
||||
|
||||
it("should return unauthenticated media URL if useAuthentication is not set", function () {
|
||||
// @ts-ignore - mocked doesn't handle overloads sanely
|
||||
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
||||
if (type === EventType.RoomAvatar && key === "") {
|
||||
return utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomAvatar,
|
||||
skey: "",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale");
|
||||
// Check for unauthenticated media prefix
|
||||
expect(url?.indexOf("/_matrix/media/v3/")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return authenticated media URL if useAuthentication=true", function () {
|
||||
// @ts-ignore - mocked doesn't handle overloads sanely
|
||||
mocked(room.currentState.getStateEvents).mockImplementation(function (type, key) {
|
||||
if (type === EventType.RoomAvatar && key === "") {
|
||||
return utils.mkEvent({
|
||||
event: true,
|
||||
type: EventType.RoomAvatar,
|
||||
skey: "",
|
||||
room: roomId,
|
||||
user: userA,
|
||||
content: {
|
||||
url: "mxc://flibble/wibble",
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const url = room.getAvatarUrl(hsUrl, 100, 100, "scale", undefined, true);
|
||||
// Check for authenticated media prefix
|
||||
expect(url?.indexOf("/_matrix/client/v1/media/")).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMember", function () {
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
MemoryCryptoStore,
|
||||
TypedEventEmitter,
|
||||
} from "../../../src";
|
||||
import { mkEvent } from "../../test-utils/test-utils";
|
||||
import { emitPromise, mkEvent } from "../../test-utils/test-utils";
|
||||
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
|
||||
import { IEventDecryptionResult, IMegolmSessionData } from "../../../src/@types/crypto";
|
||||
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
|
||||
@@ -61,9 +61,12 @@ import {
|
||||
EventShieldReason,
|
||||
ImportRoomKeysOpts,
|
||||
KeyBackupCheck,
|
||||
KeyBackupInfo,
|
||||
VerificationRequest,
|
||||
} from "../../../src/crypto-api";
|
||||
import * as testData from "../../test-utils/test-data";
|
||||
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||
import { defer } from "../../../src/utils";
|
||||
import { logger } from "../../../src/logger";
|
||||
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
|
||||
@@ -72,6 +75,7 @@ import { Curve25519AuthData } from "../../../src/crypto-api/keybackup";
|
||||
import encryptAESSecretStorageItem from "../../../src/utils/encryptAESSecretStorageItem.ts";
|
||||
import { CryptoStore, SecretStorePrivateKeys } from "../../../src/crypto/store/base";
|
||||
import { CryptoEvent } from "../../../src/crypto-api/index.ts";
|
||||
import { RustBackupManager } from "../../../src/rust-crypto/backup.ts";
|
||||
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
@@ -1532,13 +1536,58 @@ describe("RustCrypto", () => {
|
||||
|
||||
describe("pinCurrentIdentity", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
beforeEach(() => {
|
||||
olmMachine = {
|
||||
beforeEach(async () => {
|
||||
const secretStorageCallbacks = {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
} as SecretStorageCallbacks;
|
||||
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||
rustCrypto = await makeTestRustCrypto(
|
||||
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
|
||||
baseUrl: "http://server/",
|
||||
prefix: "",
|
||||
onlyData: true,
|
||||
}),
|
||||
TEST_USER,
|
||||
TEST_DEVICE_ID,
|
||||
secretStorage,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error for an unknown user", async () => {
|
||||
await expect(rustCrypto.pinCurrentUserIdentity("@other_user:example.com")).rejects.toThrow(
|
||||
"Cannot pin identity of unknown user",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error for our own user", async () => {
|
||||
jest.useRealTimers();
|
||||
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
|
||||
const e2eKeyResponder = new E2EKeyResponder("http://server");
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await expect(rustCrypto.pinCurrentUserIdentity(TEST_USER)).rejects.toThrow(
|
||||
"Cannot pin identity of own user",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("withdraw verification", () => {
|
||||
function createTestSetup(): { olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>; rustCrypto: RustCrypto } {
|
||||
const olmMachine = {
|
||||
getIdentity: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
rustCrypto = new RustCrypto(
|
||||
const rustCrypto = new RustCrypto(
|
||||
logger,
|
||||
olmMachine,
|
||||
{} as MatrixClient["http"],
|
||||
@@ -1547,21 +1596,40 @@ describe("RustCrypto", () => {
|
||||
{} as ServerSideSecretStorage,
|
||||
{} as CryptoCallbacks,
|
||||
);
|
||||
});
|
||||
return { olmMachine, rustCrypto };
|
||||
}
|
||||
|
||||
it("throws an error for an unknown user", async () => {
|
||||
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
|
||||
"Cannot pin identity of unknown user",
|
||||
const { rustCrypto } = createTestSetup();
|
||||
await expect(rustCrypto.withdrawVerificationRequirement("@alice:example.com")).rejects.toThrow(
|
||||
"Cannot withdraw verification of unknown user",
|
||||
);
|
||||
});
|
||||
|
||||
it("throws an error for our own user", async () => {
|
||||
const ownIdentity = new RustSdkCryptoJs.OwnUserIdentity();
|
||||
it("Calls withdraw for other identity", async () => {
|
||||
const { olmMachine, rustCrypto } = createTestSetup();
|
||||
const identity = {
|
||||
withdrawVerification: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OtherUserIdentity>;
|
||||
|
||||
olmMachine.getIdentity.mockResolvedValue(identity);
|
||||
|
||||
await rustCrypto.withdrawVerificationRequirement("@bob:example.com");
|
||||
|
||||
expect(identity.withdrawVerification).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Calls withdraw for own identity", async () => {
|
||||
const { olmMachine, rustCrypto } = createTestSetup();
|
||||
const ownIdentity = {
|
||||
withdrawVerification: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OwnUserIdentity>;
|
||||
|
||||
olmMachine.getIdentity.mockResolvedValue(ownIdentity);
|
||||
|
||||
await expect(rustCrypto.pinCurrentUserIdentity("@alice:example.com")).rejects.toThrow(
|
||||
"Cannot pin identity of own user",
|
||||
);
|
||||
await rustCrypto.withdrawVerificationRequirement("@alice:example.com");
|
||||
|
||||
expect(ownIdentity.withdrawVerification).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1722,6 +1790,297 @@ describe("RustCrypto", () => {
|
||||
});
|
||||
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
|
||||
});
|
||||
|
||||
it("should load the dehydration key from SSSS if available", async () => {
|
||||
fetchMock.config.overwriteRoutes = true;
|
||||
|
||||
const secretStorageCallbacks = {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
} as SecretStorageCallbacks;
|
||||
const secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||
|
||||
// Create a RustCrypto to set up device dehydration.
|
||||
const e2eKeyReceiver1 = new E2EKeyReceiver("http://server");
|
||||
const e2eKeyResponder1 = new E2EKeyResponder("http://server");
|
||||
e2eKeyResponder1.addKeyReceiver(TEST_USER, e2eKeyReceiver1);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
const rustCrypto1 = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
|
||||
|
||||
// dehydration requires secret storage and cross signing
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await rustCrypto1.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await rustCrypto1.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
});
|
||||
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await rustCrypto1.preprocessToDeviceMessages([]);
|
||||
await rustCrypto1.onSyncCompleted({});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
let dehydratedDeviceBody: any;
|
||||
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
|
||||
dehydratedDeviceBody = JSON.parse(opts.body as string);
|
||||
return {};
|
||||
});
|
||||
await rustCrypto1.startDehydration();
|
||||
await rustCrypto1.stop();
|
||||
|
||||
// Create another RustCrypto, using the same SecretStorage, to
|
||||
// rehydrate the device.
|
||||
const e2eKeyReceiver2 = new E2EKeyReceiver("http://server");
|
||||
const e2eKeyResponder2 = new E2EKeyResponder("http://server");
|
||||
e2eKeyResponder2.addKeyReceiver(TEST_USER, e2eKeyReceiver2);
|
||||
|
||||
const rustCrypto2 = await makeTestRustCrypto(
|
||||
makeMatrixHttpApi(),
|
||||
TEST_USER,
|
||||
"ANOTHERDEVICE",
|
||||
secretStorage,
|
||||
);
|
||||
|
||||
// dehydration requires secret storage and cross signing
|
||||
await rustCrypto2.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await rustCrypto2.preprocessToDeviceMessages([]);
|
||||
await rustCrypto2.onSyncCompleted({});
|
||||
|
||||
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
|
||||
device_id: dehydratedDeviceBody.device_id,
|
||||
device_data: dehydratedDeviceBody.device_data,
|
||||
});
|
||||
fetchMock.post(
|
||||
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
|
||||
{
|
||||
events: [],
|
||||
next_batch: "token",
|
||||
},
|
||||
);
|
||||
|
||||
// We check that a RehydrationCompleted event gets emitted, which
|
||||
// means that the device was successfully rehydrated.
|
||||
const rehydrationCompletedPromise = emitPromise(rustCrypto2, CryptoEvent.RehydrationCompleted);
|
||||
await rustCrypto2.startDehydration();
|
||||
await rehydrationCompletedPromise;
|
||||
await rustCrypto2.stop();
|
||||
});
|
||||
|
||||
describe("start dehydration options", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
let secretStorage: ServerSideSecretStorageImpl;
|
||||
let dehydratedDeviceInfo: Record<string, any> | undefined;
|
||||
|
||||
// Function that is called when `GET /dehydrated_device` is called
|
||||
// (i.e. when we try to rehydrate a device)
|
||||
const getDehydratedDeviceMock = jest.fn(() => {
|
||||
if (dehydratedDeviceInfo) {
|
||||
return {
|
||||
status: 200,
|
||||
body: dehydratedDeviceInfo,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
// Function that is called when `PUT /dehydrated_device` is called
|
||||
// (i.e. when we create a new dehydrated device)
|
||||
const putDehydratedDeviceMock = jest.fn((path, opts) => {
|
||||
const content = JSON.parse(opts.body as string);
|
||||
dehydratedDeviceInfo = {
|
||||
device_id: content.device_id,
|
||||
device_data: content.device_data,
|
||||
};
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
device_id: content.device_id,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Set up a RustCrypto object with secret storage and cross-signing.
|
||||
const secretStorageCallbacks = {
|
||||
getSecretStorageKey: async (keys: any, name: string) => {
|
||||
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
|
||||
},
|
||||
} as SecretStorageCallbacks;
|
||||
secretStorage = new ServerSideSecretStorageImpl(new DummyAccountDataClient(), secretStorageCallbacks);
|
||||
|
||||
const e2eKeyReceiver = new E2EKeyReceiver("http://server");
|
||||
const e2eKeyResponder = new E2EKeyResponder("http://server");
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER, e2eKeyReceiver);
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
|
||||
status: 404,
|
||||
body: {
|
||||
errcode: "M_NOT_FOUND",
|
||||
error: "Not found",
|
||||
},
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {
|
||||
status: 200,
|
||||
body: {},
|
||||
});
|
||||
rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), TEST_USER, TEST_DEVICE_ID, secretStorage);
|
||||
|
||||
// dehydration requires secret storage and cross signing
|
||||
async function createSecretStorageKey() {
|
||||
return {
|
||||
keyInfo: {} as AddSecretStorageKeyOpts,
|
||||
privateKey: new Uint8Array(32),
|
||||
};
|
||||
}
|
||||
await rustCrypto.bootstrapCrossSigning({ setupNewCrossSigning: true });
|
||||
await rustCrypto.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: false,
|
||||
});
|
||||
// we need to process a sync so that the OlmMachine will upload keys
|
||||
await rustCrypto.preprocessToDeviceMessages([]);
|
||||
await rustCrypto.onSyncCompleted({});
|
||||
|
||||
// set up mocks needed for device dehydration
|
||||
dehydratedDeviceInfo = undefined;
|
||||
fetchMock.get(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
getDehydratedDeviceMock,
|
||||
);
|
||||
fetchMock.put(
|
||||
"path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
|
||||
putDehydratedDeviceMock,
|
||||
);
|
||||
fetchMock.post(/_matrix\/client\/unstable\/org.matrix.msc3814.v1\/dehydrated_device\/.*\/events/, {
|
||||
status: 200,
|
||||
body: {
|
||||
events: [],
|
||||
next_batch: "foo",
|
||||
},
|
||||
});
|
||||
getDehydratedDeviceMock.mockClear();
|
||||
putDehydratedDeviceMock.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rustCrypto.stop();
|
||||
});
|
||||
|
||||
// Several tests require a dehydrated device and dehydration key
|
||||
// already set up.
|
||||
async function setUpInitialDehydratedDevice() {
|
||||
await rustCrypto.startDehydration();
|
||||
getDehydratedDeviceMock.mockClear();
|
||||
putDehydratedDeviceMock.mockClear();
|
||||
return await secretStorage.get("org.matrix.msc3814");
|
||||
}
|
||||
|
||||
it("should create a new key and dehydrate a device when no options given", async () => {
|
||||
// With the default options, when we don't have an existing key ...
|
||||
await rustCrypto.startDehydration();
|
||||
// ... we create a new dehydration key ...
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).toBeTruthy();
|
||||
// ... and create a new dehydrated device.
|
||||
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should rehydrate a device if available and keep existing key when no options given", async () => {
|
||||
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||
|
||||
// If we already have a dehydration key and dehydrated device...
|
||||
await rustCrypto.startDehydration();
|
||||
// ... we should fetch the device to rehydrate it ...
|
||||
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||
// ... create a new dehydrated device ...
|
||||
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||
// ... and keep the same dehydration key.
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||
});
|
||||
|
||||
it("should do nothing if onlyIfKeyCached is true and we have no key cached", async () => {
|
||||
// Since there is no key cached, so should do nothing. i.e. it
|
||||
// should not make any HTTP requests and should not create a new key.
|
||||
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
|
||||
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||
expect(putDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should start dehydration when onlyIfKeyCached is true, and we have a cached key", async () => {
|
||||
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||
|
||||
// If `onlyIfKeyCached` is `true`, and we already have have a
|
||||
// key, we should behave the same as if no options were given.
|
||||
await rustCrypto.startDehydration({ onlyIfKeyCached: true });
|
||||
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||
});
|
||||
|
||||
it("should not rehydrate if rehydrate is set to false", async () => {
|
||||
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||
|
||||
// If `rehydrate` is set to `false` ...
|
||||
await rustCrypto.startDehydration({ rehydrate: false });
|
||||
// ... we should not try to rehydrate ...
|
||||
expect(getDehydratedDeviceMock).not.toHaveBeenCalled();
|
||||
// ... but we should still create a new dehydrated device ...
|
||||
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||
// ... and we should keep the same dehydration key.
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).toEqual(origDehydrationKey);
|
||||
});
|
||||
|
||||
it("should create a new key if createNewKey is set to true", async () => {
|
||||
const origDehydrationKey = await setUpInitialDehydratedDevice();
|
||||
|
||||
// If `createNewKey` is set to `true` ...
|
||||
await rustCrypto.startDehydration({ createNewKey: true });
|
||||
// ... we should rehydrate and dehydrate as normal ...
|
||||
expect(getDehydratedDeviceMock).toHaveBeenCalled();
|
||||
expect(putDehydratedDeviceMock).toHaveBeenCalled();
|
||||
// ... and we should create a new dehydration key.
|
||||
expect(await secretStorage.get("org.matrix.msc3814")).not.toEqual(origDehydrationKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("import & export secrets bundle", () => {
|
||||
@@ -1879,6 +2238,58 @@ describe("RustCrypto", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetEncryption", () => {
|
||||
let secretStorage: ServerSideSecretStorage;
|
||||
beforeEach(() => {
|
||||
secretStorage = {
|
||||
setDefaultKeyId: jest.fn(),
|
||||
hasKey: jest.fn().mockResolvedValue(false),
|
||||
getKey: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as ServerSideSecretStorage;
|
||||
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/upload", { one_time_key_counts: {} });
|
||||
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
|
||||
});
|
||||
|
||||
it("reset should reset 4S, backup and cross-signing", async () => {
|
||||
// When we will delete the key backup
|
||||
let backupIsDeleted = false;
|
||||
fetchMock.delete("path:/_matrix/client/v3/room_keys/version/1", () => {
|
||||
backupIsDeleted = true;
|
||||
return {};
|
||||
});
|
||||
// If the backup is deleted, we will return an empty object
|
||||
fetchMock.get("path:/_matrix/client/v3/room_keys/version", () => {
|
||||
return backupIsDeleted ? {} : testData.SIGNED_BACKUP_DATA;
|
||||
});
|
||||
|
||||
// A new key backup should be created after the reset
|
||||
let newKeyBackupInfo!: KeyBackupInfo;
|
||||
fetchMock.post("path:/_matrix/client/v3/room_keys/version", (res, options) => {
|
||||
newKeyBackupInfo = JSON.parse(options.body as string);
|
||||
return { version: "2" };
|
||||
});
|
||||
|
||||
// We consider the key backup as trusted
|
||||
jest.spyOn(RustBackupManager.prototype, "isKeyBackupTrusted").mockResolvedValue({
|
||||
trusted: true,
|
||||
matchesDecryptionKey: true,
|
||||
});
|
||||
|
||||
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
|
||||
// We have a key backup
|
||||
expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull();
|
||||
|
||||
const authUploadDeviceSigningKeys = jest.fn();
|
||||
await rustCrypto.resetEncryption(authUploadDeviceSigningKeys);
|
||||
|
||||
// A new key backup should be created
|
||||
expect(newKeyBackupInfo.auth_data).toBeTruthy();
|
||||
// The new cross signing keys should be uploaded
|
||||
expect(authUploadDeviceSigningKeys).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/** Build a MatrixHttpApi instance */
|
||||
|
||||
@@ -23,12 +23,14 @@ import {
|
||||
SecretStorageCallbacks,
|
||||
SecretStorageKeyDescriptionAesV1,
|
||||
SecretStorageKeyDescriptionCommon,
|
||||
ServerSideSecretStorage,
|
||||
ServerSideSecretStorageImpl,
|
||||
trimTrailingEquals,
|
||||
} from "../../src/secret-storage";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
import { SecretInfo } from "../../src/secret-storage.ts";
|
||||
import { AccountDataEvents } from "../../src";
|
||||
import { AccountDataEvents, ClientEvent, MatrixEvent, TypedEventEmitter } from "../../src";
|
||||
import { defer, IDeferred } from "../../src/utils";
|
||||
|
||||
declare module "../../src/@types/event" {
|
||||
interface SecretStorageAccountDataEvents {
|
||||
@@ -177,7 +179,7 @@ describe("ServerSideSecretStorageImpl", function () {
|
||||
it("should return true for a correct key check", async function () {
|
||||
const secretStorage = new ServerSideSecretStorageImpl({} as AccountDataClient, {});
|
||||
|
||||
const myKey = new TextEncoder().encode(randomString(32));
|
||||
const myKey = new TextEncoder().encode(secureRandomString(32));
|
||||
const { iv, mac } = await calculateKeyCheck(myKey);
|
||||
|
||||
const keyInfo: SecretStorageKeyDescriptionAesV1 = {
|
||||
@@ -273,6 +275,78 @@ describe("ServerSideSecretStorageImpl", function () {
|
||||
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("unknown algorithm"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("setDefaultKeyId", function () {
|
||||
let secretStorage: ServerSideSecretStorage;
|
||||
let accountDataAdapter: Mocked<AccountDataClient>;
|
||||
let accountDataPromise: IDeferred<void>;
|
||||
beforeEach(() => {
|
||||
accountDataAdapter = mockAccountDataClient();
|
||||
accountDataPromise = defer();
|
||||
accountDataAdapter.setAccountData.mockImplementation(() => {
|
||||
accountDataPromise.resolve();
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
});
|
||||
|
||||
it("should set the default key id", async function () {
|
||||
const setDefaultPromise = secretStorage.setDefaultKeyId("keyId");
|
||||
await accountDataPromise.promise;
|
||||
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {
|
||||
key: "keyId",
|
||||
});
|
||||
|
||||
accountDataAdapter.emit(
|
||||
ClientEvent.AccountData,
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: { key: "keyId" },
|
||||
}),
|
||||
);
|
||||
await setDefaultPromise;
|
||||
});
|
||||
|
||||
it("should set the default key id with a null key id", async function () {
|
||||
const setDefaultPromise = secretStorage.setDefaultKeyId(null);
|
||||
await accountDataPromise.promise;
|
||||
|
||||
expect(accountDataAdapter.setAccountData).toHaveBeenCalledWith("m.secret_storage.default_key", {});
|
||||
|
||||
accountDataAdapter.emit(
|
||||
ClientEvent.AccountData,
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {},
|
||||
}),
|
||||
);
|
||||
await setDefaultPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultKeyId", function () {
|
||||
it("should return null when there is no key", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
expect(await secretStorage.getDefaultKeyId()).toBe(null);
|
||||
});
|
||||
|
||||
it("should return the key id when there is a key", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({ key: "keyId" });
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
expect(await secretStorage.getDefaultKeyId()).toBe("keyId");
|
||||
});
|
||||
|
||||
it("should return null when an empty object is in the account data", async function () {
|
||||
const accountDataAdapter = mockAccountDataClient();
|
||||
accountDataAdapter.getAccountDataFromServer.mockResolvedValue({});
|
||||
const secretStorage = new ServerSideSecretStorageImpl(accountDataAdapter, {});
|
||||
expect(await secretStorage.getDefaultKeyId()).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("trimTrailingEquals", () => {
|
||||
@@ -291,8 +365,13 @@ describe("trimTrailingEquals", () => {
|
||||
});
|
||||
|
||||
function mockAccountDataClient(): Mocked<AccountDataClient> {
|
||||
const eventEmitter = new TypedEventEmitter();
|
||||
return {
|
||||
getAccountDataFromServer: jest.fn().mockResolvedValue(null),
|
||||
setAccountData: jest.fn().mockResolvedValue({}),
|
||||
on: eventEmitter.on.bind(eventEmitter),
|
||||
off: eventEmitter.off.bind(eventEmitter),
|
||||
removeListener: eventEmitter.removeListener.bind(eventEmitter),
|
||||
emit: eventEmitter.emit.bind(eventEmitter),
|
||||
} as unknown as Mocked<AccountDataClient>;
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { IEvent } from "../../src";
|
||||
import { randomString } from "../../src/randomstring";
|
||||
import { secureRandomString } from "../../src/randomstring";
|
||||
import { getRelationsThreadFilter } from "../../src/thread-utils";
|
||||
|
||||
function makeEvent(relatesToEvent: string, relType: string): Partial<IEvent> {
|
||||
return {
|
||||
event_id: randomString(10),
|
||||
event_id: secureRandomString(10),
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
"msgtype": "m.text",
|
||||
|
||||
@@ -374,6 +374,9 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
|
||||
[EventType.Direct]: { [userId: string]: string[] };
|
||||
[EventType.IgnoredUserList]: { [userId: string]: {} };
|
||||
"m.secret_storage.default_key": { key: string };
|
||||
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
|
||||
// (I don't know why it's m.org.matrix...)
|
||||
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
|
||||
"m.identity_server": { base_url: string | null };
|
||||
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
|
||||
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
|
||||
|
||||
+5
@@ -20,6 +20,11 @@ declare module "@matrix-org/matrix-sdk-crypto-wasm" {
|
||||
interface OlmMachine {
|
||||
importSecretsBundle(bundle: RustSdkCryptoJs.SecretsBundle): Promise<void>;
|
||||
exportSecretsBundle(): Promise<RustSdkCryptoJs.SecretsBundle>;
|
||||
importCrossSigningKeys(
|
||||
master_key?: string,
|
||||
self_signing_key?: string,
|
||||
user_signing_key?: string,
|
||||
): Promise<RustSdkCryptoJs.CrossSigningStatus>;
|
||||
}
|
||||
|
||||
interface SecretsBundle {
|
||||
|
||||
@@ -65,3 +65,39 @@ export interface IUsageLimit {
|
||||
// eslint-disable-next-line camelcase
|
||||
admin_contact?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A policy name & url in a specific internationalisation
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_internationalised-policy
|
||||
*/
|
||||
export interface InternationalisedPolicy {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A versioned policy with internationalised variants
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms_response-200_policy-object
|
||||
*/
|
||||
export interface Policy {
|
||||
/**
|
||||
* The version for the policy.
|
||||
* There are no requirements on what this might be and could be “alpha”, semantically versioned, or arbitrary.
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* The policy information for the specified language.
|
||||
* @remarks the type has to include a union with string due to limitations in the type system.
|
||||
*/
|
||||
[lang: string]: InternationalisedPolicy | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the Terms API for Identity servers
|
||||
* @see https://spec.matrix.org/v1.13/identity-service-api/#get_matrixidentityv2terms
|
||||
*/
|
||||
export interface Terms {
|
||||
policies: {
|
||||
[policyName: string]: Policy;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export interface RoomNameEventContent {
|
||||
}
|
||||
|
||||
export interface RoomTopicEventContent {
|
||||
topic: string;
|
||||
topic: string | undefined | null;
|
||||
}
|
||||
|
||||
export interface RoomAvatarEventContent {
|
||||
|
||||
+7
-5
@@ -13,9 +13,6 @@ 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 { EitherAnd } from "matrix-events-sdk";
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue.ts";
|
||||
import { IMessageRendering } from "./extensible_events.ts";
|
||||
|
||||
@@ -52,12 +49,17 @@ export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
|
||||
*/
|
||||
export type MTopicContent = IMessageRendering[];
|
||||
|
||||
type MTopicStable = { [M_TOPIC.altName]: MTopicContent };
|
||||
type MTopicUnstable = { [M_TOPIC.name]: MTopicContent };
|
||||
|
||||
/**
|
||||
* The event definition for an m.topic event (in content)
|
||||
*/
|
||||
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
|
||||
export type MTopicEvent = (MTopicStable & MTopicUnstable) | MTopicStable | MTopicUnstable;
|
||||
|
||||
/**
|
||||
* The event content for an m.room.topic event
|
||||
*/
|
||||
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;
|
||||
export type MRoomTopicEventContent = {
|
||||
topic: string | null | undefined;
|
||||
} & Partial<MTopicEvent>;
|
||||
|
||||
+85
-23
@@ -157,10 +157,11 @@ import {
|
||||
IdServerUnbindResult,
|
||||
JoinRule,
|
||||
Preset,
|
||||
Terms,
|
||||
Visibility,
|
||||
} from "./@types/partials.ts";
|
||||
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper.ts";
|
||||
import { randomString } from "./randomstring.ts";
|
||||
import { secureRandomString } from "./randomstring.ts";
|
||||
import { BackupManager, IKeyBackup, IKeyBackupCheck, IPreparedKeyBackupVersion, TrustInfo } from "./crypto/backup.ts";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE, MSC3089TreeSpace } from "./models/MSC3089TreeSpace.ts";
|
||||
import { ISignatures } from "./@types/signed.ts";
|
||||
@@ -247,6 +248,7 @@ import { ImageInfo } from "./@types/media.ts";
|
||||
import { Capabilities, ServerCapabilities } from "./serverCapabilities.ts";
|
||||
import { sha256 } from "./digest.ts";
|
||||
import { keyFromAuthData } from "./common-crypto/key-passphrase.ts";
|
||||
import { discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig, validateAuthMetadataAndKeys } from "./oidc/index.ts";
|
||||
|
||||
export type Store = IStore;
|
||||
|
||||
@@ -1346,7 +1348,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.usingExternalCrypto = opts.usingExternalCrypto ?? false;
|
||||
this.store = opts.store || new StubStore();
|
||||
this.deviceId = opts.deviceId || null;
|
||||
this.sessionId = randomString(10);
|
||||
this.sessionId = secureRandomString(10);
|
||||
|
||||
const userId = opts.userId || null;
|
||||
this.credentials = { userId };
|
||||
@@ -2310,6 +2312,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
CryptoEvent.KeysChanged,
|
||||
CryptoEvent.DevicesUpdated,
|
||||
CryptoEvent.WillUpdateDevices,
|
||||
CryptoEvent.DehydratedDeviceCreated,
|
||||
CryptoEvent.DehydratedDeviceUploaded,
|
||||
CryptoEvent.RehydrationStarted,
|
||||
CryptoEvent.RehydrationProgress,
|
||||
CryptoEvent.RehydrationCompleted,
|
||||
CryptoEvent.RehydrationError,
|
||||
CryptoEvent.DehydrationKeyCached,
|
||||
CryptoEvent.DehydratedDeviceRotationError,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -4481,11 +4491,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
}
|
||||
|
||||
/**
|
||||
* @param roomId - The room to update the topic in.
|
||||
* @param topic - The plaintext topic. May be empty to remove the topic.
|
||||
* @param htmlTopic - Optional.
|
||||
* @returns Promise which resolves: TODO
|
||||
* @returns Rejects: with an error response.
|
||||
*/
|
||||
public setRoomTopic(roomId: string, topic: string, htmlTopic?: string): Promise<ISendEventResponse> {
|
||||
public setRoomTopic(roomId: string, topic?: string, htmlTopic?: string): Promise<ISendEventResponse> {
|
||||
const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
|
||||
return this.sendStateEvent(roomId, EventType.RoomTopic, content);
|
||||
}
|
||||
@@ -7348,10 +7360,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
const sr = SearchResult.fromJson(roomEvents.results![i], mapper);
|
||||
const room = this.getRoom(sr.context.getEvent().getRoomId());
|
||||
if (room) {
|
||||
// Copy over a known event sender if we can
|
||||
for (const ev of sr.context.getTimeline()) {
|
||||
const sender = room.getMember(ev.getSender()!);
|
||||
if (!ev.sender && sender) ev.sender = sender;
|
||||
ev.setMetadata(room.currentState, false);
|
||||
}
|
||||
}
|
||||
searchResults.results.push(sr);
|
||||
@@ -8000,7 +8010,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns A new client secret
|
||||
*/
|
||||
public generateClientSecret(): string {
|
||||
return randomString(32);
|
||||
return secureRandomString(32);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8248,27 +8258,33 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
/**
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*
|
||||
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
|
||||
* returned credentials. Instead, call {@link loginRequest} and create a new `MatrixClient` instance using the
|
||||
* results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
|
||||
*/
|
||||
public login(loginType: LoginRequest["type"], data: Omit<LoginRequest, "type">): Promise<LoginResponse> {
|
||||
return this.http
|
||||
.authedRequest<LoginResponse>(Method.Post, "/login", undefined, {
|
||||
...data,
|
||||
type: loginType,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.access_token && response.user_id) {
|
||||
this.http.opts.accessToken = response.access_token;
|
||||
this.credentials = {
|
||||
userId: response.user_id,
|
||||
};
|
||||
}
|
||||
return response;
|
||||
});
|
||||
return this.loginRequest({
|
||||
...data,
|
||||
type: loginType,
|
||||
}).then((response) => {
|
||||
if (response.access_token && response.user_id) {
|
||||
this.http.opts.accessToken = response.access_token;
|
||||
this.credentials = {
|
||||
userId: response.user_id,
|
||||
};
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*
|
||||
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
|
||||
* returned credentials. Instead, call {@link loginRequest} with `data.type: "m.login.password"`, and create a new
|
||||
* `MatrixClient` instance using the results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
|
||||
*/
|
||||
public loginWithPassword(user: string, password: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.password", {
|
||||
@@ -8313,6 +8329,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @param token - Login token previously received from homeserver
|
||||
* @returns Promise which resolves to a LoginResponse object
|
||||
* @returns Rejects: with an error response.
|
||||
*
|
||||
* @deprecated This method has unintuitive behaviour: it updates the `MatrixClient` instance with *some* of the
|
||||
* returned credentials. Instead, call {@link loginRequest} with `data.type: "m.login.token"`, and create a new
|
||||
* `MatrixClient` instance using the results. See https://github.com/matrix-org/matrix-js-sdk/issues/4502.
|
||||
*/
|
||||
public loginWithToken(token: string): Promise<LoginResponse> {
|
||||
return this.login("m.login.token", {
|
||||
@@ -8320,6 +8340,20 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a `POST /login` request to the server.
|
||||
*
|
||||
* If successful, this will create a new device and access token for the user.
|
||||
*
|
||||
* @see {@link MatrixClient.loginFlows} which makes a `GET /login` request.
|
||||
* @see https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv3login
|
||||
*
|
||||
* @param data - Credentials and other details for the login request.
|
||||
*/
|
||||
public async loginRequest(data: LoginRequest): Promise<LoginResponse> {
|
||||
return await this.http.authedRequest<LoginResponse>(Method.Post, "/login", undefined, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current session.
|
||||
* Obviously, further calls that require authorisation should fail after this
|
||||
@@ -10003,8 +10037,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
return this.http.authedRequest(Method.Get, path, params);
|
||||
}
|
||||
|
||||
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<any> {
|
||||
// TODO: Types
|
||||
public getTerms(serviceType: SERVICE_TYPES, baseUrl: string): Promise<Terms> {
|
||||
const url = this.termsUrlForService(serviceType, baseUrl);
|
||||
return this.http.requestOtherUrl(Method.Get, url);
|
||||
}
|
||||
@@ -10330,6 +10363,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns Resolves: A promise of an object containing the OIDC issuer if configured
|
||||
* @returns Rejects: when the request fails (module:http-api.MatrixError)
|
||||
* @experimental - part of MSC2965
|
||||
* @deprecated in favour of getAuthMetadata
|
||||
*/
|
||||
public async getAuthIssuer(): Promise<{
|
||||
issuer: string;
|
||||
@@ -10338,6 +10372,34 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover and validate delegated auth configuration
|
||||
* - delegated auth issuer openid-configuration is reachable
|
||||
* - delegated auth issuer openid-configuration is configured correctly for us
|
||||
* Fetches /auth_metadata falling back to legacy implementation using /auth_issuer followed by
|
||||
* https://oidc-issuer.example.com/.well-known/openid-configuration and other files linked therein.
|
||||
* When successful, validated metadata is returned
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
* @experimental - part of MSC2965
|
||||
*/
|
||||
public async getAuthMetadata(): Promise<OidcClientConfig> {
|
||||
let authMetadata: unknown | undefined;
|
||||
try {
|
||||
authMetadata = await this.http.request<unknown>(Method.Get, "/auth_metadata", undefined, undefined, {
|
||||
prefix: ClientPrefix.Unstable + "/org.matrix.msc2965",
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
|
||||
const { issuer } = await this.getAuthIssuer();
|
||||
return discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return validateAuthMetadataAndKeys(authMetadata);
|
||||
}
|
||||
}
|
||||
|
||||
function getUnstableDelayQueryOpts(delayOpts: SendDelayedEventRequestOpts): QueryDict {
|
||||
|
||||
+10
-6
@@ -185,27 +185,31 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
|
||||
/**
|
||||
* Topic event helpers
|
||||
*/
|
||||
export type MakeTopicContent = (topic: string, htmlTopic?: string) => MRoomTopicEventContent;
|
||||
export type MakeTopicContent = (topic: string | null | undefined, htmlTopic?: string) => MRoomTopicEventContent;
|
||||
|
||||
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
|
||||
const renderings = [{ body: topic, mimetype: "text/plain" }];
|
||||
const renderings = [];
|
||||
if (isProvided(topic)) {
|
||||
renderings.push({ body: topic, mimetype: "text/plain" });
|
||||
}
|
||||
if (isProvided(htmlTopic)) {
|
||||
renderings.push({ body: htmlTopic!, mimetype: "text/html" });
|
||||
renderings.push({ body: htmlTopic, mimetype: "text/html" });
|
||||
}
|
||||
return { topic, [M_TOPIC.name]: renderings };
|
||||
};
|
||||
|
||||
export type TopicState = {
|
||||
text: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
|
||||
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
|
||||
if (!Array.isArray(mtopic)) {
|
||||
return { text: content.topic };
|
||||
return { text: content.topic ?? undefined };
|
||||
}
|
||||
const text = mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
|
||||
const text =
|
||||
mtopic?.find((r) => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic ?? undefined;
|
||||
const html = mtopic?.find((r) => r.mimetype === "text/html")?.body;
|
||||
return { text, html };
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Events emitted by the {@link CryptoApi}
|
||||
* Cryptography-related events emitted by the {@link matrix.MatrixClient}.
|
||||
*/
|
||||
export enum CryptoEvent {
|
||||
/**
|
||||
@@ -90,4 +90,63 @@ export enum CryptoEvent {
|
||||
* `progress === total === -1`.
|
||||
*/
|
||||
LegacyCryptoStoreMigrationProgress = "crypto.legacyCryptoStoreMigrationProgress",
|
||||
|
||||
/**
|
||||
* Fires when a new dehydrated device is created locally.
|
||||
*
|
||||
* After the client calls {@link CryptoApi.startDehydration}, this event
|
||||
* will be fired every time a new dehydrated device is created. It may fire
|
||||
* before `startDehydration` returns.
|
||||
*/
|
||||
DehydratedDeviceCreated = "dehydration.DehydratedDeviceCreated",
|
||||
|
||||
/**
|
||||
* Fires when a new dehydrated device is successfully uploaded to the server.
|
||||
*
|
||||
* This should fire shortly after {@link DehydratedDeviceCreated} fires. If
|
||||
* upload is unsuccessful, this will be reported either by an error thrown
|
||||
* by {@link CryptoApi.startDehydration} (for errors that happen before
|
||||
* `startDehydration` returns), or by firing {@link DehydratedDeviceRotationError}
|
||||
* (for errors that happen during regular rotation of the dehydrated device)
|
||||
*/
|
||||
DehydratedDeviceUploaded = "dehydration.DehydratedDeviceUploaded",
|
||||
|
||||
/**
|
||||
* Fires when rehydration has started.
|
||||
*
|
||||
* After the client calls {@link CryptoApi.startDehydration}, this event will
|
||||
* fire if a dehydrated device is found and we attempt to rehydrate it.
|
||||
*/
|
||||
RehydrationStarted = "dehydration.RehydrationStarted",
|
||||
|
||||
/**
|
||||
* Fires during rehydration, to inform the application of rehydration progress.
|
||||
*
|
||||
* The payload is a pair `[roomKeyCount: number, toDeviceCount: number]`,
|
||||
* where `roomKeyCount` is the number of room keys that have been received
|
||||
* so far, and `toDeviceCount` is the number of to-device messages received
|
||||
* so far (including the messages containing room keys).
|
||||
*/
|
||||
RehydrationProgress = "dehydration.RehydrationProgress",
|
||||
|
||||
/** Fires when rehydration has completed successfully. */
|
||||
RehydrationCompleted = "dehydration.RehydrationCompleted",
|
||||
|
||||
/** Fires when there was an error in rehydration.
|
||||
*
|
||||
* The payload is an error message as a string.
|
||||
*/
|
||||
RehydrationError = "dehydration.RehydrationError",
|
||||
|
||||
/**
|
||||
* Fires when a dehydrated device key has been cached in the local database.
|
||||
*/
|
||||
DehydrationKeyCached = "dehydration.DehydrationKeyCached",
|
||||
|
||||
/**
|
||||
* Fires when an error occurs during periodic rotation of the dehydrated device.
|
||||
*
|
||||
* The payload is an error message as a string.
|
||||
*/
|
||||
DehydratedDeviceRotationError = "dehydration.DehydratedDeviceRotationError",
|
||||
}
|
||||
|
||||
@@ -30,4 +30,12 @@ export type CryptoEventHandlerMap = {
|
||||
[CryptoEvent.WillUpdateDevices]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.DevicesUpdated]: (users: string[], initialFetch: boolean) => void;
|
||||
[CryptoEvent.LegacyCryptoStoreMigrationProgress]: (progress: number, total: number) => void;
|
||||
[CryptoEvent.DehydratedDeviceCreated]: () => void;
|
||||
[CryptoEvent.DehydratedDeviceUploaded]: () => void;
|
||||
[CryptoEvent.RehydrationStarted]: () => void;
|
||||
[CryptoEvent.RehydrationProgress]: (roomKeyCount: number, toDeviceCount: number) => void;
|
||||
[CryptoEvent.RehydrationCompleted]: () => void;
|
||||
[CryptoEvent.RehydrationError]: (msg: string) => void;
|
||||
[CryptoEvent.DehydrationKeyCached]: () => void;
|
||||
[CryptoEvent.DehydratedDeviceRotationError]: (msg: string) => void;
|
||||
} & RustBackupCryptoEventMap;
|
||||
|
||||
+60
-6
@@ -41,6 +41,37 @@ import { MatrixEvent } from "../models/event.ts";
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
/**
|
||||
* The options to start device dehydration.
|
||||
*/
|
||||
export interface StartDehydrationOpts {
|
||||
/**
|
||||
* Force creation of a new dehydration key, even if there is already an
|
||||
* existing dehydration key. If `false`, and `onlyIfKeyCached` is `false`, a
|
||||
* new key will be created if there is no existing dehydration key, whether
|
||||
* already cached in our local storage or stored in Secret Storage.
|
||||
*
|
||||
* Checking for the presence of the key in Secret Storage may result in the
|
||||
* `getSecretStorageKey` callback being called.
|
||||
*
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
createNewKey?: boolean;
|
||||
/**
|
||||
* Only start dehydration if we have a dehydration key cached in our local
|
||||
* storage. If `true`, Secret Storage will not be checked. Defaults to
|
||||
* `false`.
|
||||
*/
|
||||
onlyIfKeyCached?: boolean;
|
||||
/**
|
||||
* Try to rehydrate a device before creating a new dehydrated device.
|
||||
* Setting this to `false` may be useful for situations where the client is
|
||||
* known to pre-date the dehydrated device, and so rehydration is
|
||||
* unnecessary. Defaults to `true`.
|
||||
*/
|
||||
rehydrate?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public interface to the cryptography parts of the js-sdk
|
||||
*
|
||||
@@ -221,6 +252,15 @@ export interface CryptoApi {
|
||||
*/
|
||||
pinCurrentUserIdentity(userId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove the requirement for this identity to be verified, and pin it.
|
||||
*
|
||||
* This is useful if the user was previously verified but is not anymore
|
||||
* ({@link UserVerificationStatus.wasCrossSigningVerified}) and it is not possible to verify him again now.
|
||||
*
|
||||
*/
|
||||
withdrawVerificationRequirement(userId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get the verification status of a given device.
|
||||
*
|
||||
@@ -396,6 +436,19 @@ export interface CryptoApi {
|
||||
payload: ToDevicePayload,
|
||||
): Promise<ToDeviceBatch>;
|
||||
|
||||
/**
|
||||
* Reset the encryption of the user by going through the following steps:
|
||||
* - Disable backing up room keys and delete any existing backups.
|
||||
* - Remove the default secret storage key from the account data (ie: the recovery key).
|
||||
* - Reset the cross-signing keys.
|
||||
* - Create a new key backup.
|
||||
*
|
||||
* @param authUploadDeviceSigningKeys - Callback to authenticate the upload of device signing keys.
|
||||
* Used when resetting the cross signing keys.
|
||||
* See {@link BootstrapCrossSigningOpts#authUploadDeviceSigningKeys}.
|
||||
*/
|
||||
resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Device/User verification
|
||||
@@ -627,10 +680,11 @@ export interface CryptoApi {
|
||||
/**
|
||||
* Start using device dehydration.
|
||||
*
|
||||
* - Rehydrates a dehydrated device, if one is available.
|
||||
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
|
||||
* is `true`.
|
||||
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
||||
* Storage.
|
||||
* - If `createNewKey` is set to true, always creates a new key.
|
||||
* - If `opts.createNewKey` is set to true, always creates a new key.
|
||||
* - If a dehydration key is not available, creates a new one.
|
||||
* - Creates a new dehydrated device, and schedules periodically creating
|
||||
* new dehydrated devices.
|
||||
@@ -639,11 +693,11 @@ export interface CryptoApi {
|
||||
* `true`, and must not be called until after cross-signing and secret
|
||||
* storage have been set up.
|
||||
*
|
||||
* @param createNewKey - whether to force creation of a new dehydration key.
|
||||
* This can be used, for example, if Secret Storage is being reset. Defaults
|
||||
* to false.
|
||||
* @param opts - options for device dehydration. For backwards compatibility
|
||||
* with old code, a boolean can be given here, which will be treated as
|
||||
* the `createNewKey` option. However, this is deprecated.
|
||||
*/
|
||||
startDehydration(createNewKey?: boolean): Promise<void>;
|
||||
startDehydration(opts?: StartDehydrationOpts | boolean): Promise<void>;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
|
||||
@@ -264,7 +264,10 @@ class AccountDataClientAdapter
|
||||
return event?.getContent<AccountDataEvents[K]>() ?? null;
|
||||
}
|
||||
|
||||
public setAccountData<K extends keyof AccountDataEvents>(type: K, content: AccountDataEvents[K]): Promise<{}> {
|
||||
public setAccountData<K extends keyof AccountDataEvents>(
|
||||
type: K,
|
||||
content: AccountDataEvents[K] | Record<string, never>,
|
||||
): Promise<{}> {
|
||||
const event = new MatrixEvent({ type, content });
|
||||
const lastEvent = this.values.get(type);
|
||||
this.values.set(type, event);
|
||||
|
||||
+16
-1
@@ -105,6 +105,7 @@ import {
|
||||
CryptoEventHandlerMap as CryptoApiCryptoEventHandlerMap,
|
||||
KeyBackupRestoreResult,
|
||||
KeyBackupRestoreOpts,
|
||||
StartDehydrationOpts,
|
||||
} from "../crypto-api/index.ts";
|
||||
import { Device, DeviceMap } from "../models/device.ts";
|
||||
import { deviceInfoToDevice } from "./device-converter.ts";
|
||||
@@ -1576,6 +1577,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link Crypto.CryptoApi.withdrawVerificationRequirement}.
|
||||
*/
|
||||
public async withdrawVerificationRequirement(userId: string): Promise<void> {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given device is trusted.
|
||||
*
|
||||
@@ -4320,7 +4328,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
/**
|
||||
* Stub function -- dehydration is not implemented here, so throw error
|
||||
*/
|
||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
||||
public async startDehydration(createNewKey?: StartDehydrationOpts | boolean): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@@ -4340,6 +4348,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
|
||||
): Promise<KeyBackupRestoreResult> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub function -- resetEncryption is not implemented here, so throw error
|
||||
*/
|
||||
public resetEncryption(): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { deriveRecoveryKeyFromPassphrase } from "../crypto-api/index.ts";
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
@@ -30,7 +30,7 @@ interface IKey {
|
||||
* @param passphrase - The passphrase to generate the key from
|
||||
*/
|
||||
export async function keyFromPassphrase(passphrase: string): Promise<IKey> {
|
||||
const salt = randomString(32);
|
||||
const salt = secureRandomString(32);
|
||||
|
||||
const key = await deriveRecoveryKeyFromPassphrase(passphrase, salt, DEFAULT_ITERATIONS);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from "../../../randomstring.ts";
|
||||
import { secureRandomString } from "../../../randomstring.ts";
|
||||
import { logger } from "../../../logger.ts";
|
||||
import {
|
||||
CANCEL_TYPE,
|
||||
@@ -283,7 +283,7 @@ export class ToDeviceChannel implements IVerificationChannel {
|
||||
* @returns the transaction id
|
||||
*/
|
||||
public static makeTransactionId(): string {
|
||||
return randomString(32);
|
||||
return secureRandomString(32);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import { Optional } from "matrix-events-sdk";
|
||||
* @param s - The optional to test.
|
||||
* @returns True if the value is defined.
|
||||
*/
|
||||
export function isProvided<T>(s: Optional<T>): boolean {
|
||||
export function isProvided<T>(s: Optional<T>): s is T {
|
||||
return s !== null && s !== undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ const featureSupportResolver: Record<string, FeatureSupportCondition> = {
|
||||
},
|
||||
[Feature.RelationsRecursion]: {
|
||||
unstablePrefixes: ["org.matrix.msc3981"],
|
||||
matrixVersion: "v1.10",
|
||||
},
|
||||
[Feature.IntentionalMentions]: {
|
||||
unstablePrefixes: ["org.matrix.msc3952_intentional_mentions"],
|
||||
|
||||
@@ -167,9 +167,9 @@ export class MatrixError extends HTTPError {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The recommended delay in milliseconds to wait before retrying
|
||||
* the request that triggered {@link error}, or {@link defaultMs} if the
|
||||
* error was not due to rate-limiting or if no valid delay is recommended.
|
||||
* @returns The recommended delay in milliseconds to wait before retrying the request.
|
||||
* @param error - The error to check for a retry delay.
|
||||
* @param defaultMs - The delay to use if the error was not due to rate-limiting or if no valid delay is recommended.
|
||||
*/
|
||||
export function safeGetRetryAfterMs(error: unknown, defaultMs: number): number {
|
||||
if (!(error instanceof HTTPError) || !error.isRateLimitError()) {
|
||||
|
||||
+4
-1
@@ -49,6 +49,7 @@ export * from "./scheduler.ts";
|
||||
export * from "./filter.ts";
|
||||
export * from "./timeline-window.ts";
|
||||
export * from "./interactive-auth.ts";
|
||||
export * from "./version-support.ts";
|
||||
export * from "./service-types.ts";
|
||||
export * from "./store/memory.ts";
|
||||
export * from "./store/indexeddb.ts";
|
||||
@@ -79,6 +80,8 @@ export type * from "./@types/IIdentityServerProvider.ts";
|
||||
export * from "./@types/membership.ts";
|
||||
export * from "./models/room-summary.ts";
|
||||
export * from "./models/event-status.ts";
|
||||
export * from "./models/profile-keys.ts";
|
||||
export * from "./models/related-relations.ts";
|
||||
export type { RoomSummary } from "./client.ts";
|
||||
export * as ContentHelpers from "./content-helpers.ts";
|
||||
export * as SecretStorage from "./secret-storage.ts";
|
||||
@@ -106,7 +109,7 @@ export { CallFeedEvent } from "./webrtc/callFeed.ts";
|
||||
export { StatsReport } from "./webrtc/stats/statsReport.ts";
|
||||
export { Relations, RelationsEvent } from "./models/relations.ts";
|
||||
export { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
export { LocalStorageErrors } from "./store/local-storage-events-emitter.ts";
|
||||
export { LocalStorageErrors, localStorageErrorsEventsEmitter } from "./store/local-storage-events-emitter.ts";
|
||||
export { IdentityProviderBrand, SSOAction } from "./@types/auth.ts";
|
||||
export type { ISSOFlow as SSOFlow, LoginFlow } from "./@types/auth.ts";
|
||||
export type { IHierarchyRelation as HierarchyRelation, IHierarchyRoom as HierarchyRoom } from "./@types/spaces.ts";
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
import { type MatrixClient } from "../client.ts";
|
||||
import { logger as rootLogger } from "../logger.ts";
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
import { Room } from "../models/room.ts";
|
||||
import { EncryptionConfig } from "./MatrixRTCSession.ts";
|
||||
import { secureRandomBase64Url } from "../randomstring.ts";
|
||||
import { EncryptionKeysEventContent } from "./types.ts";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
||||
import { MatrixError, safeGetRetryAfterMs } from "../http-api/errors.ts";
|
||||
import { CallMembership } from "./CallMembership.ts";
|
||||
import { EventType } from "../@types/event.ts";
|
||||
const logger = rootLogger.getChild("MatrixRTCSession");
|
||||
|
||||
/**
|
||||
* A type collecting call encryption statistics for a session.
|
||||
*/
|
||||
export type Statistics = {
|
||||
counters: {
|
||||
/**
|
||||
* The number of times we have sent a room event containing encryption keys.
|
||||
*/
|
||||
roomEventEncryptionKeysSent: number;
|
||||
/**
|
||||
* The number of times we have received a room event containing encryption keys.
|
||||
*/
|
||||
roomEventEncryptionKeysReceived: number;
|
||||
};
|
||||
totals: {
|
||||
/**
|
||||
* The total age (in milliseconds) of all room events containing encryption keys that we have received.
|
||||
* We track the total age so that we can later calculate the average age of all keys received.
|
||||
*/
|
||||
roomEventEncryptionKeysReceivedTotalAge: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This interface is for testing and for making it possible to interchange the encryption manager.
|
||||
* @internal
|
||||
*/
|
||||
export interface IEncryptionManager {
|
||||
join(joinConfig: EncryptionConfig | undefined): void;
|
||||
leave(): void;
|
||||
onMembershipsUpdate(oldMemberships: CallMembership[]): Promise<void>;
|
||||
/**
|
||||
* Process `m.call.encryption_keys` events to track the encryption keys for call participants.
|
||||
* This should be called each time the relevant event is received from a room timeline.
|
||||
* If the event is malformed then it will be logged and ignored.
|
||||
*
|
||||
* @param event the event to process
|
||||
*/
|
||||
onCallEncryptionEventReceived(event: MatrixEvent): void;
|
||||
getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>>;
|
||||
statistics: Statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class implements the IEncryptionManager interface,
|
||||
* and takes care of managing the encryption keys of all rtc members:
|
||||
* - generate new keys for the local user and send them to other participants
|
||||
* - track all keys of all other members and update livekit.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class EncryptionManager implements IEncryptionManager {
|
||||
private manageMediaKeys = false;
|
||||
private keysEventUpdateTimeout?: ReturnType<typeof setTimeout>;
|
||||
private makeNewKeyTimeout?: ReturnType<typeof setTimeout>;
|
||||
private setNewKeyTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||
|
||||
private get updateEncryptionKeyThrottle(): number {
|
||||
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
|
||||
}
|
||||
private get makeKeyDelay(): number {
|
||||
return this.joinConfig?.makeKeyDelay ?? 3_000;
|
||||
}
|
||||
private get useKeyDelay(): number {
|
||||
return this.joinConfig?.useKeyDelay ?? 5_000;
|
||||
}
|
||||
|
||||
private encryptionKeys = new Map<string, Array<{ key: Uint8Array; timestamp: number }>>();
|
||||
private lastEncryptionKeyUpdateRequest?: number;
|
||||
|
||||
// We use this to store the last membership fingerprints we saw, so we can proactively re-send encryption keys
|
||||
// if it looks like a membership has been updated.
|
||||
private lastMembershipFingerprints: Set<string> | undefined;
|
||||
|
||||
private currentEncryptionKeyIndex = -1;
|
||||
|
||||
public statistics: Statistics = {
|
||||
counters: {
|
||||
roomEventEncryptionKeysSent: 0,
|
||||
roomEventEncryptionKeysReceived: 0,
|
||||
},
|
||||
totals: {
|
||||
roomEventEncryptionKeysReceivedTotalAge: 0,
|
||||
},
|
||||
};
|
||||
private joinConfig: EncryptionConfig | undefined;
|
||||
|
||||
public constructor(
|
||||
private client: Pick<MatrixClient, "sendEvent" | "getDeviceId" | "getUserId" | "cancelPendingEvent">,
|
||||
private room: Pick<Room, "roomId">,
|
||||
private getMemberships: () => CallMembership[],
|
||||
private onEncryptionKeysChanged: (
|
||||
keyBin: Uint8Array<ArrayBufferLike>,
|
||||
encryptionKeyIndex: number,
|
||||
participantId: string,
|
||||
) => void,
|
||||
) {}
|
||||
|
||||
public getEncryptionKeys(): Map<string, Array<{ key: Uint8Array; timestamp: number }>> {
|
||||
return this.encryptionKeys;
|
||||
}
|
||||
private joined = false;
|
||||
public join(joinConfig: EncryptionConfig): void {
|
||||
this.joinConfig = joinConfig;
|
||||
this.joined = true;
|
||||
this.manageMediaKeys = this.joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
||||
if (this.joinConfig?.manageMediaKeys) {
|
||||
this.makeNewSenderKey();
|
||||
this.requestSendCurrentKey();
|
||||
}
|
||||
}
|
||||
|
||||
public leave(): void {
|
||||
const userId = this.client.getUserId();
|
||||
const deviceId = this.client.getDeviceId();
|
||||
|
||||
if (!userId) throw new Error("No userId");
|
||||
if (!deviceId) throw new Error("No deviceId");
|
||||
// clear our encryption keys as we're done with them now (we'll
|
||||
// make new keys if we rejoin). We leave keys for other participants
|
||||
// as they may still be using the same ones.
|
||||
this.encryptionKeys.set(getParticipantId(userId, deviceId), []);
|
||||
|
||||
if (this.makeNewKeyTimeout !== undefined) {
|
||||
clearTimeout(this.makeNewKeyTimeout);
|
||||
this.makeNewKeyTimeout = undefined;
|
||||
}
|
||||
for (const t of this.setNewKeyTimeouts) {
|
||||
clearTimeout(t);
|
||||
}
|
||||
this.setNewKeyTimeouts.clear();
|
||||
|
||||
this.manageMediaKeys = false;
|
||||
this.joined = false;
|
||||
}
|
||||
// TODO deduplicate this method. It also is in MatrixRTCSession.
|
||||
private isMyMembership = (m: CallMembership): boolean =>
|
||||
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
|
||||
|
||||
public async onMembershipsUpdate(oldMemberships: CallMembership[]): Promise<void> {
|
||||
if (this.manageMediaKeys && this.joined) {
|
||||
const oldMembershipIds = new Set(
|
||||
oldMemberships.filter((m) => !this.isMyMembership(m)).map(getParticipantIdFromMembership),
|
||||
);
|
||||
const newMembershipIds = new Set(
|
||||
this.getMemberships()
|
||||
.filter((m) => !this.isMyMembership(m))
|
||||
.map(getParticipantIdFromMembership),
|
||||
);
|
||||
|
||||
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
|
||||
// for this once available
|
||||
const anyLeft = Array.from(oldMembershipIds).some((x) => !newMembershipIds.has(x));
|
||||
const anyJoined = Array.from(newMembershipIds).some((x) => !oldMembershipIds.has(x));
|
||||
|
||||
const oldFingerprints = this.lastMembershipFingerprints;
|
||||
// always store the fingerprints of these latest memberships
|
||||
this.storeLastMembershipFingerprints();
|
||||
|
||||
if (anyLeft) {
|
||||
if (this.makeNewKeyTimeout) {
|
||||
// existing rotation in progress, so let it complete
|
||||
} else {
|
||||
logger.debug(`Member(s) have left: queueing sender key rotation`);
|
||||
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
|
||||
}
|
||||
} else if (anyJoined) {
|
||||
logger.debug(`New member(s) have joined: re-sending keys`);
|
||||
this.requestSendCurrentKey();
|
||||
} else if (oldFingerprints) {
|
||||
// does it look like any of the members have updated their memberships?
|
||||
const newFingerprints = this.lastMembershipFingerprints!;
|
||||
|
||||
// We can use https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference
|
||||
// for this once available
|
||||
const candidateUpdates =
|
||||
Array.from(oldFingerprints).some((x) => !newFingerprints.has(x)) ||
|
||||
Array.from(newFingerprints).some((x) => !oldFingerprints.has(x));
|
||||
if (candidateUpdates) {
|
||||
logger.debug(`Member(s) have updated/reconnected: re-sending keys to everyone`);
|
||||
this.requestSendCurrentKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new sender key and add it at the next available index
|
||||
* @param delayBeforeUse - If true, wait for a short period before setting the key for the
|
||||
* media encryptor to use. If false, set the key immediately.
|
||||
* @returns The index of the new key
|
||||
*/
|
||||
private makeNewSenderKey(delayBeforeUse = false): number {
|
||||
const userId = this.client.getUserId();
|
||||
const deviceId = this.client.getDeviceId();
|
||||
|
||||
if (!userId) throw new Error("No userId");
|
||||
if (!deviceId) throw new Error("No deviceId");
|
||||
|
||||
const encryptionKey = secureRandomBase64Url(16);
|
||||
const encryptionKeyIndex = this.getNewEncryptionKeyIndex();
|
||||
logger.info("Generated new key at index " + encryptionKeyIndex);
|
||||
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, Date.now(), delayBeforeUse);
|
||||
return encryptionKeyIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests that we resend our current keys to the room. May send a keys event immediately
|
||||
* or queue for alter if one has already been sent recently.
|
||||
*/
|
||||
private requestSendCurrentKey(): void {
|
||||
if (!this.manageMediaKeys) return;
|
||||
|
||||
if (
|
||||
this.lastEncryptionKeyUpdateRequest &&
|
||||
this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now()
|
||||
) {
|
||||
logger.info("Last encryption key event sent too recently: postponing");
|
||||
if (this.keysEventUpdateTimeout === undefined) {
|
||||
this.keysEventUpdateTimeout = setTimeout(
|
||||
this.sendEncryptionKeysEvent,
|
||||
this.updateEncryptionKeyThrottle,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendEncryptionKeysEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the known encryption keys for a given participant device.
|
||||
*
|
||||
* @param userId the user ID of the participant
|
||||
* @param deviceId the device ID of the participant
|
||||
* @returns The encryption keys for the given participant, or undefined if they are not known.
|
||||
*/
|
||||
private getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
|
||||
return this.encryptionKeys.get(getParticipantId(userId, deviceId))?.map((entry) => entry.key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-sends the encryption keys room event
|
||||
*/
|
||||
private sendEncryptionKeysEvent = async (indexToSend?: number): Promise<void> => {
|
||||
if (this.keysEventUpdateTimeout !== undefined) {
|
||||
clearTimeout(this.keysEventUpdateTimeout);
|
||||
this.keysEventUpdateTimeout = undefined;
|
||||
}
|
||||
this.lastEncryptionKeyUpdateRequest = Date.now();
|
||||
|
||||
if (!this.joined) return;
|
||||
|
||||
logger.info(`Sending encryption keys event. indexToSend=${indexToSend}`);
|
||||
|
||||
const userId = this.client.getUserId();
|
||||
const deviceId = this.client.getDeviceId();
|
||||
|
||||
if (!userId) throw new Error("No userId");
|
||||
if (!deviceId) throw new Error("No deviceId");
|
||||
|
||||
const myKeys = this.getKeysForParticipant(userId, deviceId);
|
||||
|
||||
if (!myKeys) {
|
||||
logger.warn("Tried to send encryption keys event but no keys found!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof indexToSend !== "number" && this.currentEncryptionKeyIndex === -1) {
|
||||
logger.warn("Tried to send encryption keys event but no current key index found!");
|
||||
return;
|
||||
}
|
||||
|
||||
const keyIndexToSend = indexToSend ?? this.currentEncryptionKeyIndex;
|
||||
const keyToSend = myKeys[keyIndexToSend];
|
||||
|
||||
try {
|
||||
const content: EncryptionKeysEventContent = {
|
||||
keys: [
|
||||
{
|
||||
index: keyIndexToSend,
|
||||
key: encodeUnpaddedBase64(keyToSend),
|
||||
},
|
||||
],
|
||||
device_id: deviceId,
|
||||
call_id: "",
|
||||
sent_ts: Date.now(),
|
||||
};
|
||||
|
||||
this.statistics.counters.roomEventEncryptionKeysSent += 1;
|
||||
|
||||
await this.client.sendEvent(this.room.roomId, EventType.CallEncryptionKeysPrefix, content);
|
||||
|
||||
logger.debug(
|
||||
`Embedded-E2EE-LOG updateEncryptionKeyEvent participantId=${userId}:${deviceId} numKeys=${myKeys.length} currentKeyIndex=${this.currentEncryptionKeyIndex} keyIndexToSend=${keyIndexToSend}`,
|
||||
this.encryptionKeys,
|
||||
);
|
||||
} catch (error) {
|
||||
const matrixError = error as MatrixError;
|
||||
if (matrixError.event) {
|
||||
// cancel the pending event: we'll just generate a new one with our latest
|
||||
// keys when we resend
|
||||
this.client.cancelPendingEvent(matrixError.event);
|
||||
}
|
||||
if (this.keysEventUpdateTimeout === undefined) {
|
||||
const resendDelay = safeGetRetryAfterMs(matrixError, 5000);
|
||||
logger.warn(`Failed to send m.call.encryption_key, retrying in ${resendDelay}`, error);
|
||||
this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, resendDelay);
|
||||
} else {
|
||||
logger.info("Not scheduling key resend as another re-send is already pending");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public onCallEncryptionEventReceived = (event: MatrixEvent): void => {
|
||||
const userId = event.getSender();
|
||||
const content = event.getContent<EncryptionKeysEventContent>();
|
||||
|
||||
const deviceId = content["device_id"];
|
||||
const callId = content["call_id"];
|
||||
|
||||
if (!userId) {
|
||||
logger.warn(`Received m.call.encryption_keys with no userId: callId=${callId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// We currently only handle callId = "" (which is the default for room scoped calls)
|
||||
if (callId !== "") {
|
||||
logger.warn(
|
||||
`Received m.call.encryption_keys with unsupported callId: userId=${userId}, deviceId=${deviceId}, callId=${callId}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(content.keys)) {
|
||||
logger.warn(`Received m.call.encryption_keys where keys wasn't an array: callId=${callId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
||||
// We store our own sender key in the same set along with keys from others, so it's
|
||||
// important we don't allow our own keys to be set by one of these events (apart from
|
||||
// the fact that we don't need it anyway because we already know our own keys).
|
||||
logger.info("Ignoring our own keys event");
|
||||
return;
|
||||
}
|
||||
|
||||
this.statistics.counters.roomEventEncryptionKeysReceived += 1;
|
||||
const age = Date.now() - (typeof content.sent_ts === "number" ? content.sent_ts : event.getTs());
|
||||
this.statistics.totals.roomEventEncryptionKeysReceivedTotalAge += age;
|
||||
|
||||
for (const key of content.keys) {
|
||||
if (!key) {
|
||||
logger.info("Ignoring false-y key in keys event");
|
||||
continue;
|
||||
}
|
||||
|
||||
const encryptionKey = key.key;
|
||||
const encryptionKeyIndex = key.index;
|
||||
|
||||
if (
|
||||
!encryptionKey ||
|
||||
encryptionKeyIndex === undefined ||
|
||||
encryptionKeyIndex === null ||
|
||||
callId === undefined ||
|
||||
callId === null ||
|
||||
typeof deviceId !== "string" ||
|
||||
typeof callId !== "string" ||
|
||||
typeof encryptionKey !== "string" ||
|
||||
typeof encryptionKeyIndex !== "number"
|
||||
) {
|
||||
logger.warn(
|
||||
`Malformed call encryption_key: userId=${userId}, deviceId=${deviceId}, encryptionKeyIndex=${encryptionKeyIndex} callId=${callId}`,
|
||||
);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Embedded-E2EE-LOG onCallEncryption userId=${userId}:${deviceId} encryptionKeyIndex=${encryptionKeyIndex} age=${age}ms`,
|
||||
this.encryptionKeys,
|
||||
);
|
||||
this.setEncryptionKey(userId, deviceId, encryptionKeyIndex, encryptionKey, event.getTs());
|
||||
}
|
||||
}
|
||||
};
|
||||
private storeLastMembershipFingerprints(): void {
|
||||
this.lastMembershipFingerprints = new Set(
|
||||
this.getMemberships()
|
||||
.filter((m) => !this.isMyMembership(m))
|
||||
.map((m) => `${getParticipantIdFromMembership(m)}:${m.createdTs()}`),
|
||||
);
|
||||
}
|
||||
|
||||
private getNewEncryptionKeyIndex(): number {
|
||||
if (this.currentEncryptionKeyIndex === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// maximum key index is 255
|
||||
return (this.currentEncryptionKeyIndex + 1) % 256;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an encryption key at a specified index for a participant.
|
||||
* The encryption keys for the local participant are also stored here under the
|
||||
* user and device ID of the local participant.
|
||||
* If the key is older than the existing key at the index, it will be ignored.
|
||||
* @param userId - The user ID of the participant
|
||||
* @param deviceId - Device ID of the participant
|
||||
* @param encryptionKeyIndex - The index of the key to set
|
||||
* @param encryptionKeyString - The string representation of the key to set in base64
|
||||
* @param timestamp - The timestamp of the key. We assume that these are monotonic for each participant device.
|
||||
* @param delayBeforeUse - If true, delay before emitting a key changed event. Useful when setting
|
||||
* encryption keys for the local participant to allow time for the key to
|
||||
* be distributed.
|
||||
*/
|
||||
private setEncryptionKey(
|
||||
userId: string,
|
||||
deviceId: string,
|
||||
encryptionKeyIndex: number,
|
||||
encryptionKeyString: string,
|
||||
timestamp: number,
|
||||
delayBeforeUse = false,
|
||||
): void {
|
||||
const keyBin = decodeBase64(encryptionKeyString);
|
||||
|
||||
const participantId = getParticipantId(userId, deviceId);
|
||||
if (!this.encryptionKeys.has(participantId)) {
|
||||
this.encryptionKeys.set(participantId, []);
|
||||
}
|
||||
const participantKeys = this.encryptionKeys.get(participantId)!;
|
||||
|
||||
const existingKeyAtIndex = participantKeys[encryptionKeyIndex];
|
||||
|
||||
if (existingKeyAtIndex) {
|
||||
if (existingKeyAtIndex.timestamp > timestamp) {
|
||||
logger.info(
|
||||
`Ignoring new key at index ${encryptionKeyIndex} for ${participantId} as it is older than existing known key`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (keysEqual(existingKeyAtIndex.key, keyBin)) {
|
||||
existingKeyAtIndex.timestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
participantKeys[encryptionKeyIndex] = {
|
||||
key: keyBin,
|
||||
timestamp,
|
||||
};
|
||||
|
||||
if (delayBeforeUse) {
|
||||
const useKeyTimeout = setTimeout(() => {
|
||||
this.setNewKeyTimeouts.delete(useKeyTimeout);
|
||||
logger.info(`Delayed-emitting key changed event for ${participantId} idx ${encryptionKeyIndex}`);
|
||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
||||
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
||||
}
|
||||
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
||||
}, this.useKeyDelay);
|
||||
this.setNewKeyTimeouts.add(useKeyTimeout);
|
||||
} else {
|
||||
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
|
||||
this.currentEncryptionKeyIndex = encryptionKeyIndex;
|
||||
}
|
||||
this.onEncryptionKeysChanged(keyBin, encryptionKeyIndex, participantId);
|
||||
}
|
||||
}
|
||||
|
||||
private onRotateKeyTimeout = (): void => {
|
||||
if (!this.manageMediaKeys) return;
|
||||
|
||||
this.makeNewKeyTimeout = undefined;
|
||||
logger.info("Making new sender key for key rotation");
|
||||
const newKeyIndex = this.makeNewSenderKey(true);
|
||||
// send immediately: if we're about to start sending with a new key, it's
|
||||
// important we get it out to others as soon as we can.
|
||||
this.sendEncryptionKeysEvent(newKeyIndex);
|
||||
};
|
||||
}
|
||||
|
||||
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
|
||||
function keysEqual(a: Uint8Array | undefined, b: Uint8Array | undefined): boolean {
|
||||
if (a === b) return true;
|
||||
return !!a && !!b && a.length === b.length && a.every((x, i) => x === b[i]);
|
||||
}
|
||||
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
|
||||
+118
-775
File diff suppressed because it is too large
Load Diff
@@ -153,7 +153,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
||||
|
||||
const wasActiveAndKnown = sess.memberships.length > 0 && !isNewSession;
|
||||
|
||||
sess.onMembershipUpdate();
|
||||
sess.onRTCSessionMemberUpdate();
|
||||
|
||||
const nowActive = sess.memberships.length > 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
import { EventType } from "../@types/event.ts";
|
||||
import { UpdateDelayedEventAction } from "../@types/requests.ts";
|
||||
import type { MatrixClient } from "../client.ts";
|
||||
import { HTTPError, MatrixError } from "../http-api/errors.ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { EventTimeline } from "../models/event-timeline.ts";
|
||||
import { Room } from "../models/room.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { CallMembership, DEFAULT_EXPIRE_DURATION, SessionMembershipData } from "./CallMembership.ts";
|
||||
import { Focus } from "./focus.ts";
|
||||
import { isLivekitFocusActive } from "./LivekitFocus.ts";
|
||||
import { MembershipConfig } from "./MatrixRTCSession.ts";
|
||||
/**
|
||||
* This interface defines what a MembershipManager uses and exposes.
|
||||
* This interface is what we use to write tests and allows to change the actual implementation
|
||||
* Without breaking tests because of some internal method renaming.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface IMembershipManager {
|
||||
/**
|
||||
* If we are trying to join the session.
|
||||
* It does not reflect if the room state is already configures to represent us being joined.
|
||||
* It only means that the Manager is running.
|
||||
* @returns true if we intend to be participating in the MatrixRTC session
|
||||
*/
|
||||
isJoined(): boolean;
|
||||
/**
|
||||
* Start sending all necessary events to make this user participant in the RTC session.
|
||||
* @param fociPreferred the list of preferred foci to use in the joined RTC membership event.
|
||||
* @param fociActive the active focus to use in the joined RTC membership event.
|
||||
*/
|
||||
join(fociPreferred: Focus[], fociActive?: Focus): void;
|
||||
/**
|
||||
* Send all necessary events to make this user leave the RTC session.
|
||||
* @param timeout the maximum duration in ms until the promise is forced to resolve.
|
||||
* @returns It resolves with true in case the leave was sent successfully.
|
||||
* It resolves with false in case we hit the timeout before sending successfully.
|
||||
*/
|
||||
leave(timeout: number | undefined): Promise<boolean>;
|
||||
/**
|
||||
* Call this if the MatrixRTC session members have changed.
|
||||
*/
|
||||
onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void>;
|
||||
/**
|
||||
* The used active focus in the currently joined session.
|
||||
* @returns the used active focus in the currently joined session or undefined if not joined.
|
||||
*/
|
||||
getActiveFocus(): Focus | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* This internal class is used by the MatrixRTCSession to manage the local user's own membership of the session.
|
||||
*
|
||||
* Its responsibitiy is to manage the locals user membership:
|
||||
* - send that sate event
|
||||
* - send the delayed leave event
|
||||
* - update the delayed leave event while connected
|
||||
* - update the state event when it times out (for calls longer than membershipExpiryTimeout ~ 4h)
|
||||
*
|
||||
* It is possible to test this class on its own. The api surface (to use for tests) is
|
||||
* defined in `MembershipManagerInterface`.
|
||||
*
|
||||
* It is recommended to only use this interface for testing to allow replacing this class.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class LegacyMembershipManager implements IMembershipManager {
|
||||
private relativeExpiry: number | undefined;
|
||||
|
||||
private memberEventTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
/**
|
||||
* This is a Foci array that contains the Focus objects this user is aware of and proposes to use.
|
||||
*/
|
||||
private ownFociPreferred?: Focus[];
|
||||
/**
|
||||
* This is a Focus with the specified fields for an ActiveFocus (e.g. LivekitFocusActive for type="livekit")
|
||||
*/
|
||||
private ownFocusActive?: Focus;
|
||||
|
||||
private updateCallMembershipRunning = false;
|
||||
private needCallMembershipUpdate = false;
|
||||
/**
|
||||
* If the server disallows the configured {@link membershipServerSideExpiryTimeout},
|
||||
* this stores a delay that the server does allow.
|
||||
*/
|
||||
private membershipServerSideExpiryTimeoutOverride?: number;
|
||||
private disconnectDelayId: string | undefined;
|
||||
|
||||
private get callMemberEventRetryDelayMinimum(): number {
|
||||
return this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000;
|
||||
}
|
||||
private get membershipExpiryTimeout(): number {
|
||||
return this.joinConfig?.membershipExpiryTimeout ?? DEFAULT_EXPIRE_DURATION;
|
||||
}
|
||||
private get membershipServerSideExpiryTimeout(): number {
|
||||
return (
|
||||
this.membershipServerSideExpiryTimeoutOverride ??
|
||||
this.joinConfig?.membershipServerSideExpiryTimeout ??
|
||||
8_000
|
||||
);
|
||||
}
|
||||
private get membershipKeepAlivePeriod(): number {
|
||||
return this.joinConfig?.membershipKeepAlivePeriod ?? 5_000;
|
||||
}
|
||||
private get callMemberEventRetryJitter(): number {
|
||||
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
private joinConfig: MembershipConfig | undefined,
|
||||
private room: Pick<Room, "getLiveTimeline" | "roomId" | "getVersion">,
|
||||
private client: Pick<
|
||||
MatrixClient,
|
||||
| "getUserId"
|
||||
| "getDeviceId"
|
||||
| "sendStateEvent"
|
||||
| "_unstable_sendDelayedEvent"
|
||||
| "_unstable_sendDelayedStateEvent"
|
||||
| "_unstable_updateDelayedEvent"
|
||||
>,
|
||||
private getOldestMembership: () => CallMembership | undefined,
|
||||
) {}
|
||||
|
||||
public isJoined(): boolean {
|
||||
return this.relativeExpiry !== undefined;
|
||||
}
|
||||
|
||||
public join(fociPreferred: Focus[], fociActive?: Focus): void {
|
||||
this.ownFocusActive = fociActive;
|
||||
this.ownFociPreferred = fociPreferred;
|
||||
this.relativeExpiry = this.membershipExpiryTimeout;
|
||||
// We don't wait for this, mostly because it may fail and schedule a retry, so this
|
||||
// function returning doesn't really mean anything at all.
|
||||
this.triggerCallMembershipEventUpdate();
|
||||
}
|
||||
|
||||
public async leave(timeout: number | undefined = undefined): Promise<boolean> {
|
||||
this.relativeExpiry = undefined;
|
||||
this.ownFocusActive = undefined;
|
||||
|
||||
if (this.memberEventTimeout) {
|
||||
clearTimeout(this.memberEventTimeout);
|
||||
this.memberEventTimeout = undefined;
|
||||
}
|
||||
if (timeout) {
|
||||
// The sleep promise returns the string 'timeout' and the membership update void
|
||||
// A success implies that the membership update was quicker then the timeout.
|
||||
const raceResult = await Promise.race([this.triggerCallMembershipEventUpdate(), sleep(timeout, "timeout")]);
|
||||
return raceResult !== "timeout";
|
||||
} else {
|
||||
await this.triggerCallMembershipEventUpdate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async onRTCSessionMemberUpdate(memberships: CallMembership[]): Promise<void> {
|
||||
const isMyMembership = (m: CallMembership): boolean =>
|
||||
m.sender === this.client.getUserId() && m.deviceId === this.client.getDeviceId();
|
||||
|
||||
if (this.isJoined() && !memberships.some(isMyMembership)) {
|
||||
logger.warn("Missing own membership: force re-join");
|
||||
// TODO: Should this be awaited? And is there anything to tell the focus?
|
||||
return this.triggerCallMembershipEventUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public getActiveFocus(): Focus | undefined {
|
||||
if (this.ownFocusActive) {
|
||||
// A livekit active focus
|
||||
if (isLivekitFocusActive(this.ownFocusActive)) {
|
||||
if (this.ownFocusActive.focus_selection === "oldest_membership") {
|
||||
const oldestMembership = this.getOldestMembership();
|
||||
return oldestMembership?.getPreferredFoci()[0];
|
||||
}
|
||||
} else {
|
||||
logger.warn("Unknown own ActiveFocus type. This makes it impossible to connect to an SFU.");
|
||||
}
|
||||
} else {
|
||||
// We do not understand the membership format (could be legacy). We default to oldestMembership
|
||||
// Once there are other methods this is a hard error!
|
||||
const oldestMembership = this.getOldestMembership();
|
||||
return oldestMembership?.getPreferredFoci()[0];
|
||||
}
|
||||
}
|
||||
|
||||
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
||||
// TODO: Should this await on a shared promise?
|
||||
if (this.updateCallMembershipRunning) {
|
||||
this.needCallMembershipUpdate = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateCallMembershipRunning = true;
|
||||
try {
|
||||
// if anything triggers an update while the update is running, do another update afterwards
|
||||
do {
|
||||
this.needCallMembershipUpdate = false;
|
||||
await this.updateCallMembershipEvent();
|
||||
} while (this.needCallMembershipUpdate);
|
||||
} finally {
|
||||
this.updateCallMembershipRunning = false;
|
||||
}
|
||||
};
|
||||
private makeNewMembership(deviceId: string): SessionMembershipData | {} {
|
||||
// If we're joined, add our own
|
||||
if (this.isJoined()) {
|
||||
return this.makeMyMembership(deviceId);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs our own membership
|
||||
*/
|
||||
private makeMyMembership(deviceId: string): SessionMembershipData {
|
||||
return {
|
||||
call_id: "",
|
||||
scope: "m.room",
|
||||
application: "m.call",
|
||||
device_id: deviceId,
|
||||
expires: this.relativeExpiry,
|
||||
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||
foci_preferred: this.ownFociPreferred ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
private async updateCallMembershipEvent(): Promise<void> {
|
||||
if (this.memberEventTimeout) {
|
||||
clearTimeout(this.memberEventTimeout);
|
||||
this.memberEventTimeout = undefined;
|
||||
}
|
||||
|
||||
const roomState = this.room.getLiveTimeline().getState(EventTimeline.FORWARDS);
|
||||
if (!roomState) throw new Error("Couldn't get room state for room " + this.room.roomId);
|
||||
|
||||
const localUserId = this.client.getUserId();
|
||||
const localDeviceId = this.client.getDeviceId();
|
||||
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
||||
|
||||
let newContent: {} | SessionMembershipData = {};
|
||||
// TODO: add back expiary logic to non-legacy events
|
||||
// previously we checked here if the event is timed out and scheduled a check if not.
|
||||
// maybe there is a better way.
|
||||
newContent = this.makeNewMembership(localDeviceId);
|
||||
|
||||
try {
|
||||
if (this.isJoined()) {
|
||||
const stateKey = this.makeMembershipStateKey(localUserId, localDeviceId);
|
||||
const prepareDelayedDisconnection = async (): Promise<void> => {
|
||||
try {
|
||||
const res = await resendIfRateLimited(() =>
|
||||
this.client._unstable_sendDelayedStateEvent(
|
||||
this.room.roomId,
|
||||
{
|
||||
delay: this.membershipServerSideExpiryTimeout,
|
||||
},
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{}, // leave event
|
||||
stateKey,
|
||||
),
|
||||
);
|
||||
this.disconnectDelayId = res.delay_id;
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof MatrixError &&
|
||||
e.errcode === "M_UNKNOWN" &&
|
||||
e.data["org.matrix.msc4140.errcode"] === "M_MAX_DELAY_EXCEEDED"
|
||||
) {
|
||||
const maxDelayAllowed = e.data["org.matrix.msc4140.max_delay"];
|
||||
if (
|
||||
typeof maxDelayAllowed === "number" &&
|
||||
this.membershipServerSideExpiryTimeout > maxDelayAllowed
|
||||
) {
|
||||
this.membershipServerSideExpiryTimeoutOverride = maxDelayAllowed;
|
||||
return prepareDelayedDisconnection();
|
||||
}
|
||||
}
|
||||
logger.error("Failed to prepare delayed disconnection event:", e);
|
||||
}
|
||||
};
|
||||
|
||||
await prepareDelayedDisconnection();
|
||||
// Send join event _after_ preparing the delayed disconnection event
|
||||
await resendIfRateLimited(() =>
|
||||
this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, stateKey),
|
||||
);
|
||||
// If sending state cancels your own delayed state, prepare another delayed state
|
||||
// TODO: Remove this once MSC4140 is stable & doesn't cancel own delayed state
|
||||
if (this.disconnectDelayId !== undefined) {
|
||||
try {
|
||||
const knownDisconnectDelayId = this.disconnectDelayId;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(
|
||||
knownDisconnectDelayId,
|
||||
UpdateDelayedEventAction.Restart,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof MatrixError && e.errcode === "M_NOT_FOUND") {
|
||||
// If we get a M_NOT_FOUND we prepare a new delayed event.
|
||||
// In other error cases we do not want to prepare anything since we do not have the guarantee, that the
|
||||
// future is not still running.
|
||||
logger.warn("Failed to update delayed disconnection event, prepare it again:", e);
|
||||
this.disconnectDelayId = undefined;
|
||||
await prepareDelayedDisconnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.disconnectDelayId !== undefined) {
|
||||
this.scheduleDelayDisconnection();
|
||||
}
|
||||
} else {
|
||||
// Not joined
|
||||
let sentDelayedDisconnect = false;
|
||||
if (this.disconnectDelayId !== undefined) {
|
||||
try {
|
||||
const knownDisconnectDelayId = this.disconnectDelayId;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(
|
||||
knownDisconnectDelayId,
|
||||
UpdateDelayedEventAction.Send,
|
||||
),
|
||||
);
|
||||
sentDelayedDisconnect = true;
|
||||
} catch (e) {
|
||||
logger.error("Failed to send our delayed disconnection event:", e);
|
||||
}
|
||||
this.disconnectDelayId = undefined;
|
||||
}
|
||||
if (!sentDelayedDisconnect) {
|
||||
await resendIfRateLimited(() =>
|
||||
this.client.sendStateEvent(
|
||||
this.room.roomId,
|
||||
EventType.GroupCallMemberPrefix,
|
||||
{},
|
||||
this.makeMembershipStateKey(localUserId, localDeviceId),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
logger.info("Sent updated call member event.");
|
||||
} catch (e) {
|
||||
const resendDelay = this.callMemberEventRetryDelayMinimum + Math.random() * this.callMemberEventRetryJitter;
|
||||
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
|
||||
await sleep(resendDelay);
|
||||
await this.triggerCallMembershipEventUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleDelayDisconnection(): void {
|
||||
this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod);
|
||||
}
|
||||
|
||||
private readonly delayDisconnection = async (): Promise<void> => {
|
||||
try {
|
||||
const knownDisconnectDelayId = this.disconnectDelayId!;
|
||||
await resendIfRateLimited(() =>
|
||||
this.client._unstable_updateDelayedEvent(knownDisconnectDelayId, UpdateDelayedEventAction.Restart),
|
||||
);
|
||||
this.scheduleDelayDisconnection();
|
||||
} catch (e) {
|
||||
logger.error("Failed to delay our disconnection event:", e);
|
||||
}
|
||||
};
|
||||
|
||||
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
|
||||
const stateKey = `${localUserId}_${localDeviceId}`;
|
||||
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
|
||||
return stateKey;
|
||||
} else {
|
||||
return `_${stateKey}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function resendIfRateLimited<T>(func: () => Promise<T>, numRetriesAllowed: number = 1): Promise<T> {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
return await func();
|
||||
} catch (e) {
|
||||
if (numRetriesAllowed > 0 && e instanceof HTTPError && e.isRateLimitError()) {
|
||||
numRetriesAllowed--;
|
||||
let resendDelay: number;
|
||||
const defaultMs = 5000;
|
||||
try {
|
||||
resendDelay = e.getRetryAfterMs() ?? defaultMs;
|
||||
logger.info(`Rate limited by server, retrying in ${resendDelay}ms`);
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
`Error while retrieving a rate-limit retry delay, retrying after default delay of ${defaultMs}`,
|
||||
e,
|
||||
);
|
||||
resendDelay = defaultMs;
|
||||
}
|
||||
await sleep(resendDelay);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,27 +68,7 @@ export class EventTimeline {
|
||||
* @param toStartOfTimeline - if true the event's forwardLooking flag is set false
|
||||
*/
|
||||
public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
|
||||
// When we try to generate a sentinel member before we have that member
|
||||
// in the members object, we still generate a sentinel but it doesn't
|
||||
// have a membership event, so test to see if events.member is set. We
|
||||
// check this to avoid overriding non-sentinel members by sentinel ones
|
||||
// when adding the event to a filtered timeline
|
||||
if (!event.sender?.events?.member) {
|
||||
event.sender = stateContext.getSentinelMember(event.getSender()!);
|
||||
}
|
||||
if (!event.target?.events?.member && event.getType() === EventType.RoomMember) {
|
||||
event.target = stateContext.getSentinelMember(event.getStateKey()!);
|
||||
}
|
||||
|
||||
if (event.isState()) {
|
||||
// room state has no concept of 'old' or 'current', but we want the
|
||||
// room state to regress back to previous values if toStartOfTimeline
|
||||
// is set, which means inspecting prev_content if it exists. This
|
||||
// is done by toggling the forwardLooking flag.
|
||||
if (toStartOfTimeline) {
|
||||
event.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
event.setMetadata(stateContext, toStartOfTimeline);
|
||||
}
|
||||
|
||||
private readonly roomId: string | null;
|
||||
|
||||
+58
-1
@@ -48,6 +48,7 @@ import { Room } from "./room.ts";
|
||||
import { EventTimeline } from "./event-timeline.ts";
|
||||
import { Membership } from "../@types/membership.ts";
|
||||
import { DecryptionFailureCode } from "../crypto-api/index.ts";
|
||||
import { RoomState } from "./room-state.ts";
|
||||
|
||||
export { EventStatus } from "./event-status.ts";
|
||||
|
||||
@@ -232,6 +233,7 @@ export enum MatrixEventEvent {
|
||||
Status = "Event.status",
|
||||
Replaced = "Event.replaced",
|
||||
RelationsCreated = "Event.relationsCreated",
|
||||
SentinelUpdated = "Event.sentinelUpdated",
|
||||
}
|
||||
|
||||
export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update;
|
||||
@@ -244,6 +246,7 @@ export type MatrixEventHandlerMap = {
|
||||
[MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus | null) => void;
|
||||
[MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
|
||||
[MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
|
||||
[MatrixEventEvent.SentinelUpdated]: () => void;
|
||||
} & Pick<ThreadEventHandlerMap, ThreadEvent.Update>;
|
||||
|
||||
export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap> {
|
||||
@@ -328,6 +331,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* Should be read-only
|
||||
*/
|
||||
public sender: RoomMember | null = null;
|
||||
|
||||
/**
|
||||
* The room member who is the target of this event, e.g.
|
||||
* the invitee, the person being banned, etc.
|
||||
@@ -335,6 +339,51 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* Should be read-only
|
||||
*/
|
||||
public target: RoomMember | null = null;
|
||||
|
||||
/**
|
||||
* Update the sentinels and forwardLooking flag for this event.
|
||||
* @param stateContext - the room state to be queried
|
||||
* @param toStartOfTimeline - if true the event's forwardLooking flag is set false
|
||||
* @internal
|
||||
*/
|
||||
public setMetadata(stateContext: RoomState, toStartOfTimeline: boolean): void {
|
||||
// If an event is an m.room.member state event then we should set the sentinels again in case setEventMetadata
|
||||
// was already called before the state was applied and thus the sentinel points at the member from before this event.
|
||||
const affectsSelf =
|
||||
this.isState() && this.getType() === EventType.RoomMember && this.getSender() === this.getStateKey();
|
||||
|
||||
let changed = false;
|
||||
// When we try to generate a sentinel member before we have that member
|
||||
// in the members object, we still generate a sentinel but it doesn't
|
||||
// have a membership event, so test to see if events.member is set. We
|
||||
// check this to avoid overriding non-sentinel members by sentinel ones
|
||||
// when adding the event to a filtered timeline
|
||||
if (affectsSelf || !this.sender?.events?.member) {
|
||||
const newSender = stateContext.getSentinelMember(this.getSender()!);
|
||||
if (newSender !== this.sender) changed = true;
|
||||
this.sender = newSender;
|
||||
}
|
||||
if (affectsSelf || (!this.target?.events?.member && this.getType() === EventType.RoomMember)) {
|
||||
const newTarget = stateContext.getSentinelMember(this.getStateKey()!);
|
||||
if (newTarget !== this.target) changed = true;
|
||||
this.target = newTarget;
|
||||
}
|
||||
|
||||
if (this.isState()) {
|
||||
// room state has no concept of 'old' or 'current', but we want the
|
||||
// room state to regress back to previous values if toStartOfTimeline
|
||||
// is set, which means inspecting prev_content if it exists. This
|
||||
// is done by toggling the forwardLooking flag.
|
||||
if (toStartOfTimeline) {
|
||||
this.forwardLooking = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
this.emit(MatrixEventEvent.SentinelUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sending status of the event.
|
||||
* @privateRemarks
|
||||
@@ -357,9 +406,12 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
*/
|
||||
public forwardLooking = true;
|
||||
|
||||
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||
/**
|
||||
* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
|
||||
* `Crypto` will set this the `VerificationRequest` for the event
|
||||
* so it can be easily accessed from the timeline.
|
||||
*
|
||||
* @deprecated Not used by the rust crypto implementation.
|
||||
*/
|
||||
public verificationRequest?: VerificationRequest;
|
||||
|
||||
@@ -847,6 +899,8 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
* @param userId - the user who received this event
|
||||
*
|
||||
* @returns a promise that resolves when the request is queued
|
||||
*
|
||||
* @deprecated Not used by the rust crypto implementation.
|
||||
*/
|
||||
public cancelAndResendKeyRequest(crypto: Crypto, userId: string): Promise<void> {
|
||||
const wireContent = this.getWireContent();
|
||||
@@ -1671,6 +1725,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used by the rust crypto implementation.
|
||||
*/
|
||||
public setVerificationRequest(request: VerificationRequest): void {
|
||||
this.verificationRequest = request;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
* match a timezone provided in https://www.iana.org/time-zones.
|
||||
*
|
||||
* @see https://github.com/matrix-org/matrix-spec-proposals/blob/clokep/profile-tz/proposals/4175-profile-field-time-zone.md
|
||||
* @experimental
|
||||
*/
|
||||
export const ProfileKeyMSC4175Timezone = "us.cloke.msc4175.tz";
|
||||
|
||||
@@ -368,6 +368,11 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
* If false, any non-matrix content URLs will be ignored. Setting this option to
|
||||
* true will expose URLs that, if fetched, will leak information about the user
|
||||
* to anyone who they share a room with.
|
||||
* @param useAuthentication - (optional) If true, the caller supports authenticated
|
||||
* media and wants an authentication-required URL. Note that server support for
|
||||
* authenticated media will not be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that useAuthentication
|
||||
* implies allowRedirects. Defaults to false (unauthenticated endpoints).
|
||||
* @returns the avatar URL or null.
|
||||
*/
|
||||
public getAvatarUrl(
|
||||
@@ -377,13 +382,23 @@ export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEve
|
||||
resizeMethod: string,
|
||||
allowDefault = true,
|
||||
allowDirectLinks: boolean,
|
||||
useAuthentication: boolean = false,
|
||||
): string | null {
|
||||
const rawUrl = this.getMxcAvatarUrl();
|
||||
|
||||
if (!rawUrl && !allowDefault) {
|
||||
return null;
|
||||
}
|
||||
const httpUrl = getHttpUriForMxc(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks);
|
||||
const httpUrl = getHttpUriForMxc(
|
||||
baseUrl,
|
||||
rawUrl,
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
allowDirectLinks,
|
||||
undefined,
|
||||
useAuthentication,
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
}
|
||||
|
||||
+16
-1
@@ -1661,6 +1661,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
* "crop" or "scale".
|
||||
* @param allowDefault - True to allow an identicon for this room if an
|
||||
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
|
||||
* @param useAuthentication - (optional) If true, the caller supports authenticated
|
||||
* media and wants an authentication-required URL. Note that server support for
|
||||
* authenticated media will not be checked - it is the caller's responsibility
|
||||
* to do so before calling this function. Note also that useAuthentication
|
||||
* implies allowRedirects. Defaults to false (unauthenticated endpoints).
|
||||
* @returns the avatar URL or null.
|
||||
*/
|
||||
public getAvatarUrl(
|
||||
@@ -1669,6 +1674,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
height: number,
|
||||
resizeMethod: ResizeMethod,
|
||||
allowDefault = true,
|
||||
useAuthentication: boolean = false,
|
||||
): string | null {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
if (!roomAvatarEvent && !allowDefault) {
|
||||
@@ -1677,7 +1683,16 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
|
||||
|
||||
const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
|
||||
if (mainUrl) {
|
||||
return getHttpUriForMxc(baseUrl, mainUrl, width, height, resizeMethod);
|
||||
return getHttpUriForMxc(
|
||||
baseUrl,
|
||||
mainUrl,
|
||||
width,
|
||||
height,
|
||||
resizeMethod,
|
||||
undefined,
|
||||
undefined,
|
||||
useAuthentication,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -17,13 +17,13 @@ limitations under the License.
|
||||
import { IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
|
||||
|
||||
import { logger } from "../logger.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
import {
|
||||
BearerTokenResponse,
|
||||
UserState,
|
||||
validateBearerTokenResponse,
|
||||
ValidatedIssuerMetadata,
|
||||
ValidatedAuthMetadata,
|
||||
validateIdToken,
|
||||
validateStoredUserState,
|
||||
} from "./validate.ts";
|
||||
@@ -52,7 +52,7 @@ export type AuthorizationParams = {
|
||||
* @returns scope
|
||||
*/
|
||||
export const generateScope = (deviceId?: string): string => {
|
||||
const safeDeviceId = deviceId ?? randomString(10);
|
||||
const safeDeviceId = deviceId ?? secureRandomString(10);
|
||||
return `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${safeDeviceId}`;
|
||||
};
|
||||
|
||||
@@ -79,9 +79,9 @@ const generateCodeChallenge = async (codeVerifier: string): Promise<string> => {
|
||||
export const generateAuthorizationParams = ({ redirectUri }: { redirectUri: string }): AuthorizationParams => ({
|
||||
scope: generateScope(),
|
||||
redirectUri,
|
||||
state: randomString(8),
|
||||
nonce: randomString(8),
|
||||
codeVerifier: randomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
|
||||
state: secureRandomString(8),
|
||||
nonce: secureRandomString(8),
|
||||
codeVerifier: secureRandomString(64), // https://tools.ietf.org/html/rfc7636#section-4.1 length needs to be 43-128 characters
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ export const generateOidcAuthorizationUrl = async ({
|
||||
urlState,
|
||||
}: {
|
||||
clientId: string;
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
metadata: ValidatedAuthMetadata;
|
||||
homeserverUrl: string;
|
||||
identityServerUrl?: string;
|
||||
redirectUri: string;
|
||||
|
||||
+16
-9
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
|
||||
import { MetadataService, OidcClientSettingsStore } from "oidc-client-ts";
|
||||
|
||||
import { isValidatedIssuerMetadata, validateOIDCIssuerWellKnown } from "./validate.ts";
|
||||
import { validateAuthMetadata } from "./validate.ts";
|
||||
import { Method, timeoutSignal } from "../http-api/index.ts";
|
||||
import { OidcClientConfig } from "./index.ts";
|
||||
|
||||
@@ -30,6 +30,7 @@ import { OidcClientConfig } from "./index.ts";
|
||||
* @param issuer - the OIDC issuer as returned by the /auth_issuer API
|
||||
* @returns validated authentication metadata and optionally signing keys
|
||||
* @throws when delegated auth config is invalid or unreachable
|
||||
* @deprecated in favour of {@link MatrixClient#getAuthMetadata}
|
||||
*/
|
||||
export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Promise<OidcClientConfig> => {
|
||||
const issuerOpenIdConfigUrl = new URL(".well-known/openid-configuration", issuer);
|
||||
@@ -38,23 +39,29 @@ export const discoverAndValidateOIDCIssuerWellKnown = async (issuer: string): Pr
|
||||
signal: timeoutSignal(5000),
|
||||
});
|
||||
const issuerWellKnown = await issuerWellKnownResponse.json();
|
||||
const validatedIssuerConfig = validateOIDCIssuerWellKnown(issuerWellKnown);
|
||||
return validateAuthMetadataAndKeys(issuerWellKnown);
|
||||
};
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Validate the authentication metadata and fetch the signing keys from the jwks_uri in the metadata
|
||||
* @param authMetadata - the authentication metadata to validate
|
||||
* @returns validated authentication metadata and signing keys
|
||||
*/
|
||||
export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promise<OidcClientConfig> => {
|
||||
const validatedIssuerConfig = validateAuthMetadata(authMetadata);
|
||||
|
||||
// create a temporary settings store, so we can use metadata service for discovery
|
||||
const settings = new OidcClientSettingsStore({
|
||||
authority: issuer,
|
||||
authority: validatedIssuerConfig.issuer,
|
||||
metadata: validatedIssuerConfig,
|
||||
redirect_uri: "", // Not known yet, this is here to make the type checker happy
|
||||
client_id: "", // Not known yet, this is here to make the type checker happy
|
||||
});
|
||||
const metadataService = new MetadataService(settings);
|
||||
const metadata = await metadataService.getMetadata();
|
||||
const signingKeys = (await metadataService.getSigningKeys()) ?? undefined;
|
||||
|
||||
isValidatedIssuerMetadata(metadata);
|
||||
|
||||
return {
|
||||
...validatedIssuerConfig,
|
||||
metadata,
|
||||
signingKeys,
|
||||
signingKeys: await metadataService.getSigningKeys(),
|
||||
};
|
||||
};
|
||||
|
||||
+3
-4
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import type { SigningKey } from "oidc-client-ts";
|
||||
import { ValidatedIssuerConfig, ValidatedIssuerMetadata } from "./validate.ts";
|
||||
import { ValidatedAuthMetadata } from "./validate.ts";
|
||||
|
||||
export * from "./authorize.ts";
|
||||
export * from "./discovery.ts";
|
||||
@@ -28,7 +28,6 @@ export * from "./validate.ts";
|
||||
* Validated config for native OIDC authentication, as returned by {@link discoverAndValidateOIDCIssuerWellKnown}.
|
||||
* Contains metadata and signing keys from the issuer's well-known (https://oidc-issuer.example.com/.well-known/openid-configuration).
|
||||
*/
|
||||
export interface OidcClientConfig extends ValidatedIssuerConfig {
|
||||
metadata: ValidatedIssuerMetadata;
|
||||
signingKeys?: SigningKey[];
|
||||
export interface OidcClientConfig extends ValidatedAuthMetadata {
|
||||
signingKeys: SigningKey[] | null;
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ export const registerOidcClient = async (
|
||||
delegatedAuthConfig: OidcClientConfig,
|
||||
clientMetadata: OidcRegistrationClientMetadata,
|
||||
): Promise<string> => {
|
||||
if (!delegatedAuthConfig.registrationEndpoint) {
|
||||
if (!delegatedAuthConfig.registration_endpoint) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) {
|
||||
if (grantTypes.some((scope) => !delegatedAuthConfig.grant_types_supported.includes(scope))) {
|
||||
throw new Error(OidcError.DynamicRegistrationNotSupported);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const registerOidcClient = async (
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(delegatedAuthConfig.registrationEndpoint, {
|
||||
const response = await fetch(delegatedAuthConfig.registration_endpoint, {
|
||||
method: Method.Post,
|
||||
headers,
|
||||
body: JSON.stringify(metadata),
|
||||
|
||||
@@ -77,11 +77,12 @@ export class OidcTokenRefresher {
|
||||
const scope = generateScope(deviceId);
|
||||
|
||||
this.oidcClient = new OidcClient({
|
||||
...config.metadata,
|
||||
metadata: config,
|
||||
signingKeys: config.signingKeys ?? undefined,
|
||||
client_id: clientId,
|
||||
scope,
|
||||
redirect_uri: redirectUri,
|
||||
authority: config.metadata.issuer,
|
||||
authority: config.issuer,
|
||||
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
+38
-62
@@ -20,13 +20,28 @@ import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
|
||||
import { logger } from "../logger.ts";
|
||||
import { OidcError } from "./error.ts";
|
||||
|
||||
export type ValidatedIssuerConfig = {
|
||||
authorizationEndpoint: string;
|
||||
tokenEndpoint: string;
|
||||
registrationEndpoint?: string;
|
||||
accountManagementEndpoint?: string;
|
||||
accountManagementActionsSupported?: string[];
|
||||
};
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedAuthMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
| "token_endpoint"
|
||||
| "revocation_endpoint"
|
||||
| "response_types_supported"
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
// The OidcMetadata type from oidc-client-ts does not include `prompt_values_supported`
|
||||
// even though it is part of the OIDC spec
|
||||
prompt_values_supported?: string[];
|
||||
};
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
!!value && typeof value === "object" && !Array.isArray(value);
|
||||
@@ -67,78 +82,39 @@ const requiredArrayValue = (wellKnown: Record<string, unknown>, key: string, val
|
||||
* Validates issuer `.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
|
||||
* @param authMetadata - 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)) {
|
||||
export const validateAuthMetadata = (authMetadata: unknown): ValidatedAuthMetadata => {
|
||||
if (!isRecord(authMetadata)) {
|
||||
logger.error("Issuer configuration not found or malformed");
|
||||
throw new Error(OidcError.OpSupport);
|
||||
}
|
||||
|
||||
const isInvalid = [
|
||||
requiredStringProperty(wellKnown, "authorization_endpoint"),
|
||||
requiredStringProperty(wellKnown, "token_endpoint"),
|
||||
requiredStringProperty(wellKnown, "revocation_endpoint"),
|
||||
optionalStringProperty(wellKnown, "registration_endpoint"),
|
||||
optionalStringProperty(wellKnown, "account_management_uri"),
|
||||
optionalStringProperty(wellKnown, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
|
||||
requiredArrayValue(wellKnown, "response_types_supported", "code"),
|
||||
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(wellKnown, "code_challenge_methods_supported", "S256"),
|
||||
requiredStringProperty(authMetadata, "issuer"),
|
||||
requiredStringProperty(authMetadata, "authorization_endpoint"),
|
||||
requiredStringProperty(authMetadata, "token_endpoint"),
|
||||
requiredStringProperty(authMetadata, "revocation_endpoint"),
|
||||
optionalStringProperty(authMetadata, "registration_endpoint"),
|
||||
optionalStringProperty(authMetadata, "account_management_uri"),
|
||||
optionalStringProperty(authMetadata, "device_authorization_endpoint"),
|
||||
optionalStringArrayProperty(authMetadata, "account_management_actions_supported"),
|
||||
requiredArrayValue(authMetadata, "response_types_supported", "code"),
|
||||
requiredArrayValue(authMetadata, "grant_types_supported", "authorization_code"),
|
||||
requiredArrayValue(authMetadata, "code_challenge_methods_supported", "S256"),
|
||||
optionalStringArrayProperty(authMetadata, "prompt_values_supported"),
|
||||
].some((isValid) => !isValid);
|
||||
|
||||
if (!isInvalid) {
|
||||
return {
|
||||
authorizationEndpoint: <string>wellKnown["authorization_endpoint"],
|
||||
tokenEndpoint: <string>wellKnown["token_endpoint"],
|
||||
registrationEndpoint: <string>wellKnown["registration_endpoint"],
|
||||
accountManagementEndpoint: <string>wellKnown["account_management_uri"],
|
||||
accountManagementActionsSupported: <string[]>wellKnown["account_management_actions_supported"],
|
||||
};
|
||||
return authMetadata as ValidatedAuthMetadata;
|
||||
}
|
||||
|
||||
logger.error("Issuer configuration not valid");
|
||||
throw new Error(OidcError.OpSupport);
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata from OIDC authority discovery
|
||||
* With validated properties required in type
|
||||
*/
|
||||
export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
|
||||
Pick<
|
||||
OidcMetadata,
|
||||
| "issuer"
|
||||
| "authorization_endpoint"
|
||||
| "token_endpoint"
|
||||
| "registration_endpoint"
|
||||
| "revocation_endpoint"
|
||||
| "response_types_supported"
|
||||
| "grant_types_supported"
|
||||
| "code_challenge_methods_supported"
|
||||
| "device_authorization_endpoint"
|
||||
> & {
|
||||
// MSC2965 extensions to the OIDC spec
|
||||
account_management_uri?: string;
|
||||
account_management_actions_supported?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps validateOIDCIssuerWellKnown in a type assertion
|
||||
* that asserts expected properties are present
|
||||
* (Typescript assertions cannot be arrow functions)
|
||||
* @param metadata - issuer openid-configuration response
|
||||
* @throws when metadata validation fails
|
||||
*/
|
||||
export function isValidatedIssuerMetadata(
|
||||
metadata: Partial<OidcMetadata>,
|
||||
): asserts metadata is ValidatedIssuerMetadata {
|
||||
validateOIDCIssuerWellKnown(metadata);
|
||||
}
|
||||
|
||||
export const decodeIdToken = (token: string): IdTokenClaims => {
|
||||
try {
|
||||
return jwtDecode<IdTokenClaims>(token);
|
||||
|
||||
+71
-19
@@ -17,9 +17,23 @@ limitations under the License.
|
||||
|
||||
import { encodeUnpaddedBase64Url } from "./base64.ts";
|
||||
|
||||
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const DIGITS = "0123456789";
|
||||
/**
|
||||
* String representing the lowercase latin alphabet for use in {@link secureRandomStringFrom}
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/**
|
||||
* String representing the uppercase latin alphabet for use in secureRandomStringFrom
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
/**
|
||||
* String representing the arabic numerals for use in secureRandomStringFrom
|
||||
* (can be combined with other such exports or other characters by appending strings)
|
||||
*/
|
||||
export const DIGITS = "0123456789";
|
||||
|
||||
export function secureRandomBase64Url(len: number): string {
|
||||
const key = new Uint8Array(len);
|
||||
@@ -28,24 +42,62 @@ export function secureRandomBase64Url(len: number): string {
|
||||
return encodeUnpaddedBase64Url(key);
|
||||
}
|
||||
|
||||
export function randomString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
/**
|
||||
* Generates a random string of uppercase and lowercase letters plus digits using a
|
||||
* cryptographically secure random number generator.
|
||||
* @param len The length of the string to generate
|
||||
* @returns Random string of uppercase and lowercase letters plus digits of length `len`
|
||||
*/
|
||||
export function secureRandomString(len: number): string {
|
||||
return secureRandomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
}
|
||||
|
||||
export function randomLowercaseString(len: number): string {
|
||||
return randomStringFrom(len, LOWERCASE);
|
||||
}
|
||||
|
||||
export function randomUppercaseString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE);
|
||||
}
|
||||
|
||||
function randomStringFrom(len: number, chars: string): string {
|
||||
let ret = "";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
/**
|
||||
* Generate a cryptographically secure random string using characters given.
|
||||
*
|
||||
* @param len - The length of the string to generate (must be positive and less than 32768).
|
||||
* @param chars - The characters to use in the random string (between 2 and 256 characters long).
|
||||
* @returns Random string of characters of length `len`.
|
||||
*/
|
||||
export function secureRandomStringFrom(len: number, chars: string): string {
|
||||
// This is intended for latin strings so 256 possibilities should be more than enough and
|
||||
// means we can use random bytes, minimising the amount of entropy we need to ask for.
|
||||
if (chars.length < 2 || chars.length > 256) {
|
||||
throw new Error("Character set must be between 2 and 256 characters long");
|
||||
}
|
||||
|
||||
return ret;
|
||||
if (len < 1 || len > 32768) {
|
||||
throw new Error("Requested random string length must be between 1 and 32768");
|
||||
}
|
||||
|
||||
// We'll generate random unsigned bytes, so get the largest number less than 256 that is a multiple
|
||||
// of the length of the character set: We'll need to discard any random values that are larger than
|
||||
// this as we can't possibly map them onto the character set while keeping each character equally
|
||||
// likely to be chosen (minus 1 to convert to indices in a string). (Essentially, we're using a d8
|
||||
// to choose between 7 possibilities and re-rolling on an 8, keeping all 7 outcomes equally likely.)
|
||||
// Our random values must be strictly less than this
|
||||
const randomValueCutoff = 256 - (256 % chars.length);
|
||||
|
||||
// Grab 30% more entropy than we need. This should be enough that we can discard the values that are
|
||||
// too high without having to go back and grab more unless we're super unlucky.
|
||||
const entropyBuffer = new Uint8Array(Math.floor(len * 1.3));
|
||||
// Mark all of this buffer as used to start with (we haven't populated it with entropy yet) so it will
|
||||
// be filled on the first iteration.
|
||||
let entropyBufferPos = entropyBuffer.length;
|
||||
|
||||
const result = [];
|
||||
while (result.length < len) {
|
||||
if (entropyBufferPos === entropyBuffer.length) {
|
||||
globalThis.crypto.getRandomValues(entropyBuffer);
|
||||
entropyBufferPos = 0;
|
||||
}
|
||||
|
||||
const randomByte = entropyBuffer[entropyBufferPos++];
|
||||
|
||||
if (randomByte < randomValueCutoff) {
|
||||
result.push(chars[randomByte % chars.length]);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { logger } from "../logger.ts";
|
||||
import { MSC4108SecureChannel } from "./channels/MSC4108SecureChannel.ts";
|
||||
import { MatrixError } from "../http-api/index.ts";
|
||||
import { sleep } from "../utils.ts";
|
||||
import { DEVICE_CODE_SCOPE, discoverAndValidateOIDCIssuerWellKnown, OidcClientConfig } from "../oidc/index.ts";
|
||||
import { DEVICE_CODE_SCOPE, OidcClientConfig } from "../oidc/index.ts";
|
||||
import { CryptoApi } from "../crypto-api/index.ts";
|
||||
|
||||
/**
|
||||
@@ -189,13 +189,12 @@ export class MSC4108SignInWithQR {
|
||||
// MSC4108-Flow: NewScanned -send protocols message
|
||||
let oidcClientConfig: OidcClientConfig | undefined;
|
||||
try {
|
||||
const { issuer } = await this.client!.getAuthIssuer();
|
||||
oidcClientConfig = await discoverAndValidateOIDCIssuerWellKnown(issuer);
|
||||
oidcClientConfig = await this.client!.getAuthMetadata();
|
||||
} catch (e) {
|
||||
logger.error("Failed to discover OIDC metadata", e);
|
||||
}
|
||||
|
||||
if (oidcClientConfig?.metadata.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||
if (oidcClientConfig?.grant_types_supported.includes(DEVICE_CODE_SCOPE)) {
|
||||
await this.send<ProtocolsPayload>({
|
||||
type: PayloadType.Protocols,
|
||||
protocols: ["device_authorization_grant"],
|
||||
|
||||
@@ -87,12 +87,18 @@ export class CrossSigningIdentity {
|
||||
"bootstrapCrossSigning: Cross-signing private keys not found locally, but they are available " +
|
||||
"in secret storage, reading storage and caching locally",
|
||||
);
|
||||
await this.olmMachine.importCrossSigningKeys(
|
||||
const status = await this.olmMachine.importCrossSigningKeys(
|
||||
masterKeyFromSecretStorage,
|
||||
selfSigningKeyFromSecretStorage,
|
||||
userSigningKeyFromSecretStorage,
|
||||
);
|
||||
|
||||
// Check that `importCrossSigningKeys` worked correctly (for example, it will fail silently if the
|
||||
// public keys are not available).
|
||||
if (!status.hasMaster || !status.hasSelfSigning || !status.hasUserSigning) {
|
||||
throw new Error("importCrossSigningKeys failed to import the keys");
|
||||
}
|
||||
|
||||
// Get the current device
|
||||
const device: RustSdkCryptoJs.Device = await this.olmMachine.getDevice(
|
||||
this.olmMachine.userId,
|
||||
|
||||
@@ -21,8 +21,10 @@ import { encodeUri } from "../utils.ts";
|
||||
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api/index.ts";
|
||||
import { IToDeviceEvent } from "../sync-accumulator.ts";
|
||||
import { ServerSideSecretStorage } from "../secret-storage.ts";
|
||||
import { decodeBase64, encodeUnpaddedBase64 } from "../base64.ts";
|
||||
import { decodeBase64 } from "../base64.ts";
|
||||
import { Logger } from "../logger.ts";
|
||||
import { CryptoEvent, CryptoEventHandlerMap, StartDehydrationOpts } from "../crypto-api/index.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
|
||||
/**
|
||||
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
|
||||
@@ -67,9 +69,7 @@ const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000;
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export class DehydratedDeviceManager {
|
||||
/** the secret key used for dehydrating and rehydrating */
|
||||
private key?: Uint8Array;
|
||||
export class DehydratedDeviceManager extends TypedEventEmitter<DehydratedDevicesEvents, DehydratedDevicesEventMap> {
|
||||
/** the ID of the interval for periodically replacing the dehydrated device */
|
||||
private intervalId?: ReturnType<typeof setInterval>;
|
||||
|
||||
@@ -79,7 +79,14 @@ export class DehydratedDeviceManager {
|
||||
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
|
||||
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||
private readonly secretStorage: ServerSideSecretStorage,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private async cacheKey(key: RustSdkCryptoJs.DehydratedDeviceKey): Promise<void> {
|
||||
await this.olmMachine.dehydratedDevices().saveDehydratedDeviceKey(key);
|
||||
this.emit(CryptoEvent.DehydrationKeyCached);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the server supports dehydrated devices.
|
||||
@@ -114,27 +121,39 @@ export class DehydratedDeviceManager {
|
||||
/**
|
||||
* Start using device dehydration.
|
||||
*
|
||||
* - Rehydrates a dehydrated device, if one is available.
|
||||
* - Rehydrates a dehydrated device, if one is available and `opts.rehydrate`
|
||||
* is `true`.
|
||||
* - Creates a new dehydration key, if necessary, and stores it in Secret
|
||||
* Storage.
|
||||
* - If `createNewKey` is set to true, always creates a new key.
|
||||
* - If `opts.createNewKey` is set to true, always creates a new key.
|
||||
* - If a dehydration key is not available, creates a new one.
|
||||
* - Creates a new dehydrated device, and schedules periodically creating
|
||||
* new dehydrated devices.
|
||||
*
|
||||
* @param createNewKey - whether to force creation of a new dehydration key.
|
||||
* This can be used, for example, if Secret Storage is being reset.
|
||||
* @param opts - options for device dehydration. For backwards compatibility
|
||||
* with old code, a boolean can be given here, which will be treated as
|
||||
* the `createNewKey` option. However, this is deprecated.
|
||||
*/
|
||||
public async start(createNewKey?: boolean): Promise<void> {
|
||||
this.stop();
|
||||
try {
|
||||
await this.rehydrateDeviceIfAvailable();
|
||||
} catch (e) {
|
||||
// If rehydration fails, there isn't much we can do about it. Log
|
||||
// the error, and create a new device.
|
||||
this.logger.info("dehydration: Error rehydrating device:", e);
|
||||
public async start(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
|
||||
if (typeof opts === "boolean") {
|
||||
opts = { createNewKey: opts };
|
||||
}
|
||||
if (createNewKey) {
|
||||
|
||||
if (opts.onlyIfKeyCached && !(await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey())) {
|
||||
return;
|
||||
}
|
||||
this.stop();
|
||||
if (opts.rehydrate !== false) {
|
||||
try {
|
||||
await this.rehydrateDeviceIfAvailable();
|
||||
} catch (e) {
|
||||
// If rehydration fails, there isn't much we can do about it. Log
|
||||
// the error, and create a new device.
|
||||
this.logger.info("dehydration: Error rehydrating device:", e);
|
||||
this.emit(CryptoEvent.RehydrationError, (e as Error).message);
|
||||
}
|
||||
}
|
||||
if (opts.createNewKey) {
|
||||
await this.resetKey();
|
||||
}
|
||||
await this.scheduleDeviceDehydration();
|
||||
@@ -151,12 +170,15 @@ export class DehydratedDeviceManager {
|
||||
* Reset the dehydration key.
|
||||
*
|
||||
* Creates a new key and stores it in secret storage.
|
||||
*
|
||||
* @returns The newly-generated key.
|
||||
*/
|
||||
public async resetKey(): Promise<void> {
|
||||
const key = new Uint8Array(32);
|
||||
globalThis.crypto.getRandomValues(key);
|
||||
await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key));
|
||||
this.key = key;
|
||||
public async resetKey(): Promise<RustSdkCryptoJs.DehydratedDeviceKey> {
|
||||
const key = RustSdkCryptoJs.DehydratedDeviceKey.createRandomKey();
|
||||
await this.secretStorage.store(SECRET_STORAGE_NAME, key.toBase64());
|
||||
// Also cache it in the rust SDK's crypto store.
|
||||
await this.cacheKey(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,19 +188,27 @@ export class DehydratedDeviceManager {
|
||||
*
|
||||
* @returns the key, if available, or `null` if no key is available
|
||||
*/
|
||||
private async getKey(create: boolean): Promise<Uint8Array | null> {
|
||||
if (this.key === undefined) {
|
||||
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
|
||||
if (keyB64 === undefined) {
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
await this.resetKey();
|
||||
} else {
|
||||
this.key = decodeBase64(keyB64);
|
||||
private async getKey(create: boolean): Promise<RustSdkCryptoJs.DehydratedDeviceKey | null> {
|
||||
const cachedKey = await this.olmMachine.dehydratedDevices().getDehydratedDeviceKey();
|
||||
if (cachedKey) return cachedKey;
|
||||
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
|
||||
if (keyB64 === undefined) {
|
||||
if (!create) {
|
||||
return null;
|
||||
}
|
||||
return await this.resetKey();
|
||||
}
|
||||
|
||||
// We successfully found the key in secret storage: decode it, and cache it in
|
||||
// the rust SDK's crypto store.
|
||||
const bytes = decodeBase64(keyB64);
|
||||
try {
|
||||
const key = RustSdkCryptoJs.DehydratedDeviceKey.createKeyFromArray(bytes);
|
||||
await this.cacheKey(key);
|
||||
return key;
|
||||
} finally {
|
||||
bytes.fill(0);
|
||||
}
|
||||
return this.key!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,6 +249,7 @@ export class DehydratedDeviceManager {
|
||||
}
|
||||
|
||||
this.logger.info("dehydration: dehydrated device found");
|
||||
this.emit(CryptoEvent.RehydrationStarted);
|
||||
|
||||
const rehydratedDevice = await this.olmMachine
|
||||
.dehydratedDevices()
|
||||
@@ -255,8 +286,11 @@ export class DehydratedDeviceManager {
|
||||
nextBatch = eventResp.next_batch;
|
||||
const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events));
|
||||
roomKeyCount += roomKeyInfos.length;
|
||||
|
||||
this.emit(CryptoEvent.RehydrationProgress, roomKeyCount, toDeviceCount);
|
||||
}
|
||||
this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`);
|
||||
this.emit(CryptoEvent.RehydrationCompleted);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -270,9 +304,11 @@ export class DehydratedDeviceManager {
|
||||
const key = (await this.getKey(true))!;
|
||||
|
||||
const dehydratedDevice = await this.olmMachine.dehydratedDevices().create();
|
||||
this.emit(CryptoEvent.DehydratedDeviceCreated);
|
||||
const request = await dehydratedDevice.keysForUpload("Dehydrated device", key);
|
||||
|
||||
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
|
||||
this.emit(CryptoEvent.DehydratedDeviceUploaded);
|
||||
|
||||
this.logger.info("dehydration: uploaded device");
|
||||
}
|
||||
@@ -287,6 +323,7 @@ export class DehydratedDeviceManager {
|
||||
await this.createAndUploadDehydratedDevice();
|
||||
this.intervalId = setInterval(() => {
|
||||
this.createAndUploadDehydratedDevice().catch((error) => {
|
||||
this.emit(CryptoEvent.DehydratedDeviceRotationError, error.message);
|
||||
this.logger.error("Error creating dehydrated device:", error);
|
||||
});
|
||||
}, DEHYDRATION_INTERVAL);
|
||||
@@ -304,3 +341,23 @@ export class DehydratedDeviceManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The events fired by the DehydratedDeviceManager
|
||||
* @internal
|
||||
*/
|
||||
type DehydratedDevicesEvents =
|
||||
| CryptoEvent.DehydratedDeviceCreated
|
||||
| CryptoEvent.DehydratedDeviceUploaded
|
||||
| CryptoEvent.RehydrationStarted
|
||||
| CryptoEvent.RehydrationProgress
|
||||
| CryptoEvent.RehydrationCompleted
|
||||
| CryptoEvent.RehydrationError
|
||||
| CryptoEvent.DehydrationKeyCached
|
||||
| CryptoEvent.DehydratedDeviceRotationError;
|
||||
|
||||
/**
|
||||
* A map of the {@link DehydratedDeviceEvents} fired by the {@link DehydratedDeviceManager} and their payloads.
|
||||
* @internal
|
||||
*/
|
||||
type DehydratedDevicesEventMap = Pick<CryptoEventHandlerMap, DehydratedDevicesEvents>;
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
CryptoEventHandlerMap,
|
||||
KeyBackupRestoreOpts,
|
||||
KeyBackupRestoreResult,
|
||||
StartDehydrationOpts,
|
||||
} from "../crypto-api/index.ts";
|
||||
import { deviceKeysToDeviceMap, rustDeviceToJsDevice } from "./device-converter.ts";
|
||||
import { IDownloadKeyResult, IQueryKeysRequest } from "../client.ts";
|
||||
@@ -79,7 +80,7 @@ import { EventType, MsgType } from "../@types/event.ts";
|
||||
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
|
||||
import { decryptionKeyMatchesKeyBackupInfo, RustBackupManager } from "./backup.ts";
|
||||
import { TypedReEmitter } from "../ReEmitter.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import { ClientStoppedError } from "../errors.ts";
|
||||
import { ISignatures } from "../@types/signed.ts";
|
||||
import { decodeBase64, encodeBase64 } from "../base64.ts";
|
||||
@@ -88,6 +89,7 @@ import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader.t
|
||||
import { DehydratedDeviceManager } from "./DehydratedDeviceManager.ts";
|
||||
import { VerificationMethod } from "../types.ts";
|
||||
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
|
||||
import { UIAuthCallback } from "../interactive-auth.ts";
|
||||
|
||||
const ALL_VERIFICATION_METHODS = [
|
||||
VerificationMethod.Sas,
|
||||
@@ -182,12 +184,23 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
);
|
||||
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
|
||||
|
||||
// re-emit the events emitted by managers
|
||||
this.reemitter.reEmit(this.backupManager, [
|
||||
CryptoEvent.KeyBackupStatus,
|
||||
CryptoEvent.KeyBackupSessionsRemaining,
|
||||
CryptoEvent.KeyBackupFailed,
|
||||
CryptoEvent.KeyBackupDecryptionKeyCached,
|
||||
]);
|
||||
this.reemitter.reEmit(this.dehydratedDeviceManager, [
|
||||
CryptoEvent.DehydratedDeviceCreated,
|
||||
CryptoEvent.DehydratedDeviceUploaded,
|
||||
CryptoEvent.RehydrationStarted,
|
||||
CryptoEvent.RehydrationProgress,
|
||||
CryptoEvent.RehydrationCompleted,
|
||||
CryptoEvent.RehydrationError,
|
||||
CryptoEvent.DehydrationKeyCached,
|
||||
CryptoEvent.DehydratedDeviceRotationError,
|
||||
]);
|
||||
|
||||
this.crossSigningIdentity = new CrossSigningIdentity(olmMachine, this.outgoingRequestProcessor, secretStorage);
|
||||
|
||||
@@ -687,6 +700,20 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
await userIdentity.pinCurrentMasterKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#withdrawVerificationRequirement}.
|
||||
*/
|
||||
public async withdrawVerificationRequirement(userId: string): Promise<void> {
|
||||
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | RustSdkCryptoJs.OwnUserIdentity | undefined =
|
||||
await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(userId));
|
||||
|
||||
if (userIdentity === undefined) {
|
||||
throw new Error("Cannot withdraw verification of unknown user");
|
||||
}
|
||||
|
||||
await userIdentity.withdrawVerification();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#isCrossSigningReady}
|
||||
*/
|
||||
@@ -956,7 +983,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
if (password) {
|
||||
// Generate the key from the passphrase
|
||||
// first we generate a random salt
|
||||
const salt = randomString(32);
|
||||
const salt = secureRandomString(32);
|
||||
// then we derive the key from the passphrase
|
||||
const recoveryKey = await deriveRecoveryKeyFromPassphrase(
|
||||
password,
|
||||
@@ -1099,7 +1126,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
* @returns the event id
|
||||
*/
|
||||
private async sendVerificationRequestContent(roomId: string, verificationEventContent: string): Promise<string> {
|
||||
const txId = randomString(32);
|
||||
const txId = secureRandomString(32);
|
||||
// Send the verification request content to the DM room
|
||||
const { event_id: eventId } = await this.http.authedRequest<{ event_id: string }>(
|
||||
Method.Put,
|
||||
@@ -1399,11 +1426,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#startDehydration}.
|
||||
*/
|
||||
public async startDehydration(createNewKey?: boolean): Promise<void> {
|
||||
public async startDehydration(opts: StartDehydrationOpts | boolean = {}): Promise<void> {
|
||||
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
|
||||
throw new Error("Device dehydration requires cross-signing and secret storage to be set up");
|
||||
}
|
||||
return await this.dehydratedDeviceManager.start(createNewKey);
|
||||
return await this.dehydratedDeviceManager.start(opts || {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1472,6 +1499,30 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
|
||||
return batch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of {@link CryptoApi#resetEncryption}.
|
||||
*/
|
||||
public async resetEncryption(authUploadDeviceSigningKeys: UIAuthCallback<void>): Promise<void> {
|
||||
this.logger.debug("resetEncryption: resetting encryption");
|
||||
|
||||
// Disable backup, and delete all the backups from the server
|
||||
await this.backupManager.deleteAllKeyBackupVersions();
|
||||
|
||||
// Disable the recovery key and the secret storage
|
||||
await this.secretStorage.setDefaultKeyId(null);
|
||||
|
||||
// Reset the cross-signing keys
|
||||
await this.crossSigningIdentity.bootstrapCrossSigning({
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
|
||||
// Create a new key backup
|
||||
await this.resetKeyBackup();
|
||||
|
||||
this.logger.debug("resetEncryption: ended");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SyncCryptoCallbacks implementation
|
||||
|
||||
+28
-10
@@ -23,7 +23,7 @@ limitations under the License.
|
||||
import { TypedEventEmitter } from "./models/typed-event-emitter.ts";
|
||||
import { ClientEvent, ClientEventHandlerMap } from "./client.ts";
|
||||
import { MatrixEvent } from "./models/event.ts";
|
||||
import { randomString } from "./randomstring.ts";
|
||||
import { secureRandomString } from "./randomstring.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import encryptAESSecretStorageItem from "./utils/encryptAESSecretStorageItem.ts";
|
||||
import decryptAESSecretStorageItem from "./utils/decryptAESSecretStorageItem.ts";
|
||||
@@ -148,7 +148,10 @@ export interface AccountDataClient extends TypedEventEmitter<ClientEvent.Account
|
||||
* @param content - the content object to be set
|
||||
* @returns an empty object
|
||||
*/
|
||||
setAccountData: <K extends keyof AccountDataEvents>(eventType: K, content: AccountDataEvents[K]) => Promise<{}>;
|
||||
setAccountData: <K extends keyof AccountDataEvents>(
|
||||
eventType: K,
|
||||
content: AccountDataEvents[K] | Record<string, never>,
|
||||
) => Promise<{}>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,9 +319,12 @@ export interface ServerSideSecretStorage {
|
||||
/**
|
||||
* Set the default key ID for encrypting secrets.
|
||||
*
|
||||
* If keyId is `null`, the default key id value in the account data will be set to an empty object.
|
||||
* This is considered as "disabling" the default key.
|
||||
*
|
||||
* @param keyId - The new default key ID
|
||||
*/
|
||||
setDefaultKeyId(keyId: string): Promise<void>;
|
||||
setDefaultKeyId(keyId: string | null): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,21 +363,33 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default key ID for encrypting secrets.
|
||||
*
|
||||
* @param keyId - The new default key ID
|
||||
* Implementation of {@link ServerSideSecretStorage#setDefaultKeyId}.
|
||||
*/
|
||||
public setDefaultKeyId(keyId: string): Promise<void> {
|
||||
public setDefaultKeyId(keyId: string | null): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const listener = (ev: MatrixEvent): void => {
|
||||
if (ev.getType() === "m.secret_storage.default_key" && ev.getContent().key === keyId) {
|
||||
if (ev.getType() !== "m.secret_storage.default_key") {
|
||||
// Different account data item
|
||||
return;
|
||||
}
|
||||
|
||||
// If keyId === null, the content should be an empty object.
|
||||
// Otherwise, the `key` in the content object should match keyId.
|
||||
const content = ev.getContent();
|
||||
const isSameKey = keyId === null ? Object.keys(content).length === 0 : content.key === keyId;
|
||||
if (isSameKey) {
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.accountDataAdapter.on(ClientEvent.AccountData, listener);
|
||||
|
||||
this.accountDataAdapter.setAccountData("m.secret_storage.default_key", { key: keyId }).catch((e) => {
|
||||
// The spec [1] says that the value of the account data entry should be an object with a `key` property.
|
||||
// It doesn't specify how to delete the default key; we do it by setting the account data to an empty object.
|
||||
//
|
||||
// [1]: https://spec.matrix.org/v1.13/client-server-api/#key-storage
|
||||
const newValue: Record<string, never> | { key: string } = keyId === null ? {} : { key: keyId };
|
||||
this.accountDataAdapter.setAccountData("m.secret_storage.default_key", newValue).catch((e) => {
|
||||
this.accountDataAdapter.removeListener(ClientEvent.AccountData, listener);
|
||||
reject(e);
|
||||
});
|
||||
@@ -417,7 +435,7 @@ export class ServerSideSecretStorageImpl implements ServerSideSecretStorage {
|
||||
// Create a unique key id. XXX: this is racey.
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
keyId = secureRandomString(32);
|
||||
} while (await this.accountDataAdapter.getAccountDataFromServer(`m.secret_storage.key.${keyId}`));
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
SetPresence,
|
||||
} from "./sync.ts";
|
||||
import { MatrixEvent } from "./models/event.ts";
|
||||
import { Crypto } from "./crypto/index.ts";
|
||||
import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator.ts";
|
||||
import { MatrixError } from "./http-api/index.ts";
|
||||
import {
|
||||
@@ -66,7 +65,7 @@ type ExtensionE2EEResponse = Pick<
|
||||
>;
|
||||
|
||||
class ExtensionE2EE implements Extension<ExtensionE2EERequest, ExtensionE2EEResponse> {
|
||||
public constructor(private readonly crypto: Crypto) {}
|
||||
public constructor(private readonly crypto: SyncCryptoCallbacks) {}
|
||||
|
||||
public name(): string {
|
||||
return "e2ee";
|
||||
@@ -373,8 +372,8 @@ export class SlidingSyncSdk {
|
||||
new ExtensionTyping(this.client),
|
||||
new ExtensionReceipts(this.client),
|
||||
];
|
||||
if (this.syncOpts.crypto) {
|
||||
extensions.push(new ExtensionE2EE(this.syncOpts.crypto));
|
||||
if (this.syncOpts.cryptoCallbacks) {
|
||||
extensions.push(new ExtensionE2EE(this.syncOpts.cryptoCallbacks));
|
||||
}
|
||||
extensions.forEach((ext) => {
|
||||
this.slidingSync.registerExtension(ext);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { RoomMember } from "./models/room-member.ts";
|
||||
import { EventType } from "./@types/event.ts";
|
||||
import { DecryptionFailureCode } from "./crypto-api/index.ts";
|
||||
import { DecryptionError, EventDecryptionResult } from "./common-crypto/CryptoBackend.ts";
|
||||
import { OidcClientConfig, ValidatedAuthMetadata } from "./oidc/index.ts";
|
||||
|
||||
/**
|
||||
* Create a {@link MatrixEvent}.
|
||||
@@ -188,3 +189,45 @@ export async function decryptExistingEvent(
|
||||
} as Parameters<MatrixEvent["attemptDecryption"]>[0];
|
||||
await mxEvent.attemptDecryption(mockCrypto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a valid OidcClientConfig with minimum valid values
|
||||
* @param issuer used as the base for all other urls
|
||||
* @param additionalGrantTypes to add to the default grant types
|
||||
* @returns OidcClientConfig
|
||||
* @experimental
|
||||
*/
|
||||
export const makeDelegatedAuthConfig = (
|
||||
issuer = "https://auth.org/",
|
||||
additionalGrantTypes: string[] = [],
|
||||
): OidcClientConfig => {
|
||||
const metadata = mockOpenIdConfiguration(issuer, additionalGrantTypes);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
signingKeys: null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Useful for mocking <issuer>/.well-known/openid-configuration
|
||||
* @param issuer used as the base for all other urls
|
||||
* @param additionalGrantTypes to add to the default grant types
|
||||
* @returns ValidatedAuthMetadata
|
||||
* @experimental
|
||||
*/
|
||||
export const mockOpenIdConfiguration = (
|
||||
issuer = "https://auth.org/",
|
||||
additionalGrantTypes: string[] = [],
|
||||
): ValidatedAuthMetadata => ({
|
||||
issuer,
|
||||
revocation_endpoint: issuer + "revoke",
|
||||
token_endpoint: issuer + "token",
|
||||
authorization_endpoint: issuer + "auth",
|
||||
registration_endpoint: issuer + "registration",
|
||||
device_authorization_endpoint: issuer + "device",
|
||||
jwks_uri: issuer + "jwks",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code", "refresh_token", ...additionalGrantTypes],
|
||||
code_challenge_methods_supported: ["S256"],
|
||||
});
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ import { checkObjectHasKeys, isNullOrUndefined, recursivelyAssign } from "../uti
|
||||
import { MatrixEvent } from "../models/event.ts";
|
||||
import { EventType, TimelineEvents, ToDeviceMessageId } from "../@types/event.ts";
|
||||
import { RoomMember } from "../models/room-member.ts";
|
||||
import { randomString } from "../randomstring.ts";
|
||||
import { secureRandomString } from "../randomstring.ts";
|
||||
import {
|
||||
MCallReplacesEvent,
|
||||
MCallAnswer,
|
||||
@@ -277,7 +277,7 @@ export class CallError extends Error {
|
||||
}
|
||||
|
||||
export function genCallID(): string {
|
||||
return Date.now().toString() + randomString(16);
|
||||
return Date.now().toString() + secureRandomString(16);
|
||||
}
|
||||
|
||||
function getCodecParamMods(isPtt: boolean): CodecParamsMod[] {
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
semver "^6.3.1"
|
||||
|
||||
"@babel/eslint-parser@^7.12.10":
|
||||
version "7.25.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.25.9.tgz#603c68a63078796527bc9d0833f5e52dd5f9224c"
|
||||
integrity sha512-5UXfgpK0j0Xr/xIdgdLEhOFxaDZ0bRPWJJchRpqOSur/3rZoPbqqki5mm0p4NE2cs28krBEiSM2MB7//afRSQQ==
|
||||
version "7.26.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.26.5.tgz#aa669f4d873f9cd617050cf3c40c19cd96307efb"
|
||||
integrity sha512-Kkm8C8uxI842AwQADxl0GbcG1rupELYLShazYEZO/2DYjhyWXJIOUVOE3tBYm6JXzUCNJOZEzqc4rCW/jsEQYQ==
|
||||
dependencies:
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1"
|
||||
eslint-visitor-keys "^2.1.0"
|
||||
@@ -1164,13 +1164,13 @@
|
||||
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
|
||||
|
||||
"@gerrit0/mini-shiki@^1.24.0":
|
||||
version "1.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.24.1.tgz#60ef10f4e2cfac7a9223e10b88c128438aa44fd8"
|
||||
integrity sha512-PNP/Gjv3VqU7z7DjRgO3F9Ok5frTKqtpV+LJW1RzMcr2zpRk0ulhEWnbcNGXzPC7BZyWMIHrkfQX2GZRfxrn6Q==
|
||||
version "1.26.1"
|
||||
resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-1.26.1.tgz#b59884bd6013976ca66dec197492a1387fdbea52"
|
||||
integrity sha512-gHFUvv9f1fU2Piou/5Y7Sx5moYxcERbC7CXc6rkDLQTUBg5Dgg9L4u29/nHqfoQ3Y9R0h0BcOhd14uOEZIBP7Q==
|
||||
dependencies:
|
||||
"@shikijs/engine-oniguruma" "^1.24.0"
|
||||
"@shikijs/types" "^1.24.0"
|
||||
"@shikijs/vscode-textmate" "^9.3.0"
|
||||
"@shikijs/engine-oniguruma" "^1.26.1"
|
||||
"@shikijs/types" "^1.26.1"
|
||||
"@shikijs/vscode-textmate" "^10.0.1"
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
@@ -1477,10 +1477,10 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^12.1.0":
|
||||
version "12.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.1.0.tgz#2aef64eab2d30c0a1ace9c0fe876f53aa2949f14"
|
||||
integrity sha512-NhJFu/8FOGjnW7mDssRUzaMSwXrYOcCqgAjZyAw9KQ9unNADKEi7KoIKe7GtrG2PWtm36y2bUf+hB8vhSY6Wdw==
|
||||
"@matrix-org/matrix-sdk-crypto-wasm@^13.0.0":
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-13.0.0.tgz#658bed951e4c8a06a6dd545575a79cf32022d4ba"
|
||||
integrity sha512-2gtpjnxL42sdJAgkwitpMMI4cw7Gcjf5sW0MXoe+OAlXPlxIzyM+06F5JJ8ENvBeHkuV2RqtFIRrh8i90HLsMw==
|
||||
|
||||
"@matrix-org/olm@3.2.15":
|
||||
version "3.2.15"
|
||||
@@ -1522,12 +1522,33 @@
|
||||
"@nodelib/fs.stat" "2.0.5"
|
||||
run-parallel "^1.1.9"
|
||||
|
||||
"@nodelib/fs.scandir@4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-4.0.1.tgz#4b981a1d6e34e2baff9a33c84b57709e30e8ce30"
|
||||
integrity sha512-vAkI715yhnmiPupY+dq+xenu5Tdf2TBQ66jLvBIcCddtz+5Q8LbMKaf9CIJJreez8fQ8fgaY+RaywQx8RJIWpw==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "4.0.0"
|
||||
run-parallel "^1.2.0"
|
||||
|
||||
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
|
||||
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
|
||||
|
||||
"@nodelib/fs.walk@1.2.8", "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
|
||||
"@nodelib/fs.stat@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-4.0.0.tgz#d609b10d9c17dac012c4d7be93602ed939861e58"
|
||||
integrity sha512-ctr6bByzksKRCV0bavi8WoQevU6plSp2IkllIsEqaiKe2mwNNnaluhnRhcsgGZHrrHk57B3lf95MkLMO3STYcg==
|
||||
|
||||
"@nodelib/fs.walk@3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-3.0.1.tgz#717a03eb1d07c09db32f884a5d0c06ccd9b657f4"
|
||||
integrity sha512-nIh/M6Kh3ZtOmlY00DaUYB4xeeV6F3/ts1l29iwl3/cfyY/OuCfUx+v08zgx8TKPTifXRcjjqVQ4KB2zOYSbyw==
|
||||
dependencies:
|
||||
"@nodelib/fs.scandir" "4.0.1"
|
||||
fastq "^1.15.0"
|
||||
|
||||
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
|
||||
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
|
||||
@@ -1582,26 +1603,26 @@
|
||||
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
|
||||
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
|
||||
|
||||
"@shikijs/engine-oniguruma@^1.24.0":
|
||||
version "1.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz#4e6f49413fbc96dabfa30cb232ca1acf5ca1a446"
|
||||
integrity sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg==
|
||||
"@shikijs/engine-oniguruma@^1.26.1":
|
||||
version "1.26.1"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz#f9de733e2473e693b3d10bff32bb9761746c1d71"
|
||||
integrity sha512-F5XuxN1HljLuvfXv7d+mlTkV7XukC1cawdtOo+7pKgPD83CAB1Sf8uHqP3PK0u7njFH0ZhoXE1r+0JzEgAQ+kg==
|
||||
dependencies:
|
||||
"@shikijs/types" "1.24.0"
|
||||
"@shikijs/vscode-textmate" "^9.3.0"
|
||||
"@shikijs/types" "1.26.1"
|
||||
"@shikijs/vscode-textmate" "^10.0.1"
|
||||
|
||||
"@shikijs/types@1.24.0", "@shikijs/types@^1.24.0":
|
||||
version "1.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.24.0.tgz#a1755b125cb8fb1780a876a0a57242939eafd79f"
|
||||
integrity sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug==
|
||||
"@shikijs/types@1.26.1", "@shikijs/types@^1.26.1":
|
||||
version "1.26.1"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-1.26.1.tgz#b5ece69e21000f53d65d15ddae33d9ad9c3763ad"
|
||||
integrity sha512-d4B00TKKAMaHuFYgRf3L0gwtvqpW4hVdVwKcZYbBfAAQXspgkbWqnFfuFl3MDH6gLbsubOcr+prcnsqah3ny7Q==
|
||||
dependencies:
|
||||
"@shikijs/vscode-textmate" "^9.3.0"
|
||||
"@shikijs/vscode-textmate" "^10.0.1"
|
||||
"@types/hast" "^3.0.4"
|
||||
|
||||
"@shikijs/vscode-textmate@^9.3.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz#b2f1776e488c1d6c2b6cd129bab62f71bbc9c7ab"
|
||||
integrity sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==
|
||||
"@shikijs/vscode-textmate@^10.0.1":
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz#d06d45b67ac5e9b0088e3f67ebd3f25c6c3d711a"
|
||||
integrity sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==
|
||||
|
||||
"@sinclair/typebox@^0.24.1":
|
||||
version "0.24.51"
|
||||
@@ -1637,9 +1658,9 @@
|
||||
p-map "^4.0.0"
|
||||
|
||||
"@stylistic/eslint-plugin@^2.9.0":
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.11.0.tgz#50d0289f36f7201055b7fa1729fdc1d8c46e93fa"
|
||||
integrity sha512-PNRHbydNG5EH8NK4c+izdJlxajIR6GxcUhzsYNRsn6Myep4dsZt0qFCz3rCPnkvgO5FYibDcMqgNHUT+zvjYZw==
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz#53bf175dac8c1ec055b370a6ff77d491cae9a70d"
|
||||
integrity sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.13.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
@@ -1803,9 +1824,9 @@
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@18":
|
||||
version "18.19.67"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.67.tgz#77c4b01641a1e3e1509aff7e10d39e4afd5ae06d"
|
||||
integrity sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==
|
||||
version "18.19.71"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.71.tgz#96d4f0a0be735ead6c8998c62a4b2c0012a5d09a"
|
||||
integrity sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -1857,174 +1878,84 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^8.0.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz#0901933326aea4443b81df3f740ca7dfc45c7bea"
|
||||
integrity sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz#395014a75112ecdb81142b866ab6bb62e3be0f2a"
|
||||
integrity sha512-eTH+UOR4I7WbdQnG4Z48ebIA6Bgi7WO8HvFEneeYBxG8qCOYgTOFPSg6ek9ITIDvGjDQzWHcoWHCDO2biByNzA==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.18.0"
|
||||
"@typescript-eslint/type-utils" "8.18.0"
|
||||
"@typescript-eslint/utils" "8.18.0"
|
||||
"@typescript-eslint/visitor-keys" "8.18.0"
|
||||
"@typescript-eslint/scope-manager" "8.21.0"
|
||||
"@typescript-eslint/type-utils" "8.21.0"
|
||||
"@typescript-eslint/utils" "8.21.0"
|
||||
"@typescript-eslint/visitor-keys" "8.21.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/parser@^8.0.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.18.0.tgz#a1c9456cbb6a089730bf1d3fc47946c5fb5fe67b"
|
||||
integrity sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.21.0.tgz#312c638aaba4f640d45bfde7c6795a9d75deb088"
|
||||
integrity sha512-Wy+/sdEH9kI3w9civgACwabHbKl+qIOu0uFZ9IMKzX3Jpv9og0ZBJrZExGrPpFAY7rWsXuxs5e7CPPP17A4eYA==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.18.0"
|
||||
"@typescript-eslint/types" "8.18.0"
|
||||
"@typescript-eslint/typescript-estree" "8.18.0"
|
||||
"@typescript-eslint/visitor-keys" "8.18.0"
|
||||
"@typescript-eslint/scope-manager" "8.21.0"
|
||||
"@typescript-eslint/types" "8.21.0"
|
||||
"@typescript-eslint/typescript-estree" "8.21.0"
|
||||
"@typescript-eslint/visitor-keys" "8.21.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.14.0":
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz#01f37c147a735cd78f0ff355e033b9457da1f373"
|
||||
integrity sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==
|
||||
"@typescript-eslint/scope-manager@8.21.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.21.0.tgz#d08d94e2a34b4ccdcc975543c25bb62917437500"
|
||||
integrity sha512-G3IBKz0/0IPfdeGRMbp+4rbjfSSdnGkXsM/pFZA8zM9t9klXDnB/YnKOBQ0GoPmoROa4bCq2NeHgJa5ydsQ4mA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
"@typescript-eslint/visitor-keys" "8.14.0"
|
||||
"@typescript-eslint/types" "8.21.0"
|
||||
"@typescript-eslint/visitor-keys" "8.21.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.16.0":
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905"
|
||||
integrity sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==
|
||||
"@typescript-eslint/type-utils@8.21.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.21.0.tgz#2e69d1a93cdbedc73fe694cd6ae4dfedd00430a0"
|
||||
integrity sha512-95OsL6J2BtzoBxHicoXHxgk3z+9P3BEcQTpBKriqiYzLKnM2DeSqs+sndMKdamU8FosiadQFT3D+BSL9EKnAJQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.16.0"
|
||||
"@typescript-eslint/visitor-keys" "8.16.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz#30b040cb4557804a7e2bcc65cf8fdb630c96546f"
|
||||
integrity sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.18.0"
|
||||
"@typescript-eslint/visitor-keys" "8.18.0"
|
||||
|
||||
"@typescript-eslint/type-utils@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz#6f0d12cf923b6fd95ae4d877708c0adaad93c471"
|
||||
integrity sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.18.0"
|
||||
"@typescript-eslint/utils" "8.18.0"
|
||||
"@typescript-eslint/typescript-estree" "8.21.0"
|
||||
"@typescript-eslint/utils" "8.21.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^1.3.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/types@8.14.0":
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.14.0.tgz#0d33d8d0b08479c424e7d654855fddf2c71e4021"
|
||||
integrity sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==
|
||||
"@typescript-eslint/types@8.21.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.21.0.tgz#58f30aec8db8212fd886835dc5969cdf47cb29f5"
|
||||
integrity sha512-PAL6LUuQwotLW2a8VsySDBwYMm129vFm4tMVlylzdoTybTHaAi0oBp7Ac6LhSrHHOdLM3efH+nAR6hAWoMF89A==
|
||||
|
||||
"@typescript-eslint/types@8.16.0":
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737"
|
||||
integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==
|
||||
|
||||
"@typescript-eslint/types@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.18.0.tgz#3afcd30def8756bc78541268ea819a043221d5f3"
|
||||
integrity sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.14.0":
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz#a7a3a5a53a6c09313e12fb4531d4ff582ee3c312"
|
||||
integrity sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==
|
||||
"@typescript-eslint/typescript-estree@8.21.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.21.0.tgz#5ce71acdbed3b97b959f6168afba5a03c88f69a9"
|
||||
integrity sha512-x+aeKh/AjAArSauz0GiQZsjT8ciadNMHdkUSwBB9Z6PrKc/4knM4g3UfHml6oDJmKC88a6//cdxnO/+P2LkMcg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
"@typescript-eslint/visitor-keys" "8.14.0"
|
||||
"@typescript-eslint/types" "8.21.0"
|
||||
"@typescript-eslint/visitor-keys" "8.21.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
ts-api-utils "^2.0.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.16.0":
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c"
|
||||
integrity sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.16.0"
|
||||
"@typescript-eslint/visitor-keys" "8.16.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz#d8ca785799fbb9c700cdff1a79c046c3e633c7f9"
|
||||
integrity sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.18.0"
|
||||
"@typescript-eslint/visitor-keys" "8.18.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^1.3.0"
|
||||
|
||||
"@typescript-eslint/utils@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.18.0.tgz#48f67205d42b65d895797bb7349d1be5c39a62f7"
|
||||
integrity sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==
|
||||
"@typescript-eslint/utils@8.21.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.21.0.tgz#bc4874fbc30feb3298b926e3b03d94570b3999c5"
|
||||
integrity sha512-xcXBfcq0Kaxgj7dwejMbFyq7IOHgpNMtVuDveK7w3ZGwG9owKzhALVwKpTF2yrZmEwl9SWdetf3fxNzJQaVuxw==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.18.0"
|
||||
"@typescript-eslint/types" "8.18.0"
|
||||
"@typescript-eslint/typescript-estree" "8.18.0"
|
||||
"@typescript-eslint/scope-manager" "8.21.0"
|
||||
"@typescript-eslint/types" "8.21.0"
|
||||
"@typescript-eslint/typescript-estree" "8.21.0"
|
||||
|
||||
"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.14.0.tgz#ac2506875e03aba24e602364e43b2dfa45529dbd"
|
||||
integrity sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==
|
||||
"@typescript-eslint/visitor-keys@8.21.0":
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.21.0.tgz#a89744c4cdc83b5c761eb5878befe6c33d1481b2"
|
||||
integrity sha512-BkLMNpdV6prozk8LlyK/SOoWLmUFi+ZD+pcqti9ILCbVvHGk1ui1g4jJOc2WDLaeExz2qWwojxlPce5PljcT3w==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.14.0"
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
"@typescript-eslint/typescript-estree" "8.14.0"
|
||||
|
||||
"@typescript-eslint/utils@^8.13.0":
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3"
|
||||
integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.16.0"
|
||||
"@typescript-eslint/types" "8.16.0"
|
||||
"@typescript-eslint/typescript-estree" "8.16.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.14.0":
|
||||
version "8.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz#2418d5a54669af9658986ade4e6cfb7767d815ad"
|
||||
integrity sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.16.0":
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705"
|
||||
integrity sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.16.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.18.0":
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz#7b6d33534fa808e33a19951907231ad2ea5c36dd"
|
||||
integrity sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.18.0"
|
||||
"@typescript-eslint/types" "8.21.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -2492,10 +2423,10 @@ chalk@^4.0.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
||||
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
|
||||
chalk@~5.4.1:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8"
|
||||
integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==
|
||||
|
||||
char-regex@^1.0.2:
|
||||
version "1.0.2"
|
||||
@@ -2746,7 +2677,7 @@ data-view-byte-offset@^1.0.0:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.6, debug@~4.3.6:
|
||||
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
|
||||
@@ -2760,7 +2691,7 @@ debug@^3.2.7:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.3.4, debug@^4.3.7:
|
||||
debug@^4.3.4, debug@^4.3.6, debug@^4.3.7, debug@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
@@ -2902,7 +2833,7 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1:
|
||||
enhanced-resolve@^5.15.0:
|
||||
version "5.17.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15"
|
||||
integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==
|
||||
@@ -2910,6 +2841,14 @@ enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.18.0:
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz#91eb1db193896b9801251eeff1c6980278b1e404"
|
||||
integrity sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
entities@^4.4.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
@@ -2992,9 +2931,9 @@ es-errors@^1.2.1, es-errors@^1.3.0:
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-module-lexer@^1.5.3:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78"
|
||||
integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21"
|
||||
integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==
|
||||
|
||||
es-object-atoms@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -3138,16 +3077,16 @@ eslint-plugin-import@^2.26.0:
|
||||
tsconfig-paths "^3.15.0"
|
||||
|
||||
eslint-plugin-jest@^28.0.0:
|
||||
version "28.9.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz#19168dfaed124339cd2252c4c4d1ac3688aeb243"
|
||||
integrity sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==
|
||||
version "28.11.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz#2641ecb4411941bbddb3d7cf8a8ff1163fbb510e"
|
||||
integrity sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
|
||||
eslint-plugin-jsdoc@^50.0.0:
|
||||
version "50.6.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.0.tgz#2c6049a40305313174a30212bc360e775b797a0a"
|
||||
integrity sha512-tCNp4fR79Le3dYTPB0dKEv7yFyvGkUCa+Z3yuTrrNGGOxBlXo9Pn0PEgroOZikUQOGjxoGMVKNjrOHcYEdfszg==
|
||||
version "50.6.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.2.tgz#9c95646d8d85a1b5ed87206695c8f10f2ee35537"
|
||||
integrity sha512-n7GNZ4czMAAbDg7DsDA7PvHo1IPIUwAXYmxTx6j/hTlXbt5V0x5q/kGkiJ7s4wA9SpB/yaiK8jF7CO237lOLew==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.49.0"
|
||||
are-docs-informative "^0.0.2"
|
||||
@@ -3437,16 +3376,16 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-glob@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
|
||||
fast-glob@^3.3.2, fast-glob@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
dependencies:
|
||||
"@nodelib/fs.stat" "^2.0.2"
|
||||
"@nodelib/fs.walk" "^1.2.3"
|
||||
glob-parent "^5.1.2"
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.4"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -3458,10 +3397,10 @@ fast-levenshtein@^2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
|
||||
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
|
||||
fastq@^1.15.0, fastq@^1.6.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.18.0.tgz#d631d7e25faffea81887fe5ea8c9010e1b36fee0"
|
||||
integrity sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
@@ -3620,9 +3559,9 @@ get-caller-file@^2.0.5:
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-east-asian-width@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz#5e6ebd9baee6fb8b7b6bd505221065f0cd91f64e"
|
||||
integrity sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389"
|
||||
integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==
|
||||
|
||||
get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4:
|
||||
version "1.2.4"
|
||||
@@ -4601,10 +4540,10 @@ jest@^29.0.0:
|
||||
import-local "^3.0.2"
|
||||
jest-cli "^29.7.0"
|
||||
|
||||
jiti@^2.4.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.1.tgz#4de9766ccbfa941d9b6390d2b159a4b295a52e6b"
|
||||
integrity sha512-yPBThwecp1wS9DmoA4x4KR2h3QoslacnDR8ypuFM962kI4/456Iy1oHx2RAgh4jfZNdn0bctsdadceiBUgpU1g==
|
||||
jiti@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.4.2.tgz#d19b7732ebb6116b06e2038da74a55366faef560"
|
||||
integrity sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==
|
||||
|
||||
jju@~1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -4738,16 +4677,16 @@ kleur@^3.0.3:
|
||||
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
|
||||
|
||||
knip@^5.0.0:
|
||||
version "5.39.2"
|
||||
resolved "https://registry.yarnpkg.com/knip/-/knip-5.39.2.tgz#1faacd8d8ef36b509b2f6e396cce85b645abb04e"
|
||||
integrity sha512-BuvuWRllLWV/r2G4m9ggNH+DZ6gouP/dhtJPXVlMbWNF++w9/EfrF6k2g7YBKCwjzCC+PXmYtpH8S2t8RjnY4Q==
|
||||
version "5.42.2"
|
||||
resolved "https://registry.yarnpkg.com/knip/-/knip-5.42.2.tgz#7f2c2078440abe7612c166b6aecedb9b690bf34f"
|
||||
integrity sha512-hVtZ6V59COFz3Y0/BHrWMlPAx82EdX/xFHXbutIRSNfJFPMGmIpxLBWTg35F4XQJGwlu5uWiJf8rBYYkmlYWWQ==
|
||||
dependencies:
|
||||
"@nodelib/fs.walk" "1.2.8"
|
||||
"@nodelib/fs.walk" "3.0.1"
|
||||
"@snyk/github-codeowners" "1.1.0"
|
||||
easy-table "1.2.0"
|
||||
enhanced-resolve "^5.17.1"
|
||||
fast-glob "^3.3.2"
|
||||
jiti "^2.4.0"
|
||||
enhanced-resolve "^5.18.0"
|
||||
fast-glob "^3.3.3"
|
||||
jiti "^2.4.2"
|
||||
js-yaml "^4.1.0"
|
||||
minimist "^1.2.8"
|
||||
picocolors "^1.1.0"
|
||||
@@ -4772,10 +4711,10 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
lilconfig@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb"
|
||||
integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==
|
||||
lilconfig@~3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4"
|
||||
integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.2.4"
|
||||
@@ -4790,25 +4729,25 @@ linkify-it@^5.0.0:
|
||||
uc.micro "^2.0.0"
|
||||
|
||||
lint-staged@^15.0.2:
|
||||
version "15.2.10"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.10.tgz#92ac222f802ba911897dcf23671da5bb80643cd2"
|
||||
integrity sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.4.1.tgz#b34e3297ae13fdb2d99b3456e2dbd8e20798bced"
|
||||
integrity sha512-P8yJuVRyLrm5KxCtFx+gjI5Bil+wO7wnTl7C3bXhvtTaAFGirzeB24++D0wGoUwxrUKecNiehemgCob9YL39NA==
|
||||
dependencies:
|
||||
chalk "~5.3.0"
|
||||
chalk "~5.4.1"
|
||||
commander "~12.1.0"
|
||||
debug "~4.3.6"
|
||||
debug "~4.4.0"
|
||||
execa "~8.0.1"
|
||||
lilconfig "~3.1.2"
|
||||
listr2 "~8.2.4"
|
||||
lilconfig "~3.1.3"
|
||||
listr2 "~8.2.5"
|
||||
micromatch "~4.0.8"
|
||||
pidtree "~0.6.0"
|
||||
string-argv "~0.3.2"
|
||||
yaml "~2.5.0"
|
||||
yaml "~2.6.1"
|
||||
|
||||
listr2@~8.2.4:
|
||||
version "8.2.4"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.4.tgz#486b51cbdb41889108cb7e2c90eeb44519f5a77f"
|
||||
integrity sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==
|
||||
listr2@~8.2.5:
|
||||
version "8.2.5"
|
||||
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.2.5.tgz#5c9db996e1afeb05db0448196d3d5f64fec2593d"
|
||||
integrity sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==
|
||||
dependencies:
|
||||
cli-truncate "^4.0.0"
|
||||
colorette "^2.0.20"
|
||||
@@ -4958,7 +4897,7 @@ merge2@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
micromatch@^4.0.4, micromatch@~4.0.8:
|
||||
micromatch@^4.0.4, micromatch@^4.0.8, micromatch@~4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
|
||||
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
|
||||
@@ -5656,7 +5595,7 @@ rimraf@^6.0.0:
|
||||
glob "^11.0.0"
|
||||
package-json-from-dist "^1.0.0"
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
run-parallel@^1.1.9, run-parallel@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
|
||||
@@ -5862,9 +5801,9 @@ spdx-expression-parse@^4.0.0:
|
||||
spdx-license-ids "^3.0.0"
|
||||
|
||||
spdx-license-ids@^3.0.0:
|
||||
version "3.0.20"
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89"
|
||||
integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==
|
||||
version "3.0.21"
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz#6d6e980c9df2b6fc905343a3b2d702a6239536c3"
|
||||
integrity sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
@@ -6127,10 +6066,10 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
ts-api-utils@^1.3.0:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
|
||||
integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
|
||||
ts-api-utils@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.0.0.tgz#b9d7d5f7ec9f736f4d0f09758b8607979044a900"
|
||||
integrity sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==
|
||||
|
||||
ts-node@^10.9.2:
|
||||
version "10.9.2"
|
||||
@@ -6248,14 +6187,14 @@ typed-array-length@^1.0.6:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
typedoc-plugin-coverage@^3.0.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-3.4.0.tgz#ca0f4a8cb7d9e2718995520a39e16fdbeb1b22ca"
|
||||
integrity sha512-I8fLeQEERncGn4sUlGZ+B1ehx4L7VRwqa3i6AP+PFfvZK0ToXBGkh9sK7xs8l8FLPXq7Cv0yVy4YCEGgWNzDBw==
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-coverage/-/typedoc-plugin-coverage-3.4.1.tgz#13b445cecb674845945e218c4560bbd91299af83"
|
||||
integrity sha512-V23DAwinAMpGMGcL1R1s8Snr26hrjfIdwGf+4jR/65ZdmeAN+yRX0onfx5JlembTQhR6AePQ/parfQNS0AZ64A==
|
||||
|
||||
typedoc-plugin-mdn-links@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.3.tgz#30d22be00bc7689a98c0b223b6a487ff6f338ec7"
|
||||
integrity sha512-q18V8nXF4MqMBGABPVodfxmU2VLK+C7RpyKgrEGP1oP3MAdavLM8Hmeh7zUJAZ4ky+zotO5ZXfhgChegmaDWug==
|
||||
version "4.0.9"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.9.tgz#5f91000d11e2fd0c6271fc05e76e08813184812a"
|
||||
integrity sha512-udtolnpREnT+SB3xZnWHIyN2KtzjlohBOtOOYw+ofKMZKTCaYzHElSyaP2oS/7w/cZtFvrXr/m+35458XUcSLQ==
|
||||
|
||||
typedoc-plugin-missing-exports@^3.0.0:
|
||||
version "3.1.0"
|
||||
@@ -6263,9 +6202,9 @@ typedoc-plugin-missing-exports@^3.0.0:
|
||||
integrity sha512-Sogbaj+qDa21NjB3SlIw4JXSwmcl/WOjwiPNaVEcPhpNG/MiRTtpwV81cT7h1cbu9StpONFPbddYWR0KV/fTWA==
|
||||
|
||||
typedoc@^0.27.0:
|
||||
version "0.27.3"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.3.tgz#0fad232181ce0ac7eda27fe78e56a4b863e1fe59"
|
||||
integrity sha512-oWT7zDS5oIaxYL5yOikBX4cL99CpNAZn6mI24JZQxsYuIHbtguSSwJ7zThuzNNwSE0wqhlfTSd99HgqKu2aQXQ==
|
||||
version "0.27.6"
|
||||
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.27.6.tgz#7e8d067bd5386b7908afcb12c9054a83e8bb326b"
|
||||
integrity sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==
|
||||
dependencies:
|
||||
"@gerrit0/mini-shiki" "^1.24.0"
|
||||
lunr "^2.3.9"
|
||||
@@ -6274,9 +6213,9 @@ typedoc@^0.27.0:
|
||||
yaml "^2.6.1"
|
||||
|
||||
typescript@^5.4.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
version "5.7.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
|
||||
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
|
||||
|
||||
uc.micro@^2.0.0, uc.micro@^2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -6355,9 +6294,9 @@ url-parse@^1.5.3:
|
||||
requires-port "^1.0.0"
|
||||
|
||||
uuid@11:
|
||||
version "11.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.3.tgz#248451cac9d1a4a4128033e765d137e2b2c49a3d"
|
||||
integrity sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==
|
||||
version "11.0.5"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.0.5.tgz#07b46bdfa6310c92c3fb3953a8720f170427fc62"
|
||||
integrity sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==
|
||||
|
||||
uuid@8.3.2:
|
||||
version "8.3.2"
|
||||
@@ -6584,15 +6523,15 @@ yallist@^3.0.2:
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
||||
yaml@^2.6.1:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"
|
||||
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
|
||||
|
||||
yaml@~2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.6.1.tgz#42f2b1ba89203f374609572d5349fb8686500773"
|
||||
integrity sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==
|
||||
|
||||
yaml@~2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130"
|
||||
integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==
|
||||
|
||||
yargs-parser@^21.1.1:
|
||||
version "21.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
|
||||
@@ -6627,6 +6566,6 @@ zod-validation-error@^3.0.3:
|
||||
integrity sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==
|
||||
|
||||
zod@^3.22.4:
|
||||
version "3.24.0"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.0.tgz#babb32313f7c5f4a99812feee806d186b4f76bde"
|
||||
integrity sha512-Hz+wiY8yD0VLA2k/+nsg2Abez674dDGTai33SwNvMPuf9uIrBC9eFgIMQxBBbHFxVXi8W+5nX9DcAh9YNSQm/w==
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||
|
||||
Reference in New Issue
Block a user