Compare commits
32 Commits
v36.0.0-rc.0
...
v36.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e54b7aab4 | |||
| e4590cac94 | |||
| 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@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
@@ -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@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
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@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -1,3 +1,41 @@
|
||||
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.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "36.0.0-rc.0",
|
||||
"version": "36.1.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+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 {
|
||||
|
||||
+42
-20
@@ -160,7 +160,7 @@ import {
|
||||
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";
|
||||
@@ -1346,7 +1346,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 };
|
||||
@@ -7348,10 +7348,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 +7998,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
* @returns A new client secret
|
||||
*/
|
||||
public generateClientSecret(): string {
|
||||
return randomString(32);
|
||||
return secureRandomString(32);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -8248,27 +8246,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 +8317,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 +8328,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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Events emitted by the {@link CryptoApi}
|
||||
* Cryptography-related events emitted by the {@link matrix.MatrixClient}.
|
||||
*/
|
||||
export enum CryptoEvent {
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +17,7 @@ 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,
|
||||
@@ -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
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+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("");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -79,7 +79,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";
|
||||
@@ -956,7 +956,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 +1099,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,
|
||||
|
||||
+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);
|
||||
|
||||
+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[] {
|
||||
|
||||
@@ -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"
|
||||
@@ -1582,26 +1582,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 +1637,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.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz#e341beb4e4315084d8be20bceeeda7d8a46f079f"
|
||||
integrity sha512-fubZKIHSPuo07FgRTn6S4Nl0uXPRPYVNpyZzIDGfp7Fny6JjNus6kReLD7NI380JXi4HtUTSOZ34LBuNPO1XLQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.13.0"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
@@ -1803,9 +1803,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.70"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.70.tgz#5a77508f5568d16fcd3b711c8102d7a430a04df7"
|
||||
integrity sha512-RE+K0+KZoEpDUbGGctnGdkrLFwi1eYKTlIHNl2Um98mUkGsm1u2Ff6Ltd0e8DktTtC98uy7rSj+hO8t/QuLoVQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -1857,174 +1857,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.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz#5f26c0a833b27bcb1aa402b82e76d3b8dda0b247"
|
||||
integrity sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==
|
||||
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.19.1"
|
||||
"@typescript-eslint/type-utils" "8.19.1"
|
||||
"@typescript-eslint/utils" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
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.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.19.1.tgz#b836fcfe7a704c8c65f5a50e5b0ff8acfca5c21b"
|
||||
integrity sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==
|
||||
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.19.1"
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/typescript-estree" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
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.19.1":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz#794cfc8add4f373b9cd6fa32e367e7565a0e231b"
|
||||
integrity sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
"@typescript-eslint/visitor-keys" "8.14.0"
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
|
||||
"@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.19.1":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz#23710ab52643c19f74601b3f4a076c98f4e159aa"
|
||||
integrity sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==
|
||||
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.19.1"
|
||||
"@typescript-eslint/utils" "8.19.1"
|
||||
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.19.1":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.19.1.tgz#015a991281754ed986f2e549263a1188d6ed0a8c"
|
||||
integrity sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==
|
||||
|
||||
"@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.19.1":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz#c1094bb00bc251ac76cf215569ca27236435036b"
|
||||
integrity sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.14.0"
|
||||
"@typescript-eslint/visitor-keys" "8.14.0"
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/visitor-keys" "8.19.1"
|
||||
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.19.1", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.13.0":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.19.1.tgz#dd8eabd46b92bf61e573286e1c0ba6bd243a185b"
|
||||
integrity sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==
|
||||
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.19.1"
|
||||
"@typescript-eslint/types" "8.19.1"
|
||||
"@typescript-eslint/typescript-estree" "8.19.1"
|
||||
|
||||
"@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.19.1":
|
||||
version "8.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz#fce54d7cfa5351a92387d6c0c5be598caee072e0"
|
||||
integrity sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==
|
||||
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.19.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@ungap/structured-clone@^1.2.0":
|
||||
@@ -2492,10 +2402,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 +2656,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 +2670,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 +2812,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 +2820,14 @@ enhanced-resolve@^5.15.0, enhanced-resolve@^5.17.1:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.17.1:
|
||||
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 +2910,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 +3056,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.10.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz#4b35b8abb0f7cfe699bff8d9060270a2ddd770ea"
|
||||
integrity sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==
|
||||
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.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.6.1.tgz#791a668fd4b0700a759e9a16a741a6a805f5b95c"
|
||||
integrity sha512-UWyaYi6iURdSfdVVqvfOs2vdCVz0J40O/z/HTsv2sFjdjmdlUI/qlKLOTmwbPQ2tAfQnE5F9vqx+B+poF71DBQ==
|
||||
dependencies:
|
||||
"@es-joy/jsdoccomment" "~0.49.0"
|
||||
are-docs-informative "^0.0.2"
|
||||
@@ -3438,15 +3356,15 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
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==
|
||||
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"
|
||||
@@ -3459,9 +3377,9 @@ fast-levenshtein@^2.0.6:
|
||||
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==
|
||||
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 +3538,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"
|
||||
@@ -4602,9 +4520,9 @@ jest@^29.0.0:
|
||||
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==
|
||||
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,9 +4656,9 @@ 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.41.1"
|
||||
resolved "https://registry.yarnpkg.com/knip/-/knip-5.41.1.tgz#b6e27186d38e6bccd2ef8346294e78d13322f1cd"
|
||||
integrity sha512-yNpCCe2REU7U3VRvMASnXSEtfEC2HmOoDW9Vp9teQ9FktJYnuagvSZD3xWq8Ru7sPABkmvbC5TVWuMzIaeADNA==
|
||||
dependencies:
|
||||
"@nodelib/fs.walk" "1.2.8"
|
||||
"@snyk/github-codeowners" "1.1.0"
|
||||
@@ -4772,10 +4690,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 +4708,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.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.3.0.tgz#32a0b3f2f2b8825950bd3b9fb093e045353bdfa3"
|
||||
integrity sha512-vHFahytLoF2enJklgtOtCtIjZrKD/LoxlaUusd5nh7dWv/dkKQJY74ndFSzxCdv7g0ueGg1ORgTSt4Y9LPZn9A==
|
||||
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 +4876,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==
|
||||
@@ -6127,10 +6045,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 +6166,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.7"
|
||||
resolved "https://registry.yarnpkg.com/typedoc-plugin-mdn-links/-/typedoc-plugin-mdn-links-4.0.7.tgz#0949bcca5bf615c4dc35abe653ebc30eade61980"
|
||||
integrity sha512-S9nUdZShdu+8HyWygmxjqyYIviZVdL36OjPWvxuLVMrS21lqxnVYLqInPYHXalmvitVEqWmrJJk8Al0x6d8wEA==
|
||||
|
||||
typedoc-plugin-missing-exports@^3.0.0:
|
||||
version "3.1.0"
|
||||
@@ -6263,9 +6181,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"
|
||||
@@ -6584,15 +6502,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 +6545,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